A language model like GPT or Claude does exactly one thing, over and over: guess the next word. This page builds that guess from scratch. Every step comes with the reason it exists and a small calculation you can check by hand.
Then follow every number through one block, see how the weights are learned, and why generating is fast.
One guess
Given a sequence of tokens (words, pieces of words, punctuation), the model outputs a probability for every possible next token. Given “the cat sat”, it might give on 40%, down 25%, and spread the rest over thousands of other tokens. Pick one, append it, and ask again. That is how it writes a paragraph.
So to understand a transformer, you only need to understand one guess: a sequence goes in, probabilities come out. Almost all the arithmetic inside is the dot product: multiply two lists pairwise and add them up.
A matrix multiply is a grid of dot products. A large model does billions of them for every token.
Words become vectors
Arithmetic needs numbers. A tokenizer gives each token an ID, but an ID is only an address: token 100 is not “twice” token 50. So each ID is used only to look up a row in a learned table, the embedding. Here are two invented rows of four numbers:
cat→(0.9, 0.3, -0.2, 0.6)dog→(0.8, 0.4, -0.1, 0.5)
The table starts random. Training nudges it, and tokens used in similar contexts drift toward similar vectors, because similar vectors lead to similar predictions.
Similar means pointing the same way
A vector is an arrow. The dot product measures how much two arrows agree:
Here is the arrow’s length and the angle between the arrows. For the rows above,
a large positive number, since they point almost the same way. Dividing by both lengths leaves , the cosine similarity, which compares direction only.
Where each word sits
Attention (next section) treats the rows as a set: nothing in it looks at a row’s index. Shuffle the input and every word gets the same new vector, just in a different order. Without help, “dog bites man” and “man bites dog” would look identical to it.
The fix is to put position into the rows. The simplest version learns one vector per position and adds it:
Most current models use RoPE instead: they rotate each query and key by an angle proportional to its position, so that when position meets position only the offset survives.
Attention
Now “the cat sat” is a grid with one row per token. But each row describes only its own token: the row for sat doesn’t know a cat did the sitting. To predict what comes next, a position needs information from other positions, and which positions matter depends on the text. So the mixing weights must be computed from the rows themselves.
A soft lookup
Think of a dictionary. You bring a query, compare it with each entry’s key, and get back that entry’s value. Attention softens this: score every key, turn the scores into weights that sum to 1, and return the weighted average of the values. Each row makes all three with learned matrices:
- Query : what this position is looking for.
- Key : what it offers to be matched on.
- Value : what it hands over when chosen.
Keys and values are separate because why a word is relevant differs from what it should contribute.
The recipe
For the position being updated, over every position it may read:
is the length of a query or key vector. The middle line is softmax: exponentiating makes every score positive, and dividing by the total makes the weights sum to 1.
Worked example. cat (position 2) may read positions 1 and 2. Say its scores are and the values are , .
- Exponentiate: and .
- Divide by the total, 4: .
- Mix the values:
Queries and keys decided how much each position counts. Values decided what got mixed.
Why divide by √d
If the entries of and are random with variance 1, then adds up such products, so its variance is and its typical size is . With , scores of ±8 are common. A gap of 8 makes one weight times another, so softmax picks a single position and the others get almost no gradient to learn from. Dividing by brings scores back to about ±1.
Why mask the future
During training the model predicts every position’s next token at once. If position could read position , it would just copy the answer. So scores for are set to , and gives them zero weight. This is the causal mask. Generation never has future tokens anyway, so training now matches use.
All positions at once, and many heads
Stack the rows into matrices (one row per position). Then holds every query–key score at once:
is the mask: 0 where , where .
One set of weights can follow one relationship. A word may need several at once: its subject, the previous word, a matching bracket. Multi-head attention runs narrower heads side by side, each with its own , then joins their outputs with one more matrix . Training decides what each head tracks.
The feed-forward network
Attention moved information between positions. The feed-forward network (FFN) then processes each row on its own, with the same weights for every position:
- Widen. makes the row about 4× wider. Each new entry is a dot product with one column of , so it measures how well the row matches that pattern.
- Bend. , called ReLU, zeroes the negatives, so each feature switches on only when its pattern is present.
- Narrow. maps back to the original width. Each active feature adds its own row of , scaled by how strongly it fired.
Why the bend matters: without it, two linear maps in a row are just one linear map,
so the extra width would buy nothing.
Assemble the block
A block wraps each of the two layers the same way:
Normalize the input, compute an update, and add it back. The output has the same shape as the input, so blocks stack: GPT-3 has 96 of them, each with its own weights.
Why add instead of replace
- Each layer only has to learn an edit. A layer with nothing to add can output about 0, and passes through untouched.
- Learning signals get a direct route back. The slope of is . The 1 lets the training signal reach the first block even when every is small; through 96 layers of plain , those small factors would multiply toward zero.
Why normalize
The running sum drifts in scale from block to block, and softmax and ReLU behave very differently at different scales. Layer normalization rescales each row before a layer reads it:
and are the row’s mean and standard deviation; and are a learned scale and shift. For , with and :
- Mean: , so the deviations are .
- Variance: , so .
- Normalized: .
(Real implementations add a tiny under the square root to avoid dividing by zero.)
Count the parameters
With model width , one block holds:
- Attention: , each , so in total.
- FFN: plus , so .
That is per block. Check it on GPT-3: 96 blocks, :
Add the embedding table ( billion) and you get the published 175 billion. Two-thirds of it sits in the FFNs.
Follow the numbers
Here is one complete block, computed. “the cat sat” enters as three rows of four numbers; the weights are invented but fixed, so every number can be checked. Select a token to follow its row, then select any number to see exactly which inputs and weights produced it.
From a vector to a word
After the last block, only the last position’s vector, call it , is used for the guess. It is the only row that has read the whole input. Each vocabulary token has an output vector , and its score is a dot product:
A token whose vector points along scores high. Stacking all the as columns of one matrix computes every score in one multiply, . These raw scores are called logits. (Some models reuse the embedding table as ; that is weight tying.)
Scores become probabilities
The same softmax as in attention:
Worked example. For scores , the exponentials are , which sum to 6, so the probabilities are .
Only gaps matter: , so adding a constant to every score changes nothing.
Temperature
Divide every score by a temperature first. That divides every gap by . For two tokens whose scores differ by 1, the probability ratio becomes :
| Temperature | 0.5 | 1 | 2 |
|---|---|---|---|
| Ratio | 7.39 | 2.72 | 1.65 |
Low stretches the gaps, so the top token takes over. High shrinks them, so the choice gets more adventurous. As you always pick the top token, which is greedy decoding.
Pick, append, repeat
With 100,000 tokens, the long tail of unlikely ones adds up, and one bad draw can derail the text. So samplers cut the tail first, then rescale what’s left to sum to 1:
- Top-k: keep the most likely tokens.
- Top-p: keep the most likely tokens until their total reaches , so the count adapts to how sure the model is.
Append the chosen token and run the model again. That loop is the whole of text generation.
How it learns
Every weight above was given. Training finds them from raw text and one question: how much probability did the model give to the token that actually came next?
Every position is a free example
the cat sat on holds three predictions: the → cat, the cat → sat, the cat sat → on. Thanks to the causal mask, one pass scores all of them at once, and the text is its own answer key. No labels are needed.
The loss: −log of the right answer’s probability
A good model makes real text likely. The probability of a whole text is the product of its per-token probabilities, . Logs turn that product into a sum (and keep thousands of small numbers from underflowing), and a minus sign makes lower better:
Here is the probability the model gave the correct token at position . This is the cross-entropy loss.
| Probability of the right token | 1.0 | 0.5 | 0.1 | 0.01 |
|---|---|---|---|---|
| Loss | 0 | 0.693 | 2.303 | 4.605 |
Confidently wrong is punished hardest: as , the loss grows without limit.
The correction is “predicted minus actual”
Let be 1 for the correct token and 0 elsewhere. The slope of the loss with respect to the scores is
Worked example. The model says , and the right answer is token 2, so :
Training steps against the slope. The right token’s score goes up by its shortfall (0.7), and each wrong token’s score goes down by the probability it took. Backpropagation (the chain rule, applied block by block) carries this signal back to every weight , which moves a small step against its own slope:
Repeat over trillions of tokens and the weights end up encoding grammar, facts, and everything else that helps predict the next word.
Generating fast
Each new token needs keys and values for every earlier position. But those never change: position 3’s key depends only on tokens 1–3. So models store them in a KV cache and compute only the new token’s row. A step then costs query–key pairs instead of recomputing all .
The whole thing in code
A complete small GPT in PyTorch
Every piece above, in about 40 lines: embeddings plus positions, pre-norm blocks with causal multi-head attention and a ReLU FFN, a final norm, and the unembedding.
import torch
import torch.nn as nn
import torch.nn.functional as F
class Block(nn.Module):
def __init__(self, d, heads):
super().__init__()
self.norm1, self.norm2 = nn.LayerNorm(d), nn.LayerNorm(d)
self.attn = nn.MultiheadAttention(d, heads, batch_first=True)
self.ffn = nn.Sequential(nn.Linear(d, 4 * d), nn.ReLU(), nn.Linear(4 * d, d))
def forward(self, x): # x: (batch, n, d)
n = x.size(1)
future = torch.triu(torch.ones(n, n, dtype=torch.bool, device=x.device), 1)
h = self.norm1(x)
x = x + self.attn(h, h, h, attn_mask=future, need_weights=False)[0]
x = x + self.ffn(self.norm2(x))
return x
class TinyGPT(nn.Module):
def __init__(self, vocab, d=64, heads=4, layers=4, max_len=256):
super().__init__()
self.tok = nn.Embedding(vocab, d)
self.pos = nn.Embedding(max_len, d)
self.blocks = nn.ModuleList(Block(d, heads) for _ in range(layers))
self.norm = nn.LayerNorm(d)
self.unembed = nn.Linear(d, vocab, bias=False)
def forward(self, ids): # ids: (batch, n)
positions = torch.arange(ids.size(1), device=ids.device)
x = self.tok(ids) + self.pos(positions)
for block in self.blocks:
x = block(x)
return self.unembed(self.norm(x)) # logits: (batch, n, vocab)
def train_step(model, optimizer, tokens): # tokens: (batch, n + 1)
logits = model(tokens[:, :-1]) # predict from each prefix
loss = F.cross_entropy(logits.flatten(0, 1), tokens[:, 1:].flatten())
optimizer.zero_grad()
loss.backward()
optimizer.step()
return loss.item()
@torch.no_grad()
def generate(model, ids, steps, temperature=1.0):
for _ in range(steps):
context = ids[:, -model.pos.num_embeddings:] # at most max_len tokens
logits = model(context)[:, -1] / temperature # last position only
next_id = torch.multinomial(F.softmax(logits, dim=-1), 1)
ids = torch.cat([ids, next_id], dim=1) # append and repeat
return ids
Further reading
- Vaswani et al. “Attention Is All You Need.” 2017. The original transformer.
- Jurafsky and Martin. Speech and Language Processing, Chapter 8: “Transformers.” A careful textbook treatment.
- Radford et al. “Language Models are Unsupervised Multitask Learners” (GPT-2). 2019. The pre-norm decoder used here.
- Brown et al. “Language Models are Few-Shot Learners” (GPT-3). 2020. The 175-billion-parameter shape used in the parameter count.
- Su et al. “RoFormer: Rotary Position Embedding.” 2021. RoPE.
- Elhage et al. “A Mathematical Framework for Transformer Circuits.” 2021. Reading the model as a residual stream.