Train next-token logits
Training a language model uses nothing you have not seen. The model gives a row of scores for each input token. Cross-entropy measures how little probability those scores put on the token that came next. Backpropagation and an optimizer step then raise the score of the right token. To keep the moving parts few, the model here is the simplest one possible: a table that predicts the next token from the current token alone.
Learn it
Words to know
- bigram model
- The simplest language model: it predicts the next token from the current token alone.
- cross-entropy
- The usual loss for choosing between classes: minus the log of the probability the model gave to the right answer.
- logit
- A raw score a model gives to one choice, before the scores are turned into probabilities.
- optimizer
- The part of the training code that changes the parameters, using their gradients.
How it works
- The model is one embedding table of shape
[vocab, vocab], starting at zero. Rowiholds the scores for what follows tokeni. - Looking up the input IDs gives one row of scores per input token.
torch.nn.functional.cross_entropy(logits, targets)applies softmax and-login one call and averages over the tokens.- Run the four-call training step from chapter 4
stepstimes. Then compute the loss once more, without updating, and return it.
Worked example
Input ids = [0] targets = [1] vocab = 2 rate = 1 steps = 1 Output 0.313262
| When | Scores in row 0 | Probabilities | Loss |
|---|---|---|---|
| Before training | [0, 0] | [0.5, 0.5] | 0.6931 |
| After 1 step | [-0.5, 0.5] | [0.2689, 0.7311] | 0.3133 |
The gradient of row 0 was [0.5, -0.5]. One step lowered the score of the wrong token and raised the right one, and the loss fell by more than half.
Why it matters
tiny_gpt.py trains with the same four calls, with the GPT in place of the table. It also limits the size of the gradients and uses the AdamW optimizer, not SGD. A real model should beat a bigram table's loss.
Your turn
Using PyTorch, train a bigram table to predict each target token from its input token, and report the loss afterwards.
ids- the input token IDs; at least one
targets- the token that follows each input, in the same order
vocab- the number of different tokens
rate- the learning rate for SGD
steps- how many training steps to run; 0 or more
- Returns
- The mean cross-entropy over the pairs, measured after the last update, as a Python float. The table is
torch.nn.Embedding(vocab, vocab)withfloat64weights set to zero.
This lesson needs PyTorch, which does not run in the browser. The app gives you the exercise as a file with its tests, to run on your own computer.
Example 1
Input ids = [0] targets = [1] vocab = 2 rate = 1 steps = 0 Output 0.693147
With no training all scores are 0. Two tokens get probability 0.5 each, and the loss is -log(0.5), about 0.693.
Example 2
Input ids = [0] targets = [1] vocab = 2 rate = 1 steps = 1 Output 0.313262
One step moves the scores to [-0.5, 0.5]. The right token now has probability 0.73 and the loss is about 0.313.
Example 3
Input ids = [0] targets = [1] vocab = 2 rate = 1 steps = 2 Output 0.194609
A second step pushes the scores further apart, and the loss falls to about 0.195.
Example 4
Input ids = [0, 1] targets = [1, 0] vocab = 2 rate = 0.5 steps = 1 Output 0.575939
Two pairs, one per row of the table. The mean halves each row's gradient and the rate 0.5 halves the step, so each row moves to [-0.125, 0.125] or its mirror, and the loss is about 0.576.
Hints
Each hint gives away a little more. Open one only when you need it.
Hint 1: Where to start
The model is one
torch.nn.Embedding. Set its weights to zero, then write the same loop as in the training-loop lesson.Hint 2: The key step
model(x)gives the scores, andtorch.nn.functional.cross_entropy(model(x), y)gives the loss. After the loop, compute that loss one more time and return its.item().Hint 3: The outline
Create
model = torch.nn.Embedding(vocab, vocab, dtype=torch.float64)and zero it withtorch.nn.init.zeros_(model.weight). Makexandytensors andtorch.optim.SGD(model.parameters(), lr=rate). Loop:zero_grad, loss,backward,step. Return the loss computed after the loop.
Solution
From here on the page gives the method away. If you are still solving, go back to the hints.
Step by step
- Row i holds the scores for what follows token i. An embedding table is used as the whole model. Zero weights make every next token equally likely at the start.
- The usual four calls.
cross_entropytakes raw scores, not probabilities. It does the softmax itself, in a numerically stable way. - Measure again, now that the last update has been applied. The
lossinside the loop was computed before that step's update. The task asks for the loss after it.
The same numbered steps are the comments in the solution.
Python
import torch
def solve(ids, targets, vocab, rate, steps):
F = torch.nn.functional
# 1. Row i holds the scores for what follows token i
model = torch.nn.Embedding(vocab, vocab, dtype=torch.float64)
torch.nn.init.zeros_(model.weight)
x = torch.tensor(ids)
y = torch.tensor(targets)
optimizer = torch.optim.SGD(model.parameters(), lr=rate)
for _ in range(steps):
# 2. The usual four calls
optimizer.zero_grad()
loss = F.cross_entropy(model(x), y)
loss.backward()
optimizer.step()
# 3. Measure again, now that the last update has been applied
return F.cross_entropy(model(x), y).item()Watch out
- Do not apply softmax before
cross_entropy. The function expects raw scores, and softmax twice gives a wrong loss. - Returning the
lossfrom inside the loop reports the value from before the last update, and fails whenstepsis 0 because the variable does not exist.
Check yourself
What does a bigram model use to predict the next token?
Only the current token. It has one row of scores for each token and ignores everything earlier.
Why is the loss computed again after the loop?
The loss inside the loop is from before that step's update. A fresh calculation shows the effect of the last step.