06

Lesson 06 · Part I · Why learning works at all

The bill for nudging

10,000 knobs: how many forward passes for one gradient?

~30 min · 1 lab · builds on 05 Bending space

This lesson builds on lessons 01–05. New here? Start at lesson 01, or carry on and load the finished pieces with one click.

2(3x−1)+52(3x - 1) + 5 is a straight line. What is its slope?

2(3x−1)+5=6x+32(3x - 1) + 5 = 6x + 3, 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?

A bigger learning rate, to get over the hump faster
A bigger step overshoots further, so the loss would grow faster still.
A smaller learning rate: each step overshoots the valley by more than the last
A loss that grows faster at every step is the signature of steps that jump past the bottom.
More steps: it hasn't settled yet
Each step makes the next one bigger, so more steps only take it further away.

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 a second: computers do a million of anything quickly
A million simple additions, yes. This is a million forward passes of the whole model, 1 ms each.
About 17 minutes: a million and one passes of 1 ms each
1,000,001 · 1 ms is about 1,000 seconds, and that buys one step; training takes thousands of steps.
About 2 ms: one pass where you stand, one with every knob nudged
Nudge all the knobs together and the loss changes once: you learn how their combined effect moved, not which knob did what.

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, ∇L\nabla L, 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 n+1n + 1 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: ∇L=[ ∂L∂w1, ∂L∂w2, … ]\nabla L = [\,\frac{\partial L}{\partial w_1},\ \frac{\partial L}{\partial w_2},\ \ldots\,] For L=3w1−2w2+5L = 3 w_1 - 2 w_2 + 5, nudging w1w_1 from 1 to 1.01 at w2w_2 = 1 moves L from 6 to 6.03, a slope of 3. Nudging w2w_2 from 1 to 1.01 at w1w_1 = 1 moves L from 6 to 5.98, a slope of −2. So ∇L=[3,−2]\nabla L = [3, -2] everywhere.

L=4w1−w2+2w3+7L = 4 w_1 - w_2 + 2 w_3 + 7. What is ∂L∂w3\frac{\partial L}{\partial w_3}?

2: raising w3w_3 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.

  1. Measure the loss where you stand once, and keep it. For each parameter, what changes between that measurement and the nudged one?
  2. 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.
  3. Nearly the code: base = f(params); a loop that appends (f(moved) - base) / h; then return grads, len(params) + 1.
  1. The gradient of p0^2 + p1^2 + p2^2 at [1, -2, 3] is about [2, -4, 6].
  2. Three knobs cost four calls to f: one at params, one per nudged knob.
  3. The reported count equals the number of times f really ran.
  4. Nudging happens on copies: the list passed in is unchanged afterwards.
  5. With h = 0.5, f = p0^2 at [1] gives (2.25 - 1) / 0.5 = 2.5 in two calls.
  6. 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.

seriespointsfirstlastlowesthighest
forward passes for one gradient62100121001

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 stays the same: one set of nudges covers any size
Each knob needs its own nudge, so the passes grow with the knobs.
It quadruples: every knob has to be nudged together with every other knob
Each partial slope nudges one knob and holds the rest still, so pairs never come into it.
It roughly doubles: n+1n + 1 becomes 2n+12n + 1
One pass where you stand, plus one per knob.

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

06

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

Continue

Report a problem with this lesson