.. _sec_mlp-implementation: Реализация многослойных перцептронов ==================================== Многослойные перцептроны (MLP) реализуются не намного сложнее, чем простые линейные модели. ⟦key⟧ концептуальное отличие заключается в том, что мы теперь объединяем несколько слоёв. .. raw:: latex \diilbookstyleinputcell .. code:: python import torch from torch import nn from d2l import torch as d2l Реализация с нуля ----------------- Давайте снова реализуем такую сеть с нуля. Инициализация параметров модели ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Напомним, что набор данных Fashion-MNIST содержит 10 классов, а каждое изображение представляет собой сетку значений пикселей в градациях серого цвета :math:`28 \times 28 = 784`. Как и ранее, мы пока будем игнорировать пространственную структуру между пикселями, поэтому можем рассматривать эту задачу как классификацию с 784 входными признаками и 10 классами. Для начала мы реализуем многослойный перцептрон с одним скрытым слоем и 256 скрытыми единицами. Как количество слоёв, так и их ширина являются настраиваемыми параметрами (гиперпараметрами). Обычно мы выбираем ширину слоёв, кратной большим степеням двойки. Это обеспечивает вычислительную эффективность благодаря способу выделения и адресации памяти в аппаратном обеспечении. Снова мы будем представлять параметры с помощью нескольких тензоров. Обратите внимание, что *для каждого слоя* необходимо отслеживать одну матрицу весов и один вектор смещения. Как всегда, мы выделяем память для градиентов функции потерь по этим параметрам. В приведённом ниже коде мы используем ``nn.Parameter`` для автоматической регистрации атрибута класса в качестве параметра, отслеживаемого ``autograd`` (:numref:`sec_autograd`). .. raw:: latex \diilbookstyleinputcell .. code:: python 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)) Модель ~~~~~~ Чтобы убедиться, что мы понимаем, как всё работает, мы **сами реализуем функцию активации ReLU**, а не будем напрямую вызывать встроенную ``relu``. .. raw:: latex \diilbookstyleinputcell .. code:: python def relu(X): a = torch.zeros_like(X) return torch.max(X, a) Поскольку мы игнорируем пространственную структуру, мы ``reshape`` каждое двумерное изображение в плоский вектор длины ``num_inputs``. Наконец, мы реализуем нашу модель всего несколькими строками кода. Поскольку мы используем встроенный в фреймворк механизм автоматического дифференцирования, этого достаточно. .. raw:: latex \diilbookstyleinputcell .. code:: python @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 Обучение ~~~~~~~~ К счастью, **цикл обучения многослойных перцептронов полностью совпадает с циклом для софтмакс-регрессии.** Мы определяем модель, данные и алгоритм обучения, а затем вызываем метод ``fit`` для модели и данных. .. raw:: latex \diilbookstyleinputcell .. code:: python 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) Краткая реализация ------------------ Как и следовало ожидать, используя высокоуровневые API, мы можем реализовать многослойные перцептроны ещё более лаконично. Модель ~~~~~~ По сравнению с нашей лаконичной реализацией софтмакс-регрессии (:numref:`sec_softmax_concise`), единственное отличие заключается в том, что мы добавляем *два* полносвязных слоя, тогда как ранее добавляли только *один*. Первый — это скрытый слой, второй — выходной слой. .. raw:: latex \diilbookstyleinputcell .. code:: python 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`` (:numref:`subsec_oo-design-models`), который просто вызывает ``self.net(X)`` (``X`` является входом), определённую теперь как последовательность преобразований через класс ``Sequential``. Класс ``Sequential`` абстрагирует прямой проход, позволяя нам сосредоточиться на преобразованиях. Мы подробнее обсудим, как работает класс ``Sequential`` в :numref:`subsec_model-construction-sequential`. Обучение ~~~~~~~~ **Цикл обучения** полностью аналогичен тому, что мы использовали при реализации софтмакс-регрессии. Такая модульность позволяет отделить вопросы, связанные с архитектурой модели, от других, независимых аспектов. .. raw:: latex \diilbookstyleinputcell .. code:: python model = MLP(num_outputs=10, num_hiddens=256, lr=0.1) trainer.fit(model, data) **Итоги** --------- Теперь, когда у нас накопился опыт проектирования глубоких сетей, переход от одной к нескольким слоям уже не кажется такой сложной задачей. В частности, мы можем повторно использовать алгоритм обучения и загрузчик данных. Однако реализация многослойных перцептронов (MLP) с нуля всё же сопряжена с трудностями: необходимость именования и отслеживания параметров модели затрудняет расширение архитектуры. Например, представьте, что вам нужно вставить ещё один слой между слоями 42 и 43. Этот слой может получить имя 42b, если вы не готовы выполнять последовательное переименование всех последующих слоёв. Кроме того, при реализации сети с нуля фреймворку гораздо сложнее выполнять эффективные оптимизации производительности. Тем не менее, на данный момент мы достигли уровня развития, характерного для конца 1980-х годов, когда полносвязные глубокие сети были основным методом моделирования в нейронных сетях. ⟦N⟧ Наш следующим концептуальным шагом будет рассмотрение изображений. ⟦N⟧ Прежде чем мы перейдём к этому, нам необходимо повторить ряд статистических основ и деталей, касающихся эффективного вычисления моделей. ⟦N⟧ Упражнения ---------- 1. Измените количество скрытых единиц ``num_hiddens`` и постройте график, показывающий, как это значение влияет на точность модели. Какое значение этого гиперпараметра является наилучшим? 2. Попробуйте добавить скрытый слой и посмотрите, как это повлияет на результаты. 3. Почему это плохая идея — добавлять скрытый слой с одним нейроном? Что может пойти не так? 4. Как изменение скорости обучения влияет на результаты? При фиксированных остальных параметрах, какая скорость обучения дает наилучшие результаты? Как это связано с количеством эпох? 5. Давайте оптимизируем все гиперпараметры совместно, а именно скорость обучения, количество эпох, число скрытых слоёв и число скрытых нейронов в каждом слое. 1. Какой наилучший результат можно получить, оптимизируя их все? 2. Почему работать с несколькими гиперпараметрами значительно сложнее? 3. Опишите эффективную стратегию совместной оптимизации нескольких параметров. 6. Сравните скорость работы фреймворка и реализации, написанной с нуля, на сложной задаче. Как она меняется при увеличении сложности сети? 7. Измерьте скорость умножения тензоров на матрицы для хорошо выровненных и невыровненных матриц. Например, протестируйте матрицы размерности 1024, 1025, 1026, 1028 и 1032. 1. Как это различается на GPU и CPU? 2. Определите ширину шины памяти вашего процессора и графического процессора. 8. Попробуйте различные функции активации. Какая из них работает лучше всего? 9. Есть ли разница между инициализациями весов сети? Имеет ли это значение? `Обсуждения `__