Обучение перспетрона#

Обратное распространение ошибки#

Пусть имеем какую-то задачу регрессии. Т.е. какой-то конечный набор \(\left\{\left(x_i, y_i\right)\right\}_{i=1}^N\), функциональную зависимость которого нам надо примерно определить.

Т.к. работаем с нейронными сетями, то и пример будет кондовый, хоть и классический: \(x_i\) - матричка \(28\times28\), которая есть картинка какой-то цифры, а \(y_i\) - это та самая цифра, изображенная на соответсвующей картинке(матрице).

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

Естественно, нам пригодятся все знания из темы регрессия. Т.е. надо определить функцию ошибки Loss, которую мы и хотим минимизировать градиентным спуском по весам модели.

Основная фишка нейронных сетей - это «аля» линейность по весам. Благодаря этому факту можно относительно несложно написать градиент в зависимости от ошибки выхода нейронных сетей - этот алгоритм называется обратное распространение ошибки (backpropagation). Подробнее о нём можно почитать тут: neerc.ifmo.ru/wiki/index.php?title=Обратное_распространение_ошибки

Dropout#

Это такая техника предотвращения переобучения и выучивания обучающей выборки.

Во время обучения случайным образом обнуляются некоторые элементы входного тензора с вероятностью \(p\), все остальные элементы при этом домнажаются на \(\frac{1}{1-p}\).

Это оказалось эффективным методом регуляризации и предотвращения совместной адаптации нейронов, как описано в статье https://arxiv.org/abs/1207.0580

Стохастический градиентный спуск#

Ествественно, надо его вспомнить. Не будем же мы всю обучающую выборку за раз сувать в вычисление градиента?

Напоминаю, что мы «нарезаем» обучающую выборку на батчи размера batch_size, а дальше делаем градиентный спуск только по нарезанным кусочкам поочередно. Как только прошли всё, считаем, что закончилась ЭпОхА.

Распознование рукописных цифр. Датасет MNIST. Реализация на torch#

torch - библиотека для удобного обращения с тензорами и нейросетями в python. Поддерживает GPU. Рекоммендую выполнять дальнейший код на коллабе, если вам жалко 5ГБ.

Загрузка датасета#

# It's better to use google collab for next code.

import torch # 5 GB! Plus we'll need GPU later (probably)
import numpy as np
from sklearn.model_selection import train_test_split
from torchvision import datasets
import torchvision.transforms as transforms

# number of subprocesses to use for data loading
num_workers = 0
# how many samples per batch to load
batch_size = 16

# convert data to torch.FloatTensor
transform = transforms.ToTensor()

# choose the training and test datasets
train_data = datasets.MNIST(root='data', train=True,
                                   download=True, transform=transform)
test_data = datasets.MNIST(root='data', train=False,
                                  download=True, transform=transform)

# prepare data loaders for stochastic gradient descent
train_loader = torch.utils.data.DataLoader(train_data, batch_size=batch_size,
    num_workers=num_workers)
test_loader = torch.utils.data.DataLoader(test_data, batch_size=batch_size, 
    num_workers=num_workers)

Беглый осмотр датасета#

import matplotlib.pyplot as plt
#%matplotlib inline
    
# obtain one batch of training images
dataiter = iter(train_loader)
images, labels = next(dataiter)
images = images.numpy()

