← Росток Своя LLM с нуля Словарик Код EN

Глава 3 из 14 35 мин

Спуск с горы

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

В этой главе

  • понять, что такое производная и градиент и почему шагают против градиента
  • вывести градиент softmax с перекрёстной энтропией: p − onehot
  • обучить биграммную нейросеть у себя в браузере и прийти к тем же вероятностям, что дал подсчёт

В первой главе мы построили модель, ничего не обучая: посчитали, как часто одна буква идёт за другой, и поделили. Во второй измерили, насколько она хороша: 2,365 ната удивления на букву на том самом тексте, по которому считали (и 2,364 на тексте, которого она не видела). Для таблицы 97 × 97 лучше не придумаешь. Но у подсчёта есть потолок, и мы очень скоро в него упрёмся.

Допустим, мы хотим смотреть не на одну предыдущую букву, а на три. Разных троек букв 97³ = 912 673, и каждой нужна строка из 97 вероятностей — почти 89 миллионов клеток. В нашем корпусе 41 миллион букв, так что большинство троек не встретится ни разу, и их строки останутся пустыми. А Росток оглядывается на 512 токенов назад. Таблица для такого контекста не поместилась бы ни в одну вселенную.

Нужен другой способ находить хорошие числа — такой, который годится для любой модели, а не только для таблицы. Идея простая: взять числа наугад, измерить ошибку и чуть-чуть подвинуть каждое число туда, где ошибка меньше. Повторить тысячи раз. Это и есть обучение, а его сердце — градиентный спуск. В этой главе мы освоим его на знакомой задаче: заново получим биграмму, только не подсчётом, а спуском. И проверим, что спуск придёт ровно туда же.

В тумане

Представьте: вы в горах, и опустился такой туман, что не видно собственных ботинок. Нужно спуститься в долину. Карты нет, тропы не видно. Что остаётся? Почувствовать ногами, куда наклонена земля прямо под вами, и шагнуть туда, где она круче всего уходит вниз. Потом снова прислушаться к склону — и снова шагнуть.

Карта спрятана в тумане; видно только то, где вы уже побывали. Стрелка у ног — направление самого крутого спуска, и кнопка «Шаг по склону» шагает ровно по ней.

Сыграйте пару раз с разных мест, а потом снимите туман. Вы заметите три вещи, и все три — про обучение нейросетей.

  1. Карта не нужна. Чтобы идти вниз, достаточно знать наклон там, где стоишь. Модели учатся именно так: всю «местность» ошибки не видит никто, но наклон в текущей точке посчитать можно всегда.
  2. Шаги сами укорачиваются. Кнопка шагает на расстояние, пропорциональное крутизне: на крутом склоне шаг широкий, у дна — мелкий. На самом дне наклон почти нулевой, и путник почти замирает.
  3. Низина — не обязательно самая глубокая. С неудачного старта спуск приводит в мелкое озеро, откуда во все стороны дорога ведёт вверх. Такое место называют локальным минимумом.

Переведём метафору на язык моделей. Высота — это ошибка модели, та самая функция потерь из главы 2. Место на карте — значения параметров. У путника их два: запад–восток и юг–север. У нашей биграммы их 9 409, у Ростка — 17 миллионов. Такую местность не нарисуешь, но наклон в ней по-прежнему можно вычислить, а значит, можно и шагать так же. Осталось понять, что такое «наклон» на языке чисел.

Производная — это наклон

Начнём с одной ручки. Пусть ошибка зависит от единственного числа $x$ и выглядит как чаша: $f(x) = \tfrac12 (x-2)^2$. Дно — при $x = 2$. Как, стоя в точке $x$, понять, куда идти?

Сдвинемся от $x$ на крошечное $h$ и посмотрим, насколько изменилась высота. Отношение подъёма к шагу и есть наклон:

$$\text{наклон} \approx \frac{f(x+h) - f(x)}{h}.$$

Чем меньше $h$, тем точнее. То, к чему стремится это отношение при $h \to 0$, называют производной и пишут $f'(x)$ или $\frac{df}{dx}$. Для нашей чаши её можно вывести в одну строчку:

