🔴 Сложный ⏱️ 55 минут

Batch normalization

📋 Содержание урока

Batch normalization 🎚️

В уроке 325 ты нормализовал входные признаки: вычёл среднее, поделил на стандартное отклонение — и подал в модель уже приведённые к разумному масштабу числа. Это разовая операция на границе между сырыми данными и моделью: сделал один раз перед обучением — и забыл. А что происходит внутри самой сети, между слоями? Выход первого скрытого слоя — это тоже вектор чисел, который дальше становится «входным признаком» для второго слоя. Нужна ли и ему такая же нормализация?

Проблема в том, что вход второго слоя — это не сырые, один раз зафиксированные данные. Это результат умножения на матрицу весов $W^{(1)}$, которая сама меняется на каждом шаге градиентного спуска. В уроке 331 ты разобрал, как правильно инициализировать веса, чтобы на первом прямом проходе дисперсия активаций сохранялась слой за слоем. Но это гарантия только для шага $t=0$. Как только оптимизатор чуть сдвинул $W^{(1)}$, распределение выхода первого слоя тоже сдвинулось — а значит, второй слой, который только-только начал подстраиваться под одно распределение входа, вдруг обнаруживает, что вход снова стал другим. Третий слой в это время подстраивается под постоянно меняющийся выход второго. Чем больше слоёв, тем сильнее эффект накапливается: последние слои глубокой сети гоняются за целью, которая непрерывно убегает. Это явление в 2015 году получило имя — internal covariate shift, внутренний сдвиг ковариат: распределение входов каждого слоя меняется в процессе обучения по мере того, как меняются веса всех предыдущих слоёв.

Идея решения обманчиво проста: если проблема в том, что распределение активаций «гуляет», давай не полагаться на удачную инициализацию и надежду, что всё останется стабильным, а будем активно возвращать активации к стандартному распределению на каждом шаге, прямо внутри сети. Берём мини-батч примеров, для каждого нейрона слоя считаем среднее и дисперсию по этому батчу — и нормализуем, ровно как в уроке 325, только теперь это происходит не один раз на входе, а заново на каждом forward pass, на каждом слое, где эта нормализация вставлена. Но здесь появляется тонкость: если жёстко навязать каждому слою вход строго со средним $0$ и дисперсией $1$, ты рискуешь отобрать у сети выразительную силу. Поэтому к нормализации добавляют два обучаемых параметра — масштаб $\gamma$ и сдвиг $\beta$, — которые позволяют сети при необходимости частично или полностью «откатить» нормализацию назад. Это и есть Batch Normalization (BatchNorm, пакетная нормализация) — слой, ставший практически обязательным компонентом почти любой современной свёрточной архитектуры.

Сегодня ты разберёшь: откуда взялась идея и кто её предложил; точную формулу с обучаемыми $\gamma$ и $\beta$; почему поведение слоя на обучении принципиально отличается от поведения на инференсе; какие побочные положительные эффекты даёт BatchNorm помимо решения исходной проблемы; и чем от него отличается Layer Normalization — альтернатива, без которой не обходится ни одна трансформерная архитектура.

🎯 Ты узнаешь:

  • Что такое internal covariate shift и почему хорошая инициализация весов (урок 331) решает эту проблему только в момент $t=0$, но не на протяжении всего обучения
  • Точную формулу BatchNorm с нормализацией по мини-батчу и обучаемыми параметрами $\gamma$ (масштаб) и $\beta$ (сдвиг)
  • Почему обучаемые $\gamma,\beta$ делают BatchNorm гибким ограничением, а не жёстким — сеть в принципе может отменить нормализацию, если сочтёт это полезным
  • Куда именно в архитектуре вставляется BatchNorm и почему именно туда
  • Чем поведение слоя на обучении (model.train()) отличается от поведения на инференсе (model.eval()) и откуда берутся running-статистики
  • Какие побочные эффекты даёт BatchNorm — более высокий learning rate, лёгкая регуляризация, устойчивость к неидеальной инициализации — и чем Layer Normalization отличается от BatchNorm и почему она обязательна в трансформерах

История

В 2015 году исследователи Google Сергей Иоффе и Кристиан Сегеди опубликовали статью «Batch Normalization: Accelerating Deep Network Training by Reducing Internal Covariate Shift» («Пакетная нормализация: ускорение обучения глубоких сетей через уменьшение внутреннего сдвига ковариат»). Оба на тот момент работали над архитектурами семейства Inception — одними из самых глубоких сверточных сетей своего времени, обучение которых было мучительно чувствительным к скорости обучения и требовало крайне аккуратной, буквально ручной настройки гиперпараметров. Идея, которую они предложили, была одновременно простой по формулировке и совершенно нетривиальной по последствиям: нормализовать активации не один раз на входе сети, а на каждом слое, прямо во время обучения, используя статистики текущего мини-батча.

Результаты в статье были эффектными. Сеть Inception с добавленным BatchNorm достигала точности исходной модели на ImageNet за в 14 раз меньшее число шагов обучения, при этом позволяя использовать значительно более высокий learning rate без риска расхождения. Ансамбль из нескольких таких сетей на момент публикации превзошёл предыдущий рекорд точности классификации ImageNet. После этой статьи BatchNorm почти мгновенно стал стандартным строительным блоком: сложно найти сверточную архитектуру второй половины 2010-х — ResNet, Inception-v3, DenseNet, EfficientNet, — где не было бы слоя BatchNorm между свёрткой и активацией.

Интересный поворот случился три года спустя. В 2018 году исследователи из MIT — Шибани Сантуркар, Дмитрий Ципрас, Андрей Ильяс и Александр Мадри — опубликовали работу «How Does Batch Normalization Help Optimization?», в которой экспериментально показали: заявленная причина эффективности BatchNorm — «уменьшение internal covariate shift» — работает не совсем так, как предполагалось изначально. Они специально сконструировали сети, где BatchNorm математически увеличивал сдвиг распределений между слоями по сравнению с сетью без него, — и такие сети всё равно обучались быстрее и стабильнее. Более убедительным объяснением оказалось то, что BatchNorm сглаживает ландшафт функции потерь: делает градиенты более предсказуемыми и позволяет уверенно делать более крупные шаги оптимизации. Этот эпизод — хороший урок сам по себе: BatchNorm остаётся одним из самых практически полезных изобретений в глубоком обучении, даже притом что исходное теоретическое объяснение его эффекта оказалось неполным. Алгоритм пережил собственное обоснование.


Internal covariate shift: почему цель для каждого слоя постоянно убегает

Интуиция

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

