Введение в PyTorch

PyTorch — один из самых популярных фреймворков глубокого обучения для специалистов в области machine learning. Создан он на основе библиотеки Torch.

Есть много вещей, которые делают PyTorch популярным. Это и простота использования, и динамический вычислительный граф, и тот факт, что он кажется более «питоновским», чем другие фреймворки, такие как Tensorflow.

В этом руководстве мы рассмотрим базовые компоненты PyTorch. Затем разберем задачу классификации изображений с использованием набора данных CIFAR10. Поскольку PyTorch имеет множество функций и вариантов их применения, очевидно, данное руководство не будет исчерпывающим. Это только введение в PyTorch. Цель этой статьи —  создать общее представление об этом фреймворке и рассмотреть несколько его компонентов.

От редакции Pythonist. О глубоком обучении можно почитать в статье «Введение в глубокое обучение: пошаговое руководство».

Тензор

Центральным компонентом PyTorch является такая структура данных, как тензор. Если вы знакомы с NumPy, вы обнаружите, что тензоры PyTorch похожи на ndarrays в NumPy. Ключевое отличие заключается в том, что они поддерживают CUDA и созданы для запуска на аппаратных ускорителях, таких как графические процессоры.

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

Эти две особенности тензоров очень важны для глубокого обучения:

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

PyTorch также поддерживает распределенные вычисления, расширяя процесс обучения за пределы одной машины!

С учетом сказанного выше давайте взглянем на тензорный API!

[machinelearning_ad_block]

Тензоры в действии

Примечание. Если вы хотите вместе с нами запускать код, то перейдите сначала к разделу Установка.

Итак, мы можем естественным образом создавать тензоры из списков в Python:

A = [[6, 9, 2],
     [3, 3, 7],
     [1, 0, 3]]
     
A_tensor = torch.tensor(A)

Это также естественно работает с Numpy ndArrays:

B = np.array([0,1,2,3])
B_tensor = torch.from_numpy(B)

Как и в NumPy, мы можем инициализировать тензоры случайными значениями, одними единицами или одними нулями. Просто укажите форму (и dtype, если хотите указать тип данных):

# with no dtype argument, torch will infer the type
C = torch.zeros(4,4) 

C
# tensor([[0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.],
#        [0., 0., 0., 0.]])

Не будем забывать, что тензоры не обязательно должны быть двумерными!

D = torch.ones(3,3,2, dtype=torch.int)

D
# tensor([[[1, 1],
#         [1, 1],
#         [1, 1]],
#
#        [[1, 1],
#         [1, 1],
#         [1, 1]],
#
#        [[1, 1],
#         [1, 1],
#         [1, 1]]], dtype=torch.int32)

Новый тензор можно создать из существующего. Итак, при желании мы могли бы создать новый тензор нулей с теми же свойствами (форма и тип данных), что и созданный нами A_tensor:

A_tensor_zeros = torch.zeros_like(A_tensor)

A_tensor_zeros 
# tensor([[0, 0, 0],
#        [0, 0, 0],
#        [0, 0, 0]])

Или, может быть, вам нужны случайные значения с плавающей запятой:

# Аргумент dtype позволяет явно указать тип данных тензора
A_tensor_rand = torch.rand_like(A_tensor, dtype=torch.float) 

A_tensor_rand
# tensor([[0.2298, 0.9499, 0.5847],
#        [0.6357, 0.2765, 0.0125],
#        [0.1215, 0.1747, 0.9935]])

Хотите получить атрибуты тензора?

A_tensor_rand.dtype
# torch.float32

A_tensor_rand.shape
# torch.Size([3, 3])

A_tensor_rand.device
# device(type='cpu')

Создавать тензоры — это хорошо. Однако настоящее веселье начнется, когда мы начнем манипулировать ими и применять математические операции. Встроенных операций существует много, мы точно не успеем обсудить их все. По этой ссылке можно ознакомиться с ними более подробно. Здесь же просто назовем несколько:

  • умножение матриц
  • вычисление собственных векторов и значений
  • сортировка
  • индексы, срезы, соединения
  • Окно Хэмминга (не уверены, что это такое, но звучит круто!!)

Модули Dataset и DataLoader

Dataset

Как и Tensorflow, PyTorch имеет несколько наборов данных, включенных в пакет (например, Text, Image и Audio). В этом руководстве будет использоваться один из таких встроенных наборов данных изображений — CIFAR10. Эти датасеты очень распространены и широко задокументированы в сообществе ML. Они отлично подходят для прототипирования и сравнительного анализа моделей, поскольку вы можете сравнить производительность своей модели с тем, чего смогли достичь другие.

