Содержание курса 21 / 27

21 / 27 · Нейронные сети

Обучение глубоких сетей

Что делает глубокую сеть обучаемой и удерживает её от переобучения: инициализация, сохраняющая дисперсию сигналов (Глоро и Хе), дропаут как ансамбль, затухание весов против L2-штрафа в Adam, расписания шага обучения с разогревом и ранняя остановка, аугментация данных, остаточные связи и нормализация слоя.

На этой странице 14

Предыдущая заметка вычислила точные градиенты для сети любой глубины. Это не делает глубокие сети простыми в обучении. В стопке из 30 слоёв градиент, доходящий до первого слоя, — произведение 30 якобианов; у простой 30-слойной сети из раздела «Код» этой заметки его норма при инициализации равна 3.2⋅10−133.2 \cdot 10^{-13}, и ранние слои не сдвигаются с места; а сеть, достаточно гибкая, чтобы подогнаться под что угодно, подгоняется и под шум в метках обучающей выборки. Эта заметка собирает стандартные средства, которые не разобраны в других местах курсов: как инициализировать веса, дропаут, затухание весов, расписания шага обучения и раннюю остановку, аугментацию данных, остаточные связи и нормализацию слоя. Три соседние темы живут в курсе о генеративных моделях, и здесь на них только ссылки: батч-нормализация и нормализация по экземпляру, механизм затухающих и взрывающихся градиентов и подробный разбор запуска обучения; самое необходимое из этого порядка действий есть в рецепте по умолчанию в конце этой заметки.

Обозначения. Слой вычисляет zl=Wlhl−1z_l = W_l h_{l-1} (смещения при инициализации равны нулю и опущены) и hl=φ(zl)h_l = \varphi(z_l). nl−1n_{l-1} и nln_l — его число входов и число выходов (fan-in / fan-out), то есть входная и выходная ширины. Веса WlW_l в начале — независимые случайные величины со средним 0 и дисперсией vlv_l. η\eta — шаг обучения, λ\lambda — коэффициент затухания весов или L2-штрафа, pp — вероятность дропаута.

Инициализация, сохраняющая масштаб#

Почему не начать с нуля? Если все веса слоя в начале равны, каждый нейрон слоя вычисляет одну и ту же функцию и получает один и тот же градиент, так что нейроны навсегда остаются одинаковыми: слой ведёт себя как один нейрон. Случайные веса нарушают эту симметрию. Вопрос в их масштабе.

Прямой проход. Возьмём одну предактивацию zi=∑j=1nl−1Wijhjz_i = \sum_{j=1}^{n_{l-1}} W_{ij} h_j при инициализации. Веса независимы друг от друга и от входов hjh_j и имеют среднее 0. Слагаемые некоррелированы, поскольку E[WijhjWikhk]=E[Wij] E[Wik] E[hjhk]=0E[W_{ij} h_j W_{ik} h_k] = E[W_{ij}]\,E[W_{ik}]\,E[h_j h_k] = 0 при j≠kj \ne k, поэтому их дисперсии складываются (заметка о совместных распределениях). Дисперсия каждого слагаемого равна E[Wij2hj2]=vl E[hj2]E[W_{ij}^2 h_j^2] = v_l\, E[h_j^2]. Следовательно,

Var⁡(zl)=nl−1 vl E[hl−12].\operatorname{Var}(z_l) = n_{l-1}\, v_l\, E\big[h_{l-1}^2\big] .

Обратим внимание: здесь стоит второй момент E[h2]E[h^2], а не дисперсия hh, потому что после ReLU активации не центрированы (He et al., 2015, раздел 2.2).

  • Линейный режим (tanh около нуля, φ(z)≈z\varphi(z) \approx z): E[hl−12]=Var⁡(zl−1)E[h_{l-1}^2] = \operatorname{Var}(z_{l-1}), так что Var⁡(zl)=nl−1vlVar⁡(zl−1)\operatorname{Var}(z_l) = n_{l-1} v_l \operatorname{Var}(z_{l-1}), и дисперсия сохраняется при vl=1/nl−1v_l = 1/n_{l-1}.
  • ReLU: если zl−1z_{l-1} симметрична относительно нуля, ReLU оставляет половину её распределения и E[ReLU⁡(zl−1)2]=12Var⁡(zl−1)E[\operatorname{ReLU}(z_{l-1})^2] = \tfrac12 \operatorname{Var}(z_{l-1}). Тогда Var⁡(zl)=12nl−1vlVar⁡(zl−1)\operatorname{Var}(z_l) = \tfrac12 n_{l-1} v_l \operatorname{Var}(z_{l-1}), и дисперсия сохраняется при
vl=2nl−1(инициализация Хе).v_l = \frac{2}{n_{l-1}} \qquad \text{(инициализация Хе).}

За LL слоёв дисперсия умножается на ∏l12nl−1vl\prod_l \tfrac12 n_{l-1} v_l: любой постоянный множитель на слой, отличный от 1, геометрически растёт или убывает.

