Это первая статья, которую я пишу, поэтому ожидайте, что она будет плохой действительно плохой, в любом случае, если кто-то случайно читает это, я прошу вас проявить терпение.

PyTorch — одна из самых популярных сред глубокого обучения. Ее основным преимуществом перед другими популярными средами, такими как TensorFlow, является парадигма Python, гибкость и простота использования. При изучении новых фреймворков часто полезно иметь простой минимальный рабочий пример, и действительно есть много статей, описывающих такие программы, но часто в погоне за минимализмом и простотой мы абстрагируемся от основных понятий и функций. Поэтому я попытаюсь сделать макет MWE (минимальный рабочий пример), который будет максимально простым, но не слишком скроет основную сложность. В таких проектах, где предпочтительнее почти полный контроль, PyTorch превосходит другие.

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

  • обработка данных
  • тренировочный цикл
  • интерпретация результатов

Обработка данных

Сначала мы синтетически генерируем данные с помощью sklearn.datasets.make_regression, чтобы не усложнять задачу, X и целевые векторы y будут одномерными.

from matplotlib import pyplot as plt
import numpy as np
import torch
from sklearn.datasets import make_regression
X_np, y_np = make_regression(n_samples=50, n_features=1, bias=2, noise=10)

Следующим шагом является преобразование сгенерированных данных из numpy.ndarrays в torch.Tensors, что легко выполняется с помощью torch.from_numpy, обратите внимание, что torch.from_numpy не копирует базовые данные. Тензоры PyTorch аналогичны массивам numpy.

X = torch.from_numpy(X_np) # shape: (50,1)
y = torch.from_numpy(y_np) # shape: (50)

Цикл тренировок

Цикл обучения, пожалуй, самая важная (после сбора данных и предварительной обработки) часть нашей системы.

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

  • Прямой проход предсказывает целевые значения (y) для заданных точек данных (X) для текущего состояния (W,b).
  • Далее мы измеряем качество текущего состояния по нашему критерию (в нашем случае по среднеквадратической ошибке).
  • Обратный проход вычисляет градиенты критерия, также известного как локальное направление, которое минимизирует ошибку.
  • Шаг обновления выполняется в направлении, рекомендованном градиентами.

Первые два шага выполняются

# Forward pass: Compute predicted y by passing x to the model
    y_pred = reg.forward(X)
# Compute loss according to the criterion
    loss = criterion(y_pred, y)

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

loss.backward()

autograd PyTorch, как следует из названия, вычисляет градиенты для всех зависимых переменных, если установлена ​​их опция requires_grad=True. И последнее, но не менее важное: мы обновляем наши параметры, вызывая

optimizer.step()

единственная цель optimizer объектов - оптимально рассчитать и сделать следующий шаг.

Цикл использует три основных объекта/функции,

  • LinearRegression(), который содержит параметры состояния системы и выполняет прямой проход
  • GDOptimizer() который оптимизирует состояние для заданных параметров и градиентов
  • и mse(), который измеряет качество нашего состояния
class LinearRegression():
    def __init__(self, n_features):
        self.W = torch.ones(n_features, dtype=torch.float64, requires_grad=True)
        self.b = torch.zeros(1, dtype=torch.float64, requires_grad=True)
    def forward(self, X):
        self.y_pred = torch.matmul(X, self.W) + self.b
        return self.y_pred
   
                
class GDOptimizer():
    def __init__(self, parameters, lr=1e-4):
        self.parameters = parameters
        self.lr = lr
    def zero_grad(self):
        for param in self.parameters:
            if param.grad is not None:
                param.grad.zero_()
    def step(self):
        with torch.no_grad():
            for param in self.parameters:
                param -= self.lr * param.grad
                
    def get_params_np(self):
        return [param.grad.detach().numpy().copy() for param in self.parameters]
            
def mse(y, y_pred):
    error = y - y_pred
    n = y.shape[0]
    return 1/n*error.t()@error

И полный тренировочный цикл

criterion = mse
reg = LinearRegression(1)
optimizer = GDOptimizer([reg.W, reg.b], lr=1e-3)
grads_hist = []
params_hist = []
loss_hist = []
for t in range(2000):
    # Forward pass: Compute predicted y by passing x to the model
    y_pred = reg.forward(X)
# Compute and print loss
    loss = criterion(y_pred, y)
    if t % 100 == 0:
        print("t={};  loss={}".format(t, loss.item()))
# Zero gradients, perform a backward pass, and update the weights.
    optimizer.zero_grad()
    loss.backward()
    
    grads_hist.append(optimizer.get_params_np())
    params_hist.append([reg.W.detach().numpy().copy(), reg.b.detach().numpy().copy()])
    loss_hist.append(loss.detach().numpy())
    optimizer.step()

Здесь мы также сохраняем параметры и их градиенты для наглядности.

Интерпретация результатов

Понимание результатов само по себе является нетривиальной задачей, и зачастую нам лучше всего подходят умные иллюстрации. Состояние наших систем задается W,b, их изменение задается градиентами, а качество состояния количественно определяется критерием, эти три являются нашими целями для изучения на иллюстрации.

Состояние/веса нашей системы можно легко представить следующим образом:

grid = np.mgrid[-4:4:0.1].reshape(1, -1).T
for idx, params in enumerate(params_hist):
    if idx % 250 == 0:
        pred = np.matmul(grid, params[0]) + params[1]
        plt.plot(grid, pred, "#FF7600")
plt.plot(X_np, y_np, "o", alpha=0.5)
plt.show()

Хотя построение градиентов для такой выпуклой функции потерь, как эта, мало что говорит, для более сложных моделей это важно.

plt.plot([grads[0][0] for grads in grads_hist])
plt.plot([grads[1][0] for grads in grads_hist])
plt.show()

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

Блокнот Jupyter можно найти в Google Colab.