$$\frac{f(x+h)-f(x)}{h} = \frac{\tfrac12 (x-2+h)^2 - \tfrac12 (x-2)^2}{h} = \frac{(x-2)h + \tfrac12 h^2}{h} = (x-2) + \tfrac12 h.$$

Когда $h$ стремится к нулю, остаётся $f'(x) = x - 2$. Знак производной говорит, в какую сторону склон поднимается: при $x = -1$ производная равна $-3$, значит, влево — вверх, вправо — вниз. Величина говорит, насколько круто. Правило спуска получается само: шагнуть против производной на небольшую её долю.

$$x \leftarrow x - \eta \, f'(x)$$

Число $\eta$ (греческая «эта») — шаг обучения; его ещё называют скоростью обучения, по-английски learning rate. Это единственная настройка спуска, и от неё зависит всё. Проверьте сами:

Пунктир — касательная, прямая с тем же наклоном, что у кривой под мячом. Стрелка внизу показывает, куда мяч прыгнет следующим шагом. Попробуйте кнопки $\eta$ по очереди.

В чаше всё считается точно. Расстояние до дна равно $x - 2$, и после шага оно становится таким:

$$x - \eta(x-2) - 2 = (1-\eta)(x-2).$$

Каждый шаг умножает расстояние до дна на $1 - \eta$. При $\eta = 0{,}1$ это $0{,}9$ — мяч ползёт. При $\eta = 1$ множитель равен нулю: ровно на дно за один шаг. При $\eta = 1{,}5$ множитель $-0{,}5$: мяч перепрыгивает дно, но каждый прыжок вдвое короче прежнего. При $\eta = 2$ получаются вечные качели, а при $\eta > 2$ каждый прыжок длиннее предыдущего, и мяч улетает. Про такое говорят «обучение разошлось»: ошибка не падает, а растёт до бесконечности.

Теперь переключите кривую на «Две ямы» и «Плато». В двух ямах мяч скатывается в ту, что ближе, даже если соседняя глубже. На плато наклон почти нулевой, поэтому и шаги почти нулевые: мяч еле движется, хотя впереди глубокая яма. Обе беды настоящие, и с обеими мы ещё встретимся — с плато уже в следующей главе.

Всё это умещается в несколько строк Python. Производную считаем «в лоб», по определению: делим подъём на крошечный шаг.

def f(x): return 0.5 * (x - 2) ** 2 def slope(f, x, h=1e-6): return (f(x + h) - f(x)) / h # rise over run, on a tiny step x = -1.0 # where the ball starts lr = 0.5 # the step size, η for step in range(8): g = slope(f, x) print(f"step {step}: x = {x:7.4f} f(x) = {f(x):7.4f} slope = {g:7.4f}") x = x - lr * g # walk against the slope

При $\eta = 0{,}5$ наклон с каждым шагом уменьшается вдвое: −3; −1,5; −0,75… — а с ним и расстояние до дна. Поменяйте lr на 2,5 и запустите снова.

Много ручек: градиент

У путника в тумане две координаты, и высота зависит от обеих: $f(x, y)$. Как измерить наклон, если направлений бесконечно много? Хитрость в том, чтобы сначала крутить ручки по одной.

Заморозим $y$ и будем двигать только $x$. Получится обычная функция одной переменной с обычной производной. Её называют частной производной по $x$ и пишут $\frac{\partial f}{\partial x}$: закруглённая «д» напоминает, что остальные переменные на это время застыли. Точно так же получается $\frac{\partial f}{\partial y}$. Вместе эти два числа и есть градиент:

$$\nabla f = \left( \frac{\partial f}{\partial x},\ \frac{\partial f}{\partial y} \right).$$

Градиент — это стрелка. Она показывает направление, в котором функция растёт быстрее всего, а её длина — насколько быстро. Нам нужно вниз, поэтому шагаем против стрелки, и правило выглядит так же, как для одной ручки:

$$(x, y) \leftarrow (x, y) - \eta \, \nabla f(x, y).$$
Та же местность, но без тумана. Каждая маленькая серая стрелка — направление спуска (минус градиент) в своей точке. Шаг мяча складывается из двух частей: по $x$ и по $y$.