Обратный проход. Тот же аргумент, применённый к hˉl−1=Wl⊤zˉl\bar h_{l-1} = W_l^\top \bar z_l, суммирует по nln_l выходам, поэтому градиенты сохраняют масштаб при nlvl=1n_l v_l = 1 в линейном режиме или при 12nlvl=1\tfrac12 n_l v_l = 1 для ReLU. Прямое и обратное условия совпадают, только если nl−1=nln_{l-1} = n_l. Glorot and Bengio (2010), раздел 4.2.1, формулы 10–12 и 16, берут компромисс vl=2/(nl−1+nl)v_l = 2/(n_{l-1} + n_l) и выбирают веса из равномерного распределения на ±6/(nl−1+nl)\pm\sqrt{6/(n_{l-1} + n_l)}, дисперсия которого ровно такая (у равномерного распределения на ±a\pm a дисперсия a2/3a^2/3). Это инициализация Ксавье/Глоро (Xavier/Glorot initialization). Для ReLU He et al. (2015), формулы 10 и 14, показывают, что на практике достаточно сохранять масштаб в одном из направлений, поскольку два произведения отличаются лишь отношением первой и последней ширин; это инициализация Хе (He initialization).

Разобранный пример. Для квадратного слоя с n=256n = 256 инициализация Глоро даёт стандартное отклонение 2/512=0.0625\sqrt{2/512} = 0.0625, а инициализация Хе — 2/256=0.0884\sqrt{2/256} = 0.0884. С ReLU и дисперсией Глоро 1/n1/n каждый слой умножает дисперсию на 12\tfrac12: после 30 слоёв на выходе остаётся 2−29≈1.9⋅10−92^{-29} \approx 1.9 \cdot 10^{-9} от дисперсии первого слоя. С инициализацией Хе она не меняется; при v=4/nv = 4/n она вырастает в 229≈5.4⋅1082^{29} \approx 5.4 \cdot 10^{8} раз.

Рисунок

Масштаб сигнала в глубокой сети при разной инициализации

Средний квадрат предактиваций по слоямЛогарифмическая шкала: синяя линия He горизонтальна, красная (1/n) падает прямой вниз, жёлтая (4/n) растёт прямой вверх, зелёная tanh медленно опускается.10⁻¹⁰10⁻⁵110⁵10¹⁰1102030слой lReLU, 2/n (He)ReLU, 1/n (Glorot)ReLU, 4/ntanh, 1/nтеорияСредний квадрат предактиваций по слоямЛогарифмическая шкала: синяя линия He горизонтальна, красная (1/n) падает прямой вниз, жёлтая (4/n) растёт прямой вверх, зелёная tanh медленно опускается.10⁻¹⁰10⁻⁵110⁵10¹⁰1102030слой lReLU, 2/n (He)ReLU, 1/n (Glorot)ReLU, 4/ntanh, 1/nтеория
30 слоёв ширины n = 128 без смещений, 32 входа из N(0, 1); веса из N(0, v). Точки — средний квадрат предактиваций zₗ в симуляции, пунктир — теория Var zₗ = nv·(nv/2)^(l−1) для ReLU (логарифмическая шкала). С дисперсией He v = 2/n сигнал сохраняет масштаб (4.0 на 30-м слое, теория 2.0); с v = 1/n каждый слой ReLU делит его на 2 (3.7·10⁻⁹, теория 1.9·10⁻⁹); с v = 4/n удваивает (4.3·10⁹, теория 2.1·10⁹). Симуляция отличается от теории примерно вдвое: случайные веса каждого слоя чуть меняют его коэффициент усиления, и за 30 слоёв эти отклонения перемножаются. Для tanh дисперсия Глоро v = 1/n не даёт геометрического затухания: средний квадрат падает медленно, до 0.014.

Симуляция следует теории с точностью до случайных колебаний, и раздел «Код» повторяет её при ширине 256. Именно инициализация Хе позволяет простой 30-слойной ReLU-стопке вообще обучаться: в разделе «Код» та же стопка с инициализацией PyTorch по умолчанию имеет на первом слое градиент 3.2⋅10−133.2 \cdot 10^{-13} и остаётся на значении функции потерь ln⁡2=0.693\ln 2 = 0.693, как у подбрасывания монеты, а с инициализацией Хе подгоняется под данные. Инициализация PyTorch по умолчанию для nn.Linear и nn.Conv2d имеет дисперсию 1/(3nin)1/(3 n_{\text{in}}), в шесть раз меньше, чем у Хе; раздел о свёртках курса о генеративных моделях объясняет, откуда она берётся и почему слои нормализации это компенсируют. Инициализация задаёт масштаб только в начале; слои нормализации и остаточные связи, о которых ниже, держат его под контролем во время обучения.

Дропаут#

Дропаут (dropout; Srivastava et al., 2014) во время обучения случайно выключает каждый нейрон слоя. В форме, которую используют сегодня («инвертированный» дропаут), каждая активация hjh_j заменяется на

h~j=mj1−p hj,mj∼Bernoulli(1−p) независимо,\tilde h_j = \frac{m_j}{1 - p}\, h_j, \qquad m_j \sim \mathrm{Bernoulli}(1 - p) \text{ независимо},