import torch
import torchvision
from torchvision.datasets import FashionMNIST # torchvision for image datasets
from torchtext.datasets import AmazonReviewFull # torchtext for text
from torchaudio.datasets import SPEECHCOMMANDS #torchaudio for audio

training_data = FashionMNIST(
    # the directory you want to store the dataset, can be a string e.g. "data"
    root = data_directory, 
    # if set to False, will give you the test set instead
    train = True, 
    # download the dataset if it's not already available in the root path you specified
    download = True, 
    # as the name implies, will transform images to tensor data structures so PyTorch can use them for training
    transform = torchvision.transforms.ToTensor() 
)

Кроме того, если в вашем наборе данных есть метки или классификации, можно быстро просмотреть их список:

training_data.classes
# ['T-shirt/top',
# 'Trouser',
# 'Pullover',
# 'Dress',
# 'Coat',
# 'Sandal',
# 'Shirt',
# 'Sneaker',
# 'Bag',
# 'Ankle boot']

training_data.class_to_idx # get the corresponding index with each class
# {'Ankle boot': 9,
# 'Bag': 8,
# 'Coat': 4,
# 'Dress': 3,
# 'Pullover': 2,
# 'Sandal': 5,
# 'Shirt': 6,
# 'Sneaker': 7,
# 'T-shirt/top': 0,
# 'Trouser': 1}

Очевидно, что встроенные наборы данных — это не все, что нужно специалисту по машинному обучению. Создание собственного набора данных с помощью PyTorch — довольно простой и гибкий процесс, хотя и немного более сложный, чем простой импорт. 

DataLoader

Итерация по набору данных будет проходить каждую выборку постепенно, поэтому PyTorch предоставляет нам модуль DataLoader для простого создания мини-пакетов в наших наборах данных. DataLoader позволяет нам указать размер батча, а также перемешать данные:

train_dataloader = DataLoader(training_data, batch_size = 32, shuffle = True)

Таким образом, в процессе глубокого обучения вы сможете передавать свои данные в модель для обучения в мини-батчах и через DataLoader.

Прежде чем перейти к глубокому обучению, разберем еще один важный момент — настройку устройства. Если вы хотите тренироваться на графическом процессоре, вы можете проверить, доступен ли он для использования PyTorch:

torch.cuda.is_available()
# True if GPU available

PyTorch по умолчанию использует центральный процессор. Поэтому даже при наличии графического процессора все равно нужно указать, что вы хотите его использовать для обучения. Если вы уверены, что ваш графический процессор доступен, вы можете использовать .to("cuda") для своих тензоров и моделей. В противном случае вы можете рассмотреть возможность установки переменной device для любого доступного устройства:

device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Device: {device}")
# 'cuda'

# you can specify .to("cuda") or .to(device)
tensor = tensor.to("cuda")

# attaching your neural network model to your GPU
model = model.to(device)

Если вы используете Google Colab, у вас будет бесплатный доступ к графическому процессору. 

Установка

Итак, мы будем использовать Google Colab.

Colab является хорошим помощником в проектах по машинному обучению. Для небольших проектов и учебных пособий это лучшее решение, т.к. Colab предоставляет бесплатный доступ к графическому процессору и среду, которая включает в себя уже установленные пакеты, такие как PyTorch, NumPy, Scikit-Learn.

Итак, для начала перейдите на страницу Google Colab и войдите в свою учетную запись Google. File > New notebook. Измените имя своего notebook на pytorchIntro.ipynb или какое-нибудь другое.

Colab не предоставляет экземпляр с доступом к графическому процессору по умолчанию. Поэтому вам нужно явно указать, что вы хотите его использовать. Вверху выберите Runtime > Change runtime type > Hardware accelerator > Select "GPU" > Save. Теперь у вас есть графический процессор для обучения ваших моделей!

Теперь, когда у нас есть Colab и графический процессор, давайте перейдем к коду.

Импорт

import torch
from torch import nn
from torch.utils.data import DataLoader
from torchvision.utils import make_grid
from torchvision.datasets import CIFAR10
from torchvision.transforms import ToTensor
from torchvision.transforms import Normalize, Compose
import os
import matplotlib.pyplot as plt
import numpy as np

Для начала импортируйте следующие модули. Это основные модули PyTorch, которые мы будем использовать, а также несколько вспомогательных импортов. Позже мы рассмотрим их более подробно.

