Перейти к содержимому
Ступень 33.4 Как обучать глубокие сети

Остаточные связи и регуляризация

Почему глубокая сеть без остаточных связей учится хуже мелкой, как ResNet из пекинского MSRA это исправил, почему остаточный поток — «шина» трансформера и как dropout и weight decay борются с переобучением.

УРОКstage-3/3-4-deep-training/02-residual-dropout.mdx1 ч 40 минУрок 2 из 3
После урока вы сможете
  • Объяснить проблему деградации и показать на эксперименте, как остаточная связь x+F(x)x + F(x) её снимает
  • Описать остаточный поток трансформера как общую «шину» и объяснить, зачем GPT-2 уменьшает инициализацию выходных проекций в 2N\sqrt{2N} раз
  • Реализовать и объяснить dropout (режимы train и eval, множитель 1/(1−p)1/(1-p)) и понимать, почему в претрейне LLM он обычно выключен
  • Настроить weight decay и объяснить, к каким параметрам его не применяют

В прошлом уроке мы научились запускать глубокую сеть: инициализация держит сигнал на старте, нормализация — во время обучения. Казалось бы, теперь можно ставить хоть 100 слоёв. В 2015 году исследователи Microsoft Research Asia в Пекине проверили это и показали странную вещь: 56-слойная сеть с BatchNorm и правильной инициализацией училась хуже 20-слойной — даже на обучающих данных.

Решение оказалось обескураживающе простым: каждый блок должен прибавлять результат к своему входу, x+F(x)x + F(x). Так появилась 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 слоёв. Хуже она стать не должна: новые слои могут просто выучить тождественное отображение f(x)=xf(x) = x и ничего не портить. Значит, ошибка глубокой сети на обучающих данных должна быть не больше, чем у мелкой.

На практике получилось иначе. Каймин Хэ и соавторы обучили на CIFAR-10 (60 000 маленьких цветных картинок 32×32 из 10 классов: самолёты, кошки, грузовики…) «обычные» (plain) сети из 20 и 56 слоёв. У 56-слойной ошибка оказалась выше и на тесте, и на обучающей выборке. Это не переобучение (оно дало бы низкую ошибку на train), а проблема деградациипроблема деградацииEN degradation problemБолее глубокая обычная сеть показывает БОЛЬШУЮ ошибку даже на обучающих данных, чем мелкая. Это трудность оптимизации, а не переобучение; её решили остаточные связи.. И не затухание градиентов: в сетях стоял BatchNorm, градиенты были здоровыми. Просто оптимизатору очень трудно найти веса, при которых стопка нелинейных слоёв ведёт себя как тождество. Нелинейный слой с весами около нуля выдаёт около нуля, а не свой вход.

Идея ResNet: пусть слой учит не всё отображение H(x)H(x), а только поправку к входу, остаток F(x)=H(x)−xF(x) = H(x) - x:

y=x+F(x).y = x + F(x).

Теперь, чтобы блок стал тождеством, достаточно F(x)=0F(x) = 0. Для этого хватит обнулить веса последнего слоя ветки, а это оптимизатору сделать легко: weight decay (о нём в конце урока) и так тянет веса к нулю. Лишние блоки перестают мешать.

flowchart LR
X["x"] --> F1["Слой"] --> R["ReLU"] --> F2["Слой"] --> S(("+"))
X -->|"тождество: x проходит как есть"| S
S --> Y["y = x + F(x)"]
Остаточный блок: ветка F вычисляет поправку, «обходная» связь несёт вход без изменений

Есть и второе объяснение — через градиенты. Сначала на одном числе: если y=x+F(x)y = x + F(x), то по правилу «производная суммы — сумма производных» (урок 1.2.1) y′=1+F′(x)y' = 1 + F'(x). Пусть F′(x)=0.01F'(x) = 0.01 — ветка почти ничего не пропускает. Без обходной связи градиент умножился бы на 0.01, а с ней — на 1.01: сигнал проходит почти целиком. Для векторов то же самое записывают так:

∂y∂x=I+∂F∂x.\frac{\partial y}{\partial x} = I + \frac{\partial F}{\partial x}.

Здесь II — единичная матрица: единицы на диагонали, нули в остальных клетках; умножение на неё ничего не меняет, это матричный аналог числа 1. Она и есть «скоростная трасса». Даже если ∂F/∂x\partial F / \partial x мала, градиент проходит через блок без затухания. Развернём сеть из LL блоков:

xL=x0+∑l=0L−1Fl(xl),∂L∂x0=∂L∂xL(I+∂∂x0∑lFl).x_L = x_0 + \sum_{l=0}^{L-1} F_l(x_l), \qquad \frac{\partial \mathcal{L}}{\partial x_0} = \frac{\partial \mathcal{L}}{\partial x_L}\left(I + \frac{\partial}{\partial x_0} \sum_l F_l\right).

Здесь L\mathcal{L} — loss (рукописная L, чтобы не путать с числом слоёв LL). Первая формула: выход сети — это вход плюс сумма поправок всех блоков. Вторая: градиент loss по входу сети содержит слагаемое ∂L/∂xL\partial \mathcal{L} / \partial x_L без единого множителя по слоям. Вспомните таблицу из прошлого урока: произведение множителей затухает, а сумма — нет. Числа: 50 множителей по 0.9 дают 0.950≈0.0050.9^{50} \approx 0.005, а 1+(что-то маленькое)1 + (\text{что-то маленькое}) остаётся около 1 при любой глубине.

Обучим на двух спиралях сети разной глубины. Две спирали — точки на плоскости, закрученные друг вокруг друга; задача — определить, к какой спирали относится точка. Блок — Linear → LayerNorm → ReLU, то есть с нормализацией, как в ResNet (там это BatchNorm). Единственная разница — h = f(h) или h = h + f(h):

import torch
import 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 слоях обычная сеть застряла ровно на уровне случайного угадывания (ln⁡2≈0.693\ln 2 \approx 0.693 — loss «монетки»), хотя нормализация на месте. Остаточная сеть той же глубины решает задачу так же легко, как мелкая. Это и есть деградация в миниатюре — и её лечение.

Поменяйте seed в train и посмотрите, насколько картина устойчива. При проверке на пяти seed остаточная сеть училась всегда, обычная на 12 слоях — тоже, а на 32 слоях застревала на ln⁡2\ln 2 в каждом запуске. Между ними лежит пограничная зона: на 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 из прошлого урока.
  • Ансамбль путей. Сеть из nn остаточных блоков содержит 2n2^n путей от входа к выходу: каждый блок можно «пройти» через ветку или по обходу. (Ансамбль — несколько моделей, чьи ответы усредняют, как в случайном лесе из урока 2.3.1.) Вейт и соавторы (2016) показали, что ResNet ведёт себя как ансамбль сравнительно неглубоких сетей: если удалить один блок на тесте, качество почти не падает.
  • Трансформер (2017) взял остаточную связь у ResNet: в «Attention Is All You Need» вокруг каждого подслоя стоит «residual connection», со ссылкой на работу Хэ и соавторов.

В трансформере остаточная связь стоит вокруг каждого подслоя: внимания и MLP. Блок Pre-Norm выглядит так:

h=x+Attn(Norm(x)),x′=h+MLP(Norm(h)).h = x + \mathrm{Attn}(\mathrm{Norm}(x)), \qquad x' = h + \mathrm{MLP}(\mathrm{Norm}(h)).

Удобно смотреть на вектор xx не как на «выход очередного слоя», а как на остаточный потокостаточный потокEN residual streamВектор шириной d, который течёт сквозь все блоки. Каждый подслой читает из него и дописывает поправку, так что выход = вход + сумма всех поправок. (residual stream) — общую шину ширины dd, которая тянется от эмбеддингов до LM-головы. Каждый подслой:

  1. читает из шины — через нормализацию;
  2. что-то вычисляет — внимание смотрит на другие токены, MLP достаёт «знания»;
  3. дописывает результат обратно — прибавляет поправку.
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 разложим предсказание модели на вклады отдельных «авторов» шины.

Сколько «шумят» авторы: инициализация выходных проекций

Заголовок раздела «Сколько «шумят» авторы: инициализация выходных проекций»

Если каждая ветка при инициализации дописывает в шину случайный вектор, дисперсия потока растёт линейно с числом подслоёв: Var(xL)≈Var(x0)+L⋅σF2\mathrm{Var}(x_L) \approx \mathrm{Var}(x_0) + L \cdot \sigma_F^2. В GPT-2 это учли: веса выходных проекций каждой ветки (последний Linear внимания и MLP) инициализируют с std, уменьшенным в 2N\sqrt{2N} раз, где NN — число блоков, а 2N2N — число подслоёв. Тогда суммарный вклад всех веток не зависит от глубины. В 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

