Итоги курса и что дальше

⏱ 40 мин · 🧩 заданий: 5

Что мы прошли

Начали с векторов и производных, а закончили механизмом, на котором работают современные языковые модели. Вспомним путь:

Модуль Что узнали Что написали сами
Линейная алгебра векторы, матрицы, преобразования, собственные векторы PCA
Матанализ производные, цепное правило, градиент численный градиент
Как учатся модели функция потерь, градиентный спуск, граф вычислений движок автодифференцирования, nanotorch
Нейронные сети нейрон, слои, активации, потери, инициализация, нормализация, оптимизаторы, регуляризация полносвязную сеть, распознающую цифры
Свёрточные сети свёртка, пулинг, архитектуры свёртку, свёрточную сеть
Трансформеры токены, эмбеддинги, внимание, энкодер BPE, word2vec, свой трансформер

Вся библиотека nanotorch, которой мы пользовались, — это около тысячи строк на Python и NumPy, и каждую её часть мы разобрали. Остался последний шаг — модель, которая пишет текст.

Декодер и GPT

Энкодер читает весь текст сразу. GPT (Generative Pre-trained Transformer) устроен почти так же — те же блоки, — но учится предсказывать следующий токен:

вход: У _ л у к о м о р цель: _ л у к о м о р ь ← тот же текст, сдвинутый на один токен

Чтобы задача не была тривиальной, позиция t не должна видеть позиции после себя. Для этого в каждом блоке включают причинную маску: веса внимания выше диагонали обнуляются. Тогда за один прямой проход модель учится сразу на всех T позициях: по первому символу предсказать второй, по двум — третий и так далее.

Генерация — цикл: подаём текст, берём распределение следующего токена, выбираем токен, дописываем, повторяем. Выбирать можно жадно (самый вероятный) или случайно из распределения \text{softmax}(z / \tau), где \tau — температура: маленькая — предсказуемый текст, большая — более смелый и бессвязный (вспомните виджет softmax из урока о классификации).

Мини-GPT на Пушкине

Обучим маленький GPT по символам на прологе к «Руслану и Людмиле». Модель: эмбеддинги символов и позиций, один блок трансформера с причинной маской, линейная голова на размер алфавита. Обучение займёт около полуминуты:

import numpy as np
import nanotorch as nt
import vis

text = """У лукоморья дуб зелёный;
Златая цепь на дубе том:
И днём и ночью кот учёный
Всё ходит по цепи кругом;
Идёт направо — песнь заводит,
Налево — сказку говорит.
Там чудеса: там леший бродит,
Русалка на ветвях сидит;
Там на неведомых дорожках
Следы невиданных зверей;
Избушка там на курьих ножках
Стоит без окон, без дверей;
Там лес и дол видений полны;
Там о заре прихлынут волны
На брег песчаный и пустой,
И тридцать витязей прекрасных
Чредой из вод выходят ясных,
И с ними дядька их морской;
Там королевич мимоходом
Пленяет грозного царя;
Там в облаках перед народом
Через леса, через моря
Колдун несёт богатыря;
В темнице там царевна тужит,
А бурый волк ей верно служит;
Там ступа с Бабою Ягой
Идёт, бредёт сама собой;
Там царь Кащей над златом чахнет;
Там русский дух… там Русью пахнет!
И там я был, и мёд я пил;
У моря видел дуб зелёный;
Под ним сидел, и кот учёный
Свои мне сказки говорил."""

chars = sorted(set(text))
stoi = {c: i for i, c in enumerate(chars)}
data = np.array([stoi[c] for c in text])
V, T, D = len(chars), 16, 32
print(f"символов в тексте: {len(text)}, алфавит: {V}")

mask = np.triu(np.ones((T, T), dtype=bool), k=1)          # причинная маска

class TinyGPT(nt.Module):
    def __init__(self):
        self.tok = nt.Embedding(V, D)
        self.pos = nt.Embedding(T, D)
        self.block = nt.TransformerEncoderLayer(D, num_heads=2, d_ff=4 * D)
        self.ln = nt.LayerNorm(D)
        self.head = nt.Linear(D, V)

    def forward(self, ids):
        t = ids.shape[1]
        h = self.tok(ids) + self.pos(np.arange(t))
        h = self.block(h, mask[:t, :t])                     # тот же блок, но с маской — это и есть декодер
        return self.head(self.ln(h))

