Токенизация и BPE: как текст превращается в числа для LLM
Введение: почему токенизация важна
Каждая LLM работает не с текстом, а с числами. Но как превратить "Привет, мир!" в последовательность токенов? От выбора токенизатора зависит:
- Сколько токенов займёт ваш промпт (и сколько это стоит)
- Как модель справляется с редкими языками
- Насколько эффективно модель использует контекстное окно
- Как модель работает с кодом, математикой, эмодзи
Разберём эволюцию токенизации: от word-level до BPE, SentencePiece и Byte-Level BPE.
Уровень 1: Word-level Tokenization
Классический подход
Текст: "I don't understand transformers"
Токены: ["I", "don't", "understand", "transformers"]
IDs: [1234, 5678, 9012, 3456]
Проблемы:
- Огромный словарь: 100K-500K уникальных слов даже для одного языка
- Неизвестные слова: OOV (out-of-vocabulary) — слово не в словаре = [UNK]
- Морфология игнорируется: "running", "ran", "runs" — три разных токена
- Неразборчивость с апострофами: "don't" → ["don", "'", "t"] или ["don't"]?
Словарь из 100K слов × контекстное окно 4K токенов
= Модель может видеть только 4K слов в промпте
= Для длинных текстов нужно много токенов
Уровень 2: Character-level Tokenization
Каждый символ — отдельный токен
Текст: "Hello"
Токены: ["H", "e", "l", "l", "o"]
IDs: [38, 12, 12, 12, 45]
Плюсы:
- Словарь фиксирован: 26 (английский) или ~1000 (китайский) символов
- Нет OOV: любой текст можно токенизировать
Минусы:
- Слишком много токенов: "Hello world" = 11 токенов вместо 2-3
- Модель тратит вычисления на последовательность, а не на смысл
- RNN/Transformer медленнее из-за длинной последовательности
Уровень 3: Subword Tokenization — BPE
Byte Pair Encoding (BPE)
BPE — компромисс между word-level и character-level. Идея: часто встречающиеся комбинации символов становятся отдельными токенами.
Алгоритм BPE (пошагово)
Шаг 1: Инициализация
Корпус: "low lower newer glue gluey"
Начальные токены (символы + граница слова):
{'l', 'o', 'w', ' ', 'l', 'o', 'w', 'e', 'r', ' ', 'n', 'e', 'w', 'e', 'r', ...}
=low =lower =newer =glue =gluey =gluey
Шаг 2: Подсчёт частот
"l" + "o" = 2 (в "low" и "lower")
"o" + "w" = 2 (в "low" и "lower")
"e" + "r" = 2 (в "lower" и "newer")
"n" + "e" = 2 (в "newer" и "newer")
"gl" + "u" + "e" = 2 (в "glue" и "gluey")
Шаг 3: Слияние самой частой пары
Самая частая пара: "l" + "o" = 2
Сливаем: "lo" → новый токен
Словарь: {'l', 'o', 'lo', 'w', ' ', 'e', 'r', 'n', 'new', 'glue', 'gluey', ...}
Шаг 4: Повторяем N раз
Итерация 1: "lo" → частота 2
Итерация 2: "er" → частота 2
Итерация 3: "low" → частота 2
Итерация 4: "new" → частота 2
Итерация 5: "lower" → частота 1
...
После 10K итераций: словарь ~10K токенов
После 50K итераций: словарь ~50K токенов
Результат токенизации
Токенизатор с 50K токенами:
"I don't understand transformers"
→ ["I", " don'", "t", " understand", " trans", "form", "ers"]
→ 7 токенов
"unbelievable"
→ ["un", "belie", "vable"]
→ 3 токена (даже если слово редкое!)
"хеллоу" (русское "hello")
→ ["х", "е", "л", "л", "о", "у"]
→ 6 токенов (каждый символ — токен)
SentencePiece: BPE для всех языков
Проблема стандартного BPE
Стандартный BPE требует пред-tokenization (разделение по пробелам). Это не работает для:
- Китайского: "你好世界" — нет пробелов
- Японского: "こんにちは世界" — нет пробелов
- Детоксикации: "don't" → "do n't" (ломает смысл)
Решение: SentencePiece
SentencePiece (Google, 2018) treats text as a raw stream of bytes:
import sentencepiece as spm
# Обучение
spm.SentencePieceTrainer.train(
'--input=train.txt --model_prefix=m --vocab_size=32000'
)
# Токенизация
sp = spm.SentencePieceProcessor(model_file='m.model')
tokens = sp.encode('Hello world!', out_type=str)
print(tokens) # ['▁Hello', '▁world', '!']
# Детокенизация
text = sp.decode(tokens)
print(text) # 'Hello world!'
Особенности SentencePiece:
▁(U+2581) обозначает начало слова- Нет зависимости от языка
- Обратимая токенизация: decode(encode(text)) == text
Byte-Level BPE (GPT-2, GPT-3)
Проблема: Unicode и кодировки
SentencePiece работает хорошо, но:
- Словарь ~50K токенов — всё ещё много
- Редкие слова могут токенизироваться неэффективно
Решение: Byte-Level BPE
Byte-Level BPE (используется в GPT-2/3, Claude) работает на уровне байтов:
Начальный словарь: все 256 байтов (0-255) + пробел
Токенизация "Hello":
H = 72 (ASCII)
e = 101
l = 108
l = 108
o = 111
Токенизация "Привет":
П = 0xD0 (UTF-8)
р = 0xD1
и = 0xB8
в = 0xB2
е = 0xD0
т = 0xD1
Токенизация "😀":
😀 = 0xF0 0x9F 0x98 0x80 (4 байта в UTF-8)
Ключевое преимущество: словарь всегда 49K токенов (256 байтов + слияния), и он покрывает любой текст.
# Пример токенизации GPT-2
import tiktoken
enc = tiktoken.get_encoding("gpt2")
text = "Hello, мир! 😀"
tokens = enc.encode(text)
print(tokens) # [15496, 11, 2706, 1649, 219, 259, 23577, 5]
print(enc.decode(tokens)) # 'Hello, мир! 😀'
# Длина в токенах
print(len(tokens)) # 8 токенов
Сравнение токенизаторов
Сколько токенов в одном слове?
Токенизатор | "transformers" | "unbelievable" | "хеллоу" | "😀😀😀"
-----------------|----------------|----------------|----------|----------
Word-level | 1 | 1 | 1 | 1
Character-level | 14 | 12 | 6 | 6
BPE (50K) | 2-3 | 2-3 | 3-4 | 1-2
Byte-Level BPE | 4-5 | 4-5 | 6-8 | 4-12
SentencePiece | 2-3 | 2-3 | 3-4 | 2-3
Реальные примеры
Текст: "I can't believe I can't unlearn my Keras habits"
GPT-4 (cl100k_base):
["I", " can", "'t", " believe", " I", " can", "'t", " un", "learn", " my", " K", "eras", " habits"]
→ 13 токенов
Llama 3 (tiktoken bpe):
["I", " can", "'", "t", " ", "belie", "ve", " I", " can", "'", "t", " ", "un", "learn", " my", " ", "K", "eras", " habits"]
→ 19 токенов
Claude (Claude tokenizer):
["I", " can't", " believe", " I", " can't", " un", "learn", " my", " Keras", " habits"]
→ 10 токенов
Разница в 50% между токенизаторами! Это значит:
- Разная стоимость промпта
- Разная эффективность контекстного окна
- Разное качество генерации (больше токенов = больше вычислений)
Как токенизация влияет на стоимость
Пример: OpenAI API pricing
GPT-4 Turbo:
Input: $10 / 1M tokens
Output: $30 / 1M tokens
Текст: 1000 слов (примерно эссе)
Word-level: ~1000 токенов → $0.01 за input
BPE (50K): ~1300 токенов → $0.013 за input
Character-level: ~5000 токенов → $0.05 за input
Разница: в 5 раз!
Оптимизация токенизации
# Плохо: дублирование
prompt = """
Ответь на вопрос.
Вопрос: Что такое трансформер?
Ответ:
"""
# Хорошо: без дублирования
prompt = """
Ответь на вопрос.
Вопрос: Что такое трансформер?
"""
# Ещё лучше: использовать system/user/assistant роли
messages = [
{"role": "system", "content": "Ты полезный ассистент."},
{"role": "user", "content": "Что такое трансформер?"}
]
Специальные токены
System tokens
Каждый токенизатор имеет специальные токены:
GPT-4 (cl100k_base):
<|endoftext|> — конец текста / разделитель документов
<|startoftext|> — начало текста
Llama 3:
<|begin_of_text|> — начало текста
<|end_of_text|> — конец текста
<|reserved_special_token_N|> — зарезервированные токены
Claude:
<|begin_of_sentence|> — начало предложения
<|end_of_sentence|> — конец предложения
GPT-3.5:
<|im_start|> — начало сообщения (system/user/assistant)
<|im_end|> — конец сообщения
<|separator|> — разделитель
Токены для диалоговых форматов
ChatML формат:
<|im_start|>system
Ты полезный ассистент.<|im_end|>
<|im_start|>user
Привет!<|im_end|>
<|im_start|>assistant
Привет! Чем могу помочь?<|im_end|>
Tokенизация:
[im_start, system, \n, Ты, полезный, ассистент, ., im_end, \n,
im_start, user, \n, Привет, !, im_end, \n,
im_start, assistant, \n, Привет, !, Чем, могу, помочь, ?, im_end]
→ 22 токена только на структуру диалога!
Проблемы токенизации
1. Fragmentation (фрагментация)
Одно слово разбивается на много токенов:
"Supercalifragilisticexpialidocious"
→ ["Su", "per", "cali", "fragi", "listi", "c", "expiali", "doci", "ous"]
→ 9 токенов для одного слова!
2. Multilingual imbalance
Английское слово "hello" → 1 токен
Китайское слово "你好" → 2-3 токена
Японское слово "こんにちは" → 5-6 токенов
→ LLM несправедливо "дороже" стоит для не-английских языков
3. Code tokenization
def calculate_sum(arr):
total = 0
for x in arr:
total += x
return total
GPT-4 токенизация:
["def", " calculate", "_", "sum", "(", "arr", ")", ":", "\n", " ", "total", " ", "=", " ", "0", "\n", ...]
→ ~40 токенов для 7 строк кода
Проблемы:
- Пробелы и отступы — отдельные токены
- Операторы (+, =, :) — отдельные токены
- Имена функций — разбиваются по "_"
4. Unicode edge cases
"café" → ["café"] или ["caf", "é"] или ["c", "a", "f", "é"]?
"naïve" → ["naïve"] или ["na", "ï", "ve"]?
"emoji" → "👨👩👧👦" → 16+ байтов в UTF-8 → 16+ токенов
Зависит от токенизатора и от того, как обучали модель.
Продвинутые техники
Dynamic BPE
Адаптивная токенизация: разные части текста токенизируются по-разному:
Контекст: математическая формула
"∑(i=1 to n) x_i² = S"
Специальный токенизатор для формул:
["∑", "(", "i", "=", "1", "to", "n", ")", " ", "x", "_", "i", "²", "=", "S"]
→ 15 токенов
Обычный BPE:
["∑", "(", "i", "=", "1", "to", "n", ")", " ", "x", "_", "i", "²", "=", "S"]
→ 15 токенов (совпало, но для кода разница больше)
Токенизаторы популярных моделей
GPT-4 (cl100k_base)
Словарь: ~100K токенов
Использует: Byte-Level BPE (tiktoken)
Пример:
"Hello, world!" → [15496, 11, 995, 0]
"Привет, мир!" → [25481, 2426, 11, 1093, 0]
Llama 3 (cl100k_base → tiktoken bpe_mixed)
Словарь: ~128K токенов
Использует: BPE (SentencePiece)
Пример:
"Hello, world!" → [122, 353, 749, 0]
"Привет, мир!" → [24263, 12837, 11, 6932, 0]
Claude (Anthropic)
Словарь: ~200K токенов
Использует: BPE (собственный)
Пример:
"Hello, world!" → [1, 4405, 11, 17272, 34699, 0]
Gemini (Google)
Словарь: ~256K токенов
Использует: SentencePiece
Пример:
"Hello, world!" → [3652, 11, 1476, 1968, 0]
Практические советы
Как посчитать токены без API?
# GPT-4 (cl100k_base)
import tiktoken
enc = tiktoken.get_encoding("cl100k_base")
tokens = enc.encode("Текст для подсчёта")
print(f"Токенов: {len(tokens)}")
# Llama 3
import transformers
tokenizer = transformers.AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
tokens = tokenizer.encode("Текст для подсчёта")
print(f"Токенов: {len(tokens)}")
Оптимизация промптов
# Плохо: повторяющаяся структура
prompt = """
Система: Ты полезный ассистент.
Пользователь: Привет!
Ассистент: Привет! Чем помочь?
Пользователь: Кто такой Эйнштейн?
Ассистент: Альберт Эйнштейн...
Пользователь: Что такое относительность?
Ассистент: Теория относительности...
"""
# Хорошо: chat format с ролями
messages = [
{"role": "system", "content": "Ты полезный ассистент."},
{"role": "user", "content": "Привет!"},
{"role": "assistant", "content": "Привет! Чем помочь?"},
{"role": "user", "content": "Кто такой Эйнштейн?"},
{"role": "assistant", "content": "Альберт Эйнштейн..."},
{"role": "user", "content": "Что такое относительность?"}
]
# → Модель сама добавляет специальные токены
Обработка длинных текстов
# Truncation: обрезаем по началу (сохраняем конец — часто важнее)
def truncate_prompt(prompt, max_tokens, tokenizer):
tokens = tokenizer.encode(prompt)
if len(tokens) <= max_tokens:
return prompt
# Сохраняем последние max_tokens
truncated = tokenizer.decode(tokens[-max_tokens:])
return truncated
# Sliding window: обрабатываем частями
def sliding_window_text(text, window_size=4096, overlap=512, tokenizer):
tokens = tokenizer.encode(text)
chunks = []
for i in range(0, len(tokens) - window_size, window_size - overlap):
chunk = tokenizer.decode(tokens[i:i+window_size])
chunks.append(chunk)
# Последний чанк
chunks.append(tokenizer.decode(tokens[-window_size:]))
return chunks
Будущее токенизации
Context-aware tokenization
Новые подходы используют саму модель для определения токенов:
Традиционный BPE:
"machine learning" → ["machine", " learning"]
→ Фиксированное разбиение
Context-aware BPE:
"machine learning" → ["machine", " learning"] (в контексте ML)
"machine learning algorithm" → ["machine", " learning", " algorithm"]
"learning to learn" → ["learning", " to", " learn"]
→ Разбиение зависит от контекста!
Variable-length tokenization
Идея: токены разной длины, как в естественном языке:
Короткие частые: "the", "is", "at" → 1 токен
Средние: "learning", "machine" → 1 токен
Длинные редкие: "superconductivity" → 1 токен
Очень редкие: "xqj" → ["x", "q", "j"]
Image tokenization (мультимодальность)
Для мультимодальных моделей текст + изображения:
Текст: "Что на картинке?"
→ 5 токенов
Изображение 224×224:
ViT разбивает на patches 16×16
→ (224/16) × (224/16) = 14 × 14 = 196 токенов
Итого: 5 + 196 = 201 токен
Итоги
- Tокенизация — первый шаг в пайплайне LLM, определяющий эффективность всей модели
- BPE — золотой стандарт: компромисс между словарём и гибкостью
- Byte-Level BPE (GPT) покрывает любой текст через байты UTF-8
- Разные токенизаторы дают разную длину одного текста (до 50% разницы)
- Специальные токены занимают значительную часть контекстного окна
- Код и математика токенизируются неэффективно — этоKnown problem
- Будущее: context-aware и variable-length tokenization
Понимание токенизации помогает оптимизировать промпты, снижать стоимость и улучшать качество генерации.
</final_file_content>