# plot the images in the batch, along with the corresponding labels
fig = plt.figure(figsize=(25, 4))
for idx in np.arange(16):
    ax = fig.add_subplot(2, 16//2, idx+1, xticks=[], yticks=[])
    ax.imshow(np.squeeze(images[idx]), cmap='gray')
    # print out the correct label for each image
    # .item() gets the value contained in a Tensor
    ax.set_title(str(labels[idx].item()))
../../_images/ffb7812b02f0ec3296ca42406739503797b8efa73ce4a09344bfc32a515c6121.png
img = np.squeeze(images[1])

fig = plt.figure(figsize = (12,12)) 
ax = fig.add_subplot(111)
ax.imshow(img, cmap='gray')
width, height = img.shape
thresh = img.max()/2.5
for x in range(width):
    for y in range(height):
        val = round(img[x][y],2) if img[x][y] !=0 else 0
        ax.annotate(str(val), xy=(y,x),
                    horizontalalignment='center',
                    verticalalignment='center',
                    color='white' if img[x][y]<thresh else 'black')
../../_images/9fe4e06d4580633a748405b09ce81b7b7d00519e4bb54c5fcb19e8d4f58586ab.png

Определение модели#

import torch.nn as nn
import torch.nn.functional as F

## TODO: Define the NN architecture
class Net(nn.Module):
    def __init__(self):
        super(Net, self).__init__()
        # linear layer (784 -> 1 hidden node)
        self.fc1 = nn.Linear(28 * 28, 512)
        self.fc2 = nn.Linear(512, 512)
        self.fc3 = nn.Linear(512, 10)
        self.dropout = nn.Dropout(p=0.2) # google it

    def forward(self, x):
        # flatten image input
        x = x.view(-1, 28 * 28)
        # add hidden layer, with relu activation function
        x = self.dropout(F.relu(self.fc1(x)))
        x = self.dropout(F.relu(self.fc2(x)))
        x = self.fc3(x)
        return x

# initialize the NN
model = Net()
print(model)
Net(
  (fc1): Linear(in_features=784, out_features=512, bias=True)
  (fc2): Linear(in_features=512, out_features=512, bias=True)
  (fc3): Linear(in_features=512, out_features=10, bias=True)
  (dropout): Dropout(p=0.2, inplace=False)
)

Обучение#

Шаги для обучения также описаны в комментариях ниже:

  1. Очистить градиенты всех оптимизированных переменных: torch за вас считает все градиенты и сохраняет их, в начале каждого шага обучения их необходимо обнулить

  2. Передача вперед: вычисление прогноза модели на текущем батче

  3. Вычислить ошибку

  4. Обратный проход (backpropogation): вычислить градиент потерь для текущих весов модели

  5. Выполните один шаг оптимизации (обновление весов)

  6. Обновление средней ошибки на батче

Ну и дальше надо взять сколько-то эпох. У меня хватило 5 для хорошего результата. В дз вам надо будет добиться точности 99%, варьируя параметры модели и количество эпох.

## TODO: Specify loss and optimization functions

# specify loss function
criterion = nn.CrossEntropyLoss()

# specify optimizer
optimizer = torch.optim.SGD(model.parameters(), lr=0.05) # Try lr=0.05 and n_epochs=1

# number of epochs to train the model
n_epochs = 1  # suggest training between 20-50 epochs

model.train() # prep model for training (enables dropout)

from tqdm import tqdm
for epoch in range(n_epochs):
    # monitor training loss
    train_loss = 0.0
    
    ###################
    # train the model #
    ###################
    for data, target in tqdm(train_loader):
        # clear the gradients of all optimized variables
        optimizer.zero_grad()
        # forward pass: compute predicted outputs by passing inputs to the model
        output = model(data)
        # calculate the loss
        loss = criterion(output, target)
        # backward pass: compute gradient of the loss with respect to model parameters
        loss.backward()
        # perform a single optimization step (parameter update)
        optimizer.step()
        # update running training loss
        train_loss += loss.item()*data.size(0)
        
    # print training statistics 
    # calculate average loss over an epoch
    train_loss = train_loss/len(train_loader.dataset)

    print('Epoch: {} \tTraining Loss: {:.6f}'.format(
        epoch+1, 
        train_loss
        ))
  3%|██▋                                                                             | 124/3750 [00:16<07:49,  7.73it/s]
---------------------------------------------------------------------------
KeyboardInterrupt                         Traceback (most recent call last)
Cell In[6], line 30
     28 loss = criterion(output, target)
     29 # backward pass: compute gradient of the loss with respect to model parameters
---> 30 loss.backward()
     31 # perform a single optimization step (parameter update)
     32 optimizer.step()

File ~/anaconda3/envs/book/lib/python3.11/site-packages/torch/_tensor.py:492, in Tensor.backward(self, gradient, retain_graph, create_graph, inputs)
    482 if has_torch_function_unary(self):
    483     return handle_torch_function(
    484         Tensor.backward,
    485         (self,),
   (...)
    490         inputs=inputs,
    491     )
--> 492 torch.autograd.backward(
    493     self, gradient, retain_graph, create_graph, inputs=inputs
    494 )

File ~/anaconda3/envs/book/lib/python3.11/site-packages/torch/autograd/__init__.py:251, in backward(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)
    246     retain_graph = create_graph
    248 # The reason we repeat the same comment below is that
    249 # some Python versions print out the first line of a multi-line function
    250 # calls in the traceback and some print out the last line
--> 251 Variable._execution_engine.run_backward(  # Calls into the C++ engine to run the backward pass
    252     tensors,
    253     grad_tensors_,
    254     retain_graph,
    255     create_graph,
    256     inputs,
    257     allow_unreachable=True,
    258     accumulate_grad=True,
    259 )

KeyboardInterrupt: 

Тестирование обученной модели#

Наконец, мы тестируем нашу полученную модель на ранее невиданных тестовых данных и оцениваем ее производительность.

model.eval()#

model.eval() переведет все слои в вашей модели в режим вычисления (а не обучения). Мы же не хотим dropout на тесте.

# initialize lists to monitor test loss and accuracy
test_loss = 0.0
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))

