{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5fc3b97f",
   "metadata": {},
   "source": [
    "# What learning is · Being told the answer\n",
    "\n",
    "The same robot, the same bow, the same habit. One thing is new: **a coach**,\n",
    "standing at the target, calling back where each arrow landed.\n",
    "\n",
    "Everything the coach says is a *label* — the answer to \"where did that one go\",\n",
    "written down by somebody who could see it. This notebook asks the two questions\n",
    "that decide what a label is worth: **how many of them do you need**, and **how\n",
    "much does each one have to say**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "267dcc55",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T12:28:05.198174Z",
     "iopub.status.busy": "2026-08-18T12:28:05.197964Z",
     "iopub.status.idle": "2026-08-18T12:28:05.621392Z",
     "shell.execute_reply": "2026-08-18T12:28:05.621083Z"
    }
   },
   "outputs": [],
   "source": [
    "# hide — plumbing so this file runs both in the repo and in Colab.\n",
    "try:\n",
    "    from _figkit import save_fig, record\n",
    "except ImportError:  # Colab — no repo, no problem\n",
    "    def save_fig(name, plot, **kw):\n",
    "        import matplotlib.pyplot as plt\n",
    "        fig, ax = plt.subplots(figsize=kw.get('figsize', (7, 4.2))); plot(ax); plt.show()\n",
    "    def record(key, value):\n",
    "        print(f'{key} = {value}'); return value"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "45bf4b18",
   "metadata": {},
   "source": [
    "## 1. The same bow as before\n",
    "\n",
    "Nothing here has changed from the last lesson. The robot pulls left and high by\n",
    "a fixed amount, and there is a wobble on top of that which no amount of coaching\n",
    "removes. The floor — the score a robot with no habit at all would average — is\n",
    "what every number below should be read against."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "d1ef9111",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T12:28:05.623265Z",
     "iopub.status.busy": "2026-08-18T12:28:05.623129Z",
     "iopub.status.idle": "2026-08-18T12:28:05.635951Z",
     "shell.execute_reply": "2026-08-18T12:28:05.635665Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "floor = 5.0\n",
      "arrows = 60\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "60"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "#| caption: The same bow, the same habit, the same unavoidable wobble\n",
    "import random\n",
    "\n",
    "BIAS = (-11.0, 7.0)      # cm: pulls left and high, every single shot\n",
    "WOBBLE = 4.0             # cm: irreducible\n",
    "ARROWS = 60\n",
    "TRIALS = 400\n",
    "STEP = 0.15              # how far it moves its aim when told the exact miss\n",
    "\n",
    "def shoot(aim, rng):\n",
    "    return (aim[0] + BIAS[0] + rng.gauss(0, WOBBLE),\n",
    "            aim[1] + BIAS[1] + rng.gauss(0, WOBBLE))\n",
    "\n",
    "def miss_by(shot):\n",
    "    return (shot[0] ** 2 + shot[1] ** 2) ** 0.5\n",
    "\n",
    "rng = random.Random(99)\n",
    "FLOOR = sum(miss_by((rng.gauss(0, WOBBLE), rng.gauss(0, WOBBLE)))\n",
    "            for _ in range(20000)) / 20000\n",
    "record('floor', round(FLOOR, 1))\n",
    "record('arrows', ARROWS)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8dc5c8b",
   "metadata": {},
   "source": [
    "## 2. The coach goes home\n",
    "\n",
    "A coach costs something. Somebody has to stand at the target, watch every arrow\n",
    "and write down where it went — and in the real version of this problem that\n",
    "somebody is a person marking four thousand rows by hand.\n",
    "\n",
    "So: label the first few arrows and then send the coach home. The robot keeps\n",
    "shooting, it just stops being told anything. How many labelled arrows does it\n",
    "actually need?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "9ae62e8b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T12:28:05.637246Z",
     "iopub.status.busy": "2026-08-18T12:28:05.637139Z",
     "iopub.status.idle": "2026-08-18T12:28:05.837169Z",
     "shell.execute_reply": "2026-08-18T12:28:05.836841Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "  0 labelled arrows -> ends at 13.7 cm\n",
      "  5 labelled arrows -> ends at 7.4 cm\n",
      " 10 labelled arrows -> ends at 5.7 cm\n",
      " 20 labelled arrows -> ends at 5.2 cm\n",
      " 40 labelled arrows -> ends at 5.2 cm\n",
      " 60 labelled arrows -> ends at 5.2 cm\n",
      "labelled_none = 13.7\n",
      "labelled_five = 7.4\n",
      "labelled_ten = 5.7\n",
      "labelled_all = 5.2\n",
      "share_from_ten = 95\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure what-a-label-buys -> being-told-the-answer.what-a-label-buys.{light,dark}.svg\n"
     ]
    }
   ],
   "source": [
    "#| caption: Labelled arrows against where the robot ends up\n",
    "BUDGETS = [0, 5, 10, 20, 40, 60]\n",
    "\n",
    "def run(seed, labelled):\n",
    "    \"\"\"Shoot ARROWS arrows, hearing from the coach only for the first `labelled`.\"\"\"\n",
    "    rng = random.Random(seed)\n",
    "    aim, misses = [0.0, 0.0], []\n",
    "    for i in range(ARROWS):\n",
    "        shot = shoot(tuple(aim), rng)\n",
    "        misses.append(miss_by(shot))\n",
    "        if i < labelled:                      # the label: where it landed\n",
    "            aim[0] -= STEP * shot[0]\n",
    "            aim[1] -= STEP * shot[1]\n",
    "    return misses\n",
    "\n",
    "def ends_at(labelled):\n",
    "    \"\"\"Average miss over the last ten arrows, across TRIALS repeats.\"\"\"\n",
    "    runs = [run(s, labelled) for s in range(TRIALS)]\n",
    "    return sum(sum(r[-10:]) / 10 for r in runs) / TRIALS\n",
    "\n",
    "scores = [ends_at(n) for n in BUDGETS]\n",
    "for n, s in zip(BUDGETS, scores):\n",
    "    print(f'{n:>3} labelled arrows -> ends at {s:.1f} cm')\n",
    "\n",
    "record('labelled_none', round(scores[0], 1))\n",
    "record('labelled_five', round(scores[1], 1))\n",
    "record('labelled_ten', round(scores[2], 1))\n",
    "record('labelled_all', round(scores[-1], 1))\n",
    "record('share_from_ten', round(100 * (scores[0] - scores[2]) / (scores[0] - scores[-1])))\n",
    "\n",
    "def plot_budget(ax):\n",
    "    ax.plot(BUDGETS, scores, color='#ee785b', linewidth=2.4, marker='o', markersize=7)\n",
    "    ax.axhline(FLOOR, color='#6b7280', linestyle=':', linewidth=1.2)\n",
    "    ax.text(28, FLOOR + 0.7, 'the wobble — nothing can beat this',\n",
    "            fontsize=9.5, color='#6b7280')\n",
    "    for n, s in zip(BUDGETS, scores):\n",
    "        ax.annotate(f'{s:.1f}', (n, s), textcoords='offset points',\n",
    "                    xytext=(0, 12), ha='center', fontsize=10)\n",
    "    ax.set_xlabel('arrows the coach was there for')\n",
    "    ax.set_ylabel('where it ends up (cm from the bullseye)')\n",
    "    ax.set_ylim(0, max(scores) * 1.25)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('what-a-label-buys', plot_budget, figsize=(7.4, 4.0))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7abca8b7",
   "metadata": {},
   "source": [
    "## 3. What the coach is allowed to say\n",
    "\n",
    "Now hold the arrows fixed and change the *label itself*.\n",
    "\n",
    "* **A number.** \"You were eleven left and seven high.\" The robot knows how far\n",
    "  to move and in which direction.\n",
    "* **A category.** \"Left. High.\" No amount — just which side. All the robot can\n",
    "  do is take a fixed step that way and shoot again.\n",
    "\n",
    "Both are labels, and both are supervised learning: somebody who could see the\n",
    "answer told the machine what it was. The only difference is how much each\n",
    "telling contains."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "e9f5fa8c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-08-18T12:28:05.839016Z",
     "iopub.status.busy": "2026-08-18T12:28:05.838759Z",
     "iopub.status.idle": "2026-08-18T12:28:06.000158Z",
     "shell.execute_reply": "2026-08-18T12:28:05.999773Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "cat_step = 0.6\n",
      "near_floor = 6.0\n",
      "arrows_number = 10\n",
      "arrows_category = 18\n",
      "ends_number = 5.2\n",
      "ends_category = 5.2\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "figure number-or-category -> being-told-the-answer.number-or-category.{light,dark}.svg\n",
      "to within a centimetre of the floor: 10 arrows vs 18\n"
     ]
    }
   ],
   "source": [
    "#| caption: The same sixty arrows, told a number and told a category\n",
    "CAT_STEP = 0.6           # cm it shifts when all it hears is 'left' or 'high'\n",
    "\n",
    "def sign(v):\n",
    "    return 1.0 if v > 0 else (-1.0 if v < 0 else 0.0)\n",
    "\n",
    "def run_kind(seed, kind):\n",
    "    rng = random.Random(seed)\n",
    "    aim, misses = [0.0, 0.0], []\n",
    "    for _ in range(ARROWS):\n",
    "        shot = shoot(tuple(aim), rng)\n",
    "        misses.append(miss_by(shot))\n",
    "        if kind == 'number':\n",
    "            aim[0] -= STEP * shot[0]\n",
    "            aim[1] -= STEP * shot[1]\n",
    "        elif kind == 'category':\n",
    "            aim[0] -= CAT_STEP * sign(shot[0])\n",
    "            aim[1] -= CAT_STEP * sign(shot[1])\n",
    "    return misses\n",
    "\n",
    "def average_curve(kind):\n",
    "    runs = [run_kind(s, kind) for s in range(TRIALS)]\n",
    "    return [sum(r[i] for r in runs) / TRIALS for i in range(ARROWS)]\n",
    "\n",
    "curve_number = average_curve('number')\n",
    "curve_category = average_curve('category')\n",
    "curve_nothing = average_curve('nothing')\n",
    "\n",
    "NEAR = FLOOR + 1.0       # 'as good as it is ever going to get', within a centimetre\n",
    "def arrows_to_near(curve):\n",
    "    return next(i + 1 for i, v in enumerate(curve) if v < NEAR)\n",
    "\n",
    "record('cat_step', CAT_STEP)\n",
    "record('near_floor', round(NEAR, 1))\n",
    "record('arrows_number', arrows_to_near(curve_number))\n",
    "record('arrows_category', arrows_to_near(curve_category))\n",
    "record('ends_number', round(sum(curve_number[-10:]) / 10, 1))\n",
    "record('ends_category', round(sum(curve_category[-10:]) / 10, 1))\n",
    "\n",
    "def plot_kinds(ax):\n",
    "    x = range(1, ARROWS + 1)\n",
    "    ax.plot(x, curve_nothing, color='#9aa1ab', linewidth=2, label='told nothing')\n",
    "    ax.plot(x, curve_category, color='#3b6fd4', linewidth=2.2,\n",
    "            label='told a category - \"left, high\"')\n",
    "    ax.plot(x, curve_number, color='#ee785b', linewidth=2.4,\n",
    "            label='told a number - \"11 left, 7 high\"')\n",
    "    ax.axhline(FLOOR, color='#6b7280', linestyle=':', linewidth=1.2)\n",
    "    ax.set_xlabel('arrows shot')\n",
    "    ax.set_ylabel('distance from the bullseye (cm)')\n",
    "    ax.set_ylim(0, max(curve_nothing) * 1.15)\n",
    "    ax.legend(frameon=False, fontsize=10)\n",
    "    ax.spines[['top', 'right']].set_visible(False)\n",
    "\n",
    "save_fig('number-or-category', plot_kinds, figsize=(7.4, 4.2))\n",
    "print('to within a centimetre of the floor:',\n",
    "      arrows_to_near(curve_number), 'arrows vs', arrows_to_near(curve_category))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1ccc83d",
   "metadata": {},
   "source": [
    "## 4. Both arrive. One arrives sooner.\n",
    "\n",
    "The two learners end in the same place, because the same habit is there to be\n",
    "removed either way and the same wobble stops them both. What differs is the\n",
    "price in arrows — and that is the honest way to think about a label.\n",
    "\n",
    "A vaguer label is not a broken label. It just costs more examples, and examples\n",
    "are the thing you were trying not to pay for."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