Dataset

Набор данных CIFAR-10

Этот набор данных состоит из 60 000 цветных изображений 32×32, помеченных как один из 10 классов. Учебный набор составляет 50 000 изображений, а тестовый — 10 000.

Вот хорошая визуализация набора данных из домашнего источника:

Цель этого проекта — создание модели, которая сможет точно классифицировать изображения по одной из 10 классификаций.

Загрузка набора данных

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

Перед подачей в модель мы должны нормализовать наши изображения. Поэтому мы определим функцию преобразования transform() и используем torchvision.transforms.Normalize для нормализации всех наших изображений при создании переменных обучающих и тестовых данных.

Метод Normalize() использует желаемое среднее значение и стандартное отклонение в качестве аргументов. Поскольку это цветные изображения, значение должно быть предоставлено для каждого цветового канала (R, G, B).

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

transform = Compose(
    [ToTensor(),
     Normalize((0.5, 0.5, 0.5),  # mean
               (0.5, 0.5, 0.5))] # std. deviation
)

Теперь мы можем использовать нашу функцию transform() в качестве аргумента transform, чтобы PyTorch применил ее ко всему набору данных.

training_data = CIFAR10(root="cifar",
                        train = True, # train set, 50k images
                        download = True,
                        transform=transform)
test_data = CIFAR10(root = "cifar",
                    train = False, # test set, 10k images
                    download = True,
                    transform = transform)

Когда наш набор данных загружен и нормализован, мы можем подготовить его для передачи в нейронную сеть с помощью PyTorch DataLoader, где можно определить размер пакета batch_size .

batch_size = 4
train_dataloader = DataLoader(training_data, 
                              batch_size=batch_size, 
                              shuffle=True)
test_dataloader = DataLoader(test_data, 
                             batch_size=batch_size, 
                             shuffle=True)

DataLoader является итерируемым, поэтому давайте взглянем на train_dataloader, проверив размеры одной итерации:

for X, y in train_dataloader:
  print(f"Shape of X [N, C, H, W]: {X.shape}")
  print(f"Shape of y: {y.shape} {y.dtype}")
  break
# Shape of X [N, C, H, W]: torch.Size([4, 3, 32, 32])
# Shape of y: torch.Size([4]) torch.int64

Здесь X — изображения, а y — метки. Мы установили batch_size = 4, чтобы каждая итерация через train_dataloader представляла собой мини-пакет из 4 изображений 32×32 и соответствующих им 4 меток.

Теперь давайте рассмотрим некоторые примеры в нашем наборе данных.

def imshow(img):
  img = img / 2 + .05 # revert normalization for viewing
  npimg = img.numpy()
  plt.imshow(np.transpose(npimg, (1,2,0)))
  plt.show()
  
classes = training_data.classes
training_data.classes
#['airplane',
# 'automobile',
# 'bird',
# 'cat',
# 'deer',
# 'dog',
# 'frog',
# 'horse',
# 'ship',
# 'truck']

dataiter = iter(train_dataloader)
images, labels = dataiter.next()
imshow(make_grid(images))
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))

Обычно стоит провести более тщательное исследование и анализ данных, прежде чем переходить к построению модели. Однако, поскольку это всего лишь введение в PyTorch, мы перейдем к построению и обучению модели.

Определение базовой модели

Давайте построим нейронную сеть.

Сперва мы определим класс нашей модели и назовем его NeuralNetwork. Наша модель будет подклассом PyTorch nn.Module, который является базовым классом для всех модулей нейронной сети в PyTorch.

Поскольку в нашем наборе данных есть цветные изображения, форма каждого изображения будет (3, 32, 32) — тензор 32×32 в каждом из 3 цветовых каналов RGB.

Т.к. наша исходная модель будет состоять из полностью связанных слоев, нам нужно будет применить функцию nn.Flatten() для входных данных изображения. Наш метод сглаживания выведет линейный слой с 3072 (32 x 32 x 3) узлами.

Функция nn.Linear() принимает количество входных нейронов и количество выходных в качестве аргументов соответственно (nn.Linear(1024 in, 512 out)). После этого вы можете добавить слои Linear layers и ReLU в основной контент. Output нашей модели составляет 10 логитов, соответствующих 10 классам в нашем наборе данных.

После определения структуры модели мы зададим последовательность прямого прохода. Поскольку наша модель представляет собой последовательную модель, метод forward будет очень простым. Он будет вычислять выходной Tensor из входных Tensors.