Посмотрите на стрелки у мяча: шаг действительно складывается из двух независимых кусочков. Каждая частная производная говорит, как повернуть свою ручку, а вместе они дают направление самого крутого спуска. И это работает при любом числе ручек: у биграммы их 9 409, значит, градиент — это 9 409 чисел, по одному на ручку. У Ростка — 17 миллионов. Один шаг спуска сдвигает все ручки разом.

Поднимите шаг η до 0,1, и мяч начнёт метаться между берегами озера, а то и вылетит из него — как в чаше при $\eta > 2$. В узких долинах большой шаг особенно опасен: поперёк долины склоны крутые, и мяч перелетает дно, почти не продвигаясь вдоль неё.

Почему градиент смотрит именно в сторону самого крутого подъёма?

Шагнём на крошечное расстояние в направлении единичного вектора $u = (u_x, u_y)$. Вблизи точки поверхность почти плоская, поэтому высота изменится примерно на $\frac{\partial f}{\partial x} u_x + \frac{\partial f}{\partial y} u_y$. Это скалярное произведение $\nabla f \cdot u$, а оно равно $|\nabla f| \cdot |u| \cdot \cos\alpha$, где $\alpha$ — угол между векторами. Оно наибольшее при $\cos\alpha = 1$, то есть когда $u$ смотрит туда же, куда градиент, и наименьшее, когда смотрит строго против. Поперёк градиента ($\cos\alpha = 0$) высота не меняется: это линия уровня, горизонталь на карте. Поэтому стрелки спуска всегда перпендикулярны горизонталям.

Вероятности из любых чисел

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

Выход — хранить не вероятности, а свободные числа и превращать их в вероятности в последний момент. Свободные числа называются логитами (обозначим их $z$), а превращение — softmax:

$$p_i = \frac{e^{z_i}}{\sum_j e^{z_j}}.$$

Тут два действия. Экспонента $e^{z}$ делает из любого числа положительное: большие логиты превращаются в очень большие числа, отрицательные — в крошечные, но не нулевые. Деление на сумму нормирует их, так что вместе они дают ровно 1. Поиграйте с логитами пяти самых частых букв после «t»:

Ползунки — логиты, столбики — вероятности после softmax. Последний столбец пока можно не замечать: он понадобится в следующем разделе.

Нажмите «+1 ко всем»: логиты выросли, а вероятности не сдвинулись ни на волос. Softmax смотрит только на разницу между логитами: $e^{z_i + c} = e^c \cdot e^{z_i}$, и множитель $e^c$ сокращается в числителе и знаменателе. А «×2» удваивает все разницы, и распределение становится острее, увереннее. (Температура при генерации текста делает то же самое — к ней мы вернёмся в главе 11.)

В нейросетевом варианте биграмма — это просто таблица $W$ размером 97 × 97, только в клетках не вероятности, а логиты. В строке $a$ лежат логиты для буквы, которая идёт после $a$. Чтобы предсказать следующую букву, берём строку и пропускаем её через softmax. Почему это называют нейросетью? Запишем текущую букву как вектор из 97 нулей с единицей на её месте — такая запись называется one-hot. Тогда «взять строку $a$» — это умножить этот вектор на матрицу $W$. Одна матрица, один слой, ни одного скрытого — самая маленькая нейросеть на свете. В главе 4 у неё появятся скрытые слои.

Куда тянуть логиты

Для спуска нужен градиент ошибки по каждому логиту. Ошибка на одном примере — это удивление из главы 2 при виде правильной буквы $y$: $L = -\ln p_y$. Распишем её через логиты, подставив softmax:

$$L = -\ln \frac{e^{z_y}}{\sum_j e^{z_j}} = -z_y + \ln \sum_j e^{z_j}.$$

Возьмём частную производную по какому-нибудь логиту $z_i$. У первого слагаемого она равна $-1$, если $i = y$, и нулю в остальных случаях. У второго:

$$\frac{\partial}{\partial z_i} \ln \sum_j e^{z_j} = \frac{e^{z_i}}{\sum_j e^{z_j}} = p_i.$$

