Lesson 06 · Part I · Why learning works at all
The bill for nudging
10,000 knobs: how many forward passes for one gradient?
This lesson builds on lessons 01–05. New here? Start at lesson 01, or carry on and load the finished pieces with one click.
is a straight line. What is its slope?
, so the slope is 2 · 3 = 6: the slopes multiply.
Gradient descent on some loss prints 4, 9, 20 and 45 over four steps. What is the most likely fix?
Shrink the learning rate. A loss that grows at every step is overshooting, and it will not settle by itself.
Nine knobs, then a million
Your two_layer network solves XOR once gradient_descent has tuned its nine knobs. It found each knob's slope by nudging that knob and measuring the loss again.
Nine knobs is nothing. A small image classifier has about a million, and a language model has billions. How much does one set of slopes cost?
To measure every partial slope by nudging, you measure the loss once where you stand, then once more with each knob nudged in turn. A model has 10,000 knobs. How many forward passes (evaluations of the loss) is that?
10,001: one where you stand, and one per knob. The bill grows in step with the number of knobs.
One forward pass of a model with 1,000,000 knobs takes 1 millisecond. How long does one nudged gradient take?
About 17 minutes for a single step of training. Nudging is exact enough and far too slow.
Count it. numerical_derivative measures the loss twice for each knob, once where you stand and once nudged. Wrap the loss in a counter and ask for every slope, one knob at a time.
Count the loss calls that one set of slopes costs, for 1, 10, 100 and 1,000 knobs.
from ember.basics import numerical_derivative
for n in [1, 10, 100, 1000]:
calls = 0
def loss(knobs):
global calls
calls += 1
return sum((k - 1.0) ** 2 for k in knobs)
knobs = [0.0] * n
for i in range(n):
numerical_derivative(lambda v: loss(knobs[:i] + [v] + knobs[i + 1:]), knobs[i])
print(f"{n:>5} knobs: {calls:>5} loss calls for one set of slopes")
1 knobs: 2 loss calls for one set of slopes 10 knobs: 20 loss calls for one set of slopes 100 knobs: 200 loss calls for one set of slopes 1000 knobs: 2000 loss calls for one set of slopes
For 1,000 knobs that is 2,000 loss calls for one set of slopes: two per knob, because each nudge also re-measures where you stand.
Every slope at once
The list of every partial slope of the loss has a name, the gradient, and a new symbol, , read "grad L". Measured by nudging, a method called finite differences, the slopes can share one measurement, the loss where you stand, which brings the bill for n knobs to passes. The gradient points uphill, the way the loss grows fastest, so gradient descent steps against it.
The gradient is the list of all the partial slopes: For , nudging from 1 to 1.01 at = 1 moves L from 6 to 6.03, a slope of 3. Nudging from 1 to 1.01 at = 1 moves L from 6 to 5.98, a slope of −2. So everywhere.
. What is ?
2: raising by 1 raises L by 2, whatever the other knobs are. The whole gradient is [4, −1, 2].
What: write numerical_gradient(f, params, h): measure f once at params, then once per parameter with only that one nudged by h; return the list of slopes and the number of calls to f.
Why: without it you can't see what a gradient costs, or check a faster one. It is the honest, slow way to get a gradient. Lessons 07 to 10 build backpropagation to replace it, and lesson 11 turns it into the checker that proves backpropagation right.
Your ember can measure a whole gradient, and it knows the bill.
- Measure the loss where you stand once, and keep it. For each parameter, what changes between that measurement and the nudged one?
- base = f(params). For each i, copy params, add h to position i, measure, and the slope is (nudged - base) / h. The count is one call for base plus one per parameter.
- Nearly the code: base = f(params); a loop that appends (f(moved) - base) / h; then return grads, len(params) + 1.
- The gradient of p0^2 + p1^2 + p2^2 at [1, -2, 3] is about [2, -4, 6].
- Three knobs cost four calls to f: one at params, one per nudged knob.
- The reported count equals the number of times f really ran.
- Nudging happens on copies: the list passed in is unchanged afterwards.
- With h = 0.5, f = p0^2 at [1] gives (2.25 - 1) / 0.5 = 2.5 in two calls.
- For sin(p0) at 0.3 the gradient is about cos(0.3).
Your numerical_gradient: forward passes against knobs, and the bill for bigger models.
import hx
from ember.basics import numerical_gradient
sizes, passes = [], []
for n in [1, 10, 100, 250, 500, 1000]:
grads, calls = numerical_gradient(lambda p: sum((v - 1.0) ** 2 for v in p), [0.0] * n)
sizes.append(n)
passes.append(calls)
print(f"{n:>5} knobs: {calls:>5} forward passes")
hx.plot("line", {"series": [{"name": "forward passes for one gradient", "x": sizes, "y": passes}]})
for n, unit, per in [(10_000, "seconds", 1), (1_000_000, "minutes", 60), (7_000_000_000, "days", 86_400)]:
print(f"{n:>13,} knobs at 1 ms a pass: about {(n + 1) / 1000 / per:,.0f} {unit} per step")
1 knobs: 2 forward passes
10 knobs: 11 forward passes
100 knobs: 101 forward passes
250 knobs: 251 forward passes
500 knobs: 501 forward passes
1000 knobs: 1001 forward passes
10,000 knobs at 1 ms a pass: about 10 seconds per step
1,000,000 knobs at 1 ms a pass: about 17 minutes per step
7,000,000,000 knobs at 1 ms a pass: about 81 days per step
Forward passes for one gradient, against the number of knobs: a straight line, one more pass per knob.
| series | points | first | last | lowest | highest |
|---|---|---|---|---|---|
| forward passes for one gradient | 6 | 2 | 1001 | 2 | 1001 |
1,001 forward passes for 1,000 knobs. At 1 ms a pass, a million knobs cost about 17 minutes per step, and seven billion about 81 days.
You double the number of knobs in a model. What happens to the number of forward passes one nudged gradient costs?
It roughly doubles, and every pass also gets slower as the model grows.
A model has 500 knobs and one forward pass takes 2 ms. How many seconds does one nudged gradient take?
501 passes · 2 ms = 1,002 ms = 1.002 seconds, for one step.
Where this goes
Backpropagation, in lessons 07 to 10, gets every partial slope from one forward pass and one backward pass, whatever the number of knobs. It starts with a number that remembers where it came from.
When you finish this lesson
Your ember can now: measure every slope of a loss at once, and count the cost
Part I gave your ember all of this:
- turn two knobs, w and b, into a prediction
- score any line with one number, and search a grid for the best one
- fit a line by walking downhill on its loss
- train a neuron that decides AND and OR
- compute XOR with two layers and a kink
- measure every slope of a loss at once, and count the cost
Back up your ember (one small zip).
Next A number that remembers · What must a number record so we can ask where it came from? · ~35 min