LayerNorm
Идея: нормализовать каждый объект
BatchNorm считает статистики «вертикально» — по объектам батча для каждого признака. LayerNorm (Ба, Кирос, Хинтон, 2016) делает то же самое «горизонтально» — по признакам каждого объекта:
Обучаемые \gamma и \beta — по одному на признак, как у BatchNorm.
import numpy as np
import nanotorch as nt
x = nt.tensor([[1.0, 2.0, 3.0, 4.0],
[10.0, 20.0, 30.0, 40.0],
[-5.0, 0.0, 5.0, 100.0]])
ln = nt.LayerNorm(4)
y = ln(x)
print(y.data.round(3))
print("среднее по строкам:", y.data.mean(axis=1).round(6), " std:", y.data.std(axis=1).round(3))
Первые две строки после нормализации совпали: LayerNorm не видит масштаб объекта, только «форму» вектора признаков.
Почему это удобно
- Не зависит от батча. Каждый объект нормализуется сам по себе — работает и при батче из одного объекта.
- Одинаково при обучении и применении. Нет скользящих средних, нет режимов, нет связанных с ними ошибок.
- Подходит для последовательностей. В тексте каждый токен нормализуется отдельно, длина текста и паддинг не мешают.
Поэтому LayerNorm стоит в каждом блоке трансформера — от BERT до GPT.
Эксперимент: крошечный батч
Обучим одну и ту же сеть с BatchNorm и с LayerNorm, с обычным батчем (32) и с крошечным (2). Число шагов обучения одинаковое — 600:
import numpy as np
import nanotorch as nt
import ml_data
X, y = ml_data.load_digits()
X = X / 16.0
X_train, X_test, y_train, y_test = nt.train_test_split(X, y, test_size=0.25, seed=0)
X_train, y_train = X_train[:600], y_train[:600]
for name, norm in [("BatchNorm", nt.BatchNorm1d), ("LayerNorm", nt.LayerNorm)]:
for bs, epochs in ((32, 32), (2, 2)): # 600 / 32 ≈ 19 шагов на эпоху, 600 / 2 = 300
nt.manual_seed(0)
model = nt.Sequential(nt.Linear(64, 64), norm(64), nt.ReLU(), nt.Linear(64, 64), norm(64), nt.ReLU(), nt.Linear(64, 10))
h = nt.fit(model, X_train, y_train, nt.cross_entropy, nt.SGD(model.parameters(), lr=0.05),
epochs=epochs, batch_size=bs, X_val=X_test, y_val=y_test, metric=nt.accuracy, verbose=False)
print(f"{name}, батч {bs:2}: точность на тесте {h['val_metric'][-1]:.1%}")
С батчем из 2 объектов BatchNorm почти бесполезен: после нормализации по двум объектам каждый признак принимает значения ±1 — информация о величине теряется, а накопленные скользящие средние плохо соответствуют тому, что сеть видела при обучении. LayerNorm тоже страдает от шумных градиентов маленького батча, но сама нормализация от размера батча не зависит.
BatchNorm против LayerNorm
| BatchNorm | LayerNorm | |
|---|---|---|
| Статистики | по батчу, для каждого признака | по признакам, для каждого объекта |
| Обучение / применение | различаются (скользящие средние) | одинаково |
| Маленький батч | плохо | без разницы |
| Последовательности | неудобно | естественно |
| Где применяется | свёрточные сети (ResNet) | трансформеры, рекуррентные сети |
Есть и промежуточные варианты: GroupNorm (статистики по группам каналов одного объекта) и InstanceNorm (по каждому каналу картинки отдельно) — для свёрточных сетей с маленькими батчами и для переноса стиля.
RMSNorm
RMSNorm (2019) — упрощение LayerNorm: не вычитаем среднее и не прибавляем \beta, только делим на «средний квадрат» (root mean square):
Оказалось, что центрирование почти ничего не даёт, а без него вычисления дешевле. RMSNorm используется в LLaMA, Mistral, Gemma и многих других современных языковых моделях.
Где стоит нормализация в трансформере
В блоке трансформера есть две части (внимание и полносвязная сеть) и остаточные связи x + f(x). Нормализацию можно поставить по-разному:
- Post-Norm (исходная статья 2017 года): x \leftarrow \text{LN}(x + f(x));
- Pre-Norm (GPT-2 и почти все современные модели): x \leftarrow x + f(\text{LN}(x)).
Pre-Norm устойчивее при обучении глубоких моделей: «магистраль» x проходит через все блоки без нормализации, и градиент по ней течёт беспрепятственно. Мы реализуем именно Pre-Norm, когда будем собирать энкодер трансформера.
import inspect
import nanotorch as nt
print(inspect.getsource(nt.LayerNorm))
Итог
- LayerNorm нормализует признаки каждого объекта: \gamma\frac{x - \mu}{\sqrt{\sigma^2 + \varepsilon}} + \beta, статистики по последней оси.
- Не зависит от батча, одинаково работает при обучении и применении, естественен для последовательностей.
- Стандарт в трансформерах; BatchNorm — в свёрточных сетях.
- RMSNorm — LayerNorm без центрирования и \beta, используется в современных языковых моделях.
- Pre-Norm (x + f(\text{LN}(x))) устойчивее Post-Norm.
Практика решено 0 из 5
BatchNorm или LayerNorm?
| BatchNorm | LayerNorm | оба | |
|---|---|---|---|
| Статистики по объектам батча для каждого признака | |||
| Статистики по признакам каждого объекта | |||
| Одинаково работает при обучении и применении | |||
| Имеет обучаемые γ и β | |||
| Стандарт в трансформерах | |||
| Хранит скользящие средние | |||
| Классика для свёрточных сетей |
Вход LayerNorm — тензор формы (batch, seq_len, d_model) (батч текстов, по d_model чисел на токен). По какой оси считаются среднее и дисперсия?
Чем RMSNorm отличается от LayerNorm?
LayerNorm своими руками. Напишите layer_norm(x, gamma, beta, eps=1e-5): нормализуйте каждый
объект по последней оси (axis=-1, keepdims=True), используя смещённую дисперсию, затем
gamma * x_hat + beta. Должно работать для входов любой размерности: (N, D), (N, T, D).
Какие тесты проверяют код (3)
rng = np.random.default_rng(0) x = rng.normal(3, 5, size=(4, 6)) out = layer_norm(x, np.ones(6), np.zeros(6)) assert np.allclose(out.mean(axis=1), 0, atol=1e-9), "Среднее каждой строки должно стать 0" assert np.allclose(out.std(axis=1), 1, atol=1e-3), "Стандартное отклонение каждой строки — 1" x = np.random.default_rng(1).normal(size=(2, 5, 8)) g, b = np.arange(8.0), np.ones(8) out = layer_norm(x, g, b) assert out.shape == x.shape ref = g * (x - x.mean(-1, keepdims=True)) / np.sqrt(x.var(-1, keepdims=True) + 1e-5) + b assert np.allclose(out, ref), "Для 3D-входа нормализация по последней оси" x = np.array([[1.0, 2, 3], [100, 200, 300]]) out = layer_norm(x, np.ones(3), np.zeros(3)) assert np.allclose(out[0], out[1], atol=1e-4), "Масштаб объекта не важен: [1,2,3] и [100,200,300] нормализуются одинаково"
import numpy as np
def layer_norm(x, gamma, beta, eps=1e-5):
mean = x.mean(axis=-1, keepdims=True)
var = x.var(axis=-1, keepdims=True)
return gamma * (x - mean) / np.sqrt(var + eps) + beta
RMSNorm. Напишите rms_norm(x, gamma, eps=1e-6):
\gamma \cdot \dfrac{x}{\sqrt{\text{mean}(x^2) + \varepsilon}}, где среднее — по последней оси.
Какие тесты проверяют код (1)
x = np.array([[3.0, 4.0], [1.0, -1.0]]) out = rms_norm(x, np.ones(2)) assert np.allclose(out, [[3 / np.sqrt(12.5), 4 / np.sqrt(12.5)], [1, -1]], atol=1e-5) assert np.allclose((out ** 2).mean(axis=-1), 1, atol=1e-5), "Средний квадрат после RMSNorm — 1" assert not np.allclose(rms_norm(np.array([[1.0, 2, 3]]), np.ones(3)).mean(), 0), "RMSNorm не вычитает среднее"
import numpy as np
def rms_norm(x, gamma, eps=1e-6):
return gamma * x / np.sqrt((x ** 2).mean(axis=-1, keepdims=True) + eps)
Войдите, чтобы отмечать пройденные уроки.