Фаза 03 · урок 08
Инициализация весов и стабильность обучения
Цель урока: Инициализируйте все веса нулями. Ничто не учится. Каждый нейрон вычисляет одну и ту же функцию, получает один и тот же градиент и обновляется одинаково. После 10 000 эпох ваш скрытый слой из 512 нейронов — всё ещё 512 копий одного…
Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.
Содержание урока
- Цели обучения
- Проблема
- Концепция
- Проблема симметрии
- Распространение дисперсии по слоям
- Инициализация Xavier/Glorot
- Инициализация Kaiming/He
- Инициализация трансформеров
- Величина активаций на протяжении 50 слоёв
- Выбор правильной инициализации
- Соберите это
- Шаг 1: стратегии инициализации
- Шаг 2: функции активации
- Шаг 3: прямой проход через 50 слоёв
- Шаг 4: эксперимент
- Шаг 5: демонстрация симметрии
- Шаг 6: послойный отчёт о величинах
- Используйте это
- Выпустите это
- Упражнения
- Ключевые термины
- Дополнительное чтение
Неправильно инициализируете — и обучение никогда не начнётся. Правильно — и 50 слоёв обучаются так же плавно, как 3.
Тип: Сборка Языки: Python Предварительные требования: Урок 03.04 (Функции активации), Урок 03.07 (Регуляризация) Время: ~90 минут
Цели обучения
- Реализовать стратегии инициализации нулями, случайными значениями, Xavier/Glorot и Kaiming/He и измерить их влияние на величину активаций в 50 слоях
- Вывести, почему инициализация Xavier использует Var(w) = 2/(fan_in + fan_out), а Kaiming — Var(w) = 2/fan_in
- Продемонстрировать проблему симметрии при нулевой инициализации и объяснить, почему одного случайного масштаба недостаточно
- Сопоставить правильную стратегию инициализации с функцией активации: Xavier для sigmoid/tanh, Kaiming для ReLU/GELU
Проблема
Инициализируйте все веса нулями. Ничто не учится. Каждый нейрон вычисляет одну и ту же функцию, получает один и тот же градиент и обновляется одинаково. После 10 000 эпох ваш скрытый слой из 512 нейронов — всё ещё 512 копий одного нейрона. Вы заплатили за 512 параметров, а получили 1.
Инициализируйте их слишком большими. Активации взрываются при прохождении по сети. К 10-му слою значения достигают 1e15. К 20-му они переполняются до бесконечности. Градиенты повторяют ту же траекторию в обратном направлении.
Инициализируйте их случайно из стандартного нормального распределения. Работает для 3 слоёв. На 50 слоях сигнал схлопывается к нулю или взрывается до бесконечности — в зависимости от того, был ли случайный масштаб чуть слишком мал или чуть слишком велик. Граница между «работает» и «сломано» чрезвычайно тонкая.
Инициализация весов — самое недооценённое решение в глубоком обучении. Архитектурам посвящают статьи. Оптимизаторам — посты в блогах. Инициализации — сноску. Но ошибитесь в ней — и всё остальное не имеет значения: ваша сеть мертва ещё до начала обучения.
Концепция
Проблема симметрии
У каждого нейрона в слое одинаковая структура: умножить входы на веса, добавить смещение, применить активацию. Если все веса начинаются с одного значения (ноль — крайний случай), каждый нейрон вычисляет один и тот же выход. При обратном распространении каждый нейрон получает один и тот же градиент. На шаге обновления каждый нейрон изменяется на одну и ту же величину.
Вы застряли. В сети сотни параметров, но все они движутся синхронно. Это называется симметрией, а случайная инициализация — грубый способ её нарушить. Каждый нейрон начинает в другой точке пространства весов, поэтому изучает другой признак.
Но «случайности» недостаточно. От масштаба случайности зависит, будет ли сеть обучаться.
Распространение дисперсии по слоям
Рассмотрим один слой с fan_in входами:
z = w1*x1 + w2*x2 + ... + w_n*x_n
Если каждый вес wi взят из распределения с дисперсией Var(w), а каждый вход xi имеет дисперсию Var(x), дисперсия выхода равна:
Var(z) = fan_in * Var(w) * Var(x)
Если Var(w) = 1 и fan_in = 512, выходная дисперсия в 512 раз больше входной. После 10 слоёв: 512^10 = 1.2e27. Ваш сигнал взорвался.
Если Var(w) = 0.001, выходная дисперсия уменьшается в 0.001 * 512 = 0.512 раза на слой. После 10 слоёв: 0.512^10 = 0.00013. Ваш сигнал исчез.
Цель: выбрать Var(w) так, чтобы Var(z) = Var(x). Тогда величина сигнала остаётся постоянной между слоями.
Инициализация Xavier/Glorot
Glorot и Bengio (2010) вывели решение для активаций sigmoid и tanh. Чтобы сохранить дисперсию постоянной и в прямом, и в обратном проходе:
Var(w) = 2 / (fan_in + fan_out)
На практике веса берутся из:
w ~ Uniform(-limit, limit) where limit = sqrt(6 / (fan_in + fan_out))
или:
w ~ Normal(0, sqrt(2 / (fan_in + fan_out)))
Это работает, поскольку sigmoid и tanh примерно линейны около нуля, где находятся корректно инициализированные активации. Дисперсия остаётся стабильной на десятках слоёв.
Инициализация Kaiming/He
ReLU уничтожает половину выходов (всё отрицательное становится нулём). Эффективный fan_in делится пополам, поскольку в среднем половина входов зануляется. Инициализация Xavier этого не учитывает — она недооценивает нужную дисперсию.
He и соавторы (2015) скорректировали формулу:
Var(w) = 2 / fan_in
Веса берутся из:
w ~ Normal(0, sqrt(2 / fan_in))
Множитель 2 компенсирует зануление половины активаций ReLU. Без него сигнал сокращается примерно в ~0.5 раза на слой. При 50 слоях: 0.5^50 = 8.8e-16. Инициализация Kaiming это предотвращает.
Инициализация трансформеров
GPT-2 ввёл другой паттерн. Остаточные соединения добавляют выход каждого подслоя к его входу:
x = x + sublayer(x)
Каждое сложение увеличивает дисперсию. При N остаточных слоях дисперсия растёт пропорционально N. GPT-2 масштабирует веса остаточных слоёв на 1/sqrt(2N), где N — число слоёв. Это сохраняет накопленную величину сигнала стабильной.
Llama 3 (405B параметров, 126 слоёв) использует похожую схему. Без этого масштабирования остаточный поток неограниченно рос бы через 126 слоёв блоков внимания и прямого распространения.
Величина активаций на протяжении 50 слоёв
Выбор правильной инициализации
weight-init-variance
Соберите это
Шаг 1: стратегии инициализации
Четыре способа инициализировать матрицу весов. Каждый возвращает список списков (двумерную матрицу) с fan_in столбцами и fan_out строками.
import math
import random
def zero_init(fan_in, fan_out):
return [[0.0 for _ in range(fan_in)] for _ in range(fan_out)]
def random_init(fan_in, fan_out, scale=1.0):
return [[random.gauss(0, scale) for _ in range(fan_in)] for _ in range(fan_out)]
def xavier_init(fan_in, fan_out):
std = math.sqrt(2.0 / (fan_in + fan_out))
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
def kaiming_init(fan_in, fan_out):
std = math.sqrt(2.0 / fan_in)
return [[random.gauss(0, std) for _ in range(fan_in)] for _ in range(fan_out)]
Шаг 2: функции активации
Нам нужны sigmoid, tanh и ReLU, чтобы проверить каждую стратегию инициализации с предназначенной для неё активацией.
def sigmoid(x):
x = max(-500, min(500, x))
return 1.0 / (1.0 + math.exp(-x))
def tanh_act(x):
return math.tanh(x)
def relu(x):
return max(0.0, x)
Шаг 3: прямой проход через 50 слоёв
Пропустим случайные данные через глубокую сеть и измерим среднюю величину активаций на каждом слое.
def forward_deep(init_fn, activation_fn, n_layers=50, width=64, n_samples=100):
random.seed(42)
layer_magnitudes = []
inputs = [[random.gauss(0, 1) for _ in range(width)] for _ in range(n_samples)]
for layer_idx in range(n_layers):
weights = init_fn(width, width)
biases = [0.0] * width
new_inputs = []
for sample in inputs:
output = []
for neuron_idx in range(width):
z = sum(weights[neuron_idx][j] * sample[j] for j in range(width)) + biases[neuron_idx]
output.append(activation_fn(z))
new_inputs.append(output)
inputs = new_inputs
magnitudes = []
for sample in inputs:
magnitudes.append(sum(abs(v) for v in sample) / width)
mean_mag = sum(magnitudes) / len(magnitudes)
layer_magnitudes.append(mean_mag)
return layer_magnitudes
Шаг 4: эксперимент
Запустите все комбинации: нулевая инициализация, случайная N(0,1), случайная N(0,0.01), Xavier с sigmoid, Xavier с tanh, Kaiming с ReLU. Выведите величины на ключевых слоях.
def run_experiment():
configs = [
("Zero init + Sigmoid", lambda fi, fo: zero_init(fi, fo), sigmoid),
("Random N(0,1) + ReLU", lambda fi, fo: random_init(fi, fo, 1.0), relu),
("Random N(0,0.01) + ReLU", lambda fi, fo: random_init(fi, fo, 0.01), relu),
("Xavier + Sigmoid", xavier_init, sigmoid),
("Xavier + Tanh", xavier_init, tanh_act),
("Kaiming + ReLU", kaiming_init, relu),
]
print(f"{'Strategy':<30} {'L1':>10} {'L5':>10} {'L10':>10} {'L25':>10} {'L50':>10}")
print("-" * 80)
for name, init_fn, act_fn in configs:
mags = forward_deep(init_fn, act_fn)
row = f"{name:<30}"
for idx in [0, 4, 9, 24, 49]:
val = mags[idx]
if val > 1e6:
row += f" {'EXPLODED':>10}"
elif val < 1e-6:
row += f" {'VANISHED':>10}"
else:
row += f" {val:>10.4f}"
print(row)
Шаг 5: демонстрация симметрии
Покажите, что нулевая инициализация создаёт идентичные нейроны.
def symmetry_demo():
random.seed(42)
weights = zero_init(2, 4)
biases = [0.0] * 4
inputs = [0.5, -0.3]
outputs = []
for neuron_idx in range(4):
z = sum(weights[neuron_idx][j] * inputs[j] for j in range(2)) + biases[neuron_idx]
outputs.append(sigmoid(z))
print("\nSymmetry Demo (4 neurons, zero init):")
for i, out in enumerate(outputs):
print(f" Neuron {i}: output = {out:.6f}")
all_same = all(abs(outputs[i] - outputs[0]) < 1e-10 for i in range(len(outputs)))
print(f" All identical: {all_same}")
print(f" Effective parameters: 1 (not {len(weights) * len(weights[0])})")
Шаг 6: послойный отчёт о величинах
Выведите визуальную столбчатую диаграмму величин активаций через 50 слоёв.
def magnitude_report(name, magnitudes):
print(f"\n{name}:")
for i, mag in enumerate(magnitudes):
if i % 5 == 0 or i == len(magnitudes) - 1:
if mag > 1e6:
bar = "X" * 50 + " EXPLODED"
elif mag < 1e-6:
bar = "." + " VANISHED"
else:
bar_len = min(50, max(1, int(mag * 10)))
bar = "#" * bar_len
print(f" Layer {i+1:3d}: {bar} ({mag:.6f})")
Используйте это
PyTorch предоставляет их как встроенные функции:
import torch
import torch.nn as nn
layer = nn.Linear(512, 256)
nn.init.xavier_uniform_(layer.weight)
nn.init.xavier_normal_(layer.weight)
nn.init.kaiming_uniform_(layer.weight, nonlinearity='relu')
nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
nn.init.zeros_(layer.bias)
Когда вы вызываете nn.Linear(512, 256), PyTorch по умолчанию использует равномерную инициализацию Kaiming. Именно поэтому большинство простых сетей «просто работает» — PyTorch уже сделал правильный выбор. Но когда вы строите нестандартные архитектуры или заходите глубже 20 слоёв, нужно понимать происходящее и при необходимости переопределять значение по умолчанию.
Для трансформеров модели HuggingFace обычно выполняют инициализацию в методе _init_weights. Реализация GPT-2 масштабирует остаточные проекции на 1/sqrt(N). Если вы строите трансформер с нуля, это нужно добавить самостоятельно.
Выпустите это
Этот урок создаёт:
outputs/prompt-init-strategy.md— промпт, который диагностирует проблемы инициализации весов и рекомендует правильную стратегию
Упражнения
-
Добавьте инициализацию LeCun (Var = 1/fan_in, разработана для активации SELU). Запустите эксперимент с 50 слоями с LeCun init + tanh и сравните с Xavier + tanh.
-
Реализуйте масштабирование остаточных связей GPT-2: умножайте выход каждого слоя на 1/sqrt(2*N) перед добавлением в остаточный поток. Запустите 50 слоёв с масштабированием и без него, измерьте, насколько быстро растёт величина остатка.
-
Создайте функцию «проверки здоровья инициализации», которая принимает размерности слоёв сети и тип активации, затем рекомендует правильную инициализацию и предупреждает, если текущая инициализация вызовет проблемы.
-
Запустите эксперимент с fan_in = 16 и fan_in = 1024. Xavier и Kaiming адаптируются к fan_in, а случайная инициализация — нет. Покажите, как разрыв между «работает» и «ломается» расширяется при больших слоях.
-
Реализуйте ортогональную инициализацию (создайте случайную матрицу, вычислите её SVD, используйте ортогональную матрицу U). Сравните её с Kaiming для сетей с ReLU при 50 слоях.
Ключевые термины
| Термин | Как обычно говорят | Что это на самом деле означает |
|---|---|---|
| Инициализация весов | «Случайно задать начальные веса» | Стратегия выбора начальных значений весов, от которой зависит, сможет ли сеть вообще обучаться |
| Нарушение симметрии | «Сделать нейроны разными» | Использование случайной инициализации, чтобы нейроны изучали разные признаки, а не вычисляли идентичные функции |
| Fan-in | «Число входов нейрона» | Число входящих соединений, определяющее, как дисперсия входа накапливается во взвешенной сумме |
| Fan-out | «Число выходов нейрона» | Число исходящих соединений, важное для сохранения дисперсии градиента при обратном распространении |
| Инициализация Xavier/Glorot | «Инициализация для sigmoid» | Var(w) = 2/(fan_in + fan_out), разработана для сохранения дисперсии при активациях sigmoid и tanh |
| Инициализация Kaiming/He | «Инициализация для ReLU» | Var(w) = 2/fan_in, учитывает, что ReLU зануляет половину активаций |
| Распространение дисперсии | «Как сигналы растут или уменьшаются в слоях» | Математический анализ того, как дисперсия активаций меняется слой за слоем в зависимости от масштаба весов |
| Масштабирование остатка | «Трюк инициализации GPT-2» | Масштабирование весов остаточного соединения на 1/sqrt(2N), чтобы предотвратить рост дисперсии через N слоёв трансформера |
| Мёртвая сеть | «Ничего не обучается» | Сеть, в которой плохая инициализация приводит к нулевым градиентам или насыщению всех активаций |
| Взрывающиеся активации | «Значения уходят в бесконечность» | Ситуация, когда дисперсия весов слишком высока и величины активаций растут экспоненциально по слоям |
Дополнительное чтение
- Glorot & Bengio, «Understanding the difficulty of training deep feedforward neural networks» (2010) — исходная статья об инициализации Xavier с анализом дисперсии
- He и соавт., «Delving Deep into Rectifiers» (2015) — представила инициализацию Kaiming для ReLU
- Radford и соавт., «Language Models are Unsupervised Multitask Learners» (2019) — статья GPT-2 с инициализацией, масштабируемой для остаточных соединений
- Mishkin & Matas, «All You Need is a Good Init» (2016) — послойная инициализация с единичной дисперсией, эмпирическая альтернатива аналитическим формулам
Источник: Weight Initialization and Training Stability 03.07 — Регуляризация · Фаза 3 — Основы глубокого обучения · Полный каталог · 03.09 — Расписания скорости обучения