Математика глубокого обучения
Практика: трансформер с нуля
Собрать блок целиком, добавить RoPE, KV-кэш и LoRA — и проверить каждое утверждение блока измерением
Блок в одной строке
Всё, что было в этом блоке, собирается в два выражения, и оба уже разобраны по частям.
Единственное разделение труда, которое здесь есть: внимание работает между позициями, MLP — внутри позиции. Больше в блоке ничего нет.
Что реализовать
1. Десять операций через einsum
Начните с нотации, потому что дальше она сэкономит все ошибки с формами. Выпишите каждую операцию блока как строку индексов и посчитайте стоимость до написания кода.
| операция | спецификация |
|---|---|
| проекция | bnD,Dhd->bhnd |
| оценки | bhqd,bhkd->bhqk |
| выход внимания | bhqk,bhkd->bhqd |
| склейка голов | bhnd,hdD->bnD |
| MLP вверх | bnD,DF->bnF |
| MLP вниз | bnF,FD->bnD |
| логиты | bnD,VD->bnV |
Проверьте себя на первой строке: она содержит переформовку (h d) -> h d внутри контракции,
и именно поэтому проекция и разбиение на головы — это одна операция, а не две.
2. Блок целиком
Масштаб nanoGPT: , слоёв, голов, контекст . На таком размере всё считается на одном CPU за минуты, а все эффекты блока видны.
Обязательные детали, каждая из которых разбиралась:
- pre-norm, а не post-norm (урок 090);
- инициализация He для MLP, выходные проекции ветвей поделены на (урок 100);
- причинная маска большим отрицательным числом, а не (урок 080);
- деление оценок на (урок 070);
- softmax со сдвигом на максимум (урок 060).
3. Пять измерений вместо доверия
Здесь главная часть работы. Каждое утверждение блока проверяется одной строкой на вашей же реализации.
Измерение 1: действительно нужен. Отключите деление и напечатайте стандартное отклонение оценок и энтропию строк весов. Ожидание: разброс порядка , энтропия падает почти до нуля ещё до обучения.
Измерение 2: инициализация даёт нужный масштаб. Напечатайте стандартное отклонение активаций после каждого слоя на случайном входе. Ожидание: с на выходных проекциях итоговая дисперсия residual-потока около при любом числе слоёв — проверьте на и .
Измерение 3: RoPE относителен точно. Посчитайте оценку для пар , , . Ожидание: совпадение до последнего знака. Сделайте то же с прибавленной синусоидальной кодировкой — там расхождение будет порядка самого размаха оценок.
Измерение 4: кэш ничего не меняет в результате. Сгенерируйте двадцать токенов дважды — с кэшем и наивным пересчётом всей последовательности — и сравните полученные последовательности. Ожидание: побитовое совпадение. Если расходятся, ошибка в кэше, и это самый надёжный способ её найти.
Измерение 5: LoRA стартует ровно с исходной модели. Добавьте к проекциям внимания при и сравните логиты до и после добавления. Ожидание: побитовое равенство. Затем сделайте один шаг оптимизации и убедитесь, что логиты изменились.
4. Генерация с кэшем
Реализуйте два режима и сравните не только результат, но и время:
| режим | что считает на шаге |
|---|---|
| наивный | весь прямой проход для всех токенов |
| с кэшем | один токен, ключи и значения читаются |
Ожидание из урока 140: разница растёт линейно по длине. Заодно посчитайте размер кэша своей модели: элементов — на этом масштабе он мал, и полезно увидеть, во что он превращается при и .
Отдельная деталь, которую легко пропустить: на шаге генерации логиты нужны только для последней позиции. Проверьте по таблице einsum выше, сколько работы экономит этот срез — на маленькой модели со словарём это самая дорогая операция.
5. Обучение на маленьком корпусе
Возьмите любой текст в несколько мегабайт и обучите до осмысленных фрагментов. Что стоит наблюдать:
- лосс на первом шаге обязан быть около — это энтропия равномерного распределения по словарю (блок 4, урок 010). Отклонение означает ошибку в инициализации выходного слоя;
- перплексия — «во сколько раз модель неуверенна» (блок 4, урок 020);
- warmup и cosine-расписание (блок 5, урок 090) — сравните с постоянным шагом;
- градиентный клиппинг: посмотрите, как часто он срабатывает в начале и в конце.
Куда смотреть, если не сходится
| симптом | что проверять первым |
|---|---|
| лосс на первом шаге | инициализацию выходного слоя |
лосс nan через несколько шагов | сдвиг в softmax, маску , слишком большой шаг |
| лосс стоит на | маску (не видит ли модель будущее наоборот), знак в residual |
| качество внезапно ниже на коротких строках | padding-маску |
| генерация с кэшем расходится с наивной | порядок применения RoPE: позиции в кэше и у нового токена |
| активации растут по слоям | множитель на выходных проекциях |
Пятая строка — самая частая ошибка в реализациях с кэшем: при повороте нового ключа нужно взять его абсолютную позицию, а не нулевую. Проверяется измерением 4 и никак иначе.
Чем это стоит закончить
Прогоните готовую модель через утверждения блока и убедитесь, что все пять измерений дают ожидаемое. Именно этот набор — а не работающая генерация — показывает, что блок понят: генерация получается и при трёх ошибках, которые взаимно компенсируются.
Источники
- Karpathy — nanoGPT — Эталонная реализация нужного масштаба
- Karpathy — Let's build GPT, from scratch — Тот же код, разобранный по строкам
- Rogozhnikov — Einops tutorial — Все операции блока через одну нотацию
Проверки
0 из 2Диагностика собственной реализации
Отметьте все верные утверждения о сборке и отладке трансформера.
Где уходит работа в вашей модели
Реализуйте
block_cost(context, model_dim, expansion, vocab, layers)— верните[block_params, attn_macs, mlp_macs, logit_macs, attn_share, logit_ratio]. Обозначим — контекст, — ширину модели, — коэффициент расширения MLP, — размер словаря, — число слоёв.block_params= — четыре квадратные проекции внимания плюс две матрицы MLP размера (смещения не считаем);attn_macs= — две контракции внимания для всех позиций, один слой;mlp_macs= — MLP для всех позиций, один слой;logit_macs= — выходной слой, один раз на всю последовательность;attn_share— доляattn_macsв работе одного слоя, то есть от суммыattn_macs,mlp_macsи проекций ;logit_ratio— отношениеlogit_macsк работе всех слоёв.
Проверить себя можно так:
attn_shareобязана равняться — сократите формулы и убедитесь.Загрузка редактора…
Ctrl/⌘ + Enter