Before a language model can process text, that text must be converted into a sequence of integer IDs. This week we explore tokenisation: how text is segmented into tokens, why the choice of tokeniser matters, and how to inspect and use tokenisers in practice.
from importlib.metadata import version
import os
import requests
if not os.path.exists("the-verdict.txt"):
url = (
"https://raw.githubusercontent.com/rasbt/"
"LLMs-from-scratch/main/ch02/01_main-chapter-code/"
"the-verdict.txt"
)
file_path = "the-verdict.txt"
response = requests.get(url, timeout=30)
response.raise_for_status()
with open(file_path, "wb") as f:
f.write(response.content)
with open("the-verdict.txt", "r", encoding="utf-8") as f:
raw_text = f.read()
print("Total number of character:", len(raw_text))
print(raw_text[:99])
import re
text = "Hello, world. This, is a test."
result = re.split(r'(\s)', text)
print(result)
result = re.split(r'([,.]|\s)', text)
print(result)
# Strip whitespace from each item and then filter out any empty strings.
result = [item for item in result if item.strip()]
print(result)
text = "Hello, world. Is this-- a test?"
result = re.split(r'([,.:;?_!"()\']|--|\s)', text)
result = [item.strip() for item in result if item.strip()]
print(result)
preprocessed = re.split(r'([,.:;?_!"()\']|--|\s)', raw_text)
preprocessed = [item.strip() for item in preprocessed if item.strip()]
print(preprocessed[:30])
print(len(preprocessed))
Neural networks require numerical inputs. Text is a sequence of characters. The simplest approach would be character-level modelling — but:
Word-level tokenisation is the other extreme:
Subword tokenisation is the practical solution used by all modern LLMs:
BPE is the most widely used subword algorithm (GPT-2, GPT-3, GPT-4, LLaMA).
Training text: low low low lower lower newest newest
Initial tokens: l o w </w> l o w e r </w> n e w e s t </w>
Most frequent pair: l o → merge to lo
Next: lo w → low
…and so on until lowest might become low est </w> etc.
WordPiece (BERT, DistilBERT):
## prefix to denote continuation subwords (e.g. play ##ing).SentencePiece (LLaMA, T5, Mistral):
▁ (U+2581) to denote word-initial subwords.Tiktoken (OpenAI):
Every tokeniser defines special tokens with reserved IDs:
| Token | Purpose |
|---|---|
<|endoftext|> |
Separator between documents (GPT family) |
[CLS] |
Classification token (BERT) |
[SEP] |
Separator between sentences (BERT) |
[PAD] |
Padding to uniform length in batches |
<s> / </s> |
Start / end of sequence (LLaMA, Mistral) |
<unk> |
Unknown token (rare in modern tokenisers) |
| Model | Tokeniser | Vocabulary Size |
|---|---|---|
| GPT-2 | BPE | 50,257 |
| GPT-3 / 4 | Tiktoken (cl100k) | 100,277 |
| BERT | WordPiece | 30,522 |
| LLaMA-2 | SentencePiece | 32,000 |
| LLaMA-3 | Tiktoken | 128,256 |
Larger vocabulary → fewer tokens per sentence → shorter sequences → faster training and inference, but larger embedding matrix.
12345 may become ['12', '345'] or ['1', '2', '3', '4', '5'] — arithmetic is hard.Hello and hello are often different tokens." dog" and "dog" are often different tokens in BPE.API usage is billed per token. Context windows are limited in tokens. Use tiktoken to count tokens before sending to a model:
import tiktoken
enc = tiktoken.encoding_for_model("gpt-4o")
tokens = enc.encode("The quick brown fox jumps over the lazy dog.")
print(len(tokens)) # 9
See practicals/week02_practical.py:
tokenizers library.tiktoken_demo.ipynbaababab. What are the first two merges?