Глава 4 из 14 40 мин
Как ошибка течёт назад
Чтобы учить модель спуском, нужно знать, насколько каждое её число виновато в ошибке. Обратное распространение находит это для всех чисел сразу — и к концу главы вы напишете его сами.
В этой главе
- проследить, как градиент течёт назад по графу вычислений, и проверить его на деле
- написать свой автоград на Python и обучить им маленькую сеть
- понять, из чего сделан нейрон и зачем сети изгибы
- увидеть, как сеть с окном в три буквы обходит биграмму
В прошлой главе спуск обучил биграмму и пришёл ровно туда же, куда подсчёт. Но для этого понадобилась формула градиента $p - \text{onehot}$, и мы вывели её руками. Это было нетрудно: биграмма — одна таблица, и между числом и ошибкой всего два действия, softmax и логарифм.
Теперь мы хотим модель поумнее — такую, которая смотрит на три предыдущие буквы. Таблица для трёх букв заняла бы 97⁴ ≈ 88,5 миллиона клеток, и большинство из них остались бы пустыми. Вместо таблицы возьмём небольшую нейросеть из 39 025 чисел. Но между каждым её числом и ошибкой уже не два действия, а длинная цепочка: умножить, сложить, изогнуть, снова умножить… У Ростка таких цепочек тысячи, а чисел — 17 миллионов. Выводить градиент для каждого из них на бумаге немыслимо.
Нужен алгоритм, который по любой цепочке вычислений сам найдёт, насколько каждое число виновато в ошибке. Он называется обратным распространением ошибки (backpropagation), и это без преувеличения мотор всего глубокого обучения. Устроен он проще, чем кажется: к концу главы вы напишете его сами, в пятьдесят строк.
Кто виноват?
Любое вычисление можно нарисовать графом: входы слева, результат справа, а между ними простые операции, каждая в своём узле. Вот совсем маленький пример: $L = (a \cdot b + c) \cdot f$. Представьте, что $L$ — ошибка модели, а $a$, $b$, $c$, $f$ — её параметры. Про каждый параметр мы хотим знать одно: если его чуть-чуть увеличить, насколько изменится $L$? Это и есть градиент — мера «вины» параметра в ошибке.
Пройдите путь назад по шагам и присмотритесь к тому, что делает каждый узел. Никто не смотрит на граф целиком. Узел получает «сверху», со стороны результата, одно число — сколько вины досталось ему, — умножает его на свою местную производную и передаёт своим входам. Сумма передаёт вину без изменений. Произведение отдаёт каждому множителю вину, умноженную на другой множитель. Вот и весь секрет.
Когда путь пройден, выберите любой вход и подвигайте его. Под графом появится проверка: градиент предсказывает, насколько сдвинется $L$, если вход подрастёт на 0,01, а рядом — настоящий сдвиг. Они совпадают. Градиент — не абстракция, а очень конкретное обещание.
Цепное правило
Почему вину нужно именно умножать? Представьте три шестерёнки. Первая вращается втрое быстрее второй, вторая вдвое быстрее третьей. Во сколько раз первая быстрее третьей? В шесть: $3 \cdot 2$. Производная и есть такое «во сколько раз»: насколько сильно выход откликается, если пошевелить вход. Когда величины сцеплены в цепочку, отклики перемножаются.
На языке формул: если $L$ зависит от $d$, а $d$ — от $e$, то
$$\frac{\partial L}{\partial e} = \frac{\partial L}{\partial d} \cdot \frac{\partial d}{\partial e}.$$Это цепное правило. Первый множитель — вина, пришедшая сверху; второй — местная производная узла. Местных производных нам понадобится совсем немного, и все они школьные:
- сумма $d = e + c$: $\frac{\partial d}{\partial e} = \frac{\partial d}{\partial c} = 1$ — вина проходит насквозь и целиком достаётся обоим слагаемым;
- произведение $e = a \cdot b$: $\frac{\partial e}{\partial a} = b$, $\frac{\partial e}{\partial b} = a$ — множители «меняются местами»;
- tanh $y = \tanh(z)$: $\frac{\partial y}{\partial z} = 1 - y^2$ — удобно, что производная считается из уже известного выхода.
Цепное правило проще всего почувствовать на толчке. Ниже цепочка из трёх звеньев: $u = 1{,}5x$, потом $y = \tanh u$, потом $L = y^2$. Толкните $x$ на $\Delta x$ — и толчок побежит по цепочке, в каждом звене умножаясь на местный наклон. До $L$ он доберётся умноженным на произведение всех наклонов, а это и есть $\frac{\partial L}{\partial x}$.
Проверим на нашем графе: $a = 2$, $b = -3$, $c = 10$, $f = -2$. Вперёд: $e = -6$, $d = 4$, $L = -8$. Назад: $\frac{\partial L}{\partial d} = f = -2$. Сумма пропускает вину как есть, поэтому $\frac{\partial L}{\partial e} = \frac{\partial L}{\partial c} = -2$. Произведение меняет множители местами: $\frac{\partial L}{\partial a} = -2 \cdot b = 6$ и $\frac{\partial L}{\partial b} = -2 \cdot a = -4$. Осталось $\frac{\partial L}{\partial f} = d = 4$. Ровно те числа, что показал виджет.
Одна тонкость. Что, если одно и то же число используется в двух местах? Переключите виджет на «a дважды»: там $L = a \cdot b + a$. Шевельнёте $a$ — изменятся оба слагаемых, и их изменения сложатся. Поэтому и вина, пришедшая к $a$ по двум путям, складывается: $\frac{\partial L}{\partial a} = b + 1 = -1$. В нейросетях так бывает сплошь и рядом: один вход читают сотни нейронов, одну и ту же матрицу применяют к каждой позиции текста. Правило всегда одно — сумма по всем путям.
Пусть $L = a \cdot a$ и $a = 3$. Какой градиент $\partial L / \partial a$ насчитает обратный проход?
Узел умножения получил на оба входа одно и то же число. По каждому пути приходит вина $1 \cdot 3 = 3$, вместе $6$ — ровно производная $a^2$, то есть $2a$. Если забыть сложить вклады, выйдет 3: это самая частая ошибка у тех, кто пишет свой автоград.
Обратный проход целиком
Соберём алгоритм. В нём два прохода по графу.
- Вперёд. Вычисляем узлы от входов к выходу и запоминаем каждое промежуточное значение — они понадобятся для местных производных.
- Назад. Выход получает вину 1. Дальше идём по узлам в обратном порядке, так чтобы каждый узел обрабатывался только после всех, кто им пользуется. Каждый умножает свою вину на местные производные и прибавляет результат к вине своих входов.
Порядок «каждый узел после всех своих пользователей» называется обратным топологическим. Он гарантирует, что к моменту, когда узел передаёт вину дальше, он уже собрал её со всех путей.
Самое замечательное здесь — цена. Обратный проход стоит примерно столько же, сколько прямой (на практике раза в два дороже), и за это время находит градиент сразу по всем параметрам. Сравните с лобовым способом из прошлой главы: пошевелить каждый параметр по отдельности и посмотреть, как изменится ошибка. Для Ростка это 17 миллионов прогонов модели на один шаг обучения. Обратный проход обходится одним.
Как проверить, что обратный проход посчитан правильно?
Лобовой способ никуда не делся — он превратился в проверку. Возьмите несколько параметров, сдвиньте каждый на крошечное $h$ в обе стороны и посчитайте $\frac{L(w + h) - L(w - h)}{2h}$. Эта «центральная разность» точнее односторонней, которой мы пользовались в прошлой главе. Если она совпадает с градиентом из обратного прохода до нескольких знаков, всё в порядке; если нет — где-то забыт множитель или +=. Исследователи, которые пишут новые слои вручную, делают такую проверку (gradient check) почти всегда: ошибки в обратном проходе коварны, потому что сеть с неправильным градиентом часто всё равно как-то учится, просто хуже.
Обратное распространение — это цепное правило, применённое с конца: каждый узел умножает пришедшую вину на свою местную производную и отдаёт её входам, а вина, пришедшая по разным путям, складывается.
Автоград на 50 строк
Напишем всё это на Python. Идея такая: вместо обычных чисел использовать объекты Value, которые помнят, из чего получились. Каждая операция создаёт новый Value и заодно записывает маленькую функцию _backward — как передать вину своим входам. Метод backward() выстраивает узлы в топологическом порядке и вызывает эти функции с конца.
Обратите внимание на += в каждой функции _backward: это то самое «вклады складываются». Проверим класс на графе из виджета. Ячейки на странице работают как тетрадка: если запустить следующую, ячейка с классом выполнится сама.
Ровно те числа, что показывал виджет. Этот класс — упрощённый micrograd Андрея Карпатого (2020): в оригинале есть ещё степень, ReLU и пара удобств, но сердце то же. PyTorch устроен по тому же принципу, только его «значения» — целые тензоры, а местные производные написаны для сотен операций.
Зачем перед каждым шагом обнулять grad?
Из-за того же +=. Градиенты копятся, и если их не обнулить, вина с прошлого шага сложится с новой. В PyTorch для этого есть optimizer.zero_grad(), и забытый вызов — классическая ошибка новичка. Иногда накопление, наоборот, используют нарочно: считают градиенты по нескольким порциям данных подряд и только потом делают шаг. Так имитируют большой батч, когда он не помещается в память.
Нейрон
Алгоритм, который умеет обучать любую цепочку операций, у нас есть. Из каких кирпичиков строить саму модель? Главный кирпичик нейросети — нейрон. Он делает три вещи: умножает каждый вход на свой вес, складывает всё вместе со сдвигом $b$ и пропускает сумму через изгибающую функцию — функцию активации:
$$y = \tanh(w_1 x_1 + w_2 x_2 + b).$$Веса и сдвиг — параметры, их подбирает спуск. Кстати, третий вариант графа выше, «нейрон», — ровно эта формула, разобранная на узлы.
Поиграйте с весами. Сумма $w_1 x_1 + w_2 x_2 + b$ постоянна вдоль прямых, поэтому один нейрон всегда делит плоскость прямой линией: по одну сторону «да», по другую «нет». Веса поворачивают эту прямую, сдвиг её двигает, а чем веса больше, тем резче переход.
Функций активации много; вот две самые важные.
- tanh — плавная ступенька от −1 до 1. Она стоит в сети mlp-char-3, которую мы разберём в конце главы.
- ReLU, $\max(0, z)$, — «положительное пропустить, отрицательное отрезать». Проще некуда, а в глубоких сетях работает отлично; в начале 2010-х она вытеснила плавные функции (Наир и Хинтон, 2010; Глоро, Борд и Бенджио, 2011). В трансформерах, включая Ростка, используют её гладких родственников — вернёмся к ним в главе 8.
Теперь посмотрите на наклон под графиком активации. Для обратного прохода это и есть местная производная нейрона: сколько вины он пропустит назад. У tanh наклон наибольший около нуля и почти исчезает по краям: при $|z| = 3$ он уже меньше 0,01. Нейрон, который «упёрся» в ±1, почти не пропускает вину и почти не учится — это то самое плато из прошлой главы. У ReLU наклон равен 1 для любого положительного $z$ и 0 для отрицательного: либо всё, либо ничего.
Слои и изгибы
Один нейрон рисует только прямую. Поставим несколько нейронов рядом — получится слой: каждый нейрон смотрит на все входы, у каждого свои веса, и слой выдаёт столько чисел, сколько в нём нейронов. Выходы одного слоя становятся входами следующего. Такая сеть называется многослойным перцептроном, MLP.
А зачем изгиб? Попробуем без него. Слой без активации — это просто $W x + b$. Два таких слоя подряд:
$$W_2 (W_1 x + b_1) + b_2 = (W_2 W_1)\, x + (W_2 b_1 + b_2).$$Снова одна матрица и один сдвиг — то есть снова один слой. Сколько линейных слоёв ни ставь, они схлопываются в один и рисуют одну прямую. Изгиб между слоями не даёт им схлопнуться: каждый следующий слой гнёт и складывает пространство, которое получилось после предыдущего.
Попробуйте все четыре набора. Кольца и крест сеть обычно раскладывает за несколько десятков шагов, луны — за пару сотен, спираль — за тысячу с лишним, а если в слое всего два или четыре нейрона, спираль ей так и не даётся. Потом выключите изгиб («нет»): граница выпрямится и больше не согнётся, сколько ни учи. Для лун прямая почти годится (на своих местах около 93% точек), а для колец и креста она не лучше монетки.
Код этой сети — те же прямой и обратный проходы, только записанные сразу для целого слоя: вместо отдельных узлов $a \cdot b$ — умножение матрицы на вектор. А вот та же идея на нашем Value: сеть из трёх слоёв учится выдавать нужные ответы на четыре примера.
41 параметр, 40 шагов спуска, ошибка падает с 4,5 до 0,02 — и ни одной производной, выведенной руками. Всё сделали Value и цепное правило.
То же на PyTorch
В PyTorch всё это уже встроено. Тензор с requires_grad=True — это наш Value, а backward() — наш обратный проход (этот код для вашего компьютера: в браузере PyTorch не запустится):
А вот настоящий код, которым обучена сеть из следующего раздела. Слой эмбеддингов, скрытые слои с tanh, выходной слой — и ни одной производной: их найдёт loss.backward().
Сеть mlp-char-3 — это MLP(97, 3, 16, [256]): алфавит из 97 символов, окно в 3 буквы, эмбеддинги по 16 чисел, один скрытый слой из 256 нейронов. Её учили 20 000 шагов по 512 окон за шаг оптимизатором AdamW — с той самой ручкой затухания весов (weight decay) из прошлой главы, выставленной на 0,01.
Окно в три буквы
Теперь у нас есть всё, чтобы разобрать первую настоящую нейросетевую языковую модель — ту, что описали Бенджио, Дюшарм, Винсент и Жовен в статье «A Neural Probabilistic Language Model» (2003). У них были слова, у нас буквы, но устройство то же.
- Каждая из трёх последних букв превращается в список из 16 чисел — свой эмбеддинг (от англ. embedding, «вложение»). Это просто строка обучаемой таблицы 97 × 16: у каждой буквы своя.
- Три эмбеддинга склеиваются в 48 чисел и идут в скрытый слой из 256 нейронов с tanh.
- Выходной слой превращает 256 чисел в 97 логитов, а softmax — в вероятности следующей буквы.
Сравните столбцы. После «ti» биграмма видит только «i» и гадает широко: на первом месте «n» с 27%. Сеть видит « ti» с пробелом впереди и на 73% уверена, что дальше «m», — это же «time». Попробуйте «Lily wanted to pl»: сеть на 94% ставит на «a», а биграмма после «l» колеблется между «e», «l» и «i». Или «Tom and his mo»: сеть разрывается между «r» и «m» — «mother» или «mom», — и это честное сомнение, а не гадание.
Сколько всё это стоит? Посчитаем параметры:
| часть | размер | чисел |
|---|---|---|
| эмбеддинги | 97 × 16 | 1 552 |
| скрытый слой | 48 × 256 + 256 | 12 544 |
| выходной слой | 256 × 97 + 97 | 24 929 |
| всего | 39 025 |
39 тысяч чисел вместо 88,5 миллиона клеток таблицы — в две с лишним тысячи раз меньше. И сеть не просто экономнее, она лучше. На одном и том же отложенном тексте, которого не видела ни одна из моделей, биграмма удивляется на 2,364 ната на букву, а сеть с окном в три буквы — на 1,479. В пересчёте на «число равновероятных вариантов» из главы 2 это $e^{2{,}364} \approx 10{,}6$ против $e^{1{,}479} \approx 4{,}4$: сеть выбирает следующую букву так, будто вариантов у неё вдвое с лишним меньше.
Откуда такая экономия? Таблица хранит каждую тройку отдельно и ничего не знает о сходстве между ними. Сеть делит знания: одни и те же 256 нейронов обслуживают все тройки, и то, что она выучила про «th», может пригодиться и для «Th», если эмбеддинги «t» и «T» окажутся похожими. Такой перенос знаний на похожие случаи и называется обобщением.
Сеть не заводит отдельную клетку на каждый случай, а делит знания между случаями. Поэтому 39 тысяч обученных чисел обходят таблицу, которой понадобились бы 88 миллионов.
Как именно устроены эмбеддинги и почему похожие буквы в них оказываются рядом, разберём в следующей главе.
Росток сейчас
Росток впервые стал настоящей нейросетью: эмбеддинги, скрытый слой, выходной слой — и всё обучено обратным распространением ошибки. Три буквы памяти заметно меняют его лепет: слова стали длиннее, настоящие попадаются чаще, пробелы и знаки препинания встают на свои места. Но фразы по-прежнему не складываются: к концу длинного слова сеть уже не помнит, с чего оно началось.
Дальше — больше памяти. В следующей главе мы посмотрим, что на самом деле выучили эмбеддинги, научимся писать такие сети на PyTorch как следует и расширим окно до восьми букв.