Машинное обучение
October 17

Нейронные сети в 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, станет ли создание такой сети сложнее, чем в нашем предыдущем примере? Немного, но давайте сначала проясним несколько понятий:

  1. Первое, что нужно понять о BNN: Чтобы оценить сеть на конкретном объекте, нам необходимо выполнить выборку из распределения параметров (или использовать ожидаемое значение в противном случае). На практике эти сети функционируют как ансамбль, выдавая несколько результатов для одного объекта. Это происходит из-за того, что мы многократно делаем выборку из распределения параметров. Затем эти выходные данные можно усреднить для получения окончательного прогноза и представления о неопределенности в предсказании.
  2. Вторая концепция заключается в том, что, как указывает слово «байесовский», мы накладываем априорное значение на параметры сети. Обычно мы определяем распределение (например, нормальное) и инициализируем параметры с помощью предшествующего. В этом случае мы используем разработанную смесь гауссиан для предыдущих параметров.

Это руководство даст только общее представление о том, как работают BNN. Мы не будем углубляться в вариационный вывод, который является подходом, используемым этими сетями для аппроксимации распределения изученных параметров. На этом этапе нам просто нужно знать, что вариационный вывод является одним из наиболее распространённых подходов (наряду с сэмплированием) для аппроксимации апостериорного распределения (возможно, вы уже слышали о вариационных автокодировщиках).

Чтобы это реализовать, нам необходимо определить:

  1. Архитектуру: Количество слоев и определение байесовского слоя.
  2. Функцию потерь: Как учитывать ошибки неправильной классификации и использовать их во время обучения.
  3. Функции обучения и тестирования.
# Добавляем нужные импорты
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 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶

Источник: Cambridge с