со свежей маской для каждого объекта и каждого шага. Деление на 1−p1 - p сохраняет математическое ожидание, E[h~j]=hjE[\tilde h_j] = h_j, поэтому на тесте маску просто убирают (model.eval() в PyTorch; p в nn.Dropout(p) — вероятность выключения). В исходной статье нейроны сохраняются с вероятностью pp, а на тесте исходящие веса вместо этого умножаются на pp (Srivastava et al., 2014, раздел 1 и рисунок 2; раздел 10 отмечает эквивалентность): там pp — вероятность сохранения, а не выключения.

Ансамбль прореженных сетей. Сеть с NN выключаемыми нейронами содержит 2N2^N «прореженных» подсетей (thinned networks), по одной на маску, и все они разделяют одни и те же веса. Каждый шаг обучения обучает одну из них, выбранную случайно. На тесте полная сеть с масштабированием, сохраняющим математическое ожидание, заменяет среднее всех 2N2^N предсказаний. Это точно лишь в частных случаях, например для одного линейного слоя, но хорошо работает на практике: на MNIST усреднение предсказаний примерно 50 сэмплированных прореженных сетей сравнивается по качеству с масштабированной сетью (Srivastava et al., 2014, раздел 7.5). Как и бэггинг, дропаут усредняет много моделей и, как случайный лес, декоррелирует их, рандомизируя то, что видит каждая; в отличие от бэггинга, члены ансамбля разделяют веса.

Разобранный пример: дропаут как штраф. Для линейной модели с дропаутом на входах ожидаемую квадратичную ошибку можно вычислить в замкнутом виде. Запишем y^=∑jrjwjxj\hat y = \sum_j r_j w_j x_j с rj=mj/(1−p)r_j = m_j/(1 - p), так что E[rj]=1E[r_j] = 1 и Var⁡(rj)=p/(1−p)\operatorname{Var}(r_j) = p/(1 - p). Поскольку E[(y−y^)2]=(y−Ey^)2+Var⁡(y^)E[(y - \hat y)^2] = (y - E\hat y)^2 + \operatorname{Var}(\hat y),

E[(y−y^)2]=(y−w⊤x)2+p1−p∑jwj2xj2.E\big[(y - \hat y)^2\big] = (y - w^\top x)^2 + \frac{p}{1 - p} \sum_j w_j^2 x_j^2 .

Дропаут добавляет штраф наподобие гребневого, вес которого для wjw_j растёт с величиной признака jj и с вероятностью дропаута (Srivastava et al., 2014, раздел 9.1, в их параметризации). Для x=(1,2)x = (1, 2), w=(0.5,−0.25)w = (0.5, -0.25), y=1y = 1 и p=0.5p = 0.5: w⊤x=0w^\top x = 0, штраф равен 1⋅(0.25⋅1+0.0625⋅4)=0.51 \cdot (0.25 \cdot 1 + 0.0625 \cdot 4) = 0.5, а ожидаемая функция потерь — 1.51.5. Среднее методом Монте-Карло по миллиону масок в разделе «Код» даёт 1.4991.499. Сам гребневый штраф разобран в заметке о регуляризации.

Типичные вероятности — от p=0.1p = 0.1 до 0.50.5 на скрытых слоях. Дропаут важнее всего для больших полносвязных слоёв, обучаемых на скромных объёмах данных; трансформеры используют p=0.1p = 0.1 на весах внимания и остаточных ветвях, а свёрточные сети с батч-нормализацией часто обходятся малым дропаутом или вовсе без него.

С Adam затухание весов — не L2-штраф#

С SGD это одно и то же. Добавление λ2∥θ∥2\tfrac\lambda2 \lVert\theta\rVert^2 к функции потерь добавляет λθ\lambda\theta к градиенту, и шаг SGD становится

θ←θ−η(g+λθ)=(1−ηλ) θ−ηg:\theta \leftarrow \theta - \eta(g + \lambda\theta) = (1 - \eta\lambda)\,\theta - \eta g :

веса на каждом шаге уменьшаются в 1−ηλ1 - \eta\lambda раз, это и есть затухание весов (weight decay). Штраф — это отрицательный логарифм гауссовского априорного распределения, поэтому минимизатор — MAP-оценка.

С Adam они различаются. Adam делит каждую координату шага на скользящую оценку v^\sqrt{\hat v} среднеквадратичного значения градиента этой координаты. С L2-штрафом слагаемое λθ\lambda\theta входит в градиент и тоже делится: вес, у которого градиенты обычно большие, затухает меньше, а вес с маленькими градиентами — больше. Отделённое затухание весов (decoupled weight decay, AdamW) применяет сжатие вне адаптивного шага, как это делает AdamW в PyTorch:

θ←θ−ηλ θ−η m^v^+ϵ,\theta \leftarrow \theta - \eta\lambda\,\theta - \eta\, \frac{\hat m}{\sqrt{\hat v} + \epsilon} ,

