Перейти к содержимому
Ступень 44.2 Токенизация

Токенизаторы современных моделей

Сравниваем словари GPT-4o, Llama 3, Qwen3 и DeepSeek-V3 на русском и китайском, разбираем спецтокены и chat-шаблоны и учимся работать с tiktoken и Hugging Face tokenizers.

УРОКstage-4/4-2-tokenization/03-tokenizers-in-practice.mdx1 ч 30 минУрок 3 из 3
После урока вы сможете
  • Сравнить словари GPT-4o, Llama 3, Qwen3 и DeepSeek-V3 по размеру и эффективности на русском и китайском тексте
  • Объяснить, как chat-шаблон превращает список сообщений в строку со спецтокенами, и найти в нём типичные ошибки
  • Считать токены с помощью tiktoken и Hugging Face (tokenizers, transformers)
  • Обучить собственный byte-level BPE-токенизатор библиотекой tokenizers

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

  • Какую модель брать под язык. Если токенизатор режет ваш русский текст в полтора раза мельче, контекст фактически короче, генерация медленнее, а дообучение дороже.
  • Сколько стоит запрос. API берут деньги за токены, и считать их нужно тем же токенизатором, что у модели.
  • Как не сломать модель на ровном месте. Неправильный chat-шаблон или лишний спецтокен не вызывают никакой ошибки — модель просто начинает отвечать хуже или не может остановиться. Это одна из самых частых «тихих» ошибок при дообучении и запуске open-weightsоткрытые весаEN open weights моделей (моделей, чьи веса выложены в открытый доступ).

Все числа ниже посчитаны настоящими файлами токенизаторов. «Токены из кириллицы» — токены, состоящие только из кириллических букв (с пробелом впереди или без); «токены из иероглифов» — то же для китайских иероглифов (основной блок Unicode U+4E00–U+9FFF).

МодельГодТипОбычных токеновИз кириллицыИз иероглифов
GPT-22019byte-level BPE50 2561742
GPT-4 (cl100k_base)2023byte-level BPE100 256729832
Llama 22023SentencePiece BPE + байтовый откат32 0001 7311 459
Llama 32024byte-level BPE128 0006 4234 132
GPT-4o (o200k_base)2024byte-level BPE199 99814 0687 080
DeepSeek-V32024byte-level BPE128 0005 25135 184
Qwen32025byte-level BPE151 6434 12224 953
MiniMind-32026byte-level BPE6 400 (вместе со спецтокенами)213 112

Сверху к этим словарям добавляются спецтокены: у Llama 3 их 256 (итого 128 256), у Qwen3 — 26 (151 669), у DeepSeek-V3 — 815 (128 815). Строго говоря, у Llama 2 и DeepSeek-V3 первые три номера (0, 1, 2) тоже служебные — <unk>, <s>, </s> у Llama 2 и BOS, EOS, PAD у DeepSeek-V3, — так что обычных токенов у них на три меньше, чем в таблице.

Поле vocab_size в конфиге модели бывает ещё больше: 151 936 у Qwen3 и 129 280 у DeepSeek-V3. Матрицу эмбеддингов дополняют с запасом до размера, кратного 128 (151 936=1187⋅128151\,936 = 1187 \cdot 128, 129 280=1010⋅128129\,280 = 1010 \cdot 128): так удобнее GPU, и остаётся место для новых спецтокенов. Лишние строки просто не используются.

Теперь главное — сколько токенов уходит на один и тот же абзац (тот же параллельный текст, что в уроке 1: 277 символов по-английски, 278 по-русски).

ТокенизаторENRUСимволов RU на токен
GPT-2533030.92
Llama 2541012.75
GPT-4521222.28
Llama 352783.56
GPT-4o52694.03
DeepSeek-V353743.76
Qwen352853.27
MiniMind-3752900.96

Через те же токенизаторы мы прогнали и китайский перевод абзаца (76 символов) — о нём пункт 4 ниже.

