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

Глава 6 из 14 30 мин

Токены

Буква — слишком мелкий кусок, слово — слишком крупный. Разберёмся, как Росток режет текст, откуда взялись его 8 192 токена и почему русский текст он видит как россыпь байтов.

В этой главе

  • объяснить, зачем модели токены и почему не буквы и не слова
  • прогнать алгоритм BPE вручную и прочитать настоящий токенизатор Ростка
  • понимать, во что обходится размер словаря и почему нелатинские языки для маленькой модели дороже

Модель из прошлой главы видит восемь последних букв. Восемь букв — это «the litt»: даже слово целиком не помещается, не то что предложение. А что, если в каждую из восьми ячеек класть не букву, а кусок побольше — целое частое слово или хотя бы слог?

Так и поступают все большие языковые модели. Текст режут на токены — кусочки разной длины, — и каждый кусочек получает свой номер и свой эмбеддинг. У Ростка токенов 8 192. Прежде чем разбираться, откуда они взялись, посмотрите, как он режет текст.

Как Росток читает

Ниже — настоящий токенизатор Ростка, тот же, с которым он обучался. Каждый цвет — отдельный токен. Напишите что угодно или нажмите на готовые примеры, в том числе на русский текст и эмодзи.

Точка «·» — пробел, «↵» — перевод строки. Пунктирные рамки — токены-байты, осколки символов. «Куски» показывают, как текст режется до склеек.

Несколько вещей бросаются в глаза сразу.

  • Частые слова — это один токен, причём вместе с пробелом перед ними: «·time», «·little», «·garden». Для модели «·the» в середине фразы и «The» в начале — разные токены.
  • Редкое слово собирается из кусков: antidisestablishmentarianism превращается в девять токенов — «·ant | id | is | est | ab | lish | ment | arian | ism». Модель никогда не встречала такого слова целиком, но его части ей знакомы.
  • Числа идут по одной цифре. Это не случайность, а правило, о нём ниже.
  • Русский текст рассыпается: почти каждая буква — два токена-осколка. В примере «Жили-были дед да баба…» 49 символов и 86 токенов. Эмодзи — по четыре осколка на каждый.

Внизу виджета видно, что мы выиграли. Окно из восьми букв — это обрывок слова. Окно из восьми токенов в английском тексте — это почти целая фраза: на нашем проверочном тексте в среднем 4,085 символа на токен, так что в восемь ячеек помещается около 33 символов. А в русском окно становится даже меньше буквенного: восемь токенов — всего четыре-пять букв. Почему так вышло — главная интрига этой главы.

Буквы — мелко, слова — крупно

Прежде чем изобретать токены, разберёмся с двумя очевидными вариантами.

Буквы. Словарь крошечный — 97 символов, и в английском тексте незнакомых букв не бывает. Но текст получается длинным: сказка в тысячу символов — это тысяча шагов, и на каждом модель решает крошечную задачу «какая буква следующая». Всё, что модель должна помнить, приходится растягивать на огромное число позиций. В главе 7 мы увидим, что цена внимания растёт как квадрат длины текста: вчетверо более длинный текст обходится в шестнадцать раз дороже.

Слова. Текст короткий, каждая ячейка — целое слово. Но словарь разбухает. Только в нашем проверочном тексте — 7,2 миллиона токенов простых историй и диалогов — мы насчитали 28 666 разных слов, и 6 357 из них встречаются ровно один раз. Каждому слову нужна своя строка в таблице эмбеддингов, а редкие строки почти не учатся. Хуже того, любое новое слово — имя, опечатка, «Росток» — оказывается вне словаря, и модели остаётся только метка «неизвестное».

Кусочки слов — золотая середина. Частые слова целиком, редкие — из частей, а совсем незнакомое слово — хоть по буквам. Неизвестных слов не бывает вовсе. На нашем проверочном тексте слово с пробелом перед ним в 95% случаев оказывается ровно одним токеном — почти как в словаре слов, но без его проблем.

В самом низу — байты

