Multilingual Tokenization: Scripts, Spaces & Costs¶
Overview¶
Multilingual tokenization is where tokenizer design decisions become expensive. Languages without spaces (Chinese, Japanese, Thai), scripts with thousands of characters (CJK, Devanagari, Arabic), and unbalanced training data can inflate token counts 2–4× compared to English — directly multiplying API cost, context-window usage, and training compute. This note covers why it's hard and the toolbox modern tokenizers use to cope.
- Root problem: tokenizers trained on English assume spaces separate words
- Real cost: 2–4× more tokens for the same meaning in CJK/Indic scripts
- Toolbox: larger vocab, character_coverage, byte_fallback, language sampling, normalization
- Prerequisites: 00 Tokenization Fundamentals, 01 Bpe (Byte Pair Encoding), 02 Sentencepiece
Why English-Centric Tokenizers Break¶
Assumption 1: Spaces Separate Words¶
BPE/WordPiece training:
- pre-tokenize: split on spaces → "hello" "world"
- learn merges WITHIN words
This works for: English, Spanish, French, German, Russian...
Breaks for: Chinese 你好世界, Japanese こんにちは, Thai สวัสดี
"你好世界" (hello world):
- no spaces → pre-tokenization returns the WHOLE string as one "word"
- BPE then merges characters of the entire sentence together
- or falls back to per-character tokens — 4 tokens for 4 chars
Assumption 2: A Small Alphabet Covers Everything¶
Latin alphabet: ~26 letters + digits + punctuation ≈ 100–200 characters
CJK scripts: thousands of common characters (GB2312: 6,763; JIS: 6,355)
Devanagari: ~60 chars + thousands of conjuncts
Emoji: 〜2,000+ distinct pictographs (and growing)
character_coverage=0.9995 on a CJK corpus → the 0.05% tail is still
thousands of characters — each unseen one becomes [UNK] or a byte
sequence.
Assumption 3: One Token ≈ One Word¶
English: "hello" ≈ 1 token (word ~ 5 chars, tokenizer learns it)
Japanese: "こんにちは" = 5 chars; a good tokenizer may pack common
words, but many stay 1 token per 1–2 chars
Chinese: ~1 token per character (characters ARE the morphemes)
→ 2–3 tokens per English word
The same meaning needs 2–4× more tokens in CJK.
The Token Inflation Problem (Concrete)¶
Same text, different languages, GPT-4-style tokenizer (approximate):
Text: "The quick brown fox jumps over the lazy dog"
English: 11 tokens (1.00× baseline)
Spanish: ~12 tokens (1.1×)
German: ~14 tokens (1.3×) (compounds: "Rechtsschutzversicherung")
Arabic: ~15 tokens (1.4×) (diacritics, joining forms)
Hindi: ~18 tokens (1.6×) (conjuncts, vowel signs)
Japanese: ~24 tokens (2.2×)
Chinese: ~28 tokens (2.5×) (1 token per character)
Thai: ~30 tokens (2.7×) (no spaces, complex clusters)
Why this hurts (real numbers)¶
Context window: 8K tokens
- English: ~6,000 words fit
- Chinese: ~2,500 "words" fit (characters) → 60% less context!
API cost: $1 per 1M tokens
- English doc: 10K tokens → $0.01
- Chinese doc (same meaning): 25K tokens → $0.025 (2.5×!)
Training: 1T token corpus
- English: 1T tokens ≈ 800B words
- Chinese: 1T tokens ≈ 400B characters → 2× less real content learned
💡 Key Insight: token inflation is not an efficiency nit — it directly shrinks context, doubles cost, and reduces the effective training data per language.
Why No-Space Languages Are Hard (Deep Dive)¶
Chinese / Japanese: no spaces¶
Chinese: 每个汉字基本就是一个语素 (every character ≈ a morpheme)
- word boundaries require SEGMENTATION ("北京欢迎你" = 北京/欢迎/你)
- good tokenizers learn common character bigrams as single tokens
- e.g. a well-trained Chinese tokenizer:
"北京" → 1 token (not 北 + 京)
Japanese: こんにちは世界
- mixed scripts: hiragana, katakana, kanji
- tokenizers often use a pre-tokenizer that splits kanji runs
from kana runs (SentencePiece has Japanese pre-tokenization built in)
Thai / Lao: no spaces AND complex clusters¶
Thai: สวัสดี (hello)
- no spaces between words
- consonants + vowel signs + tone marks combine into clusters
- naive character tokenization breaks the clusters → worse quality
Practice: tokenizers either treat clusters carefully or use
byte-level fallback for rare combinations.
Indic scripts (Devanagari, Tamil, Bengali...)¶
Hindi: नमस्ते (hello)
- characters combine: consonant + vowel sign (म + ् + ...)
- "matras" (vowel signs) attach to consonants
- normalization (NFKC) must not destroy the combining structure
The Toolbox: How Modern Tokenizers Cope¶
1. Bigger Vocabulary¶
English-only: 32–50K
Multilingual: 64–128K
Maximum coverage: 128–256K
Real models:
mT5: 250K vocab (100+ languages)
Qwen: 151K (Chinese + English + more)
LLaMA-3: 128K (30+ languages)
DeepSeek-V3: 128K (multilingual)
GPT-4: ~100K (speculated, multilingual)
2. character_coverage → 0.9999+¶
coverage=0.99 → ~1% of unique characters become [UNK] ❌
coverage=0.9999 → rare scripts included (larger vocab, no [UNK])
coverage=0.99999 → extremely rare characters (emoji, archaic)
Rule of thumb: multilingual ⇒ 0.9999+ (see [02 Sentencepiece](/02-llm-modeling/00-fundamentals/00-tokenization/02-sentencepiece/))
3. byte_fallback (the [UNK] eliminator)¶
byte_fallback=True (LLaMA, Mistral, Gemma):
- unknown characters encoded as raw UTF-8 bytes
- 256 byte tokens guaranteed in vocab → ANY input works
- unknown Chinese char → 3 byte tokens instead of [UNK]
- information preserved, at the cost of longer sequences
Byte-level BPE (GPT-2 style) achieves the same from the start:
- starts from 256 bytes → every script is representable
4. Language-Balanced Sampling (training data)¶
Problem: English dominates the web → English dominates the vocab
a Chinese word seen 1M times vs English word 1B times
→ Chinese gets starved of vocab slots
SentencePiece solution — input_sentence_size + unigram sampling
(or exponential language sampling used by LLaMA/mT5):
sample_count(lang) ∝ (corpus_share(lang))^α with α ≈ 0.3
α = 0.3 boosts low-resource languages:
corpus share 0.1% → effective ~4.4% of samples
This gives every language a fair chance at vocab allocation.
5. Normalization (NFKC)¶
Normalization rule:
- NFKC normalizes full-width/半角 to half-width, ligatures to
base chars, compatibility forms together
- "A" (full-width A) → "A" (both become ONE token)
- "fi" ligature → "fi"
- CJK: ensures visually-similar chars share tokens
SentencePiece: normalization_rule_name='nfkc' (default: 'nmt_nfkc')
⚠️ Don't apply NFKC to scripts where it destroys meaning
(e.g., some Indic combining marks).
6. Per-Script Pre-tokenization¶
Tokenizer libraries split input by script before BPE:
- Latin runs: "The quick"
- CJK runs: "你好世界" → segment into character/cJK-word pieces
- Kana runs: "こんにちは"
- Numbers: "12345"
This prevents "the quick你好" from being merged as one token.
(SentencePiece ships a Japanese pre-tokenizer; Hugging Face
tokenizers has 'Metaspace' and 'ByteLevel' pre-tokenizers.)
Measuring Multilingual Quality¶
1. Tokens per word / tokens per language:
report efficiency PER LANGUAGE, not just overall
(a 1.2 overall hides 3.0 for Chinese!)
2. [UNK] rate per language:
target < 0.01%; check each script separately
3. Coverage of common words:
do the top-1000 words of each language exist as SINGLE tokens?
4. Round-trip fidelity:
encode → decode must return identical text
(fails with broken byte fallback or bad normalization)
5. Benchmark parity:
same task in 10 languages — token count AND model quality
should not collapse for CJK/Indic scripts
def per_language_report(tokenizer, samples):
"""Report tokens-per-sample per language."""
for lang, texts in samples.items():
total_tokens = sum(len(tokenizer.encode(t)) for t in texts)
total_chars = sum(len(t) for t in texts)
print(f"{lang:12s} {total_tokens:6d} tokens "
f"{total_chars / max(total_tokens, 1):5.1f} chars/token")
Code: Training a Multilingual SentencePiece¶
import sentencepiece as spm
spm.SentencePieceTrainer.train(
input='mixed_languages.txt', # English + Chinese + Hindi + Arabic...
model_prefix='multilingual',
vocab_size=64000, # bigger than English-only
model_type='bpe',
character_coverage=0.9999, # include rare scripts
byte_fallback=True, # never emit [UNK]
normalization_rule_name='nfkc', # unify full-width/half-width
split_digits=True, # keep digits separate
allow_whitespace_only_pieces=True,
# Language balancing (input format: "prefix\ttext"):
# add <s> and </s> style prefixes, or sample with α≈0.3
# spm supports 'input_sentence_size' + random sampling
)
# Quick multilingual sanity check
sp = spm.SentencePieceProcessor(model_file='multilingual.model')
for text in ["Hello world", "你好世界", "नमस्ते दुनिया", "مرحبا بالعالم"]:
pieces = sp.encode_as_pieces(text)
print(f"{text:20s} → {len(pieces)} tokens: {pieces}")
Key Takeaways¶
🌍 Multilingual ≠ bigger vocab: it's spaces, scripts, coverage, AND balanced training data
💰 Token inflation is cost: 2–4× tokens in CJK = 2–4× API cost, 60% less context
🧰 Five tools: larger vocab, character_coverage, byte_fallback, language sampling, NFKC
📊 Measure per language: overall metrics hide 3.0-tokens/word Chinese
🚫 [UNK] is not acceptable: byte-level/byte_fallback makes it nearly extinct
⚖️ Sample languages fairly: α ≈ 0.3 exponential sampling rescues low-resource languages
Related Notes¶
- 00 Tokenization Fundamentals — core concepts this builds on
- 01 Bpe (Byte Pair Encoding) — byte-level BPE solves coverage from the start
- 02 Sentencepiece — the standard multilingual tool (▁, byte_fallback, coverage)
- 06 Wordpiece — BERT-style tokenizer (less multilingual-friendly)
- 03 Token Efficiency & Compression — the cost angle in numbers
- 04 Tokenization Best Practices — choosing a tokenizer for your use case