При желании вы можете просто вывести модель model после ее определения, чтобы получить сводную информацию о структуре.

class NeuralNetwork(nn.Module):
    def __init__(self):
        super(NeuralNetwork, self).__init__()
        self.flatten = nn.Flatten()
        self.linear_relu_stack = nn.Sequential(
            nn.Linear(32*32*3, 1024),
            nn.ReLU(),
            nn.Linear(1024, 512),
            nn.ReLU(),
            nn.Linear(512, 10)
        )
    def forward(self, x):
        x = self.flatten(x)
        logits = self.linear_relu_stack(x)
        return logits
        
model = NeuralNetwork().to(device)

print(model)
#NeuralNetwork(
#  (flatten): Flatten(start_dim=1, end_dim=-1)
#  (linear_relu_stack): Sequential(
#    (0): Linear(in_features=3072, out_features=1024, bias=True)
#    (1): ReLU()
#    (2): Linear(in_features=1024, out_features=512, bias=True)
#    (3): ReLU()
#    (4): Linear(in_features=512, out_features=10, bias=True)
#  )
#)

Функция потерь и оптимизатор

Поскольку это задача классификации, мы будем использовать функцию кросс-энтропийных потерь Cross-Entropy. Напомним, что Cross-Entropy вычисляет логарифмическую потерю, когда модель выводит прогнозируемое значение вероятности между 0 и 1. Таким образом, поскольку прогнозируемая вероятность отличается от истинного значения, потери быстро возрастают. График ниже иллюстрирует поведение функции потерь по мере того, как прогнозируемое значение становится ближе и дальше от истинного значения.

В PyTorch мы можем просто использовать функцию CrossEntropyLoss(). Для нашего алгоритма оптимизации мы применим стохастический градиентный спуск, который реализован в пакете torch.optim вместе с другими оптимизаторами, такими как Adam и RMSprop. Нам просто нужно передать параметры нашей модели и скорость обучения lr. Если вы хотите использовать затухание импульса или веса при оптимизации модели, вы можете передать это оптимизатору SGD() в качестве параметров momentum и weight_decay (по умолчанию они равны 0).

loss_fn = nn.CrossEntropyLoss()
optimizer = torch.optim.SGD( model.parameters(), lr=0.001 ) # momentum=0.9

Определение цикла обучения

Здесь мы определяем нашу функцию train(), которой мы будем передавать train_dataloader, model, loss_fn и optimizer в качестве аргументов в процессе обучения. Переменная size — это длина всего обучающего набора данных (50 КБ). В следующей строке model.train() — это метод nn.Module в PyTorch, который переводит модель в режим обучения, обеспечивая определенные варианты поведения, которые нам нужны (например, отсев, пакетная норма и т.д.). Затем мы пройдемся по каждому мини-пакету, указав, что мы хотели бы использовать GPU с to(device). Мы загружаем мини-пакет в нашу модель, вычисляем потери, а затем выполняем обратное распространение.

Результат обратного распространения и обучения

Для шага обратного распространения нам нужно сначала запустить optimizer.zero_grad(). Это устанавливает градиент в ноль перед запуском обратного распространения, поскольку мы не хотим накапливать градиент за последующие проходы.

Метод loss.backward() использует потери для вычисления градиента, затем мы используем Optimizer.step() для обновления весов.

Наконец, мы можем вывести обновления процесса обучения, печатая вычисленные потери после каждых 2000 обучающих выборок.

def train(dataloader, model, loss_fn, optimizer):
    size = len(dataloader.dataset)
    model.train()
    for batch, (X, y) in enumerate(dataloader):
        X, y = X.to(device), y.to(device)
        # Compute prediction error
        pred = model(X)
        loss = loss_fn(pred, y)
        # Backpropagation
        optimizer.zero_grad()
        loss.backward()
        optimizer.step()
        if batch % 2000 == 0:
            loss, current = loss.item(), batch * len(X)
            print(f"loss: {loss:>7f}  [{current:>5d}/{size:>5d}]")

Определение метода тестирования

Перед обучением модели давайте реализуем тестовую функцию. Это нужно для того, чтобы мы могли оценивать нашу модель и выводить точность на тестовом наборе. Большие отличия от метода тестирования заключаются в том, что мы используем model.eval(), чтобы перевести модель в режим тестирования, и torch.no_grad(), который отключит вычисление градиента, так как мы не используем обратное распространение во время тестирования. Наконец, мы вычисляем средние потери для набора тестов и общую точность.