так что каждый вес затухает с одной и той же скоростью ηλ\eta\lambda (Loshchilov and Hutter, 2019, раздел 2: предложение 1 об эквивалентности при SGD, предложение 2 о её нарушении для адаптивных методов, алгоритм 2 — AdamW, где затухание умножается на множитель расписания, но не на базовый шаг).

Разобранный пример. При η=10−3\eta = 10^{-3} и λ=10−2\lambda = 10^{-2} вес θ=1\theta = 1 под AdamW уменьшается на ηλ=10−5\eta\lambda = 10^{-5} за шаг, каковы бы ни были его градиенты. Под Adam с L2-штрафом уменьшение составляет примерно ηλθ/v^\eta\lambda\theta/\sqrt{\hat v} (если пренебречь небольшим вкладом самого штрафа в v^\hat v): 10−610^{-6}, если среднеквадратичное значение градиентов веса равно 10, и 10−410^{-4}, если оно равно 0.1. L2-вариант сильнее всего регуляризует веса, которые данные почти не ограничивают, и слабее всего — те, на которые данные давят сильнее всего; в этом примере разница стократная.

На практике AdamW с λ\lambda от 10−210^{-2} до 10−110^{-1} — выбор по умолчанию для трансформеров, а смещения и масштабы слоёв нормализации обычно освобождают от затухания. Курс о генеративных моделях использует AdamW с параметрами PyTorch по умолчанию.

Расписания шага обучения, разогрев и ранняя остановка#

Постоянный шаг обучения — компромисс. Большие шаги быстро продвигают обучение, но со стохастическими градиентами оставляют итерацию прыгать в области вокруг минимума, размер которой растёт с η\eta, как показано в заметке об оптимизаторах; маленькие шаги подходят ближе, но идут долго. Расписание (schedule) начинает с большого шага и уменьшает его:

  • Ступенчатое затухание (step decay): делить η\eta на 10 на фиксированных эпохах.
  • Косинусное затухание (cosine decay; Loshchilov and Hutter, 2017, раздел 3, формула 5): ηt=ηmin⁡+12(ηmax⁡−ηmin⁡)(1+cos⁡(πt/T))\eta_t = \eta_{\min} + \tfrac12(\eta_{\max} - \eta_{\min})\big(1 + \cos(\pi t/T)\big) — плавный спуск от ηmax⁡\eta_{\max} до ηmin⁡\eta_{\min} за TT эпох.
  • Обратный квадратный корень (inverse square root) — расписание исходного трансформера (Vaswani et al., 2017, раздел 5.3, формула 3): ηt∝min⁡(t−1/2, t⋅w−3/2)\eta_t \propto \min\big(t^{-1/2},\ t \cdot w^{-3/2}\big), линейный рост в течение ww шагов (там w=4000w = 4000), а затем убывание как 1/t1/\sqrt t.

Разогрев (warm-up) начинает с маленького шага и линейно увеличивает его в течение первых шагов. В начале веса случайны, градиенты большие и несогласованные, а оценка v^\hat v в Adam опирается на очень малое число шагов; шаг полного размера может тогда отбросить сеть в плохую область. Goyal et al. (2017), раздел 2.2, понадобился постепенный разогрев в течение 5 эпох, чтобы обучить ResNet-50 с большими батчами и соответственно большим шагом, а трансформерам с post-norm он нужен из-за больших градиентов около выхода при инициализации, как обсуждается в заметке о трансформере.

Рисунок

Три расписания шага обучения

Расписания шага обученияКрасная ступенчатая линия, синяя косинусная кривая с коротким линейным подъёмом и зелёная пунктирная кривая с подъёмом и медленным спадом.00.510255075100эпоха tη / ηₘₐₓступенчатое: ×0.1 на 50 и 75разогрев 5 эпох + косинусразогрев 10 + 1/√tРасписания шага обученияКрасная ступенчатая линия, синяя косинусная кривая с коротким линейным подъёмом и зелёная пунктирная кривая с подъёмом и медленным спадом.00.510255075100эпоха tη / ηₘₐₓступенчатое: ×0.1 на 50 и 75разогрев 5 эпох + косинусразогрев 10 + 1/√t
Доля от максимального шага ηₘₐₓ за 100 эпох. Ступенчатое расписание делит шаг на 10 на 50-й и 75-й эпохах. Косинусное после линейного разогрева за 5 эпох плавно спускается до нуля и проходит половину пути на 52.5-й эпохе. Расписание Transformer растёт линейно 10 эпох, затем убывает как 1/√t: 0.5 на 40-й эпохе и 0.316 на 100-й.

Разобранный пример. При ηmax⁡=10−3\eta_{\max} = 10^{-3}, ηmin⁡=0\eta_{\min} = 0 и T=100T = 100 косинусное расписание даёт η25=0.5⋅10−3(1+cos⁡(π/4))=8.54⋅10−4\eta_{25} = 0.5 \cdot 10^{-3}(1 + \cos(\pi/4)) = 8.54 \cdot 10^{-4}, η50=5⋅10−4\eta_{50} = 5 \cdot 10^{-4} и η75=1.46⋅10−4\eta_{75} = 1.46 \cdot 10^{-4}: оно дольше линейного затухания остаётся около пика и проводит последнюю четверть на маленьких шагах.

