4.8 Численная устойчивость и инициализация модели
До сих пор каждая реализованная нами модель инициализировала параметры из заранее заданного распределения. Можно подумать, что схема инициализации — нечто само собой разумеющееся, и не вникать в то, как именно её выбирают. Может даже показаться, что выбор схемы инициализации не так уж важен. На самом деле выбор схемы инициализации играет огромную роль в обучении нейронных сетей и критически важен для сохранения численной устойчивости. Более того, схемы инициализации интересным образом связаны с выбором нелинейных функций активации. Какую функцию мы выберем и как инициализируем параметры, может определить, насколько быстро сойдётся алгоритм оптимизации. Неудачный выбор может привести к взрыву или затуханию градиентов во время обучения. В этом разделе мы подробнее рассмотрим эти темы и обсудим некоторые полезные эвристики, которые пригодятся на протяжении всей работы с глубоким обучением.
4.8.1 Затухающие и взрывающиеся градиенты
Рассмотрим глубокую сеть с слоями, входом и выходом . Каждый слой задаётся преобразованием с параметрами — весами ; его скрытая переменная — (положим ). Нашу сеть можно записать так:
Если все скрытые переменные и входы — векторы, градиент по любому набору параметров можно записать так:
Другими словами, градиент — это произведение матриц и вектора градиента . Поэтому мы подвержены проблемам численного антипереполнения (underflow). Такие проблемы часто возникают, когда перемножается слишком много вероятностей. При работе с вероятностями распространённый приём — перейти к логарифмическому пространству, то есть перенести нагрузку представления чисел с мантиссы на показатель степени. К сожалению, в нашем случае проблема серьёзнее: матрицы изначально могут иметь самые разные собственные значения. Они могут быть как маленькими, так и большими, а их произведение — очень большим или очень малым.
Риски нестабильных градиентов не ограничиваются численным представлением: они угрожают и устойчивости алгоритма оптимизации. Возможны два варианта проблем. Первый — взрыв градиентов (gradient exploding): обновления параметров становятся слишком большими, и устойчивая сходимость модели нарушается. Второй — затухание градиентов (gradient vanishing): обновления параметров слишком малы, на каждом шаге параметры почти не сдвигаются, и модель перестаёт учиться.
4.8.1.1 Затухание градиентов
Раньше была популярна сигмоидная функция (упоминалась в разделе 4.1), потому что она напоминает пороговую функцию. Ранние искусственные нейронные сети вдохновлялись биологическими, и идея, что нейрон либо полностью активен, либо полностью неактивен (как биологический нейрон), была привлекательной. Однако именно сигмоида — одна из распространённых причин затухания градиентов. Рассмотрим подробнее, почему сигмоида приводит к затуханию градиентов.
MXNet:
%matplotlib inline
from d2l import mxnet as d2l
from mxnet import autograd, np, npx
npx.set_np()
x = np.arange(-8.0, 8.0, 0.1)
x.attach_grad()
with autograd.record():
y = npx.sigmoid(x)
y.backward()
d2l.plot(x, [y, x.grad], legend=['sigmoid', 'градиент'], figsize=(4.5, 2.5))
PyTorch:
%matplotlib inline
from d2l import torch as d2l
import torch
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', 'градиент'], figsize=(4.5, 2.5))
TensorFlow:
%matplotlib inline
from d2l import tensorflow as d2l
import tensorflow as tf
x = tf.Variable(tf.range(-8.0, 8.0, 0.1))
with tf.GradientTape() as t:
y = tf.nn.sigmoid(x)
d2l.plot(x.numpy(), [y.numpy(), t.gradient(y, x).numpy()],
legend=['sigmoid', 'градиент'], figsize=(4.5, 2.5))
PaddlePaddle:
%matplotlib inline
from d2l import paddle as d2l
import warnings
warnings.filterwarnings("ignore")
import paddle
x = paddle.arange(start=-8.0, end=8.0, step=0.1, dtype='float32')
x.stop_gradient = False
y = paddle.nn.functional.sigmoid(x)
y.backward(paddle.ones_like(x))
d2l.plot(x.detach().numpy(), [y.detach().numpy(), x.grad.numpy()],
legend=['sigmoid', 'градиент'], figsize=(4.5, 2.5))
Как видно на графике, когда вход сигмоиды велик или мал по модулю, её градиент затухает. Кроме того, при обратном распространении через множество слоёв градиент всего произведения может затухнуть, если только входы сигмоиды не окажутся близкими к нулю. Когда слоёв много, без особой осторожности градиент может обрываться на каком-нибудь слое. Именно эта проблема долгое время мешала обучению глубоких сетей. Поэтому семейство более стабильных функций ReLU стало стандартным выбором на практике (хотя с точки зрения нейронаук это выглядит не слишком правдоподобно).
4.8.1.2 Взрыв градиентов
Взрыв градиентов может быть не менее проблематичным. Чтобы это продемонстрировать, сгенерируем 100 гауссовых случайных матриц и перемножим их с некоторой начальной матрицей. При выбранном нами масштабе (дисперсия ) произведение матриц взрывается. Если это происходит из-за инициализации глубокой сети, у градиентного спуска нет шансов сойтись.
MXNet:
M = np.random.normal(size=(4, 4))
print('Одна матрица \n', M)
for i in range(100):
M = np.dot(M, np.random.normal(size=(4, 4)))
print('После умножения на 100 матриц\n', M)
PyTorch:
M = torch.normal(0, 1, size=(4,4))
print('Одна матрица \n',M)
for i in range(100):
M = torch.mm(M,torch.normal(0, 1, size=(4, 4)))
print('После умножения на 100 матриц\n', M)
TensorFlow:
M = tf.random.normal((4, 4))
print('Одна матрица \n', M)
for i in range(100):
M = tf.matmul(M, tf.random.normal((4, 4)))
print('После умножения на 100 матриц\n', M.numpy())
PaddlePaddle:
M = paddle.normal(0, 1, shape=(4,4))
print('Одна матрица \n',M)
for i in range(100):
M = paddle.mm(M, paddle.normal(0, 1, shape=(4, 4)))
print('После умножения на 100 матриц\n', M)
4.8.1.3 Нарушение симметрии
Ещё одна проблема в проектировании нейронных сетей связана с симметрией, присущей их параметризации. Представим простой многослойный перцептрон с одним скрытым слоем и двумя скрытыми нейронами. Мы можем переставить веса первого слоя , а также веса выходного слоя, и получить ту же функцию. Первый скрытый нейрон ничем принципиально не отличается от второго. Иными словами, между скрытыми нейронами каждого слоя существует симметрия перестановок.
Предположим, выходной слой превращает два скрытых нейрона многослойного перцептрона в один выходной нейрон. Представим, что мы инициализируем все параметры скрытого слоя константой . Что тогда произойдёт? При прямом распространении оба скрытых нейрона получают одинаковые входы и параметры и выдают одинаковые активации, которые передаются выходному нейрону. При обратном распространении дифференцирование выходного нейрона по даёт градиент, все элементы которого равны. Поэтому после шага градиентного спуска (например, минибатчевого стохастического градиентного спуска) все элементы по-прежнему будут одинаковыми. Такие итерации никогда не нарушат симметрию, и выразительная способность сети никогда не будет полностью использована. Скрытый слой будет вести себя так, будто в нём всего один нейрон. Обратите внимание: хотя минибатчевый стохастический градиентный спуск не нарушает эту симметрию, регуляризация исключением (dropout) — нарушает.
4.8.2 Инициализация параметров
Один из способов решить (или хотя бы смягчить) описанные проблемы — правильная инициализация параметров. Внимательность при оптимизации и подходящая регуляризация могут дополнительно повысить устойчивость.
4.8.2.1 Инициализация по умолчанию
В предыдущих разделах, например в разделе 3.3, мы инициализировали веса из нормального распределения. Если метод инициализации не указан, фреймворк использует случайную инициализацию по умолчанию; для задач средней сложности она обычно работает хорошо.
4.8.2.2 Инициализация Xavier
Посмотрим, как распределён масштаб выходов полносвязного слоя без нелинейности (например, скрытых переменных). Для этого слоя с входами и соответствующими весами выход задаётся формулой
Веса независимо извлечены из одного и того же распределения. Предположим, что это распределение имеет нулевое среднее и дисперсию . Заметим, что это не значит, что распределение обязательно гауссово: важно лишь, чтобы существовали среднее и дисперсия. Теперь предположим, что входы слоя также имеют нулевое среднее и дисперсию , независимы от и друг от друга. В этом случае можно вычислить среднее и дисперсию :
Один из способов сохранить дисперсию неизменной — задать . Теперь рассмотрим обратное распространение: мы сталкиваемся с похожей проблемой, хотя градиенты распространяются от слоёв, более близких к выходу. Рассуждая так же, как для прямого распространения, видим, что дисперсия градиентов может возрасти, если не выполнено условие , где — число выходов слоя. Получается дилемма: одновременно выполнить оба условия невозможно. Поэтому достаточно потребовать:
На этом основана стандартная и практичная инициализация Xavier (Xavier initialization), названная по имени первого автора статьи, в которой она предложена (Glorot and Bengio, 2010). Обычно при инициализации Xavier веса берутся из гауссова распределения с нулевым средним и дисперсией . Можно также выбрать равномерное распределение: дисперсия равномерного распределения равна . Подставив в условие на , получим диапазон инициализации:
Хотя предположение «нет нелинейности» в нейронных сетях легко нарушается, на практике метод инициализации Xavier оказался эффективным.
4.8.2.3 Дополнительное чтение
Приведённые рассуждения лишь слегка затрагивают современные методы инициализации параметров. Фреймворки глубокого обучения обычно реализуют десятки различных эвристик. Кроме того, инициализация параметров остаётся активной областью фундаментальных исследований в глубоком обучении. Сюда относятся эвристики для связывания (разделения) параметров, сверхразрешения, последовательных моделей и других случаев. Например, Xiao et al. (2018) показали, что с помощью тщательно продуманной инициализации можно обучить нейронную сеть из 10 000 слоёв без архитектурных ухищрений.
Если вам интересна эта тема, мы рекомендуем подробно изучить материал этого раздела, прочитать статьи, в которых предлагаются и анализируются эти эвристики, а затем познакомиться с последними публикациями по теме. Возможно, вы даже наткнётесь на удачную идею или придумаете её сами и реализуете её в одном из фреймворков глубокого обучения.
Резюме
- Затухание и взрыв градиентов — частые проблемы глубоких сетей. При инициализации параметров нужна большая осторожность, чтобы градиенты и параметры оставались под контролем.
- Эвристические методы инициализации нужны, чтобы начальные градиенты не были ни слишком большими, ни слишком малыми.
- Функция активации ReLU смягчает проблему затухания градиентов, что ускоряет сходимость.
- Случайная инициализация необходима, чтобы нарушить симметрию до начала оптимизации.
- Инициализация Xavier показывает, что для каждого слоя дисперсия выхода не зависит от числа входов, а дисперсия градиентов не зависит от числа выходов.
Упражнения
- Помимо перестановочной симметрии многослойного перцептрона, можете ли вы придумать другие случаи, в которых нейронная сеть может обладать симметрией, которую нужно нарушить?
- Можно ли инициализировать все весовые параметры линейной регрессии или softmax-регрессии одним и тем же значением?
- Найдите в литературе аналитические оценки собственных значений произведения двух матриц. Что это говорит о том, как обеспечить правильные условия для градиентов?
- Если мы знаем, что некоторые члены расходятся, можем ли мы исправить это постфактум? Ознакомьтесь со статьёй о масштабировании скорости обучения по слоям (You et al., 2017).
MXNet: Обсуждение
PyTorch: Обсуждение
TensorFlow: Обсуждение
PaddlePaddle: Обсуждение
Литература
- Glorot, X., & Bengio, Y. (2010). Understanding the difficulty of training deep feedforward neural networks. Proceedings of the Thirteenth International Conference on Artificial Intelligence and Statistics, 249–256.