Складываем:

$$\frac{\partial L}{\partial z_i} = p_i - [i = y].$$

Квадратные скобки равны 1, если условие выполнено, и 0, если нет. Весь градиент — это вектор вероятностей минус one-hot правильного ответа: $p - \text{onehot}(y)$. Формулы проще в машинном обучении почти не бывает, и её стоит запомнить.

Прочтите формулу глазами спуска, который шагает против градиента. Логит правильной буквы получает толчок вверх размером $1 - p_y$: чем меньше модель верила в правильный ответ, тем сильнее толчок. Каждый неправильный логит получает толчок вниз размером $p_i$: чем увереннее модель ошиблась, тем больше ей достаётся. Вернитесь к виджету выше, выберите верную букву и понажимайте «Шаг спуска». Последний столбец — это и есть $p - \text{onehot}$, а удивление с каждым шагом тает.

Формулу стоит проверить честно: пошевелить каждый логит и посмотреть, как меняется ошибка. Как с мячом, только ручек теперь пять.

import numpy as np def softmax(z): e = np.exp(z - z.max()) # subtracting the max changes nothing, but keeps exp() from overflowing return e / e.sum() z = np.array([2.0, 1.0, 0.5, 0.0, -1.0]) # logits for h, space, o, e, i y = 0 # the right answer is 'h' p = softmax(z) loss = -np.log(p[y]) grad = p.copy() grad[y] -= 1 # our formula: p - onehot # the slow, honest check: nudge each logit a little and watch the loss nudged = np.zeros_like(z) for i in range(len(z)): dz = np.zeros_like(z) dz[i] = 1e-6 nudged[i] = (-np.log(softmax(z + dz)[y]) - loss) / 1e-6 print("p ", p.round(4)) print("formula ", grad.round(4)) print("nudging ", nudged.round(4))

Теперь от одного примера — ко всему корпусу. Ошибка биграммы — среднее удивление по всем $N$ = 41,3 миллиона пар букв. Пара «$a$, потом $b$» встречается $n_{ab}$ раз, и каждая такая встреча толкает строку $a$ по формуле выше. Суммируем все толчки и делим на $N$:

$$\frac{\partial L}{\partial W_{ab}} = \frac{n_a}{N}\left(p_{ab} - \frac{n_{ab}}{n_a}\right),$$

где $n_a$ — сколько раз встретилась буква $a$, а $\frac{n_{ab}}{n_a}$ — доля $b$ среди букв после $a$, то есть в точности вероятность из подсчёта в главе 1. Посмотрите, когда градиент обращается в ноль: когда $p_{ab} = \frac{n_{ab}}{n_a}$ для всех пар. Спуск остановится ровно там, где вероятности сети совпадут с подсчётом.

Подсчёт и спуск ведут в одну точку. Но подсчёт умеет строить только таблицы, а спуск — обучить любую модель, у которой можно вычислить градиент.

Модель дала правильной букве вероятность 0,3, а одной из неправильных — 0,5. Какие градиенты у их логитов?

Градиент равен $p - \text{onehot}$: у правильной буквы $0{,}3 - 1 = -0{,}7$, у неправильной $0{,}5 - 0 = 0{,}5$. Спуск шагает против градиента, так что логит правильной буквы растёт, а неправильной — падает.

Обучаем биграмму

Всё готово. Таблица $W$ из 9 409 логитов, в начале сплошь нули — значит, каждый из 97 символов получает вероятность 1/97, а удивление равно $\ln 97 \approx 4{,}575$ ната. Градиент берём из формулы выше, счётчики $n_{ab}$ — из той же таблицы подсчётов, что в главе 1. Нажмите «Обучать».

Каждый шаг — проход по всем 41,3 млн пар букв (в сжатом виде, через таблицу счётчиков). Пунктиры — удивление при угадывании наугад и удивление подсчитанной таблицы.

