Языковое моделирование
Что вы узнаете
Заголовок раздела «Что вы узнаете»- Что такое языковая модель и почему «предсказать следующий токен» — это то же самое, что задать вероятность любого текста.
- Как логиты превращаются в вероятности через softmax и почему softmax считают с вычитанием максимума.
- Что такое cross-entropy (отрицательное логарифмическое правдоподобие), как она связана с методом максимального правдоподобия и KL-дивергенцией, и что такое перплексия.
- Как устроено обучение методом teacher forcing: сдвиг меток на один токен, метка
-100, causal-маска — и где это сделано в коде репозитория. - Общую схему decoder-only трансформера, на которой построены все шесть моделей библиотеки, и карту остальных глав пособия.
Предварительные знания
Заголовок раздела «Предварительные знания»Эта глава — первая, других глав она не требует. Нужны школьная вероятность (условная вероятность, произведение вероятностей), логарифм и экспонента, умение читать код на Python. Обозначения — в notation.md, термины — в glossary.md.
Задача: вероятность текста
Заголовок раздела «Задача: вероятность текста»Представим, что текст уже разбит на токены (tokens) — кусочки из конечного словаря (vocabulary) размера . Как именно режут текст, разбирается в следующей главе, tokenization.md; пока достаточно знать, что каждый токен — это целое число от до , а текст — последовательность таких чисел длины .
Языковая модель (language model) — это модель, которая каждой последовательности токенов сопоставляет вероятность . Хорошая языковая модель даёт осмысленному тексту («кошка сидит на ковре») большую вероятность, а бессмыслице («ковре на кошка сидит») — маленькую.
Зачем такая модель нужна:
- оценивать тексты: выбрать из нескольких вариантов перевода или распознанной речи самый правдоподобный;
- порождать тексты: выбирать токен за токеном, следуя вероятностям модели (подробно — в generation.md);
- учить представления: чтобы хорошо предсказывать текст, модель вынуждена выучить грамматику, факты и логику; GPT-1 показал, что такую модель затем легко дообучить на другие задачи (Radford et al., 2018).
Задать вероятность «в лоб» — таблицей для всех возможных текстов — нельзя: при и различных последовательностей , больше, чем атомов в наблюдаемой Вселенной. Нужен способ разложить эту огромную вероятность на небольшие части.
Цепное правило
Заголовок раздела «Цепное правило»Из определения условной вероятности следует цепное правило (chain rule) — разложение совместной вероятности в произведение условных:
где:
- — токен на позиции (позиции нумеруются с 0);
- — префикс (контекст), все токены до позиции ; при префикс пуст, и множитель равен ;
- — вероятность, что следующим после префикса будет именно токен ;
- — длина последовательности.
Вывод для трёх токенов
Применим определение условной вероятности дважды. Сначала отделим последний токен, считая , :
Затем разложим так же, с , :
Подставляя, получаем
Для произвольного то же рассуждение повторяется по индукции: .
Интуиция. Цепное правило — не приближение, а точное тождество: оно верно для любого распределения. Мы ничего не потеряли, но задача поменялась. Вместо одного распределения над текстами нужно уметь для любого префикса выдавать распределение над одним следующим токеном — это всего чисел.
Пример. Пусть токены — слова, и модель считает, что , , . Тогда
Вероятности длинных текстов быстро становятся крошечными: уже при сотне множителей порядка получается . Поэтому на практике работают с логарифмом, и произведение превращается в сумму:
В примере: .
Авторегрессивная факторизация
Заголовок раздела «Авторегрессивная факторизация»Модель, которая порождает последовательность слева направо, каждый раз предсказывая следующий элемент по предыдущим, называется авторегрессивной (autoregressive). Языковая модель с параметрами приближает каждый множитель цепного правила:
где:
- — все обучаемые параметры модели (матрицы эмбеддингов, attention, FFN и т. д.);
- — распределение над словарём, которое модель выдаёт, прочитав префикс : вектор из неотрицательных чисел с суммой 1.
Все модели этой библиотеки — GPT-1, GPT-2, LLaMA, Mistral, Mixtral, Gemma — именно такие: causal language models, «причинные» языковые модели, которые видят только прошлое.
Первый токен в библиотеке отдельно не моделируется: модель получает на вход готовую последовательность и предсказывает токены с позиции 1. Если нужно моделировать и , в начало добавляют специальный токен <bos> (begin of sequence), и тогда . Специальные токены описаны в tokenization.md.
Альтернативы. Авторегрессия — не единственный способ. BERT (Devlin et al., 2018) обучается как маскированная языковая модель: восстанавливает закрытые токены по контексту с обеих сторон. Такая модель хорошо понимает текст, но не задаёт вероятность последовательности через цепное правило и не умеет естественно порождать текст слева направо. В этом пособии речь только об авторегрессивных моделях.
Следующий токен как классификация на V классов
Заголовок раздела «Следующий токен как классификация на V классов»Предсказание следующего токена — это задача классификации: по контексту выбрать один из классов. Нейросеть не выдаёт вероятности напрямую. Она выдаёт вещественных чисел — логитов (logits), по одному на токен словаря:
где:
- — скрытое состояние на позиции после всех блоков трансформера (строка, — размерность модели,
embed_dim); - — матрица выходной проекции («голова» языковой модели, LM head);
- — смещение (bias); в части моделей его нет;
- — логиты: чем больше , тем «увереннее» модель, что следующий токен — .
В коде это последний nn.Linear(embed_dim, vocab_size) модели, например self._linear в Llama (models/llama/llama.py): logits = self._linear(out). Модель возвращает логиты для всех позиций сразу — тензор формы [batch, seq_len, vocab_size], то есть . Как устроена эта проекция и когда она делит веса с эмбеддингами (weight tying), разбирается в embeddings.md.
Softmax: от логитов к вероятностям
Заголовок раздела «Softmax: от логитов к вероятностям»Логиты могут быть любыми числами, в том числе отрицательными, и в сумме не дают 1. Превращает их в распределение функция softmax:
где:
- — вектор логитов для одной позиции;
- — логит токена ;
- — экспонента: делает каждое слагаемое положительным;
- знаменатель — нормирующая сумма (статистическая сумма, partition function), одна на весь вектор.
Итого .
Свойства softmax.
- Все выходы положительны и в сумме дают 1 — это корректное распределение. Ни один токен не получает вероятность ровно 0.
- Монотонность: больший логит даёт большую вероятность; порядок токенов сохраняется.
- Инвариантность к сдвигу: для любого числа . Важны только разности логитов: .
- Чувствительность к масштабу: если умножить логиты на число больше 1, распределение становится «острее» (ближе к выбору одного максимума), если на число меньше 1 — «площе». На этом основана температура при генерации (generation.md).
- Название: softmax — «мягкий» (дифференцируемый) вариант argmax. При , вся вероятность уходит на максимальный логит (если максимум единственный; при нескольких равных максимумах она делится между ними поровну).
Доказательство инвариантности к сдвигу
Вынесем общий множитель из числителя и знаменателя:
Пример. Словарь из трёх токенов, логиты :
e^z = (7.389, 2.718, 1.000), сумма = 11.107softmax = (0.665, 0.245, 0.090)Разница логитов на 1 означает отношение вероятностей : .
Численная устойчивость: вычитание максимума
Заголовок раздела «Численная устойчивость: вычитание максимума»В float32 экспонента переполняется уже при ( — максимум float32). Логиты такого размера встречаются, и наивная формула даёт inf / inf = nan. Решение следует из свойства 3: вычтем из всех логитов максимум :
Теперь наибольший показатель равен , все экспоненты лежат в , а знаменатель — не меньше 1 (слагаемое для максимума равно ). Переполнения нет, деления на ноль тоже.
import torch
def stable_softmax(z): z = z - z.max(dim=-1, keepdim=True).values # сдвиг не меняет результат e = z.exp() return e / e.sum(dim=-1, keepdim=True)
z = torch.tensor([1000.0, 999.0, 998.0])print(z.exp() / z.exp().sum()) # tensor([nan, nan, nan])print(stable_softmax(z)) # tensor([0.6652, 0.2447, 0.0900]) — как для (2, 1, 0)print(torch.softmax(z, dim=-1)) # то же: torch.softmax вычитает максимум самДля логарифма вероятности используют тот же приём — функцию log-sum-exp:
Так torch.log_softmax и F.cross_entropy считают логарифм вероятности, не вычисляя саму вероятность: даже если (в float32 это 0, и ), логарифм получается конечным.
Функция потерь: cross-entropy
Заголовок раздела «Функция потерь: cross-entropy»Максимальное правдоподобие
Заголовок раздела «Максимальное правдоподобие»Пусть есть обучающий корпус — последовательности токенов. Естественное требование к параметрам: модель должна давать наблюдаемым текстам как можно большую вероятность. Это метод максимального правдоподобия (maximum likelihood estimation, MLE):
где — искомые параметры, внешнее произведение берётся по всем текстам корпуса (они считаются независимыми), внутреннее — по позициям внутри текста; и — токены этого текста.
Логарифм — монотонная функция, поэтому максимум произведения достигается там же, где максимум суммы логарифмов. Поменяем знак, чтобы получить задачу минимизации, и усредним по числу предсказаний . Получаем отрицательное логарифмическое правдоподобие (negative log-likelihood, NLL):
где:
- — число позиций, для которых считается loss (в батче минус игнорируемые, см. ниже);
- — правильный следующий токен в -м предсказании (метка, label);
- — вероятность, которую модель дала правильному токену;
- логарифм натуральный, поэтому loss измеряется в натах (nats); чтобы перевести в биты, делят на .
Каждое слагаемое — «штраф за удивление»: если модель дала правильному токену вероятность 1, штраф 0; если 0.5 — штраф ; если 0.001 — штраф . Уверенная ошибка стоит очень дорого: при штраф уходит в бесконечность.
Почему «cross-entropy»
Заголовок раздела «Почему «cross-entropy»»Для одной позиции запишем правильный ответ как one-hot вектор : единица на месте правильного токена, остальные нули. Тогда
где:
- — распределение модели;
- — перекрёстная энтропия (cross-entropy) распределения относительно (буква здесь и в следующем разделе — энтропия, а не число голов attention из notation.md).
В сумме выживает только слагаемое с , поэтому cross-entropy с one-hot целью и NLL — одно и то же. В PyTorch это функция F.cross_entropy(logits, targets): она принимает логиты (не вероятности), сама применяет log-softmax устойчивым способом и берёт элемент правильного класса.
Пример. Логиты , правильный токен — 0. Вероятность , loss . Если бы правильным был токен 2: . Разница ровно 2 — это разница логитов, как и должно быть по формуле log-softmax.
Связь с KL-дивергенцией
Заголовок раздела «Связь с KL-дивергенцией»Пусть у языка есть «истинное» распределение следующего токена (для данного контекста), а модель выдаёт . Ожидаемый loss по данным — это cross-entropy , и она раскладывается так:
где:
- — энтропия истинного распределения: неустранимая неопределённость самого языка (после «Я пошёл в» возможны десятки продолжений);
- — дивергенция Кульбака — Лейблера: насколько модель отличается от истины; она неотрицательна и равна нулю только при .
Вывод
Прибавим и вычтем :
Неотрицательность KL (неравенство Гиббса) следует из неравенства при :
(суммы по тем , где ; равенство — только при для всех ).
Интуиция. от модели не зависит, поэтому минимизировать cross-entropy — то же самое, что минимизировать KL-дивергенцию до истинного распределения. Loss никогда не опустится ниже энтропии языка. Если на валидации loss перестал падать, это может значить, что модель упёрлась либо в свою ёмкость, либо в .
Градиент по логитам
Заголовок раздела «Градиент по логитам»Для одной позиции производная loss по логитам получается очень простой:
где , — one-hot правильного токена. Логит правильного токена тянется вверх с силой , логиты остальных — вниз пропорционально их вероятностям. В примере с логитами и правильным токеном 0 градиент равен . Вывод этой формулы и то, как градиент идёт дальше по сети, — в training.md.
Перплексия
Заголовок раздела «Перплексия»Loss в натах трудно интерпретировать. Удобнее перплексия (perplexity, PPL) — экспонента от среднего NLL:
где — средний loss в натах на токен, — число предсказаний. Последнее выражение — величина, обратная среднему геометрическому вероятностей правильных токенов.
Интерпретация. Перплексия — «эффективное число вариантов», между которыми модель в среднем колеблется на каждом шаге. PPL = 10 означает, что модель удивлена так же, как если бы на каждом шаге равновероятно выбирала из 10 токенов. Меньше — лучше; минимум 1 (модель всегда уверена и права).
Пример 1: равномерное распределение. Необученная модель, которая всем токенам даёт вероятность :
Отсюда полезная проверка: начальный loss свежей модели должен быть около . Логиты свежей модели с малыми весами близки к нулю, а по свойству 3 softmax одинаковых логитов — равномерное распределение. Для словаря GPT-2 () это ; для учебного токенизатора из experiments/llm_only на встроенном корпусе () — . Свежие GPT, GPT2 и Llama с таким словарём дают на старте 6.04–6.29. Если начальный loss намного больше , модель на старте уверенно ошибается — обычно из-за слишком крупной инициализации (см. gpt.md).
Пример 2. Модель дала правильным токенам вероятности :
-ln p = 0.693, 1.386, 2.079, 0.693 среднее L = 1.213 натаPPL = e^1.213 = 3.36 (= 2^1.75: вероятности — степени двойки)Тонкости. Перплексия зависит от токенизатора: у модели с более крупными токенами меньше предсказаний на тот же текст, и сравнивать PPL моделей с разными словарями напрямую нельзя. Для такого сравнения loss нормируют на символ или байт текста (bits per character, bits per byte). Кроме того, PPL — среднее по позициям, и оно зависит от того, какие позиции попали в усреднение (см. про паддинг ниже).
Обучение: teacher forcing и сдвиг меток
Заголовок раздела «Обучение: teacher forcing и сдвиг меток»Teacher forcing
Заголовок раздела «Teacher forcing»При генерации модель на каждом шаге получает свой же предыдущий выход. При обучении так не делают: модели всегда подают истинный префикс из корпуса и спрашивают следующий токен. Этот приём называется teacher forcing («форсирование учителем»; термин восходит к Williams & Zipser, 1989).
Плюсы: предсказания всех позиций независимы при известном тексте, поэтому их можно считать одновременно, за один прямой проход, — трансформер как раз так и устроен. Минус — exposure bias: при обучении модель не видит собственных ошибок, а при генерации ошибки накапливаются (Bengio et al., 2015). Для больших моделей этот эффект на практике обычно терпим, и teacher forcing остаётся стандартом.
Сдвиг на один токен
Заголовок раздела «Сдвиг на один токен»Модель читает последовательность и на каждой позиции выдаёт логиты — прогноз токена . Значит, вход и цель — одна и та же последовательность со сдвигом на один:
позиция t 0 1 2 3 4 5вход x_t Мир ␣программирования ␣прекрасен ␣и ␣удивителен .логиты z_t → цель ␣програм… ␣прекрасен ␣и ␣удив… . (нет цели)(токены — реальный результат BPETokenizer из experiments/llm_only для первой строки учебного корпуса; ␣ — пробел, который токенизатор прикрепляет к началу слова.) Из позиций получается обучающих примеров: логиты последней позиции не с чем сравнить.
В репозитории датасеты возвращают labels, совпадающие с input_ids (labels = input_ids.clone() в TextDataset, StreamingTextDataset, TextWithSpecialTokensDataset — datasets/), а сдвиг делает Trainer.compute_lm_loss (training/trainer.py):
shift_logits = logits[..., :-1, :].contiguous() # [B, T-1, V]: прогнозы позиций 0 … T-2shift_labels = labels[..., 1:].contiguous() # [B, T-1]: токены позиций 1 … T-1loss = F.cross_entropy( shift_logits.view(-1, shift_logits.size(-1)), # [B·(T-1), V] shift_labels.view(-1), # [B·(T-1)] ignore_index=-100,)Это та же договорённость, что в HuggingFace (labels равны входу, сдвиг внутри модели или loss). Формула, которую реализует этот код:
где:
- — номер последовательности в батче (), — позиция;
- — логиты позиции последовательности (элемент тензора
logitsформы[B, T, V]); - — правильный следующий токен (элемент
labels[b, t+1]); - — множество учитываемых позиций, — их число: усреднение идёт только по ним (
reduction="mean"по умолчанию).
Проверить, что это работает, можно на свежей модели: loss должен быть около .
import torchimport torch.nn.functional as Ffrom llm.models.gpt import GPT
V = 426model = GPT({"vocab_size": V, "embed_dim": 256, "num_heads": 4, "num_layers": 4, "max_position_embeddings": 128, "dropout": 0.0})input_ids = torch.randint(0, V, (2, 16)) # [B, T]labels = input_ids.clone() # как в TextDatasetlogits, _ = model(input_ids) # [B, T, V]; все модели возвращают (logits, cache)loss = F.cross_entropy(logits[:, :-1].reshape(-1, V), labels[:, 1:].reshape(-1), ignore_index=-100)print(loss.item(), torch.log(torch.tensor(float(V))).item()) # около 6.1–6.3 и 6.05print(loss.exp().item()) # перплексия порядка V (сотни)Метка -100: игнорируемые позиции
Заголовок раздела «Метка -100: игнорируемые позиции»Не все позиции нужно учить. Если короткую последовательность дополнили pad-токенами до длины батча, предсказывать «после паддинга снова паддинг» бессмысленно — это только размывает loss. Для таких позиций в labels ставят -100: это ignore_index в F.cross_entropy (и его значение по умолчанию). Позиции с меткой -100 не входят ни в сумму, ни в знаменатель среднего и не дают градиента. Так же -100 используют, чтобы не учить модель на промпте при дообучении на инструкциях, — loss считают только по ответу.
В репозитории метки -100 на паддинге ставят датасеты llm/datasets (функция lm_example: input_ids дополняются pad_token_id, labels — значением -100) и коллатор hf-proxy (pad в HFTokenizerAdapter). Подробнее — в разделе «Типичные ошибки и тонкости» ниже и в training.md.
Почему нужна causal-маска
Заголовок раздела «Почему нужна causal-маска»При teacher forcing все позиций обрабатываются одним проходом, и модель видит весь вход целиком — в том числе , который она должна предсказать на позиции . Без ограничений задача вырождается: достаточно скопировать следующий входной токен, loss быстро падает почти до нуля, но при генерации, где будущего нет, модель бесполезна.
Поэтому внимание на позиции разрешено только к позициям . Это causal-маска (причинная маска): перед softmax в attention к оценкам прибавляется матрица
где — позиция запроса (кто смотрит), — позиция ключа (на кого смотрят). После softmax , и веса «будущих» позиций обнуляются. Для (1 — можно смотреть, 0 — нельзя):
j=0 j=1 j=2 j=3i=0 1 0 0 0i=1 1 1 0 0i=2 1 1 1 0i=3 1 1 1 1С маской выход на позиции зависит только от — ровно то, что требует . Один проход по последовательности длины эквивалентен отдельным проходам по префиксам, но гораздо дешевле. В коде маска — буфер _tril_mask = torch.tril(...) в модулях attention и masked_fill(~mask, float("-inf")) перед softmax (MultiHeadAttention в core/multi_head_attention.py). Остальные виды масок — в masks.md, сам attention — в attention.md.
Общая схема decoder-only трансформера
Заголовок раздела «Общая схема decoder-only трансформера»Все шесть моделей библиотеки — decoder-only трансформеры: стопка одинаковых блоков с causal-вниманием, без энкодера. Схема общая, модели различаются начинкой блоков.
%%{init: {"flowchart": {"rankSpacing": 28, "nodeSpacing": 28}}}%%
flowchart TB
Ids(["token ids · [B, T]"]):::io --> Emb["Token Embedding<br/>[B, T, d]"]:::blue
Emb --> Pos["+ позиционная информация<br/>(GPT: обучаемые; LLaMA и др.: RoPE внутри attention)"]:::purple
subgraph Dec["Блок декодера × L"]
direction TB
X(["h"]):::io --> N1["Norm"]:::gray
N1 --> Attn["Causal Self-Attention"]:::blue
Attn --> A1(("+")):::add
X -. residual .-> A1
A1 --> N2["Norm"]:::gray
N2 --> FFN["Feed-Forward (FFN / MoE)"]:::purple
FFN --> A2(("+")):::add
A1 -. residual .-> A2
end
Pos --> Dec
Dec --> FN["Финальная Norm"]:::gray
FN --> Lin["Linear d → V"]:::gray
Lin --> Out(["logits · [B, T, V]"]):::io
Out -. "обучение: cross-entropy со сдвигом" .-> Loss(["loss"]):::io
Out -. "генерация: softmax → выбор токена" .-> Next(["следующий токен"]):::io
style Dec fill:transparent,stroke:#82b366,stroke-width:2px,color:#5b9a3c
classDef io fill:#ffffff,stroke:#999999,color:#1a1a1a;
classDef add fill:#ffffff,stroke:#666666,color:#1a1a1a;
classDef blue fill:#dae8fc,stroke:#6c8ebf,color:#1a1a1a;
classDef purple fill:#e1d5e7,stroke:#9673a6,color:#1a1a1a;
classDef gray fill:#f5f5f5,stroke:#666666,color:#1a1a1a;
Здесь показан pre-LN блок (нормализация перед подслоем), как в GPT-2, LLaMA, Mistral, Mixtral и Gemma. GPT-1 использует post-LN (нормализация после сложения) и не имеет финальной нормализации — см. gpt.md и normalization.md.
Пошагово, с формами тензоров:
- Токены
input_idsформы — целые числа из словаря (tokenization.md). - Эмбеддинги: каждый индекс заменяется строкой обучаемой матрицы (здесь — матрица эмбеддингов, а не число экспертов MoE); получаем (embeddings.md).
- Позиции: attention сам по себе не знает порядка токенов, его нужно сообщить — сложением с позиционными эмбеддингами (GPT, GPT-2) или поворотом Q и K (RoPE: LLaMA, Mistral, Mixtral, Gemma) (positional-encoding.md).
- блоков декодера. В каждом два подслоя с residual-связями:
- causal self-attention смешивает информацию между позициями (только из прошлого) (attention.md, masks.md);
- FFN обрабатывает каждую позицию отдельно — нелинейное преобразование «внутри токена» (feed-forward.md); в Mixtral вместо него — смесь экспертов (mixture-of-experts.md).
- Финальная нормализация (normalization.md).
- Проекция на словарь — логиты для каждой позиции.
- Дальше — либо loss (эта глава и training.md), либо выбор следующего токена (generation.md).
Residual-поток как «шина»
Заголовок раздела «Residual-поток как «шина»»Pre-LN блок записывается так:
где:
- — вход блока (матрица состояний всех позиций одной последовательности, строка — вектор ; для батча добавляется измерение );
- — LayerNorm или RMSNorm, применяется к каждой строке отдельно;
- — подслои; их выходы той же формы ;
- — состояние после подслоя attention, — выход блока, вход следующего.
Обозначим через выход блока . Развернув рекурсию по всем блокам, получаем, что финальное состояние — это сумма эмбеддинга и вкладов всех подслоёв:
где — эмбеддинги (с позициями), — состояние после последнего блока, — состояние внутри блока после подслоя attention, и — подслои блока .
Интуиция. Удобно представлять строку матрицы как общую шину (residual stream) шириной на позиции : каждый подслой читает с шины (через нормализацию), что-то вычисляет и дописывает результат обратно сложением, ничего не стирая (Elhage et al., 2021). Attention переносит информацию по шине между позициями, FFN перерабатывает её внутри позиции. Голова LM в конце читает с шины прогноз следующего токена.
Residual-связи (He et al., 2015) важны и для обучения: производная по содержит единичное слагаемое (якобиан равен ), и градиент доходит до нижних слоёв напрямую, не затухая при перемножении десятков матриц. Если убрать residual-связи, глубокий трансформер почти не обучается.
Краткая история
Заголовок раздела «Краткая история»| Год | Подход | Идея | Ограничение |
|---|---|---|---|
| 1948 | n-граммы (Shannon, 1948) | : учитываются только предыдущих токенов, вероятности — частоты в корпусе | контекст в несколько слов; число n-грамм растёт как , большинство не встречается в корпусе — нужно сглаживание (Kneser & Ney, 1995) |
| 2003 | нейросетевая LM (Bengio et al., 2003) | контекст фиксированной длины → эмбеддинги слов → MLP → softmax по словарю | контекст по-прежнему фиксированный |
| 2010 | RNN LM (Mikolov et al., 2010); LSTM (Hochreiter & Schmidhuber, 1997) | скрытое состояние переносит информацию через всю последовательность; LSTM с гейтами борется с затуханием градиента | обработка строго последовательная — плохо параллелится; дальние зависимости всё равно даются тяжело |
| 2014 | attention в переводе (Bahdanau et al., 2014) | декодер на каждом шаге взвешенно смотрит на все состояния энкодера | всё ещё поверх RNN |
| 2017 | трансформер (Vaswani et al., 2017) | только attention и FFN, без рекуррентности; обучение параллельно по всем позициям | квадратичная по стоимость attention |
| 2018 | decoder-only LM: Liu et al., 2018, GPT-1 (Radford et al., 2018) | от трансформера остаётся только декодер с causal-маской; предобучение на большом корпусе + дообучение на задачах | — |
Трансформер Vaswani et al. был encoder-decoder моделью для перевода. Liu et al. (2018) показали, что для порождения текста достаточно одного декодера, а GPT-1 применил такую модель как универсальную предобученную основу. Дальнейшая линия — GPT-2 (масштаб и pre-LN), LLaMA (RoPE, RMSNorm, SwiGLU), Mistral (GQA, скользящее окно), Mixtral (MoE), Gemma (MQA, GeGLU) — это изменения внутри той же схемы decoder-only.
Модели репозитория
Заголовок раздела «Модели репозитория»| Модель | Год | Класс | Attention | Позиции | Норма | FFN | Словарь оригинала |
|---|---|---|---|---|---|---|---|
| GPT-1 | 2018 | llm.models.gpt.GPT | MHA | обучаемые | LayerNorm, post-LN | GELU | BPE, 40 000 слияний, |
| GPT-2 | 2019 | llm.models.gpt.GPT2 | MHA | обучаемые | LayerNorm, pre-LN + финальная | GELU | byte-level BPE, |
| LLaMA | 2023 | llm.models.llama.Llama | MHA | RoPE | RMSNorm | SwiGLU | SentencePiece BPE, |
| Mistral | 2023 | llm.models.mistral.Mistral | GQA + скользящее окно | RoPE | RMSNorm | SwiGLU | SentencePiece, |
| Mixtral | 2023 | llm.models.mixtral.Mixtral | GQA (окно — опционально) | RoPE | RMSNorm | MoE из SwiGLU | SentencePiece, |
| Gemma | 2024 | llm.models.gemma.Gemma | MQA (или GQA/MHA) | RoPE | RMSNorm | GeGLU | SentencePiece, |
Все модели наследуют BaseModel (core/base_model.py): forward принимает x и необязательные use_cache=False, cache=None, attention_mask=None и возвращает кортеж (logits, cache), а метод generate общий. (У GPT порядок позиционных аргументов другой — forward(x, attention_mask, use_cache, cache), поэтому их надёжнее передавать по имени.) В экспериментах experiments/llm_only все модели обучаются с одним и тем же учебным BPE-токенизатором (BPETokenizer), а vocab_size модели берётся из токенизатора — словари оригинальных моделей используются только при загрузке весов HuggingFace.
Карта пособия
Заголовок раздела «Карта пособия»Часть I — механизмы, из которых собраны модели:
| Глава | О чём |
|---|---|
| Языковое моделирование (эта глава) | задача, цепное правило, cross-entropy, перплексия, общая схема |
| Токенизация | как текст превращается в индексы; BPE; llm/tokenizers |
| Эмбеддинги | индексы → векторы; weight tying; выходная проекция и логиты |
| Позиционное кодирование | обучаемые, синусоидальные, RoPE |
| Attention | scaled dot-product, multi-head, GQA/MQA, скользящее окно, KV-кэш |
| Маски | causal, скользящее окно, паддинг |
| Нормализация | LayerNorm, RMSNorm, pre-LN и post-LN |
| Feed-forward | FFN, GELU, SiLU, SwiGLU, GeGLU |
| Mixture-of-Experts | роутинг top-k, load-balancing loss |
| Обучение | градиент loss, AdamW, расписание lr, clipping, инициализация, Trainer |
| Генерация | greedy, температура, top-k, top-p, KV-кэш |
Часть II — сами архитектуры по порядку появления: GPT-1 → GPT-2 → LLaMA → Mistral → Mixtral, и параллельная ветка Gemma. Если нужно быстро понять одну модель, можно начать с её главы: она ссылается на нужные разделы части I.
Типичные ошибки и тонкости
Заголовок раздела «Типичные ошибки и тонкости»- Softmax перед
F.cross_entropy. Функция ждёт логиты и сама применяет log-softmax. Если подать вероятности, softmax применится дважды: loss будет считаться неверно, а обучение резко замедлится. - Забытый сдвиг. Если сравнивать
logits[:, t]сlabels[:, t], модель учится копировать текущий вход, а не предсказывать следующий. Loss быстро падает, генерация бессмысленна. В репозитории сдвиг делаетTrainer.compute_lm_loss; при собственном цикле обучения его нужно сделать самому. - Двойной сдвиг. Обратная ошибка: сдвинуть метки в датасете и использовать
Trainer, который сдвигает ещё раз. Тогда модель учится предсказывать токен через один. - Паддинг в loss. Если дополнить
labelsзначениемpad_token_id, а не-100, pad-позиции входят в loss. Вexperiments/llm_onlyкаждая строка корпуса — отдельный пример, дополненный доmax_position_embeddings = 128токенов, а строки короткие (первая — 6 токенов), и большая часть позиций в loss оказалась бы предсказанием паддинга после паддинга. Модель быстро учит это тривиальное правило, и средний loss получается заниженным относительно качества на настоящем тексте. Корректный вариант —-100на pad-позициях (так делают датасетыllm/datasetsи коллатор hf-proxy) или склейка текстов в непрерывный поток, нарезанный на куски длины без паддинга (так готовят данные для предобучения). - Первый токен не моделируется. Loss считается по позициям; не предсказывается, если в начало не добавлен
<bos>. - Перплексия с разными токенизаторами несравнима — см. раздел «Перплексия».
- Натуральный логарифм.
F.cross_entropyвозвращает наты; перплексия —exp(loss), а не2 ** loss.
- Языковая модель задаёт вероятность последовательности; по цепному правилу она раскладывается в произведение вероятностей следующего токена.
- Предсказание следующего токена — классификация на классов: логиты softmax. Softmax инвариантен к сдвигу, поэтому его считают с вычитанием максимума.
- Функция потерь — средний правильного токена (cross-entropy = NLL); её минимизация — это максимальное правдоподобие и минимизация KL до истинного распределения.
- Перплексия — эффективное число вариантов на шаг; у равномерной модели , начальный loss .
- Teacher forcing + causal-маска позволяют обучать все позиции за один проход; метки сдвигаются на один токен, позиции с меткой
-100игнорируются. - Все модели репозитория — decoder-only: эмбеддинги → блоков (attention + FFN с residual и нормализацией) → финальная норма → проекция на словарь.
Вопросы и упражнения
Заголовок раздела «Вопросы и упражнения»- Запишите цепное правило для последовательности из четырёх токенов. Какие множители меняются, если заменить последний токен?
Ответ
. Замена меняет только последний множитель: он единственный, где встречается.
- Вычислите softmax логитов . Что изменится, если к обоим логитам прибавить 100?
Ответ
, , сумма 4, softmax . Прибавление 100 ничего не меняет (инвариантность к сдвигу), но наивный расчёт в float32 даст переполнение: . Устойчивая версия вычтет максимум и получит те же .
- Модель дала правильным токенам вероятности . Найдите средний loss и перплексию.
Ответ
, , ; среднее . — то же, что .
- Какой начальный loss и какую перплексию ожидать у свежей модели со словарём GPT-2 ()? Сколько это в битах на токен?
Ответ
ната, ; в битах .
- Батч из последовательностей длины . Сколько слагаемых в loss
Trainer.compute_lm_loss, если меток-100нет? А если в каждой последовательности последние 28 позиций помечены-100?
Ответ
Без -100: . С -100 на позициях 100…127: метки сдвинутой последовательности — позиции 1…127, из них игнорируются 28, остаётся 99 на последовательность, всего . Среднее берётся по 792 позициям.
- Объясните, почему без causal-маски loss при обучении быстро падает почти до нуля, а генерация не работает.
Ответ
Без маски на позиции attention видит вход — ровно то, что нужно предсказать. Модель учится копировать его с соседней позиции, это легко, и loss близок к нулю. При генерации будущего токена во входе нет, и выученное правило ничего не даёт.
- Докажите, что , и объясните, что это значит для loss на реальных данных.
Ответ
, а (вывод — в разделе «Связь с KL-дивергенцией»). Значит, loss на данных не может быть ниже энтропии самого языка: даже идеальная модель не угадывает следующий токен наверняка, если продолжений несколько.
- (Код.) Возьмите
TextDatasetизllm.datasets.text_datasetи токенизатор, обученный на паре предложений, сblock_size=32. Посчитайте, какая доля позиций вlabelsравна-100. Заменитеlabelsнаinput_ids(паддинг войдёт в loss) и сравните loss необученной модели в обоих случаях.
Литература
Заголовок раздела «Литература»- Radford, Narasimhan, Salimans, Sutskever. Improving Language Understanding by Generative Pre-Training. OpenAI, 2018. PDF
- Vaswani et al. Attention Is All You Need. 2017. arXiv:1706.03762
- Liu et al. Generating Wikipedia by Summarizing Long Sequences. 2018. arXiv:1801.10198
- Devlin, Chang, Lee, Toutanova. BERT: Pre-training of Deep Bidirectional Transformers for Language Understanding. 2018. arXiv:1810.04805
- Shannon. A Mathematical Theory of Communication. Bell System Technical Journal, 1948. PDF
- Kneser, Ney. Improved Backing-off for M-gram Language Modeling. ICASSP, 1995. doi:10.1109/ICASSP.1995.479394
- Bengio, Ducharme, Vincent, Jauvin. A Neural Probabilistic Language Model. JMLR 3, 2003. JMLR
- Hochreiter, Schmidhuber. Long Short-Term Memory. Neural Computation 9(8), 1997. doi:10.1162/neco.1997.9.8.1735
- Mikolov, Karafiát, Burget, Černocký, Khudanpur. Recurrent Neural Network Based Language Model. Interspeech, 2010. ISCA
- Bahdanau, Cho, Bengio. Neural Machine Translation by Jointly Learning to Align and Translate. 2014. arXiv:1409.0473
- Williams, Zipser. A Learning Algorithm for Continually Running Fully Recurrent Neural Networks. Neural Computation 1(2), 1989. doi:10.1162/neco.1989.1.2.270
- Bengio, Vinyals, Jaitly, Shazeer. Scheduled Sampling for Sequence Prediction with Recurrent Neural Networks. 2015. arXiv:1506.03099
- He, Zhang, Ren, Sun. Deep Residual Learning for Image Recognition. 2015. arXiv:1512.03385
- Elhage et al. A Mathematical Framework for Transformer Circuits. Anthropic, 2021. transformer-circuits.pub