С чего начинать нарезку? Можно было бы начать с символов, но их в мире десятки тысяч: буквы всех алфавитов, иероглифы, стрелки, эмодзи. Поэтому токенизатор Ростка, как и токенизаторы GPT-2 и многих современных моделей, начинает с байтов.

Чаще всего компьютер хранит текст в кодировке UTF-8: каждый символ — от одного до четырёх байтов, а байт — число от 0 до 255. Латинские буквы без диакритики, цифры и знаки препинания занимают один байт. Русские буквы — два: первый (обычно D0 или D1 в шестнадцатеричной записи) говорит «дальше кириллица», второй уточняет, какая именно буква. Эмодзи занимают четыре байта.

for s in ['a', 'ж', 'é', '🌱', 'Росток']: b = s.encode('utf-8') print(f'{s!r:10} {len(s)} chars -> {len(b):2} bytes:', ' '.join(f'{x:02X}' for x in b))

Всего возможных байтов 256, и все они — первые 256 токенов словаря Ростка. Поэтому любой текст на любом языке токенизатор хоть как-то да запишет: в худшем случае байт за байтом. Ещё 7 932 токена — склеенные пары, выученные на корпусе. Как их выбирали — дальше.

Склеить самую частую пару

Алгоритм называется BPE, byte pair encoding — «кодирование пар байтов». Он поразительно простой:

  1. Запишите весь обучающий текст байтами.
  2. Посчитайте, какая пара соседних токенов встречается чаще всего.
  3. Объявите эту пару новым токеном и замените все её вхождения.
  4. Повторяйте, пока словарь не вырастет до нужного размера.

Попробуйте сами на крошечном тексте. Каждое нажатие — одна склейка.

Обучение BPE прямо в браузере. Текст можно заменить своим — например, повторить одно слово много раз и посмотреть, как быстро оно станет одним токеном.

Сначала склеиваются пары, которых больше всего: «a»+«t», «t»+«h», затем «th»+«e» — и вот уже «the» стал одним токеном. Потом к нему прилипает пробел. Слова, которые часто повторяются, быстро становятся целыми токенами, а редкие так и остаются кусками. Никакой лингвистики — только подсчёт.

Вот тот же алгоритм на чистом Python — всего три десятка строк. Запустите его и сравните склейки с виджетом: они совпадут.

import re from collections import Counter text = "the cat sat on the mat. the cat ate the rat. then the rat ran to the hat." # pre-split like tokenizer.py (an ASCII-only version): words keep their leading space chunks = Counter(re.findall(r" ?[A-Za-z]+| ?[0-9]| ?[^\sA-Za-z0-9]+|\s+", text)) seqs = {w: list(w.encode()) for w in chunks} vocab = {i: bytes([i]) for i in range(256)} print(sum(len(s) * chunks[w] for w, s in seqs.items()), 'tokens to start with') for step in range(10): pairs = Counter() for w, seq in seqs.items(): for pair in zip(seq, seq[1:]): pairs[pair] += chunks[w] if not pairs: break best = max(pairs, key=pairs.get) new = 256 + step vocab[new] = vocab[best[0]] + vocab[best[1]] for w, seq in seqs.items(): out, i = [], 0 while i < len(seq): if i + 1 < len(seq) and (seq[i], seq[i + 1]) == best: out.append(new) i += 2 else: out.append(seq[i]) i += 1 seqs[w] = out left = sum(len(s) * chunks[w] for w, s in seqs.items()) print(f'merge {step}: {vocab[best[0]]!r} + {vocab[best[1]]!r} -> {vocab[new]!r} seen {pairs[best]}x, {left} tokens left')

Сначала режем на куски

Одна тонкость. Если склеивать пары по всему тексту подряд, BPE охотно выучит токены вроде «g.» или «d the»: в текстах такие сочетания тоже частые. Но они смешивают конец одного слова с началом другого, и словарь тратится впустую. Поэтому перед обучением текст режут на куски регулярным выражением, и склейки никогда не выходят за границы куска. Переключите виджет в начале главы на «Куски», чтобы увидеть эти границы.

