Остаточные связи и регуляризация
Почему глубокая сеть без остаточных связей учится хуже мелкой, как ResNet из пекинского MSRA это исправил, почему остаточный поток — «шина» трансформера и как dropout и weight decay борются с переобучением.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В прошлом уроке мы научились запускать глубокую сеть: инициализация держит сигнал на старте, нормализация — во время обучения. Казалось бы, теперь можно ставить хоть 100 слоёв. В 2015 году исследователи Microsoft Research Asia в Пекине проверили это и показали странную вещь: 56-слойная сеть с BatchNorm и правильной инициализацией училась хуже 20-слойной — даже на обучающих данных.
Решение оказалось обескураживающе простым: каждый блок должен прибавлять результат к своему входу, . Так появилась ResNetResNet (остаточная сеть)EN residual network (ResNet)Сеть из блоков x + F(x) (He, Zhang, Ren, Sun, MSRA, 2015). 152 слоя, победа на ILSVRC 2015; остаточная связь из ResNet стоит в каждом трансформере., а остаточная связь стала обязательной деталью любой глубокой сети. В каждом блоке Llama, Qwen или DeepSeek-V3 их две. Во второй половине урока разберём регуляризацию — dropout и weight decay, — без которой не обходится ни одна сеть, обучаемая на ограниченных данных.
Проблема деградации
Заголовок раздела «Проблема деградации»Возьмём мелкую сеть, которая хорошо решает задачу, и добавим к ней 36 слоёв. Хуже она стать не должна: новые слои могут просто выучить тождественное отображение и ничего не портить. Значит, ошибка глубокой сети на обучающих данных должна быть не больше, чем у мелкой.
На практике получилось иначе. Каймин Хэ и соавторы обучили на CIFAR-10 (60 000 маленьких цветных картинок 32×32 из 10 классов: самолёты, кошки, грузовики…) «обычные» (plain) сети из 20 и 56 слоёв. У 56-слойной ошибка оказалась выше и на тесте, и на обучающей выборке. Это не переобучение (оно дало бы низкую ошибку на train), а проблема деградациипроблема деградацииEN degradation problemБолее глубокая обычная сеть показывает БОЛЬШУЮ ошибку даже на обучающих данных, чем мелкая. Это трудность оптимизации, а не переобучение; её решили остаточные связи.. И не затухание градиентов: в сетях стоял BatchNorm, градиенты были здоровыми. Просто оптимизатору очень трудно найти веса, при которых стопка нелинейных слоёв ведёт себя как тождество. Нелинейный слой с весами около нуля выдаёт около нуля, а не свой вход.
Остаточное обучение
Заголовок раздела «Остаточное обучение»Идея ResNet: пусть слой учит не всё отображение , а только поправку к входу, остаток :
Теперь, чтобы блок стал тождеством, достаточно . Для этого хватит обнулить веса последнего слоя ветки, а это оптимизатору сделать легко: weight decay (о нём в конце урока) и так тянет веса к нулю. Лишние блоки перестают мешать.
flowchart LR
X["x"] --> F1["Слой"] --> R["ReLU"] --> F2["Слой"] --> S(("+"))
X -->|"тождество: x проходит как есть"| S
S --> Y["y = x + F(x)"]Есть и второе объяснение — через градиенты. Сначала на одном числе: если , то по правилу «производная суммы — сумма производных» (урок 1.2.1) . Пусть — ветка почти ничего не пропускает. Без обходной связи градиент умножился бы на 0.01, а с ней — на 1.01: сигнал проходит почти целиком. Для векторов то же самое записывают так:
Здесь — единичная матрица: единицы на диагонали, нули в остальных клетках; умножение на неё ничего не меняет, это матричный аналог числа 1. Она и есть «скоростная трасса». Даже если мала, градиент проходит через блок без затухания. Развернём сеть из блоков:
Здесь — loss (рукописная L, чтобы не путать с числом слоёв ). Первая формула: выход сети — это вход плюс сумма поправок всех блоков. Вторая: градиент loss по входу сети содержит слагаемое без единого множителя по слоям. Вспомните таблицу из прошлого урока: произведение множителей затухает, а сумма — нет. Числа: 50 множителей по 0.9 дают , а остаётся около 1 при любой глубине.
Проверим: plain против residual
Заголовок раздела «Проверим: plain против residual»Обучим на двух спиралях сети разной глубины. Две спирали — точки на плоскости, закрученные друг вокруг друга; задача
— определить, к какой спирали относится точка. Блок — Linear → LayerNorm → ReLU, то есть с нормализацией, как в ResNet
(там это BatchNorm). Единственная разница — h = f(h) или h = h + f(h):
import torchimport torch.nn as nn
torch.set_num_threads(1) # маленькая сеть: один поток быстрее и результат воспроизводим
class Net(nn.Module): """Стопка блоков Linear → LayerNorm → ReLU. residual=True превращает h = f(h) в h = h + f(h)."""
def __init__(self, depth, residual, d=32): super().__init__() self.inp = nn.Linear(2, d) self.blocks = nn.ModuleList( nn.Sequential(nn.Linear(d, d), nn.LayerNorm(d), nn.ReLU()) for _ in range(depth) ) self.out = nn.Linear(d, 2) self.residual = residual
def forward(self, x): h = self.inp(x) for f in self.blocks: h = h + f(h) if self.residual else f(h) return self.out(h)
def spirals(n=400, seed=0): """Две вложенные спирали на плоскости — классическая «трудная» игрушечная задача.""" g = torch.Generator().manual_seed(seed) t = torch.rand(n, generator=g) * 3 * torch.pi y = torch.arange(n) % 2 # метка — номер спирали r = t / (3 * torch.pi) ang = t + torch.pi * y # вторая спираль повёрнута на 180° X = torch.stack([r * torch.cos(ang), r * torch.sin(ang)], 1) + 0.02 * torch.randn(n, 2, generator=g) return X, y
X, y = spirals()
def train(depth, residual, steps=400, seed=0): torch.manual_seed(seed) net = Net(depth, residual) opt = torch.optim.Adam(net.parameters(), lr=1e-3) for _ in range(steps): loss = nn.functional.cross_entropy(net(X), y) opt.zero_grad() loss.backward() opt.step() acc = (net(X).argmax(1) == y).float().mean().item() return loss.item(), acc
for depth in (3, 12, 32): for residual in (False, True): loss, acc = train(depth, residual) kind = "residual" if residual else "plain " print(f"глубина {depth:2d} {kind}: train loss {loss:.3f}, accuracy {acc:.1%}")глубина 3 plain : train loss 0.014, accuracy 99.3%глубина 3 residual: train loss 0.014, accuracy 99.3%глубина 12 plain : train loss 0.013, accuracy 99.5%глубина 12 residual: train loss 0.012, accuracy 99.3%глубина 32 plain : train loss 0.693, accuracy 54.0%глубина 32 residual: train loss 0.013, accuracy 99.3%До 12 слоёв обе сети справляются. На 32 слоях обычная сеть застряла ровно на уровне случайного угадывания ( — loss «монетки»), хотя нормализация на месте. Остаточная сеть той же глубины решает задачу так же легко, как мелкая. Это и есть деградация в миниатюре — и её лечение.
Поменяйте seed в train и посмотрите, насколько картина устойчива. При проверке на пяти seed остаточная сеть
училась всегда, обычная на 12 слоях — тоже, а на 32 слоях застревала на в каждом запуске. Между ними лежит
пограничная зона: на 20–24 слоях обычная сеть то учится, то нет, смотря как повезёт с начальными весами. Деградация
наступает не на конкретной глубине, а постепенно: чем глубже обычная сеть, тем чаще оптимизатор не находит дорогу.
Что было дальше
Заголовок раздела «Что было дальше»- ResNet-152 (152 слоя) выиграла ImageNet-соревнование ILSVRC 2015 с ошибкой top-5 3.57% (ансамбль моделей; ошибка top-5 — доля картинок, где правильного класса нет среди пяти самых вероятных ответов модели), а также соревнования по детекции и сегментации. Статья получила премию лучшей статьи CVPR 2016. На CIFAR-10 авторы обучили сеть из более чем 1000 слоёв.
- Тождественные связи «до активации» (He и соавт., 2016) перенесли BatchNorm и ReLU внутрь ветки, перед свёрткой, и оставили обходной путь совсем чистым. Это прямой предок Pre-Norm из прошлого урока.
- Ансамбль путей. Сеть из остаточных блоков содержит путей от входа к выходу: каждый блок можно «пройти» через ветку или по обходу. (Ансамбль — несколько моделей, чьи ответы усредняют, как в случайном лесе из урока 2.3.1.) Вейт и соавторы (2016) показали, что ResNet ведёт себя как ансамбль сравнительно неглубоких сетей: если удалить один блок на тесте, качество почти не падает.
- Трансформер (2017) взял остаточную связь у ResNet: в «Attention Is All You Need» вокруг каждого подслоя стоит «residual connection», со ссылкой на работу Хэ и соавторов.
Остаточный поток: шина трансформера
Заголовок раздела «Остаточный поток: шина трансформера»В трансформере остаточная связь стоит вокруг каждого подслоя: внимания и MLP. Блок Pre-Norm выглядит так:
Удобно смотреть на вектор не как на «выход очередного слоя», а как на остаточный потокостаточный потокEN residual streamВектор шириной d, который течёт сквозь все блоки. Каждый подслой читает из него и дописывает поправку, так что выход = вход + сумма всех поправок. (residual stream) — общую шину ширины , которая тянется от эмбеддингов до LM-головы. Каждый подслой:
- читает из шины — через нормализацию;
- что-то вычисляет — внимание смотрит на другие токены, MLP достаёт «знания»;
- дописывает результат обратно — прибавляет поправку.
flowchart LR
E["Эмбеддинг"] --> P0(("+")) --> P1(("+")) --> P2(("+")) --> P3(("+")) --> N["Norm"] --> H["LM-голова"]
P0 -.->|"читает"| A1["Attn 1"] -.->|"пишет"| P1
P1 -.->|"читает"| M1["MLP 1"] -.->|"пишет"| P2
P2 -.->|"читает"| A2["Attn 2"] -.->|"пишет"| P3У Qwen3-0.6B шина имеет ширину 1024, и в неё пишут 56 «авторов»: 28 вниманий и 28 MLP. У DeepSeek-V3 ширина 7168 и 61 блок. Слои общаются только через шину: если внимание в 5-м блоке нашло полезный факт, MLP в 20-м блоке увидит его, потому что поправка осталась в потоке. Этот взгляд популяризовала команда интерпретируемости Anthropic («A Mathematical Framework for Transformer Circuits», 2021). Мы вернёмся к нему в уроке 4.5, когда соберём блок трансформера, а в уроке 9.3.1 разложим предсказание модели на вклады отдельных «авторов» шины.
Сколько «шумят» авторы: инициализация выходных проекций
Заголовок раздела «Сколько «шумят» авторы: инициализация выходных проекций»Если каждая ветка при инициализации дописывает в шину случайный вектор, дисперсия потока растёт линейно
с числом подслоёв: . В GPT-2 это учли: веса
выходных проекций каждой ветки (последний Linear внимания и MLP) инициализируют с std, уменьшенным в раз,
где — число блоков, а — число подслоёв. Тогда суммарный вклад всех веток не зависит от глубины.
В nanoGPT это одна строка: std=0.02/math.sqrt(2 * config.n_layer).
import torch
torch.manual_seed(0)d = 768
def stream_std(n_sublayers, std): """Остаточный поток: x ← x + W·rms(x), где W ~ N(0, std²). Возвращает std потока в конце.""" x = torch.randn(64, d) for _ in range(n_sublayers): h = x / x.pow(2).mean(-1, keepdim=True).sqrt() # RMSNorm без веса: вход ветки с RMS = 1 W = torch.randn(d, d) * std x = x + h @ W # ветка дописывает поправку в поток return x.std().item()
for n_blocks in (12, 48): L = 2 * n_blocks # в каждом блоке 2 подслоя: внимание и MLP plain = stream_std(L, 0.02) scaled = stream_std(L, 0.02 / L ** 0.5) print(f"блоков {n_blocks}: std потока std=0.02 → {plain:.2f} std=0.02/√(2N) → {scaled:.2f}")блоков 12: std потока std=0.02 → 2.91 std=0.02/√(2N) → 1.14блоков 48: std потока std=0.02 → 5.52 std=0.02/√(2N) → 1.14Сверим с формулой. Каждая ветка добавляет дисперсию . За 24 подслоя: , за 96: . С масштабированием сумма вкладов всегда , и std потока при любой глубине.
Шина под нагрузкой: что придумали в 2024–2026 годах
Заголовок раздела «Шина под нагрузкой: что придумали в 2024–2026 годах»У остаточного потока есть слабые места. Ширина шины фиксирована (), а писателей десятки. При Pre-Norm норма потока растёт с глубиной, и поправка каждого следующего слоя на её фоне всё мельче (прошлый урок). К тому же все слои складываются с одинаковым весом 1, и слой не может «выбрать», чьи результаты ему важнее.
Почти десять лет после ResNet остаточная связь оставалась простой суммой. С 2024 года китайские лаборатории переделывают её саму:
- ГиперсвязигиперсвязиEN hyper-connections (HC)остаточный поток расширяется до n параллельных потоков с обучаемым смешиванием (ByteDance, 2024) расширяют поток до параллельных потоков с обучаемым смешиванием между ними.
- mHCmHC (гиперсвязи с ограничением на многообразие)EN manifold-constrained hyper-connections (mHC) от DeepSeek (arXiv:2512.24880, декабрь 2025) ограничивает матрицу смешивания потоков дважды стохастическимидважды стохастическая матрицаEN doubly stochastic matrixнеотрицательная матрица, у которой сумма каждой строки и каждого столбца равна 1 матрицами (суммы строк и столбцов равны 1). Так сохраняется главное свойство остаточной связи — возможность пропустить сигнал без изменений — и обучение остаётся стабильным на большой глубине. mHC используется в DeepSeek-V4.
- Attention ResidualsAttention Residuals (внимание по глубине)EN Attention Residuals (AttnRes) от Kimi (arXiv:2603.15031, март 2026) заменяют сумму с весом 1 вниманием по глубине: каждый слой сам решает, с какими весами взять выходы всех предыдущих слоёв. Это лечит «размывание» вклада слоёв при Pre-Norm. Используется в Kimi K3.
Подробно — на ступени 9, в уроке «Куда движется архитектура LLM в 2026». Пока запомните: остаточный поток — не техническая деталь, а центральная магистраль модели, и её оптимизация — одно из главных направлений 2026 года.
Регуляризация: dropout
Заголовок раздела «Регуляризация: dropout»Вторая тема урока — борьба с переобучениемпереобучениеEN overfitting. В уроке 2.1.2 вы видели L2-регуляризацию на полиноме. У нейросетей есть свой классический приём — дропаутдропаут (dropout)EN dropoutНа обучении каждая активация обнуляется с вероятностью p, остальные умножаются на 1/(1 − p); на инференсе слой ничего не делает. В претрейне современных LLM обычно p = 0. (Хинтон и соавторы, 2012; Шривастава и соавторы, 2014).
Как работает. На каждом шаге обучения каждая активация обнуляется с вероятностью , независимо от других. Выжившие умножаются на :
Читается так: для каждой активации бросаем «монетку» , которая с вероятностью выпадает единицей и с вероятностью — нулём (распределение Бернулли из урока 1.3.1). Выпал ноль — активация обнулена, единица — активация умножена на .
Множитель нужен, чтобы среднее не изменилось: . При выжившие умножаются на . На инференсе dropout выключается, и слой просто пропускает вход.
import torchimport torch.nn as nn
torch.manual_seed(0)drop = nn.Dropout(p=0.25)x = torch.ones(2, 8)
drop.train() # режим обучения (по умолчанию после создания модели)print(drop(x)) # четверть нулей, остальные умножены на 1 / (1 − 0.25)drop.eval() # режим инференса: model.eval()print(drop(x)) # dropout выключен — вход проходит как есть
drop.train()big = drop(torch.ones(1_000_000))print(f"доля нулей {(big == 0).float().mean():.3f}, среднее {big.mean():.3f}")tensor([[0.0000, 1.3333, 1.3333, 0.0000, 1.3333, 0.0000, 1.3333, 1.3333], [1.3333, 1.3333, 1.3333, 1.3333, 1.3333, 1.3333, 1.3333, 1.3333]])tensor([[1., 1., 1., 1., 1., 1., 1., 1.], [1., 1., 1., 1., 1., 1., 1., 1.]])доля нулей 0.250, среднее 1.000На маленьком тензоре нулей не обязательно ровно четверть (в первой строке 3 из 8, во второй ни одного), но на миллионе элементов доля и среднее точно такие, как обещает формула.
Почему это помогает. Две интуиции:
- Против «сговора» нейронов. Нейрон не может полагаться на конкретного соседа: того в любой момент выключат. Приходится учить признаки, полезные сами по себе.
- Ансамбль. Каждая маска — своя «прореженная» подсеть, и все они делят веса. Обучение с dropout — это обучение огромного ансамбля подсетей, а инференс без dropout — приближённое усреднение их ответов.
Dropout в трансформерах и LLM
Заголовок раздела «Dropout в трансформерах и LLM»В трансформере 2017 года dropout с стоит после эмбеддингов и на выходе каждой ветки перед сложением
с потоком. В первой GPT (2018) к ним добавлен dropout на весах внимания, тоже с ; все три dropout по 0.1
стоят и в конфигурации GPT-2 в библиотеке transformers. А в претрейне современных LLM dropout почти всегда выключают:
в конфигурациях Llama, Qwen3, DeepSeek-V3 и MiniMind attention_dropout = 0.0 или dropout = 0.0. В nanoGPT прямо
написано: «for pretraining 0 is good, for finetuning try 0.1+».
Причина — в режиме обучения. LLM видит большую часть токенов корпуса один-два раза. Она недообучена, а не
переобучена: ошибка на новых текстах почти равна ошибке на обучающих. Dropout в таком режиме только выбрасывает
полезный сигнал и замедляет обучение. Зато при дообучении на маленьком наборе данных переобучение возвращается,
и dropout снова полезен: например, у LoRA-адаптеров (ступень 6) есть свой параметр lora_dropout.
Регуляризация: weight decay
Заголовок раздела «Регуляризация: weight decay»затухание весовзатухание весов (weight decay)EN weight decayНа каждом шаге веса умножаются на (1 − ηλ) — «притягиваются» к нулю. Для обычного SGD равносильно L2-регуляризации, для Adam — нет. вы знаете из урока 1.4: на каждом шаге AdamW умножает веса на и тем самым тянет их к нулю. Добавим три практических замечания для глубоких сетей.
1. Это «забывание» с понятной скоростью. Вес, который градиент не поддерживает, на каждом шаге умножается на , а за шагов — на . При маленьком это почти то же, что (экспонента из урока 1.0.2). Числа: при и множитель за шаг равен , и за 10 000 шагов — вес уменьшится в раза. Сохраняются только веса, которые постоянно нужны для уменьшения loss.
2. Не все параметры стоит тянуть к нулю. Смещения и веса нормализаций ( в LayerNorm, в RMSNorm) — это сдвиги и масштабы, а не «знания». Если тянуть к нулю, вход каждого подслоя будет искусственно сжиматься. Поэтому параметры делят на две группы. В nanoGPT правило простое: weight decay применяют к тензорам с двумя и более измерениями (матрицы весов и эмбеддинги) и не применяют к одномерным (смещения и веса норм). Код с группами параметров — в уроке 1.4.2.
3. Значения в LLM. Много лет стандартом был AdamW с : GPT-3, Llama, DeepSeek-V2 и DeepSeek-V3. Новые китайские модели переходят на оптимизатор MuonMuon (оптимизатор)EN Muon optimizerОптимизатор для матриц весов (Келлер Джордан, 2024) — momentum, затем ортогонализация обновления итерациями Ньютона–Шульца. Moonshot масштабировала его до Kimi K2. (его анонс был в уроке 1.4.2): так обучены Kimi K2 и DeepSeek-V4. Но weight decay сохраняют и в нём — в техотчёте DeepSeek-V4 это те же 0.1.
Dropout и weight decay в деле
Заголовок раздела «Dropout и weight decay в деле»Двадцать зашумлённых точек кривой и MLP с 66 тысячами параметров — идеальный рецепт переобучения. Сравним
четыре варианта. Ошибку на валидации меряем относительно чистой кривой без шума. Запись run(**kw) передаёт
словарь как именованные аргументы (урок 0.2.2): run(**{"p_drop": 0.3}) — то же, что run(p_drop=0.3):
import torchimport torch.nn as nn
torch.set_num_threads(1)g = torch.Generator().manual_seed(0)f = lambda x: torch.sin(3 * x)X_tr = torch.rand(20, 1, generator=g) * 2 - 1y_tr = f(X_tr) + 0.3 * torch.randn(20, 1, generator=g) # 20 шумных точекX_va = torch.linspace(-1, 1, 200).unsqueeze(1)y_va = f(X_va) # честная кривая без шума
def run(p_drop=0.0, wd=0.0, steps=3000, seed=0): torch.manual_seed(seed) net = nn.Sequential(nn.Linear(1, 256), nn.ReLU(), nn.Dropout(p_drop), nn.Linear(256, 256), nn.ReLU(), nn.Dropout(p_drop), nn.Linear(256, 1)) opt = torch.optim.AdamW(net.parameters(), lr=3e-3, weight_decay=wd) for _ in range(steps): net.train() loss = ((net(X_tr) - y_tr) ** 2).mean() opt.zero_grad(); loss.backward(); opt.step() net.eval() # dropout выключается with torch.no_grad(): tr = ((net(X_tr) - y_tr) ** 2).mean().item() va = ((net(X_va) - y_va) ** 2).mean().item() return tr, va
for name, kw in [("без регуляризации", {}), ("dropout 0.3", {"p_drop": 0.3}), ("weight decay 1.0", {"wd": 1.0}), ("dropout + wd", {"p_drop": 0.3, "wd": 1.0})]: tr, va = run(**kw) print(f"{name:18s} train MSE {tr:.3f} val MSE {va:.3f}")без регуляризации train MSE 0.001 val MSE 0.173dropout 0.3 train MSE 0.033 val MSE 0.045weight decay 1.0 train MSE 0.049 val MSE 0.040dropout + wd train MSE 0.059 val MSE 0.053Без регуляризации сеть выучила шум: на обучающих точках ошибка 0.001, хотя дисперсия самого шума . Ошибка на чистой кривой при этом 0.173. Dropout и weight decay поднимают ошибку на train ближе к уровню шума, а ошибку на валидации в этом запуске снижают в 4 раза. Вместе они здесь немного перестараются: регуляризацию тоже можно передозировать.
Запустите с другими seed — это поучительно. Без регуляризации val MSE скачет от запуска к запуску (у нас выходило
от 0.07 до 0.17): переобученная сеть каждый раз выучивает шум по-своему. С dropout или weight decay она стабильно
держится около 0.04–0.06. Это тот самый высокий разброс (variance) из урока 2.1.2, и регуляризация его снижает.
взято намеренно большим для крошечной задачи, в LLM это 0.1.
Проверь себя
Заголовок раздела «Проверь себя»- Деградация: обычная глубокая сеть учится хуже мелкой даже на train — это трудность оптимизации. ResNet (MSRA, 2015) решил её остаточной связью : слой учит только поправку, а градиент идёт по «трассе» .
- Остаточный поток — шина трансформера: , подслои читают через норму и дописывают поправки. GPT-2 уменьшает инициализацию выходных проекций в раз, чтобы поток не разрастался.
- В 2026 году шину переделывают: mHC (DeepSeek-V4) и Attention Residuals (Kimi K3) — подробно на ступени 9.
- Dropout обнуляет активации с вероятностью и умножает остальные на ; на инференсе выключен. В претрейне LLM , при дообучении на малых данных — снова полезен.
- Weight decay ( в AdamW) тянет веса к нулю; смещения и веса нормализаций исключают.
Дальше: две архитектуры, с которых началось глубокое обучение, — свёрточные сети для изображений и рекуррентные для последовательностей. Разберёмся, почему RNN проиграли трансформеру и почему их идеи возвращаются в 2026 году.