Что из этого следует.

  1. Английский решён давно. От GPT-2 до Qwen3 — 52–54 токена. Вся борьба идёт за остальные языки.
  2. На русском лидирует GPT-4o (69), за ним DeepSeek-V3 (74) и Llama 3 (78). Qwen3 (85) уступает: его словарь надстроен над GPT-4, и русских токенов в нём меньше всего среди современных.
  3. Размер словаря — не главное, главное — как он поделён между языками. У Llama 2 словарь втрое меньше, чем у GPT-4, но кириллических токенов в нём больше (1731 против 729) — и русский текст у неё короче (101 против 122).
  4. На китайском лидируют китайские модели. Китайский перевод абзаца DeepSeek-V3 кодирует в 47 токенов, Qwen3 — в 53, а GPT-4o и Llama 3 — в 64 и 66 (GPT-2 — в 156, Llama 2 — в 80). В словарях DeepSeek-V3 и Qwen3 35 и 25 тысяч токенов из иероглифов — в 6–8.5 раза больше, чем у Llama 3. DeepSeek-V3 кодирует китайский абзац даже короче английского.
  5. Llama 3 — это GPT-4 плюс 27 744 токена. Первые 100 256 токенов её словаря совпадают с cl100k_base вместе с номерами. Добавка почти целиком ушла на другие языки: русский абзац сократился со 122 до 78 токенов.
  6. Маленький словарь — осознанная жертва. MiniMind-3 хорошо справляется с китайским, а русского в её корпусе почти нет: 21 кириллический токен, и русский текст распадается почти на байты.

За пять лет типичный словарь вырос с 32–50 тысяч (Llama 2, GPT-2) до 128–200 тысяч (Llama 3, Qwen, DeepSeek, GPT-4o). Причина — многоязычность: язык со своей письменностью требует тысяч, а то и десятков тысяч мест в словаре (в таблице выше: 4–14 тысяч для кириллицы у современных моделей, 25–35 тысяч для иероглифов у китайских). В техотчёте Llama 3 прямо сказано, что 100 тысяч токенов взяли из tiktoken, а 28 тысяч добавили ради других языков.

Цена роста — та же формула из урока 1: V⋅dV \cdot d параметров в эмбеддингах и столько же в выходном слое без связывания весов. Для модели на сотни миллиардов параметров это доли процента. Для маленькой — заметная часть бюджета, поэтому учебные модели вроде MiniMind (6400 токенов) и LLaMA2-подобной модели из happy-llm (6144 токена) берут маленькие словари.

Базовая модельбазовая модельEN base model после претрейна умеет только продолжать текст. Чтобы она стала ассистентом, её дообучают на диалогах (модуль 6.1), а диалог нужно как-то записать одной строкой. Для этого есть chat-шаблонchat-шаблонEN chat templateПравило (обычно Jinja в tokenizer_config.json), которое превращает список сообщений с ролями в одну строку со спецтокенами.: правило, которое превращает список сообщений с ролями в строку со спецтокенамиспециальный токен (спецтокен)EN special tokenУправляющий токен, а не кусок текста — начало/конец текста (BOS/EOS), заполнитель для батча (PAD), разметка ролей в чате (например, <|im_start|> у Qwen).. Шаблон хранится в tokenizer_config.json (поле chat_template) в виде программы на языке шаблонов Jinja — в новых репозиториях иногда отдельным файлом chat_template.jinja. Применяет его метод tokenizer.apply_chat_template(messages).

flowchart LR
A["messages: system, user"] --> B["chat_template (Jinja)"]
B --> C["строка со спецтокенами"]
C --> D["токенизатор"]
D --> E["id → модель"]
E --> F["ответ до end-of-turn токена"]
Путь сообщения от JSON до модели

Один и тот же диалог (system: «Ты — полезный ассистент.», user: «Что такое токен?») в форматах трёх моделей. Это вывод apply_chat_template(..., add_generation_prompt=True), проверенный на их настоящих шаблонах.

Qwen3 — формат ChatML. Здесь показан вызов с enable_thinking=False, поэтому в конце стоит пустой блок рассуждений:

<|im_start|>system
Ты — полезный ассистент.<|im_end|>
<|im_start|>user
Что такое токен?<|im_end|>
<|im_start|>assistant
<think>
</think>

Llama 3 (шаблон первых моделей Llama 3 Instruct; у Llama 3.1 и новее в system-сообщение дополнительно вписываются даты):

