3.5. Краткая реализация линейной регрессии¶
За последнее десятилетие глубокое обучение пережило своего рода кембрийский взрыв. Число методов, приложений и алгоритмов далеко превосходит достижения предыдущих десятилетий. Это стало возможным благодаря удачному сочетанию нескольких факторов, одним из которых являются мощные бесплатные инструменты, предлагаемые рядом фреймворков глубокого обучения с открытым исходным кодом. Theano (Bergstra et al., 2010), DistBelief (Dean et al., 2012) и Caffe (Jia et al., 2014), пожалуй, представляют первое поколение таких фреймворков, получивших широкое распространение. В отличие от более ранних (пионерских) работ, таких как SN2 (Simulateur Neuristique) (Bottou and Le Cun, 1988), которые обеспечивали опыт программирования, похожий на Lisp, современные фреймворки предлагают автоматическое дифференцирование и удобство использования Python. Эти фреймворки позволяют автоматизировать и модуляризировать рутинную работу по реализации алгоритмов обучения на основе градиента.
В Section 3.4 мы опирались лишь на (i) тензоры для хранения данных и операций линейной алгебры; и (ii) автоматическое дифференцирование для вычисления градиентов. На практике, поскольку итераторы данных, функции потерь, оптимизаторы и слои нейронных сетей являются общими компонентами, современные библиотеки реализуют их за нас. В этом разделе мы покажем, как компактно реализовать модель линейной регрессии из Section 3.4 с использованием высокоуровневых API фреймворков глубокого обучения.
import numpy as np
import torch
from torch import nn
from d2l import torch as d2l
3.5.1. Определение модели¶
Когда мы реализовывали линейную регрессию с нуля в Section 3.4, мы явно определяли параметры модели и вручную прописывали вычисления для получения выходных данных с помощью базовых операций линейной алгебры. Вы должны уметь это делать. Но когда модели станут сложнее, а вам придется заниматься этим практически каждый день, вы будете рады такой помощи. Ситуация аналогична написанию собственного блога с нуля. Сделать это один или два раза — полезно и поучительно, но вы были бы плохим веб-разработчиком, если бы потратили месяц на изобретение велосипеда.
Для стандартных операций мы можем использовать предопределённые слои фреймворка, что позволяет сосредоточиться на слоях, используемых для построения модели, а не на деталях их реализации. Вспомним архитектуру однослойной сети, описанную в Fig. 3.1.2. Этот слой называется полносвязным, поскольку каждый его вход соединён с каждым выходом посредством умножения матрицы на вектор.
В PyTorch полносвязный слой определен в классах Linear и
LazyLinear (доступны начиная с версии 1.8.0). Последний позволяет
пользователям указывать только выходную размерность, тогда как первый
дополнительно требует указания количества входов. Указание формы входа
неудобно и может потребовать нетривиальных вычислений (например, в
свёрточных слоях). Поэтому для простоты мы будем использовать такие
«ленивые» слои, whenever это возможно.
class LinearRegression(d2l.Module): #@save
"""The linear regression model implemented with high-level APIs."""
def __init__(self, lr):
super().__init__()
self.save_hyperparameters()
self.net = nn.LazyLinear(1)
self.net.weight.data.normal_(0, 0.01)
self.net.bias.data.fill_(0)
В методе forward мы просто вызываем встроенный метод __call__
предопределённых слоёв для вычисления результатов.
@d2l.add_to_class(LinearRegression) #@save
def forward(self, X):
return self.net(X)
3.5.2. Определение функции потерь¶
Класс MSELoss вычисляет среднеквадратичную ошибку (без множителя
\(1/2\) в (3.1.5)). По умолчанию MSELoss возвращает
среднее значение функции потерь по примерам. Это быстрее (и проще в
использовании), чем реализовывать собственную функцию.
@d2l.add_to_class(LinearRegression) #@save
def loss(self, y_hat, y):
fn = nn.MSELoss()
return fn(y_hat, y)
3.5.3. Определение алгоритма оптимизации¶
Мини-батч SGD — стандартный инструмент оптимизации нейронных сетей,
поэтому PyTorch поддерживает его наряду с рядом вариаций этого алгоритма
в модуле optim. При создании объекта ``SGD`` мы указываем
параметры, подлежащие оптимизации, которые можно получить из модели с
помощью self.parameters(), а также скорость обучения (self.lr),
требуемую алгоритмом оптимизации.
@d2l.add_to_class(LinearRegression) #@save
def configure_optimizers(self):
return torch.optim.SGD(self.parameters(), self.lr)
3.5.4. Обучение¶
Вы могли заметить, что реализация модели с помощью высокоуровневых API фреймворка глубокого обучения требует меньше строк кода. Нам не приходилось выделять параметры по отдельности, определять функцию потерь или реализовывать мини-батч SGD. Как только мы начнем работать с гораздо более сложными моделями, преимущества высокоуровневого API возрастут значительно.
Теперь, когда все основные компоненты готовы, сам цикл обучения такой
же, как тот, что мы реализовали с нуля. Поэтому мы просто вызываем
метод fit (описанный в Section 3.2.4), который
опирается на реализацию метода fit_epoch из
Section 3.4, для обучения нашей модели.
model = LinearRegression(lr=0.03)
data = d2l.SyntheticRegressionData(w=torch.tensor([2, -3.4]), b=4.2)
trainer = d2l.Trainer(max_epochs=3)
trainer.fit(model, data)
Ниже мы сравниваем параметры модели, полученные при обучении на конечном наборе данных, с истинными параметрами, которые сгенерировали наш набор данных. Чтобы получить доступ к параметрам, мы обращаемся к весам и смещению соответствующего слоя. Как и в нашей реализации с нуля, обратите внимание, что наши оценённые параметры близки к их истинным значениям.
@d2l.add_to_class(LinearRegression) #@save
def get_w_b(self):
return (self.net.weight.data, self.net.bias.data)
w, b = model.get_w_b()
print(f'error in estimating w: {data.w - w.reshape(data.w.shape)}')
print(f'error in estimating b: {data.b - b}')
3.5.5. Итог¶
В этом разделе представлена первая реализация глубокой сети (в данной книге), позволяющая воспользоваться удобством современных фреймворков для глубокого обучения, таких как MXNet (Chen et al., 2015), JAX (Frostig et al., 2018), PyTorch (Paszke et al., 2019) и Tensorflow (Abadi et al., 2016). Мы использовали настройки по умолчанию фреймворка для загрузки данных, определения слоя, функции потерь, оптимизатора и цикла обучения. Если фреймворк предоставляет все необходимые функции, как правило, целесообразно использовать их, поскольку реализации этих компонентов в библиотеках обычно сильно оптимизированы для производительности и тщательно протестированы на надёжность. В то же время не забывайте, что эти модули могут быть реализованы непосредственно. Это особенно важно для начинающих исследователей, которые хотят находиться на переднем крае разработки моделей, где вам придется создавать новые компоненты, которых ещё нет ни в одной из существующих библиотек.
В PyTorch модуль data предоставляет инструменты для обработки
данных, а модуль nn определяет большое количество слоёв нейронных
сетей и распространённых функций потерь. Параметры можно
инициализировать, заменяя их значения методами, названия которых
заканчиваются на _. Обратите внимание, что необходимо указывать
входные размеры сети. Хотя на данный момент это кажется тривиальной
задачей, это может иметь существенные последствия при проектировании
сложных сетей с множеством слоёв. Чтобы обеспечить переносимость,
требуется тщательно продумать, как параметризовать такие сети.
3.5.6. Упражнения¶
Как нужно изменить скорость обучения, если заменить сумму потерь по мини-батчу на среднее значение потерь по мини-батчу?
Изучите документацию фреймворка, чтобы узнать, какие функции потерь в нём предусмотрены. В частности, замените квадратичную функцию потерь на робастную функцию потерь Хубера. То есть используйте функцию потерь
(3.5.1)¶\[\begin{split}l(y,y') = \begin{cases}|y-y'| -\frac{\sigma}{2} & \textrm{ if } |y-y'| > \sigma \\ \frac{1}{2 \sigma} (y-y')^2 & \textrm{ otherwise}\end{cases}\end{split}\]Как получить доступ к градиенту весов модели?
Как изменение скорости обучения и количества эпох влияет на решение? Продолжает ли оно улучшаться?
Как меняется решение при изменении количества сгенерированных данных?
Постройте график ошибки оценки для \(\hat{\mathbf{w}} - \mathbf{w}\) и \(\hat{b} - b\) в зависимости от объёма данных. Подсказка: увеличивайте объём данных логарифмически, а не линейно, то есть 5, 10, 20, 50,…, 10 000, а не 1000, 2000,…, 10 000.
Почему предложенная в подсказке рекомендация является уместной?