GenAI Course · Module 2 · Session 4

Tokenization

⏱ 14 min read🎯 Beginner friendly🖼 1 diagram🧪 2 live demos🐍 Python code

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

Text“UPI payment failed”TokensUPI · payment · failedToken IDse.g. 4782 · 8323 · 4523Vectorsone per tokenModelBERT, GPT…
Every NLP model starts the same way: text → tokens → numbers. (Token IDs are examples; each tokenizer has its own.)

Tokenization means splitting text into units. The simplest choices are sentences, words or single characters:

tok_basics.py
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])
Sentences: ['Dr. Rao paid ₹1,250 via UPI.', "It didn't go through!", 'Can I retry?'] split(): ['Dr.', 'Rao', 'paid', '₹1,250', 'via', 'UPI.', 'It', "didn't", 'go', 'through!', 'Can', 'I', 'retry?'] NLTK words: ['Dr.', 'Rao', 'paid', '₹1,250', 'via', 'UPI', '.', 'It', 'did', "n't", 'go', 'through', '!', 'Can', 'I', 'retry', '?'] Characters: ['U', 'P', 'I', ' ', 'p', 'a', 'i', 'd']

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.

LevelExample for “unhappiness”Problem
WordunhappinessHuge vocabulary; any new or misspelt word becomes unknown
Characteru n h a p p i n e s sVery long sequences; single letters carry little meaning
Subwordun happi nessThe 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”).

repayments are instant

One word, four tokens. Highlighted pieces come from the single word “repayments”.

AlgorithmHow it builds the vocabularyUsed by
BPE (Byte-Pair Encoding)Start from characters; repeatedly merge the most frequent pairGPT-2, GPT-3/4, Llama 3, RoBERTa
WordPieceLike BPE, but merges the pair that most increases the likelihood of the data; marks word continuations with ##BERT, DistilBERT
UnigramStart with a big vocabulary and remove the pieces that matter leastT5, ALBERT, XLNet (via SentencePiece)
SentencePieceA library, not an algorithm: runs BPE or Unigram on raw text and treats spaces as a symbol ▁, so it works for any languageT5, 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:

bpe_scratch.py
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))
merge 1: e + s -> es (seen 9 times) merge 2: es + t -> est (seen 9 times) merge 3: est + </w> -> est</w> (seen 9 times) merge 4: l + o -> lo (seen 7 times) merge 5: lo + w -> low (seen 7 times) Final pieces: low </w> low e r </w> n e w est</w> w i d est</w>
Demo · Watch BPE merge

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:

train_tokenizers.py
# 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}")
payments BPE: ['payments'] WordPiece: ['payments'] repayment BPE: ['re', 'payment'] WordPiece: ['re', '##p', '##ayment'] interested BPE: ['interest', 'ed'] WordPiece: ['interest', '##ed'] UPI BPE: ['UPI'] WordPiece: ['UPI']

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”.

ModelVocabulary size
BERT-base (uncased)30,522
GPT-250,257
Llama 3about 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.

Other languages often cost more. Many tokenizers start from UTF-8 bytes, and Indian scripts need 3 bytes per character. The same greeting needs many more bytes, and usually more tokens, in Hindi or Tamil than in English.
bytes_cost.py
# 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")
hello characters= 5 utf8_bytes=5 नमस्ते characters= 6 utf8_bytes=18 வணக்கம் characters= 7 utf8_bytes=21 10,000 calls/day x (800 in + 200 out) tokens = $2.40 per day, about $72 per month
Demo · How cost grows with traffic

05Summary

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

Created by Ajeetkumar