Сверим с формулой. Каждая ветка добавляет дисперсию d⋅σ2=768⋅0.022≈0.31d \cdot \sigma^2 = 768 \cdot 0.02^2 \approx 0.31. За 24 подслоя: 1+24⋅0.31≈2.9\sqrt{1 + 24 \cdot 0.31} \approx 2.9, за 96: 1+96⋅0.31≈5.5\sqrt{1 + 96 \cdot 0.31} \approx 5.5. С масштабированием сумма вкладов всегда ≈0.31\approx 0.31, и std потока ≈1.31≈1.14\approx \sqrt{1.31} \approx 1.14 при любой глубине.

Шина под нагрузкой: что придумали в 2024–2026 годах

Заголовок раздела «Шина под нагрузкой: что придумали в 2024–2026 годах»

У остаточного потока есть слабые места. Ширина шины фиксирована (dd), а писателей десятки. При Pre-Norm норма потока растёт с глубиной, и поправка каждого следующего слоя на её фоне всё мельче (прошлый урок). К тому же все слои складываются с одинаковым весом 1, и слой не может «выбрать», чьи результаты ему важнее.

Почти десять лет после ResNet остаточная связь оставалась простой суммой. С 2024 года китайские лаборатории переделывают её саму:

  • ГиперсвязигиперсвязиEN hyper-connections (HC)остаточный поток расширяется до n параллельных потоков с обучаемым смешиванием (ByteDance, 2024) расширяют поток до nn параллельных потоков с обучаемым смешиванием между ними.
  • 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 года.

Вторая тема урока — борьба с переобучениемпереобучениеEN overfitting. В уроке 2.1.2 вы видели L2-регуляризацию на полиноме. У нейросетей есть свой классический приём — дропаутдропаут (dropout)EN dropoutНа обучении каждая активация обнуляется с вероятностью p, остальные умножаются на 1/(1 − p); на инференсе слой ничего не делает. В претрейне современных LLM обычно p = 0. (Хинтон и соавторы, 2012; Шривастава и соавторы, 2014).

Как работает. На каждом шаге обучения каждая активация обнуляется с вероятностью pp, независимо от других. Выжившие умножаются на 1/(1−p)1/(1-p):

h~i=mi1−p hi,mi∼Bernoulli(1−p).\tilde{h}_i = \frac{m_i}{1-p}\, h_i, \qquad m_i \sim \mathrm{Bernoulli}(1-p).

Читается так: для каждой активации hih_i бросаем «монетку» mim_i, которая с вероятностью 1−p1 - p выпадает единицей и с вероятностью pp — нулём (распределение Бернулли из урока 1.3.1). Выпал ноль — активация обнулена, единица — активация умножена на 1/(1−p)1/(1-p).

Множитель нужен, чтобы среднее не изменилось: E[h~i]=(1−p)⋅hi1−p=hi\mathbb{E}[\tilde h_i] = (1-p) \cdot \frac{h_i}{1-p} = h_i. При p=0.25p = 0.25 выжившие умножаются на 1/0.75≈1.331/0.75 \approx 1.33. На инференсе dropout выключается, и слой просто пропускает вход.

import torch
import 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 — приближённое усреднение их ответов.

В трансформере 2017 года dropout с p=0.1p = 0.1 стоит после эмбеддингов и на выходе каждой ветки перед сложением с потоком. В первой GPT (2018) к ним добавлен dropout на весах внимания, тоже с p=0.1p = 0.1; все три 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)EN weight decayНа каждом шаге веса умножаются на (1 − ηλ) — «притягиваются» к нулю. Для обычного SGD равносильно L2-регуляризации, для Adam — нет. вы знаете из урока 1.4: на каждом шаге AdamW умножает веса на (1−ηλ)(1 - \eta\lambda) и тем самым тянет их к нулю. Добавим три практических замечания для глубоких сетей.