nt.manual_seed(0)
model = TinyGPT()
print("параметров:", model.num_params())
opt = nt.AdamW(model.parameters(), lr=0.003, weight_decay=0.01)
rng = np.random.default_rng(0)

def generate(prompt, n, temperature):
    model.eval()
    ids = [stoi[c] for c in prompt]
    for _ in range(n):
        logits = model(np.array([ids[-T:]])).data[0, -1] / temperature
        p = np.exp(logits - logits.max())
        ids.append(int(rng.choice(V, p=p / p.sum())))
    model.train()
    return "".join(chars[i] for i in ids)

losses = []
for step in range(401):
    starts = rng.integers(0, len(data) - T - 1, size=32)
    x = np.stack([data[s:s + T] for s in starts])
    y = np.stack([data[s + 1:s + T + 1] for s in starts])          # цель — сдвиг на один символ
    loss = nt.cross_entropy(model(x).reshape(-1, V), y.reshape(-1))
    opt.zero_grad()
    loss.backward()
    opt.step()
    losses.append(loss.data.item())
    if step in (0, 100, 400):
        print(f"\n--- шаг {step}, потери {loss.data.item():.2f} ---")
        print(generate("Там ", 80, temperature=0.8))

vis.line(losses, title="Потери мини-GPT", xlabel="шаг")

Посмотрите, как меняется текст. На шаге 0 — случайные символы. Через сотню шагов появляются пробелы, переносы строк и слоги, похожие на русские. К концу — слова из текста, точки с запятой в конце строк и «пушкинский» ритм, хотя смысла почти нет. Модель с 17 тысячами параметров, обученная полминуты на 900 символах, выучила статистику: какие буквы следуют за какими, где ставить пробел и перенос строки.

Попробуйте изменить код: температуру (0.3 или 1.5), начало текста, число шагов, размер модели D, число блоков. Если обучать дольше, модель начнёт просто запоминать стихотворение наизусть — данных слишком мало, это переобучение в чистом виде.

Чем настоящие модели отличаются от нашей

По устройству — почти ничем: эмбеддинги, позиционные кодировки, блоки «внимание + FFN» с LayerNorm и остаточными связями, причинная маска, голова на словарь, кросс-энтропия, AdamW. Разница — в масштабе и в том, как их учат:

  • Размер. У нас 17 тысяч параметров и 1 блок; у больших моделей — миллиарды и сотни миллиардов параметров, десятки и сотни блоков, d_{\text{model}} в тысячи, десятки голов.
  • Данные. У нас 900 символов; большие модели учат на триллионах токенов — значительной части интернета, книг и кода. Токены — части слов (BPE), а не символы.
  • Вычисления. Тысячи видеокарт неделями и месяцами. Всё, что мы делали циклами на NumPy, там — оптимизированные ядра на GPU, смешанная точность, распределение модели по многим устройствам.
  • Этапы обучения. Предобучение на предсказании следующего токена даёт модель, которая умеет продолжать текст. Чтобы она стала полезным ассистентом, её дообучают на примерах диалогов (instruction tuning) и затем по оценкам людей (RLHF) — модель учится отвечать так, как люди считают хорошим и безопасным.
  • Улучшения архитектуры. RMSNorm вместо LayerNorm, поворотные позиционные кодировки (RoPE), активации SwiGLU в FFN, KV-cache и группированные ключи для быстрой генерации, смесь экспертов (MoE). Всё это — вариации того, что мы прошли.

Бывают и другие варианты трансформера: энкодер-декодер (исходный трансформер, T5) — для перевода и пересказа; декодер в нём смотрит на выход энкодера через перекрёстное внимание, как в модели перевода из урока о внимании. Трансформеры работают и с картинками (ViT), звуком, видео, а мультимодальные модели принимают всё сразу.

От nanotorch к PyTorch

Интерфейс nanotorch мы специально сделали похожим на PyTorch — главную библиотеку глубокого обучения. Переход почти механический:

nanotorch PyTorch
import nanotorch as nt import torch, torch.nn as nn, torch.nn.functional as F
nt.tensor(x) torch.tensor(x)
nt.Linear, nt.Conv2d, nt.Embedding, nt.LayerNorm nn.Linear, nn.Conv2d, nn.Embedding, nn.LayerNorm
class Net(nt.Module) + forward class Net(nn.Module) + __init__ с super().__init__() + forward
nt.cross_entropy(logits, y) F.cross_entropy(logits, y)
nt.AdamW(model.parameters(), lr=…) torch.optim.AdamW(model.parameters(), lr=…)
opt.zero_grad(); loss.backward(); opt.step() то же самое
model.train(), model.eval() то же самое
nt.fit(...) цикл обучения пишут сами (или берут Lightning, Hugging Face Trainer)

