Блок трансформера
Из каких четырёх деталей собран блок трансформера, почему нормализацию ставят перед подслоем, сколько в блоке параметров и как из одного блока получились BERT, T5 и GPT — и почему победил GPT.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В модуле 4.4 вы собрали многоголовое внимание. Но внимание — это ещё не модель. Любая современная LLM устроена как стопка одинаковых блоков: токены превращаются в эмбеддинги, проходят через блоков подряд, а в конце LM-головаLM-голова (языковая голова)EN LM head (language modeling head)Последний линейный слой GPT: превращает вектор ширины d в V логитов, по одному на токен словаря. Часто делит матрицу с эмбеддингами (weight tying). (последний линейный слой , как выходной слой классификатора из модуля 2.2) выдаёт логиты следующего токена. У GPT-2 small таких блоков 12, у Qwen3-8B — 36, у DeepSeek-V3 — 61. Блоки отличаются только весами, устройство у всех одно.
Значит, если понять один блок трансформераблок трансформераEN Transformer block (layer)Внимание + FFN, каждое с остаточной связью и нормализацией. Вход и выход — матрица (T, d), поэтому блоки складываются в стопку (12 у GPT-2 small, 36 у Qwen3-8B, 61 у DeepSeek-V3)., вы поймёте почти всю архитектуру трансформератрансформерEN TransformerАрхитектура из статьи «Attention Is All You Need» (2017) — стопка одинаковых блоков из внимания и FFN без рекуррентности. В китайских текстах название обычно не переводят.. В этом уроке соберём блок из четырёх деталей, разберёмся, почему порядок деталей поменялся с 2017 года, посчитаем параметры GPT-2 руками, а в конце выясним, как из одного блока выросли три семейства моделей — и почему ChatGPT, Qwen и DeepSeek принадлежат одному из них.
flowchart TB T["Токены: «Карп прыгает через»"] --> E["Эмбеддинги + позиции<br/>(T, d)"] E --> B1["Блок 1"] --> B2["Блок 2"] --> D["…"] --> BN["Блок N"] BN --> N["Финальная нормализация"] --> H["LM-голова: d → V"] --> L["Логиты (T, V)"]
Обратите внимание на формы: вход блока — матрица , по строке на токен, и выход — тоже . Именно поэтому блоки можно ставить друг за другом сколько угодно раз.
Две половины блока: общение и размышление
Заголовок раздела «Две половины блока: общение и размышление»Внутри блока две части, и у них разные роли.
- Внимание — единственное место во всей модели, где токены обмениваются информацией. Токен «через» узнаёт, что перед ним «прыгает» и «карп».
- Полносвязный блок (полносвязный блокполносвязный блок (FFN)EN feed-forward network (FFN), feed-forward network) — каждый токен обрабатывается отдельно, одной и той же маленькой нейросетью. Он «обдумывает» то, что собрало внимание.
Андрей Карпатый в видео «Let’s build GPT» описывает это коротко: сначала общение (communication), потом вычисление (computation). Внимание смешивает строки матрицы между собой, FFN преобразует каждую строку по отдельности.
FFN: двухслойный перцептрон на каждый токен
Заголовок раздела «FFN: двухслойный перцептрон на каждый токен»Запись читается « — матрица из строк и столбцов с вещественными числами» (урок 1.0.3). Что здесь написано: вектор токена размера сначала расширяют в 4 раза, пропускают через нелинейность и сжимают обратно до . Например, при (GPT-2) вектор из 768 чисел становится вектором из 3072 чисел, а потом снова из 768. Это обычный двухслойный перцептрон из ступени 3, только применённый к каждой позиции. В статье 2017 года — ReLU, в GPT-2 и BERT — GELUGELUEN Gaussian error linear unit (GELU)GELU(z) = z·Φ(z), где Φ — функция распределения N(0, 1). Гладкая активация BERT и GPT-2 (в GPT-2 — tanh-приближение).:
| ReLU | 0 | 0 | 0 | 1 | 2 |
| GELU | 0 | 0.841 | 1.954 |
GELU — «сглаженный ReLU»: — вероятность, что стандартная нормальная величина меньше . Положительные числа почти не трогает, отрицательные плавно гасит. В уроке 4.5.3 его сменит SwiGLU.
Сколько это стоит. Две матрицы по — это параметров. А внимание, как вы считали в уроке 4.4.3, стоит . Значит, две трети параметров блока живут в FFN. Для это 4 722 432 параметра FFN против 2 362 368 у внимания (со смещениями).
Что там хранится. Гева и соавторы (2020) показали, что FFN работает как память «ключ–значение». Каждый из скрытых нейронов — это пара векторов: столбец служит «ключом», который срабатывает на определённый шаблон во входе, а соответствующая строка — «значением», которое FFN дописывает к токену с весом «насколько сработал ключ». Условный пример: на шаблон «столица Франции —» срабатывает нейрон, который подталкивает модель к «Париж». Поэтому говорят, что внимание находит, а FFN знает. Эта идея приведёт нас к смеси экспертов (MoE) в модуле 5.3: если FFN — это память, её можно разбить на много «экспертов» и включать только нужных.
Остаточные связи: поток, в который все пишут
Заголовок раздела «Остаточные связи: поток, в который все пишут»Каждая из двух частей блока не заменяет вход, а прибавляет к нему поправку:
Это остаточная связьостаточная связьEN residual connection из урока 3.4.2. Вспомним две причины, по которым без неё глубокую сеть не обучить:
- Градиент. Производная по равна . Единица — это «скоростная трасса»: градиент от loss доходит до первого слоя, даже если маленькая.
- Старт с тождества. При инициализации поправки малы, и вся стопка из 36 блоков почти ничего не портит. Обучение начинается с разумной точки и постепенно учится полезным поправкам.
Удобно смотреть на это так. Если развернуть все блоки, выход модели — это вход плюс сумма всех поправок:
Вектор , который течёт сквозь модель, называют остаточным потокомостаточный потокEN residual streamВектор шириной d, который течёт сквозь все блоки. Каждый подслой читает из него и дописывает поправку, так что выход = вход + сумма всех поправок. (residual stream). Это общая «шина» шириной : каждый подслой читает из неё (через нормализацию), что-то вычисляет и дописывает результат обратно. У GPT-2 small в шину пишут 24 «автора»: 12 вниманий и 12 FFN. Такой взгляд популяризировала команда интерпретируемости Anthropic в работе «A Mathematical Framework for Transformer Circuits» (2021), и он пригодится на ступени 9: DeepSeek и Kimi переделывают саму шину (mHC и Attention Residuals), а в уроке 9.3.1 мы через неё заглянем внутрь модели — logit lens и разложение ответа на вклады голов.
Нормализация: до или после?
Заголовок раздела «Нормализация: до или после?»Четвёртая деталь — нормализация слоянормализация слояEN layer normalization (LayerNorm). Она берёт вектор одного токена, вычитает его среднее, делит на стандартное отклонение и умножает на обучаемые , :
Здесь и — среднее и дисперсия чисел внутри вектора , — крошечное число
(например, ), чтобы не делить на ноль, а — поэлементное умножение (как * для массивов NumPy).
Пример: , , , после нормализации (при , ) —
. Зачем она нужна и чем отличается от BatchNorm, разобрано в
уроке 3.4.1. Там же вы впервые встретили вопрос, к которому
мы сейчас вернёмся уже на настоящем блоке трансформера: куда её поставить.
Post-Norm: как в 2017 году
Заголовок раздела «Post-Norm: как в 2017 году»В «Attention Is All You Need» нормализация стоит после сложения:
Это Post-NormPost-Norm (нормализация после сложения)EN post-normalization (Post-LN)Norm(x + F(x)), как в исходном трансформере 2017 года и BERT. Без разогрева learning rate нестабилен на глубине.. Выглядит логично: после каждого шага поток приводится к стандартному масштабу. Но остаточная «трасса» больше не чистая: сигнал снизу проходит через нормализаций, и каждая его перемасштабирует. Сюн и соавторы (2020) показали, что у Post-Norm при инициализации градиенты слоёв у выхода получаются большими, и с обычным learning rate обучение разваливается. Спасает только разогрев (warmupразогрев learning rate (warmup)EN learning rate warmupПервые сотни–тысячи шагов learning rate линейно растёт от нуля до пика, пока статистики Adam не накопились.): несколько тысяч шагов с маленьким шагом в начале. Чем глубже модель, тем капризнее Post-Norm.
Pre-Norm: как почти во всех современных LLM
Заголовок раздела «Pre-Norm: как почти во всех современных LLM»В GPT-2 (2019) нормализацию перенесли внутрь ветки, на вход подслоя. Идея встречалась и раньше, в работах по машинному переводу, но именно после GPT-2 она стала стандартом языковых моделей:
Это Pre-NormPre-Norm (нормализация на входе подслоя)EN pre-normalization (Pre-LN)x + F(Norm(x)): нормализуется только вход ветки, остаточный путь чистый. Стандарт с GPT-2 (2019); требует финальной нормализации перед LM-головой.. Теперь подслой получает аккуратно нормированный вход, а сам поток никто не трогает: остаточный путь — чистая сумма , как в формуле выше. Поскольку поток к концу ничем не нормирован, после последнего блока добавляют финальную нормализацию перед LM-головой. Сюн и соавторы показали, что Pre-Norm обучается без разогрева и с большим learning rate.
Pre-Norm — стандарт, но не догма. Некоторые семейства моделей добавляют нормализацию ещё и на выход подслоя, перед сложением с потоком: так устроены Gemma 2 и Gemma 3 (нормализация до и после подслоя) и OLMo 2 (нормализация только на выходе подслоя). Цель та же — устойчивость обучения больших моделей; основа у всех — чистый остаточный поток.
Проверим в эксперименте
Заголовок раздела «Проверим в эксперименте»В уроке 3.4.1 вы смотрели на градиенты последнего слоя. Теперь посмотрим на прямой проход. Возьмём стопку случайных (необученных) блоков трансформера и измерим, сколько исходного сигнала доходит до верха: косинус между входом и выходом стопки. И заодно — во сколько раз выросла норма потока. На свободном процессоре скрипт работает несколько секунд; если ждать приходится долго, уберите 64 из списка глубин.
import torchimport torch.nn as nnimport torch.nn.functional as F
class TinyBlock(nn.Module): """Блок с переключателем: pre=True — Pre-Norm, pre=False — Post-Norm (как в статье 2017 года)."""
def __init__(self, d, n_heads, pre): super().__init__() self.pre, self.h = pre, n_heads self.ln1, self.ln2 = nn.LayerNorm(d), nn.LayerNorm(d) self.qkv, self.proj = nn.Linear(d, 3 * d), nn.Linear(d, d) # W_Q, W_K, W_V одной матрицей и W_O self.ffn = nn.Sequential(nn.Linear(d, 4 * d), nn.GELU(), nn.Linear(4 * d, d)) # FFN: d → 4d → d
def attn(self, x): """Многоголовое причинное внимание из урока 4.4.3.""" B, T, D = x.shape # qkv(x) имеет форму (B, T, 3D); split(D, -1) режет её на три куска (B, T, D) — это q, k, v; # каждый кусок раскладываем на головы: (B, T, D) → (B, H, T, d_h) q, k, v = (t.view(B, T, self.h, D // self.h).transpose(1, 2) for t in self.qkv(x).split(D, -1)) y = F.scaled_dot_product_attention(q, k, v, is_causal=True) return self.proj(y.transpose(1, 2).reshape(B, T, D))
def forward(self, x): if self.pre: # Pre-Norm: x + f(LN(x)) x = x + self.attn(self.ln1(x)) return x + self.ffn(self.ln2(x)) x = self.ln1(x + self.attn(x)) # Post-Norm: LN(x + f(x)) return self.ln2(x + self.ffn(x))
def probe(pre, n_layers, d=64, seed=0): """Косинус «вход–выход» стека (сколько исходного сигнала дошло до верха) и рост нормы потока.""" torch.manual_seed(seed) # nn.Sequential(*список) — стопка блоков, которые применяются по очереди blocks = nn.Sequential(*[TinyBlock(d, 4, pre) for _ in range(n_layers)]) x0 = torch.randn(4, 16, d) # 4 текста по 16 «токенов» with torch.no_grad(): # только прямой проход, градиенты не нужны y = blocks(x0) cos = F.cosine_similarity(x0, y, dim=-1).mean().item() # косинус вход↔выход для каждого токена, среднее growth = (y.norm(dim=-1) / x0.norm(dim=-1)).mean().item() # во сколько раз выросла длина вектора return cos, growth
for L in (2, 8, 32, 64): (c_post, g_post), (c_pre, g_pre) = probe(False, L), probe(True, L) print(f"слоёв {L:2d}: Post-Norm cos {c_post:.2f}, норма ×{g_post:.1f} Pre-Norm cos {c_pre:.2f}, норма ×{g_pre:.1f}")слоёв 2: Post-Norm cos 0.92, норма ×1.0 Pre-Norm cos 0.93, норма ×1.1слоёв 8: Post-Norm cos 0.72, норма ×1.0 Pre-Norm cos 0.77, норма ×1.3слоёв 32: Post-Norm cos 0.24, норма ×1.0 Pre-Norm cos 0.51, норма ×2.0слоёв 64: Post-Norm cos 0.04, норма ×1.0 Pre-Norm cos 0.36, норма ×2.8Как это читать:
- Post-Norm забывает вход. После 64 слоёв косинус 0.04: от исходного сигнала почти ничего не осталось, а норма всё время равна исходной — её держит LayerNorm. Прямой «трассы» от входа к выходу нет: сигнал проходит через перемасштабирований подряд, и градиенту на обратном пути к нижним слоям приходится так же трудно.
- Pre-Norm вход не теряет, а разбавляет. Заметьте: . Так и должно быть, если , а поправки почти перпендикулярны . Исходный сигнал лежит в потоке целиком, просто поток стал длиннее.
Отсюда и цена Pre-Norm. Норма потока растёт с глубиной, и поправка каждого следующего слоя на её фоне всё мельче. Глубокие слои влияют на результат слабее, чем могли бы. Поэтому исследователи периодически возвращаются к Post-Norm с хитрыми поправками (DeepNorm от Microsoft Research Asia позволил обучить трансформер в 1000 слоёв) или переделывают сам остаточный поток (mHC у DeepSeek, Attention Residuals у Kimi — ступень 9). Но базовый выбор индустрии с 2019 года — Pre-Norm: стабильное обучение ценнее небольшого выигрыша в качестве, который иногда даёт тонко настроенный Post-Norm.
| Post-Norm (2017) | Pre-Norm (GPT-2 и почти все LLM) | |
|---|---|---|
| Формула | ||
| Остаточный путь | искажён нормализациями | чистая сумма |
| Разогрев LR | обязателен | желателен, но не критичен |
| Глубокие модели | нестабильны без трюков | обучаются |
| Финальная норма перед головой | не нужна | нужна |
| Слабое место | обучение | рост нормы потока, «размытые» глубокие слои |
Блок целиком
Заголовок раздела «Блок целиком»Соберём всё вместе. Pre-Norm блок GPT-2:
| Шаг | Операция | Форма |
|---|---|---|
| вход | остаточный поток | |
| 1 | — по каждому токену отдельно | |
| 2 | многоголовое причинное внимание (урок 4.4.3) | |
| 3 | ||
| 4 | ||
| 5 | FFN: | |
| 6 |
Единственная операция, которая смешивает позиции, — шаг 2. Всё остальное работает с каждым токеном отдельно.
Напишем блок на PyTorch и посчитаем параметры GPT-2 small: , 12 голов, 12 блоков, словарь 50 257 токенов, контекст 1024.
import torchimport torch.nn as nnimport torch.nn.functional as F
torch.manual_seed(0)
class CausalSelfAttention(nn.Module): """Многоголовое причинное внимание из урока 4.4 (со смещениями, как в GPT-2)."""
def __init__(self, d_model, n_heads): super().__init__() self.h = n_heads self.qkv = nn.Linear(d_model, 3 * d_model) # W_Q, W_K, W_V одной матрицей self.proj = nn.Linear(d_model, d_model) # выходная проекция W_O
def forward(self, x): B, T, D = x.shape q, k, v = self.qkv(x).split(D, dim=-1) # (B, T, 3D) → три тензора (B, T, D) q, k, v = (t.view(B, T, self.h, D // self.h).transpose(1, 2) for t in (q, k, v)) # → (B, H, T, d_h) y = F.scaled_dot_product_attention(q, k, v, is_causal=True) return self.proj(y.transpose(1, 2).reshape(B, T, D))
class FFN(nn.Module): """Полносвязный блок: расширяем в 4 раза, GELU, сжимаем обратно. Работает с каждым токеном отдельно."""
def __init__(self, d_model): super().__init__() self.up = nn.Linear(d_model, 4 * d_model) self.down = nn.Linear(4 * d_model, d_model)
def forward(self, x): return self.down(F.gelu(self.up(x)))
class Block(nn.Module): """Pre-Norm блок трансформера (GPT-2)."""
def __init__(self, d_model, n_heads): super().__init__() self.ln1 = nn.LayerNorm(d_model) self.attn = CausalSelfAttention(d_model, n_heads) self.ln2 = nn.LayerNorm(d_model) self.ffn = FFN(d_model)
def forward(self, x): x = x + self.attn(self.ln1(x)) # токены обмениваются информацией x = x + self.ffn(self.ln2(x)) # каждый токен «обдумывает» полученное return x
block = Block(d_model=768, n_heads=12)count = lambda m: sum(p.numel() for p in m.parameters()) # numel() — число элементов тензора; суммируем по всем весамprint(f"внимание: {count(block.attn):>9,}")print(f"FFN: {count(block.ffn):>9,}")print(f"нормы: {count(block.ln1) + count(block.ln2):>9,}")print(f"блок: {count(block):>9,}")
x = torch.randn(2, 10, 768) # батч 2, 10 токеновprint("вход:", tuple(x.shape), "-> выход:", tuple(block(x).shape))
# Причинность: меняем последний токен — ранние позиции не меняютсяx2 = x.clone()x2[:, -1] += 10 # сильно меняем только последний токен каждого текстаprint("ранние позиции не изменились:", torch.allclose(block(x)[:, :-1], block(x2)[:, :-1], atol=1e-5))
# Вся GPT-2 small: 12 блоков + эмбеддинги токенов и позиций + финальная нормаtotal = 12 * count(block) + 50257 * 768 + 1024 * 768 + 2 * 768print(f"GPT-2 small: {total:,} параметров")внимание: 2,362,368FFN: 4,722,432нормы: 3,072блок: 7,087,872вход: (2, 10, 768) -> выход: (2, 10, 768)ранние позиции не изменились: TrueGPT-2 small: 124,439,808 параметровРовно 124 439 808 — столько параметров у официального чекпоинта GPT-2 small (LM-голова делит веса с матрицей эмбеддингов, поэтому отдельно её не считаем). Из них 85 млн — в блоках и 39 млн — в эмбеддингах. Формула на будущее: блок , модель плюс эмбеддинги.
Три семейства из одного блока
Заголовок раздела «Три семейства из одного блока»Исходный трансформер 2017 года делал машинный перевод и состоял из двух стопок блоков. Энкодер читал исходное предложение с двунаправленным вниманием: каждый токен видит всё предложение. Декодер писал перевод с причинной маской и вдобавок смотрел на энкодер через перекрёстное вниманиеперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности. (урок 4.4.1). Уже через год стопки разошлись:
flowchart LR T["Transformer 2017<br/>энкодер + декодер, перевод"] --> E["Encoder-only<br/>BERT, 2018"] T --> ED["Encoder-decoder<br/>T5, 2019"] T --> D["Decoder-only<br/>GPT, 2018 → GPT-3, 2020"] D --> L["Llama, Qwen, DeepSeek,<br/>Kimi, GLM-4.5, MiniMax"] E --> R["эмбеддинги и реранкеры<br/>для поиска (BGE)"]
- Encoder-onlyencoder-only (только энкодер)EN encoder-only architectureДвунаправленное внимание, обучение через маскированное языковое моделирование (BERT, RoBERTa). Сегодня — модели эмбеддингов, реранкеры, классификаторы. — BERT (Google, 2018). Только энкодер, маска двунаправленная. Цель обучения — маскированное языковое моделированиемаскированное языковое моделирование (MLM)EN masked language modeling (MLM)Цель обучения BERT — закрыть около 15% токенов и угадать их по контексту с обеих сторон. Сигнал дают только замаскированные позиции. (MLM): закрыть 15% токенов и угадать их по контексту с обеих сторон. Модель отлично понимает текст (классификация, поиск именованных сущностей), но генерировать не умеет: она никогда не училась писать слева направо.
- Encoder-decoderэнкодер–декодерEN encoder-decoder architectureДвунаправленный энкодер читает вход, причинный декодер генерирует выход и смотрит на энкодер через cross-attention (исходный Transformer, T5, Whisper). — T5 (Google, 2019). Обе стопки, любая задача записывается как «текст на входе → текст на выходе». Цель — восстанавливать выкинутые куски текста. Естественный выбор для перевода и суммаризации; по этой схеме устроен и Whisper для распознавания речи (урок 9.1.5).
- Decoder-onlydecoder-only (только декодер)EN decoder-only architectureОдна стопка блоков с причинной маской, цель — следующий токен. Архитектура GPT, Llama, Qwen, DeepSeek, Kimi, GLM-4.5. — GPT (OpenAI, 2018). Только декодер без cross-attention, маска причинная, цель — предсказание следующего токенапредсказание следующего токенаEN next-token predictionГлавная задача языковой модели — по началу текста оценить вероятности следующего токена. Ответ чат-бота собирается из таких шагов по одному токену.. Сюда относятся практически все современные LLM.
Есть и гибрид — префиксная языковая модельпрефиксная языковая модель (Prefix LM)EN prefix language model (Prefix LM)Внутри префикса (например, запроса) внимание двунаправленное, дальше — причинное. UniLM, GLM, ChatGLM-6B; начиная с ChatGLM2 Zhipu перешла на причинную маску. (Prefix LM): одна стопка, но внутри «префикса» (например, вопроса пользователя) внимание двунаправленное, а дальше причинное. Так устроены UniLM от Microsoft и GLM из Университета Цинхуа (см. «Китайский угол»). Разница между всеми вариантами — только в маске внимания:
import numpy as np
T, P = 5, 2 # 5 токенов, из них первые 2 — «префикс» (запрос пользователя)
bidirectional = np.ones((T, T), dtype=int) # BERT, энкодер T5: все видят всехcausal = np.tril(np.ones((T, T), dtype=int)) # GPT, Llama, Qwen, DeepSeek: только прошлое и себяprefix = causal.copy()prefix[:, :P] = 1 # Prefix LM (GLM, ChatGLM-6B): префикс виден целиком
for name, m in [("двунаправленная", bidirectional), ("причинная", causal), ("префиксная", prefix)]: print(name) print(m)
# Сколько позиций дают обучающий сигнал на тексте из 1000 токеновT_text = 1000print("decoder-only:", T_text - 1, "предсказаний; BERT (15% масок):", int(0.15 * T_text))двунаправленная[[1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1] [1 1 1 1 1]]причинная[[1 0 0 0 0] [1 1 0 0 0] [1 1 1 0 0] [1 1 1 1 0] [1 1 1 1 1]]префиксная[[1 1 0 0 0] [1 1 0 0 0] [1 1 1 0 0] [1 1 1 1 0] [1 1 1 1 1]]decoder-only: 999 предсказаний; BERT (15% масок): 150Строка показывает, на какие позиции смотрит токен (1 — можно). В префиксной маске два первых токена видят друг друга, а всё, что после префикса, видит только прошлое.
| Encoder-only | Encoder-decoder | Decoder-only | |
|---|---|---|---|
| Пример | BERT, RoBERTa, BGE | T5, исходный Transformer, Whisper | GPT, Llama, Qwen, DeepSeek |
| Маска | двунаправленная | двунаправленная + причинная | причинная |
| Цель обучения | угадать 15% замаскированных | восстановить пропуски | следующий токен |
| Генерация | нет | да | да |
| Где сегодня | эмбеддинги, реранкеры, классификаторы | перевод, речь | все чат-модели и агенты |
Почему победил decoder-only
Заголовок раздела «Почему победил decoder-only»В 2019 году ставка на GPT не была очевидной: BERT лидировал почти во всех бенчмаркахбенчмаркEN benchmark понимания текста. Аргументов за decoder-only накопилось несколько.
- Сигнал с каждой позиции. Из текста в 1000 токенов decoder-only получает 999 обучающих примеров, BERT — около 150. При одинаковых данных первый учится на всём тексте сразу (вывод из кода выше).
- Один интерфейс для всех задач. Перевод, ответ на вопрос, код — всё это «продолжи текст». GPT-3 (2020) показала, что большой модели хватает нескольких примеров прямо в запросе (обучение в контекстеобучение в контекстеEN in-context learning (ICL)Модель решает новую задачу по примерам в промпте, без изменения весов. Ярко показано в статье о GPT-3 (2020).), без дообучения и отдельных «голов» под задачи.
- Простота и масштаб. Одна однородная стопка, одна маска, одна функция потерь. Законы масштабирования (модуль 5.1) изучали в основном на decoder-only, и под эту схему выросла вся инфраструктура: GPU-ядра, параллелизм, сервинг (обслуживание запросов к модели, ступень 7).
- Дешёвая генерация и диалог. С причинной маской состояния старых токенов не меняются, когда приходят новые. Ключи и значения прошлого можно посчитать один раз и хранить — это KV-кэшKV-кэшEN KV cache (урок 4.6.4). В диалоге кэш прошлых реплик переиспользуется. С двунаправленной маской каждая новая реплика меняет состояния всех предыдущих токенов, и кэш приходится пересчитывать. Проверим на одном слое:
import numpy as np
rng = np.random.default_rng(0)d = 8W_q, W_k, W_v = (rng.normal(size=(d, d)) / np.sqrt(d) for _ in range(3))
def layer(X, causal): """Один слой self-attention (без FFN — для наглядности хватит).""" T = len(X) S = (X @ W_q) @ (X @ W_k).T / np.sqrt(d) if causal: S = np.where(np.tril(np.ones((T, T), dtype=bool)), S, -np.inf) A = np.exp(S - S.max(axis=1, keepdims=True)) return X + (A / A.sum(axis=1, keepdims=True)) @ (X @ W_v) # с остаточной связью
dialog = rng.normal(size=(6, d)) # 6 токенов уже обработаныnew = rng.normal(size=(1, d)) # пришёл 7-й токенfor causal in (True, False): old = layer(dialog, causal) # состояния 6 токенов до прихода нового upd = layer(np.vstack([dialog, new]), causal)[:6] # те же 6 токенов, когда в тексте уже 7 print("причинная" if causal else "двунаправленная", "— старые состояния не изменились:", np.allclose(old, upd))причинная — старые состояния не изменились: Trueдвунаправленная — старые состояния не изменились: FalseПочему это важно именно для кэша: выход слоя — это вход следующего, и из него следующий слой считает свои ключи и значения. Если выходы старых токенов поменялись (двунаправленная маска), поменяются и их , во всех слоях выше — сохранённый кэш устарел. С причинной маской старые выходы те же, значит, и кэш верен.
- Теоретический довод Су Цзяньлиня. В статье 2023 года автор RoPE замечает: матрица причинного внимания треугольная с положительной диагональю, а значит, всегда полного ранга. У двунаправленного внимания такой гарантии нет, и оно может «терять ранг», то есть выразительность. В его эксперименте на моделях около 1 млрд параметров двунаправленный префикс (UniLM) не дал выигрыша над обычной GPT. Полный ранг треугольной матрицы — строгий факт, а вот связь «ранг → качество модели» — гипотеза, хотя с практикой она согласуется.
Честная оговорка: decoder-only выигрывает не везде. Ван и соавторы (2022, arXiv:2204.05832) сравнили архитектуры при равном бюджете: сразу после претрейна лучше всех в zero-shot (задача в запросе без единого примера решения) была причинная decoder-only модель, а после многозадачного дообучения — модели с двунаправленным вниманием на входе (encoder-decoder и префиксные), обученные восстанавливать замаскированные куски. Индустрия выбрала первый вариант плюс масштаб. А энкодеры никуда не делись: модели эмбеддингов и реранкерыреранкерEN rerankerВторая ступень поиска — точная, но медленная модель (обычно кросс-энкодер) пересортировывает десятки кандидатов от быстрого ретривера. (модели, которые заново упорядочивают найденные документы по полезности) для поиска (например, BGE от Пекинской академии ИИ, BAAI) — это BERT-подобные энкодеры. Они понадобятся в RAG на ступени 8.
Проверь себя
Заголовок раздела «Проверь себя»- Блок трансформера = внимание (токены общаются) + FFN (каждый токен думает сам) + остаточные связи + нормализация. Форма на входе и выходе одинакова, поэтому блоки складываются в стопку.
- FFN расширяет вектор в 4 раза и сжимает обратно; в нём две трети параметров блока и, судя по исследованиям, значительная часть «знаний» модели. Блок стоит параметров, GPT-2 small — 124 439 808.
- Pre-Norm сохраняет чистый остаточный путь и обучается стабильно; Post-Norm из 2017 года требует разогрева и ломается на глубине. Цена Pre-Norm — растущая норма потока.
- Encoder-only (BERT), encoder-decoder (T5) и decoder-only (GPT) различаются маской и целью. Decoder-only победил: сигнал с каждой позиции, один интерфейс для всех задач, простота масштабирования и KV-кэш.
Дальше: в блоке нет ни слова о порядке токенов — внимание видит текст как мешок слов. Как дать модели позиции и почему весь мир перешёл на китайское изобретение RoPE — Позиционное кодирование и RoPE.