XP: 0
⚙️ Part 1 · Module 4

Datasets & Tokenizers

The training loop needs fuel: the x and y it learns from. This module is about data — where it comes from, how you load it with Hugging Face, how you batch it, and the crucial text step: tokenization, turning words into numbers a model can actually eat.
The fuel

What a dataset really is

A dataset is just a big table of examples: inputs (x) paired with correct answers (y). The model studies these pairs to learn the pattern. That's it — no magic.

#text (x)label (y)
0"this movie was fantastic"positive
1"total waste of time"negative
2"I loved every minute"positive
3"boring and predictable"negative
Loading real data — Hugging Face 🤗

You rarely make datasets by hand. Hugging Face hosts thousands, free, loaded in one line. It's the "GitHub of datasets and models."

python · huggingface
from datasets import load_dataset

# download a real sentiment dataset in one line
data = load_dataset("imdb")
print(data)                       # train / test splits
print(data["train"][0])          # {'text': '...', 'label': 1}
🎮 Game 1

Explore a dataset

Click through examples like you would after loading one. Real datasets are just rows of x → y. (Simulated preview of what load_dataset gives you.)

IMDB movie reviews (preview)
Step through rows.
The crucial text step

Tokenizers — turning text into numbers

A model can't read letters — only numbers. A tokenizer does two jobs: (1) split text into chunks called tokens, and (2) map each token to a number (its ID) from a fixed vocabulary. (You met this conceptually in fundamentals Module 12 — here it's the real tool.)

Why sub-word tokens (BPE)?

Modern tokenizers don't split on whole words — they use sub-words (a method called BPE). Common words stay whole ("the"), but rare/long words break into pieces: "tokenization""token" + "ization". This handles any word (even typos or new words) with a small vocabulary. Notice: 1 token ≠ 1 word — roughly 1 token ≈ ¾ of a word in English.

python · huggingface tokenizer
from transformers import AutoTokenizer

tok = AutoTokenizer.from_pretrained("gpt2")

text = "Today is a mesmerizing day with lot's of memories"
ids = tok(text)["input_ids"]
tokens = tok.convert_ids_to_tokens(ids)

print(ids)
print(tokens)
print(list(zip(ids, tokens)))  # pair every ID with its token
print(tok.decode(ids))
Reading GPT-2's token pieces

Ġ is GPT-2's visible marker for a space before the token. For example, Ġis means " is". The marker disappears when the tokens are decoded back into normal text.

8888 maps to the single vocabulary token Today:

[(8888, 'Today'), (318, 'Ġis'), (257, 'Ġa'), (46814, 'Ġmesmer'), (2890, 'izing'), (1110, 'Ġday'), (351, 'Ġwith'), (1256, 'Ġlot'), (338, "'s"), (286, 'Ġof'), (9846, 'Ġmemories')]

Token IDs are vocabulary lookup numbers, not semantic categories. Therefore, 8888 does not belong to a special “Today words” group. Its relevant unit is the exact token Today; nearby ID numbers do not necessarily represent related words.

🎮 Game 2

Tokenize your own text (live)

Type anything and watch it split into tokens with IDs — the way a real tokenizer would. Try a long/rare word and see it break into sub-word pieces.

Text → tokens → IDs
Type below (simulated BPE-style splitting).
🎮 Game 3

Tokens ≠ words — the counter

This matters for real reasons: API costs and context limits are counted in tokens, not words. Slide the word count and see the token estimate (~1.33 tokens per word).

Word → token estimator
Rough rule: 100 words ≈ 133 tokens.
words: 50
≈ tokens
67
fits in GPT-2 (1024)?
✓ yes
🎮 Game 4

Split the data (train / val / test)

From Module 3 you know why to split. Here's how — slide the split and see the proportions. The standard is roughly 80 / 10 / 10.

1000 examples → three sets
Drag the train share.
train share: 80%
python
# split a dataset 80/20 (then split the 20 into val/test)
split = data["train"].train_test_split(test_size=0.2)
train = split["train"]        # 80%
temp  = split["test"].train_test_split(test_size=0.5)
val, test = temp["train"], temp["test"]   # 10% / 10%
🎮 Game 5

Batching — feed data in chunks

You don't feed one example at a time (too slow) or all at once (too much memory). You feed batches — small groups. The batch size is how many examples per training step. PyTorch's DataLoader handles this.

12 examples, batch size = ?
See how the data groups into batches.
batch size: 4
python
from torch.utils.data import DataLoader

