3.2 Линейная регрессия с нуля
Разобравшись с основными идеями линейной регрессии, можно перейти к её реализации в коде. В этом разделе мы реализуем весь метод с нуля: конвейер данных, модель, функцию потерь и оптимизатор — стохастический градиентный спуск по минибатчам. Современные фреймворки глубокого обучения почти полностью автоматизируют эту работу, но реализация с нуля гарантирует, что мы понимаем, что происходит. Кроме того, знание внутреннего устройства пригодится, когда понадобится самостоятельно изменить модель, слой или функцию потерь. В этом разделе мы используем только тензоры и автоматическое дифференцирование. В последующих главах мы будем опираться на возможности фреймворков и рассмотрим более краткие реализации.
MXNet:
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
import random
npx.set_np()
PyTorch:
%matplotlib inline
from d2l import torch as d2l
import torch
import random
TensorFlow:
%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf
import random
PaddlePaddle:
%matplotlib inline
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import random
import paddle
3.2.1 Генерация набора данных
Для простоты создадим искусственный набор данных на основе линейной модели с шумом. Наша задача — по этому конечному набору восстановить параметры модели. Мы будем использовать данные небольшой размерности, чтобы их было легко визуализировать. В приведённом ниже коде создаётся набор из 1000 примеров, каждый из которых содержит 2 признака, выбранных из стандартного нормального распределения. Синтетический набор данных представляет собой матрицу .
Метки и данные создадим по линейной модели с параметрами , и шумовым членом :
Величину можно рассматривать как возможную ошибку наблюдения при измерении меток. Здесь мы считаем выполненными стандартные предположения: распределена нормально со средним 0. Для простоты стандартное отклонение положим равным 0.01. Код ниже генерирует синтетический набор данных.
MXNet, PyTorch, PaddlePaddle:
def synthetic_data(w, b, num_examples): #@save
"""Генерация y = Xw + b + шум"""
X = d2l.normal(0, 1, (num_examples, len(w)))
y = d2l.matmul(X, w) + b
y += d2l.normal(0, 0.01, y.shape)
return X, d2l.reshape(y, (-1, 1))
TensorFlow:
def synthetic_data(w, b, num_examples): #@save
"""Генерация y = Xw + b + шум"""
X = d2l.zeros((num_examples, w.shape[0]))
X += tf.random.normal(shape=X.shape)
y = d2l.matmul(X, tf.reshape(w, (-1, 1))) + b
y += tf.random.normal(shape=y.shape, stddev=0.01)
y = d2l.reshape(y, (-1, 1))
return X, y
true_w = d2l.tensor([2, -3.4])
true_b = 4.2
features, labels = synthetic_data(true_w, true_b, 1000)
Обратите внимание: каждая строка features содержит двумерный пример данных, а каждая строка labels — одномерное значение метки (скаляр).
print('features:', features[0],'\nlabel:', labels[0])
Чтобы увидеть линейную зависимость между признаками и метками, построим диаграмму рассеяния второго признака features[:, 1] и labels.
d2l.set_figsize()
d2l.plt.scatter(d2l.numpy(features[:, 1]), d2l.numpy(labels), 1);
3.2.2 Чтение набора данных
Напомним, что при обучении модели мы проходим по набору данных, на каждом шаге берём небольшой минибатч и используем его для обновления модели. Поскольку этот процесс лежит в основе обучения алгоритмов машинного обучения, стоит определить функцию, которая перемешивает примеры и выдаёт их минибатчами.
В приведённом ниже коде определим функцию data_iter, которая принимает размер батча, матрицу признаков и вектор меток, а возвращает минибатчи размера batch_size.
Каждый минибатч содержит набор признаков и меток.
MXNet, PyTorch, PaddlePaddle:
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# Примеры читаются в случайном порядке, без определённой последовательности
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
batch_indices = d2l.tensor(
indices[i: min(i + batch_size, num_examples)])
yield features[batch_indices], labels[batch_indices]
TensorFlow:
def data_iter(batch_size, features, labels):
num_examples = len(features)
indices = list(range(num_examples))
# Примеры читаются в случайном порядке, без определённой последовательности
random.shuffle(indices)
for i in range(0, num_examples, batch_size):
j = tf.constant(indices[i: min(i + batch_size, num_examples)])
yield tf.gather(features, j), tf.gather(labels, j)
Обычно мы используем преимущество параллельных вычислений на GPU и обрабатываем минибатчи разумного размера. Каждый пример можно обрабатывать параллельно с остальными, и градиенты функции потерь для каждого примера тоже вычисляются параллельно. GPU может обработать несколько сотен примеров почти за то же время, что и один.
Чтобы почувствовать работу с минибатчами, прочитаем первый минибатч и выведем его.
Форма минибатча признаков показывает размер батча и число входных признаков.
Аналогично, форма батча меток совпадает с batch_size.
batch_size = 10
for X, y in data_iter(batch_size, features, labels):
print(X, '\n', y)
break
При итерации мы последовательно получаем разные минибатчи, пока не пройдём весь набор данных. Реализованная выше итерация хорошо подходит для учебных целей, но работает неэффективно и на реальных задачах может создать проблемы. Например, она требует загрузить все данные в память и выполнять много случайных обращений к памяти. Встроенные в фреймворки итераторы работают значительно эффективнее: они умеют читать данные из файлов и принимать данные из потоков.
3.2.3 Инициализация параметров модели
Прежде чем начинать оптимизацию параметров модели методом минибатчевого стохастического градиентного спуска, нам нужны начальные значения параметров. В приведённом коде веса инициализируются случайными числами из нормального распределения со средним 0 и стандартным отклонением 0.01, а смещение — нулём.
MXNet:
w = np.random.normal(0, 0.01, (2, 1))
b = np.zeros(1)
w.attach_grad()
b.attach_grad()
PyTorch:
w = torch.normal(0, 0.01, size=(2,1), requires_grad=True)
b = torch.zeros(1, requires_grad=True)
TensorFlow:
w = tf.Variable(tf.random.normal(shape=(2, 1), mean=0, stddev=0.01),
trainable=True)
b = tf.Variable(tf.zeros(1), trainable=True)
PaddlePaddle:
w = d2l.normal(0, 0.01, shape=(2,1))
b = d2l.zeros(shape=[1])
# w и b — создаваемые параметры модели; stop_gradient по умолчанию равен True, то есть градиент не вычисляется, поэтому его нужно установить в False
w.stop_gradient = False
b.stop_gradient = False
После инициализации параметров наша задача — обновлять их до тех пор, пока они не станут достаточно хорошо описывать данные. На каждом шаге нужно вычислять градиент функции потерь по параметрам модели. Зная градиент, мы можем сдвинуть каждый параметр в направлении уменьшения потерь. Вычислять градиенты вручную утомительно и легко ошибиться, поэтому никто этого не делает. Для вычисления градиентов мы используем автоматическое дифференцирование, введённое в разделе 2.5.
3.2.4 Определение модели
Далее определим модель, которая связывает входные данные и параметры с выходом модели. Напомним, что для вычисления выхода линейной модели достаточно умножить матрицу признаков на вектор весов и прибавить смещение . Обратите внимание: — вектор, а — скаляр. Как описано в разделе 2.1.3 о broadcasting, при сложении вектора со скаляром скаляр прибавляется к каждому элементу вектора.
def linreg(X, w, b): #@save
"""Модель линейной регрессии"""
return d2l.matmul(X, w) + b
3.2.5 Определение функции потерь
Поскольку нам нужен градиент функции потерь, сначала определим её.
Здесь используем квадратичную функцию потерь, описанную в разделе 3.1.
В реализации нужно привести форму истинных значений y к форме предсказаний y_hat.
def squared_loss(y_hat, y): #@save
"""Квадратичные потери"""
return (y_hat - d2l.reshape(y, y_hat.shape)) ** 2 / 2
3.2.6 Определение алгоритма оптимизации
Как мы обсуждали в разделе 3.1, у линейной регрессии есть аналитическое решение. Однако у других моделей, которые встретятся в книге, аналитического решения нет. Поэтому здесь мы используем минибатчевый стохастический градиентный спуск.
На каждом шаге берём минибатч, случайно выбранный из набора данных, вычисляем градиент потерь по параметрам и сдвигаем параметры в направлении уменьшения потерь.
Функция ниже реализует обновление минибатчевым стохастическим градиентным спуском.
Она принимает набор параметров модели, скорость обучения и размер батча.
Величина шага определяется скоростью обучения lr.
Поскольку потери, которые мы вычисляем, — это сумма по минибатчу, мы нормируем шаг на размер батча (batch_size), чтобы размер шага не зависел от выбранного размера батча.
MXNet:
def sgd(params, lr, batch_size): #@save
"""Минибатчевый стохастический градиентный спуск"""
for param in params:
param[:] = param - lr * param.grad / batch_size
PyTorch:
def sgd(params, lr, batch_size): #@save
"""Минибатчевый стохастический градиентный спуск"""
with torch.no_grad():
for param in params:
param -= lr * param.grad / batch_size
param.grad.zero_()
TensorFlow:
def sgd(params, grads, lr, batch_size): #@save
"""Минибатчевый стохастический градиентный спуск"""
for param, grad in zip(params, grads):
param.assign_sub(lr*grad/batch_size)
PaddlePaddle:
#@save
def sgd(params, lr, batch_size):
"""Минибатчевый стохастический градиентный спуск"""
with paddle.no_grad():
for i, param in enumerate(params):
param -= lr * params[i].grad / batch_size
params[i].set_value(param)
params[i].clear_gradient()
3.2.7 Обучение
Теперь у нас есть все элементы, необходимые для обучения модели, и можно реализовать основную часть — процесс обучения.
Понимание этого кода очень важно: после начала работы с глубоким обучением почти один и тот же процесс будет повторяться снова и снова.
На каждой итерации мы берём минибатч обучающих примеров, пропускаем его через модель и получаем предсказания.
После вычисления потерь запускаем обратное распространение ошибки и сохраняем градиенты параметров.
Наконец, вызываем оптимизатор sgd, чтобы обновить параметры модели.
Кратко весь процесс выглядит так:
- инициализировать параметры;
- повторять обучение, пока оно не завершится:
- вычислить градиент ;
- обновить параметры .
В течение каждой эпохи (epoch) функция data_iter проходит весь набор данных, и каждый обучающий пример используется один раз (предполагается, что число примеров делится на размер батча).
Число эпох num_epochs и скорость обучения lr — гиперпараметры; здесь они равны 3 и 0.03 соответственно.
Подбор гиперпараметров — непростая задача, обычно он требует многих проб и ошибок.
Сейчас мы оставим эти детали в стороне; подробно о них пойдёт речь в главе 11.
lr = 0.03
num_epochs = 3
net = linreg
loss = squared_loss
MXNet:
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
with autograd.record():
l = loss(net(X, w, b), y) # Потери на минибатче X и y
# Вычислить градиент l по [w, b]
l.backward()
sgd([w, b], lr, batch_size) # Обновить параметры по их градиентам
train_l = loss(net(features, w, b), labels)
print(f'эпоха {epoch + 1}, потери {float(train_l.mean()):f}')
PyTorch:
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # Потери на минибатче X и y
# Поскольку форма l равна (batch_size, 1), а не скаляр, все элементы l
# складываются, и по полученной сумме вычисляется градиент по [w, b]
l.sum().backward()
sgd([w, b], lr, batch_size) # Обновить параметры по их градиентам
with torch.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'эпоха {epoch + 1}, потери {float(train_l.mean()):f}')
TensorFlow:
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
with tf.GradientTape() as g:
l = loss(net(X, w, b), y) # Потери на минибатче X и y
# Вычислить градиент l по [w, b]
dw, db = g.gradient(l, [w, b])
# Обновить параметры по их градиентам
sgd([w, b], [dw, db], lr, batch_size)
train_l = loss(net(features, w, b), labels)
print(f'эпоха {epoch + 1}, потери {float(tf.reduce_mean(train_l)):f}')
PaddlePaddle:
for epoch in range(num_epochs):
for X, y in data_iter(batch_size, features, labels):
l = loss(net(X, w, b), y) # Потери на минибатче X и y
# Поскольку форма l равна (batch_size, 1), а не скаляр, все элементы l
# складываются, и по полученной сумме вычисляется градиент по [w, b]
l.sum().backward()
sgd([w, b], lr, batch_size) # Обновить параметры по их градиентам
with paddle.no_grad():
train_l = loss(net(features, w, b), labels)
print(f'эпоха {epoch + 1}, потери {float(train_l.mean()):f}')
Поскольку мы сгенерировали данные сами, нам известны истинные параметры. Поэтому можно сравнить истинные параметры с параметрами, найденными в ходе обучения, чтобы оценить, насколько успешно прошло обучение. На практике найденные параметры действительно оказываются очень близкими к истинным.
print(f'Ошибка оценки w: {true_w - d2l.reshape(w, true_w.shape)}')
print(f'Ошибка оценки b: {true_b - b}')
Не стоит считать, что параметры всегда восстанавливаются идеально. В машинном обучении нас обычно интересует не точное восстановление истинных параметров, а то, насколько точно модель предсказывает. К счастью, даже в сложных задачах оптимизации стохастический градиентный спуск обычно находит очень хорошие решения. Одна из причин в том, что в глубоких сетях существует много комбинаций параметров, дающих очень точные предсказания.
Резюме
- Мы изучили, как реализовать и оптимизировать глубокую сеть. При этом использовались только тензоры и автоматическое дифференцирование: не нужно было ни определять слои, ни писать сложные оптимизаторы.
- Этот раздел лишь затронул основы. В следующих разделах мы опишем другие модели на основе введённых понятий и научимся реализовывать их более кратко.
Упражнения
- Что произойдёт, если инициализировать веса нулями? Будет ли алгоритм по-прежнему работать?
- Предположим, мы хотим построить модель зависимости напряжения от тока. Можно ли использовать автоматическое дифференцирование для обучения параметров модели?
- Можно ли, опираясь на закон Планка, по спектральной плотности энергии излучения определить температуру объекта?
- Какие проблемы могут возникнуть при вычислении производных второго порядка? Как их можно решить?
- Зачем в функции
squared_lossнужна операцияreshape? - Попробуйте разные скорости обучения и посмотрите, как быстро уменьшается значение функции потерь.
- Как изменится поведение
data_iter, если число примеров не делится на размер батча?
MXNet: Обсуждение
PyTorch: Обсуждение
TensorFlow: Обсуждение
PaddlePaddle: Обсуждение