Глава 5 из 17 30 мин
Буквы как точки
Заменим номер символа обучаемым вектором. Посмотрим на карту таких векторов, разберём нужную арифметику и проследим обучение по двум кривым ошибки.
В этой главе
- объяснить, чем эмбеддинг лучше номера буквы и one-hot-вектора
- считать сходство векторов через скалярное произведение и косинус, а умножение матриц видеть как пачку скалярных произведений
- прочитать настоящий цикл обучения на PyTorch и отличить обучение от переобучения
Прежде чем предсказать четвёртый символ, наша сеть заменяет каждый из трёх предыдущих вектором из шестнадцати чисел. Почему именно такие числа? Их значения подбирались во время обучения вместе с остальными параметрами. Посмотрим, как устроено это представление и какие свойства текста в нём можно заметить.
Шестнадцатимерный вектор нельзя напрямую показать точкой на плоскости без потери информации. Поэтому для этого опыта мы обучили отдельную сеть с более узкими эмбеддингами — всего по два числа на символ. Их можно использовать как координаты. Перед нами будут сами обученные векторы этой сети, а не проекция шестнадцатимерных векторов из прошлой главы.
Карта, которую нарисовала сеть
Ниже — все 97 символов, которые знают наши буквенные модели: строчные и заглавные буквы, цифры, знаки препинания, пробел, перевод строки и метка начала текста. Место каждого на карте — это его два числа после обучения. Раскраску по группам добавили мы; сеть о группах ничего не знала.
Присмотритесь: порядок виден сразу. Гласные a, e, i, o, u собрались в одном углу, и рядом с ними их заглавные A, I, U, O, E. Точка, восклицательный и вопросительный знаки, двоеточие и точка с запятой держатся вместе на противоположном краю. Пробел, перевод строки и метка начала текста сбились в кучку: для сети это одно и то же событие — «здесь кончилось слово». Многие заглавные стоят рядом со своей строчной парой: t и T, h и H, n и N.
А редкие значки вроде ^, {, ~ и | толпятся в середине без всякого порядка. Мы проверили: они почти не сдвинулись с тех случайных мест, где оказались до обучения. В текстах эти значки попадаются редко, и сеть их просто не трогала.
Мы не сообщали сети, какие буквы гласные. Группы возникают потому, что символы часто встречаются в сходных контекстах и требуют похожих продолжений. Близкие векторы позволяют следующим слоям обрабатывать такие случаи сходным образом. Но в функции потерь нет отдельного требования «сдвинуть все гласные вместе»: расположение складывается при совместном обучении эмбеддингов и остальных весов. В другом запуске карта может повернуться или выглядеть иначе.
Проверьте, что сеть действительно видит только координаты. В окошке прогноза — контекст «␣th»: пробел, t, h. После него сеть с вероятностью около 76% ждёт e — дальше, скорее всего, будет the. Теперь перетащите букву h в угол к гласным, поближе к a. Прогноз перевернётся: сеть начнёт ждать согласные m и l, как после «␣ta». Вы изменили два обученных параметра эмбеддинга; остальные веса остались прежними. А если утащить h к точке, сеть решит, что предложение кончилось, и будет ждать перевод строки.
Эмбеддинг — обучаемый вектор символа или токена. Его координаты подбираются так, чтобы модель лучше решала свою задачу; близость векторов иногда помогает увидеть, какие случаи она обрабатывает сходным образом.
Номер, выключатели и точка
Разберёмся, зачем вообще понадобились эти координаты. В главе 1 каждый символ получил номер: пробел — 2, a — 67, b — 68, z — 92. Почему бы не подать в сеть сам номер?
Плохая идея. Сеть умножает входы на веса и складывает, то есть обращается с ними как с величинами. Для неё b окажется «чуть больше» a, а тильда с номером 96 — «огромной» буквой. Номера мы раздали по порядку таблицы ASCII, и никакого смысла в их величине нет.
Следующая мысль — one-hot, «один горячий». Заводим ряд из 97 выключателей, по одному на символ, и для буквы a включаем только выключатель номер 67. Получается вектор из 97 чисел: одна единица, остальные нули. Никаких ложных «больше-меньше» — все символы равноправны.
Расстояния между разными one-hot-векторами одинаковы: такая запись сама по себе не выражает сходство «a» и «A». Однако сеть, получающая one-hot-векторы, всё равно может научиться общим закономерностям. Если умножить их на обучаемую матрицу, мы как раз получим эмбеддинги. Разница в том, что плотный короткий вектор удобнее хранить и передавать дальше, чем 97 чисел с единственной единицей.
Эмбеддинг — третий путь. Каждому символу выдаём короткий вектор из $d$ обычных чисел — у нашей карты $d = 2$, у сети из прошлой главы 16, у Ростка 384 — и позволяем обучению эти числа двигать. Все такие векторы хранятся в одной таблице $E$ размером $97 \times d$: строка номер 67 — это вектор буквы a. Чтобы превратить букву в вектор, достаточно взять нужную строку.
Насколько похожи два вектора
На карте можно измерять расстояния между точками, а для векторов часто полезно сравнивать направления. Начнём со скалярного произведения: перемножим соответствующие координаты и сложим результаты. Оно зависит и от угла между векторами, и от их длин, поэтому само по себе ещё не является чистой мерой сходства направлений:
$$a \cdot b = a_1 b_1 + a_2 b_2 + \dots + a_d b_d.$$Для двух измерений это две пары множителей. Поиграйте со стрелками и посмотрите, когда число получается большим, когда обращается в ноль, а когда уходит в минус.
Закономерность такая: стрелки смотрят в одну сторону — произведение большое и положительное; под прямым углом — ноль; в разные стороны — отрицательное. Но попробуйте кнопку «b вдвое длиннее»: направление то же, а число выросло вдвое («b вдвое короче» так же вдвое его уменьшит). Скалярное произведение чувствует и направление, и длину.
Чтобы сравнить только направления двух ненулевых векторов, разделим скалярное произведение на произведение их длин. Получится косинусное сходство от −1 до 1. Для нулевого вектора направление не определено, и этой формулой пользоваться нельзя:
$$\cos\theta = \frac{a \cdot b}{|a|\,|b|}, \qquad |a| = \sqrt{a \cdot a} = \sqrt{a_1^2 + a_2^2 + \dots}$$Этот приём мы будем встречать постоянно. Нейрон из прошлой главы — это скалярное произведение входа на веса плюс сдвиг. Внимание в главе 7 решает, какие слова важны, через скалярные произведения. И даже последний шаг Ростка — вероятность каждого из 8 192 токенов — начинается с 8 192 скалярных произведений.
Умножение матриц — это много скалярных произведений
Нейрон берёт вход и считает одно скалярное произведение. Слой из 256 нейронов считает 256 скалярных произведений с одним и тем же входом. А сеть при обучении обрабатывает не одно окно из трёх букв, а сотни окон сразу. Сотни входов на сотни нейронов — это десятки тысяч скалярных произведений, и записывать их поодиночке никто не станет. Для этого придумана одна операция — умножение матриц.
Матрица — прямоугольная таблица чисел. Произведение $C = A B$ устроено так: клетка $c_{ij}$ равна скалярному произведению строки $i$ матрицы $A$ на столбец $j$ матрицы $B$:
$$c_{ij} = \sum_{k} a_{ik}\, b_{kj}.$$Отсюда правило размеров: длина строки $A$ должна совпадать с высотой столбца $B$. Матрица $n \times k$ умножается на $k \times m$, и получается $n \times m$. Посмотрите, как это выглядит:
Теперь переключите виджет на «One-hot × таблица». Слева — слово из трёх букв, записанное one-hot-строками, сверху — маленькая таблица эмбеддингов (настоящие координаты с карты). Посчитайте клетки: в каждом скалярном произведении работает только одно слагаемое, то, что стоит под единицей, а остальные умножаются на ноль. В результате получаются просто строки таблицы.
Умножение one-hot-вектора на таблицу просто выбирает одну строку. Поэтому в коде используется обращение по индексу: E[id]. Оно не требует создавать большой вектор нулей и выполнять лишние умножения. Математический результат тот же; выигрыш по времени зависит от размеров и реализации. Проверим равенство на NumPy:
Значок @ в numpy и PyTorch означает умножение матриц. В таблице косинусов видно то же, что на карте: у a и e сходство +0,86, а h с обеими гласными почти противоположна.
Тензоры и пачки
Осталось ещё одно слово — тензор. Это просто таблица чисел с любым количеством измерений: число — ноль измерений, вектор — одно, матрица — два. Пачка из $B$ окон по 3 буквы — это тензор номеров формы $(B, 3)$. Когда мы достаём эмбеддинги, у каждой буквы появляется 16 чисел, и форма становится $(B, 3, 16)$. Три вектора каждого окна склеиваем в один длинный — $(B, 48)$ — и умножаем на матрицу весов скрытого слоя. Проследим за формами на всём пути от номеров до логитов:
Одна строчка flat @ W1.T — это $4 \times 256 = 1\,024$ скалярных произведения по 48 слагаемых. Видеокарты и процессоры умеют делать такие умножения невероятно быстро, и вся современная нейросетевая техника держится на том, что почти любую работу удаётся записать через умножение матриц.
Что получится, если one-hot-строку буквы «t» умножить на таблицу эмбеддингов?
В one-hot-строке одна единица, остальные нули. В каждом скалярном произведении выживает одно слагаемое, и в итоге получается ровно строка «t». Поэтому умножение заменяют простым E[id].
Та же сеть на PyTorch
До сих пор мы писали всё руками. Настоящие модели курса написаны на PyTorch — библиотеке, которая умеет три вещи: хранить тензоры, быстро их перемножать (в том числе на видеокарте) и самостоятельно считать градиенты — как наш Value из прошлой главы, только для тензоров любого размера. Вот класс, из которого сделаны все буквенные сети курса, — от карты в начале главы до модели в конце. Он взят из файла snapshots.py без изменений:
Три детали, и все они нам уже знакомы:
nn.Embedding(vocab, d_embed)— таблица $E$ размером 97 ×d_embed. Вызовself.embed(x)достаёт строки по номерам — тот самыйE[x]. Градиент при обучении попадает только в строки тех букв, которые были в пачке.nn.Linear(a, b)— слой изbнейронов сaвходами каждый: матрица весов $b \times a$ и вектор сдвигов. Вызов слоя — этоx @ W.T + bias, пачка скалярных произведений.flatten(1)склеивает векторы букв окна в один: форма $(B, 3, 16)$ превращается в $(B, 48)$.
Карта в начале главы — это MLP(97, 3, 2, [128]): три буквы по два числа, скрытый слой на 128 нейронов. Сеть из прошлой главы — MLP(97, 3, 16, [256]). Модель, которую вы увидите в конце, — MLP(97, 8, 24, [512, 512]): восемь букв по 24 числа и два скрытых слоя.
Учимся пачками
Как такую сеть обучить? Идея та же, что в главе 3: посчитать ошибку, найти градиент, сделать шаг вниз. Вопрос в том, на скольких примерах считать ошибку перед каждым шагом.
Можно на всех сразу. Но в нашем буквенном корпусе десятки миллионов окон, и ради одного маленького шага пришлось бы прогнать их все. Можно на одном — тогда шаги быстрые, но каждый дёргается в свою сторону: одно окно ничего не говорит о языке в целом. Золотая середина — мини-батч (minibatch): берём пачку из нескольких сотен случайных окон, усредняем по ним ошибку и делаем шаг. Направление получается не идеальным, но в среднем верным, а считается такая пачка за доли секунды — умножением матриц.
Вот настоящий цикл обучения всех буквенных моделей курса, тоже из snapshots.py:
Прочитаем его сверху вниз.
windowsнарезает текст на окна: нужное число букв контекста плюс следующая буква — правильный ответ. Первые 2% окон (len(data) // 50) откладываются в сторону — о них ниже.- Каждый шаг берёт 512 случайных окон:
b[:, :-1]— контекст,b[:, -1]— ответы. F.cross_entropy— наша старая знакомая из главы 2: softmax по логитам, минус логарифм вероятности правильного ответа, среднее по пачке. Одна функция вместо трёх строк.loss.backward()— обратный проход из главы 4: PyTorch сам находит градиент по каждому из сотен тысяч параметров, включая строки таблицы эмбеддингов.opt.step()— шаг вниз. Вместо простого градиентного спуска здесь AdamW, более хитрый способ выбирать шаг для каждого параметра; его разберём в главе 10. Последние две строки плавно уменьшают скорость обучения к концу.
Модель из конца главы прошла 20 000 таких шагов, то есть увидела около 10 миллионов окон. На ноутбуке с M4 Pro это заняло около трёх минут.
Экзамен на незнакомом тексте
Зачем откладывать 2% окон? Затем, что ошибка на обучающем тексте врёт. Сеть может научиться хорошо предсказывать именно те окна, которые видела, — просто запомнив их, — и при этом ничего не понять о языке. Проверить можно только одним способом: дать ей текст, которого она никогда не видела, и посмотреть на ошибку там. Отложенный текст называют проверочным (validation), остальной — обучающим.
Лучше всего увидеть это вживую. Ниже — крошечная сеть с двумя числами на букву, которая учится прямо у вас в браузере на кусочке настоящих историй из нашего корпуса. Рядом с её картой букв — две кривые: ошибка на обучающем тексте и на проверочном. Начните с 3 000 букв, потом попробуйте 400 и весь текст.
На всём тексте обе кривые идут вниз вместе, а гласные одна за другой отползают от кучки согласных — карта складывается на глазах. На 400 буквах происходит другое. Обучающая ошибка падает гораздо ниже, чем на большом тексте, а проверочная сначала чуть снижается, потом разворачивается и уходит вверх — выше, чем если бы сеть гадала наугад. У сети 1 764 параметра, а примеров меньше 400: ей проще запомнить каждое окно наизусть, чем искать правила. Это называется переобучением (overfitting). На 3 000 буквах видно, как оно начинается: проверочная ошибка какое-то время снижается, находит минимум и медленно ползёт вверх.
При обучении следите за обеими кривыми. Обучающая показывает, удаётся ли оптимизация, а проверочная — переносится ли улучшение на отложенные данные. Если разрыв растёт, могут помочь более разнообразные данные, регуляризация, меньшая модель или ранняя остановка. Росток обработал около 330 миллионов токенов из корпуса размером 1,43 миллиарда. Это уменьшает число повторных встреч по сравнению с многократным проходом по небольшому набору, но не исключает запоминания или повторов при случайном выборе окон. После подбора настроек полезна и отдельная тестовая выборка, которая не участвовала в выборе модели.
Ошибка на обучающем тексте падает, а на проверочном растёт. Что происходит?
Это переобучение. Модель всё лучше помнит то, что видела, и всё хуже работает на новом. Помогают больше данных, модель поменьше или ранняя остановка там, где проверочная ошибка была минимальной.
Шире окно — умнее модель
Теперь у нас есть всё, чтобы сделать модель сильнее. Самое очевидное — дать ей видеть больше. Биграмма из главы 1 смотрела на одну букву, сеть из прошлой главы — на три. Возьмём восемь, дадим каждой букве 24 числа и добавим второй скрытый слой. Все эти модели мы проверили на одном и том же тексте, которого ни одна из них не видела при обучении, — 4,9 миллиона букв:
| Модель | Видит букв | Параметров | Наты на букву |
|---|---|---|---|
| наугад из 97 символов | 0 | 0 | 4,575 |
| биграмма, глава 1 | 1 | 9 409 | 2,364 |
| карта, 2 числа на букву | 3 | 13 603 | 1,874 |
| сеть, глава 4 | 3 | 39 025 | 1,479 |
| сеть, эта глава | 8 | 413 561 | 1,103 |
Каждая строчка — заметный шаг. Даже крошечная карта с двумя числами на букву уже сильно обходит биграмму: ей помогают три буквы контекста и то, что похожие буквы делят опыт. Больше чисел на букву и больше нейронов дают 1,479. Восемь букв — 1,103. Для сравнения: перплексия (глава 2) падает с $e^{2{,}364} \approx 10{,}6$ «равновероятных вариантов» на букву у биграммы до $e^{1{,}103} \approx 3{,}0$.
Посмотрите на разницу своими глазами: три модели пишут одновременно, с одного начала и с одинаковой температурой.
Биграмма лепит обрывки, похожие на слова. Сеть на трёх буквах пишет уже много настоящих коротких слов. Восьмибуквенная складывает целые обороты из историй — «there was a little», «One day,» или «felt a», — но смысл всё равно рвётся через каждые несколько слов. Причина понятна: восемь букв — это полтора-два слова. Всего, что было раньше, модель не видит вообще.
Почему бы не взять окно в 500 букв?
Первый слой такой сети получает на вход все векторы окна подряд. У нашей модели это 8 × 24 = 192 числа, и первый слой хранит 192 × 512 = 98 304 веса. При окне в 500 букв весов в первом слое стало бы в 62 раза больше, и для их надёжного обучения понадобились бы подходящие данные.
Но хуже другое. Каждое место в окне обрабатывается своими весами: буква «t» на первой позиции и та же «t» на пятой проходят через разные числа. Всё, что сеть выучила про «the» в начале окна, приходится учить заново для каждого сдвига. Это расточительно, и из этого тупика есть два выхода. Первый — сделать кусочки крупнее, чтобы в то же окно помещалось больше текста: это токены, следующая глава. Второй — механизм, который обращается с любой позицией одинаково и сам решает, куда смотреть: внимание, глава 7.
Эмбеддинги, скалярные произведения и умножение матриц — три кирпича, из которых сложен весь Росток. Всё остальное — способы решить, какие числа с какими перемножать.
Росток сейчас
Росток пока читает по буквам, но видит сразу восемь и знает, какие буквы похожи друг на друга. Он пишет почти настоящие английские фразы, хотя через пару слов забывает, о чём начинал. Удивление упало с 2,364 ната на букву у биграммы до 1,103. В следующей главе мы перестанем кормить его буквами: в окне останется восемь ячеек, но в каждую ляжет целый кусок слова.