Глава 5 из 14 30 мин
Буквы как точки
Нейросеть из прошлой главы держала для каждой буквы короткий список чисел. Посмотрим, что это за числа, почему похожие буквы оказываются рядом и как учить такую сеть на миллионах примеров.
В этой главе
- объяснить, чем эмбеддинг лучше номера буквы и one-hot-вектора
- считать сходство векторов через скалярное произведение и косинус, а умножение матриц видеть как пачку скалярных произведений
- прочитать настоящий цикл обучения на PyTorch и отличить обучение от переобучения
В прошлой главе нейросеть смотрела на три буквы и угадывала четвёртую. Но прежде чем что-то считать, она делала странную вещь: заменяла каждую букву списком из шестнадцати чисел. Никто эти числа не придумывал — сеть подобрала их сама, вместе со всеми остальными весами. Что в них записано?
Шестнадцать чисел не нарисуешь. Поэтому мы обучили ту же сеть ещё раз, но оставили ей всего два числа на букву. Два числа — это координаты точки на плоскости. Значит, каждую букву можно поставить на карту и посмотреть, как сеть их расставила.
Карта, которую нарисовала сеть
Ниже — все 97 символов, которые знают наши буквенные модели: строчные и заглавные буквы, цифры, знаки препинания, пробел, перевод строки и метка начала текста. Место каждого на карте — это его два числа после обучения. Раскраску по группам добавили мы; сеть о группах ничего не знала.
Присмотритесь: порядок виден сразу. Гласные a, e, i, o, u собрались в одном углу, и рядом с ними их заглавные A, I, U, O, E. Точка, восклицательный и вопросительный знаки, двоеточие и точка с запятой держатся вместе на противоположном краю. Пробел, перевод строки и метка начала текста сбились в кучку: для сети это одно и то же событие — «здесь кончилось слово». Многие заглавные стоят рядом со своей строчной парой: t и T, h и H, n и N.
А редкие значки вроде ^, {, ~ и | толпятся в середине без всякого порядка. Мы проверили: они почти не сдвинулись с тех случайных мест, где оказались до обучения. В текстах эти значки попадаются редко, и сеть их просто не трогала.
Почему гласные оказались вместе? Никто не говорил сети, что это гласные. Но у них похожая судьба: после них обычно идут согласные, перед ними — тоже согласные. Сети выгодно, чтобы буквы, которые ведут себя одинаково, выглядели для неё одинаково: тогда то, что она выучила про a, само собой работает и для o. Обучение подталкивало такие буквы друг к другу, пока они не оказались по соседству.
Проверьте, что сеть действительно видит только координаты. В окошке прогноза — контекст «␣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, как на запятую. Всё, что сеть узнает про a, ничего не скажет ей про A, — каждую букву придётся изучать с нуля. И вектор выходит длинным и пустым: 96 нулей ради одной единицы.
Эмбеддинг — третий путь. Каждому символу выдаём короткий вектор из $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 буквах видно, как оно начинается: проверочная ошибка какое-то время снижается, находит минимум и медленно ползёт вверх.
Отсюда два практических правила. Первое: судить о модели только по проверочной ошибке. Второе: лучшее лекарство от переобучения — больше данных. Росток при 17 миллионах параметров прочитал за обучение 330 миллионов токенов — меньше четверти корпуса, так что почти всё прочитанное он видел впервые и зубрить ему было нечего.
Ошибка на обучающем тексте падает, а на проверочном растёт. Что происходит?
Это переобучение. Модель всё лучше помнит то, что видела, и всё хуже работает на новом. Помогают больше данных, модель поменьше или ранняя остановка там, где проверочная ошибка была минимальной.
Шире окно — умнее модель
Теперь у нас есть всё, чтобы сделать модель сильнее. Самое очевидное — дать ей видеть больше. Биграмма из главы 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. В следующей главе мы перестанем кормить его буквами: в окне останется восемь ячеек, но в каждую ляжет целый кусок слова.