4.5. Краткая реализация софтмакс-регрессии¶
Как и в случае с линейной регрессией (см. Section 3.5), высокоуровневые фреймворки глубокого обучения значительно упрощают реализацию здесь.
import torch
from torch import nn
from torch.nn import functional as F
from d2l import torch as d2l
4.5.1. Определение модели¶
Как и в Section 3.5, мы создаём полносвязный слой с
помощью встроенного слоя. Затем встроенный метод __call__ вызывает
forward всякий раз, когда необходимо применить сеть к входным
данным.
Мы используем слой Flatten для преобразования тензора четвёртого
порядка X в тензор второго порядка, сохраняя размерность по первой
оси неизменной.
class SoftmaxRegression(d2l.Classifier): #@save
"""The softmax regression model."""
def __init__(self, num_outputs, lr):
super().__init__()
self.save_hyperparameters()
self.net = nn.Sequential(nn.Flatten(),
nn.LazyLinear(num_outputs))
def forward(self, X):
return self.net(X)
4.5.2. Софтмакс: повторное рассмотрение¶
В Section 4.4 мы вычислили выходные данные нашей модели и применили перекрёстную энтропию. Хотя это вполне корректно с математической точки зрения, с вычислительной точки зрения такой подход рискован из-за переполнения и недополнения при возведении в степень.
Вспомним, что функция софтмакс вычисляет вероятности с помощью \(\hat y_j = \frac{\exp(o_j)}{\sum_k \exp(o_k)}\). Если некоторые из \(o_k\) очень велики, то есть сильно положительны, то \(\exp(o_k)\) может превысить наибольшее число, допустимое для определённых типов данных. Это называется переполнением. Аналогично, если все аргументы являются очень большими отрицательными числами, мы получим недополнение. Например, числа с плавающей запятой одинарной точности приблизительно покрывают диапазон от \(10^{-38}\) до \(10^{38}\). Таким образом, если наибольший член в \(\mathbf{o}\) выходит за пределы интервала \([-90, 90]\), результат будет нестабильным. Одним из способов решения этой проблемы является вычитание \(\bar{o} \stackrel{\textrm{def}}{=} \max_k o_k\) из всех элементов:
По построению известно, что \(o_j - \bar{o} \leq 0\) для всех
\(j\). Следовательно, для задачи классификации на \(q\) классов
знаменатель находится в интервале \([1, q]\). Кроме того, числитель
никогда не превышает \(1\), что предотвращает численное
переполнение. Численное недополнение возникает только тогда, когда
\(\exp(o_j - \bar{o})\) вычисляется как \(0\). Тем не менее,
чуть ниже по тексту мы можем столкнуться с трудностями при вычислении
\(\log \hat{y}_j\) как \(\log 0\). В частности, при обратном
распространении ошибки мы можем столкнуться с экраном, заполненным
пугающими результатами NaN (Not a Number).
К счастью, нас спасает то, что, хотя мы и вычисляем экспоненциальные функции, в конечном итоге мы берём их логарифм (при вычислении функции потерь перекрёстной энтропии). Объединив софтмакс и перекрёстную энтропию, мы можем полностью избежать проблем с численной устойчивостью. Имеем:
Это позволяет избежать как переполнения, так и недооценки. Нам следует сохранить стандартную функцию софтмакса под рукой на случай, если потребуется оценить выходные вероятности нашей модели. Однако вместо передачи вероятностей софтмакса в новую функцию потерь мы просто передаём логиты и вычисляем софтмакс и его логарифм одновременно внутри функции потерь перекрёстной энтропии,, которая использует такие приёмы, как «трюк LogSumExp».
@d2l.add_to_class(d2l.Classifier) #@save
def loss(self, Y_hat, Y, averaged=True):
Y_hat = Y_hat.reshape((-1, Y_hat.shape[-1]))
Y = Y.reshape((-1,))
return F.cross_entropy(
Y_hat, Y, reduction='mean' if averaged else 'none')
4.5.3. Обучение¶
Далее мы обучаем нашу модель. Мы используем изображения Fashion-MNIST, преобразованные в 784-мерные векторы признаков.
data = d2l.FashionMNIST(batch_size=256)
model = SoftmaxRegression(num_outputs=10, lr=0.1)
trainer = d2l.Trainer(max_epochs=10)
trainer.fit(model, data)
Как и прежде, этот алгоритм сходится к решению с приёмлемой точностью, хотя на этот раз требуется меньше строк кода, чем раньше.
4.5.4. Итоги¶
Высокоуровневые API удобны тем, что скрывают от пользователя потенциально опасные аспекты, такие как численная устойчивость. Кроме того, они позволяют проектировать модели лаконично, используя всего несколько строк кода. Это одновременно и благо, и проклятие. Очевидное преимущество заключается в том, что это делает технологии высокодоступными, даже для инженеров, которые никогда не посещали ни одного курса по статистике (по сути, они входят в целевую аудиторию книги). Однако скрытие острых углов также имеет свою цену: это снижает мотивацию добавлять новые и нестандартные компоненты самостоятельно, поскольку для этого мало развитых навыков. Кроме того, это затрудняет исправление проблем, когда защитное дополнение фреймворка не покрывает все крайние случаи полностью. Опять же, это связано с недостатком знакомства.
Таким образом, мы настоятельно рекомендуем вам ознакомиться как с минималистичными, так и с элегантными версиями многих последующих реализаций. Хотя мы делаем акцент на понятности, эти реализации, как правило, обладают высокой производительностью (за исключением, пожалуй, свёрток). Мы хотим, чтобы вы могли использовать их в качестве основы для создания собственных решений, которые невозможно реализовать с помощью стандартных фреймворков.
4.5.5. Упражнения¶
Глубокое обучение использует множество различных числовых форматов, включая FP64 (двойная точность, применяется крайне редко), FP32 (одинарная точность), BFLOAT16 (хорошо подходит для сжатых представлений), FP16 (очень нестабильный), TF32 (новый формат от NVIDIA) и INT8. Вычислите наименьшее и наибольшее значение аргумента экспоненциальной функции, при которых результат не приводит к численному переполнению или недопустимому уменьшению разрядности.
INT8 — это очень ограниченный формат, состоящий из ненулевых чисел от \(1\) до \(255\). Как можно расширить его динамический диапазон, не увеличивая количество битов? Будут ли по-прежнему корректно работать стандартные операции умножения и сложения?
Увеличьте количество эпох обучения. Почему точность на валидационной выборке может снизиться через некоторое время? Как это можно исправить?
Что происходит при увеличении скорости обучения? Сравните кривые потерь для нескольких значений скорости обучения. Какая из них работает лучше? В каких случаях?