loader = DataLoader(train, batch_size=32, shuffle=True)

for batch in loader:      # the loop from Module 3 iterates batches
    pred = model(batch["x"])
    ...                    # forward, loss, backward, step
🎮 Game 6

Order the data pipeline

Put the steps of a real data pipeline in the right order. Click them in sequence — from raw text to a batch the model can train on.

Build the pipeline in order
Click the steps 1 → 5 in the correct order.
Visual bridge

Follow 12 examples through the whole pipeline

Here is the full journey from raw rows to model updates. The numbers stay visible so you can see what changes at each stage.

Load → tokenize → split → batch → train
Example: 12 text rows, an 8/2/2 split, and batch size 2.
1
Load raw data
0123 4567 891011
12 rows containing raw text and labels
2
Tokenize
"Today"[8888] "is sunny"[318, 27737]
Every row's text becomes token IDs. The model's embedding layer later turns IDs into vectors.
3
Split
TRAIN: 0–7 (8) VALIDATION: 8–9 (2) TEST: 10–11 (2)
The three sets are now separate. Validation and test examples are not training examples.
4
Create batches
B0 [0,1]B1 [2,3] B2 [4,5]B3 [6,7]
The 8 training examples become 4 training batches. Validation and test get their own loaders.
5
Train
prediction loss backward optimizer.step()
One epoch processes B0, B1, B2, and B3: four training steps and four parameter updates.
What happens after training?

Validation: check performance during development without updating weights. Final test: evaluate the finished model once on unseen data.

Visual bridge

From token IDs to 768-number vectors

The tokenizer stops at integer IDs. GPT-2's embedding layer uses each ID as a row number in its learned lookup table and returns one 768-number vector per token.

Text → token IDs → embedding vectors
GPT-2 keeps the vector width at 768, even when the number of input tokens changes.
1 · Input text
Today is sunny
2 · Tokenizer output
Today8888
Ġis318
Ġsunny27737
3 · GPT-2 embedding lookup
8888[0.12, -0.44, 0.08, ... 765 more]
318[0.71, 0.03, -0.52, ... 765 more]
27737[-0.20, 0.61, 0.37, ... 765 more]
Output shape: [1, 3, 768] = 1 sentence × 3 tokens × 768 numbers per token
Two different jobs

Tokenizer: text → IDs. Embedding layer: IDs → vectors. The vector values shown in the illustration are shortened examples; run the code below to see GPT-2's real values.

python · real GPT-2 embeddings
from transformers import AutoTokenizer, AutoModel

tokenizer = AutoTokenizer.from_pretrained("gpt2")
model = AutoModel.from_pretrained("gpt2")

text = "Today is sunny"
inputs = tokenizer(text, return_tensors="pt")
ids = inputs["input_ids"]
tokens = tokenizer.convert_ids_to_tokens(ids[0])

# Each ID selects one row from GPT-2's embedding table.
embedding_layer = model.get_input_embeddings()
vectors = embedding_layer(ids)

print("Tokens:", tokens)
print("IDs:", ids)
print("Vector tensor shape:", vectors.shape)
# torch.Size([1, 3, 768])

for token, token_id, vector in zip(tokens, ids[0], vectors[0]):
    print(f"\n{token!r} (ID {token_id.item()})")
    print("First 10 of 768 numbers:", vector[:10])

# ID 8888 directly retrieves the same vector as "Today".
today_vector = embedding_layer.weight[8888]
print("\nToday vector shape:", today_vector.shape)
# torch.Size([768])
🎯 Check your understanding

Quick challenge

1. What does a tokenizer do?
2. Why do modern tokenizers use sub-words (BPE)?
3. Roughly how many tokens is 100 English words?
4. What is a batch?
5. What is Hugging Face mainly used for here?
Takeaway

What you learned

A dataset is rows of x → y; load real ones in one line with Hugging Face (load_dataset). Text must be tokenized: split into sub-word tokens (BPE) and mapped to numeric IDs — remember 1 token ≈ ¾ word, and that's what API costs/context limits count. Split data into train/val/test (~80/10/10), and feed it in batches via a DataLoader. The pipeline: load → tokenize → split → batch → train.

Next up → Module 5: makemore — a name generator. Now you have all the pieces (tensors, autograd, training loop, data). We combine them to build a real character-level model from scratch that invents new names — the concepts from fundamentals Modules 5–8, in working PyTorch.