<|begin_of_text|><|start_header_id|>system<|end_header_id|>
Ты — полезный ассистент.<|eot_id|><|start_header_id|>user<|end_header_id|>
Что такое токен?<|eot_id|><|start_header_id|>assistant<|end_header_id|>

DeepSeek-V3 (вертикальные черты в маркерах здесь напечатаны обычным символом |, почему — ниже):

<|begin▁of▁sentence|>Ты — полезный ассистент.<|User|>Что такое токен?<|Assistant|>

Как это читать:

  • Каждый маркер — один токен, а не набор символов: у Qwen <|im_start|> = 151644, <|im_end|> = 151645; у Llama 3 <|begin_of_text|> = 128000, <|eot_id|> = 128009; у DeepSeek-V3 <|User|> = 128803, <|Assistant|> = 128804. Обратите внимание: в настоящих маркерах DeepSeek стоят не обычные | и _, а полноширинная вертикальная черта (отдельный символ Unicode U+FF5C, в Python — "\uff5c") и символ ▁. На этой странице мы для простоты печатаем обычную |; если будете искать эти токены в словаре сами, копируйте их из файлов токенизатора, а не набирайте вручную.
  • Шаблон заканчивается заголовком ассистента (add_generation_prompt=True). Модель продолжает строку — и пишет ответ именно от лица ассистента.
  • Ответ модель заканчивает своим токеном конца реплики: <|im_end|> у Qwen, <|eot_id|> у Llama 3, <|end▁of▁sentence|> у DeepSeek-V3. Движок генерации должен остановиться на нём.
  • Разметка стоит токенов. У Qwen3 весь промпт выше — 35 токенов, а сам текст сообщений — 18. Остальные 17 — разметка: спецтокены, имена ролей, переводы строк и 4 токена пустого блока <think>. В длинном диалоге это мелочь, в миллионе коротких запросов — нет.
  1. Чужой шаблон. Формат Llama для Qwen или «просто текст» вместо шаблона. Ошибки не будет, но модель видит разметку, которой не встречала при дообучении, — отвечает хуже, путает роли, не останавливается.
  2. Двойной BOS. Шаблоны Llama 3 и DeepSeek-V3 уже начинаются с BOS, а их токенизаторы в transformers ещё и сами добавляют BOS в начало. Если отрендерить шаблон в строку и затем токенизировать её с add_special_tokens=True (а это значение по умолчанию в tok(text) и tok.encode(text)), BOS окажется в начале дважды. У Qwen такой проблемы нет: его токенизатор BOS не добавляет. Документация transformers советует в этом случае add_special_tokens=False или сразу apply_chat_template(..., tokenize=True).
  3. Нет остановки. Если движок не знает, что конец реплики — <|im_end|>, модель допишет за пользователя следующую реплику.
  4. Спецтокены в пользовательском тексте. Пользователь присылает строку, в которой есть <|im_end|>, а за ним — поддельная реплика system. Hugging Face tokenizers по умолчанию превращает такую строку в настоящий спецтокен 151645: это проверено на токенизаторе Qwen3. Получается промпт-инъекцияпромпт-инъекцияEN prompt injectionАтака, при которой инструкции злоумышленника попадают в контекст модели (напрямую или через веб-страницу, письмо, файл) и перехватывают управление. на уровне разметки. tiktoken по умолчанию в такой ситуации бросает исключение (пример ниже). Надёжная защита — вырезать или экранировать такие строки из пользовательского ввода до токенизации; в transformers для этого также есть параметр split_special_tokens.

Библиотека OpenAI на Rust: быстрые encode и decode для словарей GPT (gpt2, cl100k_base, o200k_base). Для обучения своих словарей она не предназначена — есть лишь учебный модуль tiktoken._educational с простой реализацией BPE. Установка: pip install tiktoken, при первом вызове скачивается файл словаря.