Главное отличие — PyTorch считает на видеокартах (model.to("cuda")) в сотни раз быстрее и содержит готовые реализации всего, что мы писали руками. Но теперь вы знаете, что происходит внутри каждого вызова.

Что изучать дальше

  • Практика. Установите PyTorch и повторите проекты курса: распознавание цифр (на полноценном MNIST 28 \times 28), свёрточную сеть на CIFAR-10, мини-GPT на большом тексте. Соревнования и датасеты — на Kaggle.
  • Андрей Карпати, «Neural Networks: Zero to Hero» — видеокурс, где с нуля пишут движок автодифференцирования (micrograd) и GPT; после нашего курса он пойдёт легко.
  • «Погружение в глубокое обучение» (Dive into Deep Learning, d2l.ai) — бесплатный учебник с кодом, есть перевод на русский.
  • Курсы Stanford CS231n (свёрточные сети и компьютерное зрение) и CS224n (обработка естественного языка) — лекции и задания в открытом доступе.
  • Hugging Face — библиотека transformers и тысячи предобученных моделей; там же бесплатный курс по работе с ними.
  • Книга Гудфеллоу, Бенжио, Курвилля «Глубокое обучение» — фундаментальный учебник по теории.
  • Для классического машинного обучения (деревья решений, градиентный бустинг, кластеризация) — библиотека scikit-learn и её документация.

Итог курса

  • Нейросеть — это композиция простых дифференцируемых функций с обучаемыми параметрами.
  • Обучение — градиентный спуск по функции потерь; градиенты считает обратное распространение по графу вычислений.
  • Архитектура встраивает знания о данных: свёртки — о локальности и сдвигах в картинках, внимание — о связях между элементами последовательности.
  • Большие языковые модели — это трансформеры-декодеры, обученные предсказывать следующий токен на огромных данных.

Вы прошли путь от вектора до GPT и написали каждую деталь сами. Дальше — практика.

Практика решено 0 из 5

Вы не вошли — ответы проверяются, но прогресс не сохранится. Войти или зарегистрироваться.
Задание 1 Один вариант ответа

Зачем в GPT причинная (causal) маска внимания?

Задание 2 Один вариант ответа

Что происходит при генерации, если поставить температуру очень близкой к нулю?

Задание 3 Ввод ответа

Сколько раз нужно запустить модель (прямой проход), чтобы сгенерировать 100 новых токенов?

Задание 4 Программа на Python

Причинная маска. Напишите causal_mask(T) — булев массив (T, T), где True означает «смотреть нельзя»: позиция i может смотреть только на позиции j \le i.

Какие тесты проверяют код (2)
m = causal_mask(4)
assert m.dtype == bool and m.shape == (4, 4)
assert np.array_equal(m, [[0, 1, 1, 1], [0, 0, 1, 1], [0, 0, 0, 1], [0, 0, 0, 0]]), "True строго выше диагонали"

import nanotorch as nt
rng = np.random.default_rng(0)
q = nt.tensor(rng.normal(size=(5, 4)))
_, w = nt.scaled_dot_product_attention(q, q, q, mask=causal_mask(5))
assert np.allclose(np.triu(w.data, 1), 0), "С такой маской веса над диагональю нулевые"

Задание 5 Программа на Python

Жадная генерация. Напишите greedy_generate(next_logits, prompt, n): начните со списка токенов prompt и n раз добавьте токен с наибольшим логитом. next_logits(tokens) — функция, которая по списку токенов возвращает массив логитов следующего токена. Верните весь список (подсказка + новые токены).

Какие тесты проверяют код (2)
# «модель»: следующий токен = (последний + 1) mod 5
f = lambda toks: np.eye(5)[(toks[-1] + 1) % 5]
assert greedy_generate(f, [3], 4) == [3, 4, 0, 1, 2]

f = lambda toks: np.eye(3)[len(toks) % 3]
prompt = [0]
out = greedy_generate(f, prompt, 3)
assert out == [0, 1, 2, 0], "Каждый шаг — новый вызов модели на всём накопленном тексте"
assert prompt == [0], "Не меняйте переданный список подсказки"

Войдите, чтобы отмечать пройденные уроки.