Формально: вход слоя $l$ на шаге обучения $t$ — это $z^{(l)}_t = W^{(l)}_t a^{(l-1)}_t + b^{(l)}_t$, где $a^{(l-1)}_t$ сам является функцией всех весов $W^{(1)}_t,\dots,W^{(l-1)}_t$ на текущем шаге. Пока оптимизатор обновляет эти веса на каждой итерации (а он это делает постоянно — в этом и состоит обучение), распределение $z^{(l)}$ неизбежно меняется от шага к шагу, даже если сам слой $l$ ещё не обновлял собственные веса.

Формула

Internal covariate shift (внутренний сдвиг ковариат). Пусть $P_l^{(t)}$ — распределение входа слоя $l$ на шаге обучения $t$, порождённое текущими весами всех предыдущих слоёв. Internal covariate shift — это изменение $P_l^{(t)}$ от шага к шагу:

$$P_l^{(t)} \neq P_l^{(t+1)} \neq P_l^{(t+2)} \neq \ldots$$

вызванное тем, что параметры $W^{(1)},\dots,W^{(l-1)}$ обновляются градиентным спуском на каждом шаге, и слою $l$ приходится обучаться, целясь по постоянно смещающейся мишени.

Разбор примеров

Пример 1 (численная иллюстрация сдвига распределения на соседнем слое). Пусть слой 1 состоит из одного нейрона с весом $w$ и смещением $b=0$, принимающего скалярный вход $x=2$, без активации: $z=wx$. В начале обучения $w_0=0{,}5$, значит $z_0 = 0{,}5\cdot2=1{,}0$ — это и есть вход слоя 2. После нескольких шагов градиентного спуска вес подрос до $w_1=1{,}3$ (обычное дело — веса меняются на десятки процентов за первые сотни итераций), тогда $z_1=1{,}3\cdot2=2{,}6$. Слой 2, который только начал подстраивать свои веса под вход около $1{,}0$, вдруг получает вход $2{,}6$ — более чем в два раза больше. Если у слоя 2 сигмоидная активация, разница драматична: $\sigma(1{,}0)\approx0{,}731$, а $\sigma(2{,}6)\approx0{,}931$ — совсем другая рабочая точка на кривой, с совсем другой производной ($\sigma'(1{,}0)\approx0{,}197$ против $\sigma'(2{,}6)\approx0{,}064$, то есть градиент, проходящий через слой 2, уже втрое слабее).

Пример 2 (чем это отличается от нормализации входа из урока 325). В уроке 325 ты нормализовал матрицу признаков $X$ ровно один раз, до начала обучения: $\hat X = (X-\mu_X)/\sigma_X$, где $\mu_X,\sigma_X$ — фиксированные числа, посчитанные по всему обучающему датасету раз и навсегда. Эта нормализация никогда не меняется, сколько бы шагов обучения ни прошло, — она относится к сырым данным, которые сами по себе не зависят от весов модели. А «признаки», которые видит второй скрытый слой сети, — это активации первого слоя, то есть функция от обучаемых весов $W^{(1)}$. Статичной нормализации здесь попросту не существует: любое число, посчитанное по активациям на шаге $t$, устареет уже на шаге $t+1$, как только $W^{(1)}$ обновится.

Пример 3 (как это усиливается с глубиной — связь с уроком 331). В уроке 331 мы говорили: правильная инициализация (например, He-инициализация для ReLU) сохраняет дисперсию активаций $\approx 1$ на первом forward pass через все слои. Пусть в 10-слойной сети на шаге $t=0$ дисперсия активаций на каждом слое действительно держится около $1{,}0$ — инициализация сработала идеально. К шагу $t=2000$ веса первых трёх слоёв заметно подросли под влиянием градиентов (это нормально — так модель обучается), и дисперсия активаций на выходе третьего слоя сместилась до, скажем, $4{,}0$. Четвёртый слой, спроектированный (через инициализацию) под вход с дисперсией $1{,}0$, теперь получает вход, «разбросанный» вчетверо сильнее, — эффективно попадая в ту же ловушку, для решения которой был выбран He, только теперь уже не на старте, а в разгар обучения, и никакая инициализация это не предотвратит: она отвечает только за шаг $t=0$.

Почему это важно

Internal covariate shift — не абстрактная теоретическая придирка, а конкретная практическая причина, по которой глубокие сети до 2015 года обучались медленно и капризно даже при аккуратной инициализации и удачном выборе активации. Каждому слою приходится тратить часть своей «обучающей способности» не на выучивание полезных признаков, а на постоянную подстройку под меняющийся масштаб и центр входного распределения — а это заставляет использовать очень маленький learning rate (чтобы веса менялись плавно и не создавали слишком резких скачков для последующих слоёв) и удлиняет обучение. Проблема тем острее, чем глубже сеть: сдвиг, вызванный первым слоем, проходит через все последующие и накапливается.


Формула Batch Normalization: нормализация по мини-батчу с обучаемыми γ и β

Интуиция

Раз проблема в том, что распределение активаций каждого слоя всё время «гуляет», логичный ответ — не полагаться на то, что оно останется стабильным, а активно возвращать его к стандартному виду на каждом шаге. Batch Normalization делает именно то, что ты делал в уроке 325 с входными признаками — вычитает среднее и делит на стандартное отклонение, — но теперь эти среднее и отклонение считаются заново на каждом прямом проходе, по текущему мини-батчу, прямо внутри сети, для активаций конкретного слоя. Но здесь важна вторая часть идеи: жёстко навязанный стандарт (среднее ровно $0$, дисперсия ровно $1$) может оказаться неоптимальным для конкретного нейрона — например, сигмоиде иногда полезно работать не строго вокруг нуля, а смещённо, чтобы использовать более насыщенный участок кривой. Поэтому после нормализации добавляют два обучаемых параметра — масштаб $\gamma$ и сдвиг $\beta$, — которые позволяют сети выучить собственный, оптимальный для неё масштаб и центр, вплоть до полной отмены нормализации.

Формула

Batch Normalization. Для мини-батча из $m$ примеров и активации одного нейрона $\{x_1,\dots,x_m\}$:

$$\mu_B = \frac{1}{m}\sum_{i=1}^m x_i \qquad \text{(среднее по батчу)}$$$$\sigma_B^2 = \frac{1}{m}\sum_{i=1}^m (x_i-\mu_B)^2 \qquad \text{(дисперсия по батчу)}$$$$\hat x_i = \frac{x_i-\mu_B}{\sqrt{\sigma_B^2+\varepsilon}} \qquad \text{(нормализация, } \varepsilon\approx 10^{-5}\text{ для численной устойчивости)}$$$$y_i = \gamma\,\hat x_i + \beta \qquad \text{(масштаб и сдвиг — обучаемые параметры)}$$