import tiktoken
enc = tiktoken.get_encoding("o200k_base") # токенизатор GPT-4o
print(enc.n_vocab) # размер словаря вместе со спецтокенами
text = "Токенизатор режет текст на кусочки."
ids = enc.encode(text)
print(len(text), "символов ->", len(ids), "токенов")
print([enc.decode([i]) for i in ids])
# Спецтокены в пользовательском тексте по умолчанию запрещены:
try:
enc.encode("конец<|endoftext|>")
except ValueError as e:
print("ValueError:", str(e).splitlines()[0])
print(enc.encode("конец<|endoftext|>", allowed_special={"<|endoftext|>"})) # как спецтокен
print(enc.encode("конец<|endoftext|>", disallowed_special=())) # как обычный текст
200019
35 символов -> 12 токенов
['Т', 'ок', 'ен', 'из', 'атор', ' реж', 'ет', ' текст', ' на', ' кус', 'очки', '.']
ValueError: Encountered text corresponding to disallowed special token '<|endoftext|>'.
[31817, 11892, 199999]
[31817, 11892, 27, 91, 419, 1440, 919, 91, 29]

Последняя строка — та же надпись, разобранная на обычные куски: <, |, end, of, text, |, >.

tokenizers — библиотека Hugging Face на Rust: обучение и применение BPE, WordPiece и Unigram, формат tokenizer.json. transformers оборачивает её в AutoTokenizer и добавляет chat-шаблоны. Токенизатор скачивается без весов модели: у Qwen3 главный файл tokenizer.json весит около 11 МБ.

from transformers import AutoTokenizer
# from_pretrained скачивает с Hugging Face Hub файлы токенизатора по имени репозитория «организация/модель»
# (при первом запуске — из интернета, потом из локального кэша)
tok = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
print(len(tok)) # обычные + служебные токены
ids = tok.encode("Привет, мир!")
print(ids)
print(tok.convert_ids_to_tokens(ids)) # как токены записаны в словаре
print([tok.decode([i]) for i in ids]) # как они выглядят текстом
messages = [
{"role": "system", "content": "Ты — полезный ассистент."},
{"role": "user", "content": "Что такое токен?"},
]
# tokenize=False — вернуть строку, а не id; add_generation_prompt=True — дописать заголовок ассистента;
# enable_thinking=False — особый параметр шаблона Qwen3: выключить режим рассуждений
prompt = tok.apply_chat_template(messages, tokenize=False, add_generation_prompt=True, enable_thinking=False)
print(prompt)
151669
[53645, 26991, 8178, 11, 137144, 0]
['ÐŁÑĢ', 'ив', 'еÑĤ', ',', 'ĠмиÑĢ', '!']
['Пр', 'ив', 'ет', ',', ' мир', '!']
<|im_start|>system
Ты — полезный ассистент.<|im_end|>
<|im_start|>user
Что такое токен?<|im_end|>
<|im_start|>assistant
<think>
</think>

Третья строка вывода — byte-level запись из урока 2: Ġ — пробел, Ð и Ñ — первые байты кириллицы.

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

import tiktoken
from transformers import AutoTokenizer
texts = {
"en": "Large language models read text as a sequence of tokens. A tokenizer splits words into frequent pieces, "
"so common words become a single token, and rare words are broken into several parts. This is why "
"the same sentence costs a different number of tokens in different languages.",
"ru": "Большие языковые модели читают текст как последовательность токенов. Токенизатор разбивает слова "
"на частые кусочки: распространённые слова становятся одним токеном, а редкие распадаются на несколько "
"частей. Поэтому одна и та же фраза стоит разное число токенов на разных языках.",
}
qwen = AutoTokenizer.from_pretrained("Qwen/Qwen3-0.6B")
deepseek = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-V3")
# имя модели → функция «текст → список id»; add_special_tokens=False — не добавлять BOS, считаем только текст
counters = {
"GPT-2": tiktoken.get_encoding("gpt2").encode,
"GPT-4o": tiktoken.get_encoding("o200k_base").encode,
"Qwen3": lambda s: qwen.encode(s, add_special_tokens=False),
"DeepSeek-V3": lambda s: deepseek.encode(s, add_special_tokens=False),
}
print(f"{'':12}{'EN':>5}{'RU':>5} символов RU на токен")
for name, encode in counters.items():
n = {lang: len(encode(t)) for lang, t in texts.items()} # число токенов для каждого языка
print(f"{name:12}{n['en']:5}{n['ru']:5} {len(texts['ru']) / n['ru']:.2f}")
EN RU символов RU на токен
GPT-2 53 303 0.92
GPT-4o 52 69 4.03
Qwen3 52 85 3.27
DeepSeek-V3 53 74 3.76