Ранняя остановка (early stopping) использует число шагов как регуляризатор. Ошибка на обучении продолжает падать, а ошибка на валидации сначала падает, а затем растёт, когда сеть начинает подгоняться под шум; сохраняют контрольную точку с наименьшей функцией потерь на валидации и останавливаются, когда она не улучшалась заданное число эпох (терпение, patience). Для квадратичной функции потерь τ\tau шагов градиентного спуска с шагом η\eta действуют приблизительно как L2-штраф с λ≈1/(ητ)\lambda \approx 1/(\eta\tau) (Goodfellow, Bengio and Courville, 2016, раздел 7.8; Bishop, 2006, раздел 5.5.2): чем меньше шагов, тем сильнее регуляризация. В разделе «Код» сеть обучается на 300 цифрах, у 30% которых метки случайные. Функция потерь на валидации минимальна на эпохе 127 (0.5920.592) и к эпохе 300 вырастает до 0.7570.757; контрольная точка с эпохи 127 верно классифицирует 0.8810.881 тестовых цифр, последняя — только 0.7880.788. Валидационная выборка здесь используется для принятия решения, поэтому сообщаемая доля правильных ответов должна быть получена на отдельной тестовой выборке, как настаивает заметка о валидации.

Аугментация данных#

Когда известно, что метка не меняется при некоторых преобразованиях входа, можно обучаться на преобразованных копиях: случайные кадрирования, отражения и изменения цвета для фотографий, сдвиги во времени и добавленный шум для звука, выбрасывание слов или обратный перевод для текста. Модель в каждой эпохе видит новый вариант каждого примера и вынуждена становиться инвариантной к этим изменениям. Формально функция потерь на обучении становится математическим ожиданием по случайному преобразованию τ\tau,

1n∑i=1nEτ[ℓ(f(τ(xi)),yi)],\frac1n \sum_{i=1}^n E_\tau\big[\ell\big(f(\tau(x_i)), y_i\big)\big],

и каждый мини-батч оценивает его с одним сэмплированным τ\tau на объект — это оценка методом Монте-Карло. Преобразования кодируют априорное знание, и неверное знание вредит: горизонтально отражённая «b» — это «d», а 6, повёрнутая на 180°, — это 9. AlexNet обучалась на случайных кадрах 224×224224 \times 224 и их зеркальных отражениях, что увеличило обучающую выборку в 2048 раз, а на тесте усредняла предсказания по десяти кадрам (Krizhevsky, Sutskever and Hinton, 2012, раздел 4.1). Mixup идёт дальше и обучается на выпуклых комбинациях пар примеров и их меток (Zhang et al., 2018, раздел 2).

Разобранный пример. В разделе «Код» сеть обучается на 300 цифрах — один раз на них как есть и один раз с четырьмя дополнительными копиями, сдвинутыми на один пиксель. На тестовых цифрах, центрированных так же, как обучающие, обе сети верно классифицируют 0.940.94 (0.9380.938 и 0.9370.937). На тестовых цифрах, сдвинутых на один пиксель в случайном направлении, обычная сеть падает до 0.460.46, а сеть с аугментацией сохраняет 0.8880.888. Аугментация покупает устойчивость к той изменчивости, которую покрывает; повысит ли она качество на тесте, зависит от того, есть ли эта изменчивость в тестовых данных.

Остаточные связи#

Глубже не значит автоматически лучше, даже на обучающей выборке. У простой 56-слойной свёрточной сети на CIFAR-10 ошибка на обучении выше, чем у 20-слойной (He et al., 2016a, рисунок 1). Эта деградация (degradation) — не переобучение, ведь более глубокая модель могла бы скопировать менее глубокую и сделать лишние слои тождественными; просто оптимизатор не находит таких решений. Остаточная связь (residual connection) делает тождественное отображение вариантом по умолчанию:

xl+1=xl+Fl(xl),x_{l+1} = x_l + F_l(x_l),

где FlF_l — небольшая стопка слоёв, остаточная ветвь. При малых весах Fl≈0F_l \approx 0, и блок пропускает вход насквозь; блоку нужно выучить только поправку.

Два следствия объясняют, почему это помогает оптимизации (He et al., 2016b, формулы 4–5). В развёрнутом виде выход глубокой стопки — это вход плюс сумма поправок, xL=xl+∑i=lL−1Fi(xi)x_L = x_l + \sum_{i=l}^{L-1} F_i(x_i), а не произведение. А по цепному правилу

∂L∂xl=∂L∂xL(I+∂∂xl∑i=lL−1Fi(xi)),\frac{\partial L}{\partial x_l} = \frac{\partial L}{\partial x_L}\Big(I + \frac{\partial}{\partial x_l} \sum_{i=l}^{L-1} F_i(x_i)\Big),