Каждый нейрон слоя имеет собственную пару $(\gamma,\beta)$, обучаемую обычным градиентным спуском вместе со всеми остальными весами сети.

Разбор примеров

Пример 1 (полная ручная нормализация мини-батча из четырёх значений). Пусть один нейрон полносвязного слоя выдал для мини-батча из четырёх примеров значения $x=[1,3,5,7]$.

Шаг 1. Среднее по батчу: $\mu_B = \dfrac{1+3+5+7}{4} = \dfrac{16}{4} = 4$.

Шаг 2. Отклонения от среднего: $-3,-1,1,3$. Дисперсия: $\sigma_B^2 = \dfrac{9+1+1+9}{4} = \dfrac{20}{4} = 5$.

Шаг 3. Стандартное отклонение (с $\varepsilon=10^{-5}$, пренебрежимо мало здесь): $\sqrt{5{,}00001}\approx 2{,}236$.

Шаг 4. Нормализуем: $\hat x = \left[\dfrac{1-4}{2{,}236},\ \dfrac{3-4}{2{,}236},\ \dfrac{5-4}{2{,}236},\ \dfrac{7-4}{2{,}236}\right] \approx [-1{,}342,\ -0{,}447,\ 0{,}447,\ 1{,}342]$.

Проверка: среднее $\hat x$ равно $0$ ($-1{,}342-0{,}447+0{,}447+1{,}342=0$), а дисперсия — ровно $1$ (проверим: $(1{,}342^2+0{,}447^2+0{,}447^2+1{,}342^2)/4 \approx (1{,}801+0{,}200+0{,}200+1{,}801)/4 = 4{,}00/4 = 1{,}0$ ✅).

Шаг 5. Пусть сеть уже выучила $\gamma=2$, $\beta=1$ для этого нейрона. Тогда $y = 2\hat x + 1 \approx [-1{,}683,\ 0{,}106,\ 1{,}894,\ 3{,}683]$.

Это и есть весь алгоритм BatchNorm для одного нейрона на одном шаге обучения — четыре арифметических действия, повторённые для каждого нейрона слоя.

Пример 2 (зачем нужен $\varepsilon$ — вырожденный случай нулевой дисперсии). Пусть в батче все четыре значения случайно совпали: $x=[5,5,5,5]$ (на практике такое бывает, например, у только что инициализированного bias-нейрона или у мёртвого ReLU, о котором шла речь в уроке 331). Тогда $\mu_B=5$, а $\sigma_B^2 = \dfrac{0+0+0+0}{4}=0$. Без $\varepsilon$ формула нормализации потребовала бы деления на $\sqrt{0}=0$ — операция не определена, и код буквально упадёт с ошибкой деления на ноль или выдаст NaN. С $\varepsilon=10^{-5}$: $\sqrt{0+10^{-5}}\approx 0{,}00316$, и хотя $\hat x_i = (5-5)/0{,}00316 = 0$ для всех $i$ (числитель всё равно нулевой), сама операция остаётся математически корректной и численно устойчивой. Итоговый выход в этом крайнем случае — просто $y_i=\beta$ для всех примеров: если внутри батча нет никакой изменчивости, нормализовать нечего, и сигнал целиком определяется обучаемым сдвигом.

Пример 3 (та же операция в коде — torch.nn.BatchNorm1d).

import torch
import torch.nn as nn

bn = nn.BatchNorm1d(num_features=1)
x = torch.tensor([[1.0], [3.0], [5.0], [7.0]])  # батч из 4 примеров, 1 признак

y = bn(x)
print(y.flatten())
# tensor([-1.3416, -0.4472,  0.4472,  1.3416])

По умолчанию nn.BatchNorm1d инициализирует $\gamma=1$ и $\beta=0$ для каждого признака, поэтому при первом вызове выход совпадает с чистой нормализацией $\hat x$ из примера 1 — теми же числами $[-1{,}342,\ -0{,}447,\ 0{,}447,\ 1{,}342]$, которые ты только что посчитал вручную. По мере обучения $\gamma$ и $\beta$ будут меняться градиентным спуском вместе с остальными весами сети.

Почему это важно

BatchNorm — это не разовая процедура на входе, как нормализация признаков из урока 325, а живой, постоянно действующий регулятор, встроенный прямо в архитектуру и работающий заново на каждом прямом проходе. Именно поэтому он динамически противодействует internal covariate shift, разобранному в предыдущем разделе: сколько бы веса предыдущих слоёв ни менялись, следующий слой BatchNorm всегда получает на входе активации, повторно приведённые к контролируемому масштабу — вне зависимости от того, что происходило до этого момента.


Обучаемые γ, β и место BatchNorm в архитектуре

Интуиция

Жёсткая нормализация — рискованная идея сама по себе. Если насильно заставить вход каждого нейрона иметь ровно нулевое среднее и единичную дисперсию, ты можешь непреднамеренно ограничить то, что вообще способна выразить сеть. Например, для сигмоиды диапазон значений $\hat x \in [-2, 2]$ (типичный разброс при дисперсии $1$) — это почти линейный, ненасыщенный участок кривой; а иногда именно насыщение сигмоиды на самом деле полезно модели. Поэтому Иоффе и Сегеди добавили в формулу два обучаемых параметра $\gamma$ и $\beta$, которые дают сети возможность самостоятельно решить, сколько нормализации ей на самом деле нужно — вплоть до полного восстановления немасштабированных значений, если это окажется выгоднее.

Формула

Напомним последний шаг формулы: $y_i = \gamma\hat x_i+\beta$. Ключевое наблюдение — если сеть в процессе обучения выучит $\gamma = \sqrt{\sigma_B^2+\varepsilon}$ и $\beta=\mu_B$ (то есть в точности величины, которые были использованы для нормализации), то

$$y_i = \sqrt{\sigma_B^2+\varepsilon}\cdot\frac{x_i-\mu_B}{\sqrt{\sigma_B^2+\varepsilon}}+\mu_B = x_i$$

— нормализация полностью отменяется, и слой возвращает исходные, ненормализованные значения. Значит, BatchNorm с обучаемыми $\gamma,\beta$ никогда не может строго уменьшить выразительную силу сети по сравнению с её отсутствием — в худшем случае сеть выучит параметры, откатывающие эффект нормализации назад.

По архитектуре BatchNorm почти всегда ставится между линейным (или свёрточным) слоем и функцией активации: Linear → BatchNorm → Activation, а не после активации. Причина прямо следует из мотивации: цель — контролировать масштаб именно того сигнала, который подаётся в нелинейность, чтобы он не «улетал» в область насыщения сигмоиды или ReLU-нуля ещё до того, как активация успеет что-то с ним сделать.

Разбор примеров

