Почему в рекуррентных сетях и трансформерах используют не Batch Normalization, а Layer Normalization?

Ответ

Batch Normalization (BN) плохо подходит для последовательностей, в отличие от Layer Normalization (LN), и вот ключевые технические причины, с которыми я сталкивался на практике:

  1. Зависимость от статистик батча в RNN: В RNN мы разворачиваем сеть по времени. BN нормировал бы активации, используя среднее и дисперсию по батчу и по всем временным шагам. Это проблема:

    • При инференсе (предсказании для одного примера) статистики батча не определены, используются скользящие средние, рассчитанные на обучении. Для последовательностей переменной длины это ненадежно.
    • Статистики меняются от шага к шагу, внося нестабильность в обучение.
  2. Несовместимость с переменной длиной последовательностей: В одном батче часто пакуют последовательности разной длины (дополняя нулями). BN, вычисляя статистики по всем элементам батча, будет смешивать реальные активации и паддинг-нули, что искажает нормализацию.

Layer Normalization решает эти проблемы:

  • Нормирование по измерениям признаков (features): LN вычисляет среднее и дисперсию для каждого примера в батче и для каждого временного шага независимо по всем нейронам (признакам) в слое. Формула для вектора активаций h длины L: LN(h) = γ * (h - μ) / √(σ² + ε) + β, где μ, σ² вычисляются по L.
  • Стабильность: LN не зависит от размера батча, что критично для RNN и идеально подходит для трансформеров, где self-attention уже работает независимо для каждого токена.
  • Ускорение сходимости: Как и BN, LN стабилизирует распределение активаций, смягчая проблему исчезающих/взрывающихся градиентов, но делает это более предсказуемым образом для последовательностей.

Пример использования в PyTorch для временного ряда:

import torch.nn as nn

# Допустим, вход имеет shape (batch_size, seq_len, feature_dim)
# LN нормирует по последнему измерению (feature_dim)
layer_norm = nn.LayerNorm(normalized_shape=feature_dim) 
# Для входного тензора размера (32, 10, 64) он вычислит 32*10=320 независимых средних и дисперсий по 64 признакам.

# В архитектуре трансформера (например, блок энкодера) LN применяется ДО саб-лейеров:
class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model, nhead):
        super().__init__()
        self.norm1 = nn.LayerNorm(d_model)
        self.self_attn = nn.MultiheadAttention(d_model, nhead)
        self.norm2 = nn.LayerNorm(d_model)
        self.ff = nn.Linear(d_model, d_model)

    def forward(self, src):
        # Pre-LN архитектура (сейчас более распространена)
        src_norm = self.norm1(src)
        src = src + self.self_attn(src_norm, src_norm, src_norm)[0]
        src_norm = self.norm2(src)
        src = src + self.ff(src_norm)
        return src

Ответ 18+ 🔞

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

Batch Normalization — это, конечно, крутая штука, когда у тебя статичные картинки, но как только ты суёшь её в RNN или трансформеры, начинается ёперный театр. Почему? Давай по пунктам, а то я сам, блядь, когда впервые с этим столкнулся, охуел от неочевидных граблей.

Первая причина — пиздец с батчем в RNN. Ты же помнишь, как там сеть разворачивается по времени? Так вот, BN тупо начинает считать среднее и дисперсию по всему батчу и по всем временным шагам сразу. Это как смешать борщ с компотом — вроде всё жидкое, но жрать невозможно. А главная засада — на инференсе. Когда ты уже всё обучил и пытаешься предсказать что-то для одной последовательности, у тебя нет батча! Приходится использовать какие-то скользящие средние, которые насчитались во время обучения. Для последовательностей, которые ещё и разной длины бывают, это полный вротберунчик. Статистики пляшут от шага к шагу, и обучение становится нестабильным, как жопа алкоголика.

Вторая причина — переменная длина. Ну реально, чувак, ты же не будешь резать все тексты или временные ряды под одну гребёнку. В батче всегда будут последовательности разной длины, и короткие дополняются нулями (паддинг). И что делает BN? Правильно, он тупо берёт и усредняет все активации в батче, включая эти ебучие паддинг-нули! Получается, он нормализует данные, смешивая реальную информацию с пустотой. Это как пытаться измерить среднюю температуру по больнице, учитывая градусники в морге. Доверия ебать ноль к таким статистикам.


А теперь смотри, как Layer Normalization выручает эту ситуацию. Это вообще хитрая жопа, которая решает проблемы BN на раз-два.

  • Нормирует по измерениям признаков. LN не парится насчёт батча вообще. Он для каждого примера в батче и для каждого временного шага независимо считает своё среднее и дисперсию, но только по нейронам (признакам) внутри этого слоя. Формула простая: берёшь вектор активаций h, вычитаешь его собственное среднее μ, делишь на его же дисперсию σ² (с маленьким ε, чтобы на ноль не делить), и потом масштабируешь/сдвигаешь параметрами γ и β. Всё. Никакой зависимости от соседей по батчу.
  • Стабильность — овердохуища. Поскольку LN не смотрит на батч, ему похуй, один у тебя пример или тысяча. Это критично для RNN и просто идеально ложится на архитектуру трансформеров, где self-attention и так работает для каждого токена отдельно.
  • Сходится быстрее. Как и BN, он стабилизирует распределение активаций, не даёт градиентам исчезнуть или взорваться. Но делает это предсказуемо и аккуратно, именно для последовательностей.

Вот тебе кусок кода на PyTorch, чтобы было понятнее, как это встраивается:

import torch.nn as nn

# Допустим, вход имеет shape (batch_size, seq_len, feature_dim)
# LN нормирует по последнему измерению (feature_dim)
layer_norm = nn.LayerNorm(normalized_shape=feature_dim)
# Для входного тензора размера (32, 10, 64) он вычислит 32*10=320 независимых средних и дисперсий по 64 признакам.

# В архитектуре трансформера (например, блок энкодера) LN применяется ДО саб-лейеров:
class TransformerEncoderLayer(nn.Module):
    def __init__(self, d_model, nhead):
        super().__init__()
        self.norm1 = nn.LayerNorm(d_model)
        self.self_attn = nn.MultiheadAttention(d_model, nhead)
        self.norm2 = nn.LayerNorm(d_model)
        self.ff = nn.Linear(d_model, d_model)

    def forward(self, src):
        # Pre-LN архитектура (сейчас более распространена)
        src_norm = self.norm1(src)
        src = src + self.self_attn(src_norm, src_norm, src_norm)[0]
        src_norm = self.norm2(src)
        src = src + self.ff(src_norm)
        return src

Короче, вывод простой: для последовательностей Batch Norm — это хуй с горы, а Layer Norm — твой верный пёс. Не наступай на эти грабли, береги нервы и вычислительные ресурсы.