Остаточные связи: почему глубокие трансформеры вообще удаётся обучить
Почему простое наращивание слоёв ломает обучение, как skip-связи x + F(x) чинят поток градиентов, как их используют блоки с Pre-LayerNorm, и короткая реализация на PyTorch.
Если наивно сложить десятки слоёв друг на друга, обучение становится нестабильным. Остаточные связи, их ещё называют shortcut-связями, решают эту проблему: и информация, и градиенты получают возможность проходить сквозь сеть напрямую.
Почему простое наслоение не работает
К этому моменту блок трансформера состоит из трёх частей: многоголового самовнимания, нормализации слоя и feed-forward сети. Возникает соблазн просто повторять их одну за другой:
Input
│
▼
Attention
│
▼
Feed Forward
│
▼
Attention
│
▼
Feed Forward
│
▼
...
Обучается такое плохо. Чем глубже сеть, тем сложнее её оптимизировать, и до появления остаточных связей в ResNet в 2015 году это было одним из главных препятствий в глубоком обучении. От них зависят почти все современные трансформеры, включая GPT, BERT, LLaMA, Claude и Gemini.
Начнём с сети из двух слоёв:
Input ↓Layer 1 ↓Layer 2 ↓Output
Обучить её обычно несложно. А теперь представьте 96 слоёв:
Input↓Layer 1↓Layer 2↓...↓Layer 96↓Output
Каждый слой преобразует выход предыдущего. При обратном распространении градиент должен пройти назад через каждый слой, и по дороге он может сжаться почти до нуля (затухание), неограниченно вырасти (взрыв) или становиться всё шумнее. В итоге самые первые слои почти ничему не учатся.
Испорченный телефон
Представьте сообщение, которое шёпотом передают по цепочке из пятидесяти человек:
Person 1↓Person 2↓Person 3↓...↓Person 50
К концу оно сильно меняется: что-то пропало, что-то исказилось. Глубокая сеть ведёт себя похоже: каждый слой немного сдвигает представление, и после достаточного числа слоёв полезный сигнал может исчезнуть.
Добавим короткий путь
А что, если данным не обязательно проходить через каждый слой и они могут ещё и обходить их?
Input
│
├──────────────┐
▼ │
Layer │
▼ │
Output │
│ │
└────── Add ◄──┘
│
▼
Final Output
Теперь у исходного входа всегда есть прямой путь к последующим слоям, и это небольшое изменение сильно меняет поведение глубоких сетей.
Математика
Допустим, слой вычисляет вот это:
F(x)
Без остаточной связи выход — это просто результат слоя:
Output = F(x)
С ней выход становится таким:
Output = x + F(x)
Вход прибавляется прямо к преобразованному выходу. Эта операция называется skip-связью, или shortcut-связью.
Почему это помогает
Предположим, конкретному слою нечего добавить. В обычной сети ему пришлось бы как-то научиться работать тождественной функцией, а это, как выясняется, трудно. С остаточной связью тождество достаётся бесплатно. Слою достаточно выучить:
F(x) = 0
чтобы:
Output = x + 0 = x
Вход проходит без изменений, и оптимизация становится гораздо проще.
Учимся на разнице
Отсюда и название. Слою не нужно выучивать всё преобразование целиком. Он учит только разницу, тот самый остаток, между входом и целевым выходом. Хорошее сравнение — редактирование фото: картинку редко перерисовывают полностью, её корректируют, добавляя яркость, контраст или резкость. Слой трансформера тоже редко должен заменять всё представление. Ему достаточно немного его улучшить.
Два коротких пути на блок
В каждом блоке трансформера две остаточные связи. Первая охватывает слой внимания:
Input
│
├──────────────┐
▼ │
Multi-Head Attention
▼ │
Add ◄───────────┘
│
▼
Output
Вторая охватывает feed-forward сеть:
Input
│
├──────────────┐
▼ │
Feed Forward Network
▼ │
Add ◄───────────┘
│
▼
Output
Pre-LayerNorm
Современные модели GPT используют схему Pre-LayerNorm, которая вычисляется так:
Input
│
▼
LayerNorm
│
▼
Multi-Head Attention
│
▼
Add Residual
│
▼
LayerNorm
│
▼
Feed Forward Network
│
▼
Add Residual
│
▼
Output
LayerNorm здесь применяется до каждого подслоя, а не после, и такая компоновка заметно стабилизирует обучение глубоких трансформеров.
Поток градиентов
При обратном распространении у градиента теперь два пути:
Gradient↓Transformer Layer↓Residual Path
Если основное преобразование оказывается трудным для оптимизации, короткий путь всё равно даёт другую дорогу назад, так что градиент не может исчезнуть полностью. На языке матанализа производная от
x + F(x)
равна
1 + F'(x)
Эта константа 1 очень важна. Даже если
F'(x)
становится очень маленьким, у градиента остаётся прямой путь через тождественную связь. Это одна из главных причин, по которым остаточные сети успешно обучаются.
Числовой пример
Пусть входной эмбеддинг такой:
[0.5, 1.2, -0.3]
а внимание выдаёт:
[0.1, -0.2, 0.4]
Вместо того чтобы заменить вход, их складывают:
[0.5, 1.2, -0.3] + [0.1, -0.2, 0.4] = [0.6, 1.0, 0.1]
Исходная информация сохраняется, а новая к ней подмешивается.
На PyTorch
В коде это почти ничего не стоит:
x = x + self.attention(
self.layer_norm1(x)
)
x = x + self.ffn(
self.layer_norm2(x)
)
Это сложение — одна из самых значимых операций во всей архитектуре.
Один полный блок
Input
│
▼
LayerNorm
│
▼
Multi-Head Attention
│
▼
Add Residual
│
▼
LayerNorm
│
▼
Feed-Forward Network
│
▼
Add Residual
│
▼
Output
Современные LLM повторяют эту структуру десятки и даже сотни раз.
Что меняется с ними и без них
Без остаточных связей глубокие сети трудно оптимизировать, градиенты легче затухают, информация постепенно утекает, а дополнительные слои часто ухудшают результат. С ними информация проходит сквозь сеть напрямую, градиенты остаются стабильными, более глубокие модели становятся обучаемыми, а оптимизация сильно упрощается. Остаточное обучение — один из прорывов, благодаря которым стали возможны сегодняшние большие языковые модели.