def test(dataloader, model, loss_fn):
    size = len(dataloader.dataset)
    num_batches = len(dataloader)
    model.eval()
    test_loss, correct = 0, 0
    with torch.no_grad():
        for X, y in dataloader:
            X, y = X.to(device), y.to(device)
            pred = model(X)
            test_loss += loss_fn(pred, y).item()
            correct += (pred.argmax(1) == y).type(torch.float).sum().item()
    test_loss /= num_batches
    correct /= size
    print(f"Test Error: \n Accuracy: {(100*correct):>0.1f}%, Avg loss: {test_loss:>8f} \n")

Обучение модели

Теперь, когда мы всё сделали, мы готовы к обучению! Укажите количество epochs, на которых вы хотите обучить модель. Каждая эпоха будет проходить цикл train, который выводит прогресс каждые 2000 выборок. Затем он проверяет модель на тестовом наборе и выводит точность и потери на тестовом наборе.

epochs = 10
for t in range(epochs):
    print(f"Epoch {t+1}\n-------------------------------")
    train(train_dataloader, model, loss_fn, optimizer)
    test(test_dataloader, model, loss_fn)
print("Done!")

Сохранение и загрузка модели

После завершения обучения, если вы хотите сохранить свою модель для использования в выводах, используйте torch.save().

Передайте model.state_dict() в качестве первого аргумента. Это просто словарь, который сопоставляет слои с их соответствующими изученными параметрами (весами и смещениями).

В качестве второго аргумента дайте имя вашей модели (принято сохранять модели PyTorch с использованием расширений .pth или .pt). Также можно указать полный путь, если вы хотите сохранить его в определенном каталоге.

torch.save(model.state_dict(), "cifar_fc.pth")

Если вы хотите загрузить свою модель для логического вывода, используйте torch.load(), чтобы получить сохраненную модель, и сопоставьте изученные параметры с помощью load_state_dict.

model = NeuralNetwork()
model.load_state_dict(torch.load("cifar_fc.pth"))

Оценка модели

Вы можете пройти через test_dataloader, чтобы сверить образец изображений с их метками.

dataiter = iter(test_dataloader)
images, labels = dataiter.next()

imshow(make_grid(images))
print('Ground Truth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

Затем сравните его с предсказанными метками нашей модели, чтобы предварительно оценить ее производительность:

outputs = model(images)
_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}' for j in range(4)))
# Predicted:  dog   ship  automobile deer

Итак, мы видим, что наша модель учится классифицировать! Давайте посмотрим на цифры производительности нашей модели.

correct = 0
total = 0

with torch.no_grad():
   for data in test_dataloader:
     images, labels = data
     outputs = model(images)
     _, predicted = torch.max(outputs.data, 1)
     total += labels.size(0)
     correct += (predicted == labels).sum().item()
     
print(f'Model accuracy: {100 * correct // total} %')
# Model accuracy: 53 %

Точность в 53% далеко не самый хороший результат. Однако это намного лучше, чем случайное угадывание или просто предсказание одного класса. Так что наша модель определенно чему-то научилась! 

Далее мы можем быстро проверить, как она работает при классификации каждого класса:

correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0  for classname in classes}

with torch.no_grad():
  for data in test_dataloader:
    images, labels = data
    outputs = model(images)
    _, predictions = torch.max(outputs, 1)
    for label,prediction in zip(labels, predictions):
      if label == prediction:
        correct_pred[classes[label]] += 1
      total_pred[classes[label]] += 1

for classname, correct_count in correct_pred.items():
  accuracy = 100 * float(correct_count) / total_pred[classname]
  print(f'Accuracy for class {classname:5s}: {accuracy:.1f}%')
# Accuracy for class airplane: 58.9%
# Accuracy for class automobile: 61.2%
# Accuracy for class bird : 33.5%
# Accuracy for class cat  : 35.4%
# Accuracy for class deer : 52.8%
# Accuracy for class dog  : 49.4%
# Accuracy for class frog : 60.6%
# Accuracy for class horse: 59.6%
# Accuracy for class ship : 64.5%
# Accuracy for class truck: 63.1%

Теперь у нас есть немного лучшее представление о производительности нашей модели. Сети было сложнее классифицировать изображения кошек и птиц.

Заключение

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

Надеемся, данная статья была вам полезна! Успехов в написании кода!

Перевод статьи «Intro to PyTorch: Part 1».