Llama 3 в скрипт не включена: её репозиторий на Hugging Face закрытый, и доступ нужно сначала запросить на странице модели.

Так обучает словарь проект MiniMind (trainer/train_tokenizer.py): byte-level BPE из библиотеки tokenizers. Ниже — уменьшенная версия на семи предложениях.

from tokenizers import Tokenizer, decoders, models, pre_tokenizers, trainers
texts = [
"Языковая модель не видит букв. Она видит только номера токенов.",
"Токенизатор режет текст на кусочки и переводит каждый кусочек в номер.",
"Частые слова становятся одним токеном, редкие слова распадаются на части.",
"Алгоритм BPE начинает с байтов и много раз сливает самую частую пару соседних токенов.",
"Сначала из байтов собираются буквы, потом слоги, потом целые слова.",
"Модель учится предсказывать следующий токен, а токенизатор решает, что такое токен.",
"Хороший токенизатор делает текст короче, и модель обрабатывает больше смысла за тот же контекст.",
]
tok = Tokenizer(models.BPE()) # пустой BPE-токенизатор
tok.pre_tokenizer = pre_tokenizers.ByteLevel(add_prefix_space=False) # байты + предразбиение в духе GPT-2
tok.decoder = decoders.ByteLevel() # как собирать текст из байтов при decode
trainer = trainers.BpeTrainer(
vocab_size=400, # итоговый размер словаря
special_tokens=["<|endoftext|>", "<|im_start|>", "<|im_end|>"], # спецтокены: в словарь целиком, BPE их не режет
initial_alphabet=pre_tokenizers.ByteLevel.alphabet(), # все 256 байтов
)
tok.train_from_iterator(texts, trainer=trainer) # обучение слияний — тот же цикл, что в уроке 2
print(tok.get_vocab_size())
enc = tok.encode("<|im_start|>модель режет текст на токены<|im_end|>")
print(enc.ids)
print([tok.decode([i], skip_special_tokens=False) for i in enc.ids])
print(tok.decode(enc.ids, skip_special_tokens=False))
tok.save("my_tokenizer.json")
400
[1, 330, 324, 292, 266, 376, 338, 319, 282, 278, 2]
['<|im_start|>', 'м', 'одель', ' р', 'е�', '�ет', ' текст', ' на', ' токен', 'ы', '<|im_end|>']
<|im_start|>модель режет текст на токены<|im_end|>

Словарь в 400 токенов — это 3 спецтокена (они получают первые номера 0, 1, 2), 256 байтов и 141 слияние. Спецтокены распознаются целиком: <|im_start|> — один токен с id 1. Разбиение модель режет текст на токены почти совпадает с результатом RegexTokenizer из урока 2: алгоритм тот же, данные те же. Отличия есть: здесь 141 слияние, а не 100 (поэтому второй байт ж успел склеиться с ет в �ет), другое правило выбора при равных частотах и встроенное предразбиение ByteLevel.

У настоящего MiniMind 6400 токенов, из них 36 служебных: <|im_start|>, <|im_end|>, <think>, <tool_call>, метки для картинок и аудио, а также резервные <|buffer1|> и далее — запас на будущее расширение. В комментарии к скрипту авторы предупреждают: обучать свой токенизатор стоит ради учёбы, а модели с разными словарями несовместимы между собой.

Пробелы и регистр. В словаре Qwen3 мир (id 130178) и мир (id 137144) — разные токены, а Мир — уже два токена: М + ир. Для модели это три разных объекта, связь между ними она выучивает из данных. Отсюда неустойчивость к регистру и к лишнему пробелу в конце промпта.

«Глючные» токены. Если строка попала в словарь, но почти не встречалась в обучающих данных модели, эмбеддинг этого токена остаётся почти случайным. Самый известный пример — токен SolidGoldMagikarp (id 43453) в словаре GPT-2: в 2023 году исследователи показали, что GPT-3 с тем же словарём не могла даже повторить его. В новых словарях OpenAI эта строка уже режется на 5 обычных кусков. В словаре Llama 3 есть токен из 32 букв џ подряд — тоже явный мусор в данных.