Пример 1 (численная проверка отката нормализации). Возьмём батч из примера 1 предыдущего раздела: $x=[1,3,5,7]$, $\mu_B=4$, $\sigma_B^2=5$, $\sqrt{\sigma_B^2+\varepsilon}\approx2{,}236$. Пусть сеть выучила $\gamma=2{,}236$ (то есть ровно $\sqrt{\sigma_B^2+\varepsilon}$) и $\beta=4$ (то есть ровно $\mu_B$). Тогда: $y_1=2{,}236\cdot(-1{,}342)+4\approx-3{,}0+4=1{,}0$; $y_2=2{,}236\cdot(-0{,}447)+4\approx-1{,}0+4=3{,}0$; $y_3=2{,}236\cdot0{,}447+4\approx1{,}0+4=5{,}0$; $y_4=2{,}236\cdot1{,}342+4\approx3{,}0+4=7{,}0$. Результат — в точности исходный батч $[1,3,5,7]$: сеть выучила параметры, полностью аннулирующие нормализацию.

Пример 2 (правильный и неправильный порядок слоёв).

import torch.nn as nn

# Правильно: BatchNorm нормализует "сырой" сигнал перед нелинейностью
block_ok = nn.Sequential(
    nn.Linear(128, 64),
    nn.BatchNorm1d(64),
    nn.ReLU()
)

# Неправильно: после ReLU половина значений уже обрезана в нуль,
# и BatchNorm нормализует урезанное, несимметричное распределение
block_bad = nn.Sequential(
    nn.Linear(128, 64),
    nn.ReLU(),
    nn.BatchNorm1d(64)
)

В block_bad на вход BatchNorm поступают уже прошедшие через ReLU значения — все они неотрицательны, а значительная доля попросту равна нулю. Нормализация такого урезанного, несимметричного распределения работает хуже: смысл BatchNorm — стабилизировать сигнал, подаваемый в нелинейность, а не пытаться выправить то, что нелинейность уже необратимо изменила.

Пример 3 (инициализация γ и β по умолчанию — связь с уроком 331). В PyTorch (и в оригинальной статье) параметры BatchNorm инициализируются как $\gamma=1$, $\beta=0$ для каждого нейрона. Это значит, что в самом начале обучения слой BatchNorm работает как «чистая» нормализация без каких-либо искажений — сеть стартует из нейтральной, предсказуемой точки и, если найдёт это полезным, постепенно отклоняется от неё градиентным спуском. Это тот же самый принцип, что и в уроке 331 про инициализацию весов: начинать обучение из состояния, которое заведомо не создаёт патологий (взрыва или затухания дисперсии), и доверить остальное оптимизации.

Почему это важно

Обучаемость $\gamma$ и $\beta$ — это то, что превращает BatchNorm из «жёсткого архитектурного ограничения» в гибкий инструмент. Слой физически не может сделать сеть менее выразительной, чем без него: в худшем случае он выучится быть прозрачным (пример 1). Именно эта гарантия — «BatchNorm может только помочь оптимизации, но не может ограничить то, что сеть в принципе способна выразить» — вместе с эмпирическими результатами Иоффе и Сегеди сделала его практически стандартным компонентом любого свёрточного блока, с которым ты столкнёшься в следующем блоке курса про CNN.


Обучение против инференса: batch-статистики и скользящее среднее

Интуиция

На обучении у тебя всегда есть мини-батч из нескольких примеров, и по нему честно можно посчитать $\mu_B,\sigma_B^2$. А что происходит на инференсе, когда модель делает предсказание для одной-единственной фотографии, одного пользователя, одного запроса? Батча из нескольких примеров попросту нет. И даже если бы он был — представь, что предсказание модели для твоей фотографии зависело бы от того, какие ещё случайные фотографии оказались рядом с ней в этом конкретном батче. Результат менялся бы от запуска к запуску без всякой логики, а в проде это означало бы: два одинаковых запроса, отправленные в разное время (и потому оказавшиеся в разных батчах), могли бы получить разные ответы. Это неприемлемо ни для отладки, ни для доверия к модели.

Решение: пока идёт обучение, BatchNorm не только нормализует по текущему батчу, но и параллельно копит скользящее (экспоненциальное) среднее статистик $\mu,\sigma^2$ по всем батчам, увиденным за всё обучение. На инференсе слой переключается на использование этих накопленных, уже зафиксированных чисел вместо статистик случайного батча — и тогда предсказание для одного и того же входа всегда одинаково, вне зависимости от того, что ещё передано вместе с ним.

Формула

Накопление скользящих статистик (running statistics) во время обучения, для каждого батча с $\mu_B,\sigma_B^2$ и коэффициента momentum (в PyTorch по умолчанию $0{,}1$):

$$\text{running\_mean} \leftarrow (1-\text{momentum})\cdot\text{running\_mean} + \text{momentum}\cdot\mu_B$$

$$\text{running\_var} \leftarrow (1-\text{momentum})\cdot\text{running\_var} + \text{momentum}\cdot\sigma_B^2$$

На инференсе используются уже не статистики текущего батча, а зафиксированные running_mean, running_var:

$$\hat x = \frac{x-\text{running\_mean}}{\sqrt{\text{running\_var}+\varepsilon}}, \qquad y=\gamma\hat x+\beta$$

Обрати внимание на терминологическую ловушку: momentum здесь означает вес нового батча (доля, с которой свежая статистика подмешивается в накопленное среднее) — это противоположно смыслу momentum в оптимизаторах вроде SGD с моментумом, где этот параметр отвечает за вес старого направления движения.

Разбор примеров

Пример 1 (ручной расчёт накопления running-статистик за два батча). Пусть running_mean и running_var инициализированы значениями по умолчанию в PyTorch: $0$ и $1$ соответственно, momentum=0,1.

Батч 1: $\mu_{B1}=6$, $\sigma_{B1}^2=4$.

$$\text{running\_mean} = 0{,}9\cdot0 + 0{,}1\cdot6 = 0{,}6$$

$$\text{running\_var} = 0{,}9\cdot1 + 0{,}1\cdot4 = 0{,}9+0{,}4=1{,}3$$

Батч 2: $\mu_{B2}=10$, $\sigma_{B2}^2=2$.

$$\text{running\_mean} = 0{,}9\cdot0{,}6 + 0{,}1\cdot10 = 0{,}54+1{,}0=1{,}54$$

$$\text{running\_var} = 0{,}9\cdot1{,}3 + 0{,}1\cdot2 = 1{,}17+0{,}2=1{,}37$$

После этих двух батчей на инференсе слой будет использовать $\text{running\_mean}=1{,}54$ и $\text{running\_var}=1{,}37$ — приближённые, усреднённые по всей истории обучения статистики, а не значения ни одного конкретного батча.