Вот это регулярное выражение — прямо из tokenizer.py:

# Split text into chunks first, so a merge never crosses a word boundary: # contractions, words with their leading space, single digits, punctuation, spaces. SPLIT = re.compile(r"""'(?:[sdmt]|ll|ve|re)| ?\p{L}+| ?\p{N}| ?[^\s\p{L}\p{N}]+|\s+(?!\S)|\s+""")

Разберём его по частям; вертикальная черта означает «или»:

  • '(?:[sdmt]|ll|ve|re) — английские сокращения: 's, 't, 'll, 've, 're. «didn't» режется на «didn» и «'t».
  • ?\p{L}+ — слово из букв любого алфавита вместе с одним пробелом перед ним. Вот откуда «·little».
  • ?\p{N} — одна цифра. Поэтому 2024 — это четыре токена. Так модели легче видеть разряды, и числа не превращаются в тысячи случайных токенов.
  • ?[^\s\p{L}\p{N}]+ — знаки препинания подряд, например «!"» или «...».
  • \s+(?!\S)|\s+ — пробелы и переводы строк.

А вот сам цикл обучения. Он делает ровно то же, что наш мини-BPE, но хитрее считает: помнит, в каких кусках встречается каждая пара, и после склейки пересчитывает только их. Без этого 7 932 склейки на 120 тысячах документов шли бы часами, а так токенизатор Ростка обучился за 19 секунд.

@classmethod def train(cls, texts, vocab_size, specials=SPECIALS, verbose=False): n_merges = vocab_size - 256 - len(specials) words = Counter() for text in texts: for chunk in SPLIT.findall(text): words[chunk] += 1 # every distinct chunk is a list of token ids, counted once with its frequency seqs = [list(w.encode()) for w in words] freqs = list(words.values()) pairs = Counter() where = defaultdict(set) # pair -> indices of chunks that contain it for i, seq in enumerate(seqs): for pair in zip(seq, seq[1:]): pairs[pair] += freqs[i] where[pair].add(i) merges = [] for step in range(n_merges): if not pairs: break best = max(pairs, key=pairs.get) new_id = 256 + step merges.append(best) for i in list(where[best]): seq, f = seqs[i], freqs[i] for pair in zip(seq, seq[1:]): # forget this chunk's old pairs pairs[pair] -= f if pairs[pair] <= 0: del pairs[pair] where[pair].discard(i) seq = merge(seq, best, new_id) seqs[i] = seq for pair in zip(seq, seq[1:]): # and count the new ones pairs[pair] += f where[pair].add(i) if verbose and step % 500 == 0: tok = cls(merges).vocab[new_id] print(f'merge {step:5d}: {tok!r}') return cls(merges, specials)

Вывод — настоящий, из обучения Ростка (журнал считает склейки с нуля): самая первая — «·t», к шагу 500 токенизатор уже склеивает целые слова вроде «·flow», а шаг 7 000 даёт «·ambulance».

Как собирается слово

Словарь выучен. Как теперь разрезать новый текст? Кодирование просто повторяет склейки в том порядке, в котором они были выучены: пока в куске есть пара из словаря, склеиваем ту, что выучена раньше всех.

def encode_chunk(self, chunk): ids = self.cache.get(chunk) if ids is not None: return ids ids = list(chunk.encode()) while len(ids) > 1: # the pair that was learned earliest gets merged first pair = min(zip(ids, ids[1:]), key=lambda p: self.merges.get(p, float('inf'))) if pair not in self.merges: break ids = merge(ids, pair, self.merges[pair]) if len(self.cache) < 500_000: self.cache[chunk] = ids return ids

Результат удобно рисовать деревом: внизу байты, над ними склейки, наверху готовый токен. Номер у каждой склейки — это её место в очереди обучения.

Дерево склеек из настоящего словаря Ростка. Внизу байты, выше каждая склейка с номером. Пробел в начале слова — часть токена.

