Это первая статья, которую я пишу, поэтому ожидайте, что она будет плохой действительно плохой, в любом случае, если кто-то случайно читает это, я прошу вас проявить терпение.
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.ndarray
s в torch.Tensor
s, что легко выполняется с помощью 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.