Строим GPT с нуля: пошаговое руководство по архитектуре Transformer
Пошаговая реализация GPT-модели на Python с нуля — от биграммной языковой модели до полноценного Transformer с self-attention, multi-head attention и генерацией текста.
Для AI-агентов и LLM
Экстракт доступен в структурированном Markdown. Скачать .md · JSON API · Site index
💡 Ключевые тезисы (12)
1 ChatGPT — это языковая модель, которая предсказывает следующий токен #
2 Transformer — единственная архитектура, которая имеет значение #
3 Токенизация — перевод текста в числа и обратно #
4 Данные подаются батчами из случайных чанков фиксированной длины #
5 Биграммная модель — простейший, но работающий baseline #
6 Self-attention — механизм коммуникации между токенами через Query, Key, Value #
7 Матричное умножение с нижнетреугольной маской — ключевой трюк реализации #
8 Multi-head attention — несколько каналов коммуникации параллельно #
9 Transformer-блок = коммуникация (attention) + вычисление (feedforward) #
10 Residual connections и LayerNorm — необходимы для обучения глубоких сетей #
11 Scaled attention: деление на √d_k предотвращает схлопывание softmax #
12 Dropout — регуляризация через случайное «выключение» нейронов #
Строим GPT с нуля: пошаговое руководство по архитектуре Transformer
Спикер: Андрей Карпати | Длительность: 1:56:20
Транскрипт
Введение: ChatGPT и Transformer
Андрей Карпати начинает с демонстрации ChatGPT — системы, которая генерирует текст слева направо, токен за токеном. Это языковая модель: она моделирует последовательности слов и знает, как слова следуют друг за другом в языке. Под капотом работает архитектура Transformer из статьи «Attention Is All You Need» (2017).
Цель лекции — построить Transformer-модель с нуля, обучить её на корпусе Шекспира (tiny Shakespeare, ~1 МБ) и научить генерировать «шекспироподобный» текст посимвольно. Весь код доступен в репозитории nanoGPT.
Подготовка данных и токенизация
Датасет — все произведения Шекспира в одном файле (~1 млн символов). Словарь — 65 уникальных символов (буквы, пробелы, знаки препинания). Создаются функции encode (строка → список чисел) и decode (обратно).
Обсуждаются альтернативные токенизаторы: SentencePiece (Google), tiktoken (OpenAI, ~50 000 токенов для GPT-2). Компромисс: маленький словарь = длинные последовательности и наоборот.
Данные разбиваются на train (90%) и val (10%) для контроля переобучения.
DataLoader: батчи чанков данных
Вводятся ключевые параметры: block_size (максимальная длина контекста, начинаем с 8) и batch_size (количество параллельных последовательностей, 4). В чанке из 9 символов упаковано 8 обучающих примеров — модель учится предсказывать по контексту от 1 до 8 символов.
Функция get_batch генерирует случайные смещения в массиве данных, вырезает чанки и складывает в тензор (B, T). Итого 32 независимых примера в одном батче.
Биграммная языковая модель
Простейшая модель: таблица эмбеддингов vocab_size × vocab_size. Каждый токен по своему индексу извлекает строку из таблицы — это logits (оценки) для следующего символа. Токены не взаимодействуют.
Loss измеряется через cross-entropy. Начальный loss ~4.87 (теоретический при равномерном распределении: -ln(1/65) ≈ 4.17). После 10 000 шагов обучения с AdamW loss падает до ~2.5. Генерация — авторегрессивная: softmax → multinomial sampling → конкатенация.
Математический трюк: матричное умножение как взвешенное усреднение
Три версии одной операции — усреднение предыдущих токенов:
Версия 1 (цикл for): Для каждой позиции t вычисляем среднее всех предыдущих векторов. Работает, но медленно.
Версия 2 (матричное умножение): Создаём нижнетреугольную матрицу (T×T), нормализуем строки, умножаем на X. Результат идентичен, но параллельный.
Версия 3 (softmax): Начинаем с нулей, заполняем верхний треугольник значением -∞, применяем softmax. Получаем те же нормализованные веса. Этот вариант обобщается: нули можно заменить на data-dependent значения — и это уже self-attention.
Self-Attention: ключевой механизм
Каждый токен генерирует три вектора через линейные проекции:
- Query (Q): «что я ищу»
- Key (K): «что я содержу»
- Value (V): «что я сообщу, если вы меня найдёте»
Афинность между токенами = Q · Kᵀ. Масштабирование: делим на √head_size, чтобы сохранить дисперсию ~1. Маскирование будущих позиций через -∞. Softmax → нормализованные веса. Агрегация: weights · V.
Важные замечания:
- Attention — механизм коммуникации на направленном графе
- Нет понятия пространства — нужны позиционные эмбеддинги
- Батчи обрабатываются независимо
- Encoder-блоки (полная связность) vs decoder-блоки (маскирование будущего)
- Self-attention (Q, K, V из одного источника) vs cross-attention (K, V из внешнего источника)
Multi-Head Attention
Вместо одной «головы» с head_size=32 используются 4 головы по 8. Каждая голова ищет свой тип паттернов. Результаты конкатенируются обратно в 32-мерный вектор. Loss снижается с 2.4 до 2.28.
FeedForward и Transformer-блок
FeedForward — это MLP: Linear → ReLU → Linear. Применяется к каждому токену независимо (per-node computation). Attention собрал информацию, feedforward её обработал.
Полный блок: MultiHeadAttention → residual + LayerNorm → FeedForward → residual + LayerNorm. Блоки повторяются N раз (в финальной модели — 4-6).
Residual Connections и LayerNorm
Residual connections: x = x + block(x). Создают прямой путь для градиентов через глубокую сеть. Без них глубокие Transformer-ы не обучаются.
LayerNorm нормализует активации по последнему измерению. Используется pre-norm формулировка (LayerNorm до attention/feedforward), что отличается от оригинальной статьи.
Dropout и финальная модель
Dropout добавляется после softmax в attention и в feedforward-слоях. Регуляризация через случайное зануление нейронов во время обучения.
Финальная модель: n_embd=384, 6 блоков, 6 голов, dropout=0.2. Val_loss ~1.48. Генерация выдаёт текст, стилистически похожий на Шекспира с правильной структурой диалогов.
Масштабирование до GPT и ChatGPT
Путь от nanoGPT до ChatGPT:
- Pre-training: обучение на интернет-данных (предсказание следующего токена)
- Fine-tuning: обучение на примерах «вопрос → ответ»
- RLHF: обучение с подкреплением на основе человеческих оценок
nanoGPT — это только первый этап. Fine-tuning и RLHF превращают «генератор текста» в «полезного ассистента».
Практические задания
Задание 1: Реализовать посимвольный токенизатор
Напишите с нуля функции encode() и decode() для посимвольной токенизации. Возьмите любой текстовый файл, постройте словарь уникальных символов, создайте маппинг символ→число и число→символ. Проверьте, что decode(encode(text)) == text. Сравните длину закодированной последовательности с результатом tiktoken.
Задание 2: Реализовать взвешенное усреднение через матричное умножение
Создайте случайный тензор X размера (B, T, C) = (4, 8, 2). Реализуйте три версии усреднения предыдущих токенов: наивный цикл, умножение на нижнетреугольную матрицу и masked_fill + softmax. Убедитесь через torch.allclose, что все три версии дают идентичный результат.
Задание 3: Построить и обучить биграммную модель
Реализуйте BigramLanguageModel как подкласс nn.Module. Обучите на tiny Shakespeare с batch_size=32 и lr=1e-3 на 10 000 итераций. Достигните loss ~2.5. Сгенерируйте 500 символов и оцените качество.
Задание 4: Реализовать single-head self-attention
Добавьте head самовнимания с линейными проекциями Q, K, V. Реализуйте масштабирование, маскирование, softmax и агрегацию V. Обучите и убедитесь в снижении loss с ~2.5 до ~2.4.
Задание 5: Собрать полный Transformer-блок
Объедините MultiHeadAttention + FeedForward + residual + LayerNorm в TransformerBlock. Сложите 4 блока, добавьте dropout. Целевой val_loss ~1.48.
Задание 6: Эксперименты с гиперпараметрами
Измените количество голов (1, 4, 8), глубину (2, 4, 6), n_embd (32, 64, 128), dropout (0.0, 0.2, 0.4). Постройте таблицу результатов.
Задание 7: Перенести на собственный датасет
Возьмите текст на русском языке (≥500 КБ). Адаптируйте токенизатор, обучите Transformer, оцените качество генерации.
Лучшие цитаты
«Attention is really just a communication mechanism. You can think about it as nodes in a directed graph, where every node has some vector of information and it gets to aggregate information via a weighted sum from all of the nodes that point to it.» — Андрей Карпати
«The query vector roughly speaking is 'what am I looking for' and the key vector roughly speaking is 'what do I contain'.» — Андрей Карпати
«These tokens have a lot to talk about — they want to find the consonants, the vowels, vowels from certain positions — and so it helps to create multiple independent channels of communication.» — Андрей Карпати
«Self-attention does the communication, the feedforward does the computation. The tokens look at each other, but then they need to think about what they found.» — Андрей Карпати
«We don't want these values to be too extreme especially at initialization, otherwise softmax will be way too peaky and you're basically aggregating information from a single node — that's not what we want.» — Андрей Карпати
«Residual connections are these skip connections — instead of just going through the block, you fork off, do some computation, and come back. This is extremely important for training deep networks.» — Андрей Карпати
«In a chunk of nine characters there's actually eight individual examples packed in there.» — Андрей Карпати
«If you have unit Gaussian inputs and you just do the dot product naively, your variance will be on the order of head_size. But if you multiply by one over square root of head_size, the variance will be preserved at one.» — Андрей Карпати
🏋️ Практикум
Реализовать посимвольный токенизатор
Напишите с нуля функции encode() и decode() для посимвольной токенизации. Возьмите любой текстовый файл, постройте словарь уникальных символов, создайте маппинг символ→число и число→символ. Проверьте, что decode(encode(text)) == text. Сравните длину закодированной последовательности с результатом tiktoken.
Реализовать взвешенное усреднение через матричное умножение
Создайте случайный тензор X размера (B, T, C) = (4, 8, 2). Реализуйте три версии усреднения предыдущих токенов: 1) наивный цикл for, 2) умножение на нижнетреугольную матрицу с нормализацией строк, 3) через masked_fill(-inf) + softmax. Убедитесь с помощью torch.allclose, что все три версии дают одинаковый результат.
Построить и обучить биграммную модель
Реализуйте BigramLanguageModel как подкласс nn.Module с одной таблицей эмбеддингов. Обучите на tiny Shakespeare с batch_size=32 и learning_rate=1e-3 на 10 000 итераций. Достигните loss ~2.5. Сгенерируйте 500 символов и оцените качество. Сравните начальный loss с теоретическим -ln(1/65) ≈ 4.17.
Реализовать single-head self-attention
Добавьте к биграммной модели один head самовнимания: создайте линейные проекции для Q, K, V с head_size=32. Реализуйте масштабирование (÷√head_size), маскирование будущих позиций и softmax. Обучите модель и убедитесь, что loss снизился с ~2.5 до ~2.4. Визуализируйте матрицу attention-весов для одного примера.
Собрать полный Transformer-блок
Соберите TransformerBlock, включающий: MultiHeadAttention (4 головы по 8 измерений), FeedForward (линейный слой с расширением ×4 и ReLU), residual connections и LayerNorm. Сложите 4 таких блока. Обучите на tiny Shakespeare — целевой val_loss ~1.48. Сгенерируйте 500 символов и сравните с биграммной моделью.
Эксперименты с гиперпараметрами
Проведите серию экспериментов: измените количество голов (1, 4, 8), глубину (2, 4, 6 блоков), n_embd (32, 64, 128), dropout (0.0, 0.2, 0.4). Для каждого варианта зафиксируйте train_loss, val_loss и качество генерации. Постройте таблицу результатов и определите, какой параметр влияет сильнее всего.
Перенести модель на собственный датасет
Возьмите текст на русском языке (минимум 500 КБ — например, произведения из lib.ru). Адаптируйте токенизатор под новый набор символов (кириллица + знаки препинания). Обучите Transformer и сгенерируйте текст. Оцените, насколько модель уловила структуру русского языка по сравнению с английским Шекспиром.
💬 Цитаты (8)
«Attention is really just a communication mechanism. You can think about it as nodes in a directed graph, where every node has some vector of information and it gets to aggregate information via a weighted sum from all of the nodes that point to it.» #
«The query vector roughly speaking is 'what am I looking for' and the key vector roughly speaking is 'what do I contain', and then we get affinities between tokens by doing a dot product between the keys and the queries.» #
«If you have unit Gaussian inputs, and you just do the dot product naively, your variance will be on the order of head_size. But if you multiply by one over square root of head_size, the variance will be preserved at one.» #
«These tokens have a lot to talk about — they want to find the consonants, the vowels, vowels from certain positions — and so it helps to create multiple independent channels of communication.» #
«Self-attention does the communication, the feedforward does the computation. The tokens look at each other, but then they need to think about what they found.» #
«Residual connections are these skip connections — instead of just going through the block, you fork off, do some computation, and come back. This is extremely important for training deep networks because gradients flow directly through the residual pathway.» #
«In a chunk of nine characters there's actually eight individual examples packed in there. We train on all these examples with context between one all the way up to block_size.» #
«We don't want these values to be too extreme especially at initialization, otherwise softmax will be way too peaky and you're basically aggregating information from a single node — that's not what we want.» #
Популярное в категории
Читать далее
Andrej Karpathy
Как использовать LLM на максимум: практическое руководство от Андрея Карпати
Андрей Карпати
Поделитесь с коллегами