Нейронные сети в Python: От Sklearn до PyTorch и вероятностных нейронных сетей
В этом материале мы рассмотрим различные концепции, связанные с реализацией нейронных сетей при помощи Sklearn и PyTorch. Многие нейронные сети давно превосходят человеческие возможности в различных задачах обработки изображений и естественного языка. Например, нейронная сеть, обученная на хорошо известной наборе изображений ImageNet, определяет разницу между различными породами собак, ошибаясь в 4,58% случаев. А средний человек ошибается в среднем в 5% случаях.
Для начала посмотрим, как легко обучать многослойные перцептроны в Sklearn на примере распространённого набора данных MNIST с рукописными цифрами. Затем мы перейдем к PyTorch, где процесс обучения станет немного сложнее. Сначала мы создадим сеть с четырьмя слоями (более глубокую, чем та, которую будем использовать в Scikit-learn) для работы с тем же набором данных. После этого мы кратко рассмотрим байесовские (вероятностные) нейронные сети. Если вы совсем не знакомы с Python и основами нейронок — может быть непросто.
Введение
- Scikit-learn — это открытая библиотека машинного обучения для Python, которая значительно упрощает процесс создания классических моделей машинного обучения.
- PyTorch — так же открытая библиотека машинного обучения, основанная на Torch и предназначенная для реализации нейронных сетей. Многие люди предпочитают использовать PyTorch вместо TensorFlow. Главная причина в том, что PyTorch позволяет создавать динамически вычисляемые графы. Это означает, что вы можете изменять архитектуру сети прямо во время выполнения, что очень полезно для некоторых типов архитектур. Кроме того, PyTorch очень легко освоить, и построение моделей с его помощью интуитивно понятно, как мы увидим позже.
И машинное обучение тесно связано с вероятностями! Вы слышали о вероятностном программировании? Это парадигма, которая позволяет легко создавать вероятностные модели и интерпретировать их. Языки, такие как Pyro, значительно упрощают задачу написания кода на основе вероятностных подходов. И хотя мы не будем углубляться в вероятностное программирование в этой статье, важно отметить, что вероятностные методы сами по себе играют важную роль в машинном обучении. Сеть, которую мы собираемся построить, не будет написана в рамках вероятностного программирования, но она всё равно будет использовать вероятностные подходы!
Итак, начинаем с Sklearn, затем щупаем PyTorch, а потом подмешиваем нотки вероятностных моделей.
Многослойный персептрон в Sklearn для классификации рукописных цифр
Мы будем использовать набор данных MNIST, который по-прежнему остается одним из самых популярных в учебных задачах компьютерного зрения. Наша цель — определить, какая цифра написана от руки на изображении.
Это можно было бы сделать при помощи свёрточных нейронных сетей, которые являются самым распространённым подходом для обнаружения пространственных паттернов. Но для упрощения мы просто преобразуем изображение в вектор (на освное изображений размером 28x28 пикселей получим вектора размером 784, где каждый признак представляет пиксель) и используем простую полносвязную нейронную сеть.
Наша цель — создать математическую функцию, которая сможет предсказать интересующую нас цифру на основе пикселей. Именно здесь нейронные сети становятся особенно полезными, так как они представляют собой универсальные аппроксиматоры, способные приблизить любую функцию при достаточном количестве степеней свободы.
Нейронные сети реализуют не только линейные функции, но и нелинейные преобразования, известные как функции активации (например, ReLU).
Давайте посмотрим, насколько легко это можно сделать в Sklearn. Мы построим как простой линейный перцептрон, так и многослойный перцептрон с функциями активации ReLU, которые по умолчанию используются в Sklearn. Не забудем сравнить точность обеих моделей и поэкспериментировать с гиперпараметрами и архитектурой сети!
import numpy as np from sklearn.datasets import fetch_openml from sklearn.neural_network import MLPClassifier from sklearn.linear_model import Perceptron from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score # Загрузка и разделение датасета MNIST тренировочную и тестовую выборки X, y = fetch_openml('mnist_784', version = 1, return_X_y = True) X_train, X_test, y_train, y_test = train_test_split(X/255., y, test_size = 0.20, random_state = 1) # Сначала давайте используем очень простой персептрон; # Мы указываем гиперпараметры и обучаем персептрон, не используя # функции активации. Это означает, что модель по сути линейна. per = Perceptron(random_state = 1, max_iter = 30, tol = 0.001) per.fit(X_train, y_train) # Делаем прогнозы с помощью построенного персептрона yhat_train_per = per.predict(X_train) yhat_test_per = per.predict(X_test) print(f"Перцептрон: точность на трейне: {accuracy_score(y_train, yhat_train_per))}") print(f"Перцептрон: точность на тесте : {accuracy_score(y_test, yhat_test_per))}") # Теперь давайте попробуем многослойный персептрон # ReLU - функция активации по умолчанию mlp = MLPClassifier(max_iter = 50, alpha = 1e-4, solver = 'sgd', verbose = 10, random_state = 1, learning_rate_init = .1, hidden_layer_sizes = (784, 100, 2)) mlp.fit(X_train, y_train) # Прогноз с нашим новым классификатором yhat_train_mlp = mlp.predict(X_train) yhat_test_mlp = mlp.predict(X_test) print(f"Многослойный персептрон: точность на трейне: {accuracy_score(y_train, yhat_train_mlp))}") print(f"Многослойный персептрон: точность на тесте : {accuracy_score(y_test, yhat_test_mlp))}")
Перцептрон: точность на тренировочных данных: 0.90 Перцептрон: точность на тестовых данных: 0.88 Iteration 1, loss = 0.78419346 Iteration 2, loss = 0.40267290 Iteration 3, loss = 0.35031849 Iteration 4, loss = 0.20264795 Iteration 5, loss = 0.16538864 Iteration 6, loss = 0.13211615 Iteration 7, loss = 0.11156361 Iteration 8, loss = 0.09866746 Iteration 9, loss = 0.09287331 Iteration 10, loss = 0.08113442 Iteration 11, loss = 0.07667319 Iteration 12, loss = 0.06786269 Iteration 13, loss = 0.06098940 Iteration 14, loss = 0.05970302 Iteration 15, loss = 0.04826575 Iteration 16, loss = 0.05097064 Iteration 17, loss = 0.04555298 Iteration 18, loss = 0.04354965 Iteration 19, loss = 0.02969784 Iteration 20, loss = 0.03825500 Iteration 21, loss = 0.03304720 Iteration 22, loss = 0.02669491 Iteration 23, loss = 0.02801079 Iteration 24, loss = 0.02894119 Iteration 25, loss = 0.02782336 Iteration 26, loss = 0.03339268 Iteration 27, loss = 0.03719307 Iteration 28, loss = 0.05146029 Iteration 29, loss = 0.02408222 Iteration 30, loss = 0.27404516 Iteration 31, loss = 0.11683277 Iteration 32, loss = 0.06812019 Iteration 33, loss = 0.04645325 Iteration 34, loss = 0.03130887 Iteration 35, loss = 0.02770269 Iteration 36, loss = 0.04352652 Iteration 37, loss = 0.03645387 Iteration 38, loss = 0.02776002 Iteration 39, loss = 0.03372111 Iteration 40, loss = 0.03562547 Training loss did not improve more than tol=0.000100 for 10 consecutive epochs. Stopping. Многослойный персептрон: точность на тренировочных данных: 1.00 Многослойный персептрон: точность на тестовых данных: 0.97
Стандартная нейронная сеть в PyTorch
Модуль Torch предоставляет все необходимые операции с тензорами, которые нам понадобятся для построения первой нейронной сети в PyTorch. И да, в PyTorch всё является тензорами.
Мы будем использовать архитектуру, похожую на ту, что мы использовали ранее с Sklearn, но более глубокую. Это означает, что потребуется обучить гораздо больше параметров. Мы могли бы создать точно такую же модель в Sklearn, но обучение займет больше времени, поэтому в PyTorch мы будем использовать графический процессор для ускорения этого процесса. PyTorch также был бы полезен для обучения более сложных архитектур, таких как сверточные нейронные сети, которые очень хорошо подходят для обработки данных компьютерного зрения.
Нам понадобятся библиотеки torch и torchvision, так что не забудьте установить их на свою машину!
# Делаем необходимые импорты import torch import torchvision import torch.nn.functional as F from torch.utils.data.dataloader import DataLoader from torch import nn
Затем мы определим несколько глобальных переменных:
# Задаём размер батча в 100 объектов, # чтобы уместить их в памяти BATCH_SIZE = 100 # Обучение будет длиться 10 эпох TRAIN_EPOCHS = 10 # Количество классов в датасете CLASSES = 10 # Размер изображений в пикселях, в данном # случае 28x28 пикселей INPUT_HEIGHT = 28 INPUT_WIDTH = 28 # Общее количество признаков (пикселей) = 28*28 TOTAL_INPUT = 784 # Размеры тренировочной и тестовой выборок TRAIN_SIZE = 50000 TEST_SIZE = 10000
Определим нашу модель в виде класса, который будет наследоваться от nn.Module
— базового класса для всех модулей нейронных сетей в Torch. Этот класс мы назовём FCN, что означает Fully Connected Network, или полносвязная сеть.
class FCN(nn.Module): # Следующий шаг - инициализации, которые # будут выполнены при создании экземлпяра # нашейней ронной сети. def __init__(self, n_hidden = 600, n_classes = CLASSES): super().__init__() # У нашей нейронной сети будет четыре слоя (l1 - l4). # Количество нейронов в скрытых слоях - n_hidden. self.l1 = nn.Linear(INPUT_HEIGHT * INPUT_WIDTH, n_hidden) self.l2 = nn.Linear(n_hidden, n_hidden) self.l3 = nn.Linear(n_hidden, n_hidden) self.l4 = nn.Linear(n_hidden, n_classes) # Данная функция - это то место, где происходит магия. # Здесь данные поступают и скармливаются вычислительному # графу для всех нужных расчетов def forward(self, x): # Реализация прямого прохода нейронной сети для всех вычислений x = x.view(-1, INPUT_HEIGHT * INPUT_WIDTH) x = F.relu(self.l1(x)) x = F.relu(self.l2(x)) x = F.relu(self.l3(x)) x = F.log_softmax(self.l4(x), dim = 1) return x
Определим две функции: для обучения и тестирования нейросети:
def train_fcn(net, optimizer, epoch, train_loader): # Обучает нейросеть по одному батчу за раз net.train() total_loss = 0.0 total = 0.0 correct = 0.0 for images, labels in train_loader: images, labels = images.to(DEVICE), labels.to(DEVICE) # Обнуляет градиенты всех параметров модели net.zero_grad() # Делает прогнозы (прямой проход) pred = net.forward(images.cuda().view(-1, 784)) # Рассчитываем функцию потерь и точность для каждого класса loss = F.binary_cross_entropy_with_logits(pred, F.one_hot(labels.cuda(), CLASSES).float()) total_loss += loss total += labels.size(0) correct += (pred.argmax(-1) == labels.cuda()).sum().item() # Обратный проход loss.backward() # Оптимизируем параметры нейросети optimizer.step() # Выводим результаты по эпохе print(f"Эпоха {epoch}: loss {total_loss:.5f} accuracy {correct / total * 100:.5f}")
И вот мы обучаем и оцениваем нейросеть!
DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu") # Преобразуем данные в тензор и нормализуем их tr = torchvision.transforms.Compose([ torchvision.transforms.ToTensor(), torchvision.transforms.Normalize((0.5,), (0.5,))]) # Загружаем данные mnist = torchvision.datasets.mnist.MNIST( root = 'images', transform = tr, download = True) # Разделяем данные на трейн и тест train_set, test_set = torch.utils.data.random_split( mnist, lengths = (TRAIN_SIZE, TEST_SIZE)) # Создаём даталоадеры train_loader = DataLoader(train_set, batch_size = BATCH_SIZE, shuffle = True) test_loader = DataLoader(test_set, batch_size = BATCH_SIZE, shuffle = True) # Создаем экземпляр нашей нейронной сети fcn = FCN().to(DEVICE) # Устанавливаем оптимизатор (стохастический градиентный спуск) # с гиперпараметрами optim = torch.optim.SGD(fcn.parameters(recurse = True), lr = 0.1, momentum = 0.95) print("Обучение...") # Обучаем и тестируем! for epoch in range(TRAIN_EPOCHS): train_fcn(fcn, optim, epoch, train_loader) test_fcn(fcn, test_loader)
Training... Эпоха 0: loss 63.53376 accuracy 83.78600 Эпоха 1: loss 43.67813 accuracy 94.97800 Эпоха 2: loss 41.00309 accuracy 96.47600 Эпоха 3: loss 39.71790 accuracy 97.10400 Эпоха 4: loss 38.77234 accuracy 97.63000 Эпоха 5: loss 38.07506 accuracy 98.06800 Эпоха 6: loss 37.79471 accuracy 98.17000 Эпоха 7: loss 37.04946 accuracy 98.61000 Эпоха 8: loss 36.80310 accuracy 98.72800 Эпоха 9: loss 36.77231 accuracy 98.74800
Подведём итог. Мы определили класс с архитектурой нашей нейронной сети, функциями для обучения и тестирования, а также основную часть кода: мы загрузили данные, разделили их на выборки, выполнили предварительную обработку, установили оптимизатор и гиперпараметры, а затем обучили и оценили модель. После 10 эпох мы достигли высокой точности на тестовых данных. Мы использовали сеть, состоящую из 4 слоёв с функциями активации ReLU, что, кажется, очень хорошо подходит для нашего набора данных. Тем не менее, эти и другие гиперпараметры, такие как скорость обучения, требуют оптимизации, чтобы найти наилучшую конфигурацию для каждой конкретной задачи.
Попробуйте поэкспериментировать с этими гиперпараметрами и сравнить точность сети, например, с 3 и 5 слоями.
Теперь давайте перейдем к вероятностной версии.
Что такое вероятностная нейронная сеть?
Байесовские нейронные сети (BNNs) используют правило Байеса для создания вероятностной нейронной сети. Эти сети можно описать как прямые связи, которые включают в свои параметры понятия неопределенности.
Давайте на секунду вернёмся к уравнению прямой: y = W * X + b
, где X — наши признаки, в y
— целевая величина. Обучаемыми параметрами являются W
и b
, и они обычно оптимизируются с помощью функции оценки максимального правдоподобия. Однако вместо того, чтобы иметь только один скалярный параметр для b
(или любого из элементов в W
), мы можем изучить распределение (нормальное, Лапласовское и т.д.). В случае нормального распределения мы затем определим каждый параметр со средним значением и стандартным отклонением. То есть BNN изучает распределение для каждого параметра сети.
Это полезно по нескольким причинам:
- Во-первых, это позволяет нашей сети выдавать результаты с неопределённостью или даже говорить «Я не знаю». Вы можете спросить, почему это так важно? Представьте, что у вас есть система, которая может отличить собак от кошек (какое клише!). Как вы думаете, что произойдет, если вы ей передадите, скажем, свою фотографию? Она присвоит ей класс собаки или кошки, но на практике это не всегда полезно. Итак, первое преимущество вероятностной нейронной сети — это возможность сказать: «На самом деле, я не уверен, к какому классу относится этот объект».
- Второе замечательное преимущество BNN заключается в их простой оптимизации. Например, если после обучения нашей сети у нас есть вес, среднее значение которого близко к нулю, и мы очень уверены в этом (то есть неопределённость очень мала), мы можем удалить связанный с ним нейрон, что хорошо с точки зрения оптимизации.
- Также она регуляризирует веса, улучшая скорость сопоставимо с техникой Drop Out.
Однако не всё так идеально. BNN, как правило, работают медленнее, чем их не вероятностные аналоги при классификации новых объектов, и им требуется больше места в памяти для хранения модели.
Теперь, когда мы понимаем, как работают BNN, станет ли создание такой сети сложнее, чем в нашем предыдущем примере? Немного, но давайте сначала проясним несколько понятий:
- Первое, что нужно понять о BNN: Чтобы оценить сеть на конкретном объекте, нам необходимо выполнить выборку из распределения параметров (или использовать ожидаемое значение в противном случае). На практике эти сети функционируют как ансамбль, выдавая несколько результатов для одного объекта. Это происходит из-за того, что мы многократно делаем выборку из распределения параметров. Затем эти выходные данные можно усреднить для получения окончательного прогноза и представления о неопределенности в предсказании.
- Вторая концепция заключается в том, что, как указывает слово «байесовский», мы накладываем априорное значение на параметры сети. Обычно мы определяем распределение (например, нормальное) и инициализируем параметры с помощью предшествующего. В этом случае мы используем разработанную смесь гауссиан для предыдущих параметров.
Это руководство даст только общее представление о том, как работают BNN. Мы не будем углубляться в вариационный вывод, который является подходом, используемым этими сетями для аппроксимации распределения изученных параметров. На этом этапе нам просто нужно знать, что вариационный вывод является одним из наиболее распространённых подходов (наряду с сэмплированием) для аппроксимации апостериорного распределения (возможно, вы уже слышали о вариационных автокодировщиках).
Чтобы это реализовать, нам необходимо определить:
- Архитектуру: Количество слоев и определение байесовского слоя.
- Функцию потерь: Как учитывать ошибки неправильной классификации и использовать их во время обучения.
- Функции обучения и тестирования.
# Добавляем нужные импорты import math import numpy as np # Количество выборок из распределения параметров SAMPLES = 10 PI = 0.5 SIGMA1 = torch.cuda.FloatTensor([math.exp(-0)]) SIGMA2 = torch.cuda.FloatTensor([math.exp(-6)]) NUM_BATCHES = len(train_loader)
Наша байесовская нейронная сеть будет похожа на предыдущую нейросеть FCN:
class BNN(nn.Module): # Как можно заметить, всё почти то же самое - мы просто # заменили nn.Linear на BayesianLinear, # являющийся типом слоя, который мы определим позже def __init__(self, n_hidden = 600, n_classes = CLASSES): super().__init__() self.l1 = BayesianLinear(INPUT_HEIGHT * INPUT_WIDTH, n_hidden) self.l2 = BayesianLinear(n_hidden, n_hidden) self.l3 = BayesianLinear(n_hidden, n_hidden) self.l4 = BayesianLinear(n_hidden, n_classes) # Функция прямого прохода также очень-очень похожа, # за исключением атрибута sample. Этот атрибут необходим, потому # что у нас есть распределение параметров и каждый раз, когда # мы делаем прямой проход по нейронной сети, нам нужно брать сэмпл # распределения параметров. Если мы не делаем этого, то просто # берём ожидаемое значение (среднее). def forward(self, x, sample = False): x = x.view(-1, INPUT_HEIGHT * INPUT_WIDTH) x = F.relu(self.l1(x, sample)) x = F.relu(self.l2(x, sample)) x = F.relu(self.l3(x, sample)) x = F.log_softmax(self.l4(x, sample), dim = 1) return x # Также нам понадобится несколько дополнительных функций. # Они реализуют уравнения, необходимые для вариационного # вывода, который мы используем для аппроксимации # последующих параметров. Следующие 2 функции определяются # для каждого слоя, а результатом является сумма слоёв. def log_prior(self): return self.l1.log_prior + self.l2.log_prior + self.l3.log_prior + self.l4.log_prior def log_variational_post(self): return self.l1.log_variational_post + self.l2.log_variational_post + self.l3.log_variational_post + self.l4.log_variational_post # Нижняя вариационная граница (ELBO). Эта функция # реализует вычисление функции потерь, используемой # для изучения распределения по параметрам нейросети. # Уравнение получено с помощью вариационного вывода, # которого, как мы уже сказали, особо касаться не будем. def elbo(self, input, target, samples = SAMPLES, batch_size = BATCH_SIZE): outputs = torch.zeros(samples, batch_size, CLASSES) log_priors = torch.zeros(samples) log_variational_post = torch.zeros(samples) # Как можно заметить, мы несколько раз берем сэмплы и # получаем несколько прогнозов на одних и тех же # данных for i in range(samples): outputs[i] = self(input, sample = True) log_priors[i] = self.log_prior() log_variational_post = self.log_variational_post() # Средние log-prior и log-posterior log_prior = log_priors.mean() log_variational_post = log_variational_post.mean() # Мы берём среднее пресказание, используя # outputs.mean(0) # Рассчитываем Negative Log Likelihood loss nll = F.nll_loss(outputs.mean(0), target, reduction = 'sum') # Рассчитываем дивергенцию Кульбака-Лейблера kl = (log_variational_post - log_prior) / NUM_BATCHES # Это функция потерь ELBO return kl + nll
Теперь надо определить слой BayesianLinear
:
class BayesianLinear(nn.Module): def __init__(self, dim_in, dim_out): super(BayesianLinear, self).__init__() self.dim_in = dim_in self.dim_out = dim_out # Поскольку мы предполагаем Гауссово распределение # параметров, нам нужны два параметра (mean и std), # но лучше использовать rho, который является преобразованием # сигмы (std). Таким образом, инициализируем средние # значения и rho. self.w_mu = nn.Parameter((-0.2 - 0.2) * torch.rand(dim_in, dim_out) + 0.2) self.w_rho = nn.Parameter((-5. + 4.) * torch.rand(dim_in, dim_out) - 4.) # Создаём Гауссово распределение для w self.w = Gaussian(self.w_mu, self.w_rho) # То же самое применимо и к свободным членам self.b_mu = nn.Parameter((-0.2 - 0.2) * torch.rand(dim_in, dim_out) + 0.2) self.b_rho = nn.Parameter((-5. + 4.) * torch.rand(dim_in, dim_out) - 4.) self.b = Gaussian(self.w_mu, self.w_rho) # Распределение определено, # как сконструированная смесь двух гауссиан, где PI # является параметром для смеси self.w_prior = ScaledGaussianMixture(PI, SIGMA1, SIGMA2) self.b_prior = ScaledGaussianMixture(PI, SIGMA1, SIGMA2) self.log_prior = 0 self.log_variational_post = 0 def forward(self, input, sample = False, calc_log_prob = False): if self.training or sample: w = self.w.sample() b = self.b.sample() else: w = self.w.mu b = self.b.mu if self.training or calc_log_prob: # Подсчёт logprob предварительных значений выбранных весов self.log_prior = self.w_prior.log_prob(w) + self.b_prior.log_prob(b) # Подсчёт logprob последюущих (w, b) распределений self.log_variational_post = self.w.log_prob(w) + self.b.log_prob(b) else: self.log_prior, self.log_variational_post = 0, 0 return F.linear(input, w, b)
Теперь определим тип объекта, на котором базируется наша функция BayesianLinear
:
class Gaussian: def __init__(self, mu, rho): self.mu = mu self.rho = rho self.normal = torch.distributions.Normal(0, 1) @property def sigma(self): # log1p <- ln(1 + input) return torch.log1p(torch.exp(self.rho)) # Реализуем выборку из нормального распределения def sample(self): epsilon = self.normal.sample(self.mu.size()) return self.mu + self.sigma * epsilon def log_prob(self, input): return(-math.log(math.sqrt(2 * math.pi)) - torch.log(self.sigma) - ((input - self.mu) ** 2) / (2 * self.sigma ** 2)).sum() class ScaledGaussianMixture: def __init__(self, pi, sigma1, sigma2): self.pi = pi self.sigma1 = sigma1 self.sigma2 = sigma2 self.gaussian1 = torch.distributions.Normal(0, sigma1) self.gaussian2 = torch.distributions.Normal(0, sigma2) def log_prob(self, input): prob1 = torch.exp(self.gaussian1.log_prob(input)) prob2 = torch.exp(self.gaussian2.log_prob(input)) return (torch.log(self.pi * prob1 + (1 - self.pi) * prob2)).sum()
Теперь давайте переопределим функции обучения и оценки модели:
def train_bnn(net, optimizer, epoch, train_loader): # Обучает сеть по батчу за раз net.train() total_loss = 0.0 total = 0.0 correct = 0.0 for images, labels in train_loader: images, labels = images.to(DEVICE), labels.to(DEVICE) # Обнуляет все параметры модели net.zero_grad() # Прямой проход для получения прогноза pred = net.forward(images.cuda.view(-1, 784), False) # Расчёт функции потерь и точности loss = net.elbo(images, labels) total_loss += loss total += labels.size(0) correct += (pred.argmax(-1) == labels.cuda()).sum().item() # Обратный проход loss.backward() # Оптимизируем параметры сети optimizer.step() # Выводим результаты по эпохам print(f"Эпоха {epoch}: loss {total_loss:.5f} accuracy {correct / total * 100:.5f}") def test_bnn(net, test_loader): # Вычисляет точность на тесте для нашего ансамбля # путём выборки из распределения параметров net.eval() correct = 0 coorects = np.zeros(SAMPLES + 1, dtype = int) for images, labels in test_loader: images, labes = images.to(DEVICE), labels.to(DEVICE) outputs = torch.zeros(SAMPLES + 1, BATCH_SIZE, CLASSES) # Выборка! for i in range(CLASSES): outputs[i] = net(images, sample = True) outputs[SAMPLES] = net(images, sample = False) output = outputs.mean(0) preds = preds = outputs.max(2, keepdim = True)[1] pred = output.max(1, keepdim = True)[1] # Индекс максимальной лог-вероятности corrects += preds.eq(labels.view_as(pred)).sum(dim = 1).squeeze().cpu().numpy() correct += pred.eq(labels.view_as(pred)).sum().item() for index, num in enumerate(corrects): if index < SAMPLES: print(f'Компонент {index} точности ансамбля: {num}/{TEST_SIZE}') else: print(f'Точность последующего среднего значения: {num}/{TEST_SIZE}') print(f'Точность ансамбля: {correct}/{TEST_SIZE}')
Будем терпеливы, сети BNN обучаются дольше!
bnn = BNN().to(DEVICE) optimizer = torch.optim.Adam(bnn.parameters(), lr = 0.001) for epoch in range(TRAIN_EPOCHS): train_bnn(bnn, optimizer, epoch, train_loader) test_bnn(bnn, test_loader)
Заключение
Теперь мы видим, что точность на тесте примерно одинакова для всех трёх сетей (сеть Sklearn достигла 97%, небайесовская версия PyTorch получила 97,64%, а байесовская реализация 96,93%). Однако, если мы будем тренировать нашу BNN дольше, то результаты могут измениться, так как обычно требуется больше эпох для достижения высокой точности. Как мы уже говорили ранее, мы можем легко оптимизировать сеть, и у нас есть понятие неопределённости в прогнозах (которые мы генерируем путём многократной выборки). Тот факт, что сеть в Sklearn с более мелкой архитектурой работает так же хорошо, как и более глубокая версия, может указывать на то, что дополнительные слои не нужны. Но чтобы быть уверенными в этом, нам нужно было бы гораздо лучше изучить результативность этих сетей, протестировав различные конфигурации гиперпараметров.
Теперь возникает вопрос: как нам включить прогноза "Я не знаю" в нашу сеть? Опять же, мы будем делать выборку много раз (возможно, более 10, скажем, 100), что даст нам гораздо лучшую оценку вероятности принадлежности цифры к классу. Затем нам нужно будет установить пороговое значение (например, 0,2), и мы откажемся от классификации каждой цифры, для которой мы не уверены хотя бы на 20%, что она принадлежит определённому классу! Это означает, что если сеть не уверена в своём прогнозе до определённого порога, она откажется классифицировать этот пример.
Итак, мы реализовали три многослойных персептрона с функцией активации ReLU, один в Sklearn и два в PyTorch для обучения на очень известном наборе данных MNIST. Мы также узнали о преимуществах вероятностных нейронных сетей и базовое понимание, как они работают.
👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻
Источник: Cambridge с