Инициализация и нормализация
Почему сигнал и градиенты в глубокой сети затухают или взрываются, как правильный выбор начальных весов (Xavier, Kaiming) удерживает их на старте, а нормализация — во время всего обучения, и почему почти все современные LLM используют RMSNorm в схеме Pre-Norm.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В модуле 3.2 вы обучили MLP с одним скрытым слоем из шести нейронов, и всё получилось с первой попытки. Теперь добавьте слоёв: 10, 30, 50. Скорее всего, loss застынет на месте с первого шага. До конца 2000-х глубокие сети считались почти необучаемыми: сигнал, проходя через десятки слоёв, либо гаснет до нуля, либо разрастается до бесконечности.
У Qwen3-0.6B 28 блоков трансформера, у DeepSeek-V3 — 61. Эти модели обучаются стабильно благодаря трём приёмам: правильной инициализации весов, нормализации и остаточным связям. Первые два — тема этого урока, третий — следующего. Без них не работает ни одна LLM.
Сигнал через 50 слоёв
Заголовок раздела «Сигнал через 50 слоёв»Вспомните обратный проход: по цепному правилуцепное правилоEN chain ruleПроизводная композиции — произведение производных звеньев — (f(g(x)))′ = f′(g(x))·g′(x). Математическая основа backprop. градиент для первого слоя — это произведение множителей от всех слоёв выше. Произведение многих чисел ведёт себя капризно:
| Множитель на слой | Через 10 слоёв | Через 50 слоёв |
|---|---|---|
| 0.25 (максимум производной сигмоиды) | ||
| 0.9 | 0.35 | 0.005 |
| 1.0 | 1 | 1 |
| 1.1 | 2.6 | 117 |
Если множитель меньше единицы, градиент первых слоёв затухает: они перестают учиться, хотя их веса
формально обновляются. Это затухание градиентовзатухание градиентовEN vanishing gradientsГрадиент первых слоёв — произведение множителей всех слоёв выше; если они меньше 1, он экспоненциально уменьшается с глубиной (0.9⁵⁰ ≈ 0.005), и нижние слои перестают учиться.. Если больше единицы — градиенты взрываются,
шаги оптимизатора становятся гигантскими, а loss превращается в nan. Это взрыв градиентоввзрыв градиентовEN exploding gradientsОбратная ситуация: множители больше 1, градиенты растут экспоненциально (1.1⁵⁰ ≈ 117), loss становится nan. Лечат инициализацией, нормализацией и gradient clipping..
Устойчиво только окно около единицы, и сеть надо в нём удержать.
С прямым проходом то же самое. Каждый слой умножает масштаб активаций на некоторое число. Если оно не равно единице, то к 50-му слою активации либо крошечные, либо огромные. Поэтому нам нужен способ выбрать веса так, чтобы масштаб сигнала не менялся от слоя к слою.
Почему «просто случайные числа» не работают
Заголовок раздела «Почему «просто случайные числа» не работают»Масштаб сигнала будем мерить дисперсией — средним квадратом отклонения от среднего — и стандартным отклонением std (урок 1.3.1). Запись означает нормальное распределение («колокол») со средним 0 и дисперсией , то есть со std . Например, — колокол со std 0.01.
Возьмём слой с входами: . Пусть входы и веса независимы, со средним 0. Понадобятся два факта:
- Для независимых и со средним 0 дисперсия произведения равна произведению дисперсий: . (Среднее равно нулю, а средний квадрат для независимых величин распадается в произведение средних квадратов.)
- Дисперсия суммы независимых слагаемых равна сумме дисперсий (урок 1.3.1). У нас одинаковых слагаемых, поэтому
Здесь написано: каждый слой умножает дисперсию сигнала на . Проверим формулу числами, прежде чем ей пользоваться:
import numpy as np
rng = np.random.default_rng(0)n = 512x = rng.standard_normal((10_000, n)) # 10 000 примеров, у каждого входа Var = 1W = rng.standard_normal((n, 1)) * 0.1 # веса со std 0.1, то есть Var(w) = 0.01z = x @ W # выход одного нейрона на каждом примереprint(f"по формуле: {n * 0.01 * 1:.2f}, на деле: {z.var():.2f}")по формуле: 5.12, на деле: 4.95Совпало с точностью до случайного разброса. Подставим теперь разные std весов:
- Веса из , то есть std = 1. Множитель , а std выхода — . После почти все значения окажутся у , в зоне насыщения. Производная там почти нулевая, и градиент дальше не проходит. С ReLU активации просто растут: к 50-му слою их std порядка .
- Веса из . Множитель для std: . Каждый слой уменьшает сигнал в 4 раза, и к 10-му слою от него остаётся .
Нужен std ровно такой, чтобы множитель был равен единице: тогда сигнал проходит через любое число слоёв, не раздуваясь и не съёживаясь.
Xavier и Kaiming: инициализация с расчётом
Заголовок раздела «Xavier и Kaiming: инициализация с расчётом»Инициализация весовинициализация весовEN weight initializationВыбор начальных значений весов; главное — дисперсия, при которой сигнал не затухает и не взрывается от слоя к слою. — это выбор их начальных значений. Одинаковыми (например, нулями) веса делать нельзя: в уроке 3.2.3 одинаковые нейроны получали одинаковые градиенты и так и оставались копиями друг друга. Поэтому веса берут случайными — из нормального или равномерного распределения со средним 0. Вопрос только в дисперсии.
Xavier (Glorot, 2010)
Заголовок раздела «Xavier (Glorot, 2010)»Условие «дисперсия не меняется» даёт , то есть . Но обратный проход умножает градиент на , и для него то же рассуждение даёт . Ксавье Глоро и Йошуа Бенджио предложили компромисс: подставить вместо среднее арифметическое .
Это инициализация Xavierинициализация Xavier (Glorot)EN Xavier / Glorot initializationVar(w) = 2 / (n_in + n_out) (Glorot, Bengio, 2010): компромисс между условиями прямого и обратного прохода. Рассчитана на активации, которые около нуля ведут себя как тождество (tanh, линейные слои); с ReLU сигнал затухает — там нужна Kaiming.. Для слоя 784 → 256 получаем std . Есть и равномерный вариант: — равномерное распределение, где любое число от до одинаково вероятно, — с (у равномерного распределения дисперсия , поэтому дисперсия выходит та же). Вывод предполагает, что активация около нуля ведёт себя как тождество — функция, которая возвращает вход без изменений, . Для это так (), для ReLU — нет: она обнуляет всё отрицательное.
Kaiming (He, 2015)
Заголовок раздела «Kaiming (He, 2015)»Сначала идея словами: ReLU выбрасывает половину сигнала, поэтому веса должны быть «вдвое сильнее» (по дисперсии), чтобы это компенсировать. Теперь аккуратно.
ReLU обнуляет отрицательную половину входов. Её выход неотрицателен, и среднее у него уже не 0. Поэтому в формулу для следующего слоя честнее подставлять не дисперсию входа, а его средний квадрат (среднее значение ; — матожидание, то есть среднее, урок 1.3.1): . Это верно при любых входах, если среднее весов равно 0. Если симметрично относительно нуля, ReLU обнуляет ровно половину его значений, и : ReLU «съедает» половину среднего квадрата сигнала. Пример: принимает значения поровну. Средний квадрат равен . После ReLU остаются , и средний квадрат равен — ровно половина. От слоя к слою получаем . Чтобы множитель был равен единице, дисперсию весов удваивают:
Это инициализация Kaimingинициализация Kaiming (He)EN Kaiming / He initializationVar(w) = 2 / n_in (He и соавт., MSRA, 2015). Двойка компенсирует то, что ReLU обнуляет половину входов. В PyTorch — nn.init.kaiming_normal_., её вывели Каймин Хэ и соавторы из Microsoft Research Asia (подробности — в китайском углу ниже). Для : std .
Проверим на сети из 30 слоёв ширины 256 с ReLU (это код задания ниже):
small слой 1: 0.0937 слой 10: 2.82e-10 слой 30: 2.85e-29xavier слой 1: 0.583 слой 10: 0.0229 слой 30: 1.73e-05kaiming слой 1: 0.825 слой 10: 0.75 слой 30: 0.663Каждая строка — std активаций после 1-го, 10-го и 30-го слоя. Запись 2.82e-10 означает
(научная запись из урока 1.0.2). С «маленькими» весами сигнал умирает уже к 10-му слою. Xavier теряет на
каждом слое: с ReLU он недодаёт ровно ту двойку. Kaiming держит масштаб на всей глубине. Std здесь около 0.8, а не 1, потому что выход
ReLU неотрицателен: единице равен его средний квадрат, а std меньше.
| Схема | Для чего | В PyTorch | |
|---|---|---|---|
| Xavier (2010) | tanh, сигмоида, линейные слои | nn.init.xavier_normal_ | |
| Kaiming (2015) | ReLU и родственные | nn.init.kaiming_normal_(w, nonlinearity="relu") | |
nn.Linear по умолчанию | «разумное» значение для неглубоких сетей | ||
| GPT-2, nanoGPT | std | трансформеры | nn.init.normal_(w, std=0.02) |
А как в трансформерах?
Заголовок раздела «А как в трансформерах?»У трансформера (GPT-2 small), и формула Ксавье дала бы std . В GPT-2 и nanoGPT все веса берут из — чуть меньше. Это эмпирическое число: в трансформере устойчивость обеспечивают нормализация и остаточные связи, поэтому инициализация может быть «с запасом» маленькой. В следующем уроке мы увидим ещё один приём GPT-2: выходные проекции блоков дополнительно уменьшают в раз, где — число блоков.
Но инициализация отвечает только за старт. Через тысячу шагов веса изменятся, и распределения активаций поплывут. Поэтому масштаб нужно поддерживать постоянно — это работа нормализации.
Нормализация: держать масштаб на каждом шаге
Заголовок раздела «Нормализация: держать масштаб на каждом шаге»Идея любой нормализации одна: взять группу чисел, вычесть среднее, поделить на стандартное отклонение (как стандартизация признаковстандартизация признаковEN feature standardization (z-score)(x − μ)/σ. Не путайте с нормализацией (normalization) — обычно это min-max масштабирование в [0, 1]; в китайских текстах эти термины иногда смешивают. из ступени 2), а потом дать сети обучаемый масштаб и сдвиг , чтобы она могла вернуть нужный ей масштаб. Способы отличаются прежде всего тем, какую группу чисел нормировать. (RMSNorm к тому же пропускает вычитание среднего, о ней ниже.)
Представьте активации слоя как таблицу «примеры батча × признаки». Буквой B отмечены числа, которые BatchNorm нормирует вместе, буквой L — числа, которые вместе нормируют LayerNorm и RMSNorm:
| признак 1 | признак 2 | признак 3 | признак 4 | |
|---|---|---|---|---|
| пример 1 | · | B | · | · |
| пример 2 | L | B, L | L | L |
| пример 3 | · | B | · | · |
BatchNorm берёт столбец: один признак у всех примеров батча. LayerNorm и RMSNorm берут строку: все признаки одного примера. На деле нормируются все столбцы (BatchNorm) или все строки (LayerNorm и RMSNorm), каждый отдельно; в таблице для наглядности выделено по одному.
BatchNorm (2015)
Заголовок раздела «BatchNorm (2015)»пакетная нормализацияпакетная нормализация (BatchNorm)EN batch normalizationНормирует каждый признак по примерам батча, хранит скользящие статистики для инференса (Ioffe, Szegedy, 2015). Стандарт CNN; в LLM не используется. предложили Иоффе и Сегеди из Google. Для каждого признака считаются среднее и дисперсия по примерам батча:
Число (например, ) защищает от деления на ноль. BatchNorm сделал возможным обучение глубоких CNN с большим learning rate и стал стандартом компьютерного зрения. Но у него есть неприятные свойства:
- Выход примера зависит от соседей по батчу. Поменяли состав батча — поменялся ответ.
- Обучение и инференс различаются. На инференсеинференс, вывод моделиEN inferenceСм. «reasoning»: в китайском одно и то же слово. (когда готовую модель применяют к
новым данным) батча может не быть, поэтому BatchNorm во время обучения копит скользящие средние среднего и дисперсии
каждого признака и в режиме
model.eval()использует их. Забытыйmodel.eval()— классический баг. - Маленькие батчи дают шумную статистику. При обучении LLM на одну GPU часто приходится 1–4 последовательности.
LayerNorm (2016)
Заголовок раздела «LayerNorm (2016)»нормализация слоянормализация слояEN layer normalization (Ба, Кирос, Хинтон) нормирует каждый пример по его признакам. В трансформере пример — это вектор одного токена длины :
Пример: . Среднее , дисперсия , . Результат (при , ): . Соседи по батчу ни на что не влияют, обучение и инференс считаются одинаково.
RMSNorm (2019)
Заголовок раздела «RMSNorm (2019)»RMSNormRMSNorm (нормализация по среднеквадратичному)EN root mean square layer normalization (RMSNorm)x / sqrt(mean(x²) + ε) · g — LayerNorm без вычитания среднего и без сдвига β (Zhang, Sennrich, 2019). Стандарт Llama, Qwen, DeepSeek, MiniMind. (Бяо Чжан и Рико Сеннрих) — LayerNorm без центрирования и без сдвига . Вектор делится на свой среднеквадратичный размер (root mean square):
Словами: возводим числа в квадрат, усредняем, извлекаем корень — это «типичный размер» числа в векторе; делим вектор на этот размер и умножаем на обучаемый вектор (у LayerNorm его зовут ).
Пример: . , результат — . Если умножить на 10, ответ не изменится: даёт и те же . Нормализация инвариантна к масштабу — ей всё равно, во сколько раз растянут вход.
Посмотрим на все три в деле. Вторая строка — та же, что первая, только в 10 раз больше:
import torchimport torch.nn as nn
torch.set_printoptions(precision=2, sci_mode=False)x = torch.tensor([[1.0, 2.0, 3.0, 6.0], # «токен» 1 [10.0, 20.0, 30.0, 60.0], # тот же вектор, умноженный на 10 [0.0, -1.0, 1.0, 0.0]]) # совсем другой токен
bn = nn.BatchNorm1d(4) # нормирует КАЖДЫЙ ПРИЗНАК по батчу (по столбцам)ln = nn.LayerNorm(4) # нормирует КАЖДЫЙ ПРИМЕР по признакам (по строкам)rms = nn.RMSNorm(4) # то же, но без вычитания среднего (PyTorch ≥ 2.4)
print("BatchNorm:\n", bn(x))print("LayerNorm:\n", ln(x))print("RMSNorm:\n", rms(x))
# BatchNorm зависит от соседей по батчу: уберём третью строку — первая изменитсяprint("BatchNorm без 3-й строки, строка 1:", bn(x[:2])[0])print("LayerNorm без 3-й строки, строка 1:", ln(x[:2])[0])
# Параметры: у LayerNorm γ и β, у RMSNorm только γprint(sum(p.numel() for p in ln.parameters()), sum(p.numel() for p in rms.parameters()))BatchNorm: tensor([[-0.59, -0.54, -0.63, -0.59], [ 1.41, 1.40, 1.41, 1.41], [-0.82, -0.86, -0.78, -0.82]], grad_fn=<NativeBatchNormBackward0>)LayerNorm: tensor([[-1.07, -0.53, 0.00, 1.60], [-1.07, -0.53, 0.00, 1.60], [ 0.00, -1.41, 1.41, 0.00]], grad_fn=<NativeLayerNormBackward0>)RMSNorm: tensor([[ 0.28, 0.57, 0.85, 1.70], [ 0.28, 0.57, 0.85, 1.70], [ 0.00, -1.41, 1.41, 0.00]], grad_fn=<MulBackward0>)BatchNorm без 3-й строки, строка 1: tensor([-1.00, -1.00, -1.00, -1.00], grad_fn=<SelectBackward0>)LayerNorm без 3-й строки, строка 1: tensor([-1.07, -0.53, 0.00, 1.60], grad_fn=<SelectBackward0>)8 4Что видно:
- BatchNorm превратил первый токен почти в константу : внутренняя «форма» вектора потерялась, осталось только «меньше или больше среднего по батчу». А без третьей строки ответ для первой стал другим, .
- LayerNorm и RMSNorm дали первым двум строкам одинаковый ответ: масштаб убран, форма вектора сохранена. От состава батча ничего не зависит.
- У RMSNorm параметров вдвое меньше: только .
Почему в LLM — RMSNorm
Заголовок раздела «Почему в LLM — RMSNorm»BatchNorm отпадает сразу. Кроме перечисленных проблем, есть ещё одна: если нормировать по позициям последовательности, статистика будет включать будущие токены. Тогда модель будет подглядывать в ответ, и обучение на предсказание следующего токена потеряет смысл. А при генерации по одному токену «статистики батча» просто нет.
Между LayerNorm и RMSNorm выбор решили практические соображения:
- Дешевле. Не нужно считать среднее и вычитать его, нет вектора . Нормализация в LLM ограничена не арифметикой, а чтением памяти, но любая сэкономленная операция над тензором размера «батч × длина × » повторяется в каждом слое на каждом шаге. Авторы RMSNorm сообщили об ускорении на 7–64% в зависимости от модели.
- Качество то же. Чжан и Сеннрих предположили, что LayerNorm помогает обучению благодаря инвариантности к масштабу, а центрирование почти ничего не добавляет. В их экспериментах на нескольких задачах RMSNorm действительно работал наравне с LayerNorm.
- Так сложилось. Ту же нормировку «только масштаб, без центрирования и сдвига» независимо применил T5 (2019), затем Llama (2023). После Llama её взяли Qwen, DeepSeek, Mistral, MiniMind. GPT-2 и GPT-3 ещё использовали LayerNorm.
| BatchNorm | LayerNorm | RMSNorm | |
|---|---|---|---|
| Нормирует | признак по батчу | пример по признакам | пример по признакам |
| Вычитает среднее | да | да | нет |
| Параметры | , + скользящие статистики | , | |
| Обучение и инференс | различаются | одинаковы | одинаковы |
| Где живёт | CNN (ResNet) | BERT, GPT-2, GPT-3 | T5, Llama, Qwen, DeepSeek |
Куда ставить нормализацию: Pre-Norm и Post-Norm
Заголовок раздела «Куда ставить нормализацию: Pre-Norm и Post-Norm»Трансформер (его мы соберём на ступени 4) состоит из одинаковых блоков, а в каждом блоке два подслоя: внимание (attention), через которое токены — кусочки текста — обмениваются информацией, и MLP. Что такое внимание, сейчас знать не нужно: для этого раздела подслой — просто «какая-то функция ». Каждый подслой обёрнут остаточной связьюостаточная связьEN residual connection: выход равен , то есть подслой не заменяет вход, а прибавляет поправку. Подробно о ней — в следующем уроке. Сейчас важно другое: куда поставить нормализацию относительно этого сложения? Вариантов два.
flowchart LR
subgraph P1["Post-Norm (2017, BERT)"]
a1["x"] --> f1["F"] --> s1(("+"))
a1 --> s1
s1 --> n1["Norm"] --> o1["выход"]
end
subgraph P2["Pre-Norm (GPT-2, Llama, Qwen, DeepSeek)"]
a2["x"] --> n2["Norm"] --> f2["F"] --> s2(("+"))
a2 --> s2
s2 --> o2["выход"]
endPost-NormPost-Norm (нормализация после сложения)EN post-normalization (Post-LN)Norm(x + F(x)), как в исходном трансформере 2017 года и BERT. Без разогрева learning rate нестабилен на глубине., как в «Attention Is All You Need» и BERT:
Pre-NormPre-Norm (нормализация на входе подслоя)EN pre-normalization (Pre-LN)x + F(Norm(x)): нормализуется только вход ветки, остаточный путь чистый. Стандарт с GPT-2 (2019); требует финальной нормализации перед LM-головой., как в GPT-2, Llama, Qwen и DeepSeek:
В Pre-Norm остаточный путь «чистый»: развернув все слои, получаем . Производная по содержит слагаемое-единицу (тождество), и градиент доходит до нижних слоёв по прямой «трассе». В Post-Norm сигнал снизу проходит через нормализацию на каждом слое, и каждая его перемасштабирует.
Сюн и соавторы (arXiv:2002.04745, 2020) доказали поучительную вещь. При инициализации градиент весов последних слоёв Post-Norm не зависит от глубины, а у Pre-Norm он сам уменьшается как , где — число слоёв: в 4 раза больше слоёв — градиент вдвое меньше. Проверим. Код ниже собирает сети из 6, 24 и 96 одинаковых блоков, делает один обратный проход и печатает размер градиента последнего блока; подробности кода не важны, важны три строки вывода:
import torchimport torch.nn as nn
d = 64
class Block(nn.Module): """MLP-блок с остаточной связью: mode='post' — LN(x + F(x)), mode='pre' — x + F(LN(x))."""
def __init__(self, mode): super().__init__() self.mode = mode self.norm = nn.LayerNorm(d) self.f = nn.Sequential(nn.Linear(d, 4 * d), nn.ReLU(), nn.Linear(4 * d, d)) nn.init.kaiming_normal_(self.f[0].weight, nonlinearity="relu") nn.init.kaiming_normal_(self.f[2].weight, nonlinearity="linear")
def forward(self, x): if self.mode == "post": return self.norm(x + self.f(x)) # Post-Norm: нормируем сумму return x + self.f(self.norm(x)) # Pre-Norm: нормируем только вход ветки
def last_block_grad(mode, depth): """Норма градиента по первой матрице ПОСЛЕДНЕГО блока при инициализации.""" torch.manual_seed(0) blocks = nn.Sequential(*[Block(mode) for _ in range(depth)]) # depth блоков подряд final = nn.LayerNorm(d) if mode == "pre" else nn.Identity() # Pre-Norm требует финальной нормы; Identity — «ничего не делать» x, r = torch.randn(256, d), torch.randn(256, d) (final(blocks(x)) * r).mean().backward() # простой «loss»: случайная взвешенная сумма выходов return blocks[-1].f[0].weight.grad.norm().item()
for depth in (6, 24, 96): post, pre = last_block_grad("post", depth), last_block_grad("pre", depth) print(f"блоков {depth:2d}: Post-Norm {post:.4f} Pre-Norm {pre:.4f}")блоков 6: Post-Norm 0.0305 Pre-Norm 0.0178блоков 24: Post-Norm 0.0320 Pre-Norm 0.0096блоков 96: Post-Norm 0.0288 Pre-Norm 0.0040Глубина выросла в 16 раз (с 6 до 96), и градиент последнего блока Pre-Norm уменьшился в раза — почти ровно . У Post-Norm он стоит на месте. Почему это плохо для Post-Norm? Интуиция такая: изменения всех слоёв за один шаг складываются, и чем глубже сеть, тем сильнее общий шаг меняет её выход. Pre-Norm сам «притормаживает» верхние слои пропорционально глубине, а Post-Norm — нет. На практике это значит: в Post-Norm верхние слои глубокой сети с первого шага получают слишком крупные обновления, и с обычным learning rate обучение разваливается. Спасает разогрев (warmup) — тысячи шагов с маленьким learning rate в начале — или особые приёмы вроде DeepNorm (Microsoft Research Asia, 2022), который усиливает остаточный путь и уменьшает начальные веса веток; его вы попробуете в домашней работе модуля. Pre-Norm сам гасит обновления верхних слоёв и обучается стабильно без всяких ухищрений. Это главная причина, почему с 2019 года почти все LLM — Pre-Norm.
У Pre-Norm две особенности:
- Финальная нормализация. Поток внутри модели никто не нормирует, поэтому после последнего блока ставят
ещё одну норму, и только потом LM-голову (последний линейный слой, который превращает вектор в логиты токенов).
В коде Qwen3 и DeepSeek-V3 это поле
normмодели. - Растущий поток. Норма растёт с глубиной, и вклад каждого следующего слоя в ней «размывается». Эксперимент с косинусом вход–выход — в уроке 4.5, а как с этим борются в 2026 году (mHC у DeepSeek, Attention Residuals у Kimi) — на ступени 9.
Практика
Заголовок раздела «Практика»Реализуйте три схемы инициализации, «прогон» сигнала через глубокую сеть и две нормализации на NumPy.
Тесты проверят std весов, сохранение сигнала с Kaiming и его гибель с Xavier и «маленькими» весами при ReLU,
а также значения LayerNorm и RMSNorm на примерах из урока. Когда init_weights и activation_stds готовы, кнопка
«Запустить» печатает ту самую таблицу из раздела про Kaiming — сверьте с ней свои числа.
import numpy as np
def init_weights(n_in, n_out, scheme, rng):
"""Возвращает матрицу весов формы (n_in, n_out) из нормального распределения N(0, std²).
scheme — схема инициализации:
'small' — std = 0.01 (наивные «маленькие случайные числа»);
'xavier' — std = sqrt(2 / (n_in + n_out)) (Glorot, 2010);
'kaiming' — std = sqrt(2 / n_in) (He, 2015, под ReLU).
Для любой другой строки — raise ValueError.
rng — генератор np.random.default_rng(...): берите rng.standard_normal((n_in, n_out)).
"""
# TODO: выберите std по схеме и верните rng.standard_normal(...) * std
...
def activation_stds(x, weights, act):
"""Прогоняет x через слои: h = act(h @ W) для каждой W из списка weights.
Возвращает список из len(weights) чисел — std активаций после каждого слоя.
"""
# TODO: цикл по слоям, после каждого — stds.append(float(h.std()))
...
def layer_norm(x, eps=1e-5):
"""LayerNorm без обучаемых γ и β: по последней оси вычесть среднее
и поделить на sqrt(дисперсия + eps). Подсказка: axis=-1, keepdims=True."""
# TODO
...
def rms_norm(x, eps=1e-6):
"""RMSNorm без обучаемого веса: x / sqrt(mean(x²) + eps) по последней оси.
Среднее НЕ вычитается."""
# TODO
...
def relu(z):
return np.maximum(z, 0.0)
# Эксперимент для кнопки «Запустить»: 30 слоёв ширины 256 с ReLU
rng = np.random.default_rng(0)
x = rng.standard_normal((200, 256))
for scheme in ("small", "xavier", "kaiming"):
Ws = [init_weights(256, 256, scheme, rng) for _ in range(30)]
s = activation_stds(x, Ws, relu)
if s:
print(f"{scheme:8s} слой 1: {s[0]:.3g} слой 10: {s[9]:.3g} слой 30: {s[29]:.3g}")
else:
print(f"{scheme:8s} — пока пусто: реализуйте init_weights и activation_stds")
Градиенты по слоям в PyTorch (локально)
Заголовок раздела «Градиенты по слоям в PyTorch (локально)»Повторим эксперимент с обратным проходом: 30 слоёв Linear + ReLU, инициализация PyTorch по умолчанию против Kaiming.
import torchimport torch.nn as nn
torch.manual_seed(0)
# 1) Какое std даёт nn.Linear «из коробки»?lin = nn.Linear(512, 512)print(f"std весов nn.Linear(512, 512): {lin.weight.std():.4f}") # ≈ 1/sqrt(3·512)print(f"Kaiming для ReLU хочет: {(2 / 512) ** 0.5:.4f}")
# 2) Глубокая сеть: 30 слоёв Linear + ReLU. Смотрим градиенты по слоям.def deep_mlp(init, depth=30, d=256): layers = [] for _ in range(depth): lin = nn.Linear(d, d) if init == "kaiming": nn.init.kaiming_normal_(lin.weight, nonlinearity="relu") # std = sqrt(2 / d) nn.init.zeros_(lin.bias) layers += [lin, nn.ReLU()] return nn.Sequential(*layers)
x = torch.randn(512, 256)r = torch.randn(512, 256) # фиксированное «направление» loss, одинаковое для всех сетейfor init in ("default", "kaiming"): torch.manual_seed(1) net = deep_mlp(init) loss = (net(x) * r).mean() loss.backward() g = [m.weight.grad.norm().item() for m in net if isinstance(m, nn.Linear)] print(f"{init:8s} ‖grad‖: слой 1 = {g[0]:.1e}, слой 15 = {g[14]:.1e}, слой 30 = {g[29]:.1e}")std весов nn.Linear(512, 512): 0.0255Kaiming для ReLU хочет: 0.0625default ‖grad‖: слой 1 = 1.7e-13, слой 15 = 1.1e-09, слой 30 = 9.0e-04kaiming ‖grad‖: слой 1 = 3.1e-02, слой 15 = 2.7e-02, слой 30 = 2.5e-02С инициализацией по умолчанию градиент первого слоя примерно в раз меньше, чем у последнего: первые слои фактически заморожены. С Kaiming градиенты всех слоёв одного порядка. Такие графики «норма градиента по слоям» — первое, что стоит построить, если глубокая сеть не учится (см. урок об отладке).
Проверь себя
Заголовок раздела «Проверь себя»- Градиент первых слоёв — произведение множителей всех слоёв выше. Если множитель отличается от единицы, градиенты затухают или взрываются экспоненциально по глубине.
- Инициализация выбирает дисперсию весов так, чтобы сигнал сохранял масштаб: Xavier для tanh,
Kaiming для ReLU.
nn.Linearпо умолчанию не Kaiming. - Нормализация держит масштаб на протяжении всего обучения. BatchNorm нормирует признак по батчу (CNN), LayerNorm и RMSNorm — токен по признакам. В LLM стоит RMSNorm: дешевле и не хуже.
- Pre-Norm оставляет остаточный путь чистым и обучается без капризов, поэтому его выбирают почти все современные LLM. Цена — финальная норма и растущий поток.
Дальше: остаточные связи. Почему 56-слойная сеть может учиться хуже 20-слойной, как ResNet из пекинского MSRA это исправил, почему остаточный поток — «шина» трансформера и как бороться с переобучением с помощью dropout и weight decay.