Кривая сначала падает круто, потом всё медленнее ползёт к зелёному пунктиру. За первые 100 шагов удивление опускается с 4,575 до 2,49, за 1 000 шагов — до 2,381, за 20 000 — до 2,366, всего на тысячную выше подсчёта. Таблица под графиком тоже проявляется: в строке «q» загорается «u», после пробела — «t», «a», «s». Это те же узоры, что в главе 1, только теперь сеть нашла их сама.

Нажмите на строку «q». Даже когда кривая почти легла на пунктир, эта строка ещё заметно отстаёт. После 300 шагов строка пробела расходится с подсчётом меньше чем на 1% вероятности, а строка «q» — на целых 60%. Всё дело в множителе $\frac{n_a}{N}$: чем реже буква, тем слабее толчок её строке. Пробел — 18% всех букв, «q» — три сотых процента. Частые буквы учатся первыми, редкие — последними. Эта несправедливость — одна из причин, по которым настоящие модели учат не голым спуском, а оптимизаторами вроде Adam, которые подбирают шаг для каждого числа отдельно (глава 10).

Почему последние тысячные даются так тяжело? В таблице 7 051 клетка с нулём: эти пары ни разу не встретились, и подсчёт даёт им вероятность ровно 0. Сеть так не умеет: чтобы $e^{z}$ стало нулём, логит должен уйти в минус бесконечность. Спуск честно толкает такие логиты вниз, но толчок пропорционален самой вероятности $p$, а она с каждым шагом меньше. Сеть бесконечно приближается к подсчёту, но никогда его не достигает.

Теперь поиграйте с шагом. При η = 5 спуск идёт, но вяло: через 1 000 шагов удивление всё ещё 2,49. При η = 150 кривая сначала падает, а потом начинает дрожать около 2,5 и ниже не опускается: для строк самых частых букв такой шаг велик — как $\eta > 2$ в чаше, — и они раскачиваются. Одной строке пробела достаётся почти пятая часть всех толчков, да и строки «e», «t», «h» отстают не так уж сильно. При η = 500 обучение не сходится вовсе: удивление скачет где-то между 6 и 9,5 — хуже, чем при угадывании наугад. Ровно как мяч в чаше, только в 9 409 измерениях.

И последний опыт: начните со случайных логитов вместо нулей. Удивление стартует выше, около 5,0 (случайная модель уверенно ошибается), но приходит в ту же точку. У этой задачи одна-единственная долина без ложных ям (математики говорят, что функция потерь выпуклая), и откуда ни начни, спуск найдёт её дно. У настоящих нейросетей долина не одна, и всё же на практике спуск раз за разом находит хорошие места. Почему так выходит, до конца не понимают даже исследователи.

Вот тот же спуск на numpy: 600 шагов, пара секунд прямо в браузере.

import json import numpy as np from pyodide.http import open_url # in the browser; on your computer: open('bigram-counts.json') data = json.load(open_url('/llm/data/bigram-counts.json')) C = np.array(data['counts'], dtype=float) # C[a, b]: how often b came right after a N = C.sum() # 41.3 million pairs n = C.sum(axis=1, keepdims=True) # how often each letter a occurs W = np.zeros((97, 97)) # logits; all equal = every symbol gets 1/97 lr = 50 for step in range(601): P = np.exp(W - W.max(axis=1, keepdims=True)) P /= P.sum(axis=1, keepdims=True) # softmax, row by row loss = -(C * np.log(P)).sum() / N # average surprise per letter if step % 100 == 0: print(f"step {step:4d} loss {loss:.4f}") W -= lr * (n * P - C) / N # the gradient: (n_a / N) * (p - counted) q, u = data['alphabet'].index('q') + 1, data['alphabet'].index('u') + 1 print("p(u | q): network", round(P[q, u], 3), " counting", round(C[q, u] / n[q, 0], 3))

В настоящих проектах градиент руками не выводят: библиотека вроде PyTorch находит его сама. Проверим, что она получает ровно нашу формулу (этот код — для вашего компьютера, в браузере PyTorch не запустится):

