.. _sec_numerical_stability: Численная устойчивость и инициализация ====================================== До сих пор все модели, которые мы реализовывали, требовали инициализации их параметров в соответствии с некоторым заранее заданным распределением. До настоящего момента мы принимали схему инициализации как данность, не вдаваясь в подробности того, как принимаются эти решения. У вас могло сложиться впечатление, что эти выборы не особенно важны. Напротив, выбор схемы инициализации играет значительную роль в обучении нейронной сети и может быть решающим для обеспечения численной устойчивости. Более того, эти выборы могут быть тесно связаны с выбором нелинейной функции активации. То, какую функцию мы выбираем и как инициализируем параметры, может определить, насколько быстро будет сходиться наш алгоритм оптимизации. Плохие выборы здесь могут привести к тому, что при обучении мы столкнемся с взрывающимися или затухающими градиентами. В этом разделе мы подробнее рассмотрим эти темы и обсудим некоторые полезные эвристики, которые пригодятся вам на протяжении всей карьеры в глубоком обучении. .. raw:: latex \diilbookstyleinputcell .. code:: python %matplotlib inline import torch from d2l import torch as d2l Исчезающие и взрывающиеся градиенты ----------------------------------- Рассмотрим глубокую сеть с :math:`L` слоями, входом :math:`\mathbf{x}` и выходом :math:`\mathbf{o}`. Каждый слой :math:`l` определяется преобразованием :math:`f_l`, параметризованным весами :math:`\mathbf{W}^{(l)}`, выход скрытого слоя которого равен :math:`\mathbf{h}^{(l)}` (обозначим его :math:`\mathbf{h}^{(0)} = \mathbf{x}`). Наша сеть можно выразить следующим образом: .. math:: \mathbf{h}^{(l)} = f_l (\mathbf{h}^{(l-1)}) \textrm{ and thus } \mathbf{o} = f_L \circ \cdots \circ f_1(\mathbf{x}). Если выходные данные каждого скрытого слоя и вход являются векторами, то градиент :math:`\mathbf{o}` по любому набору параметров :math:`\mathbf{W}^{(l)}` можно записать следующим образом: .. math:: \partial_{\mathbf{W}^{(l)}} \mathbf{o} = \underbrace{\partial_{\mathbf{h}^{(L-1)}} \mathbf{h}^{(L)}}_{ \mathbf{M}^{(L)} \stackrel{\textrm{def}}{=}} \cdots \underbrace{\partial_{\mathbf{h}^{(l)}} \mathbf{h}^{(l+1)}}_{ \mathbf{M}^{(l+1)} \stackrel{\textrm{def}}{=}} \underbrace{\partial_{\mathbf{W}^{(l)}} \mathbf{h}^{(l)}}_{ \mathbf{v}^{(l)} \stackrel{\textrm{def}}{=}}. Другими словами, этот градиент представляет собой произведение матриц :math:`L-l` :math:`\mathbf{M}^{(L)} \cdots \mathbf{M}^{(l+1)}` и вектора градиента :math:`\mathbf{v}^{(l)}`. Таким образом, мы сталкиваемся с теми же проблемами численного недопереполнения (underflow), которые часто возникают при перемножении слишком большого количества вероятностей. При работе с вероятностями распространённым приёмом является переход в логарифмическое пространство, то есть перенос нагрузки с мантиссы на показатель степени числового представления. К сожалению, ситуация выше более серьёзная: изначально матрицы :math:`\mathbf{M}^{(l)}` могут иметь широкий спектр собственных значений. Они могут быть как малыми, так и большими, а их произведение может быть *очень большим* или *очень малым*. Риски, связанные с нестабильными градиентами, выходят за рамки проблем численного представления. Градиенты непредсказуемой величины также угрожают стабильности наших алгоритмов оптимизации. Мы можем столкнуться с обновлениями параметров, которые либо (i) чрезмерно велики, разрушая нашу модель (проблема *взрывающегося градиента*); либо (ii) чрезмерно малы (проблема *затухания градиента*), что делает обучение невозможным, поскольку параметры практически не изменяются при каждом обновлении. Исчезающие градиенты ~~~~~~~~~~~~~~~~~~~~ Одной из частых причин проблемы исчезающих градиентов является выбор функции активации :math:`\sigma`, применяемой после линейных операций каждого слоя. Исторически функция сигмоиды :math:`1/(1 + \exp(-x))` (представленная в :numref:`sec_mlp`) была популярна, поскольку она напоминает функцию порогового срабатывания. Поскольку ранние искусственные нейронные сети были вдохновлены биологическими нейронными сетями, идея нейронов, которые срабатывают либо *полностью*, либо *не срабатывают вовсе* (как биологические нейроны), казалась привлекательной. Давайте рассмотрим сигмоиду подробнее, чтобы понять, почему она может приводить к исчезающим градиентам. .. raw:: latex \diilbookstyleinputcell .. code:: python x = torch.arange(-8.0, 8.0, 0.1, requires_grad=True) y = torch.sigmoid(x) y.backward(torch.ones_like(x)) d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()], legend=['sigmoid', 'gradient'], figsize=(4.5, 2.5)) Как видно, градиент сигмоиды исчезает как при больших, так и при малых значениях входных данных. Более того, при обратном распространении градиента через множество слоёв, если мы не находимся в «золотой середине», где входы большинства сигмоид близки к нулю, градиенты всего произведения могут исчезнуть. Когда наша сеть имеет много слоёв, если мы не будем осторожны, градиент, скорее всего, затухнет на каком-то из слоёв. Действительно, эта проблема ранее сильно затрудняла обучение глубоких сетей. В результате функции активации ReLU, которые более устойчивы (хотя и менее биологически правдоподобны), стали выбором по умолчанию для практиков. Взрывающиеся градиенты ~~~~~~~~~~~~~~~~~~~~~~ Проблема, обратная описанной, когда градиенты взрываются, может быть столь же неприятной. Чтобы проиллюстрировать это лучше, мы сгенерируем 100 случайных матриц из гауссова распределения и перемножим их с некоторой начальной матрицей. Для выбранного нами масштаба (выбор дисперсии :math:`\sigma^2=1`) произведение матриц взрывается. Когда это происходит из-за инициализации глубокой сети, у нас нет шансов заставить оптимизатор градиентного спуска сойтись. .. raw:: latex \diilbookstyleinputcell .. code:: python M = torch.normal(0, 1, size=(4, 4)) print('a single matrix \n',M) for i in range(100): M = M @ torch.normal(0, 1, size=(4, 4)) print('after multiplying 100 matrices\n', M) Нарушение симметрии ~~~~~~~~~~~~~~~~~~~ Ещё одна проблема в проектировании нейронных сетей связана с симметрией, присущей их параметризации. Предположим, что у нас есть простой многослойный перцептрон со одним скрытым слоем и двумя единицами. В этом случае мы можем переставить веса :math:`\mathbf{W}^{(1)}` первого слоя, а также аналогично переставить веса выходного слоя, чтобы получить ту же самую функцию. Нет ничего особенного, что отличало бы первую и вторую скрытые единицы. Иными словами, между скрытыми единицами каждого слоя существует симметрия перестановок. Это не просто теоретическая проблема. Рассмотрим упомянутый выше многослойный перцептрон с одним скрытым слоем и двумя скрытыми единицами. Для наглядности предположим, что выходной слой преобразует эти две скрытые единицы в одну выходную. Представьте, что произойдет, если мы инициализируем все параметры скрытого слоя значением :math:`\mathbf{W}^{(1)} = c` для некоторой константы :math:`c`. В этом случае во время прямого распространения обе скрытые единицы получают одинаковые входные данные и параметры, что приводит к одинаковым значениям активации, которые подаются на выходную единицу. Во время обратного распространения ошибки дифференцирование выходной единицы по параметрам :math:`\mathbf{W}^{(1)}` дает градиент, все элементы которого принимают одинаковое значение. Следовательно, после итерации градиентного спуска (например, стохастического градиентного спуска на мини-батче) все элементы :math:`\mathbf{W}^{(1)}` по-прежнему принимают одинаковое значение. Такие итерации сами по себе никогда не *нарушат симметрию*, и мы, возможно, никогда не сможем в полной мере реализовать выразительную способность сети. Скрытый слой будет вести себя так, как если бы он содержал только одну единицу. Обратите внимание, что, хотя стохастический градиентный спуск на мини-батчах не нарушит эту симметрию, регуляризация дропаутом (которая будет представлена позже) нарушит! Инициализация параметров ------------------------ Один из способов решения — или хотя бы смягчения — описанных выше проблем заключается в тщательной инициализации. Как мы увидим позже, дополнительная осторожность при оптимизации и подходящая регуляризация могут дополнительно повысить устойчивость. Инициализация по умолчанию ~~~~~~~~~~~~~~~~~~~~~~~~~~ В предыдущих разделах, например в :numref:`sec_linear_concise`, мы использовали нормальное распределение для инициализации значений наших весов. Если не указывать метод инициализации, фреймворк использует метод случайной инициализации по умолчанию, который на практике часто хорошо работает для задач умеренного размера. .. _subsec_xavier: Инициализация Ксавье ~~~~~~~~~~~~~~~~~~~~ Рассмотрим распределение масштаба выхода :math:`o_{i}` для некоторого полносвязного слоя *без нелинейностей*. При входах :math:`n_\textrm{in}` :math:`x_j` и соответствующих весах :math:`w_{ij}` этого слоя выход определяется как .. math:: o_{i} = \sum_{j=1}^{n_\textrm{in}} w_{ij} x_j. Все веса :math:`w_{ij}` независимо выбираются из одного и того же распределения. Кроме того, предположим, что это распределение имеет нулевое среднее и дисперсию :math:`\sigma^2`. Обратите внимание, что это не означает, что распределение должно быть гауссовским, а лишь то, что его среднее и дисперсия должны существовать. Пока будем считать, что входы слоя :math:`x_j` также имеют нулевое среднее и дисперсию :math:`\gamma^2`, и что они независимы от :math:`w_{ij}` и независимы друг от друга. В этом случае мы можем вычислить среднее :math:`o_i`: .. math:: \begin{aligned} E[o_i] & = \sum_{j=1}^{n_\textrm{in}} E[w_{ij} x_j] \\&= \sum_{j=1}^{n_\textrm{in}} E[w_{ij}] E[x_j] \\&= 0, \end{aligned} а также дисперсию: .. math:: \begin{aligned} \textrm{Var}[o_i] & = E[o_i^2] - (E[o_i])^2 \\ & = \sum_{j=1}^{n_\textrm{in}} E[w^2_{ij} x^2_j] - 0 \\ & = \sum_{j=1}^{n_\textrm{in}} E[w^2_{ij}] E[x^2_j] \\ & = n_\textrm{in} \sigma^2 \gamma^2. \end{aligned} Один из способов сохранить дисперсию неизменной — это задать :math:`n_\textrm{in} \sigma^2 = 1`. Теперь рассмотрим обратное распространение ошибки. Там мы сталкиваемся с аналогичной проблемой, хотя градиенты распространяются от слоёв, ближайших к выходу. Используя те же рассуждения, что и для прямого распространения, мы видим, что дисперсия градиентов может неограниченно возрастать, если не выполняется условие :math:`n_\textrm{out} \sigma^2 = 1`, где :math:`n_\textrm{out}` — количество выходов данного слоя. Таким образом, мы оказываемся в тупике: удовлетворить оба условия одновременно невозможно. Вместо этого мы просто пытаемся выполнить следующее условие: .. math:: \begin{aligned} \frac{1}{2} (n_\textrm{in} + n_\textrm{out}) \sigma^2 = 1 \textrm{ or equivalently } \sigma = \sqrt{\frac{2}{n_\textrm{in} + n_\textrm{out}}}. \end{aligned} Именно этот подход лежит в основе ставшей стандартной и практически полезной *инициализации Глорота*, названной в честь первого автора из числа её создателей :cite:`Glorot.Bengio.2010`. Как правило, инициализация Глорота предполагает выбор весов из гауссова распределения с нулевым средним и дисперсией :math:`\sigma^2 = \frac{2}{n_\textrm{in} + n_\textrm{out}}`. Мы также можем адаптировать этот подход для выбора дисперсии при выборке весов из равномерного распределения. Обратите внимание, что равномерное распределение :math:`U(-a, a)` имеет дисперсию :math:`\frac{a^2}{3}`. Подстановка :math:`\frac{a^2}{3}` в наше условие на :math:`\sigma^2` подсказывает нам инициализировать веса в соответствии с .. math:: U\left(-\sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}, \sqrt{\frac{6}{n_\textrm{in} + n_\textrm{out}}}\right). Хотя предположение об отсутствии нелинейностей, лежащее в основе приведённого выше математического рассуждения, в нейронных сетях часто оказывается неверным, метод инициализации Xavier на практике демонстрирует хорошие результаты. Выход за рамки ~~~~~~~~~~~~~~ Вышеизложенные рассуждения лишь слегка затрагивают современные подходы к инициализации параметров. Фреймворки глубокого обучения часто реализуют более дюжины различных эвристик. Более того, инициализация параметров остается горячей областью фундаментальных исследований в глубоком обучении. Среди них есть эвристики, специализированные для связанных (общих) параметров, задач суперразрешения, моделей последовательностей и других случаев. Например, :cite:t:`Xiao.Bahri.Sohl-Dickstein.ea.2018` продемонстрировал возможность обучения нейронных сетей с 10 000 слоями без архитектурных ухищрений с помощью тщательно разработанного метода инициализации. Если эта тема вас заинтересовала, мы рекомендуем углубиться в материалы этого модуля, прочитать статьи, в которых были предложены и проанализированы каждую эвристику, а также изучить последние публикации по данной теме. Возможно, вы наткнетесь на остроумную идею или даже изобретете её и внесете реализацию в фреймворки глубокого обучения. Итоги ----- Исчезающие и взрывающиеся градиенты — распространённые проблемы в глубоких сетях. Необходимо тщательно подходить к инициализации параметров, чтобы градиенты и параметры оставались хорошо контролируемыми. Применяются эвристики инициализации, гарантирующие, что начальные градиенты не будут ни слишком большими, ни слишком малыми. Случайная инициализация является ключом к обеспечению нарушения симметрии перед оптимизацией. Инициализация по методу Ксавье предполагает, что для каждого слоя дисперсия любого выхода не зависит от количества входов, а дисперсия любого градиента не зависит от количества выходов. Функции активации ReLU смягчают проблему исчезающих градиентов. Это может ускорить сходимость. Упражнения ---------- 1. Можете ли вы придумать другие случаи, в которых нейронная сеть может проявлять симметрию, требующую нарушения, помимо перестановочной симметрии в слоях многослойного перцептрона? 2. Можно ли инициализировать все параметры весов в линейной регрессии или софтмакс-регрессии одним и тем же значением? 3. Найдите аналитические оценки для собственных значений произведения двух матриц. Что это говорит о том, как обеспечить хорошую обусловленность градиентов? 4. Если известно, что некоторые члены расходятся, можно ли исправить это постфактум? В качестве источника вдохновения обратимся к статье о масштабировании скорости обучения на уровне слоёв :cite:`You.Gitman.Ginsburg.2017`. `Обсуждения `__