Пример 2 (переключение режима в коде — model.train() и model.eval()).

model.train()          # BatchNorm использует статистики ТЕКУЩЕГО батча
                        # и одновременно обновляет running_mean/running_var
for x_batch, y_batch in train_loader:
    preds = model(x_batch)
    loss = criterion(preds, y_batch)
    loss.backward()
    optimizer.step()

model.eval()            # BatchNorm переключается на накопленные running-статистики
                         # и перестаёт их обновлять
with torch.no_grad():
    prediction = model(single_image.unsqueeze(0))

Если забыть вызвать model.eval() перед инференсом, слой BatchNorm продолжит считать статистики по тому, что ему передали сейчас — а не по накопленной за обучение картине. Это одна из самых частых практических ошибок в PyTorch-коде: модель как будто корректно обучилась, но выдаёт странные, нестабильные предсказания в продакшене — причина почти всегда именно здесь.

Пример 3 (почему батч размера 1 в режиме обучения ломает всё). Представь, что model.eval() забыли вызвать, и на инференс подан ровно один пример: $x=[7]$. В режиме обучения BatchNorm честно считает $\mu_B=7$ (единственное значение — само себе среднее) и $\sigma_B^2=0$ (отклонение от единственного значения — всегда ноль). С $\varepsilon=10^{-5}$: $\hat x = (7-7)/\sqrt{10^{-5}} = 0$ — и это верно для любого входного значения при батче размера 1, потому что оно всегда равно собственному среднему. Итоговый выход становится $y=\beta$ независимо от того, что было на входе — вся информация о значении $x$ теряется. В режиме eval() та же самая ситуация обрабатывается корректно: используются зафиксированные running_mean и running_var, посчитанные по тысячам батчей за всё обучение, и результат содержательно зависит от реального входного значения.

Почему это важно

Асимметрия между обучением и инференсом — не деталь реализации, о которой можно забыть, а прямое следствие того, зачем вообще появился BatchNorm: он был спроектирован для стабилизации обучения по мини-батчам, а на инференсе задача другая — дать детерминированный, воспроизводимый ответ для одного примера. Понимание этой асимметрии — то, что отличает разработчика, который просто пишет model.eval(), потому что «так написано в документации», от того, кто способен диагностировать нестабильные предсказания в проде, зная, откуда именно берётся проблема.


Побочные эффекты BatchNorm и Layer Normalization

Интуиция

Помимо решения исходной проблемы internal covariate shift, у BatchNorm обнаружился ряд полезных побочных эффектов, которые во многом и объясняют, почему он стал практически обязательным компонентом. Во-первых, он позволяет использовать значительно более высокий learning rate: поскольку активации каждого слоя постоянно возвращаются к контролируемому масштабу, крупные обновления весов в ранних слоях меньше «взрывают» распределения в поздних слоях — оптимизационный ландшафт становится более гладким и предсказуемым (это и есть та самая находка Сантуркара и коллег из истории урока). Во-вторых, BatchNorm действует как лёгкая регуляризация: статистики $\mu_B,\sigma_B^2$ зависят от того, какие именно примеры случайно оказались в одном батче, а значит один и тот же пример на разных эпохах нормализуется чуть по-разному — в вычисления подмешивается небольшой шум, похожий по духу на dropout (урок 333), хотя и заметно слабее. В-третьих — и это прямая связь с уроком 331 — BatchNorm заметно снижает чувствительность сети к качеству инициализации весов: даже если начальный масштаб весов выбран не идеально, слой BatchNorm на каждом шаге принудительно возвращает активации к разумному диапазону, не позволяя ошибке накапливаться и усиливаться слой за слоем, как это происходило бы без нормализации.

Но у BatchNorm есть и ограничение: он нормализует по оси батча, то есть статистики одного примера зависят от того, какие ещё примеры оказались рядом с ним. Это создаёт проблемы там, где размер батча мал, переменный или вовсе равен единице — типичная ситуация для рекуррентных сетей и трансформеров, где длина последовательности меняется от примера к примеру, а при авторегрессионной генерации текста модель нередко обрабатывает буквально один пример за раз. Решение — Layer Normalization: вместо усреднения по батчу для каждого признака она усредняет по признакам одного примера.

Формула

Layer Normalization. Для одного примера с вектором активаций $x_i = (x_{i,1},\dots,x_{i,H})$ из $H$ признаков (в трансформере — размерность эмбеддинга):

$$\mu_i = \frac{1}{H}\sum_{j=1}^H x_{i,j}, \qquad \sigma_i^2 = \frac{1}{H}\sum_{j=1}^H (x_{i,j}-\mu_i)^2$$

$$\hat x_{i,j} = \frac{x_{i,j}-\mu_i}{\sqrt{\sigma_i^2+\varepsilon}}, \qquad y_{i,j} = \gamma_j\,\hat x_{i,j}+\beta_j$$

Статистики считаются независимо для каждого примера $i$, без какой-либо зависимости от других примеров в батче — поэтому формула и её поведение одинаковы что при обучении, что при инференсе, и никакие running-статистики не нужны.

Разбор примеров

Пример 1 (ось усреднения — численная разница на одной матрице). Возьмём батч из трёх примеров с двумя признаками каждый:

$$X = \begin{pmatrix}1 & 5\\3 & 7\\5 & 9\end{pmatrix}$$

BatchNorm усредняет по столбцам (по батчу, для каждого признака отдельно): столбец 1 $=[1,3,5]$, $\mu=3$, $\sigma^2=\dfrac{4+0+4}{3}=\dfrac{8}{3}\approx2{,}667$; столбец 2 $=[5,7,9]$, $\mu=7$, $\sigma^2=\dfrac{8}{3}\approx2{,}667$.

LayerNorm усредняет по строкам (по признакам, для каждого примера отдельно): строка 1 $=[1,5]$, $\mu=3$, $\sigma^2=\dfrac{4+4}{2}=4$; строка 2 $=[3,7]$, $\mu=5$, $\sigma^2=4$; строка 3 $=[5,9]$, $\mu=7$, $\sigma^2=4$.

Две совершенно разные пары статистик, посчитанные по одной и той же матрице, — просто по разным осям. Это и есть всё различие между слоями с точки зрения арифметики.