import torch import torch.nn.functional as F C = torch.tensor(data['counts'], dtype=torch.float64) N, n = C.sum(), C.sum(1, keepdim=True) W = torch.zeros(97, 97, dtype=torch.float64, requires_grad=True) loss = -(C * F.log_softmax(W, dim=1)).sum() / N loss.backward() # PyTorch works out the gradient by itself P = F.softmax(W, dim=1).detach() print(f"loss {loss.item():.4f}") print(torch.allclose(W.grad, (n * P - C) / N)) # the same as our formula?

Как loss.backward() справляется с любой, даже очень запутанной формулой, — тема следующей главы.

Сглаживание — это штраф

У подсчитанной таблицы есть неприятная черта: пары, которых не было в корпусе, получают вероятность ноль. А удивление при нулевой вероятности бесконечно: одна непредвиденная пара — и оценка всего текста рушится. Поэтому к каждому счётчику обычно добавляют понемногу, например по единице. Это сглаживание; модель bigram-char, которая лепетала в конце главы 1, сглажена именно так.

У нейросети есть свой способ сделать то же самое — добавить к ошибке штраф за большие логиты:

$$L_{\text{всего}} = L + \lambda \sum_{a,b} W_{ab}^2.$$

Градиент штрафа равен $2\lambda W_{ab}$: он тянет каждый логит к нулю, а значит, всю строку — к равномерному распределению. Данные тянут к подсчёту, штраф — к «все буквы равны», и сеть останавливается где-то посередине. Где данных много, побеждают данные; где мало — штраф. Сглаживание ведёт себя точно так же: лишняя единица почти ничего не меняет в строке пробела с её 7,5 миллиона пар, но сильно размывает строку редкой буквы.

Попробуйте штраф λ в виджете выше (после «Сначала»). Вот что выходит за 20 000 шагов:

λудивлениеp(u | q)p(u | Q)
02,3660,9940,93
10⁻⁶2,3710,9490,57
10⁻⁵2,4030,6670,03
10⁻⁴2,5770,0450,01

Чем сильнее штраф, тем выше удивление на обучающем тексте: сеть сознательно отказывается подгонять таблицу под данные до последней цифры. Строка «Q» (958 раз в корпусе) размывается уже от слабого штрафа, строка «q» (13 061 раз) держится дольше. Зачем это нужно, если удивление растёт? Затем, что нам важен не обучающий текст, а тот, которого модель ещё не видела. Биграмме, обученной на 41 миллионе букв, заучивать почти нечего: на новом тексте подсчитанная таблица удивляется на 2,364 ната — практически как на старом. А моделям с миллионами параметров штраф помогает не заучивать случайные особенности обучающих данных. Такой приём называют регуляризацией, а этот её вид — L2-регуляризацией, или затуханием весов (weight decay); у оптимизатора AdamW, которым учат большие модели, для него есть отдельная ручка.

Росток сейчас

Росток по-прежнему видит только одну предыдущую букву, поэтому лепечет так же, как в главе 1. Но по духу это уже другая модель: её числа не посчитаны, а найдены спуском — тем же способом, каким обучены все 17 миллионов чисел настоящего Ростка. Ниже три текста, написанные с одинаковыми бросками кубика: до обучения, вашей сетью и подсчитанной таблицей. Если вы ещё не обучали сеть, это можно сделать прямо здесь.

Чтобы видеть дальше одной буквы, модели нужны скрытые слои, а градиент через них уже не выведешь на листочке, как $p - \text{onehot}$. В следующей главе мы научимся находить его автоматически — обратным распространением ошибки.

Главы

  1. 0 Знакомство
  2. 1 Считаем буквы
  3. 2 Мера удивления
  4. 3 Градиентный спуск
    1. В тумане
    2. Производная — это наклон
    3. Много ручек: градиент
    4. Вероятности из любых чисел
    5. Куда тянуть логиты
    6. Обучаем биграмму
    7. Сглаживание — это штраф
    8. Росток сейчас
  5. 4 Обратное распространение
  6. 5 Эмбеддинги
  7. 6 Токены
  8. 7 Внимание
  9. 8 Трансформер
  10. 9 Корпус
  11. 10 Обучение
  12. 11 Выборка
  13. 12 Разговор
  14. 13 LoRA
  15. 14 Что дальше