так что градиент доходит до каждого слоя через тождественное слагаемое, не проходя ни через одну матрицу весов. Это делает затухание градиента гораздо менее вероятным, но не гарантирует его отсутствия: второе слагаемое может погасить тождественное. Ветвь Fl(x)=−xF_l(x) = -x выдаёт xl+1=0x_{l+1} = 0 при любом входе, и якобиан блока I+(−I)I + (-I) равен нулю. Эквивалентно, стопка из nn остаточных блоков содержит 2n2^n путей разной длины, в зависимости от того, через какие блоки проходит каждый путь, и большая часть градиента 110-слойной остаточной сети течёт по путям глубиной всего от 10 до 34 слоёв (Veit, Wilber and Belongie, 2016).

В разделе «Код» стопка из 30 слоёв ширины 32 с инициализацией PyTorch по умолчанию имеет на первом слое градиент 3.2⋅10−133.2 \cdot 10^{-13} и не уходит со значения функции потерь 0.6930.693, а те же слои с остаточными связями получают на первом слое градиент 2.42.4 и идеально подгоняются под данные «две луны» за 200 шагов Adam. Остаточные связи требуют, чтобы вход и выход блока имели одинаковую форму (или проекции на обходном пути); благодаря им ResNet достигают сотен слоёв, а трансформеры складывают в стопку десятки блоков. Курс о генеративных моделях называет их среди средств против затухающих градиентов, рядом с рекуррентными ячейками с гейтами.

Нормализация слоя#

Слои нормализации перемасштабируют активации во время обучения, а не только при инициализации. Батч-нормализация стандартизирует каждый признак по объектам батча, что связывает каждый выход с остальной частью батча и требует скользящих средних при инференсе (курс о генеративных моделях). Нормализация слоя (layer normalization; Ba, Kiros and Hinton, 2016) стандартизирует каждый объект по его собственным признакам, LN⁡(x)=γ⊙(x−μ)/σ2+ϵ+β\operatorname{LN}(x) = \gamma \odot (x - \mu)/\sqrt{\sigma^2 + \epsilon} + \beta, где μ\mu и σ2\sigma^2 — среднее и дисперсия dd компонент xx. Она не зависит от батча, одинаково ведёт себя при обучении и инференсе и работает с последовательностями любой длины, поэтому её (или её вариант RMSNorm) использует каждый трансформер, как описано в заметке о трансформере. Таблица слоёв нормализации в курсе о генеративных моделях сравнивает, какие значения делят общее среднее и дисперсию в батч-нормализации, нормализации по экземпляру и нормализации слоя.

Рецепт по умолчанию#

В сумме разумная отправная точка для новой сети такова:

  1. Инициализация Хе для слоёв с активациями семейства ReLU (или инициализация библиотеки по умолчанию плюс слои нормализации).
  2. Остаточные связи и слой нормализации, как только в сети больше нескольких слоёв.
  3. AdamW с затуханием весов от 10−210^{-2} до 10−110^{-1}, не применяемым к смещениям и параметрам нормализации.
  4. Пиковый шаг обучения, найденный коротким перебором, линейный разогрев, затем косинусное затухание.
  5. Аугментация для каждой инвариантности, которая действительно есть у задачи; дропаут там, где сеть всё ещё переобучается.
  6. Валидационная выборка, проверяемая каждую эпоху, с сохранением лучшей контрольной точки; а до всего этого — проверка, что сеть может довести функцию потерь почти до нуля на одном маленьком батче: это ловит большинство ошибок в модели и функции потерь.
  7. Оценка в режиме оценки: model.eval() отключает дропаут и заставляет батч-нормализацию использовать накопленные статистики, torch.no_grad() не строит граф, а model.train() возвращает режим обучения перед следующим шагом. Контрольная точка хранит состояние модели и оптимизатора и номер эпохи, чтобы запуск можно было продолжить или вернуться к его лучшей точке. Фиксированные начальные значения генераторов (torch.manual_seed и генератор NumPy) делают запуск воспроизводимым, а несколько таких значений показывают, какая часть разницы — шум. Тестовая выборка оценивается один раз, в конце, с настройками, выбранными на валидации.

Что изменилось#

Простая глубокая сетьСо средствами из этой заметки
Масштаб сигнала в началегеометрически убывает или растёт с глубинойсохраняется инициализацией Хе или Глоро
Путь градиента к ранним слоямпроизведение одного якобиана на слойтождественный путь через остаточные связи
Регуляризациянет: сеть подгоняется под шум в меткахдропаут, отделённое затухание весов, аугментация, ранняя остановка
Шаг обученияодна константаразогрев, затем затухание
Нормализациянетбатч-нормализация для свёрточных сетей, нормализация слоя для трансформеров

