Токенизаторы современных моделей
Сравниваем словари GPT-4o, Llama 3, Qwen3 и DeepSeek-V3 на русском и китайском, разбираем спецтокены и chat-шаблоны и учимся работать с tiktoken и Hugging Face tokenizers.
Зачем это нужно
Заголовок раздела «Зачем это нужно»В прошлых уроках вы разобрались, как устроен BPE. Теперь посмотрим на токенизаторы, с которыми вы будете работать каждый день. От них зависят три практических решения.
- Какую модель брать под язык. Если токенизатор режет ваш русский текст в полтора раза мельче, контекст фактически короче, генерация медленнее, а дообучение дороже.
- Сколько стоит запрос. API берут деньги за токены, и считать их нужно тем же токенизатором, что у модели.
- Как не сломать модель на ровном месте. Неправильный chat-шаблон или лишний спецтокен не вызывают никакой ошибки — модель просто начинает отвечать хуже или не может остановиться. Это одна из самых частых «тихих» ошибок при дообучении и запуске open-weightsоткрытые весаEN open weights моделей (моделей, чьи веса выложены в открытый доступ).
Сравнение словарей
Заголовок раздела «Сравнение словарей»Все числа ниже посчитаны настоящими файлами токенизаторов. «Токены из кириллицы» — токены, состоящие только из кириллических букв (с пробелом впереди или без); «токены из иероглифов» — то же для китайских иероглифов (основной блок Unicode U+4E00–U+9FFF).
| Модель | Год | Тип | Обычных токенов | Из кириллицы | Из иероглифов |
|---|---|---|---|---|---|
| GPT-2 | 2019 | byte-level BPE | 50 256 | 17 | 42 |
GPT-4 (cl100k_base) | 2023 | byte-level BPE | 100 256 | 729 | 832 |
| Llama 2 | 2023 | SentencePiece BPE + байтовый откат | 32 000 | 1 731 | 1 459 |
| Llama 3 | 2024 | byte-level BPE | 128 000 | 6 423 | 4 132 |
GPT-4o (o200k_base) | 2024 | byte-level BPE | 199 998 | 14 068 | 7 080 |
| DeepSeek-V3 | 2024 | byte-level BPE | 128 000 | 5 251 | 35 184 |
| Qwen3 | 2025 | byte-level BPE | 151 643 | 4 122 | 24 953 |
| MiniMind-3 | 2026 | byte-level BPE | 6 400 (вместе со спецтокенами) | 21 | 3 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 (, ): так удобнее GPU,
и остаётся место для новых спецтокенов. Лишние строки просто не используются.
Теперь главное — сколько токенов уходит на один и тот же абзац (тот же параллельный текст, что в уроке 1: 277 символов по-английски, 278 по-русски).
| Токенизатор | EN | RU | Символов RU на токен |
|---|---|---|---|
| GPT-2 | 53 | 303 | 0.92 |
| Llama 2 | 54 | 101 | 2.75 |
| GPT-4 | 52 | 122 | 2.28 |
| Llama 3 | 52 | 78 | 3.56 |
| GPT-4o | 52 | 69 | 4.03 |
| DeepSeek-V3 | 53 | 74 | 3.76 |
| Qwen3 | 52 | 85 | 3.27 |
| MiniMind-3 | 75 | 290 | 0.96 |
Через те же токенизаторы мы прогнали и китайский перевод абзаца (76 символов) — о нём пункт 4 ниже.
Что из этого следует.
- Английский решён давно. От GPT-2 до Qwen3 — 52–54 токена. Вся борьба идёт за остальные языки.
- На русском лидирует GPT-4o (69), за ним DeepSeek-V3 (74) и Llama 3 (78). Qwen3 (85) уступает: его словарь надстроен над GPT-4, и русских токенов в нём меньше всего среди современных.
- Размер словаря — не главное, главное — как он поделён между языками. У Llama 2 словарь втрое меньше, чем у GPT-4, но кириллических токенов в нём больше (1731 против 729) — и русский текст у неё короче (101 против 122).
- На китайском лидируют китайские модели. Китайский перевод абзаца 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 кодирует китайский абзац даже короче английского.
- Llama 3 — это GPT-4 плюс 27 744 токена. Первые 100 256 токенов её словаря совпадают с
cl100k_baseвместе с номерами. Добавка почти целиком ушла на другие языки: русский абзац сократился со 122 до 78 токенов. - Маленький словарь — осознанная жертва. 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: параметров в эмбеддингах и столько же в выходном слое без связывания весов. Для модели на сотни миллиардов параметров это доли процента. Для маленькой — заметная часть бюджета, поэтому учебные модели вроде MiniMind (6400 токенов) и LLaMA2-подобной модели из happy-llm (6144 токена) берут маленькие словари.
Спецтокены и chat-шаблоны
Заголовок раздела «Спецтокены и chat-шаблоны»Базовая модельбазовая модель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 токена"]
Один и тот же диалог (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>. В длинном диалоге это мелочь, в миллионе коротких запросов — нет.
Типичные ошибки
Заголовок раздела «Типичные ошибки»- Чужой шаблон. Формат Llama для Qwen или «просто текст» вместо шаблона. Ошибки не будет, но модель видит разметку, которой не встречала при дообучении, — отвечает хуже, путает роли, не останавливается.
- Двойной 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). - Нет остановки. Если движок не знает, что конец реплики —
<|im_end|>, модель допишет за пользователя следующую реплику. - Спецтокены в пользовательском тексте. Пользователь присылает строку, в которой есть
<|im_end|>, а за ним — поддельная реплика system. Hugging Face tokenizers по умолчанию превращает такую строку в настоящий спецтокен 151645: это проверено на токенизаторе Qwen3. Получается промпт-инъекцияпромпт-инъекцияEN prompt injectionАтака, при которой инструкции злоумышленника попадают в контекст модели (напрямую или через веб-страницу, письмо, файл) и перехватывают управление. на уровне разметки. tiktoken по умолчанию в такой ситуации бросает исключение (пример ниже). Надёжная защита — вырезать или экранировать такие строки из пользовательского ввода до токенизации; в transformers для этого также есть параметрsplit_special_tokens.
Практика: tiktoken и Hugging Face
Заголовок раздела «Практика: tiktoken и Hugging Face»tiktoken
Заголовок раздела «tiktoken»Библиотека 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-4oprint(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=())) # как обычный текст20001935 символов -> 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, |, >.
Hugging Face: tokenizers и transformers
Заголовок раздела «Hugging Face: tokenizers и transformers»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 tiktokenfrom 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.92GPT-4o 52 69 4.03Qwen3 52 85 3.27DeepSeek-V3 53 74 3.76Llama 3 в скрипт не включена: её репозиторий на Hugging Face закрытый, и доступ нужно сначала запросить на странице модели.
Свой токенизатор за 15 строк
Заголовок раздела «Свой токенизатор за 15 строк»Так обучает словарь проект 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-2tok.decoder = decoders.ByteLevel() # как собирать текст из байтов при decodetrainer = 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):
где сумма cross-entropy (в натах — единицах информации при натуральном логарифме, урок 1.3.3) берётся по всем токенам текста, а — длина того же текста в байтах UTF-8. Деление на переводит наты в биты. В числителе — сколько всего «удивления» модель испытала на тексте, в знаменателе — сколько в тексте байтов, поэтому от токенизатора результат уже не зависит.
Пример. Текст длиной 1000 байт. Токенизатор модели A режет его на 250 токенов, средний loss — 2.0, сумма — 500 нат. Токенизатор модели B даёт 400 токенов со средним loss 1.6, сумма — 640 нат. По loss на токен B выглядит лучше (1.6 против 2.0), но , а : на самом деле текст лучше предсказывает A. Авторы MiniMind в README советуют именно BPB для сравнения моделей с разными токенизаторами.
Подсчёт стоимости. Считайте токены тем же токенизатором, что у модели, и на своих данных. Популярное правило «токен ≈ 4 символа» придумано для английского (в нашем абзаце вышло даже 5.3). Для русского у GPT-4o в нашем замере около 4, у GPT-2 было меньше 1, а для китайского у современных моделей — 1.2–1.6 символа на токенстепень сжатия токенизатораEN compression ratio (characters per token)Сколько символов (или байтов) текста приходится в среднем на один токен. Чем выше — тем «дешевле» язык для модели..
Проверь себя
Заголовок раздела «Проверь себя»- Эффективность токенизатора на языке определяется не размером словаря, а тем, сколько мест в нём отдано этому языку. На русском лучше всех из нашей выборки 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, слова как векторы
и арифметика смыслов — Слова как векторы.