Пример 2 (почему трансформеры используют LayerNorm, а не BatchNorm). В задачах с текстом длина последовательности меняется от примера к примеру (одно предложение — 5 токенов, другое — 40), а при авторегрессионной генерации (когда модель, например, дописывает твой запрос токен за токеном) часто обрабатывается ровно один пример за раз — батч размера 1. Для BatchNorm батч размера 1 — вырожденный случай с нулевой дисперсией (см. пример 3 предыдущего раздела), а статистики по переменной длине последовательности вообще плохо определены: непонятно, что усреднять — все токены всех предложений сразу вперемешку? LayerNorm же по конструкции никогда не смотрит на другие примеры батча — она нормализует вектор эмбеддинга каждого токена по его собственным признакам, независимо от размера батча и длины последовательности. Именно поэтому torch.nn.LayerNorm — обязательный, стандартный компонент архитектуры трансформера, который будет подробно разобран дальше в курсе.

Пример 3 (та же операция в коде — torch.nn.LayerNorm).

import torch
import torch.nn as nn

ln = nn.LayerNorm(normalized_shape=2)
x = torch.tensor([[1.0, 5.0], [3.0, 7.0], [5.0, 9.0]])

y = ln(x)
print(y)
# tensor([[-1.0000,  1.0000],
#         [-1.0000,  1.0000],
#         [-1.0000,  1.0000]], grad_fn=<...>)

Каждая строка нормализуется независимо от двух других — результат в точности совпадает с ручным расчётом из примера 1 (среднее $0$, дисперсия $1$ внутри каждой строки), а поведение слоя будет одинаковым и при обучении, и на инференсе: никаких running-статистик, никакого model.eval() для этого конкретного слоя не требуется.

Почему это важно

Выбор между BatchNorm и LayerNorm — это не вопрос вкуса, а прямое следствие того, по какой оси у тебя есть надёжная статистика. В свёрточных сетях (следующий блок курса) батчи, как правило, большие и фиксированного размера, а каналы имеют устойчивый статистический смысл по всему датасету — там доминирует torch.nn.BatchNorm1d/BatchNorm2d. В трансформерной архитектуре, где размер батча и длина последовательности переменны, а генерация иногда идёт по одному токену за раз, единственный устойчивый источник статистики — признаки самого примера, и потому стандартом стала torch.nn.LayerNorm. Оба слоя решают одну и ту же исходную проблему — нестабильное распределение активаций внутри сети, — но выбирают разные оси усреднения, потому что архитектуры, в которых они применяются, устроены принципиально по-разному.


Практика: 30 заданий

Базовые (задания 1–10)

Задание 1: Для мини-батча активаций одного нейрона $x=[2,4,6,8]$ найди $\mu_B$ и $\sigma_B^2$.


Задание 2: Дано $\mu_B=5$, $\sigma_B^2=4$, $\varepsilon=10^{-5}$. Найди нормализованное значение для $x=7$.


Задание 3: Дано $\hat x=1{,}5$, $\gamma=3$, $\beta=-1$. Найди $y$.


Задание 4: Батч $x=[3,3,3,3]$. Объясни, зачем в формуле нормализации нужен $\varepsilon$, и посчитай $\hat x$ с $\varepsilon=10^{-5}$.


Задание 5: В архитектуре нужно расставить Linear, BatchNorm1d, ReLU. Какой порядок правильный и почему?


Задание 6: Каковы значения $\gamma$ и $\beta$ по умолчанию при инициализации nn.BatchNorm1d в PyTorch? Что это означает для самого первого forward pass необученной сети?


Задание 7: Батч $x=[10,12,12,14]$. Найди $\mu_B$, $\sigma_B^2$ и $\sqrt{\sigma_B^2}$.


Задание 8: Дано running_mean=2, running_var=3, momentum=0,1. Новый батч даёт $\mu_B=6$, $\sigma_B^2=7$. Найди обновлённые running_mean и running_var.


Задание 9: Объясни одним предложением разницу между model.train() и model.eval() с точки зрения того, какие статистики использует BatchNorm.


Задание 10 (машинное обучение): Модель делает предсказание для одной фотографии в продакшене. Какие статистики должен использовать BatchNorm — по этой одной фотографии или накопленные во время обучения? Почему?

Средние (задания 11–20)

Задание 11: Батч $x=[2,6,10,14]$, обучены $\gamma=3$, $\beta=2$. Найди полный выход BatchNorm $y$.


Задание 12: Для батча $x=[3,5,7,9]$ докажи (числами), что если $\gamma=\sqrt{\sigma_B^2+\varepsilon}$ и $\beta=\mu_B$, то BatchNorm возвращает исходные значения.


Задание 13: momentum=0,2, начальные running_mean=0, running_var=1. Батч 1: $\mu=4,\sigma^2=2$. Батч 2: $\mu=8,\sigma^2=6$. Батч 3: $\mu=5,\sigma^2=3$. Найди running_mean и running_var после трёх батчей.


Задание 14 (машинное обучение): В коде забыли вызвать model.eval() перед инференсом и подают в модель батч размера 1. Что произойдёт с выходом BatchNorm и почему это ошибка?


Задание 15: Дана матрица активаций батча из трёх примеров с двумя признаками: $X=\begin{pmatrix}1&5\\3&7\\5&9\end{pmatrix}$. Найди $\mu,\sigma^2$ по BatchNorm (по столбцам) и по LayerNorm (по строкам).


Задание 16 (машинное обучение): Почему BatchNorm плохо работает при очень маленьком размере батча (например, batch=2)?


Задание 17: При обучении в батч случайно попал единственный пример $x=[7]$ (batch size = 1). Посчитай $\mu_B,\sigma_B^2,\hat x$ с $\varepsilon=10^{-5}$ и объясни результат.


Задание 18 (машинное обучение): Объясни, почему BatchNorm действует как лёгкая регуляризация.


Задание 19: Дана архитектура Conv → BN → ReLU → Conv → BN → ReLU → Linear. В скольких местах происходит нормализация активаций и почему её обычно не ставят после финального Linear?


Задание 20 (машинное обучение): Почему BatchNorm снижает чувствительность сети к качеству инициализации весов (связь с уроком 331)?

Продвинутые (задания 21–30)

Задание 21: Докажи в общем виде, что для любого батча $x_1,\dots,x_m$, где не все значения одинаковы, нормализованные значения $\hat x_i=(x_i-\mu_B)/\sigma_B$ (без $\varepsilon$) имеют среднее ровно $0$ и дисперсию ровно $1$.


Задание 22 (машинное обучение): Объясни качественно, почему BatchNorm позволяет использовать более высокий learning rate, чем сеть без него.


Задание 23: Для карты признаков формы (batch=2, channels=1, H=2, W=2): пример 1 $=\begin{pmatrix}1&3\\5&7\end{pmatrix}$, пример 2 $=\begin{pmatrix}2&4\\6&8\end{pmatrix}$. Найди $\mu,\sigma^2$, которые вычислит BatchNorm2d для этого единственного канала.


Задание 24: Один пример с вектором признаков $x=(2,4,6,8)$. Найди $\hat x$ по LayerNorm (с $\gamma=1,\beta=0$).


