Skip to content
Expedify
What learning is

Module · What learning is

Reinforcement learning — only hit or miss

Lesson 4 of 6 · 13 min

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

All three re-measured here, on one scoring rule.what-learning-is/only-hit-or-miss.ipynb

The third bar is many times longer than the other two, and all three end in the same place.

The same bow and the same destination. 43 times the experience for the same result.

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.

The same sixty arrows, judged three different ways.what-learning-is/only-hit-or-miss.ipynb

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

Both ends of the curve are the same failure.what-learning-is/only-hit-or-miss.ipynb

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.

The same robot and the same bow throughout.

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.

The whole path, on one bow.

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.

All three kinds of feedback, on one measuring stick

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
The same bow, the same target, and one scoring rule for all three
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))
Try something, see if it hits more often, keep it if it did
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))
What counts as a hit, at three sizes of gold ring
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))
The cost of one word, against how often that word changes
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))