Посмотрите на «·the»: склейка № 1 — «·t», № 2 — «he», и уже седьмой склейкой выучен целый «·the». Длинные слова ждут своей очереди долго: «·butterfly» появился только на склейке № 2 095, а «·Congratulations» (16 байтов, один из двух самых длинных токенов словаря; второй — «·recommendations») — только на № 4 312. Любопытно, что «·sprout» со строчной буквы — один токен, а «·Sprout» с заглавной — три: заглавный вариант в историях встречался реже. А «·Росток» — тринадцать байтов (пробел и по два на каждую букву) и ни одной склейки.

Что лежит в словаре

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

Все 7 932 склейки по порядку. Первые — самые частые пары корпуса, последние — редкие слова, которые едва набрали нужную частоту.

Первые склейки — «·t», «he», «·a», «·s», «·w», «in», и седьмая — «·the». В самом конце — «·mushrooms», «Josh», «·armcha» (полкресла: целиком «armchair» не успел стать токеном), «·recommendations». Больше половины словаря — токены вида «пробел и буквы», то есть целые слова или их начала.

Последние четыре номера — 8188–8191 — занимают служебные токены: <|endoftext|>, <|user|>, <|assistant|> и <|end|>. Из обычного текста они никогда не получаются — их вставляет программа. Первый отделяет одну историю от другой, остальные понадобятся в главе 12, когда мы будем учить Ростка разговаривать. Попробуйте пример «служебные» в виджете в начале главы: каждая метка — один цельный токен-плашка.

Сколько токенов нужно

Почему именно 8 192, а не 1 000 или 100 000? Больше словарь — короче текст, но дороже модель. Ползунок ниже показывает, что было бы с меньшим словарём. Заново обучать токенизатор для этого не нужно: BPE учится жадно, по одной склейке, и если бы мы остановили обучение раньше, получили бы ровно первые склейки нашего словаря.

Ползунок оставляет только первые склейки. Точки на кривой — замеры на проверочном тексте.
СловарьСимволов на токенТаблица эмбеддингов ×384
256 (только байты)1,0098 304
5122,21196 608
1 0242,91393 216
2 0483,50786 432
4 0963,881 572 864
8 1924,093 145 728

Первые 256 склеек укорачивают текст больше чем вдвое. Каждое следующее удвоение словаря даёт всё меньше: переход от 4 096 к 8 192 токенам экономит лишь 5% длины, а таблица эмбеддингов при этом удваивается. У Ростка она и так весит 3,15 миллиона параметров — почти пятую часть модели.

Есть и вторая цена. Чем больше словарь, тем реже встречается каждый токен и тем хуже учится его эмбеддинг. Даже при 8 192 токенах 137 выученных склеек ни разу не встретились в 7,2 миллиона токенов проверочного текста, а 506 — меньше десяти раз. Для модели на 17 миллионов параметров, которая пишет простые английские истории, 8 192 — разумный компромисс.

Как сравнивать модели с разными токенами

Раз уж длина текста зависит от словаря, появляется ловушка. Модель в конце этой главы устроена так же, как в прошлой: тот же класс MLP из snapshots.py, только другого размера — MLP(8192, 8, 48, [384]), восемь токенов по 48 чисел и один скрытый слой на 384 нейрона. На проверочном тексте её удивление — 3,31 ната на токен. У буквенной модели было 1,103 ната на букву. Стало втрое хуже?

Нет: мы сравниваем разные вещи. Угадать следующий токен — значит угадать сразу около четырёх символов, и удивление за них складывается. Честное сравнение — на одну и ту же единицу текста. Если у токена в среднем $c$ символов, то

$$\text{наты на символ} = \frac{\text{наты на токен}}{c} = \frac{3{,}31}{4{,}085} \approx 0{,}81.$$

Это заметно лучше, чем 1,103 у буквенной модели. Та же ловушка подстерегает и исследователей: перплексию моделей с разными токенизаторами напрямую сравнивать нельзя, её пересчитывают на символ или на байт.

Закон Ципфа

