Глава 1 из 17 30 мин
Считаем буквы
Посчитаем, какие символы идут друг за другом в английских историях и диалогах. Из таблицы частот получится первая модель, которая сама продолжает текст.
В этой главе
- превратить текст в числа — ровно такие, какие видит модель
- посчитать пары букв и получить из счёта условные вероятности
- вытянуть случайную букву из распределения с помощью кубика с перевесом — и заставить таблицу писать
Начнём с вопроса, который можно решить без нейросети: какая буква чаще всего идёт после «t»? Посчитаем такие пары в тексте и превратим частоты в вероятности. Получится небольшая языковая модель — таблица, способная выбирать следующую букву. Подсчёт и будет её обучением: параметры мы найдём сразу по данным, без многократных шагов оптимизации.
Идея такая. Учитывать всё, что было раньше, трудно, поэтому схитрим и посмотрим только на последнюю букву. Допустим, текст пока заканчивается на «t». Что дальше? Чутьё у вас уже есть: часто «h» (the, that, then), иногда пробел, иногда «o». Такому правилу вас никто не учил — вы просто много читали. Компьютер тоже умеет много читать, а вести счёт у него получается куда лучше нашего.
Буквы превращаются в числа
Чтобы выполнять расчёты, программе нужны числа. Поэтому сначала договоримся, какие единицы текста использует наша модель, и присвоим каждой номер. В этой главе единицей будет отдельный символ. Позже мы перейдём к токенам, которые могут включать несколько символов.
В первых главах курса алфавит Ростка состоит из 97 символов. Это 95 печатных знаков обычной английской клавиатуры (пробел, знаки препинания, цифры, заглавные и строчные латинские буквы), перевод строки и одна особая метка под номером 0. Мы будем рисовать её так: ◆. Она стоит на границе текстов — перед первой буквой каждой истории и после последней. Благодаря ей модель узнаёт, как начинаются истории, и может сказать «на этом всё».
Напечатайте что-нибудь в поле и посмотрите, во что текст превращается на входе модели.
Почему буквы, а не целые слова?
Можно дать собственный номер каждому слову: тогда «cat» станет одной единицей вместо трёх. Но что делать с новой фамилией или опечаткой, которой нет в словаре? Понадобится запасной способ записи, например разбиение на более мелкие части. Пока возьмём небольшой набор символов: он покрывает английские тексты после нашей очистки, но не весь английский вообще — за его пределами остаются, например, типографские кавычки и буквы с диакритикой. С таким набором таблицу можно рассмотреть целиком. К более универсальной токенизации вернёмся в главе 6.
Заметим две вещи. Во-первых, «a» и «A» — разные символы, 67 и 35, и для модели они различаются ровно так же, как «a» и «?». Ничто не подсказывает ей, что одна буква — заглавная форма другой; если эта связь важна, модель должна найти её в данных сама. Во-вторых, сами номера ничего не значат. Из того, что у «a» номер 67, не следует, что она «больше» «A». Это просто имена, как номера кресел в театре: их можно было раздать в любом другом порядке.
Кто за кем ходит
Начнём с совсем маленького примера — со слова banana. Поставим по краям метки, ◆banana◆, и выпишем все пары соседей: ◆b, ba, an, na, an, na, a◆. Семь пар. Теперь сгруппируем их по первой букве:
| после… | шла… |
|---|---|
| ◆ | b — один раз |
| b | a — один раз |
| a | n — два раза, ◆ — один |
| n | a — два раза |
Вот и всё: эта табличка — полноценная языковая модель слова banana. Она знает, что слово начинается с «b», что после «n» всегда идёт «a», а после «a» — либо «n», либо конец.
Теперь сделаем то же самое, только всерьёз. Мы взяли 41,3 миллиона букв из историй и диалогов, на которых учится Росток, — всего 39 184 текста — и посчитали все пары соседей. Таблица, где у каждого из 97 символов есть строка и столбец, состоит из 97 × 97 = 9 409 клеток. Вот она. Строка — предыдущая буква, столбец — следующая; чем ярче клетка, тем вероятнее пара.
Посидите над картой минуту — в ней полно маленьких историй про английский язык.
- После q почти всегда идёт u. Из 13 061 буквы «q» за 13 003 следовала «u» — 99,6%. Строка «q» почти пуста: одна яркая клетка и несколько бледных точек.
- Самая частая пара — «e␣», то есть «e» на конце слова: 1 312 444 раза, 3,2% всех пар. Следом идут «he» и «␣t». Узнаёте «the»?
- У знаков препинания свои привычки. После запятой в 96,5% случаев стоит пробел. После заглавной T в 82,3% случаев идёт «h»: The, They, There.
- Четверть всех историй начинается с заглавной O. В строке ◆ у буквы «O» 25,4%: «Once upon a time…», «One day…».
- Большая часть клеток пуста. 7 051 пара из 9 409 не встретилась ни разу — три четверти таблицы. Даже среди строчных букв и пробела не бывает 136 пар из 729: «jj», «qa», «fq»… Нет и двух пробелов подряд: корпус перед обучением почистили, и двойные пробелы пропали.
Посчитаем пары в коротком фрагменте, где результат легко проверить глазами. В коде словарь counts хранит число появлений каждой пары: встретили её ещё раз — прибавили единицу. Нажмите «Запустить», затем замените текст своим и сравните результаты.
zip(text, text[1:]) — изящный приём: он идёт одновременно по тексту и по его копии, сдвинутой на одну букву, так что на каждом шаге мы получаем букву вместе с её правым соседом.
От счёта к вероятностям
Пара «th» встретилась 829 367 раз. Чтобы понять, часто ли это, нужно знать, сколько всего раз после «t» мог появиться другой символ. Таких случаев было 2 702 151: примерно за 31 буквой «t» из ста шла «h». Это наблюдаемая частота. Наша модель использует её как оценку вероятности следующей буквы. В другом корпусе частота могла бы отличаться.
Нас интересует вероятность следующей буквы $b$ при условии, что предыдущая была $a$. Записывают её как $p(b \mid a)$, а по нашей таблице она считается элементарно:
$$p(b \mid a) = \frac{\text{count}(a, b)}{\text{count}(a)}$$Сверху — сколько раз встретилась пара $a b$, снизу — сколько раз встретилась $a$ вообще (с чем угодно после неё). Для нашей «t»: $p(\text{h} \mid \text{t}) = 829\,367 / 2\,702\,151 \approx 0{,}307$. Для banana: $p(\text{n} \mid \text{a}) = 2/3$, а $p(\blacklozenge \mid \text{a}) = 1/3$.
Такая вероятность называется условной: мы спрашиваем не о том, насколько вероятна «h» вообще, а о том, насколько она вероятна при условии, что только что была «t». Разница огромная. Среди всех 41,3 миллиона пар на «th» приходится всего 2,0%. Но стоит нам узнать, что перед нами «t», — шанс увидеть «h» подскакивает до 30,7%. Знание о том, что было раньше, — ровно то, ради чего языковая модель существует.
Биграммная модель — это просто таблица. Строка — предыдущая буква, числа в строке — вероятности следующей, и каждая строка в сумме даёт единицу.
Биграмма — последовательность из двух единиц текста. Здесь это символы, но биграммы бывают и словесными: например, «little cat». Последовательности из трёх единиц называют триграммами, а общее название — n-граммы.
Есть одна загвоздка. Ноль в таблице означает «невозможно». Но так ли невозможна пара «jj»? Слово «hajj» существует — просто не в детских сказках. Модель, которая клянётся, что чего-то не бывает, окажется в большой беде, как только это всё-таки случится. Поэтому настоящая таблица Ростка перед делением добавляет единицу в каждую клетку:
$$p(b \mid a) = \frac{\text{count}(a, b) + 1}{\text{count}(a) + 97}$$Это называется сглаживанием Лапласа (или «плюс один»). Для частых пар оно почти ничего не меняет: 829 368 / 2 702 248 — всё те же 30,7%. Зато ни одна пара больше не запрещена наглухо. Почему это так важно, станет ясно в следующей главе: там ноль превратится в бесконечность.
После «q» буква «u» встретилась 13 003 раза, сама «q» — 13 061 раз, а всего в корпусе 41,3 миллиона букв. Чему равна p(u | q)?
Условную вероятность делят на число шансов — сколько раз встретилась «q», а не на размер всего корпуса. И это не ровно единица: 25 раз после «q» стояло двоеточие — в диалогах героев по имени Eriq, Tariq и Tyriq («Tariq: So…»), — и ещё несколько раз другие символы.
Кубик с перевесом
Таблица говорит, насколько вероятна каждая следующая буква. Но чтобы она что-нибудь написала, букву надо выбрать. Можно всегда брать самую вероятную — но тогда после «t» всегда будет «h», после «h» всегда «e», после «e» всегда пробел… и выйдет бесконечное «the the the the». Нам нужно выбирать случайно, но честно: после «t» брать «h» три раза из десяти, пробел — два с половиной раза из десяти, и так далее.
Представим выбор как бросок кубика, у которого исходы имеют разные вероятности. В исходных данных после «h» встречались 42 разных символа, причём больше половины случаев приходится на «e». После сглаживания небольшой шанс есть у всех 97 символов. Чтобы разыграть такое распределение, начнём с равномерного псевдослучайного числа $u$ от 0 до 1: генератор случайных чисел есть в стандартных библиотеках языков программирования.
Хитрость в том, чтобы уложить вероятности друг за другом на отрезке от 0 до 1. Первая буква получает кусок от 0 до $p_1$, вторая — от $p_1$ до $p_1 + p_2$, и так далее: границы кусков — это накопленные суммы вероятностей. Теперь роняем на отрезок стрелку $u$. Она попадает в каждый кусок с шансом, в точности равным его длине, то есть вероятности буквы. Кубик брошен.
Бросьте несколько раз вручную, затем нажмите «×10 000». При большом числе независимых бросков частоты обычно оказываются близки к заданным вероятностям, хотя точного совпадения ждать не стоит. Это проявление закона больших чисел. С текстом нужно быть осторожнее: соседние буквы зависимы, а набор детских историй не представляет весь язык. Частоты дают полезную модель выбранного корпуса, но не универсальную таблицу английского.
На Python весь кубик умещается в несколько строк. В стандартной библиотеке есть и готовый вариант — random.choices, который проходит по тем же накопленным суммам. Эта ячейка пользуется парами, посчитанными в предыдущей: все ячейки страницы живут в одном сеансе Python, как в блокноте Jupyter.
Буква за буквой
Теперь у нас есть всё, чтобы писать. Начинаем с метки ◆ и бросаем кубик строки ◆ — получаем первую букву. Потом бросаем кубик строки этой буквы — и так далее. Каждая новая буква выбирает кубик для следующего броска. Когда выпадает ◆, история окончена.
Получается лепет — но любопытный лепет. Один из наших запусков начался так: «Frthendd, aced. tre sedsck.. oale, ateyinet thee prerootosal s thu fear frime». У «слов» правдоподобная длина, и между ними пробелы. После точки — пробел и часто заглавная буква. Кавычки открываются и закрываются, мелькают «the» и «he». Смысла, конечно, никакого: модель помнит одну-единственную букву и не знает, где она — посреди слова или в конце фразы. Увидев «h», она понятия не имеет, пришла эта «h» из «th» или из «sh».
Уже по таким коротким зависимостям видны некоторые привычки текста: распространённые пары букв, пробелы после пунктуации, частые окончания. Но таблица не выучила правило «после точки и пробела нужна заглавная»: дойдя до пробела, она уже забыла точку. И открывающую кавычку до закрывающей она тоже помнить не может. Похожий на текст вид ещё не означает, что модель удерживает его структуру.
Вся таблица в numpy
Словари хороши для одной фразы, а для 97 × 97 чисел нужна настоящая таблица — массив numpy. Ячейка ниже загружает те самые числа, что вы видели на карте (22 КБ JSON), превращает их в вероятности с единицей Лапласа и пишет несколько сотен букв.
Вся нормировка — строчка с P. Выражение (N + 1).sum(1, keepdims=True) складывает каждую строку и оставляет результат столбцом из 97 чисел; деление таблицы 97 × 97 на такой столбец делит каждую строку на её собственную сумму (в numpy это называется broadcasting, «растягивание»). А rng.choice(97, p=…) — наш кубик с перевесом: внутри он проходит по тем же накопленным суммам.
А вот функция, которая построила настоящую таблицу Ростка, — из кода курса, файл snapshots.py. Она читает 41,3 миллиона букв, одной строкой считает все пары через np.add.at (для каждой позиции прибавляет единицу в клетку «эта буква, следующая буква»), нормирует с единицей Лапласа и сохраняет результат в файл модели, который умеет запускать браузер.
Две строчки в ней пока выглядят загадочно. Таблица хранится в виде логарифмов вероятностей, np.log(probs), а в конце печатается какая-то «loss» — 2,3650 «ната на букву». И то и другое — тема следующей главы: это число и есть ответ на вопрос «насколько модель хороша».
Чего не видно с одной буквы
Наша таблица честная и быстрая, но у неё жёсткий предел: она помнит одну букву. Напрашивается очевидное — считать контексты подлиннее. Будем смотреть на две предыдущие буквы — и таблице понадобится строка на каждую пару букв: 97 × 97 строк по 97 столбцов, 912 673 клетки. Три буквы контекста — уже 88,5 миллиона клеток, больше, чем 41,3 миллиона букв, которые мы посчитали. Восемь букв — около $7{,}6 \cdot 10^{17}$ клеток.
Размер таблицы — лишь часть проблемы. Большинство длинных сочетаний в нашем корпусе не встретится, а для нового контекста простая таблица не найдёт отдельной строки с наблюдениями. В статистических моделях это смягчают сглаживанием и переходом к более короткому контексту. Нейросеть предлагает другой подход: общие параметры позволяют переносить выученные закономерности между похожими контекстами. Например, встретив «the cat sat» и «the dog sat», она может повысить вероятность похожей конструкции с «fox». Этому обобщению и посвящены следующие главы.
Почему одной таблицы с восемью символами контекста недостаточно даже при неограниченной памяти?
Сам подсчёт как раз быстрый: окон столько же, сколько букв, и на каждое уходит одно прибавление. Память — проблема настоящая, но даже бесконечная память не спасёт: при $7{,}6 \cdot 10^{17}$ клетках и всего лишь миллиардах букв текста почти все клетки останутся пустыми. Нужна модель, которая переносит выученное с похожих контекстов, а не ищет точное совпадение.
Остался открытым ещё один вопрос. Мы посмотрели на лепет и сказали: «вроде похоже на английский». Это впечатление, а не измерение. Как выразить качество модели числом — чтобы сравнивать две модели или видеть, как модель становится лучше во время обучения? Об этом следующая глава.
Росток сейчас
Росток впервые заговорил — пока лепетом. Сейчас он — таблица из этой главы, 9 409 вероятностей, и работает прямо у вас в браузере. Каждую букву он выбирает, бросая новый кубик с перевесом. Начало можно задать любое, но важна только его последняя буква: «Once upon a time» и «I like» стартуют с одного и того же кубика — кубика «e». Попробуйте заготовку «Q»: в девяти случаях из десяти следом придёт «u». В следующей главе мы точно измерим, насколько эта модель плоха, — и получим число, которое обучение будет толкать вниз.