Трансформер не равен attention. Блок чередует смешивание позиций и
нелинейное преобразование каждой позиции; residual оставляет прямой путь,
normalization управляет масштабом. Глубина возникает из многократного повторения
этой простой грамматики — и почти всё, что кажется в трансформере магией, при
разборке оказывается свойством маршрута, а не свойством одной формулы.
Два вида обмена в одном тензоре
Пусть X∈Rn×d — представления n токенов, по строке на
позицию. С этим прямоугольником можно сделать ровно две принципиально разные
вещи: перемешать строки или перемешать столбцы. Self-attention занимается
первым: позиция получает взвешенную смесь значений других позиций. Feed-forward
network, или MLP, занимается вторым, обрабатывая каждую строку независимо и
одинаково:
MLP(x)=W2ϕ(W1x+b1)+b2,x∈Rd.
Attention смешивает по длине, MLP смешивает по каналам. Ни одно из
преобразований не заменяет другое. Внимание, если смотреть на него при
замороженной карте весов, есть линейная комбинация values: никакой новой
нелинейной функции признаков оно не производит. MLP умеет строить нелинейные
комбинации, но для него пятый токен и первый живут в разных вселенных.
В pre-norm записи блок выглядит так:
X′Y=X+MHA(LN(X)),=X′+MLP(LN(X′)).
Устройство Q, K, V разобрано в уроке про внимание; нам
достаточно помнить формулу одной головы
Рис. 77.1. Грамматика pre-norm блока: две ветви и один сквозной путь
Золотая вертикаль — residual stream: путь, по которому представление проходит
блок без единого умножения на обучаемую матрицу. Каждая ветвь сначала
нормируется, затем преобразуется подслоем и возвращается в поток сложением.
Подписаны точные размеры при d=512: MHA — 1 050 624 параметра, MLP —
2 099 712, обе LayerNorm вместе — 2048 чисел.
Бюджет блока: где на самом деле лежат параметры
Внимание получает почти все картинки в популярных статьях, но не большинство
весов. Посчитаем честно для d=512, восьми голов и MLP с расширением 4.
Проекции WQ,WK,WV,WO — четыре матрицы 512×512 плюс четыре вектора
смещений:
PMHA=4d2+4d=1050624.
MLP состоит из W1∈Rd×4d, W2∈R4d×d и
двух смещений:
PMLP=8d2+5d=2099712.
Два слоя LayerNorm добавляют по паре обучаемых векторов длины d:
PLN=2⋅2d=2048.
Итого в блоке 3152384 параметра, из которых 66,6% приходится на MLP, 33,3%
на внимание и 0,065% на нормировку. Отношение PMLP/PMHA
при d=512 равно 1,999 и стремится к двум при росте d: слагаемые с d
исчезающе малы рядом с d2.
Слева: при любом d отношение держится около ×2,00 — это чистая
алгебра, 8d2 против 4d2. Справа: доля блока, лежащая в MLP, как функция
расширения. При expansion 2 — 49,9%, при стандартном 4 — 66,6%, при 8 — 80,0%.
Разговор «сколько параметров во внимании» бессмыслен без указания ширины MLP.
Residual: право ничего не менять
Каждый подслой обучает не новое представление, а поправку к старому:
y=x+F(x).
Разница с записью y=G(x) кажется косметической, но она меняет то, что модели
даётся легко. Чтобы ничего не испортить, достаточно F≈0 — нулевая
матрица всегда в пределах досягаемости оптимизатора. Чтобы ничего не испортить в
записи y=G(x), нужно попасть в тождественное отображение, а единичная матрица
среди всех матриц ничем не выделена.
Для градиента разница ещё нагляднее:
∂x∂y=I+∂x∂F,
и через стек из L подслоёв
∂x(0)∂y(L)=ℓ=1∏L(I+Jℓ),Jℓ=∂x∂Fℓ.
Единичное слагаемое не зависит ни от одного веса: даже если все Jℓ
выродились, градиент дойдёт до первого слоя. Это не гарантия устойчивой
оптимизации — произведение ∏(I+Jℓ) прекрасно умеет расходиться, — но
короткая дорога в архитектуре есть всегда. Механику самого произведения мы
разбирали в уроке про обратное распространение, а похожую идею
аддитивной памяти видели у LSTM, где ячейка тоже складывает, а не
переписывает.
Обратите внимание, что́ именно тогда заметили: деградировала не тестовая, а
обучающая ошибка. Проблема была не в переобучении, а в том, что оптимизатору
трудно найти тождественное отображение там, где оно нужно.
Стек как сумма экспоненциально многих путей
Раскроем произведение для линейного случая Fℓ(x)=Aℓx:
Скобки раскрываются в 2L слагаемых, и каждое слагаемое — свой путь: список
подслоёв, через которые сигнал прошёл, при том что остальные он обошёл по
прямой. Сеть с residual — не одна глубокая цепочка, а ансамбль путей всех длин
от нуля до L.
Отсюда следует практический вывод: обнулить один подслой в обученной сети с
residual обычно не смертельно — исчезает часть путей, а не весь сигнал. В сети
без residual удаление слоя разрывает единственную цепь, и выход превращается в
шум.
Давиденко: глубина как шаг по параметру
За «прибавлением маленькой поправки на каждом шаге» стоит старая вычислительная
идея. В 1953 году Дмитрий Фёдорович Давиденко предложил решать нелинейную
систему F(x)=0 не итерациями Ньютона из случайной точки, а погружением задачи
в семейство
H(x,t)=F(x)−(1−t)F(x0),t∈[0,1],
где при t=0 решение известно (это x0), а при t=1 стоит исходная задача.
Дифференцируя тождество H(x(t),t)=0 по параметру, он получил обыкновенное
дифференциальное уравнение на траекторию решения:
dtdx=−[Hx′(x,t)]−1Ht′(x,t),
которое дальше интегрируется численно. Метод продолжения по параметру превращает
«решить» в «доехать маленькими шагами от известного к искомому».
Это ровно шаг явного метода Эйлера по «времени глубины». Слои перестают быть
независимыми преобразованиями и становятся дискретизацией непрерывного движения
представления. Отсюда и характерное наблюдение практиков: в обученных
трансформерах соседние блоки часто вносят похожие по направлению поправки, а
перестановка двух соседних блоков портит результат меньше, чем можно было бы
ожидать от «разных этапов обработки». Та же линия мысли позже дала непрерывные
по глубине модели, где число слоёв заменяется шагом интегратора.
Статистики берутся по каналам внутри одной позиции: ни батч, ни соседние
токены не участвуют. Поэтому один пример обрабатывается независимо от других, а
на генерации длина контекста не меняет формулу — важное отличие от batch
normalization, где статистики зависят от компании, в которой пример оказался.
До обучаемых γ,β нормировка стирает ровно две вещи: сдвиг всех
координат на константу и общий положительный множитель. Формально для любых
a>0, b
LN(ax+b1)=LN(x),
то есть выход зависит только от направления вектора после вычитания среднего.
Геометрически LN проецирует x на гиперплоскость ∑jzj=0 и растягивает
результат до фиксированной длины: при ε=0, γ=1, β=0
Рис. 77.3. Layer normalization стирает сдвиг и масштаб позиции
Реальные данные load_digits: вектор одной цифры длиной d=64 и он же после
преобразования 3x+7. До LN профили каналов заметно разные. После LN они
совпадают: максимум модуля разности 4,6⋅10−7 — остаточный след от
ε=10−5, а не настоящее различие. Средняя длина вектора после LN
равна 8,000, то есть в точности 64, при разбросе исходных норм от 46,8
до 76,9.
Pre-norm и post-norm — не вопрос вкуса
Исходная работа 2017 года нормировала после сложения:
X′=LN(X+MHA(X)).
Современный вариант нормирует до подслоя, оставляя поток чистым:
X′=X+MHA(LN(X)).
Разница принципиальна для градиента. В post-norm между входом стека и выходом
нет ни одного участка без нормировки: единичный путь проходит через LN на каждом
слое и умножается на её якобиан. В pre-norm сквозной путь чист, поэтому градиент
до нижних слоёв доходит без накопленного множителя — глубокие модели на
post-norm без разогрева learning rate часто просто не стартуют.
Что происходит с сигналом по глубине
Проверим утверждения численно. Возьмём 24 случайных блока (d=64, 16 позиций,
выходные проекции масштабированы на 1/2L), прогоним через них случайный
вход и посмотрим на две величины: относительную норму потока и средний косинус
между представлением позиции и её исходным embedding.
Рис. 77.4. Residual удерживает представление сквозь глубину
С residual (синяя) поток за 24 блока меняет норму всего в 1,10 раза, а косинус с
исходным вектором остаётся 0,89: позиция помнит, кто она. Без residual (красная)
норма падает до 0,09 от начальной, а косинус — до 0,01, то есть до случайного
направления. Post-norm (зелёная) при этой инициализации почти неотличим от
pre-norm по forward-статистикам: разница между ними живёт в градиентах, и одного
этого графика для выбора порядка нормировки недостаточно.
Косинус 0,89 не значит, что блоки ничего не сделали: 24 поправки заметно
повернули вектор, и именно на этом повороте держится вся полезная работа сети.
Но исходное содержимое никуда не исчезло — оно осталось главным слагаемым.
И наоборот, падение косинуса само по себе не равно потере информации:
представление может поворачиваться, сохраняя декодируемый признак. Проверять
надо линейным probe, а не глазами.
Лаборатория блока
Сигнал через attention, MLP и residual
График шире экрана — листайте по горизонтали →
Загружается живая иллюстрация…
Веса в лаборатории случайные и фиксированные, обучения нет — измеряется именно
маршрут сигнала через архитектуру. Отключайте подслои по одному и следите за
тремя числами внизу.
Выключите residual — и на глубине 12 косинус с исходным embedding падает с 0,41
до 0,11, а норма потока проваливается: сеть перестаёт помнить вход. Выключите
attention — и нижняя строка отчёта показывает ровно ноль: пути между позициями
физически нет, сколько блоков ни ставь. Уберите нормировку и поднимите масштаб
подслоя до 2 — за 12 блоков норма вырастает больше чем в миллион раз, и никакая
точность float этого не спасёт. Верните pre-LN — и тот же масштаб становится
безопасным.
Кто вообще может увидеть первый токен
Измерим маршрут напрямую. Возьмём causal-стек и оценим центральной разностью,
насколько сдвинется представление последней позиции, если чуть шевельнуть
embedding первой:
С attention при шести блоках получаем g≈0,90. Без attention — в
точности 0,0: не «маленькое число», а машинный нуль, и так на любой глубине
от 1 до 12.
Слева: зависимость последней позиции от первой существует только при наличии
attention; красная линия лежит на машинном нуле на всех глубинах. Справа:
линейный probe поверх замороженных случайных блоков на реальных SMS. Синие
столбцы — R2 для доли редких слов в сообщении, золотые — accuracy
распознавания спама; штриховая линия 0,70 — частота большинства.
Ablation на реальных SMS
Проверим то же самое на настоящем корпусе. Берём коллекцию SMS-сообщений (та же,
что в уроке про наивный Байес), оставляем 2272 сообщения длиной не
меньше 16 слов, кодируем первые 16 слов случайными фиксированными embeddings,
прогоняем через два замороженных случайных блока и обучаем только линейный
probe поверх представления последней позиции. Разбиение — 1590 сообщений на
обучение и 682 на тест.
Цели две. Первая — доля редких слов в сообщении (в среднем 0,306): чтобы её
оценить, последняя позиция обязана что-то знать обо всех остальных. Вторая —
метка спама (доля спама 0,300, то есть базовая точность большинства 0,70).
вариант
R2 (доля редких слов)
accuracy (спам)
полный блок
0,85
0,77
без attention
0,07
0,70
без MLP
0,88
0,80
без positional
0,79
0,78
Однозначно читается одна строка — и это честнее, чем хотелось бы. Без attention
R2 падает с 0,85 до 0,07, а спам определяется ровно на уровне частоты
большинства: голова видит один последний токен и угадывает «не спам». Без
positional embeddings качество проседает умеренно (0,85 → 0,79): смешивание
сохраняется, теряется только различение порядка, а обе наши цели от порядка слов
почти не зависят.
А вот строка «без MLP» показывает улучшение, и это не опечатка. Случайный
необученный MLP не добавляет полезных признаков — он добавляет нелинейный шум
поверх аккуратно перемешанных значений. Вывод из такого опыта строго один:
эксперимент с замороженными весами измеряет маршрутизацию, а не
выразительность. Чтобы измерить вклад MLP, его нужно обучать, и тогда картина
меняется на противоположную.
Decoder, encoder и роль маски
Всё сказанное не зависит от того, куда смотрит внимание: маска решает лишь, какие
пары позиций разрешены. Decoder-only модель использует causal mask, после L
блоков берёт представление позиции t и превращает его в logits:
zt=Wvocabht(L)+b,p(w∣x≤t)=softmax(zt)w.
Обучающая цель — предсказание следующего токена. Во время обучения
все позиции считаются параллельно, потому что правильные предыдущие токены
известны; при генерации шаги последовательны, и KV-cache экономит пересчёт
префикса. Weight tying связывает Wvocab с матрицей входных
embeddings: параметров меньше, а геометрия чтения и предсказания становится
общей.
Encoder видит вход двунаправленно и строит контекстные представления для
понимания; encoder–decoder добавляет cross-attention, где queries приходят из
decoder, а keys и values — из encoder. Граница не абсолютна: decoder можно
заставить классифицировать текстовым ответом, но интерфейс цели и цена вывода
будут другими.
Цена блока: два разных режима
Сложность считается по двум слагаемым. Проекции и MLP линейны по длине и
квадратичны по ширине, карта внимания — наоборот:
FLOPMHA≈8nd2+4n2d,FLOPMLP≈16nd2.
Приравняв квадратичную часть внимания к стоимости MLP, получаем точку баланса
4n2d=16nd2, то есть n=4d; при d=512 это n=2048. Но формула считает
операции, а не время. Мы измерили реальный forward в numpy при d=512 и
float32.
При n=64 внимание в пять-шесть раз дешевле MLP: квадратичная часть ещё
ничтожна, а плотных умножений у него вдвое меньше. При n=4096 оно уже впятеро дороже. Отношение пересекает единицу между n=512 и n=1024 — раньше
теоретических 2048, потому что большие плотные умножения MLP лучше используют
кэш и векторные регистры, чем цепочка операций внимания. Это одно измерение на
одной машине, а не универсальная константа.
Генерация устроена иначе. Без кэша шаг t пересчитывает весь префикс и стоит
порядка 4t2d+8td2, с кэшем — только новую строку:
FLOPtcache≈4td+8d2.
Суммируя по t до 512 при d=512, получаем разницу в 274 раза.
Практический вывод из графика важнее самих чисел: оптимизировать надо ту часть,
которая доминирует в вашем режиме. Ускорение внимания вдвое при n=64 даст
меньше десятой доли общего выигрыша. Ту же логику «одинаковые FLOP — разное
время» мы уже разбирали на стоимости матричного умножения.
Что меняется с глубиной
Статистическая картина такая: ранние блоки чаще держат лексические и локальные
признаки, средние собирают отношения между словами, поздние подстраиваются под
выходную цель. Это тенденция, а не расписание, и она разная для разных корпусов
и целей обучения.
Инструментов проверки два, и они отвечают на разные вопросы. Linear probe
обучает простой классификатор поверх замороженных представлений слоя: если
признак декодируется, он там присутствует линейно. Activation patching
подменяет активацию одного запуска активацией другого и смотрит, перевернётся ли
ответ: это уже вопрос о причинном влиянии. Признак может прекрасно
декодироваться и при этом не использоваться моделью.
Визуализации карт внимания обязаны дополняться такими
вмешательствами. Отдельная голова — лишь одна ветвь residual stream среди сотен,
и красивая диагональ на heatmap сама по себе ничего не доказывает.
Мини-исследование: где хранится признак
Составьте предложения с двумя именами и отношением: «Анна выше Бориса». После
нескольких отвлекающих фраз задайте вопрос, кто ниже. Для каждого слоя обучите
probe, предсказывающий отношение из представления последнего токена, — получится
кривая декодируемости по глубине. Затем возьмите пару примеров с
противоположными отношениями и подмените активацию выбранного слоя и позиции.
Если ответ переворачивается, участок потока не просто содержит признак, а влияет
на результат.
Измеряйте изменение log-odds, а не только итоговый класс: класс меняется скачком
и скрывает величину эффекта. И сравнивайте с контролем: тот же патч на случайной
позиции того же слоя эффекта давать не должен.
Мини-исследование: свой crossover
Повторите замер из рис. 77.6 на своём железе. Зафиксируйте d, точность
(float32 или float64), число повторов и прогрев; профилируйте MHA и MLP
отдельно; постройте отношение времён и найдите свою точку перелома. Затем
повторите при расширении 2 и 8. Проверьте предсказание n=4d и объясните
расхождение — оно почти наверняка будет.
Отдельно измерьте генерацию с KV-cache и без него и обязательно напишите
unit-тест: logits полного префикса и пошагового декодирования с кэшем должны
совпадать в пределах численной погрешности на нескольких длинах. Этот тест ловит
самую частую ошибку реализации — рассинхронизацию позиций в кэше. Разговор о том,
во что превращается вся эта арифметика при масштабировании модели, ждёт нас в
уроке про законы масштабирования.
Блок как система путей
Трансформер — композиция двух операций и двух инфраструктурных опор. Attention
смешивает позиции и создаёт единственный канал между ними; MLP перестраивает
каналы и добавляет нелинейность, забирая две трети параметров; residual держит
сквозной путь, вдоль которого сеть распадается на 2L маршрутов разной длины;
LayerNorm убирает сдвиг и масштаб на входе каждой ветви, не трогая сам поток.
Понимание блока начинается не с рисунка из сотни прямоугольников, а с отключения
частей и трёх контрольных чисел: норма, косинус, влияние соседа.