← Build a Tiny LLM Lesson 4 of 10

Let the model learn

The problem

Counting got me stuck. Is there another way to find my numbers?

Right now your model is stuck:

  • It sees only the last letter, so it writes words like β€œsathe”.
  • Looking further back makes the counting table explode: 3 letters need 38,416 boxes.
  • A smarter model (coming in Lesson 5) would need far fewer boxes, but its numbers can't be counted.

So the question is: can your model find good numbers without counting?

Without counting, where would your model even start?

Start random

Instead of counting, your model starts with random scores and fixes them. Row t:

after t␣.acdeghlmnost
Counts31000004000000
Random scores0.481.35βˆ’0.16βˆ’0.420.94βˆ’0.180.191.071.310.460.26βˆ’0.76βˆ’2.05βˆ’1.53

A score for every letter, even ones that never follow t: 14 rows Γ— 14 = 196 scores. Any number, even negative, so easy to nudge.

Counting

t β†’ h seen 4 times β†’ 4.

Only when a number means β€œhow many times”.

Learning (this lesson)

Guess, see how wrong, fix a little, repeat.

Works even when numbers can't be counted.

Learning repeats 4 steps:

  1. a)Predict the next character
  2. b)Measure how wrong
  3. c)Find which way to change each score
  4. d)Move each score a little

To predict, your model needs chances, not scores. How do scores become chances?

Scores into chances

Step a) Predict

To guess, your model needs chances that add up to 1, like the rows of the counting table.

These are your model's random scores for row t. Drag h up. Watch the others shrink.
You can drag 5 of the 14 scores. The other 9 letters keep their scores and are added together at the bottom.

Any scores in, chances out. They always add up to 1, and raising one score takes chance away from the others.

This is softmax.

Now your model can guess. But its scores are random, so its guesses are bad. How bad?

How wrong?

Step b) Measure

To get better, your model needs one number that says how bad its guesses are. The text has the right answers. Take its first pair, t β†’ h:

Your model guessed: h 13.3% (on the last page)
The text says: h comes next

It gave the right answer only 13.3%. How wrong is that?

Drag the chance for h. Try 90%, 50% and 1%.
chance for h13.3%
How wrong (the loss)
2.02
0 = perfectvery wrong

The less chance for the right answer, the bigger the loss. 100% gives 0.
The rule: loss = βˆ’log(chance)

Every pair in the text gets its own loss: 46 pairs, 24 different ones. A repeated pair has the same loss each time, and counts each time:

Try it on new numbers.

✏️ Practice 4.1

New numbers, same ideas.
Remember: loss = βˆ’log(chance) (the ln button on a calculator)
The loss of several pairs is the average of their losses.

Work each one out. Type the answer, press Check.
Pair A gives the right letter a chance of 1/2. Pair B gives it 1/4. How many times bigger is B's loss than A's?
Four pairs give the right letter chances 1, 1/2, 1/2 and 1/4. What is the loss for the four together?

The goal is to make the loss small. But which of the 196 scores should change, and which way?

Which way?

Steps c) Find directions and d) Move

After t, our text has h 4 times, space 3 times and a full stop once. Start with just one pair, t β†’ h. Your model gave h only 0.133 (13.3%). (The next page uses all 8.)

The rule: gradient = chance βˆ’ goal
For this pair, h's goal is 1 and every other letter's is 0.
For h: 0.133 βˆ’ 1 = βˆ’0.867

Then move:
new score = old score βˆ’ gradient
For h: 1.069 βˆ’ (βˆ’0.867) = 1.936, up.
A negative gradient moves a score up, a positive one down, and the further from 0, the bigger the move.

Every other letter goes down, even space and full stop, because this pair says h. On the next page, their own pairs push them up.

Press β€œMove a little” a few times. Watch h's chance grow and the loss shrink.
Each press: every score moves by its gradient β†’ new chances β†’ new gradients. Watch h's gradient shrink: βˆ’0.867, βˆ’0.711, βˆ’0.526… The closer to 100%, the smaller the move.

In code, loss.backward() finds all 196 gradients at once.

Your turn: fix the scores yourself, using these directions.

You be the model

Now fix row t using all 8 pairs starting with t:

Each pair's goal comes from the text: 1 for its right letter, 0 for the rest. For h, at chance 0.133:

  • 4 pairs t β†’ h: 0.133 βˆ’ 1 = βˆ’0.867 (up)
  • 4 other pairs: 0.133 βˆ’ 0 = +0.133 (down)

Averaged: (4 Γ— βˆ’0.867 + 4 Γ— 0.133) Γ· 8 = βˆ’0.367
= 0.133 βˆ’ 4/8
4/8 is the share of pairs wanting h: counting's answer, 0.5. The model never sees it; the pairs give it. (Space 3/8, full stop 1/8.)

The loss is the average of the 8 pairs' losses.

Push scores up or down. Get the loss under 1.2.
The green dashed line is counting's answer, only to check against. l and d never come after t, so counting gives them 0 (the same for the 9 letters not shown). Each ↑ or ↓ moves that score by 0.5.
Loss for the 8 t-pairs–
Moves0
Best possible: 0.97, when every chance matches counting's answer.
Or let your model do it: each step moves every score by its gradient (new = old βˆ’ gradient), using all 8 pairs.

Press β–Ά Play: below counting's answer the gradient is negative: score up. Above it, down. At it, 0: it stops on the green line.

In code, PyTorch's loss.backward() finds the same gradients. You'll see it on the code page.

Try it on new numbers.

✏️ Practice 4.2

Row x has 4 pairs: x β†’ p, x β†’ p, x β†’ p and x β†’ q. Only p and q exist, and their scores start equal, so each chance is 1/2.
Remember: gradient = chance βˆ’ goal (goal: 1 for the right letter, 0 for the others)
A row's gradient is the average of its pairs' gradients: add them up, divide by the number of pairs.

Work each one out. Type the answer, press Check.
For a pair x β†’ p, p's goal is 1. What is p's gradient for that pair?
For the pair x β†’ q, p's goal is 0. What is p's gradient for that pair?
Average the four pairs' gradients for p. What do you get?
At what chance for p does its average gradient become 0?
New problem: 3 letters a, b, c, all with score 0, so each chance is 1/3. The right answer is b. What is b's gradient?

That was one row (14 scores) and 8 pairs. Next: all 14 rows, 196 scores and all 46 pairs at once.

Let it learn

So far: one row. Now your model trains all 14 rows at once. One step:

a) Predict: every pair (all 46) gets its chances.
b) Measure: the loss averages the 46 pairs' losses.
c) Find directions: every pair finds its own gradient (chance βˆ’ goal). Each row averages its own pairs' gradients: one gradient per score.
d) Move every score by that average.
Then it repeats, 300 times.

One new thing: how far to move. The average is multiplied by the learning rate:

  • new score = old score βˆ’ learning rate Γ— gradient
  • h in row t starts with gradient βˆ’0.367 (the average we found for row t)
  • learning rate 1: h goes up 0.367
  • learning rate 10: h goes up 3.67
Pick a learning rate. Press Train.
Each point is the loss for all 46 pairs after that many steps. Lower is better. It drops fast, then flattens onto the green line: the best possible, (the loss of the counting chances).
The chances after t. Green dashed marks = what counting gives.
Steps0
Loss (all 46 pairs)3.0865

Too small: it creeps. Too big: it jumps past where it should settle, and back.

after tspace.h
Counting0.3750.1250.500
Learning (300 steps)0.3720.1220.497

The same answer, and your model never saw counting's answer. Learning works.

Try it on new numbers.

✏️ Practice 4.3

Remember: new score = old score βˆ’ learning rate Γ— gradient
gradient = chance βˆ’ goal
chance = escore Γ· (sum of all escore)

Work each one out. Type the answer, press Check.
Row x from the last practice: p's gradient is βˆ’0.25. With learning rate 2, by how much does p's score go up?
Three letters a, b, c start with score 0. The right answer is b, and the gradients are a +1/3, b βˆ’2/3, c +1/3. With learning rate 3, what is b's new score?
Same problem: what is a's new score? (c's is the same.)
The new scores are a βˆ’1, b 2, c βˆ’1. What is b's new chance? (eΒ² = 7.39, e⁻¹ = 0.37)