1. Это «забывание» с понятной скоростью. Вес, который градиент не поддерживает, на каждом шаге умножается на (1−ηλ)(1 - \eta\lambda), а за kk шагов — на (1−ηλ)k(1 - \eta\lambda)^k. При маленьком ηλ\eta\lambda это почти то же, что e−kηλe^{-k\eta\lambda} (экспонента из урока 1.0.2). Числа: при η=10−3\eta = 10^{-3} и λ=0.1\lambda = 0.1 множитель за шаг равен 0.99990.9999, и за 10 000 шагов 0.999910 000≈0.368=1/e0.9999^{10\,000} \approx 0.368 = 1/e — вес уменьшится в e≈2.7e \approx 2.7 раза. Сохраняются только веса, которые постоянно нужны для уменьшения loss.

2. Не все параметры стоит тянуть к нулю. Смещения и веса нормализаций (γ\gamma в LayerNorm, gg в RMSNorm) — это сдвиги и масштабы, а не «знания». Если тянуть gg к нулю, вход каждого подслоя будет искусственно сжиматься. Поэтому параметры делят на две группы. В nanoGPT правило простое: weight decay применяют к тензорам с двумя и более измерениями (матрицы весов и эмбеддинги) и не применяют к одномерным (смещения и веса норм). Код с группами параметров — в уроке 1.4.2.

3. Значения в LLM. Много лет стандартом был AdamW с λ=0.1\lambda = 0.1: 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.

Двадцать зашумлённых точек кривой sin⁡3x\sin 3x и MLP с 66 тысячами параметров — идеальный рецепт переобучения. Сравним четыре варианта. Ошибку на валидации меряем относительно чистой кривой без шума. Запись run(**kw) передаёт словарь как именованные аргументы (урок 0.2.2): run(**{"p_drop": 0.3}) — то же, что run(p_drop=0.3):

import torch
import 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 - 1
y_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.173
dropout 0.3 train MSE 0.033 val MSE 0.045
weight decay 1.0 train MSE 0.049 val MSE 0.040
dropout + wd train MSE 0.059 val MSE 0.053

Без регуляризации сеть выучила шум: на обучающих точках ошибка 0.001, хотя дисперсия самого шума 0.32=0.090.3^2 = 0.09. Ошибка на чистой кривой при этом 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, и регуляризация его снижает. λ=1.0\lambda = 1.0 взято намеренно большим для крошечной задачи, в LLM это 0.1.

Проверь себя6 вопросов
1 56-слойная обычная сеть на CIFAR-10 даёт большую ошибку на обучающих данных, чем 20-слойная. Как это называется?
2 Какие утверждения об остаточной связи y=x+F(x)y = x + F(x) верны?Можно выбрать несколько ответов
3 Dropout с p=0.2p = 0.2. На какое число умножаются выжившие активации при обучении? (до двух знаков)
4 Вы оцениваете модель с dropout и получаете разные метрики при каждом запуске на одних и тех же данных. Вероятная причина?
5 Зачем GPT-2 инициализирует выходные проекции веток с std, уменьшенным в 2N\sqrt{2N} раз (NN — число блоков)?
6 AdamW с η=5⋅10−4\eta = 5 \cdot 10^{-4} и λ=0.1\lambda = 0.1. Примерно за сколько шагов вес, который градиент не поддерживает, уменьшится в ee раз?
  • Деградация: обычная глубокая сеть учится хуже мелкой даже на train — это трудность оптимизации. ResNet (MSRA, 2015) решил её остаточной связью y=x+F(x)y = x + F(x): слой учит только поправку, а градиент идёт по «трассе» II.
  • Остаточный поток — шина трансформера: xL=x0+∑Flx_L = x_0 + \sum F_l, подслои читают через норму и дописывают поправки. GPT-2 уменьшает инициализацию выходных проекций в 2N\sqrt{2N} раз, чтобы поток не разрастался.
  • В 2026 году шину переделывают: mHC (DeepSeek-V4) и Attention Residuals (Kimi K3) — подробно на ступени 9.
  • Dropout обнуляет активации с вероятностью pp и умножает остальные на 1/(1−p)1/(1-p); на инференсе выключен. В претрейне LLM p=0p = 0, при дообучении на малых данных — снова полезен.
  • Weight decay (λ=0.1\lambda = 0.1 в AdamW) тянет веса к нулю; смещения и веса нормализаций исключают.

Дальше: две архитектуры, с которых началось глубокое обучение, — свёрточные сети для изображений и рекуррентные для последовательностей. Разберёмся, почему RNN проиграли трансформеру и почему их идеи возвращаются в 2026 году.