Сравнение моделей. PerplexityперплексияEN perplexity и loss считаются на токен, а в токенах разных словарей разное количество текста. Модель с крупными токенами получит больший loss на токен, даже если предсказывает текст не хуже. Для честного сравнения loss пересчитывают на байт текста — это bits per byteбит на байт (BPB)EN bits per byte (BPB)Loss, пересчитанный на байт исходного текста; в отличие от perplexity, его можно сравнивать между моделями с разными токенизаторами. (BPB):

BPB=∑tCEtln⁡2⋅Nбайт\mathrm{BPB} = \frac{\sum_t \mathrm{CE}_t}{\ln 2 \cdot N_{\text{байт}}}

где сумма cross-entropy (в натах — единицах информации при натуральном логарифме, урок 1.3.3) берётся по всем токенам текста, а NбайтN_{\text{байт}} — длина того же текста в байтах UTF-8. Деление на ln⁡2\ln 2 переводит наты в биты. В числителе — сколько всего «удивления» модель испытала на тексте, в знаменателе — сколько в тексте байтов, поэтому от токенизатора результат уже не зависит.

Пример. Текст длиной 1000 байт. Токенизатор модели A режет его на 250 токенов, средний loss — 2.0, сумма — 500 нат. Токенизатор модели B даёт 400 токенов со средним loss 1.6, сумма — 640 нат. По loss на токен B выглядит лучше (1.6 против 2.0), но BPBA=500/(0.693⋅1000)≈0.72\mathrm{BPB}_A = 500 / (0.693 \cdot 1000) \approx 0.72, а BPBB=640/(0.693⋅1000)≈0.92\mathrm{BPB}_B = 640 / (0.693 \cdot 1000) \approx 0.92: на самом деле текст лучше предсказывает A. Авторы MiniMind в README советуют именно BPB для сравнения моделей с разными токенизаторами.

Подсчёт стоимости. Считайте токены тем же токенизатором, что у модели, и на своих данных. Популярное правило «токен ≈ 4 символа» придумано для английского (в нашем абзаце вышло даже 5.3). Для русского у GPT-4o в нашем замере около 4, у GPT-2 было меньше 1, а для китайского у современных моделей — 1.2–1.6 символа на токенстепень сжатия токенизатораEN compression ratio (characters per token)Сколько символов (или байтов) текста приходится в среднем на один токен. Чем выше — тем «дешевле» язык для модели..

Проверь себя6 вопросов
1 Какой токенизатор из нашего сравнения закодировал китайский перевод абзаца в наименьшее число токенов?
2 Выберите верные утверждения о chat-шаблонах.Можно выбрать несколько ответов
3 Контекст модели — 128 000 токенов. Сколько тысяч символов русского текста в него поместится, если токенизатор даёт 3.5 символа на токен?
4 Что сделает tiktoken при вызове enc.encode("текст<|endoftext|>") без дополнительных параметров?
5 Почему нельзя напрямую сравнивать perplexity Qwen3 и Llama 3?
6 Сколько токенов в словаре Llama 3 добавлено поверх 100 256 токенов cl100k_base, если обычных токенов 128 000?
  • Эффективность токенизатора на языке определяется не размером словаря, а тем, сколько мест в нём отдано этому языку. На русском лучше всех из нашей выборки GPT-4o, на китайском — DeepSeek-V3 и Qwen3.
  • Chat-шаблон из tokenizer_config.json превращает сообщения в строку со спецтокенами. Чужой шаблон, двойной BOS или отсутствие остановки на <|im_end|> ломают модель без единой ошибки в логах.
  • Спецтокены из пользовательского текста нельзя пропускать как есть: Hugging Face tokenizers превратит их в настоящую разметку.
  • tiktoken — быстрые encode и decode для словарей OpenAI; tokenizers и transformers — обучение своих словарей и chat-шаблоны для open-weights моделей.
  • Для сравнения моделей с разными токенизаторами используйте bits per byte, а не perplexity на токен.

Дальше: модуль 4.3 «Эмбеддинги» — что модель делает с номерами токенов: таблица nn.Embedding, слова как векторы и арифметика смыслов — Слова как векторы.