The real code averages a little differently.

Averaging all 46

So far, row t averaged over its own 8 pairs. The usual way averages every row over all 46 pairs, the same average the loss already uses.

Row t has 8 of the 46 pairs, so its gradient shrinks by 8/46: h's βˆ’0.367 becomes βˆ’0.367 Γ— 8/46 = βˆ’0.064.

Smaller gradients, so steps are smaller. The fix is a bigger learning rate: the code uses 10. Try both ways.

Pick how to average and the learning rate. Press Train.
First step, gradient of h in row t: –
The chances after t. Green dashed marks = what counting gives.
Steps0
Loss (all 46 pairs)3.0865

Averaging over all 46 with learning rate 10 gets there, the same place. Learning rate 1 with it is too slow.

Try it on new numbers.

✏️ Practice 4.4

Row x again: 4 pairs, with 3 followed by p. But now the text has 20 pairs in all, and the average is over all 20.
Remember: gradient = sum of the pairs' gradients Γ· all 20 pairs
new score = old score βˆ’ learning rate Γ— gradient

Work each one out. Type the answer, press Check.
What is p's gradient now? (Its 4 pairs are 4 of the 20.)
To move p's score by the same 0.5 as before, what learning rate is needed?

Now the code makes sense.

The code

Guess what each print shows. Then press β–Ά Run.

import torch
import torch.nn.functional as F

text = "the cat sat on the mat. the dog sat on the log."
unique_chars = sorted(set(text))
vocab_size = len(unique_chars)
char_to_number = {}
number_to_char = {}
for number, char in enumerate(unique_chars):
    char_to_number[char] = number
    number_to_char[number] = char

# Pairs: this character, and the next one
current_chars = []
next_chars = []
for i in range(len(text) - 1):
    current_chars.append(char_to_number[text[i]])
    next_chars.append(char_to_number[text[i + 1]])
current_chars = torch.tensor(current_chars)
next_chars = torch.tensor(next_chars)

# The model: random scores. requires_grad=True lets PyTorch find gradients.
torch.manual_seed(42)
weights = torch.randn((vocab_size, vocab_size), requires_grad=True)

learning_rate = 10
for step in range(300):
    scores = weights[current_chars]                # a) predict
    loss = F.cross_entropy(scores, next_chars)     # b) measure
    weights.grad = None
    loss.backward()                                # c) find directions
    weights.data = weights.data - learning_rate * weights.grad   # d) move
    if step % 50 == 0:
        print("Step", step, "loss:", round(loss.item(), 3))

# What did it learn after 't'?
probabilities = F.softmax(weights, dim=1)
t_row = probabilities[char_to_number['t']]
for char in [' ', '.', 'h']:
    print(repr(char), round(t_row[char_to_number[char]].item(), 3))

Colab runs Python with PyTorch in your browser for free. You just need a Google account.

Quick check: did it all make sense?

Lesson 4 test

8 questions, one for each idea in this lesson. Answer them all right to pass. A wrong answer tells you why, and which page to look at again.

1. Starting pointBefore any learning, which characters does your model have a score for after t?
2. PredictYou raise h's score. Space's score stays the same. What happens to space's chance?
3. CompareFor the pair t β†’ h, model A gives h 80% and model B gives h 20%. Which one has the bigger loss?
4. The truthWhere does the loss get the right answer from?
5. Read a gradientFor the pair t β†’ h, your model gives β€œ.” a chance of 0.6. What is its gradient, and what happens to its score?
6. Almost rightYour model already gives the right answer 95%. How much will that answer's score move?
7. Work it outA score is 2.0, its gradient is +0.3, and the learning rate is 10. What is the new score?
8. Spot the mistakeA friend says: β€œLearning is better than counting because it's faster.” What's wrong?

What your model still can't do.

Still one letter

Your model learned its numbers by itself. But it still looks at only the last letter.

So it still writes words like β€œsathe”. To fix that, it must look further back, without the table exploding.

I learned the same chances as counting, all by myself! Now I can find numbers without counting. Can I finally look further back?

This leaves a question
Your model can learn now, but it still sees only 1 letter. How can it look further back without the table exploding?
Lesson 5 answers it.