Фаза 03 · урок 08

Инициализация весов и стабильность обучения

Цель урока: Инициализируйте все веса нулями. Ничто не учится. Каждый нейрон вычисляет одну и ту же функцию, получает один и тот же градиент и обновляется одинаково. После 10 000 эпох ваш скрытый слой из 512 нейронов — всё ещё 512 копий одного…

Текущий релиз AlexBred.com: первые 100 уроков русскоязычной программы.

Курс
AI Engineering from Scratch
Фаза
Основы глубокого обучения
Чтение
13 мин.
Проверено
Содержание урока
  1. Цели обучения
  2. Проблема
  3. Концепция
  4. Проблема симметрии
  5. Распространение дисперсии по слоям
  6. Инициализация Xavier/Glorot
  7. Инициализация Kaiming/He
  8. Инициализация трансформеров
  9. Величина активаций на протяжении 50 слоёв
  10. Выбор правильной инициализации
  11. Соберите это
  12. Шаг 1: стратегии инициализации
  13. Шаг 2: функции активации
  14. Шаг 3: прямой проход через 50 слоёв
  15. Шаг 4: эксперимент
  16. Шаг 5: демонстрация симметрии
  17. Шаг 6: послойный отчёт о величинах
  18. Используйте это
  19. Выпустите это
  20. Упражнения
  21. Ключевые термины
  22. Дополнительное чтение

Неправильно инициализируете — и обучение никогда не начнётся. Правильно — и 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 — промпт, который диагностирует проблемы инициализации весов и рекомендует правильную стратегию

Упражнения

  1. Добавьте инициализацию LeCun (Var = 1/fan_in, разработана для активации SELU). Запустите эксперимент с 50 слоями с LeCun init + tanh и сравните с Xavier + tanh.

  2. Реализуйте масштабирование остаточных связей GPT-2: умножайте выход каждого слоя на 1/sqrt(2*N) перед добавлением в остаточный поток. Запустите 50 слоёв с масштабированием и без него, измерьте, насколько быстро растёт величина остатка.

  3. Создайте функцию «проверки здоровья инициализации», которая принимает размерности слоёв сети и тип активации, затем рекомендует правильную инициализацию и предупреждает, если текущая инициализация вызовет проблемы.

  4. Запустите эксперимент с fan_in = 16 и fan_in = 1024. Xavier и Kaiming адаптируются к fan_in, а случайная инициализация — нет. Покажите, как разрыв между «работает» и «ломается» расширяется при больших слоях.

  5. Реализуйте ортогональную инициализацию (создайте случайную матрицу, вычислите её 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 — Расписания скорости обучения