Zero to Hero, лекция 7: GPT с нуля, в коде, по шагам
Подробный конспект лекции «Let's build GPT»: от биграммной модели на «Крошечном Шекспире» через «математический трюк» с усреднением к self-attention, блокам трансформера и модели на 10 млн параметров — и путь от неё к ChatGPT.
Коротко
Заголовок раздела «Коротко»- Цель лекции — показать, что находится «под капотом» ChatGPT: языковая модель на трансформере из статьи «Attention Is All You Need» (2017). Вместо интернета берётся «Крошечный Шекспир» (около 1 МБ текста), вместо подслов — отдельные символы.
- Сначала строится базовая линия — биграммная модель (val ≈ 2.5). Затем «математический трюк»: усреднение прошлого можно записать как умножение на нижнетреугольную матрицу. Из этого трюка естественно вырастает self-attentionсамовниманиеEN self-attention.
- Внимание Карпатый объясняет как механизм общения токенов. Каждый токен выпускает запрос, ключ и значение; веса — скалярные произведения запросов и ключей; маска запрещает смотреть в будущее; деление на не даёт softmax стать слишком «острым».
- Трансформер собирается по кусочку, и вклад каждого кусочка виден по loss на валидации: одна голова 2.4 → несколько голов 2.28 → FFN 2.24 → остаточные связи 2.08 → LayerNorm 2.06 → увеличенная модель на 10 млн параметров 1.48.
- Получается decoder-only трансформер, как в GPT. В конце — путь от такой модели к ChatGPT: претрейн на интернете, затем SFT, модель вознаграждения и PPO.
Как смотреть вместе с курсом
Заголовок раздела «Как смотреть вместе с курсом»Карпатый предупреждает: лекция опирается на серию makemore — языковое моделирование и основы PyTorch считаются известными. В курсе это модуль 4.1 и раздел 3.3.
Лучший порядок — сначала модуль внимания 4.4 и урок 4.5.1 «Блок трансформера», потом лекция. Она закрепляет пройденное и даёт другую интуицию: внимание как общение узлов в графе. Урок 4.6.2 «GPT на PyTorch» собирает ту же модель в стиле nanoGPT на русском корпусе и на CPU — это идеальная пара к лекции. Можно и наоборот: сначала посмотреть лекцию как интуитивное введение, потом пройти уроки.
Два часа удобно разбить на три захода: данные и биграммная модель (0:00–42:13), трюк и внимание (42:13–1:19:11), сборка трансформера и выводы (1:19:11–конец).
Код лекции — репозиторий karpathy/ng-video-lecture (MIT): bigram.py
и gpt.py. Его история коммитов повторяет лекцию шаг за шагом, и в сообщениях коммитов записан loss на валидации.
Ноутбук из видео — в Google Colab.
По-китайски — ноутбук 007_GPT в учебной версии курса на китайском
(karpathy-zero-to-hero-zh-transcripts).
Карта лекции
Заголовок раздела «Карта лекции»Таймкоды и названия глав — из описания видео.
| Время | Глава в оригинале | О чём |
|---|---|---|
| 0:00 | intro: ChatGPT, Transformers, nanoGPT, Shakespeare | что строим и зачем |
| 7:52 | reading and exploring the data | «Крошечный Шекспир», словарь из 65 символов |
| 9:28 | tokenization, train/val split | посимвольный токенизатор, 90/10 |
| 14:27 | data loader: batches of chunks of data | окна длины block_size, батчи |
| 22:11 | simplest baseline: bigram language model, loss, generation | биграммная модель на nn.Embedding |
| 34:53 | training the bigram model | AdamW, loss ≈ 2.5 |
| 38:00 | port our code to a script | bigram.py: GPU, оценка loss |
| 42:13 | version 1: averaging past context with for loops | «мешок слов» циклами |
| 47:11 | the trick in self-attention: matrix multiply as weighted aggregation | трюк с треугольной матрицей |
| 51:54 | version 2: using matrix multiply | усреднение умножением |
| 54:42 | version 3: adding softmax | маска и softmax |
| 58:26 | minor code cleanup | n_embd, lm_head |
| 1:00:18 | positional encoding | таблица позиционных эмбеддингов |
| 1:02:00 | THE CRUX OF THE VIDEO: version 4: self-attention | запросы, ключи, значения |
| 1:11:38 | note 1: attention as communication | внимание как общение в графе |
| 1:12:46 | note 2: attention has no notion of space | внимание работает над множеством |
| 1:13:40 | note 3: no communication across batch dimension | примеры батча не общаются |
| 1:14:14 | note 4: encoder blocks vs. decoder blocks | маска есть или нет |
| 1:15:39 | note 5: attention vs. self-attention vs. cross-attention | откуда берутся ключи и значения |
| 1:16:56 | note 6: “scaled” self-attention | зачем делить на |
| 1:19:11 | inserting a single self-attention block | одна голова: 2.4 |
| 1:21:59 | multi-headed self-attention | 4 головы: 2.28 |
| 1:24:25 | feedforward layers of transformer block | FFN: 2.24 |
| 1:26:48 | residual connections | остаточные связи: 2.08 |
| 1:32:51 | layernorm | LayerNorm и pre-norm: 2.06 |
| 1:37:49 | scaling up the model! adding dropout | 10 млн параметров: 1.48 |
| 1:42:39 | encoder vs. decoder vs. both (?) Transformers | почему у нас только декодер |
| 1:46:22 | super quick walkthrough of nanoGPT | головы как ещё одна ось батча |
| 1:48:53 | back to ChatGPT, GPT-3, pretraining vs. finetuning, RLHF | от GPT к ChatGPT |
| 1:54:32 | conclusions | итоги |
Часть 1. Данные и постановка задачи
Заголовок раздела «Часть 1. Данные и постановка задачи»Что такое ChatGPT с точки зрения модели
Заголовок раздела «Что такое ChatGPT с точки зрения модели»Лекция начинается с ChatGPT. Модель пишет ответ слева направо, а на один и тот же запрос отвечает по-разному: это вероятностная система. По сути это языковая модель: она продолжает последовательность символов, слов или, в общем случае, токенов. Нейросеть внутри — трансформер. Буква T в GPT (Generatively Pretrained Transformer) означает именно его. Статья 2017 года выглядела как обычная работа о машинном переводе, но за пять лет её архитектура с небольшими изменениями разошлась почти по всему ИИ.
Повторить ChatGPT в лекции нельзя, зато можно обучить небольшую языковую модель на трансформере и понять принцип. Готовая версия кода — репозиторий nanoGPT: два файла примерно по 300 строк, модель и обучение. Карпатый показывает, что nanoGPT воспроизводит самую маленькую GPT-2 (124 млн параметров) на датасете OpenWebText. В лекции этот код пишется с пустого файла.
«Крошечный Шекспир» и посимвольный токенизатор
Заголовок раздела ««Крошечный Шекспир» и посимвольный токенизатор»Датасет — все пьесы Шекспира в одном файле, около миллиона символов. Уникальных символов 65: пробел, перевод строки, знаки препинания, заглавные и строчные буквы. Это и есть словарь.
Токенизация здесь простейшая: каждый символ — номер в словаре. Функция encode переводит строку в список чисел,
decode — обратно: encode("hii there") даёт [46, 47, 47, 1, 58, 46, 43, 56, 43]. Карпатый сразу показывает
альтернативы. SentencePiece от Google режет текст на подслова. Библиотека tiktoken от OpenAI использует BPE: у GPT-2
около 50 тыс. токенов, и та же строка становится тремя числами. Это компромисс: маленький словарь даёт длинные
последовательности, большой — короткие. Подробно о нём — в уроке 4.2.1,
а самой токенизации посвящена лекция 8.
Весь текст превращается в один длинный тензор чисел. Первые 90% идут на обучение, последние 10% — на валидацию: нужна модель, которая пишет «как Шекспир», а не заучивает его наизусть.
Окна и батчи
Заголовок раздела «Окна и батчи»Целиком текст в трансформер не подаётся — это слишком дорого. Обучение идёт на случайных кусках длиной до
block_size (в других местах это называют длиной контекста). Важная деталь: в куске из 9 символов спрятано
8 примеров. После первого символа должен идти второй, после первых двух — третий, и так далее.
Учить сразу на всех длинах контекста, от 1 до block_size, полезно не только ради скорости. Так модель привыкает
предсказывать и по одному символу, и по полному окну — а генерация как раз начинается с одного символа. Дальше
block_size контекст приходится обрезать: больше модель никогда не видит.
Вторая ось — батч. Несколько кусков складываются в один тензор, чтобы загрузить видеокарту, но друг с другом они
не взаимодействуют. Батч 4×8 — это 32 независимых примера. Функция из gpt.py (MIT, комментарии переведены):
def get_batch(split): # небольшой батч входов x и целей y data = train_data if split == 'train' else val_data ix = torch.randint(len(data) - block_size, (batch_size,)) # случайные начала окон x = torch.stack([data[i:i+block_size] for i in ix]) # (B, T) y = torch.stack([data[i+1:i+block_size+1] for i in ix]) # то же окно, сдвинутое на 1 x, y = x.to(device), y.to(device) return x, yЧасть 2. Биграммная модель — базовая линия
Заголовок раздела «Часть 2. Биграммная модель — базовая линия»Простейшая нейросетевая языковая модель — биграммнаябиграммаEN bigramПара соседних токенов. Биграммная модель оценивает P(x_t | x_{t−1}) — таблица V × V.: следующий символ угадывается только
по текущему. В PyTorch это одна таблица nn.Embedding(vocab_size, vocab_size). Номер символа выбирает строку,
и эта строка из 65 чисел сразу служит логитами следующего символа. На выходе — тензор формы (B, T, C) с C = 65.
Loss. F.cross_entropy ждёт логиты формы (N, C), поэтому их приходится вытянуть в (B·T, C), а цели — в (B·T).
До обучения loss оказался 4.87. Ожидание для случайной модели — : все 65 символов
равновероятны. Значит, начальные предсказания не совсем равномерные. Эта проверка есть и в курсе:
урок 3.3.3, «loss на старте ≈ ln C».
Генерация. Функция generate берёт логиты только последней позиции, превращает их в вероятности через softmax,
выбирает символ torch.multinomial и дописывает его к последовательности. Старт — символ с номером 0, перевод строки.
Для биграммы подавать в модель весь контекст бессмысленно: нужен только последний символ. Но Карпатый оставляет
функцию общей, потому что позже модель начнёт смотреть в историю.
Обучение. Оптимизатор AdamW. Для больших сетей типичный learning rate — около , но маленькие терпят и больше, здесь взят . При батче 32 после 10 тыс. шагов loss около 2.5. Текст уже похож на «что-то», но не на Шекспира: символы ещё не «разговаривают» друг с другом.
Скрипт bigram.py. Код переезжает из ноутбука в файл, и появляются три привычки, которые останутся до конца
серии:
- автоопределение устройства:
device = 'cuda' if torch.cuda.is_available() else 'cpu', а модель и данные переносятся на него; - функция
estimate_loss: loss усредняется по 200 батчам для train и val, иначе оценка слишком шумная; - оценка идёт под
@torch.no_grad()и в режимеmodel.eval(). Для биграммы режимы не различаются, но так полезно делать всегда — позже появятся dropout и другие слои с разным поведением.
Часть 3. Математический трюк
Заголовок раздела «Часть 3. Математический трюк»Самое простое общение — среднее
Заголовок раздела «Самое простое общение — среднее»Теперь токены должны начать обмениваться информацией, причём только с прошлым: токен на пятой позиции не должен видеть шестую и дальше, ведь их мы как раз и предсказываем. Самый простой способ — усреднить векторы всех предыдущих токенов и текущего. Это «мешок слов»: среднее теряет порядок и почти всё остальное, но для начала годится.
Три версии одного вычисления
Заголовок раздела «Три версии одного вычисления»Наивно среднее считается двумя циклами. Тот же результат даёт одно матричное умножение. Возьмём нижнетреугольную матрицу из единиц и нормируем строки, чтобы сумма в каждой была 1:
Строка произведения — среднее первых строк : , , . Меняя числа в , можно получить любую взвешенную сумму прошлого. Третья версия делает то же через softmax, и именно она станет вниманием:
import torchimport torch.nn.functional as Ftorch.manual_seed(1337)B, T, C = 4, 8, 2x = torch.randn(B, T, C)
# версия 1: циклы — среднее векторов на позициях 0..txbow = torch.zeros(B, T, C)for b in range(B): for t in range(T): xbow[b, t] = x[b, :t + 1].mean(dim=0)
# версия 2: умножение на нормированную нижнетреугольную матрицуwei = torch.tril(torch.ones(T, T))wei = wei / wei.sum(dim=1, keepdim=True)xbow2 = wei @ x # (T, T) @ (B, T, C) -> (B, T, C): умножение по батчу
# версия 3: через softmax — так будет устроено вниманиеtril = torch.tril(torch.ones(T, T))wei = torch.zeros(T, T) # «сродство» токенов: пока все нулиwei = wei.masked_fill(tril == 0, float('-inf')) # будущее недоступноwei = F.softmax(wei, dim=-1) # каждая строка — равномерные веса по прошломуxbow3 = wei @ x
print(torch.allclose(xbow, xbow2, atol=1e-6), torch.allclose(xbow, xbow3, atol=1e-6)) # True TrueПочему третья версия интереснее. Нули в wei — это «сродство» токенов: насколько каждый прошлый токен интересен
текущему. Пока оно у всех одинаковое, но скоро станет зависеть от данных. Строка с -inf говорит: из будущего
ничего не берём. Softmax превращает сродство в веса, сумма которых равна 1. Итог главы: взвешенное усреднение прошлого
— это умножение на нижнетреугольную матрицу весов.
Подготовка: эмбеддинги и позиции
Заголовок раздела «Подготовка: эмбеддинги и позиции»Перед главным шагом две правки. Таблица эмбеддингов теперь выдаёт не логиты, а векторы размера n_embd = 32,
а логиты получает отдельный линейный слой lm_head. Кроме того, добавляется вторая таблица — позиционные
эмбеддинги размера block_size × n_embd. Вход блока — сумма tok_emb + pos_emb: вектор знает и какой это символ,
и где он стоит. Для биграммы позиция пока бесполезна, но вниманию она понадобится.
Часть 4. Self-attention — суть видео
Заголовок раздела «Часть 4. Self-attention — суть видео»Среднее одинаково для всех токенов, а нужно, чтобы веса зависели от содержимого. Например, гласная ищет согласные в своём прошлом и хочет узнать о них побольше. Self-attention решает это так: каждый токен выпускает два вектора.
- Запрос (query) — «что я ищу».
- Ключ (key) — «что у меня есть».
Сродство двух токенов — скалярное произведение запроса одного и ключа другого. Если они «совпали», вес большой,
и токен получит много информации от этого соседа. Третий вектор — значение (value): «что я сообщу, если я тебе
интересен». Карпатый предлагает думать об исходном x как о «личной» информации токена, а о v — как о том,
чем он делится в этой голове.
Одна голова внимания размером head_size = 16:
key = nn.Linear(C, head_size, bias=False)query = nn.Linear(C, head_size, bias=False)value = nn.Linear(C, head_size, bias=False)
k = key(x) # (B, T, 16) — все токены параллельно и независимоq = query(x) # (B, T, 16)wei = q @ k.transpose(-2, -1) * head_size**-0.5 # (B, T, 16) @ (B, 16, T) -> (B, T, T)wei = wei.masked_fill(tril == 0, float('-inf')) # в будущее не смотримwei = F.softmax(wei, dim=-1) # строки — распределения по прошломуout = wei @ value(x) # (B, T, T) @ (B, T, 16) -> (B, T, 16)Теперь у каждого примера батча своя матрица весов: она вычисляется из данных. Это и есть одна голова причинногопричинная (каузальная) маскаEN causal maskЗапрещает токену смотреть на будущие позиции — оценки выше диагонали заменяются на −∞ до softmax. Нужна, чтобы GPT-подобная модель училась предсказывать следующий токен на всех позициях сразу. self-attention. В курсе ту же формулу по шагам и с числами разбирает урок 4.4.2.
Шесть замечаний о внимании
Заголовок раздела «Шесть замечаний о внимании»1. Внимание — механизм общения. Представьте ориентированный граф: у каждого узла есть вектор, и узел собирает взвешенную сумму векторов тех узлов, которые на него указывают. Веса зависят от данных. В языковой модели граф особый: на первый узел указывает только он сам, на второй — первый и он сам, на восьмой — все предыдущие. Но в принципе внимание работает на любом графе.
2. У внимания нет понятия пространства. Оно действует на множество векторов и не знает, где какой стоит. Поэтому позиции приходится добавлять явно. У свёртки всё наоборот: она по построению «живёт» в пространстве.
3. Примеры батча не общаются. При батче 4 и длине 8 это четыре отдельные группы по восемь узлов, хотя обрабатываются все 32 узла разом.
4. Блоки энкодера и декодера. Треугольная маска нужна, только когда предсказываем будущее. Если, например, определяется тональность предложения, всем токенам можно общаться со всеми: строку с маской просто удаляют. Это блок энкодера. С маской — блок декодера: он «декодирует» язык авторегрессионно.
5. Внимание, self-attention и cross-attention. «Self» — потому что запросы, ключи и значения берутся из одного
источника x. В cross-attentionперекрёстное внимание (cross-attention)EN cross-attentionЗапросы из одной последовательности, ключи и значения — из другой (декодер смотрит на выход энкодера). Ср. self-attention, где все три из одной последовательности. запросы идут из x, а ключи и значения — из
другого источника, например из выхода энкодера.
6. Зачем делить на . Если и — случайные векторы с нулевым средним и единичной
дисперсией, у их скалярного произведения дисперсия порядка head_size. Множитель
возвращает её к единице:
q = torch.randn(10_000, 16)k = torch.randn(10_000, 16)s = (q * k).sum(dim=-1) # 10 тыс. скалярных произведенийprint(s.var(), (s * 16**-0.5).var()) # примерно 16 и примерно 1Это важно из-за softmax. При больших по модулю входах он стремится к one-hot: токен собирает информацию почти от одного соседа. Пример: softmax от — это примерно , а от тех же чисел, умноженных на 8, — . На старте обучения веса должны быть «размытыми», поэтому масштаб и нужен. Подробнее с числами — в уроке 4.4.2.
Часть 5. Собираем трансформер
Заголовок раздела «Часть 5. Собираем трансформер»Дальше Карпатый добавляет к модели по одной детали и каждый раз смотрит на loss на валидации. Сводка (в истории коммитов репозитория для одной головы записано 2.5):
| Шаг | Глава | val loss |
|---|---|---|
| биграммная модель | 38:00 | ≈ 2.5 |
одна голова внимания, head_size = n_embd = 32 | 1:19:11 | ≈ 2.4 |
| четыре головы по 8 | 1:21:59 | 2.28 |
| + полносвязный слой (Linear + ReLU) | 1:24:25 | 2.24 |
| блоки + остаточные связи + проекции + FFN ×4 | 1:26:48 | 2.08 |
| + LayerNorm (pre-norm) и финальный LayerNorm | 1:32:51 | 2.06 |
6 слоёв, 6 голов, n_embd = 384, контекст 256, dropout 0.2 | 1:37:49 | 1.48 |
Одна голова и несколько голов
Заголовок раздела «Одна голова и несколько голов»С одной головой появляется деталь в генерации: контекст нужно обрезать до последних block_size токенов, иначе
таблице позиций не хватит строк. Learning rate приходится снизить — внимание не терпит слишком больших шагов.
Несколько головголова вниманияEN attention headОдна независимая копия внимания со своими проекциями размера d_h = d_model / H. Головы работают параллельно и смотрят на разные отношения между токенами. — несколько независимых каналов общения параллельно. Вместо одной головы размером 32 — четыре по 8, выходы склеиваются обратно в 32. Карпатый сравнивает это с групповой свёрткой. Токенам «есть о чём поговорить»: искать согласные, гласные, определённые позиции — отдельным каналам проще. В курсе — урок 4.4.3.
Общение и вычисление
Заголовок раздела «Общение и вычисление»После внимания токены «посмотрели друг на друга», но им некогда «подумать» о найденном перед выдачей логитов. Поэтому добавляется маленький перцептрон, который применяется к каждому токену отдельно. Внимание — это общение, FFNполносвязный блок (FFN)EN feed-forward network (FFN) — вычисление. Блок трансформера чередует одно с другим, и блоки повторяются.
Две идеи, без которых глубокая сеть не учится
Заголовок раздела «Две идеи, без которых глубокая сеть не учится»Когда блоков становится несколько, сеть начинает плохо оптимизироваться. Из статьи берутся два средства.
Остаточные связи (Residual Learning, 2015). Карпатый рисует их так: сверху вниз идёт «остаточный путь», от него ответвляется вычисление, и результат прибавляется обратно. Сложение при обратном проходе раздаёт градиент обеим ветвям поровну — это было видно ещё в micrograd. Поэтому от loss ко входу идёт «магистраль» градиента без препятствий. Блоки на старте почти ничего не добавляют и «включаются» постепенно. Нужна ещё проекция — линейный слой, возвращающий результат в остаточный путь. А внутренний слой FFN, как в статье (512 → 2048), в 4 раза шире.
Нормализация слоя. LayerNormнормализация слояEN layer normalization — родственник BatchNorm из третьей лекции makemore,
только нормирует не столбцы (по батчу), а строки — каждый вектор токена. Сеть получается проще: не нужны скользящие
буферы и нет разницы между обучением и инференсом; обучаемые масштаб и сдвиг остаются. Есть и отступление от статьи:
теперь LayerNorm принято ставить перед вниманием и FFN (pre-norm), а не после. Плюс ещё один LayerNorm в конце,
перед lm_head.
flowchart TB
X["x: (B, T, n_embd)"] --> LN1["LayerNorm"] --> SA["Многоголовое внимание<br/>(общение)"]
SA --> P1(("+"))
X --> P1
P1 --> LN2["LayerNorm"] --> FF["FFN: Linear → ReLU → Linear<br/>(вычисление)"]
FF --> P2(("+"))
P1 --> P2
P2 --> Y["x на выходе блока"]Масштабирование и dropout
Заголовок раздела «Масштабирование и dropout»Последний шаг — сделать модель больше. Появляются переменные n_layer и n_head и dropoutдропаут (dropout)EN dropoutНа обучении каждая активация обнуляется с вероятностью p, остальные умножаются на 1/(1 − p); на инференсе слой ничего не делает. В претрейне современных LLM обычно p = 0.
(2014): на каждом шаге случайная часть нейронов обнуляется, и сеть как бы учит ансамбль подсетей, который при тесте
работает целиком. Dropout ставится перед возвратом в остаточный путь и на веса внимания после softmax. Он нужен,
потому что большая модель на миллионе символов начнёт переобучаться.
Гиперпараметры итоговой модели: батч 64, контекст 256 символов, learning rate , n_embd = 384,
6 голов по 64, 6 слоёв, dropout 0.2. Обучение заняло около 15 минут на A100, val loss 1.48. Текст по форме похож
на пьесу — реплики, имена персонажей, — но по смыслу бессвязен. Пример длиной 10 тыс. символов лежит в репозитории
в файле more.txt. На CPU такую модель Карпатый обучать не советует: нужно уменьшать число слоёв и размерность.
Ядро gpt.py (MIT, комментарии переведены; имя FeedFoward — как в оригинале):
class Head(nn.Module): """ одна голова self-attention """
def __init__(self, head_size): super().__init__() self.key = nn.Linear(n_embd, head_size, bias=False) self.query = nn.Linear(n_embd, head_size, bias=False) self.value = nn.Linear(n_embd, head_size, bias=False) # маска — не параметр, а буфер модуля self.register_buffer('tril', torch.tril(torch.ones(block_size, block_size))) self.dropout = nn.Dropout(dropout)
def forward(self, x): B, T, C = x.shape k = self.key(x) # (B, T, hs) q = self.query(x) # (B, T, hs) # оценки внимания («сродство») wei = q @ k.transpose(-2, -1) * k.shape[-1]**-0.5 # (B, T, T) wei = wei.masked_fill(self.tril[:T, :T] == 0, float('-inf')) wei = F.softmax(wei, dim=-1) wei = self.dropout(wei) # взвешенная сумма значений v = self.value(x) # (B, T, hs) return wei @ v # (B, T, hs)
class MultiHeadAttention(nn.Module): """ несколько голов параллельно """
def __init__(self, num_heads, head_size): super().__init__() self.heads = nn.ModuleList([Head(head_size) for _ in range(num_heads)]) self.proj = nn.Linear(head_size * num_heads, n_embd) # проекция в остаточный путь self.dropout = nn.Dropout(dropout)
def forward(self, x): out = torch.cat([h(x) for h in self.heads], dim=-1) # склеиваем выходы голов return self.dropout(self.proj(out))
class FeedFoward(nn.Module): """ линейный слой и нелинейность: вычисление для каждого токена отдельно """
def __init__(self, n_embd): super().__init__() self.net = nn.Sequential( nn.Linear(n_embd, 4 * n_embd), nn.ReLU(), nn.Linear(4 * n_embd, n_embd), # проекция обратно в остаточный путь nn.Dropout(dropout), )
def forward(self, x): return self.net(x)
class Block(nn.Module): """ блок трансформера: общение, затем вычисление """
def __init__(self, n_embd, n_head): super().__init__() head_size = n_embd // n_head self.sa = MultiHeadAttention(n_head, head_size) self.ffwd = FeedFoward(n_embd) self.ln1 = nn.LayerNorm(n_embd) self.ln2 = nn.LayerNorm(n_embd)
def forward(self, x): x = x + self.sa(self.ln1(x)) # pre-norm + остаточная связь x = x + self.ffwd(self.ln2(x)) return xЧасть 6. Энкодер, декодер и nanoGPT
Заголовок раздела «Часть 6. Энкодер, декодер и nanoGPT»Почему у нас только декодер
Заголовок раздела «Почему у нас только декодер»Схема из статьи сложнее нашей модели: в ней есть энкодер и блок cross-attention. Мы построили decoder-onlydecoder-only (только декодер)EN decoder-only architectureОдна стопка блоков с причинной маской, цель — следующий токен. Архитектура GPT, Llama, Qwen, DeepSeek, Kimi, GLM-4.5. трансформер. Декодером его делает треугольная маска: модель авторегрессионная, из неё можно сэмплировать. Энкодер не нужен, потому что генерация ни от чего не зависит — мы просто подражаем тексту.
В статье решалась задача машинного перевода, поэтому архитектура была энкодер–декодерэнкодер–декодерEN encoder-decoder architectureДвунаправленный энкодер читает вход, причинный декодер генерирует выход и смотрит на энкодер через cross-attention (исходный Transformer, T5, Whisper).. Энкодер читает французское предложение без маски: все его токены общаются со всеми. Декодер генерирует английский перевод, начиная со специального токена начала и заканчивая токеном конца. В каждом блоке декодера есть cross-attention: запросы идут из декодера, а ключи и значения — из выхода энкодера. Так перевод опирается и на уже сгенерированное начало, и на всё исходное предложение. В курсе три семейства моделей из одного блока сравниваются в уроке 4.5.1.
Быстрый обзор nanoGPT
Заголовок раздела «Быстрый обзор nanoGPT»В nanoGPT два главных файла. train.py — обвязка обучения: чекпоинты, загрузка готовых весов, затухание learning rate,
компиляция, обучение на нескольких GPU. model.py почти совпадает с лекцией, с двумя отличиями:
- головы внимания не отдельные модули, а ещё одна ось батча: тензор становится четырёхмерным (B, головы, T, размер головы). Математически это то же самое, но быстрее;
- в MLP вместо ReLU стоит GELU — как у OpenAI, чтобы можно было загрузить их веса.
Есть и разделение параметров на те, к которым применяется weight decay, и те, к которым нет. Всё это подробно разбирается в лекции 9 и уроках 4.6.2–4.6.3.
Часть 7. От GPT к ChatGPT
Заголовок раздела «Часть 7. От GPT к ChatGPT»Претрейн. Наш «маленький Шекспир» — около 10 млн параметров и около 1 млн символов. В словаре OpenAI это было бы примерно 300 тыс. токенов. Самая большая модель GPT-3 — 175 млрд параметров, обучена на 300 млрд токенов: данных примерно в миллион раз больше. Гиперпараметры в таблице статьи GPT-3 выглядят знакомо, а архитектура почти та же. Главная трудность — инфраструктура: тысячи видеокарт, которые должны работать согласованно.
Что получается после претрейна. Не ассистент, а «продолжатель документов». Модель болтает интернетом вместо Шекспира. На вопрос она может ответить новыми вопросами или продолжить случайную новость: поведение не определено.
Дообучение. По описанию в блоге OpenAI о ChatGPT, этапов три:
- SFT — дообучение на примерах вида «вопрос сверху, ответ ассистента снизу». Таких примеров нужно намного меньше, чем данных претрейна: Карпатый предполагает, что порядка тысяч. Большие модели очень эффективно учатся на малом числе примеров.
- Модель вознаграждения. Модель даёт несколько ответов, люди ранжируют их по качеству, и на этих оценках обучается отдельная сеть, предсказывающая желательность ответа.
- PPO — алгоритм обучения с подкреплением: модель дообучается так, чтобы её ответы получали высокую награду.
Данные этих этапов почти не публикуются, поэтому их гораздо труднее повторить. nanoGPT покрывает только претрейн. В курсе эти этапы — ступень 6: базовая модель против чат-модели, SFT, RLHF и PPO.
Итог лекции. Весь код обучения — около 200 строк. GPT-3 архитектурно почти такая же, только в 10 тыс. — 1 млн раз больше, смотря как считать.
Ошибки и уточнения после видео
Заголовок раздела «Ошибки и уточнения после видео»- 57:00 — правильно «токены из будущего не общаются с прошлым», а не наоборот (поправка в описании видео).
- 1:20:05 — нормировать нужно на размер головы, а не на
C. Исправлено в репозитории. - Инициализация. В README репозитория Карпатый пишет, что в видео почти не говорилось об инициализации весов,
хотя для качества она важна: код работает, но сходится медленнее. Позже в
gpt.pyдобавили_init_weights(нормальное распределение с std 0.02 для линейных слоёв и эмбеддингов, нулевые смещения), как в nanoGPT. Подробно об этом — в лекции 9 и уроке 3.4.1.
Где курс объясняет то же самое и что есть сверх курса
Заголовок раздела «Где курс объясняет то же самое и что есть сверх курса»| Тема лекции | Где в курсе | Что в лекции сверх курса (или наоборот) |
|---|---|---|
| Посимвольный токенизатор, размер словаря против длины | 4.2.1, 4.6.2 | — |
| Окна со сдвигом на один, батчи | 4.6.2, 4.6.3 | зачем учить на всех длинах контекста сразу |
| Биграммная модель | 4.1.2 | биграмма как nn.Embedding в PyTorch |
| От среднего к вниманию | 4.4.2 (маска) | вывод в три шага: циклы → матрица → softmax |
| Q, K, V, масштаб, маска | 4.4.1, 4.4.2 | внимание как общение в графе, шесть замечаний |
| Многоголовое внимание | 4.4.3 | головы как отдельные модули; в курсе сразу батчевая версия |
| FFN, остаточные связи, LayerNorm, pre-norm | 4.5.1, 3.4.1, 3.4.2 | абляция: вклад каждой детали в loss |
| Позиционные эмбеддинги | 4.5.2 | курс идёт дальше: синусоиды и RoPE |
| Энкодер, декодер, cross-attention | 4.5.1, 4.5.4 | пример с переводом и токенами начала и конца |
| Полная GPT и генерация | 4.6.2 | курс идёт дальше: связанные веса, проверка числа параметров; KV-кэш в 4.6.4 |
| От претрейна к ChatGPT | 6.1.1, 6.3.2 | — |
Что сделать руками
Заголовок раздела «Что сделать руками»- Склонируйте
ng-video-lectureи пройдите историю:git log --reverse --oneline, затемgit checkout <коммит>иpython gpt.pyна ключевых шагах. Сверьте loss с таблицей выше. - Упражнение 1 из описания видео — «мастерство n-мерных тензоров». Объедините
HeadиMultiHeadAttentionв один класс, который считает все головы сразу как ещё одну ось батча. Ответ есть в nanoGPT и в уроке 4.4.3. - Упражнение 2. Обучите GPT на своих данных — например, на русском корпусе из урока 4.6.2. Продвинутый вариант
Карпатого — научить модель складывать числа
a+b=c. Его подсказки: выдавать цифрыcв обратном порядке (сложение идёт справа налево), генерировать примеры на лету и не считать loss на позициях условия — цель-1иignore_indexв cross-entropy. - Упражнение 3. Найдите датасет настолько большой, что разрыва между train и val нет. Предобучите на нём модель, затем дообучите её на Шекспире с меньшим learning rate и числом шагов. Станет ли val loss ниже?
- Упражнение 4. Прочитайте пару статей о трансформерах и добавьте одно популярное улучшение — например, RMSNorm или RoPE из уроков 4.5.2–4.5.3. Улучшился ли loss?
Словарик
Заголовок раздела «Словарик»| EN | RU |
|---|---|
| language model | языковая модель |
| character-level tokenizer | посимвольный токенизатор |
| block size, context length | длина контекста (block_size) |
| bigram model | биграммная модель |
| logits | логиты |
| affinities | «сродство» токенов, оценки внимания |
| query / key / value | запрос / ключ / значение |
| head size | размер головы |
| causal mask | причинная маска |
| self-attention / cross-attention | самовнимание / перекрёстное внимание |
| multi-head attention | многоголовое внимание |
| feed-forward network | полносвязный блок (FFN) |
| residual connection, residual pathway | остаточная связь, остаточный путь |
| layer normalization, pre-norm | нормализация слоя, pre-norm |
| dropout | дропаут |
| decoder-only transformer | decoder-only трансформер (только декодер) |
| pretraining / fine-tuning | предобучение (претрейн) / дообучение |
| reward model, PPO | модель вознаграждения, PPO |