5.2. Реализация многослойных перцептронов¶
Многослойные перцептроны (MLP) реализуются не намного сложнее, чем простые линейные модели. ⟦key⟧ концептуальное отличие заключается в том, что мы теперь объединяем несколько слоёв.
import torch
from torch import nn
from d2l import torch as d2l
5.2.1. Реализация с нуля¶
Давайте снова реализуем такую сеть с нуля.
5.2.1.1. Инициализация параметров модели¶
Напомним, что набор данных Fashion-MNIST содержит 10 классов, а каждое изображение представляет собой сетку значений пикселей в градациях серого цвета \(28 \times 28 = 784\). Как и ранее, мы пока будем игнорировать пространственную структуру между пикселями, поэтому можем рассматривать эту задачу как классификацию с 784 входными признаками и 10 классами. Для начала мы реализуем многослойный перцептрон с одним скрытым слоем и 256 скрытыми единицами. Как количество слоёв, так и их ширина являются настраиваемыми параметрами (гиперпараметрами). Обычно мы выбираем ширину слоёв, кратной большим степеням двойки. Это обеспечивает вычислительную эффективность благодаря способу выделения и адресации памяти в аппаратном обеспечении.
Снова мы будем представлять параметры с помощью нескольких тензоров. Обратите внимание, что для каждого слоя необходимо отслеживать одну матрицу весов и один вектор смещения. Как всегда, мы выделяем память для градиентов функции потерь по этим параметрам.
В приведённом ниже коде мы используем nn.Parameter для
автоматической регистрации атрибута класса в качестве параметра,
отслеживаемого autograd (Section 2.5).
class MLPScratch(d2l.Classifier):
def __init__(self, num_inputs, num_outputs, num_hiddens, lr, sigma=0.01):
super().__init__()
self.save_hyperparameters()
self.W1 = nn.Parameter(torch.randn(num_inputs, num_hiddens) * sigma)
self.b1 = nn.Parameter(torch.zeros(num_hiddens))
self.W2 = nn.Parameter(torch.randn(num_hiddens, num_outputs) * sigma)
self.b2 = nn.Parameter(torch.zeros(num_outputs))
5.2.1.2. Модель¶
Чтобы убедиться, что мы понимаем, как всё работает, мы сами реализуем
функцию активации ReLU, а не будем напрямую вызывать встроенную
relu.
def relu(X):
a = torch.zeros_like(X)
return torch.max(X, a)
Поскольку мы игнорируем пространственную структуру, мы reshape
каждое двумерное изображение в плоский вектор длины num_inputs.
Наконец, мы реализуем нашу модель всего несколькими строками кода.
Поскольку мы используем встроенный в фреймворк механизм автоматического
дифференцирования, этого достаточно.
@d2l.add_to_class(MLPScratch)
def forward(self, X):
X = X.reshape((-1, self.num_inputs))
H = relu(torch.matmul(X, self.W1) + self.b1)
return torch.matmul(H, self.W2) + self.b2
5.2.1.3. Обучение¶
К счастью, цикл обучения многослойных перцептронов полностью совпадает
с циклом для софтмакс-регрессии. Мы определяем модель, данные и
алгоритм обучения, а затем вызываем метод fit для модели и данных.
model = MLPScratch(num_inputs=784, num_outputs=10, num_hiddens=256, lr=0.1)
data = d2l.FashionMNIST(batch_size=256)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)
5.2.2. Краткая реализация¶
Как и следовало ожидать, используя высокоуровневые API, мы можем реализовать многослойные перцептроны ещё более лаконично.
5.2.2.1. Модель¶
По сравнению с нашей лаконичной реализацией софтмакс-регрессии (Section 4.5), единственное отличие заключается в том, что мы добавляем два полносвязных слоя, тогда как ранее добавляли только один. Первый — это скрытый слой, второй — выходной слой.
class MLP(d2l.Classifier):
def __init__(self, num_outputs, num_hiddens, lr):
super().__init__()
self.save_hyperparameters()
self.net = nn.Sequential(nn.Flatten(), nn.LazyLinear(num_hiddens),
nn.ReLU(), nn.LazyLinear(num_outputs))
Ранее мы определили forward методы для преобразования входных данных
с помощью параметров модели. Эти операции представляют собой конвейер:
вы берете входные данные и применяете преобразование (например,
матричное умножение с весами, за которым следует добавление смещения), а
затем используете результат текущего преобразования в качестве входных
данных для следующего. Однако вы могли заметить, что здесь не определен
метод forward. На самом деле, MLP наследует метод forward от
класса Module (Section 3.2.2), который просто
вызывает self.net(X) (X является входом), определённую теперь
как последовательность преобразований через класс Sequential. Класс
Sequential абстрагирует прямой проход, позволяя нам сосредоточиться
на преобразованиях. Мы подробнее обсудим, как работает класс
Sequential в Section 6.1.2.
5.2.2.2. Обучение¶
Цикл обучения полностью аналогичен тому, что мы использовали при реализации софтмакс-регрессии. Такая модульность позволяет отделить вопросы, связанные с архитектурой модели, от других, независимых аспектов.
model = MLP(num_outputs=10, num_hiddens=256, lr=0.1)
trainer.fit(model, data)
5.2.3. Итоги¶
Теперь, когда у нас накопился опыт проектирования глубоких сетей, переход от одной к нескольким слоям уже не кажется такой сложной задачей. В частности, мы можем повторно использовать алгоритм обучения и загрузчик данных. Однако реализация многослойных перцептронов (MLP) с нуля всё же сопряжена с трудностями: необходимость именования и отслеживания параметров модели затрудняет расширение архитектуры. Например, представьте, что вам нужно вставить ещё один слой между слоями 42 и 43. Этот слой может получить имя 42b, если вы не готовы выполнять последовательное переименование всех последующих слоёв. Кроме того, при реализации сети с нуля фреймворку гораздо сложнее выполнять эффективные оптимизации производительности.
Тем не менее, на данный момент мы достигли уровня развития, характерного для конца 1980-х годов, когда полносвязные глубокие сети были основным методом моделирования в нейронных сетях. ⟦N⟧ Наш следующим концептуальным шагом будет рассмотрение изображений. ⟦N⟧ Прежде чем мы перейдём к этому, нам необходимо повторить ряд статистических основ и деталей, касающихся эффективного вычисления моделей. ⟦N⟧
5.2.4. Упражнения¶
Измените количество скрытых единиц
num_hiddensи постройте график, показывающий, как это значение влияет на точность модели. Какое значение этого гиперпараметра является наилучшим?Попробуйте добавить скрытый слой и посмотрите, как это повлияет на результаты.
Почему это плохая идея — добавлять скрытый слой с одним нейроном? Что может пойти не так?
Как изменение скорости обучения влияет на результаты? При фиксированных остальных параметрах, какая скорость обучения дает наилучшие результаты? Как это связано с количеством эпох?
Давайте оптимизируем все гиперпараметры совместно, а именно скорость обучения, количество эпох, число скрытых слоёв и число скрытых нейронов в каждом слое.
Какой наилучший результат можно получить, оптимизируя их все?
Почему работать с несколькими гиперпараметрами значительно сложнее?
Опишите эффективную стратегию совместной оптимизации нескольких параметров.
Сравните скорость работы фреймворка и реализации, написанной с нуля, на сложной задаче. Как она меняется при увеличении сложности сети?
Измерьте скорость умножения тензоров на матрицы для хорошо выровненных и невыровненных матриц. Например, протестируйте матрицы размерности 1024, 1025, 1026, 1028 и 1032.
Как это различается на GPU и CPU?
Определите ширину шины памяти вашего процессора и графического процессора.
Попробуйте различные функции активации. Какая из них работает лучше всего?
Есть ли разница между инициализациями весов сети? Имеет ли это значение?