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 | β£ | . | a | c | d | e | g | h | l | m | n | o | s | t |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Counts | 3 | 1 | 0 | 0 | 0 | 0 | 0 | 4 | 0 | 0 | 0 | 0 | 0 | 0 |
| Random scores | 0.48 | 1.35 | β0.16 | β0.42 | 0.94 | β0.18 | 0.19 | 1.07 | 1.31 | 0.46 | 0.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.
t β h seen 4 times β 4.
Only when a number means βhow many timesβ.
Guess, see how wrong, fix a little, repeat.
Works even when numbers can't be counted.
Yes. Counting reads the text once. Learning goes through all 46 pairs, fixes every score a little, and repeats that 300 times.
For this small table, counting is quicker. But the smarter model in Lesson 5 has numbers that can't be counted, and learning still works there.
Here we can check it: learning ends with the same chances as counting.
Learning repeats 4 steps:
- a)Predict the next character
- b)Measure how wrong
- c)Find which way to change each score
- 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.
t. Drag h up. Watch the others shrink.Each score becomes escore: always positive, and a bigger score gives a much bigger result. Then each one is divided by the total, so all the chances add up to 1.
e is a fixed number, about 2.718.
Any scores in, chances out. They always add up to 1, and raising one score takes chance away from the others.
This is softmax.
Because scores can be negative. Row t starts with s at β2.05, and all 14 scores add up to about 0.95. Dividing gives β2.05 Γ· 0.95 = β216%. A chance can't be negative.
escore is always positive: eβ2.05 = 0.13. And a higher score still gives a bigger number, so the order stays the same.
With e, a small nudge to the score grows escore by about that much, as a percentage. Nudge a score up by 0.01:
| 21 = 2.000 | β 21.01 = 2.014 | 0.69% bigger |
| e1 = 2.718 | β e1.01 = 2.746 | 1.00% bigger |
| 101 = 10.00 | β 101.01 = 10.23 | 2.30% bigger |
With e, nudge and growth are the same number. With 2 or 10, every nudge gets an extra 0.69 or 2.30.
Learning keeps asking βif I nudge this score, how much does the chance change?β. With e there's no extra number, so the answer is the simple rule two pages on: move each score by βchance β goalβ.
2 or 10 would still work: your model would just learn bigger or smaller scores, and the chances would come out the same. e only keeps the math cleanest.
Max means: pick the biggest score, give it everything. Soft means: not all-or-nothing.
Row t's three highest scores: β.β 1.35, l 1.31, h 1.07.
| . | l | h | |
| max | 100% | 0% | 0% |
| softmax | 17.7% | 16.9% | 13.3% |
Max gives l nothing, though its score is almost the same as β.β. Softmax gives the biggest score the most, but shares the rest by how close each one is.
Make the gaps bigger (all scores Γ 10) and softmax hardens towards max: β.β 58.9%, l 36.7%, h 3.4%. So softmax is a softened max.
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:
h 13.3% (on the last page)The text says:
h comes nextIt gave the right answer only 13.3%. How wrong is that?
The less chance for the right answer, the bigger the loss. 100% gives 0.
The rule: loss = βlog(chance)
You don't need the math. βlog just turns a chance into a βhow wrongβ number:
100% β 0 (perfect), 90% β 0.11, 50% β 0.69, 13.3% β 2.02, 1% β 4.61.
The smaller the chance, the bigger the number.
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.
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.
It's the gap between what your model gives and what it should give. Below the goal, the gap is negative, so the score goes up. Above it, positive, so it goes down. At the goal it's 0: nothing to fix.
If your model already gave h 1 (100%): 1 β 1 = 0, so the score doesn't move. It gave only 0.133: 0.133 β 1 = β0.867, far from the goal, so it moves up a lot.
h's chance grow and the loss shrink.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.
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.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.
x β p, p's goal is 1. What is p's gradient for that pair?x β q, p's goal is 0. What is p's gradient for that pair?p. What do you get?p does its average gradient become 0?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
tstarts with gradient β0.367 (the average we found for rowt) - learning rate 1: h goes up 0.367
- learning rate 10: h goes up 3.67
Nothing special: it's enough steps for the loss to settle. With learning rate 1:
step 0: 3.086 Β· 50: 0.743 Β· 100: 0.721 Β· 300: 0.708 Β· 1,000: 0.703
best possible: 0.701
After 300, more steps barely help. A smaller learning rate needs more steps.
t. Green dashed marks = what counting gives.Too small: it creeps. Too big: it jumps past where it should settle, and back.
| after t | space | . | h |
|---|---|---|---|
| Counting | 0.375 | 0.125 | 0.500 |
| Learning (300 steps) | 0.372 | 0.122 | 0.497 |
The same answer, and your model never saw counting's answer. Learning works.
Here counting is quicker: one pass through the text, against 300 for learning.
But learning never asks βhow many times did this pair appear?β. It only asks βshould this score go up or down?β. So it still works where numbers can't be counted, coming in Lesson 5.
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)
x from the last practice: p's gradient is β0.25. With learning rate 2, by how much does p's score go up?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.
t: βt. Green dashed marks = what counting gives.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
p's gradient now? (Its 4 pairs are 4 of the 20.)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))
$ python lesson4.py
Step 0 loss: 3.086
Step 50 loss: 0.746
Step 100 loss: 0.722
Step 150 loss: 0.714
Step 200 loss: 0.711
Step 250 loss: 0.709
' ' 0.374
'.' 0.124
'h' 0.499weights[current_chars]: copies the row of each of the 46 characters.F.cross_entropy: softmax, pick the right answer's chance, βlog, average. The average is built into the loss, soloss.backward()gives averaged gradients.weights.grad = None: clears old gradients. PyTorch adds new ones on top.loss.backward(): fillsweights.gradwith a gradient for every score.weights.data - learning_rate * weights.grad: the minus flips the sign, so negative goes up.
Why learning rate 10? cross_entropy averages over all 46 pairs so gradients are smaller and the learning rate is bigger.
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.
t?t β h, model A gives h 80% and model B gives h 20%. Which one has the bigger loss?t β h, your model gives β.β a chance of 0.6. What is its gradient, and what happens to its score?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?