Ограничения#

  • Выводы верны при инициализации. Сохранение дисперсии предполагает независимость весов и входов; после начала обучения веса становятся коррелированы с данными, и масштаб держит под контролем только нормализация.
  • Дропаут не бесплатен. Он замедляет сходимость, взаимодействует с батч-нормализацией, а масштабирование на тесте лишь приближает среднее по ансамблю.
  • Каждое средство добавляет гиперпараметры: вероятность дропаута, λ\lambda, пиковый шаг, длину разогрева, терпение. Они взаимодействуют, и значения по умолчанию выше — отправные точки, а не гарантии.
  • Аугментации нужно знание предметной области. Преобразование, меняющее метку, учит неверной инвариантности, а для табличных данных естественного преобразования обычно нет.
  • Теория отстаёт от практики. Почему помогают остаточные связи и нормализация, отчасти объяснено (распространение сигнала, более гладкие функции потерь), а отчасти всё ещё обсуждается.

Что нужно запомнить#

  • Инициализацию выбирают так, чтобы Var⁡(zl)\operatorname{Var}(z_l) оставалась постоянной: v=2/ninv = 2/n_{\text{in}} для ReLU (Хе), v=2/(nin+nout)v = 2/(n_{\text{in}} + n_{\text{out}}) для tanh (Глоро); любой другой постоянный множитель на слой геометрически растёт или убывает.
  • Дропаут умножает активации на m/(1−p)m/(1-p) с m∼Bernoulli(1−p)m \sim \mathrm{Bernoulli}(1-p) только во время обучения; он обучает ансамбль из 2N2^N подсетей с общими весами и для линейной модели действует как гребневый штраф, масштабированный по признакам.
  • При SGD L2-штраф — это затухание весов; при Adam — нет, а в AdamW каждый вес затухает со скоростью ηλ\eta\lambda.
  • Сначала разогрев, потом затухание шага обучения (ступенчатое, косинусное, 1/t1/\sqrt t); остановка на лучшей валидационной контрольной точке действует как L2-штраф λ≈1/(ητ)\lambda \approx 1/(\eta\tau).
  • Аугментация обучает на преобразованиях, сохраняющих метку, и покупает ровно те инвариантности, которые покрывает.
  • Остаточные связи x+F(x)x + F(x) дают градиенту тождественный путь; нормализация слоя стандартизирует каждый объект по его признакам независимо от батча.

Код#

Масштаб активаций при трёх инициализациях, дропаут как штраф, сжатие весов в AdamW против L2, ранняя остановка на зашумлённых метках, аугментация сдвигами и 30-слойная сеть с инициализацией Хе и остаточными связями и без них:

import numpy as np, torch
from torch import nn
from sklearn.datasets import load_digits, make_moons
from sklearn.model_selection import train_test_split

T = lambda a: torch.tensor(a, dtype=torch.float32)

# 1. масштаб z_30 в стопке из 30 ReLU-слоёв ширины 256 при дисперсиях весов 1/n, 2/n, 4/n
#    (теория: 2^-29 = 1.9e-9, 2, 4 * 2^29 = 2.1e9)
torch.manual_seed(0)
x = torch.randn(512, 256)
for var in (1 / 256, 2 / 256, 4 / 256):
    h = x
    for _ in range(30):
        z = h @ (torch.randn(256, 256) * var**0.5)
        h = torch.relu(z)
    print(f'{z.pow(2).mean().item():.2g}', end=' ')
print()                                                  # 1e-09 1.1 7.5e+09

# 2. дропаут в линейной модели: E(y - sum m_j w_j x_j / (1-p))^2 = (y - w.x)^2 + p/(1-p) sum w_j^2 x_j^2
xv, w, yv, p = np.array([1.0, 2.0]), np.array([0.5, -0.25]), 1.0, 0.5
m = np.random.default_rng(0).random((1_000_000, 2)) > p   # каждый вход сохраняется с вероятностью 1 - p
print(round(((yv - (m / (1 - p) * w * xv).sum(1)) ** 2).mean(), 3), (yv - w @ xv) ** 2 + p / (1 - p) * (w**2 * xv**2).sum())   # 1.499 1.5

# 3. затухание весов: уменьшение за шаг веса theta = 1 под Adam с L2 (eta*lam*theta / RMS его
#    градиентов) и под AdamW (eta*lam*theta) при RMS градиентов 10 и 0.1
eta, lam = 1e-3, 1e-2
print([f'{rms}: {eta * lam / rms:.0e} vs {eta * lam:.0e}' for rms in (10.0, 0.1)])   # ['10.0: 1e-06 vs 1e-05', '0.1: 1e-04 vs 1e-05']

# 4. ранняя остановка: 300 цифр, у 30% которых метки заменены случайными, сеть 64 -> 256 -> 10, Adam по всей выборке
D = load_digits()
Xtr, Xrest, ytr, yrest = train_test_split(D.data / 16, D.target, train_size=300, random_state=0, stratify=D.target)
Xva, Xte, yva, yte = train_test_split(Xrest, yrest, train_size=300, random_state=0, stratify=yrest)
rng = np.random.default_rng(0); noisy = ytr.copy(); flip = rng.random(300) < 0.3; noisy[flip] = rng.integers(0, 10, flip.sum())
torch.manual_seed(0)
net = nn.Sequential(nn.Linear(64, 256), nn.ReLU(), nn.Linear(256, 10))
opt = torch.optim.Adam(net.parameters(), lr=1e-3)
best = (np.inf, 0, None)
for epoch in range(1, 301):
    opt.zero_grad(); nn.functional.cross_entropy(net(T(Xtr)), torch.tensor(noisy)).backward(); opt.step()
    with torch.no_grad():
        val = nn.functional.cross_entropy(net(T(Xva)), torch.tensor(yva)).item()
    if val < best[0]:
        best = (val, epoch, {k: v.clone() for k, v in net.state_dict().items()})
