Tokenization
Computers cannot read words. Before any NLP model or LLM sees your text, it must be cut into small pieces called tokens and turned into numbers. How we cut decides what the model can understand, how much text fits in its memory, and how much you pay. Let us learn tokenization properly.
In this session
- Word, sentence and character tokenization
- Why modern models use subword tokens
- BPE from scratch in 30 lines, plus WordPiece, SentencePiece and Unigram
- Training real tokenizers and comparing them
- Token counting and the cost of an LLM app
01Word, sentence and character tokens
Tokenization means splitting text into units. The simplest choices are sentences, words or single characters:
import re
from nltk.tokenize import sent_tokenize, word_tokenize # nltk.download("punkt_tab") once
text = "Dr. Rao paid ₹1,250 via UPI. It didn't go through! Can I retry?"
print("Sentences:", sent_tokenize(text))
print("split(): ", text.split())
print("NLTK words:", word_tokenize(text))
print("Characters:", list("UPI paid")[:8])Notice the problems with plain split(): “UPI.” keeps the full stop and “didn’t” stays one piece. NLTK handles punctuation and splits “didn’t” into did + n’t. It also knows “Dr.” does not end a sentence.
| Level | Example for “unhappiness” | Problem |
|---|---|---|
| Word | unhappiness | Huge vocabulary; any new or misspelt word becomes unknown |
| Character | u n h a p p i n e s s | Very long sequences; single letters carry little meaning |
| Subword | un happi ness | The balance used by almost all modern models |
02Subword tokenization
Subword tokenizers keep common words whole and split rare words into meaningful pieces. So “repayment” can be understood as re + payment even if the model never saw that exact word.
Like LEGO bricks. With a few thousand bricks you can build any word, even one nobody has seen before. Common shapes come pre-built (“payment”), rare ones are built from smaller bricks (“pre” + “pay” + “ments”).
One word, four tokens. Highlighted pieces come from the single word “repayments”.
| Algorithm | How it builds the vocabulary | Used by |
|---|---|---|
| BPE (Byte-Pair Encoding) | Start from characters; repeatedly merge the most frequent pair | GPT-2, GPT-3/4, Llama 3, RoBERTa |
| WordPiece | Like BPE, but merges the pair that most increases the likelihood of the data; marks word continuations with ## | BERT, DistilBERT |
| Unigram | Start with a big vocabulary and remove the pieces that matter least | T5, ALBERT, XLNet (via SentencePiece) |
| SentencePiece | A library, not an algorithm: runs BPE or Unigram on raw text and treats spaces as a symbol ▁, so it works for any language | T5, Llama 1/2, Gemma |
03BPE from scratch
BPE is simple enough to write ourselves. Each word starts as characters. We count every neighbouring pair across the corpus, merge the most frequent pair into a new token, and repeat:
from collections import Counter
# word frequencies in a tiny corpus; each word starts as characters + end marker
corpus = {"low": 5, "lower": 2, "newest": 6, "widest": 3}
vocab = {tuple(w) + ("</w>",): f for w, f in corpus.items()}
def pair_counts(vocab):
pairs = Counter()
for word, f in vocab.items():
for a, b in zip(word, word[1:]):
pairs[(a, b)] += f
return pairs
def merge(vocab, pair):
out = {}
for word, f in vocab.items():
w, i = [], 0
while i < len(word):
if i < len(word) - 1 and (word[i], word[i + 1]) == pair:
w.append(word[i] + word[i + 1]); i += 2
else:
w.append(word[i]); i += 1
out[tuple(w)] = f
return out
for step in range(1, 6):
best, n = pair_counts(vocab).most_common(1)[0]
vocab = merge(vocab, best)
print(f"merge {step}: {best[0]} + {best[1]} -> {best[0] + best[1]} (seen {n} times)")
print("\nFinal pieces:")
for word in vocab: print(" ", " ".join(word))Real tokenizers do tens of thousands of merges on terabytes of text. Here we train BPE and WordPiece on a tiny banking corpus with the Hugging Face tokenizers library and test words:
# pip install tokenizers
from tokenizers import Tokenizer, models, trainers, pre_tokenizers
corpus = [
"UPI payments are instant and free for customers.",
"The payment failed, please retry the UPI payment.",
"Fixed deposit interest is paid every quarter.",
"Interest rates for senior citizens are higher.",
"Payments, repayments and prepayments are tracked daily.",
] * 20
def train(model, trainer):
tok = Tokenizer(model)
tok.pre_tokenizer = pre_tokenizers.Whitespace()
tok.train_from_iterator(corpus, trainer)
return tok
bpe = train(models.BPE(unk_token="[UNK]"), trainers.BpeTrainer(vocab_size=120, special_tokens=["[UNK]"]))
wp = train(models.WordPiece(unk_token="[UNK]"), trainers.WordPieceTrainer(vocab_size=120, special_tokens=["[UNK]"]))
for word in ["payments", "repayment", "interested", "UPI"]:
print(f"{word:11s} BPE: {bpe.encode(word).tokens} WordPiece: {wp.encode(word).tokens}")Both learned interest + ed for a word they had never seen as a whole. WordPiece marks the second piece with ##, meaning “continues the previous word”.
| Model | Vocabulary size |
|---|---|
| BERT-base (uncased) | 30,522 |
| GPT-2 | 50,257 |
| Llama 3 | about 128,000 |
04Token counting and cost
LLM providers charge per token, and every model has a context window limit counted in tokens. A useful rule of thumb for English: 1 token ≈ 4 characters ≈ ¾ of a word, so 100 tokens is about 75 words.
# Same greeting, different scripts: byte-level tokenizers start from UTF-8 bytes
for text in ["hello", "नमस्ते", "வணக்கம்"]:
print(f"{text:8s} characters={len(text):2d} utf8_bytes={len(text.encode('utf-8'))}")
# Cost estimate: tokens x price. Example rates only: check your provider's price page.
price_in, price_out = 0.15, 0.60 # US$ per 1 million tokens
calls_per_day, tokens_in, tokens_out = 10_000, 800, 200
daily = calls_per_day * (tokens_in * price_in + tokens_out * price_out) / 1_000_000
print(f"\n{calls_per_day:,} calls/day x ({tokens_in} in + {tokens_out} out) tokens")
print(f"= ${daily:.2f} per day, about ${daily * 30:.0f} per month")- To count exactly, use the model’s own tokenizer, e.g.
tiktokenfor OpenAI models orAutoTokenizerfor Hugging Face models. - Output tokens usually cost more than input tokens.
- Cut cost by shortening system prompts, sending fewer RAG chunks and capping
max_tokens.
05Summary
- Tokenization turns text into units (tokens) that become numbers.
- Subword tokenizers keep common words whole and split rare words into pieces.
- BPE merges frequent pairs; WordPiece uses
##pieces; Unigram prunes a big vocabulary; SentencePiece runs them on raw text. - English: about 4 characters per token; many other scripts need more tokens.
- LLM cost = tokens × price, so token counts drive your bill.
06Quick quiz
1. Why do modern models use subword tokens?
2. What does BPE merge at each step?
3. In WordPiece, what does “##ed” mean?
4. Roughly how many tokens is 400 characters of English?
5. Why can Hindi text cost more tokens than English?
07References
- Neural Machine Translation of Rare Words with Subword Units (BPE) (Sennrich et al., 2016)
- SentencePiece
- Hugging Face: Summary of the tokenizers
- OpenAI: What are tokens?