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

«Почему в рекуррентных сетях и трансформерах используют не Batch Normalization, а Layer Normalization?» — вопрос из категории Нейронные сети и Deep Learning, который задают на 26% собеседований Data Scientist / ML Инженер. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

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