05 · Обучение и генерация: цикл, который замыкает microGPT
Четыре урока механизмов — и ни один не объяснил, откуда взялись настройки
всех 4 000 ручек. Этот объясняет: модель, которая начинается как чистый
гауссовский шум, перемешанный список человеческих имён и цикл — прямой
проход, потери, обратный проход, толчок — повторённый тысячу раз, пока шум
не перестроится во что-то, что умеет лепетать karia, dell, aanna:
имена, которых не существует, но которые могли бы. Это урок-награда. Здесь
всё наконец запускается.
Прежде чем нырнуть. Обучение — это рецепт из 00 · Основы §4, ставший реальностью: посчитать, насколько модель неправа (потери), получить градиент каждой ручки через обратный проход из урока 02, а затем подтолкнуть каждую ручку на маленький шаг под горку — тысячи раз. Adam — тот же рецепт, но с амортизаторами: он сглаживает недавние градиенты каждой ручки, чтобы один шумный шаг не дёргал её туда-сюда. А температура — это трюк из §5: подели оценки перед софтмаксом, чтобы генерация стала смелее или осторожнее. Если ты прочитал 00 и 02, в этом уроке нет ничего нового — здесь всё наконец работает вместе.
Теория
Уроки 01–04 построили прямой проход. Этот урок — весь остаток файла: как веса выучиваются (обучение) и как обученная модель лепечет новые имена (генерация). И то и другое коротко, и то и другое — прямиком из src/microgpt_annotated.py.
Данные и токенизатор
Датасет docs — перемешанный список имён. Словарь — просто отсортированное множество встречающихся символов:
uchars = sorted(set(''.join(docs))) # the characters → token ids 0..n-1
BOS = len(uchars) # one extra sentinel id, AFTER the chars
vocab_size = len(uchars) + 1 # chars + 1 (the BOS)BOS — один особый токен. Каждый обучающий документ обёрнут им с обеих сторон, а целью для каждой позиции служит просто следующий токен:
tokens = [BOS] + [uchars.index(ch) for ch in doc] + [BOS]
# predict tokens[pos+1] from tokens[pos]Ведущий BOS — это «СТАРТ»; замыкающий BOS — то, что модель должна научиться предсказывать, чтобы сказать «это имя закончилось».
Обучение: прямой проход → потери → обратный проход → Adam
Для каждой позиции модель выдаёт логиты, софтмакс превращает их в вероятности, а потери — это минус логарифм вероятности, которую модель присвоила настоящему следующему токену, усреднённый по документу:
probs = softmax(logits)
loss_t = -probs[target_id].log()
loss = (1 / n) * sum(losses) # mean cross-entropy
loss.backward() # the lesson-02 autograd, run on the whole graphЗатем Adam (а не простой SGD) обновляет каждый параметр по его градиенту. Adam держит для каждого параметра два бегущих буфера — первый момент m (сглаженный градиент) и второй момент v (сглаженный квадрат градиента), — корректирует их смещение и линейно затухает скорость обучения по ходу тренировки:
learning_rate, beta1, beta2, eps_adam = 0.01, 0.85, 0.99, 1e-8
lr_t = learning_rate * (1 - step / num_steps) # linear LR decay
for i, p in enumerate(params):
m[i] = beta1 * m[i] + (1 - beta1) * p.grad
v[i] = beta2 * v[i] + (1 - beta2) * p.grad ** 2
m_hat = m[i] / (1 - beta1 ** (step + 1)) # bias correction
v_hat = v[i] / (1 - beta2 ** (step + 1))
p.data -= lr_t * m_hat / (v_hat ** 0.5 + eps_adam)
p.grad = 0 # reset for the next stepГенерация: лепет в ответ
После обучения модель генерирует по одному символу за раз, начиная с BOS и подавая каждое предсказание обратно на вход. В Python-версии Карпатого растущий KV-кэш (keys, values) накапливает прошлый контекст, поэтому каждый новый токен делает работу только за себя (браузерный порт считает это иначе — см. примечание в разделе «Аннотированный код»). Температура делит логиты перед софтмаксом: низкая температура концентрирует распределение (сосредоточенно, с повторами), высокая — расплющивает его (случайно, творчески). Генерация останавливается, когда модель снова вытягивает BOS:
temperature = 0.5 # in (0, 1]
for pos_id in range(block_size):
logits = gpt(token_id, pos_id, keys, values)
probs = softmax([l / temperature for l in logits])
token_id = random.choices(range(vocab_size), weights=[p.data for p in probs])[0]
if token_id == BOS:
break
sample.append(uchars[token_id])Аннотированный код
Цикл обучения — это Section 5 файла src/microgpt_annotated.py (подраздел overview-training-step, строки py 194–226); генерация — Section 6 (строки py 234–247). Гиперпараметры Adam learning_rate=0.01, beta1=0.85, beta2=0.99, eps=1e-8 и num_steps=1000 — те самые значения, с которыми тренировались поставляемые веса.
Замечание о том, что эта песочница делает и чего не делает. ~89 КБ весов были обучены офлайн Python-файлом; браузер модель не переобучает.
Режим Train вычисляет один настоящий расчёт градиента и обновления Adam для одного параметра LM-головы (остальная сеть зафиксирована): он токенизирует твой документ, гонит настоящий прямой проход ради потерь, вызывает loss.backward() через движок Value из урока 02 ради честного градиента и вычисляет точную формулу Adam, приведённую выше. Каждое число настоящее — но рассчитанное обновление только отображается, а не сохраняется в загруженную модель, и каждое изменение входа начинает заново со свежих буферов Adam на шаге 0. Это расчёт обновления, а не сохранённый шаг обучения, и модель, из которой ты сэмплируешь в режиме Generate, остаётся неизменной.
Режим Generate гоняет настоящую модель авторегрессивно при выбранной тобой температуре. Одна деталь исполнения, о которой стоит сказать точно: эталонный Python использует растущий KV-кэш, а браузерный порт на TypeScript пересчитывает весь причинный префикс на каждом шаге генерации. Итоговые логиты — та же самая математика, но стратегия исполнения другая: браузер не ведёт инкрементальный KV-кэш.
Песочница
- Generate — смотри, как модель строит имя начиная с BOS. Тяни temperature — и столбики вероятностей следующего символа перестраиваются вживую (низкая = сосредоточенно, высокая = случайно); resample вытягивает другое имя. Останавливается, когда модель предсказывает стража STOP. (Каждый шаг пересчитывает весь префикс — те же логиты, без инкрементального KV-кэша.)
- Train — набери документ и увидь один настоящий расчёт градиента + обновления Adam: данные → прямой проход → потери → обратный проход → Adam. Панель показывает настоящую среднюю кросс-энтропию и настоящие
m / v / m̂ / v̂ / lr_t / Δдля одного параметра LM-головы, прямо из формулы. Обновление показывается, но не сохраняется — поменяй вход, и всё пересчитается со свежих буферов Adam на шаге 0.
Попробуй сам.
- В Generate поставь температуру на минимум и пересэмплируй пять раз. Потом выкрути её на максимум и пересэмплируй ещё пять. Ты только что одним слайдером прогнал модель от «скучно, но надёжно» до «креативно, но без тормозов» — тот самый компромисс, который настраивает каждый чат-бот как продукт.
- Смотри на столбики вероятностей, пока тянешь температуру. Порядок столбиков никогда не меняется — только контраст. Почему? (Подсказка: деление на константу не может переупорядочить оценки.)
- В Train дай модели
emmaи запомни потери. Теперь дайxqzzv. Какой документ удивляет модель сильнее, и что это говорит об именах, на которых она выросла?- Посмотри на настоящую
Δдля параметра LM-головы. Она крошечная. Обучение — это тысяча крошечных толчков, а не одно большое озарение.