Токены встречаются очень неравномерно. Лингвист Джордж Ципф заметил это для слов ещё в 1930–1940-х: второе по частоте слово встречается примерно вдвое реже первого, третье — втрое, и так далее. Частота обратно пропорциональна рангу. Если нарисовать это в логарифмическом масштабе по обеим осям, получится почти прямая.

Настоящие частоты токенов Ростка на проверочном тексте, обе оси логарифмические. Пунктир — идеальный закон Ципфа. Двигайте ползунок или ведите пальцем по графику.

Картина у токенов почти ципфовская: наклон нашей кривой в средней части около −1,2 против −1 у идеала. На практике это значит, что горстка токенов делает почти всю работу. Самый частый токен — точка — это 7,2% текста. Первые десять токенов покрывают 27,9% текста, первые сто — 56,2%, первая тысяча — 86,1%. Остальные семь тысяч делят между собой меньше седьмой части текста. Именно поэтому огромный словарь окупается плохо: каждая новая склейка — всё более редкое слово.

Честно о русском

Теперь понятно, почему русский текст рассыпается на осколки. Токенизатор Ростка учился только на английских историях. Кириллицы в корпусе практически не было — при чистке мы убирали неанглийские тексты, — и ни одна из 7 932 склеек не содержит байта больше 127. Поэтому для русского текста у токенизатора есть только 256 байтовых токенов: каждая буква — два токена, каждый пробел — ещё один.

  • «Once upon a time there lived an old man and an old woman. They had a hen.» — 73 символа, 20 токенов: 3,65 символа на токен.
  • «Жили-были старик со старухой. Была у них курочка.» — 49 символов, 88 токенов: 0,56 символа на токен.
  • «Росток» — 6 букв, 12 токенов.
  • 🌱 — один символ, 4 токена.

Одна и та же история по-русски выходит для Ростка вчетверо длиннее. Контекст Ростка — 512 токенов: это около 2 100 символов английского текста, но меньше 300 символов русского — пара абзацев против пары фраз. И это ещё не всё. В проверочном тексте на 7,2 миллиона токенов байты больше 127 встретились всего 30 раз. Эмбеддинги этих токенов почти не обучались: для Ростка русская буква — это два почти пустых вектора подряд.

Так устроены все модели: язык, которого мало в обучающем корпусе, дорого обходится и в токенизаторе. Токенизаторы больших многоязычных моделей учат на текстах сразу на многих языках, и у них есть склейки и для кириллицы. Но «налог на токены» для языков, которых в корпусе меньше, чем английского, не исчезает — только уменьшается.

Токенизатор — это словарь частых кусочков, выученный подсчётом. Всё, чего в корпусе было много, он читает крупно и дёшево; всё, чего было мало, — мелко и дорого.

Почему Росток тратит на русское слово «кот» шесть токенов, а на английское « cat» — один?

BPE выучил только те пары, которые часто встречались в обучающем тексте. Кириллицы там почти не было, поэтому русские буквы остаются парами байтов, а « cat» склеился в один токен ещё в начале обучения.

А если в тексте встретится что-то совсем странное?

Ничего страшного не случится: любой текст — это байты, а все 256 байтов есть в словаре. Китайский иероглиф станет тремя токенами-байтами, редкий эмодзи — четырьмя, а составной эмодзи «семья из четырёх человек» — двадцатью пятью. Модель может такого не понимать, но записать и прочитать обратно без потерь сумеет всегда. Поэтому у byte-level BPE не бывает «неизвестного токена».

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

Устройство Ростка почти не изменилось — окно из восьми ячеек, эмбеддинги, скрытый слой, — но в каждую ячейку теперь ложится токен, и окно охватывает около 33 символов вместо восьми. Удивление на токен — 3,31 ната на проверочном тексте, или около 0,81 ната на символ против 1,103 у буквенной модели. Параметров стало больше: из 3,7 миллиона около 3,15 миллиона занимает выходной слой, который оценивает каждый из 8 192 токенов. Проза стала связнее, но восемь токенов — это всё ещё одна фраза. Как заглянуть дальше, не раздувая сеть, расскажет следующая глава — о внимании.

Главы

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