четверг, 27 декабря 2018 г.

PyTorch: нейронные сети

Нейронные сети могут быть построены с использованием пакета torch.nn.

torch.nn зависит от autograd в определении моделей и их дифференцировании. nn.Module содержит слои и метод forward(input), который возвращает output.

Например, посмотрите на эту сеть, которая классифицирует цифровые изображения:

Это простая сеть прямой связи. Она принимает вход, передает его через несколько слоев один за другим, а затем, наконец, возвращает вывод.

Типичная процедура обучения для нейронной сети следующая:

  1. Определение нейронной сети, которая имеет некоторые изучаемые параметры (или веса)
  2. Итерация по набору входных данных
  3. Обработка ввода через сеть
  4. Рассчет потери (насколько далеки результаты от правильных)
  5. Распространение градиентов обратно на параметры сети
  6. Обновление весов сети, как правило, используя простое правило обновления:
    weight = weight - learning_rate * gradient

Определение сети

Давайте определим сеть:

# -*- coding: utf-8 -*-
import torch
import torch.nn as nn
import torch.nn.functional as F


class Net(nn.Module):

    def init(self):
        super(Net, self).init()
        # 1 канал ввода изображения, 
        # 6 каналов вывода, 
        # 5x5 квадратное сверточное ядро
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # аффинная операция: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, x):
        # Максимальное объединение через (2, 2) окно
        x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
        # Если размер - это квадрат,
        # тогда вы можете задать только одно число
        x = F.max_pool2d(F.relu(self.conv2(x)), 2)
        x = x.view(-1, self.num_flat_features(x))
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x

    def num_flat_features(self, x):
        # все измерения, исключая измерение пакета
        size = x.size()[1:]  
        num_features = 1
        for s in size:
            num_features *= s
        return num_features


net = Net()
print(net)

Вывод:

Net(
  (conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))
  (conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))
  (fc1): Linear(in_features=400, out_features=120, bias=True)
  (fc2): Linear(in_features=120, out_features=84, bias=True)
  (fc3): Linear(in_features=84, out_features=10, bias=True)
)

Вам просто нужно определить функцию forward, а функция backward (где вычисляются градиенты) автоматически определена для вас, используя autograd. Вы можете использовать любую из операций Tensor в функции forward.

Изучаемые параметры модели возвращаются net.parameters()

params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight

Вывод:

10
torch.Size([6, 1, 5, 5])

Попробуем случайный 32x32 ввод (Ожидаемый размер ввода для этой net(LeNet) - 32x32.) Для использования этой сети на MNIST наборе следует изменить размер изображений в наборе на 32x32.

input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)

Вывод:

tensor([[-0.0233,  0.0159, -0.0249,  0.1413,  
          0.0663,  0.0297, -0.0940, -0.0135,
          0.1003, -0.0559]], grad_fn=<AddmmBackward>)

Обнулим градиентные буферы всех параметров и backprops со случайными градиентами:

net.zero_grad()
out.backward(torch.randn(1, 10))

Примечание

torch.nn поддерживает только мини-пакеты. Весь torch.nn пакет поддерживает только вводы, которые являются мини-пакетами примеров, а не отдельным примером.

Например, nn.Conv2d будет принимать 4D тензор
nSamples x nChannels x Height x Width.

Если у вас есть отдельный пример, просто используйте input.unsqueeze(0) для того чтобы добавить поддельное измерение пакета.

Рассмотрим все классы, которые встрчались ранее.

  • torch.Tensor - многомерный массив с поддержкой autograd операций как backward(). Также содержит градиенты тензора.
  • nn.Module - модуль нейронной сети. Удобный способ инкапсуляции параметров с вспомогательными интрументами для их перемещения в GPU, экспорта, загрузки и т. д.
  • nn.Parameter - это разновидность Tensor, который автоматически регистрируется как параметр при назначении в качестве атрибута Module.
  • autograd.Function - реализует прямое и обратное определения (forward and backward) autograd операции. Каждая Tensor операция создает как минимум один Function узел, который подключается к функциям, которые создали Tensor и кодирует его историю.

На данный момент мы рассмотрели:

  • Определение нейронной сети
  • Обработка входных данных и обратный вызов

Еще осталось:

  • Вычисление потери
  • Обновление весов сети

Функция потери

Функция потери принимает (выходную, целевую) пару входных данных и вычисляет значение, определяющее, насколько далеко вывод от цели.

Существует несколько разных функций потери в пакете torch.nn. Простая потеря: nn.MSELoss, которая вычисляет среднеквадратичную ошибку между вводом и целью.

Например:

output = net(input)
target = torch.randn(10)  # случайный target, например
target = target.view(1, -1)  # сделаем его такой же формы как вывод
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

Вывод:

tensor(1.3389, grad_fn=<MseLossBackward>)

Теперь, если вы следуете потере в обратном направлении, используя .grad_fn атрибут, вы увидите граф вычислений, который выглядит схожим образом:

input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
      -> view -> linear -> relu -> linear -> relu -> linear
      -> MSELoss
      -> loss

Таким образом, когда мы вызываем loss.backward(), весь граф дифференцирован в соотвествии с потерей, и все тензоры в графе, которые имеют requires_grad=True будут иметь их .grad тензор накопленный с градиентом.

Для иллюстрации проследуем по нескольким шагам в обратном направлении:

print(loss.grad_fn)  # MSELoss
print(loss.grad_fn.next_functions[0][0])  # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0])  # ReLU

Вывод:

<MseLossBackward object at 0x7fab77615278>
<AddmmBackward object at 0x7fab77615940>
<AccumulateGrad object at 0x7fab77615940>


Backprop (Обратное распространение)

Для обратного распространения ошибки все, что нам нужно сделать, это loss.backward(). Вы должны очистить существующие градиенты, иначе градиенты будут накапливается к существующим градиентам.

Теперь мы вызовем loss.backward() и посмотрим на смещение conv1 градиентов до и после backward.

# обнуляем градиентные буферы всех параметров
net.zero_grad()  

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

Вывод:

conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([-0.0054,  0.0011,  0.0012,  0.0148, -0.0186,  0.0087])


Обновление весов

Самым простым правилом обновления, используемым на практике, является стохастический градиентный спуск (Stochastic Gradient Descent (SGD)):

weight = weight - learning_rate * gradient

Мы можем реализовать это с помощью простого Python кода:

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

Однако, когда вы используете нейронные сети, вы хотите использовать различные правила обновления, такие как SGD, Nesterov-SGD, Adam, RMSProp и др. Чтобы включить их, существует небольшой пакет: torch.optim, который реализует все эти методы. Использовать его очень просто:

import torch.optim as optim

# создайте ваш оптимизатор
optimizer = optim.SGD(net.parameters(), lr=0.01)

# в вашем тренировочном цикле:
optimizer.zero_grad()   # обнуляем градиентные буфферы
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step()    # Выполняем обновление



Читайте также другие статьи в блоге:

PyTorch: тензоры (tensors)

PyTorch: Autograd, автоматическая дифференциация

Анатомия нейронных сетей