with torch.no_grad():
    last_acc = (net(T(Xte)).argmax(1).numpy() == yte).mean()
    net.load_state_dict(best[2]); best_acc = (net(T(Xte)).argmax(1).numpy() == yte).mean()
print(best[1], round(best[0], 3), round(val, 3), round(best_acc, 3), round(last_acc, 3))   # 127 0.592 0.757 0.881 0.788

# 5. аугментация: 300 обучающих цифр с копиями, сдвинутыми на один пиксель, и без них; тест на центрированных
#    изображениях и на изображениях, сдвинутых на один пиксель в случайном направлении
def shift(img, dx, dy):
    out = np.zeros_like(img)
    out[max(dy, 0):8 + min(dy, 0), max(dx, 0):8 + min(dx, 0)] = img[max(-dy, 0):8 + min(-dy, 0), max(-dx, 0):8 + min(-dx, 0)]
    return out
moves = [(1, 0), (-1, 0), (0, 1), (0, -1)]
Itr, Ite, ltr, lte = train_test_split(D.images / 16, D.target, train_size=300, random_state=0, stratify=D.target)
aug = np.concatenate([Itr] + [np.stack([shift(i, *mv) for i in Itr]) for mv in moves])
Ish = np.stack([shift(i, *moves[k]) for i, k in zip(Ite, np.random.default_rng(1).integers(0, 4, len(Ite)))])
def fit(X, y, epochs):
    torch.manual_seed(0)
    net = nn.Sequential(nn.Flatten(), nn.Linear(64, 128), nn.ReLU(), nn.Linear(128, 10))
    opt = torch.optim.Adam(net.parameters(), lr=1e-3)
    for _ in range(epochs):
        for idx in torch.randperm(len(X)).split(64):
            opt.zero_grad(); nn.functional.cross_entropy(net(T(X[idx])), torch.tensor(y[idx])).backward(); opt.step()
    return lambda Z: (net(T(Z)).argmax(1).numpy() == lte).mean().round(3)
plain, augmented = fit(Itr, ltr, 100), fit(aug, np.tile(ltr, 5), 60)
print(plain(Ite), augmented(Ite), plain(Ish), augmented(Ish))   # 0.938 0.937 0.46 0.888

# 6. глубина 30, ширина 32, две луны: простая стопка с инициализацией PyTorch по умолчанию, та же стопка с
#    инициализацией Хе и стопка по умолчанию с остаточными связями; норма градиента первого слоя при
#    инициализации, затем функция потерь на обучении и доля правильных ответов после 200 шагов Adam
Xm, ym = make_moons(500, noise=0.1, random_state=0)
X, Y = T(Xm), T(ym)
class Deep(nn.Module):
    def __init__(self, residual, he):
        super().__init__()
        self.inp, self.out, self.residual = nn.Linear(2, 32), nn.Linear(32, 1), residual
        self.blocks = nn.ModuleList(nn.Linear(32, 32) for _ in range(30))
        if he:
            for layer in [self.inp, *self.blocks, self.out]:
                nn.init.kaiming_normal_(layer.weight, nonlinearity='relu'); nn.init.zeros_(layer.bias)
    def forward(self, x):
        h = self.inp(x)
        for block in self.blocks:
            h = h + block(torch.relu(h)) if self.residual else block(torch.relu(h))
        return self.out(torch.relu(h)).squeeze(1)
for residual, he in ((False, False), (False, True), (True, False)):
    torch.manual_seed(0); net = Deep(residual, he)
    nn.functional.binary_cross_entropy_with_logits(net(X), Y).backward()
    g0 = net.inp.weight.grad.norm().item()
    opt = torch.optim.Adam(net.parameters(), lr=1e-3)
    for _ in range(200):
        opt.zero_grad(); loss = nn.functional.binary_cross_entropy_with_logits(net(X), Y); loss.backward(); opt.step()
    print(residual, he, f'{g0:.1e}', f'{loss.item():.3f}', ((net(X) > 0).float() == Y).float().mean().item())
# False False 3.2e-13 0.693 0.5 | False True 1.8e-01 0.000 1.0 | True False 2.4e+00 0.000 1.0

Дальше#

Все методы до сих пор, от ближайших соседей до глубоких сетей, учились на примерах с метками: цена, класс, оценка. Часто меток нет вовсе, есть только входы, и вопрос в том, какая у них структура: группы похожих клиентов, типичные и необычные транзакции, несколько направлений, объясняющих большую часть разброса. Следующая заметка открывает часть курса об обучении без учителя кластеризацией, начиная с k-средних и его функции потерь.

Литература#