3.3 Линейная регрессия: краткая реализация
За последние годы благодаря большому интересу к глубокому обучению многие компании, учёные и энтузиасты создали множество зрелых открытых фреймворков. Эти фреймворки автоматизируют повторяющуюся работу, которая встречается во всех алгоритмах обучения на основе градиентов. В разделе 3.2 мы использовали только две возможности: (1) хранение данных и линейную алгебру с помощью тензоров; (2) вычисление градиентов с помощью автоматического дифференцирования. На практике же итераторы данных, функции потерь, оптимизаторы и слои нейронных сетей встречаются так часто, что современные библиотеки глубокого обучения уже реализуют все эти компоненты.
В этом разделе мы покажем, как с помощью фреймворка глубокого обучения кратко реализовать линейную регрессию из раздела 3.2.
3.3.1 Генерация набора данных
Как и в разделе 3.2, сначала сгенерируем набор данных.
MXNet:
from d2l import mxnet as d2l
from mxnet import autograd, gluon, np, npx
npx.set_np()
PyTorch:
from d2l import torch as d2l
import numpy as np
import torch
from torch.utils import data
TensorFlow:
from d2l import tensorflow as d2l
import numpy as np
import tensorflow as tf
PaddlePaddle:
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import numpy as np
import paddle
true_w = d2l.tensor([2, -3.4])
true_b = 4.2
features, labels = d2l.synthetic_data(true_w, true_b, 1000)
3.3.2 Чтение набора данных
Вместо того чтобы писать итератор самостоятельно, мы можем вызвать готовый API фреймворка для чтения данных.
Передадим в API массивы features и labels, а размер батча задаём через итератор данных.
Булев параметр is_train определяет, нужно ли итератору перемешивать данные на каждой эпохе.
MXNet:
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Построение итератора данных Gluon"""
dataset = gluon.data.ArrayDataset(*data_arrays)
return gluon.data.DataLoader(dataset, batch_size, shuffle=is_train)
PyTorch:
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Построение итератора данных PyTorch"""
dataset = data.TensorDataset(*data_arrays)
return data.DataLoader(dataset, batch_size, shuffle=is_train)
TensorFlow:
def load_array(data_arrays, batch_size, is_train=True): #@save
"""Построение итератора данных TensorFlow"""
dataset = tf.data.Dataset.from_tensor_slices(data_arrays)
if is_train:
dataset = dataset.shuffle(buffer_size=1000)
dataset = dataset.batch(batch_size)
return dataset
PaddlePaddle:
#@save
def load_array(data_arrays, batch_size, is_train=True):
"""Построение итератора данных Paddle"""
dataset = paddle.io.TensorDataset(data_arrays)
return paddle.io.DataLoader(dataset, batch_size=batch_size,
shuffle=is_train,
return_list=True)
batch_size = 10
data_iter = load_array((features, labels), batch_size)
Использовать data_iter мы будем так же, как функцию data_iter в разделе 3.2.
Чтобы проверить, что всё работает, прочитаем и выведем первый минибатч.
В отличие от раздела 3.2, здесь мы создаём итератор Python с помощью iter и получаем из него первый элемент с помощью next.
next(iter(data_iter))
3.3.3 Определение модели
В разделе 3.2 мы явно задавали параметры модели и писали вычисления через базовые операции линейной алгебры. Но когда модели становятся сложнее, а строить их приходится почти каждый день, хочется упростить эту работу. Это похоже на создание страницы блога с нуля: один раз это полезно, но если на каждый новый блог инженеру приходится тратить месяц, это неэффективно.
Для стандартных моделей глубокого обучения можно использовать готовые слои фреймворка.
Тогда остаётся только выбрать, из каких слоёв строить модель, и не нужно заботиться о деталях их реализации.
Сначала определим переменную модели net — экземпляр класса Sequential.
Класс Sequential соединяет слои последовательно: при подаче входных данных Sequential передаёт их первому слою, затем выход первого слоя становится входом второго, и так далее.
В примере ниже модель состоит из одного слоя, поэтому Sequential здесь формально не нужен.
Но почти все следующие модели будут многослойными, и уже сейчас стоит познакомиться со «стандартным конвейером».
Вспомним однослойную сеть с рис. 3.1.2. Этот слой называется полносвязным (fully-connected layer), потому что каждый его вход через матрично-векторное умножение связан с каждым выходом.
MXNet: в Gluon полносвязный слой определяется классом Dense.
Поскольку нам нужен только один скалярный выход, число выходов задаём равным 1.
Для удобства Gluon не требует указывать размерность входа для каждого слоя.
Поэтому здесь не нужно говорить, сколько входов поступает в слой.
Когда мы впервые передаём данные в модель, например при вызове net(X), Gluon сам определяет размерности входов каждого слоя.
Подробнее этот механизм будет описан ниже в этом разделе.
PyTorch: в PyTorch полносвязный слой определяется классом Linear.
Обратите внимание, что в nn.Linear передаются два аргумента.
Первый задаёт размерность входных признаков (2), второй — размерность выходных признаков, которая равна 1, так как выход — один скаляр.
TensorFlow: в Keras полносвязный слой определяется классом Dense.
Поскольку нам нужен только один скалярный выход, число выходов задаём равным 1.
Для удобства Keras не требует указывать размерность входа для каждого слоя.
Поэтому здесь не нужно говорить, сколько входов поступает в слой.
Когда мы впервые передаём данные в модель, например при вызове net(X), Keras сам определяет размерности входов каждого слоя.
Подробнее этот механизм будет описан ниже в этом разделе.
PaddlePaddle: в PaddlePaddle полносвязный слой определяется классом Linear.
Обратите внимание, что в nn.Linear передаются два аргумента.
Первый задаёт размерность входных признаков (2), второй — размерность выходных признаков, которая равна 1, так как выход — один скаляр.
# nn — сокращение от neural network (нейронная сеть)
from mxnet.gluon import nn
net = nn.Sequential()
net.add(nn.Dense(1))
# nn — сокращение от neural network (нейронная сеть)
from torch import nn
net = nn.Sequential(nn.Linear(2, 1))
# keras — высокоуровневый API TensorFlow
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1))
# nn — сокращение от neural network (нейронная сеть)
from paddle import nn
net = nn.Sequential(nn.Linear(2, 1))
3.3.4 Инициализация параметров модели
Прежде чем использовать net, нужно инициализировать параметры модели: веса и смещение линейной регрессии.
Фреймворки обычно предоставляют готовые методы инициализации.
Здесь мы задаём, что каждый вес случайно выбирается из нормального распределения со средним 0 и стандартным отклонением 0.01, а смещение инициализируется нулём.
MXNet: мы импортируем модуль initializer из MXNet, который содержит различные методы инициализации параметров.
В Gluon init — сокращённое обращение к пакету initializer.
Способ инициализации весов задаётся вызовом init.Normal(sigma=0.01).
По умолчанию смещения инициализируются нулями.
PyTorch: так же как и при создании nn.Linear, где мы задаём размерности входа и выхода, теперь мы можем напрямую обращаться к параметрам и задавать их начальные значения.
Первый слой сети выбирается как net[0], а доступ к параметрам осуществляется через атрибуты weight.data и bias.data.
Значения параметров можно также перезаписать методами с суффиксом _ — normal_ и fill_.
TensorFlow: модуль initializers в TensorFlow предоставляет различные методы инициализации параметров.
В Keras самый простой способ задать инициализацию — указать параметр kernel_initializer при создании слоя.
Здесь мы заново создаём net.
PaddlePaddle: так же как и при создании nn.Linear, где мы задаём размерности входа и выхода, теперь мы можем напрямую обращаться к параметрам и задавать их начальные значения.
Первый слой сети выбирается как net[0], а доступ к параметрам осуществляется через атрибуты weight и bias.
Метод инициализации весов задаётся вызовом nn.initializer.Normal(0, 0.01).
По умолчанию смещения инициализируются нулями.
from mxnet import init
net.initialize(init.Normal(sigma=0.01))
net[0].weight.data.normal_(0, 0.01)
net[0].bias.data.fill_(0)
initializer = tf.initializers.RandomNormal(stddev=0.01)
net = tf.keras.Sequential()
net.add(tf.keras.layers.Dense(1, kernel_initializer=initializer))
weight_attr = paddle.ParamAttr(initializer=
paddle.nn.initializer.Normal(0, 0.01))
bias_attr = paddle.ParamAttr(initializer=None)
net = nn.Sequential(nn.Linear(2, 1, weight_attr=weight_attr,
bias_attr=bias_attr))
MXNet: код выше может показаться простым, но здесь есть одна важная деталь. Мы инициализируем параметры сети, хотя Gluon ещё не знает, сколько входов будет у модели. Вход может иметь 2 признака, а может и 2000. Gluon обходит эту проблему: инициализация на самом деле выполняется отложенно (deferred) — только тогда, когда мы впервые передаём данные через сеть. Заметьте, что до этого параметры не инициализированы, поэтому обращаться к ним или изменять их нельзя.
TensorFlow: код выше может показаться простым, но здесь есть одна важная деталь. Мы инициализируем параметры сети, хотя Keras ещё не знает, сколько входов будет у модели. Вход может иметь 2 признака, а может и 2000. Keras обходит эту проблему: инициализация выполняется отложенно (deferred) — только тогда, когда мы впервые передаём данные через сеть. Заметьте, что до этого параметры не инициализированы, поэтому обращаться к ним или изменять их нельзя.
3.3.5 Определение функции потерь
MXNet: в Gluon модуль loss содержит различные функции потерь.
В этом примере мы используем среднеквадратичную ошибку Gluon (L2Loss).
PyTorch: среднеквадратичная ошибка вычисляется классом MSELoss, также известным как квадрат -нормы.
По умолчанию он возвращает среднее значение потерь по всем примерам.
TensorFlow: среднеквадратичная ошибка вычисляется классом MeanSquaredError, также известным как квадрат -нормы.
По умолчанию он возвращает среднее значение потерь по всем примерам.
PaddlePaddle: среднеквадратичная ошибка вычисляется классом MSELoss, также известным как квадрат -нормы.
По умолчанию он возвращает среднее значение потерь по всем примерам.
loss = gluon.loss.L2Loss()
loss = nn.MSELoss()
loss = tf.keras.losses.MeanSquaredError()
loss = nn.MSELoss()
3.3.6 Определение алгоритма оптимизации
Минибатчевый стохастический градиентный спуск — стандартный инструмент для оптимизации нейронных сетей. Фреймворки реализуют множество его вариантов.
MXNet: Gluon реализует этот алгоритм и его варианты через класс Trainer.
При создании Trainer мы указываем параметры, которые нужно оптимизировать (их можно получить из модели net через net.collect_params()), алгоритм оптимизации (sgd) и словарь гиперпараметров, необходимых алгоритму.
Для минибатчевого стохастического градиентного спуска достаточно задать learning_rate; здесь он равен 0.03.
PyTorch: минибатчевый стохастический градиентный спуск — стандартный инструмент для оптимизации нейронных сетей, а PyTorch реализует множество его вариантов в модуле optim.
Когда мы создаём экземпляр SGD, мы указываем параметры для оптимизации (их можно получить из модели через net.parameters()) и словарь гиперпараметров, нужных алгоритму.
Для минибатчевого стохастического градиентного спуска достаточно задать lr; здесь он равен 0.03.
TensorFlow: минибатчевый стохастический градиентный спуск — стандартный инструмент для оптимизации нейронных сетей, а Keras реализует множество его вариантов в модуле optimizers.
Для минибатчевого стохастического градиентного спуска достаточно задать learning_rate; здесь он равен 0.03.
PaddlePaddle: минибатчевый стохастический градиентный спуск — стандартный инструмент для оптимизации нейронных сетей, а PaddlePaddle реализует множество его вариантов в модуле optimizer.
Для минибатчевого стохастического градиентного спуска достаточно задать learning_rate; здесь он равен 0.03.
from mxnet import gluon
trainer = gluon.Trainer(net.collect_params(), 'sgd', {'learning_rate': 0.03})
trainer = torch.optim.SGD(net.parameters(), lr=0.03)
trainer = tf.keras.optimizers.SGD(learning_rate=0.03)
trainer = paddle.optimizer.SGD(learning_rate=0.03,
parameters=net.parameters())
3.3.7 Обучение
Реализовать модель с помощью высокоуровневого API фреймворка можно достаточно коротким кодом. Нам не нужно отдельно выделять параметры, задавать функцию потерь или вручную реализовывать минибатчевый стохастический градиентный спуск. По мере усложнения моделей преимущества высокоуровневого API становятся всё заметнее. Когда все базовые компоненты готовы, код обучения очень похож на тот, что мы писали с нуля.
Напомним: в каждой эпохе мы полностью проходим набор данных (train_data) и последовательно получаем из него минибатчи входов и соответствующих меток.
Для каждого минибатча выполняем следующие шаги:
- вызываем
net(X), получаем предсказание и вычисляем потериl(прямое распространение); - вычисляем градиенты с помощью обратного распространения ошибки;
- обновляем параметры модели, вызывая оптимизатор.
Чтобы оценить качество обучения, после каждой эпохи вычисляем потери на всём наборе данных и выводим их для контроля процесса.
MXNet:
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
with autograd.record():
l = loss(net(X), y)
l.backward()
trainer.step(batch_size)
l = loss(net(features), labels)
print(f'эпоха {epoch + 1}, потери {l.mean().asnumpy():f}')
PyTorch:
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
l = loss(net(X) ,y)
trainer.zero_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'эпоха {epoch + 1}, потери {l:f}')
TensorFlow:
num_epochs = 3
for epoch in range(num_epochs):
for X, y in data_iter:
with tf.GradientTape() as tape:
l = loss(net(X, training=True), y)
grads = tape.gradient(l, net.trainable_variables)
trainer.apply_gradients(zip(grads, net.trainable_variables))
l = loss(net(features), labels)
print(f'эпоха {epoch + 1}, потери {l:f}')
PaddlePaddle:
num_epochs = 3
for epoch in range(num_epochs):
for i,(X, y) in enumerate (data_iter()):
l = loss(net(X) ,y)
trainer.clear_grad()
l.backward()
trainer.step()
l = loss(net(features), labels)
print(f'эпоха {epoch + 1},'f'потери {l}')
Теперь сравним истинные параметры, использованные при генерации набора данных, с параметрами модели, полученными в результате обучения на ограниченных данных.
Чтобы получить параметры, обращаемся к нужному слою net, а затем читаем его веса и смещение.
Как и в разделе 3.2, оцененные параметры оказываются очень близки к истинным.
MXNet:
w = net[0].weight.data()
print(f'Ошибка оценки w: {true_w - d2l.reshape(w, true_w.shape)}')
b = net[0].bias.data()
print(f'Ошибка оценки b: {true_b - b}')
PyTorch:
w = net[0].weight.data
print('Ошибка оценки w:', true_w - d2l.reshape(w, true_w.shape))
b = net[0].bias.data
print('Ошибка оценки b:', true_b - b)
TensorFlow:
w = net.get_weights()[0]
print('Ошибка оценки w:', true_w - d2l.reshape(w, true_w.shape))
b = net.get_weights()[1]
print('Ошибка оценки b:', true_b - b)
PaddlePaddle:
w = net[0].weight
print('Ошибка оценки w:', true_w - w.reshape(true_w.shape))
b = net[0].bias
print('Ошибка оценки b:', true_b - b)
Резюме
MXNet:
- Модель можно реализовать в Gluon более кратко.
- В Gluon модуль
dataсодержит инструменты для обработки данных, модульnn— большое число слоёв нейронных сетей, а модульloss— многие распространённые функции потерь. - Модуль
initializerв MXNet предоставляет различные методы инициализации параметров модели. - Размерности и хранение данных определяются автоматически, но не следует пытаться обращаться к параметрам до их инициализации.
PyTorch:
- Модель можно реализовать в PyTorch с помощью высокоуровневого API более кратко.
- В PyTorch модуль
dataсодержит инструменты для обработки данных, а модульnn— большое число слоёв нейронных сетей и распространённых функций потерь. - Параметры можно инициализировать, заменяя их значения методами с суффиксом
_.
TensorFlow:
- Модель можно реализовать в TensorFlow с помощью высокоуровневого API более кратко.
- В TensorFlow модуль
dataсодержит инструменты для обработки данных, а модульkeras— большое число слоёв нейронных сетей и распространённых функций потерь. - Модуль
initializersв TensorFlow предоставляет различные методы инициализации параметров модели. - Размерности и хранение данных определяются автоматически, но не следует пытаться обращаться к параметрам до их инициализации.
Упражнения
- Как нужно изменить скорость обучения, если вместо суммарных потерь минибатча использовать среднее значение потерь по минибатчу?
- Посмотрите документацию фреймворков: какие функции потерь и методы инициализации они предоставляют? Замените исходную функцию потерь на функцию Хубера (Huber loss):
- Как получить доступ к градиентам линейной регрессии?
MXNet: Обсуждение
PyTorch: Обсуждение
TensorFlow: Обсуждение
PaddlePaddle: Обсуждение