Reinforcement learning — only hit or miss
The coach is still at the target and may now say one word. Hit, or miss. Not how far and not which side — and that turns out to be a different problem rather than a smaller one.
Being told the answer changed how much the coach was allowed to say, and found that a vaguer label costs more examples.
This lesson takes that as far as it goes. The coach may say one word. Hit, or miss. Not how far, not which side.
“Miss” says nothing about which way to go
Line the three up and the break is not where people expect.
- “Eleven left and seven high.” Says where to go. Move that far, that way.
- “Left. High.” Says nothing about how far, but still says which way. The robot can step in a direction it knows is right.
- “Miss.” Says nothing about direction. There is no way to correct, because nothing said what correcting would mean.
The first two can be corrected. The third can only be searched. That is why this is a separate branch of the subject rather than a harder setting of the same one.
So it tries a different setting and keeps whichever hit more
Unable to correct, the robot does the only thing left. The loop is three steps.
- Fire ten arrows where it aims now. Count the hits. That is the only number it will ever get.
- Fire ten at a nearby guess. Deliberately somewhere else, which is the part that costs.
- Keep whichever hit more, and go again. No direction was used anywhere, because none was available.
Every arrow is spent, including the ones fired knowingly in the wrong place. That is what searching costs, and nothing that cannot correct avoids paying it.
One word costs 43 times as many arrows
The third bar is many times longer than the other two, and all three end in the same place.
| What the coach says | Arrows to get there |
|---|---|
| the exact miss | 12 |
| which side | 21 |
| hit, or miss | 517 |
the exact miss
- Arrows to get there
- 12
which side
- Arrows to get there
- 21
hit, or miss
- Arrows to get there
- 517
Nothing about the bow changed and nothing about the destination changed. The whole difference is how much each answer was allowed to contain.
Re-measure both settings every round
The three numbers above are measured here rather than quoted. The standard is how close the robot's aim has come to the setting that cancels its habit, within 2.5 centimetres.
Measuring the current aim once and remembering the score sounds like a saving. It is a trap. A lucky ten out of ten can then never be beaten, and the robot freezes for good. That happened while this lesson was being written.
A learner that spends half its arrows shooting badly on purpose also cannot be judged on its recent arrows. Judge it on what it now knows, and the comparison is fair.
A ring of 20 cm gets 60 hits out of 60. A ring of 2 cm gets 7.
One word only teaches you something if it sometimes changes. How often it changes is decided by something nobody thinks of as a setting: how big the ring is.
Nothing about the robot differs between those three rings. Only what somebody decided to call a hit.
Both ends of the ring fail the same way
The curve has a bottom, and it climbs on both sides of it. A ring that is too small is expensive. A ring that is too large never finishes.
| Size of the gold ring | Arrows to get there |
|---|---|
| 6 cm | 471 |
| small | 1156 |
| very large | never, inside 4000 arrows |
6 cm
- Arrows to get there
- 471
small
- Arrows to get there
- 1156
very large
- Arrows to get there
- never, inside 4000 arrows
A word that never changes is not feedback. Miss everything and every answer is the same. Hit everything and every answer is the same. The robot is being told nothing, twice over.
A measure everyone passes and a measure nobody passes are equally useless
This turns up wherever people try to improve something.
- A target hit every single quarter. Either the work is remarkable or the target is not a target.
- A test suite that has never gone red. It cannot tell you a change was safe, because it has never told you anything else.
- A satisfaction score pinned at the top. Nothing you could do next quarter would move it, so it cannot help you choose.
All three feel like success and are the same failure as the ring that is too big. Nothing in them varies with what anybody did, so nothing in them can be learned from.
Reinforcement learning is learning from consequences rather than answers
Nobody says what the right action was. Something happens afterwards that was better or worse, and everything has to be worked backwards from that.
That sounds like a bad deal until you notice how many problems only come this way. Nobody can tell a game-playing program the correct move, only who won. Nobody can say what a system should have replied, only whether the person came back.
The methods are a course of their own and not this one. How to spread one delayed word back over everything that led to it, and how to balance trying new things against using what already works.
Three lessons, one robot
The bow never changed. The habit never changed. The wobble never changed. Only what somebody was willing to say after each arrow.
| What it is told | Which way to go? | What it learns | Arrows |
|---|---|---|---|
| the exact miss | yes, and how far | where to aim | 12 |
| which side | yes | where to aim | 21 |
| nothing at all | — | how its arrows differ from each other | — |
| hit, or miss | no | where to aim | 517 |
the exact miss
- Which way to go?
- yes, and how far
- What it learns
- where to aim
- Arrows
- 12
which side
- Which way to go?
- yes
- What it learns
- where to aim
- Arrows
- 21
nothing at all
- Which way to go?
- —
- What it learns
- how its arrows differ from each other
- Arrows
- —
hit, or miss
- Which way to go?
- no
- What it learns
- where to aim
- Arrows
- 517
Read the last column as the price of the second. The third row is the one to look at twice. Told nothing, the robot does not learn a worse version of where to aim. It learns something else entirely.
content/notebooks/what-learning-is/only-hit-or-miss.ipynb
Change how far each guess strays, or how many arrows judge a setting. Worth trying: cut the batch from ten arrows to two and watch it start believing noise.
Show the code4 cells
import random
BIAS = (-11.0, 7.0)
WOBBLE = 4.0
STEP = 0.15 # told a number
CAT_STEP = 0.6 # told a category
GOLD = 10.0 # inside this ring, the coach says 'hit'
NEAR = 2.5 # 'has learned it': aim this close to perfect
BUDGET = 4000 # arrows after which we stop waiting
IDEAL = (-BIAS[0], -BIAS[1])
def residual(aim):
return ((aim[0] - IDEAL[0]) ** 2 + (aim[1] - IDEAL[1]) ** 2) ** 0.5
def sign(v):
return 1.0 if v > 0 else (-1.0 if v < 0 else 0.0)
def fire(aim, rng):
return (aim[0] + BIAS[0] + rng.gauss(0, WOBBLE),
aim[1] + BIAS[1] + rng.gauss(0, WOBBLE))
def told(seed, kind, budget=400):
"""Told a number, or told a category. Both can correct; only the step differs."""
rng = random.Random(seed)
aim = [0.0, 0.0]
for n in range(1, budget + 1):
shot = fire(aim, rng)
if kind == 'number':
aim[0] -= STEP * shot[0]
aim[1] -= STEP * shot[1]
else:
aim[0] -= CAT_STEP * sign(shot[0])
aim[1] -= CAT_STEP * sign(shot[1])
if residual(aim) < NEAR:
return n
return budget
record('near_cm', NEAR)
record('gold_cm', int(GOLD))BATCH = 10 # arrows spent judging one setting
SIGMA = 4.0 # how far a guess strays from where it is now
def hit_or_miss(seed, gold=GOLD, budget=BUDGET):
rng = random.Random(seed)
aim = [0.0, 0.0]
spent = 0
def hit_rate(a):
"""Fire BATCH arrows at `a`. All the coach ever says is hit or miss."""
nonlocal spent
hits = 0
for _ in range(BATCH):
shot = fire(a, rng)
spent += 1
hits += 1 if (shot[0] ** 2 + shot[1] ** 2) ** 0.5 <= gold else 0
return hits / BATCH
while spent < budget:
here = hit_rate(aim) # re-measured, every round
guess = [aim[0] + rng.gauss(0, SIGMA), aim[1] + rng.gauss(0, SIGMA)]
there = hit_rate(guess)
if there > here:
aim = guess
if residual(aim) < NEAR:
return spent
return budget
def average(f, trials=40, **kw):
return round(sum(f(s, **kw) for s in range(trials)) / trials)
arrows = {
'told a number': average(lambda s: told(s, 'number')),
'told a category': average(lambda s: told(s, 'category')),
'told hit or miss': average(hit_or_miss),
}
for label, n in arrows.items():
print(f'{label:>18}: {n} arrows')
record('arrows_number', arrows['told a number'])
record('arrows_category', arrows['told a category'])
record('arrows_hitmiss', arrows['told hit or miss'])
record('times_more', round(arrows['told hit or miss'] / arrows['told a number']))
def plot_cost(ax):
labels = list(arrows)
values = list(arrows.values())
bars = ax.bar(labels, values, color=['#9aa1ab', '#3b6fd4', '#ee785b'])
for b, v in zip(bars, values):
ax.text(b.get_x() + b.get_width() / 2, v + max(values) * 0.02, f'{v}',
ha='center', fontsize=11)
ax.set_ylabel('arrows until it has learned the habit')
ax.set_ylim(0, max(values) * 1.16)
ax.spines[['top', 'right']].set_visible(False)
save_fig('what-one-word-costs', plot_cost, figsize=(7.0, 3.8))import matplotlib.pyplot as plt
import matplotlib.patches as patches
def plot_rings(ax):
rng2 = random.Random(11)
shots = [fire((11.0, -7.0), rng2) for _ in range(60)] # a robot already aiming well
ax.scatter([s[0] for s in shots], [s[1] for s in shots], s=22, color='#ee785b',
alpha=0.7, zorder=1)
for r, style in ((20, ':'), (6, '--'), (2, '-')):
inside = sum(1 for s in shots if (s[0] ** 2 + s[1] ** 2) ** 0.5 <= r)
ax.add_artist(patches.Circle((0, 0), r, facecolor='none', edgecolor='#3a3a3a',
lw=1.4, ls=style, zorder=2))
ax.annotate(f'{r} cm ring — {inside} of {len(shots)} arrows are a "hit"',
xy=(r * 0.71, r * 0.71), xytext=(26, r - 1), fontsize=9.5, color='#3a3a3a',
va='center', arrowprops=dict(arrowstyle='-', color='#b9b2aa', lw=1))
record(f'hits_at_{r}', inside)
ax.set_xlim(-24, 68); ax.set_ylim(-24, 24); ax.set_aspect('equal')
ax.set_xticks([]); ax.set_yticks([]); ax.grid(False)
for side in ('top', 'right', 'bottom', 'left'):
ax.spines[side].set_visible(False)
save_fig('what-counts-as-a-hit', plot_rings, figsize=(7.4, 3.4))golds = [2, 3, 6, 10, 14, 20]
costs = [average(hit_or_miss, trials=30, gold=float(g)) for g in golds]
for g, c in zip(golds, costs):
cap = ' (never got there — stopped at the budget)' if c >= BUDGET * 0.95 else ''
print(f'gold {g:>2} cm -> {c:>5} arrows{cap}')
record('budget', BUDGET)
record('cost_tiny', costs[0])
record('cost_best', min(costs))
record('best_gold', golds[costs.index(min(costs))])
record('cost_huge', costs[-1])
def plot_signal(ax):
ax.plot(golds, costs, color='#ee785b', linewidth=2.4, marker='o', markersize=7)
ax.axhline(BUDGET, color='#6b7280', linestyle=':', linewidth=1.2)
ax.text(3, BUDGET * 0.93, 'gave up here', fontsize=9.5, color='#6b7280')
ax.annotate('almost never hits\nevery word is "miss"', xy=(2, costs[0]),
xytext=(2.6, costs[0] + BUDGET * 0.16), fontsize=9.5, color='#6b7280')
ax.annotate('almost always hits\nevery word is "hit"', xy=(20, costs[-1]),
xytext=(12.4, costs[-1] - BUDGET * 0.20), fontsize=9.5, color='#6b7280')
ax.set_xlabel('how big the gold ring is (cm)')
ax.set_ylabel('arrows until it has learned the habit')
ax.set_ylim(0, BUDGET * 1.1)
ax.spines[['top', 'right']].set_visible(False)
save_fig('a-word-that-never-changes', plot_signal, figsize=(7.2, 4.0))Related lessons
Base rates — what a piece of evidence is actually worth
A face-recognition system that is 99.9% accurate and almost entirely wrong, and a number that sent an innocent woman to prison. Both are the same arithmetic, and it is the arithmetic that decides what any piece of evidence is worth.
ReadConfirmation and survivorship — what you never looked for
Two questions about evidence you did not go looking for. One is a rule you have to discover, and one is a pattern in five famous people — and in both, the thing that would have told you the truth is the thing nobody checks.
ReadLoss aversion, sunk cost and regression — what it costs you
Four questions you answer about yourself rather than about a scenario, and your own answers are the finding. Then the pattern that makes praise look useless and criticism look like it works, whatever you actually do.
Read