Задание 25 (машинное обучение): Обученная CNN с BatchNorm переведена в model.eval() и используется для предсказания по одной фотографии за раз в реальном времени. Почему это корректно работает, хотя batch size = 1?


Задание 26 (машинное обучение): Почему объяснение «BatchNorm помогает за счёт уменьшения internal covariate shift» оказалось неполным (работа Сантуркара и коллег, 2018)? Какая альтернатива была предложена?


Задание 27: Докажи алгебраически (для произвольных $x,\mu,\sigma^2,\varepsilon$), что при $\gamma=\sqrt{\sigma^2+\varepsilon}$ и $\beta=\mu$ выход BatchNorm в точности равен $x$.


Задание 28 (машинное обучение): Объясни, почему декодер трансформера использует LayerNorm, а не BatchNorm, учитывая переменную длину последовательностей и авторегрессионную генерацию.


Задание 29 (машинное обучение): Двухслойная сеть с BatchNorm между слоями. Объясни, почему такая сеть терпимее к более высокому learning rate, чем та же сеть без BatchNorm.


Задание 30 (машинное обучение): Ты проектируешь: (а) небольшой сверточный блок для изображений с типичным batch size 64; (б) блок трансформера для текста с переменной длиной последовательности и batch size, иногда равным 1 при генерации. Какую нормализацию выбрать для каждого случая и почему?


Частые ошибки

  • Забывают вызвать model.eval() перед инференсом. Слой BatchNorm продолжает использовать статистики того, что ему передали прямо сейчас, вместо накопленных за обучение running_mean/running_var — при батче размера 1 это полностью уничтожает информацию о входе (задания 14, 17), а предсказания становятся нестабильными и зависящими от случайного состава батча.

  • Ставят BatchNorm после функции активации вместо позиции перед ней. Как разобрано в разделе про обучаемые $\gamma,\beta$, цель слоя — контролировать масштаб сигнала, подаваемого в нелинейность, а не пытаться нормализовать уже урезанное ReLU или насыщенное сигмоидой распределение (задание 5).

  • Путают направление формулы running-статистик. В PyTorch momentum — это вес нового батча, а не вес старого накопленного значения, как в момент-оптимизаторах вроде SGD с моментумом; перепутанное направление обновления даёт полностью неверные накопленные статистики.

  • Используют BatchNorm с очень маленьким или переменным batch size без понимания последствий. При batch size 1–4 статистики батча статистически ненадёжны и вносят больше шума, чем пользы (задание 16); в таких случаях (а тем более при переменной длине последовательности, как в трансформерах) правильный выбор — LayerNorm или GroupNorm.

  • Считают, что BatchNorm может сделать сеть менее выразительной. Обучаемые $\gamma,\beta$ гарантируют, что в худшем случае сеть выучит параметры, полностью откатывающие нормализацию (задания 12, 27) — BatchNorm расширяет пространство возможных решений, а не сужает его.

  • Путают ось нормализации BatchNorm и LayerNorm. BatchNorm усредняет по батчу для каждого признака (столбцы), LayerNorm — по признакам для каждого примера (строки) — задание 15 показывает, что для одной и той же матрицы это два совершенно разных набора статистик, и подмена одного другим меняет содержательный смысл нормализации.

  • Объясняют пользу BatchNorm исключительно через «уменьшение internal covariate shift», не зная о работе 2018 года. Экспериментально показано, что BatchNorm может даже увеличивать измеримый сдвиг распределений и при этом всё равно ускорять обучение (задание 26) — более точное объяснение связано со сглаживанием ландшафта функции потерь, а не с устранением ICS как таковым.


Главное запомнить

  • Internal covariate shift — распределение входа каждого слоя меняется в процессе обучения, потому что меняются веса всех предыдущих слоёв; хорошая инициализация (урок 331) решает эту проблему только на первом forward pass, но не на протяжении всего обучения.

  • Формула BatchNorm: $\mu_B=\frac1m\sum x_i$, $\sigma_B^2=\frac1m\sum(x_i-\mu_B)^2$, $\hat x_i=(x_i-\mu_B)/\sqrt{\sigma_B^2+\varepsilon}$, $y_i=\gamma\hat x_i+\beta$ — статистики считаются по текущему мини-батчу на каждом forward pass.

  • Обучаемые параметры $\gamma$ (масштаб) и $\beta$ (сдвиг) делают нормализацию гибкой, а не жёсткой: при $\gamma=\sqrt{\sigma_B^2+\varepsilon}$, $\beta=\mu_B$ сеть в точности восстанавливает исходные, ненормализованные значения.

  • $\varepsilon\approx10^{-5}$ в знаменателе предотвращает деление на ноль при нулевой дисперсии батча — численная деталь, без которой формула ломается в вырожденных случаях.

  • BatchNorm обычно вставляется между линейным/свёрточным слоем и функцией активации: Linear → BatchNorm → Activation, чтобы контролировать масштаб именно того сигнала, который подаётся в нелинейность.

  • На обучении используются статистики текущего батча и параллельно накапливаются running_mean/running_var через экспоненциальное скользящее среднее; на инференсе используются только зафиксированные running-статистики — это и есть разница между model.train() и model.eval().

  • При batch size = 1 в режиме обучения дисперсия батча всегда равна нулю, а нормализованное значение всегда равно нулю независимо от входа — забытый model.eval() перед инференсом полностью уничтожает содержательный сигнал.

  • Побочные эффекты BatchNorm: позволяет использовать более высокий learning rate (сглаживает ландшафт потерь), действует как лёгкая регуляризация (шум от случайного состава батча) и снижает чувствительность сети к качеству инициализации весов (урок 331) — ошибки масштаба не накапливаются по глубине.

  • Layer Normalization нормализует по признакам одного примера, а не по батчу, и потому не зависит ни от размера батча, ни от того, какие ещё примеры оказались рядом, — обязательный компонент трансформерной архитектуры (torch.nn.LayerNorm), где длина последовательности переменна, а генерация иногда идёт батчами размера 1.

  • torch.nn.BatchNorm1d/BatchNorm2d — стандартный компонент почти любой современной свёрточной архитектуры; torch.nn.LayerNorm — стандартный компонент трансформерной архитектуры. Оба решают одну и ту же исходную проблему нестабильных активаций, но выбирают разные оси усреднения под разные архитектурные условия.


Связь с темами курса