model.eval() # prep model for *evaluation*

for data, target in test_loader:
    # forward pass: compute predicted outputs by passing inputs to the model
    output = model(data)
    # calculate the loss
    loss = criterion(output, target)
    # update test loss 
    test_loss += loss.item()*data.size(0)
    # convert output probabilities to predicted class
    _, pred = torch.max(output, 1)
    # compare predictions to true label
    correct = np.squeeze(pred.eq(target.data.view_as(pred)))
    # calculate test accuracy for each object class
    for i in range(batch_size):
        label = target.data[i]
        class_correct[label] += correct[i].item()
        class_total[label] += 1

# calculate and print avg test loss
test_loss = test_loss/len(test_loader.dataset)
print('Test Loss: {:.6f}\n'.format(test_loss))

for i in range(10):
    if class_total[i] > 0:
        print('Test Accuracy of %5s: %2d%% (%2d/%2d)' % (
            str(i), 100 * class_correct[i] / class_total[i],
            np.sum(class_correct[i]), np.sum(class_total[i])))
    else:
        print('Test Accuracy of %5s: N/A (no training examples)' % (classes[i]))

print('\nTest Accuracy (Overall): %2d%% (%2d/%2d)' % (
    100. * np.sum(class_correct) / np.sum(class_total),
    np.sum(class_correct), np.sum(class_total)))

Visualize Sample Test Results#

This cell displays test images and their labels in this format: predicted (ground-truth). The text will be green for accurately classified examples and red for incorrect predictions.

# obtain one batch of test images
dataiter = iter(test_loader)
images, labels = next(dataiter)

# get sample outputs
output = model(images)
# convert output probabilities to predicted class
_, preds = torch.max(output, 1)
# prep images for display
images = images.numpy()

# plot the images in the batch, along with predicted and true labels
fig = plt.figure(figsize=(25, 4))
for idx in np.arange(16):
    ax = fig.add_subplot(2, 16//2, idx+1, xticks=[], yticks=[])
    ax.imshow(np.squeeze(images[idx]), cmap='gray')
    ax.set_title("{} ({})".format(str(preds[idx].item()), str(labels[idx].item())),
                 color=("green" if preds[idx]==labels[idx] else "red"))