Project: Transformer from Scratch (tiny GPT)¶
The translation project's attention was target-to-source. The Transformer
uses self-attention -- every position attends to every earlier position in
the same sequence -- which is what powers modern language models. This project
trains a miniature GPT on real Shakespeare text, character by character, using
only torchlight primitives (there is no Attention module; you build it).
The task and the data¶
Tiny Shakespeare is ~1.1M characters of public-domain plays. A character-level language model learns P(c_{t+1}\mid c_1,\dots,c_t), the probability of the next character given what came before. Its vocabulary is just the 65 distinct characters in the file, so every token is one character.
Training window: a BLOCK of 64 characters; the model must predict the
character following each position. The validation slice is the last 10% of
the text (deterministic positional split reused by post.py).
Model from first principles¶
1. Input embeddings: characters + where they are¶
Each character id v and each position t get their own learned vector from two embedding matrices (this is a huge win -- attention itself is permutation-equivariant, so it needs an explicit position signal):
2. Causal self-attention¶
Every position computes a query, a key and a value from its embedding via learned projections:
"Self" means keys/values come from other positions of the same sequence. The relevance of token j to token t is the scaled dot product; a causal mask M sends positions j>t (the future) to -\infty so softmax gives them weight zero -- the model never peeks ahead:
Dividing by \sqrt{d_h} keeps the softmax from saturating as d_h grows
(the inputs to softmax don't explode). This is done for n_head=4 heads in
parallel on d_h=16-dimensional slices, then the heads are concatenated and
mixed with W_O -- a parameter-efficient way to let different heads specialise
(e.g. one head tracks "the next char is a vowel", another tracks "has an o").
In code, heads come from a single Linear(n_embd, 3*n_embd) split with
chunk and reshaped via view -> permute.
3. LayerNorm and the residual road¶
Training many stacked layers is only stable if gradients flow by shortcut. Every block uses pre-norm layers with residual connections:
LayerNorm normalises each row to zero mean / unit variance, then re-scales: with \mu,\sigma per-position-row,
(shareable \gamma,\beta per feature). The MLP is a two-layer ReLU network
ReLU(x W_1) W_2 expanding to 4× width. The residual keeps gradients alive --
that is what makes 100s of layers trainable in real LLMs.
4. The head and the loss¶
The final Linear maps each position's representation to logits over the 65
characters. Training objective is next-character cross-entropy:
where a shifted target makes position t answer for c_{t+1} and the final
column is masked (y was rolled left by one; mask[:, -1] = 0) because there
is no "next char" after the window. Reported alongside the loss is the
perplexity \exp(\mathcal{L}): the model's average number of characters it
is unsure between.
Training¶
Adam, lr 3\times10^{-3}, batch 64 windows, 1200 gradient steps
(TORCHLIGHT_ITERS to override). A one-iteration baseline on real data:
step 1/1200 ... val 4.362 perplexity 78.4
-- near the ~65 you'd get from random guessing, and easing downward as
training climbs to 1200 steps. train.py saves out/gpt_model.npz,
meta.json (the character table) and history.json.
Generation: turning a distribution into text¶
Inference recycles the training forward pass one window at a time. From the logits of the last position, sample the next id (temperature softmax):
Low temperature repeats the char-rnn's most confident words; high temperature
wanders. Greedy \arg\max is just T\to0. post.py generates a sample at
T=0.8 and one at T=1.2, so the temperature readout is visible.
Evaluation (post.py)¶
projects/transformers-from-scratch/post.py reloads the checkpoint and reports
on the held-out validation slice:
- val cross-entropy loss,
- perplexity e^{L},
- top-1 character accuracy -- the fraction of positions where the greedy argmax character matches the true next character,
- fresh generated text at two temperatures,
- a loss-curve PNG from
history.json.
Run it yourself¶
python projects/transformers-from-scratch/train.py # trains + saves out/
python projects/transformers-from-scratch/post.py # perplexity + generation + plot
# quick checks: TORCHLIGHT_ITERS=300
What this project teaches¶
- self-attention as a weighted lookup built from Q,K,V -- the core of every modern LM,
- causal masking so nothing reads the future,
- why scaling by 1/\sqrt{d_h} matters,
- the shape gymnastics of multi-head attention (
view/permute/chunk), - LayerNorm + residuals as the stability recipe,
- temperature sampling as the "creative knob" of generative models.
The three projects now cover three architectures on real data: pooling MLP (sentiment), GRU + attention (translation), and GPT (generation) -- the arc from bags-of-words to language models, all running on torchlight's own autograd and backends (see Tutorial 5 — Backends).