Этот урок напрямую продолжает две предыдущие темы блока. В уроке 325 (feature engineering) ты нормализовал входные признаки — вычитал среднее и делил на стандартное отклонение — но это была разовая операция на границе между сырыми данными и моделью, посчитанная один раз по всему датасету. Batch Normalization берёт ровно тот же математический приём — центрирование и масштабирование — и переносит его внутрь сети, применяя заново на каждом слое и на каждом шаге обучения к активациям, которые сами являются функцией постоянно обновляющихся весов. Разница принципиальная: там нормализация была статичной операцией над фиксированными данными, здесь — динамическим, живым механизмом, действующим на движущуюся цель.

Связь с уроком 331 (инициализация весов) ещё теснее. Там мы разбирали, как Xavier- и He-инициализация сохраняют разумную дисперсию активаций на самом первом forward pass через глубокую сеть. Сегодняшний урок объясняет, что происходит после этого первого шага: по мере того как веса обновляются, тщательно выстроенный на старте баланс дисперсий начинает расползаться — это и есть internal covariate shift. BatchNorm не заменяет хорошую инициализацию (её по-прежнему стоит использовать), но страхует сеть от последствий её постепенного «размывания» в ходе обучения, принудительно возвращая активации к контролируемому масштабу на каждом шаге, а не только в момент $t=0$.

Дальше в блоке — урок 333 про dropout и регуляризацию, который органично продолжает тему побочного регуляризирующего эффекта, разобранного сегодня: BatchNorm вносит лёгкий шум за счёт случайного состава батча, а dropout делает это гораздо более явно и агрессивно, случайно отключая часть нейронов на каждом шаге обучения. Понимание того, что оба механизма конкурируют за одну и ту же роль — борьбу с переобучением, — поможет тебе осмысленно решать, когда их комбинировать, а когда одного из двух уже достаточно.

Наконец, знание BatchNorm и Layer Normalization — прямая подготовка к двум большим архитектурным блокам курса впереди: сверточные сети (CNN), где torch.nn.BatchNorm2d — практически неотъемлемая часть любого свёрточного блока, и трансформеры, где torch.nn.LayerNorm — обязательный компонент каждого блока внимания и полносвязного слоя внутри архитектуры. Без сегодняшнего материала обе эти архитектуры будут выглядеть как набор магических строчек кода; с ним — как логичное продолжение уже понятой тобой идеи.


Интересные факты

  • Статья Иоффе и Сегеди 2015 года заявляла ускорение обучения Inception-сети в 14 раз по числу шагов до достижения прежней точности на ImageNet — редкий случай, когда одна архитектурная идея давала настолько радикальный практический выигрыш почти без побочных издержек.

  • В 2018 году команда MIT (Сантуркар, Ципрас, Ильяс, Мадри) экспериментально построила сети, где BatchNorm математически увеличивал internal covariate shift по сравнению с сетью без нормализации, — и такие сети всё равно обучались быстрее. Это заставило пересмотреть исходное объяснение пользы BatchNorm спустя три года после его изобретения, хотя сам алгоритм при этом ничуть не потерял в практической ценности.

  • Строгое название параметра momentum в формуле накопления running-статистик BatchNorm обозначает вес нового батча, а не вес старого накопленного значения — это прямо противоположно смыслу параметра momentum в оптимизаторах вроде SGD с моментумом, где он отвечает за инерцию предыдущего направления движения. Одно и то же слово, два противоположных по смыслу использования в соседних областях глубокого обучения.

  • Layer Normalization была предложена уже в 2016 году, всего через год после BatchNorm, специально для рекуррентных сетей, где переменная длина последовательности делала батч-статистики плохо определёнными. К моменту появления трансформеров в 2017 году LayerNorm уже была готовым, проверенным решением именно той проблемы, с которой столкнулась новая архитектура, — редкий случай, когда нужный инструмент оказался под рукой раньше, чем возникла явная потребность в нём.


Лайфхаки

  • Проверяй режим модели перед любым предсказанием вне обучающего цикла. Одна строка model.eval() перед инференсом и model.train() перед возвращением к обучению — самая дешёвая профилактика одной из самых частых практических ошибок в PyTorch-коде.

  • Если нужно вручную проверить, что BatchNorm работает корректно, посчитай статистики батча вручную (как в примерах этого урока) и сравни с тем, что выдаёт layer.running_mean и layer.running_var после нескольких вызовов в режиме train() — расхождение сразу укажет на ошибку в понимании momentum или в порядке вызовов.

  • При работе с маленькими батчами (меньше 8) сразу думай о GroupNorm или LayerNorm вместо BatchNorm — статистика по нескольким примерам слишком шумная, чтобы приносить пользу, а не вред.

  • Не удивляйся, если с BatchNorm можно уменьшить или вовсе убрать dropout в некоторых архитектурах — оба механизма отчасти конкурируют за одну и ту же роль регуляризации, и их совместное использование иногда избыточно; экспериментируй с обоими по отдельности перед тем, как комбинировать.

  • Для отладки нестабильного обучения глубокой сети проверяй, не забыт ли BatchNorm (или Layer/GroupNorm) между свёрточными или полносвязными блоками — отсутствие нормализации активаций часто маскируется под «неудачный learning rate» или «плохая инициализация», хотя на самом деле причина в накапливающемся internal covariate shift.

  • Выбирай тип нормализации по архитектуре, а не по привычке: для CNN с большим стабильным батчем — BatchNorm2d, для трансформеров и последовательностей переменной длины — LayerNorm, для очень маленьких батчей или задач с сильной зависимостью между каналами — присмотрись к GroupNorm как к промежуточному варианту.


Сегодня ты закрыл важный практический разрыв между «сеть теоретически обучаема» и «сеть обучается быстро и стабильно на практике». Инициализация из урока 331 даёт хороший старт, но не защищает от постепенного расползания масштабов активаций по мере того, как веса меняются, — а Batch Normalization берёт эту защиту на себя, действуя заново на каждом шаге, а не один раз в начале. Обучаемые $\gamma$ и $\beta$ превращают нормализацию из жёсткого ограничения в гибкий инструмент, который сеть может настроить под себя, а различие между поведением на обучении и на инференсе — это не техническая деталь для запоминания наизусть, а прямое следствие того, зачем вообще нужна пакетная статистика.

Дальше — урок 333 про dropout и регуляризацию, где ты увидишь родственный, но гораздо более прямой способ бороться с переобучением. А чуть позже в курсе, когда дело дойдёт до сверточных сетей и трансформеров, ты будешь расставлять BatchNorm2d и LayerNorm в архитектурах не потому, что «так принято», а потому что теперь точно понимаешь, какую именно проблему каждый из этих слоёв решает и почему выбор между ними — не вопрос вкуса, а прямое следствие устройства конкретной архитектуры. 🚀

Понял тему? Закрепи в боте! 🚀

Попрактикуйся на задачах и получи персональные рекомендации от AI

💪 Начать тренировку
💬 Есть вопрос? Спроси бота!