Генеративный AI
Токенизация: BPE, SentencePiece
2023 год. Meta выпускает LLaMA 3 и увеличивает словарь с 32K до 128K токенов. Это не маркетинг - это исправление фундаментального бага. Предыдущая версия тратила в 3 раза больше токенов на русский и арабский тексты, чем на английский. Тот же запрос, в 3 раза дороже, в 3 раза медленнее. Токенизация определяет, что модель вообще «видит».
- **GPT-4 API** тарифицирует по токенам: один и тот же вопрос на русском стоит в 2-3 раза дороже, чем на английском - из-за менее эффективной токенизации кириллицы
- **BERT** использует WordPiece с 30K словарём и маркером ## для продолжений - именно так модель понимает морфологию: «play» + «##ing» = «playing»
- **LLaMA 3** увеличил словарь с 32K до 128K именно для лучшей мультиязычной токенизации - и качество на не-английских языках заметно выросло
Рико Сеннрих и адаптация BPE для NLP
В 1994 году Филипп Гейдж придумал BPE как алгоритм сжатия данных - заменять частые пары байт одним байтом. В 2016 году Рико Сеннрих из Эдинбургского университета адаптировал эту идею для нейронного машинного перевода. Проблема была простой: как перевести слово «Bezirksregierungen» (немецкое, 18 символов), которого нет в словаре? BPE разбивал его на известные кусочки. Статья Sennrich et al. набрала 10 000 цитирований за 8 лет - один из самых влиятельных вкладов в современный NLP. Сегодня BPE живёт в каждой LLM.
Предварительные знания
Byte Pair Encoding: от символов к субсловам
GPT-2 спросили: сколько будет 1234 + 5678? Ошибся. Не потому что не «умеет» складывать - а потому что число «5678» разбито на токены «56» и «78». Модель видит **два отдельных куска**, а не одно число. Это следствие **токенизации**: процесса, который превращает текст в последовательность чисел (token IDs) ещё *до* того, как нейросеть начинает работать. Невидимый, но критически важный этап.
Почему нельзя просто разбить текст по словам? Три причины: 1. **словарь слишком большой** - в английском ~170,000 слов, а с учётом форм, опечаток, имён - миллионы 2. **OOV (out-of-vocabulary)** - новые слова, аббревиатуры, сленг не попадают в словарь 3. **морфология** - «играю», «играешь», «играет» - три отдельных токена, хотя корень один. Решение: **субсловная токенизация** - разбивать слова на частотные подстроки.
**Byte Pair Encoding (BPE)** - самый популярный алгоритм субсловной токенизации. Изначально алгоритм сжатия данных (Gage, 1994), адаптированный для NLP (Sennrich et al., 2016). Идея проста: начинаем с отдельных символов и **итеративно сливаем** самую частую пару символов в новый токен. Повторяем, пока словарь не достигнет нужного размера.
**Применение обученного BPE к новому тексту.** После обучения есть упорядоченный список слияний. Для токенизации нового слова: 1. разбиваем на символы 2. применяем слияния **в порядке обучения** - каждое слияние заменяет пару, если она встречается. Незнакомые слова разбиваются на известные подстроки. Слово «lowest» -> «low» + «est» - модель *видит* корень и суффикс.
**GPT-2/3/4 используют Byte-level BPE.** Вместо Unicode-символов работают с **байтами** (256 базовых токенов). Это гарантирует, что *любой* текст (включая эмодзи, иероглифы, бинарные данные) может быть токенизирован без OOV-ошибок. Tiktoken - библиотека OpenAI для быстрой BPE-токенизации.
BPE-токенизатор обучен на английском корпусе. Как он обработает новое немецкое слово «Handschuh» (перчатка), которого нет в словаре?
WordPiece: максимизация likelihood
**WordPiece** (Schuster & Nakajima, 2012) - алгоритм, используемый в **BERT**, **DistilBERT** и других моделях Google. По духу похож на BPE, но с важным отличием в критерии слияния: BPE сливает самую **частую** пару, а WordPiece - пару, которая **максимально увеличивает likelihood** обучающего корпуса.
Формально, WordPiece выбирает пару (a, b) для слияния, если слияние максимизирует: **score(a, b) = freq(ab) / (freq(a) * freq(b))**. Это похоже на **pointwise mutual information (PMI)** - меру того, насколько часто два символа встречаются вместе по сравнению с независимостью. Пара «qu» получит высокий score, потому что «q» почти всегда идёт с «u».
**Маркировка продолжений с ##.** WordPiece помечает токены, которые являются *продолжением* слова, префиксом `##`. Это позволяет модели отличать начало слова от его середины. Например: «playing» -> [«play», «##ing»], «unbelievable» -> [«un», «##believ», «##able»]. Без `##` модель не отличила бы «a play» (существительное) от «play##ing» (часть слова).
| Аспект | BPE | WordPiece |
|---|---|---|
| Критерий слияния | Максимальная частота пары | Максимальный likelihood (PMI) |
| Маркировка | Нет специального маркера | ## для продолжений |
| Используется в | GPT-2/3/4, RoBERTa, LLaMA | BERT, DistilBERT, Electra |
| Обработка редких слов | Разбивка на субслова | Разбивка на субслова с ## |
| Размер словаря | 32K-100K (GPT: 50,257) | 30K (BERT: 30,522) |
**На практике разница между BPE и WordPiece невелика.** Оба алгоритма производят субсловные токенизации схожего качества. Выбор зависит от экосистемы: если работа с моделями OpenAI/Meta - BPE, с Google - WordPiece. Для новых проектов чаще выбирают BPE или Unigram.
BERT-токенизатор (WordPiece) разбивает слово «playing» на [«play», «##ing»]. Зачем нужен префикс ##?
SentencePiece: language-agnostic токенизация
BPE и WordPiece предполагают, что текст уже **предварительно обработан**: разбит на слова пробелами, очищен от лишних символов, нормализован. Это работает для английского, но для **японского, китайского, тайского** (языков без пробелов) или **немецкого** (длинные составные слова) - нет. **SentencePiece** (Kudo & Richardson, 2018) решает эту проблему: работает с **сырым текстом** напрямую, включая пробелы как часть алфавита.
Ключевая идея: SentencePiece **не требует pre-tokenization**. Пробел обозначается специальным символом `▁` (Unicode U+2581) и становится частью токенов. Текст «I love cats» представляется как «▁I▁love▁cats», и токенизация работает на этой цепочке символов. Это делает алгоритм **language-agnostic** - одинаково хорошо работает с любым языком.
SentencePiece поддерживает два алгоритма: **BPE** (как описано выше) и **Unigram** (Kudo, 2018). Unigram - принципиально другой подход: вместо итеративного *добавления* токенов (BPE), начинает с **большого** словаря и итеративно *удаляет* наименее полезные токены. На каждом шаге Unigram убирает токен, удаление которого **меньше всего увеличивает loss** корпуса.
**Кто использует SentencePiece?** LLaMA (Meta), T5 (Google), ALBERT, XLNet, mBART - все мультиязычные модели. GPT-2/3/4 используют свой byte-level BPE через библиотеку tiktoken, но идея та же: работа с байтами вместо символов делает токенизатор language-agnostic.
**Unigram vs BPE на практике.** Unigram имеет теоретическое преимущество: может давать *несколько* возможных сегментаций с вероятностями, что полезно для data augmentation. Во время обучения модели можно сэмплировать разные токенизации одного текста - это **subword regularization** (Kudo, 2018), улучшает робастность модели.
Почему SentencePiece обозначает пробел специальным символом ▁ и включает его в токены, а не просто разбивает текст по пробелам?
Размер словаря и специальные токены
Один из важнейших гиперпараметров токенизатора - **размер словаря (vocabulary size)**. GPT-2 использует 50,257 токенов, LLaMA - 32,000, GPT-4 - 100,256. Это не случайные числа - за ними стоит фундаментальный **trade-off**, который напрямую влияет на качество модели.
**Как размер словаря влияет на длину последовательности?** Предложение «Tokenization is important for language models» при словаре 1K разбивается на ~15 токенов (много коротких кусков), а при словаре 100K - на ~6 токенов (целые слова). Критично: у Transformer сложность **O(n^2)** от длины последовательности. Вдвое длиннее = вчетверо дороже по памяти и вычислениям.
**Специальные токены** - зарезервированные ID с особым значением для модели. Они не встречаются в обычном тексте, но управляют поведением модели.
| Токен | Значение | Где используется |
|---|---|---|
| [CLS] | Начало входа, его embedding = представление всего текста | BERT |
| [SEP] | Разделитель между двумя сегментами текста | BERT |
| [MASK] | Замаскированный токен (для предсказания при обучении) | BERT |
| [PAD] | Дополнение до одинаковой длины в batch | Все модели |
| <|endoftext|> | Конец документа / начало нового | GPT-2/3 |
| <|im_start|> | Начало сообщения (роль: system/user/assistant) | ChatGPT |
| <|im_end|> | Конец сообщения | ChatGPT |
| <s>, </s> | Начало и конец последовательности | LLaMA, T5 |
**Мультиязычная дискриминация.** Токенизаторы, обученные преимущественно на английском, расходуют 2-5x больше токенов на тот же текст в других языках. Предложение из 10 слов на английском = ~10 токенов, на русском = ~20, на японском = ~30. Это означает: 1. меньше контекста влезает в окно 2. генерация медленнее 3. **дороже по деньгам** (API тарифицирует по токенам). LLaMA 3 увеличил словарь до 128K именно для лучшей мультиязычной поддержки.
**Byte-fallback** - страховочный механизм. Если токенизатор не может разбить последовательность байтов на известные субслова, он откатывается к **отдельным байтам** (0-255). Это гарантирует, что *любой* вход можно токенизировать - от UTF-8 текста до бинарных данных. GPT-2+ использует byte-level BPE, где базовые 256 токенов - это байты, а всё остальное - слияния поверх них.
**Почему GPT-2 не мог считать?** Число «13579» токенизировалось как «135» + «79» или «1» + «3579» - непредсказуемо. Модель не видела целое число и не могла выполнить арифметику. Современные решения: 1. специальная tokenization для чисел (каждая цифра - отдельный токен) 2. chain-of-thought prompting (разбить вычисление на шаги) 3. tool use (калькулятор).
Ключевые идеи
- **BPE** - итеративное слияние частых пар символов. Начинает с алфавита, добавляет токены снизу вверх. Используется в GPT-2/3/4, LLaMA. Помните, как «5678» разбивалось на «56» + «78»? Теперь понятно почему
- **WordPiece** - как BPE, но выбирает пары по likelihood (PMI), а не по частоте. Маркер ## для продолжений слов. Используется в BERT
- **SentencePiece** - language-agnostic: работает с сырым текстом, пробел ▁ - часть алфавита. Одинаково обрабатывает английский, японский, арабский
- **Размер словаря** (32K-100K) - критический trade-off: маленький = длинные последовательности, большой = разреженные embeddings. Токенизация определяет, что модель *видит* - и чего она не может
Связанные темы
Токенизация - первый шаг в pipeline любой языковой модели:
- Языковые модели: от n-gram до GPT — Токенизация определяет единицы, которые модель предсказывает - именно токены, а не слова или символы
- Transformer-архитектура — Embedding layer преобразует token IDs в векторы - размер словаря определяет размер embedding-матрицы
- Embeddings и представления — Качество токенизации влияет на качество обученных embeddings - редкие токены получают плохие представления
Вопросы для размышления
- Если токенизатор обучен на 90% английского текста, модель будет хуже работать с русским. Как бы спроектировать «справедливый» мультиязычный токенизатор? Какие trade-offs возникают?
- BPE разбивает числа непредсказуемо: «12345» -> «123» + «45» или «1» + «2345». Как решить проблему арифметики в LLM, не меняя архитектуру модели?
- Unigram-модель может давать *несколько* возможных сегментаций одного текста с разными вероятностями. Как это можно использовать для улучшения обучения модели? (Подсказка: data augmentation)
Связанные уроки
- gai-02 — Языковые модели предсказывают токены - нужно понимать что именно предсказывается
- gai-04 — Embedding layer преобразует token IDs в векторы - размер словаря определяет матрицу
- gai-05 — Качество токенизации влияет на качество обученных embeddings
- nlp-03 — TF-IDF также работает с частотами слов - разные подходы к представлению текста
- it-01 — BPE - алгоритм сжатия: информационная теория объясняет почему частые пары сливаются
- alg-01 — BPE - жадный алгоритм, понимание big-O помогает оценить стоимость обучения
- fl-05-regex