.. _sec_weight_decay: Затухание весов =============== Теперь, когда мы охарактеризовали проблему переобучения, мы можем перейти к нашей первой технике регуляризации. Напомним, что мы всегда можем смягчить переобучение, собрав больше обучающих данных. Однако это может быть дорого, требовать много времени или вообще выходить за рамки нашего контроля, что делает это невозможным в краткосрочной перспективе. Пока мы можем предположить, что у нас уже есть столько качественных данных, сколько позволяют наши ресурсы, и сосредоточиться на инструментах, которые у нас есть, когда набор данных считается заданным. Вспомним, что в примере полиномиальной регрессии (:numref:`subsec_polynomial-curve-fitting`) мы могли ограничить емкость модели, изменяя степень аппроксимирующего полинома. Действительно, ограничение числа признаков — популярный метод борьбы с переобучением. Однако простое отбрасывание признаков может оказаться слишком грубым инструментом. Продолжая использовать пример полиномиальной регрессии, рассмотрим, что может произойти при работе с входными данными высокой размерности. Естественным обобщением полиномов на многомерные данные являются *мономы*, которые представляют собой произведения переменных в некоторых степенях. Степень монома равна сумме этих степеней. Например, :math:`x_1^2 x_2` и :math:`x_3 x_5^2` — оба монома третьей степени. Обратите внимание, что количество членов степени :math:`d` быстро возрастает по мере увеличения :math:`d`. При наличии :math:`k` переменных количество мономов степени :math:`d` равно :math:`{k - 1 + d} \choose {k - 1}`. Даже небольшие изменения степени, например, с :math:`2` на :math:`3`, существенно увеличивают сложность нашей модели. Поэтому нам часто требуется более тонкий инструмент для регулирования сложности функции. .. raw:: latex \diilbookstyleinputcell .. code:: python %matplotlib inline import torch from torch import nn from d2l import torch as d2l Нормы и затухание весов ----------------------- Вместо того чтобы напрямую управлять количеством параметров, *затухание весов* работает путем ограничения значений, которые могут принимать параметры. Более известная как :math:`\ell_2` регуляризация за пределами круга специалистов по глубокому обучению при оптимизации с помощью стохастического градиентного спуска на мини-батчах, затухание весов, возможно, является наиболее широко используемой техникой регуляризации параметрических моделей машинного обучения. Этот метод основан на базовой интуиции о том, что среди всех функций :math:`f` функция :math:`f = 0` (присваивающая значение :math:`0` всем входам) в некотором смысле является *наиболее простой*, и что сложность функции можно измерять расстоянием её параметров от нуля. Но как именно следует измерять расстояние между функцией и нулем? Единого правильного ответа не существует. На самом деле, целые разделы математики, включая функциональный анализ и теорию банаховых пространств, посвящены решению таких вопросов. Одна из простых интерпретаций заключается в том, чтобы измерять сложность линейной функции :math:`f(\mathbf{x}) = \mathbf{w}^\top \mathbf{x}` с помощью некоторой нормы её вектора весов, например, :math:`\| \mathbf{w} \|^2`. Напомним, что мы ввели :math:`\ell_2` норму и :math:`\ell_1` норму, которые являются частными случаями более общей :math:`\ell_p` нормы, в :numref:`subsec_lin-algebra-norms`. Наиболее распространённый способ ограничения вектора весов заключается в добавлении его нормы в качестве штрафного члена к задаче минимизации функции потерь. Таким образом, мы заменяем нашу исходную цель — *минимизацию ошибки предсказания на обучающих примерах* — на новую цель — *минимизацию суммы ошибки предсказания и штрафного члена*. Теперь, если наш вектор весов становится слишком большим, алгоритм обучения может начать фокусироваться на минимизации нормы весов :math:`\| \mathbf{w} \|^2` вместо минимизации ошибки на обучающей выборке. Именно этого мы и хотим добиться. Чтобы проиллюстрировать это на практике, вернёмся к нашему предыдущему примеру из :numref:`sec_linear_regression` для линейной регрессии. Там функция потерь задавалась как .. math:: L(\mathbf{w}, b) = \frac{1}{n}\sum_{i=1}^n \frac{1}{2}\left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right)^2. Напомним, что :math:`\mathbf{x}^{(i)}` — это признаки, :math:`y^{(i)}` — метка для любого примера данных :math:`i`, а :math:`(\mathbf{w}, b)` — параметры весов и смещения соответственно. Чтобы штрафовать за величину вектора весов, нам необходимо как-то добавить :math:`\| \mathbf{w} \|^2` к функции потерь, но как модель должна балансировать между стандартной функцией потерь и этим новым аддитивным штрафом? На практике этот баланс определяется с помощью *константы регуляризации* :math:`\lambda`, неотрицательного гиперпараметра, который мы подбираем с помощью данных валидации: .. math:: L(\mathbf{w}, b) + \frac{\lambda}{2} \|\mathbf{w}\|^2. Для :math:`\lambda = 0` мы получаем исходную функцию потерь. Для :math:`\lambda > 0` мы ограничиваем величину :math:`\| \mathbf{w} \|`. По соглашению мы делим на :math:`2`: при взятии производной квадратичной функции :math:`2` и :math:`1/2` сокращаются, что обеспечивает аккуратный и простой вид выражения для обновления. Внимательный читатель может спросить, почему мы работаем с квадратом нормы, а не с обычной нормой (т. е. евклидовым расстоянием). Мы делаем это ради вычислительного удобства. Возводя норму :math:`\ell_2` в квадрат, мы избавляемся от квадратного корня, получая сумму квадратов каждого компонента вектора весов. Это упрощает вычисление производной штрафа: производная суммы равна сумме производных. Более того, может возникнуть вопрос, почему мы работаем именно с :math:`\ell_2` нормой, а не, скажем, с :math:`\ell_1` нормой. На самом деле, другие варианты также допустимы и широко используются в статистике. В то время как линейные модели с :math:`\ell_2`-регуляризацией составляют классический алгоритм *гребневой регрессии*, линейная регрессия с :math:`\ell_1`-регуляризацией является столь же фундаментальным методом в статистике, широко известным как *лассо-регрессия*. Одна из причин использования :math:`\ell_2` нормы заключается в том, что она накладывает непропорционально большой штраф на большие компоненты вектора весов. Это смещает наш алгоритм обучения в сторону моделей, которые равномерно распределяют веса между большим количеством признаков. На практике это может сделать их более устойчивыми к ошибкам измерения в отдельной переменной. Напротив, штрафы :math:`\ell_1` приводят к моделям, которые концентрируют веса на небольшом наборе признаков, обнуляя остальные. Это дает нам эффективный метод *отбора признаков*, который может быть желателен по другим причинам. Например, если наша модель опирается лишь на несколько признаков, то нам, возможно, не потребуется собирать, хранить или передавать данные для остальных (отброшенных) признаков. Используя те же обозначения, что и в :eq:`eq_linreg_batch_update`, стохастический градиентный спуск на мини-батче обновляет параметры для регрессии с регуляризацией :math:`\ell_2` следующим образом: .. math:: \begin{aligned} \mathbf{w} & \leftarrow \left(1- \eta\lambda \right) \mathbf{w} - \frac{\eta}{|\mathcal{B}|} \sum_{i \in \mathcal{B}} \mathbf{x}^{(i)} \left(\mathbf{w}^\top \mathbf{x}^{(i)} + b - y^{(i)}\right). \end{aligned} Как и ранее, мы обновляем :math:`\mathbf{w}` на основе разницы между нашей оценкой и наблюдением. Однако мы также уменьшаем величину :math:`\mathbf{w}`, приближая её к нулю. Именно поэтому метод иногда называют «затуханием весов»: если рассматривать только штрафной член, то алгоритм оптимизации *уменьшает* вес на каждом шаге обучения. В отличие от отбора признаков, затухание весов предоставляет нам механизм для плавной настройки сложности функции. Меньшие значения :math:`\lambda` соответствуют менее ограниченным :math:`\mathbf{w}`, тогда как большие значения :math:`\lambda` значительно сильнее ограничивают :math:`\mathbf{w}`. То, включаем ли мы соответствующий штраф для смещения :math:`b^2`, может варьироваться в зависимости от реализации и даже различаться для разных слоёв нейронной сети. Часто член смещения не регуляризуется. Кроме того, хотя :math:`\ell_2` регуляризация может не быть эквивалентной затуханию весов для других алгоритмов оптимизации, идея регуляризации через уменьшение величин весов остаётся справедливой. Линейная регрессия в высокоразмерном пространстве ------------------------------------------------- Мы можем проиллюстрировать преимущества затухания весов на простом синтетическом примере. Сначала мы **сгенерируем данные, как и раньше**: .. math:: y = 0.05 + \sum_{i = 1}^d 0.01 x_i + \epsilon \textrm{ where } \epsilon \sim \mathcal{N}(0, 0.01^2). В этом синтетическом наборе данных метка задается скрытой линейной функцией от входных данных, искаженной гауссовским шумом с нулевым средним и стандартным отклонением 0,01. Для наглядности мы можем сделать эффекты переобучения более выраженными, увеличив размерность задачи до :math:`d = 200` и используя небольшую обучающую выборку всего из 20 примеров. .. raw:: latex \diilbookstyleinputcell .. code:: python class Data(d2l.DataModule): def __init__(self, num_train, num_val, num_inputs, batch_size): self.save_hyperparameters() n = num_train + num_val self.X = torch.randn(n, num_inputs) noise = torch.randn(n, 1) * 0.01 w, b = torch.ones((num_inputs, 1)) * 0.01, 0.05 self.y = torch.matmul(self.X, w) + b + noise def get_dataloader(self, train): i = slice(0, self.num_train) if train else slice(self.num_train, None) return self.get_tensorloader([self.X, self.y], train, i) Реализация с нуля ----------------- Теперь давайте реализуем затухание весов с нуля. Поскольку стохастический градиентный спуск в мини-батчах является нашим оптимизатором, нам нужно просто добавить штраф за квадрат :math:`\ell_2` к исходной функции потерь. Определение штрафа :math:`\ell_2`-нормы ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Наиболее удобным способом реализации этого штрафа является возведение всех слагаемых в квадрат на месте и их суммирование. .. raw:: latex \diilbookstyleinputcell .. code:: python def l2_penalty(w): return (w ** 2).sum() / 2 Определение модели ~~~~~~~~~~~~~~~~~~ В итоговой модели линейная регрессия и квадратичная функция потерь не изменились с момента :numref:`sec_linear_scratch`, поэтому мы просто определим подкласс ``d2l.LinearRegressionScratch``. Единственное изменение заключается в том, что теперь наша функция потерь включает штрафной член. .. raw:: latex \diilbookstyleinputcell .. code:: python class WeightDecayScratch(d2l.LinearRegressionScratch): def __init__(self, num_inputs, lambd, lr, sigma=0.01): super().__init__(num_inputs, lr, sigma) self.save_hyperparameters() def loss(self, y_hat, y): return (super().loss(y_hat, y) + self.lambd * l2_penalty(self.w)) Приведённый ниже код обучает нашу модель на обучающей выборке из 20 примеров и оценивает её на валидационной выборке из 100 примеров. .. raw:: latex \diilbookstyleinputcell .. code:: python data = Data(num_train=20, num_val=100, num_inputs=200, batch_size=5) trainer = d2l.Trainer(max_epochs=10) def train_scratch(lambd): model = WeightDecayScratch(num_inputs=200, lambd=lambd, lr=0.01) model.board.yscale='log' trainer.fit(model, data) print('L2 norm of w:', float(l2_penalty(model.w))) Обучение без регуляризации ~~~~~~~~~~~~~~~~~~~~~~~~~~ Теперь мы запустим этот код с ``lambd = 0``, отключив затухание весов. Обратите внимание, что мы сильно переобучаемся: ошибка на обучающей выборке уменьшается, но ошибка на валидационной выборке — классический пример переобучения. .. raw:: latex \diilbookstyleinputcell .. code:: python train_scratch(0) Использование затухания весов ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Ниже мы применяем значительное затухание весов. Обратите внимание, что ошибка на обучающей выборке возрастает, а ошибка на валидационной выборке уменьшается. Это именно тот эффект, который мы ожидаем от регуляризации. .. raw:: latex \diilbookstyleinputcell .. code:: python train_scratch(3) Краткая реализация ------------------ Поскольку затухание весов повсеместно используется при оптимизации нейронных сетей, фреймворки глубокого обучения предоставляют для него особенно удобные средства, интегрируя затухание весов непосредственно в алгоритм оптимизации. Это упрощает его использование в сочетании с любой функцией потерь. Кроме того, такая интеграция даёт вычислительные преимущества: позволяет реализовать затухание весов с помощью оптимизаций, не добавляя никаких дополнительных накладных расходов. Поскольку часть обновления, отвечающая за затухание весов, зависит только от текущих значений каждого параметра, оптимизатор всё равно должен обращаться к каждому параметру хотя бы один раз. Ниже мы задаём гиперпараметр затухания весов непосредственно через ``weight_decay`` при создании оптимизатора. По умолчанию PyTorch одновременно применяет затухание весов и смещений, но мы можем настроить оптимизатор так, чтобы разные параметры обрабатывались по разным правилам. Здесь мы устанавливаем ``weight_decay`` только для весов (параметров ``net.weight``), поэтому смещение (параметр ``net.bias``) не будет затухать. .. raw:: latex \diilbookstyleinputcell .. code:: python class WeightDecay(d2l.LinearRegression): def __init__(self, wd, lr): super().__init__(lr) self.save_hyperparameters() self.wd = wd def configure_optimizers(self): return torch.optim.SGD([ {'params': self.net.weight, 'weight_decay': self.wd}, {'params': self.net.bias}], lr=self.lr) График выглядит так же, как и при реализации затухания весов с нуля. Однако этот вариант работает быстрее и проще в реализации, причём эти преимущества станут более заметными по мере решения более крупных задач, когда такая работа станет рутинной. .. raw:: latex \diilbookstyleinputcell .. code:: python model = WeightDecay(wd=3, lr=0.01) model.board.yscale='log' trainer.fit(model, data) print('L2 norm of w:', float(l2_penalty(model.get_w_b()[0]))) До сих пор мы рассмотрели лишь одно понятие простой линейной функции. Однако даже для простых нелинейных функций ситуация может быть гораздо более сложной. Концепция `воспроизводящего ядерного гильбертова пространства (RKHS) `__ позволяет применять инструменты, введенные для линейных функций, в нелинейном контексте. К сожалению, алгоритмы, основанные на RKHS, плохо масштабируются на большие данные высокой размерности. В этой книге мы будем часто использовать распространенную эвристику, согласно которой затухание весов применяется ко всем слоям глубокой сети. Резюме ------ Регуляризация — распространенный метод борьбы с переобучением. Классические методы регуляризации добавляют штрафной член к функции потерь (на этапе обучения), чтобы снизить сложность изучаемой модели. Одним из способов упрощения модели является использование :math:`\ell_2`-штрафа. Это приводит к затуханию весов на шагах обновления алгоритма стохастического градиентного спуска по мини-батчам. На практике функциональность затухания весов предоставляется оптимизаторами в фреймворках глубокого обучения. Разные наборы параметров могут иметь различное поведение при обновлении в рамках одного цикла обучения. Упражнения ---------- 1. Экспериментируйте со значением :math:`\lambda` в задаче оценки из этого раздела. Постройте графики точности на обучающей и валидационной выборках в зависимости от :math:`\lambda`. Что вы наблюдаете? 2. Используйте валидационную выборку для поиска оптимального значения :math:`\lambda`. Является ли оно действительно оптимальным? Имеет ли это значение? 3. Как бы выглядели уравнения обновления, если бы вместо :math:`\|\mathbf{w}\|^2` мы использовали :math:`\sum_i |w_i|` в качестве выбранной нами штрафной функции (:math:`\ell_1` регуляризация)? 4. Мы знаем, что :math:`\|\mathbf{w}\|^2 = \mathbf{w}^\top \mathbf{w}`. Можете ли вы найти аналогичное уравнение для матриц (см. норму Фробениуса в :numref:`subsec_lin-algebra-norms`)? 5. Рассмотрите взаимосвязь между ошибкой на обучающей выборке и ошибкой обобщающей способности. Помимо затухания весов, увеличения объёма обучающей выборки и использования модели подходящей сложности, какие ещё способы могут помочь справиться с переобучением? 6. В байесовской статистике мы используем произведение априорного распределения и правдоподобия для получения апостериорного распределения через :math:`P(w \mid x) \propto P(x \mid w) P(w)`. Как можно связать :math:`P(w)` с регуляризацией? `Обсуждения `__