1405 lines
121 KiB
Plaintext
1405 lines
121 KiB
Plaintext
{
|
||
"cells": [
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "153d6f74",
|
||
"metadata": {},
|
||
"source": [
|
||
"# Micrograd — backprop from scratch\n",
|
||
"\n",
|
||
"A tiny automatic-gradient engine in pure Python — **concept -> code -> Your turn** each step.\n",
|
||
"\n",
|
||
"This is notebook **00** of the series and the right place to start: it explains what a\n",
|
||
"*gradient* and *backpropagation* really are, using single numbers you can follow by hand.\n",
|
||
"Every later notebook (the bigram in `01`, the transformer in `06`) calls `loss.backward()`\n",
|
||
"and trusts it. Here we build that machinery ourselves so it is never magic."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "aac17ea7",
|
||
"metadata": {},
|
||
"source": [
|
||
"## Prologue — what this notebook does, in plain English\n",
|
||
"\n",
|
||
"We build a small object called a **`Value`**. A `Value` is just a number that also\n",
|
||
"**remembers how it was made** (which other numbers, and which operation: plus, times, ...).\n",
|
||
"\n",
|
||
"Once numbers remember their own history, the computer can answer one very useful question\n",
|
||
"automatically:\n",
|
||
"\n",
|
||
"> *If I nudge this input a tiny bit, how much does the final answer change?*\n",
|
||
"\n",
|
||
"That sensitivity is the **gradient**. Computing all those sensitivities in one efficient\n",
|
||
"backward sweep is **backpropagation** — the single algorithm that trains essentially every\n",
|
||
"neural network, including GPT.\n",
|
||
"\n",
|
||
"This notebook follows Andrej Karpathy's *\"The spelled-out intro to neural networks and\n",
|
||
"backpropagation: building micrograd\"* lecture."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "44dd4b0d",
|
||
"metadata": {},
|
||
"source": [
|
||
"### The whole idea, as a chain of gears\n",
|
||
"\n",
|
||
"Picture a row of **gears** connected together. You turn the first gear a little; the last\n",
|
||
"gear also turns, by an amount that depends on all the gears in between.\n",
|
||
"\n",
|
||
"- The **forward pass** is turning the first gears and reading the last gear (the output).\n",
|
||
"- The **gradient** answers: *if I turn this one gear slightly, how much does the final gear move?*\n",
|
||
"- **Backpropagation** is figuring out that answer for **every** gear at once, by walking\n",
|
||
" backward from the last gear to the first and multiplying the little ratios along the way\n",
|
||
" (that multiplication is the **chain rule**).\n",
|
||
"\n",
|
||
"A neural network is just a very big gear train. Training = nudging each gear a hair in the\n",
|
||
"direction that makes the output less wrong."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "334f0f03",
|
||
"metadata": {},
|
||
"source": [
|
||
"### Mental picture — a factory line with tags\n",
|
||
"\n",
|
||
"Imagine a **cookie factory**. Each station does one small job and puts a **tag** on the box\n",
|
||
"saying *who gave me ingredients* and *what I did* (mixed, baked, frosted).\n",
|
||
"\n",
|
||
"| Step | Real life | In code |\n",
|
||
"|------|-----------|---------|\n",
|
||
"| Forward pass | Ingredients move down the line; you get a final cookie score | `d = a * b + c` computes numbers left to right |\n",
|
||
"| History | Every box keeps tags — no separate diary | `_prev` (parents) and `_op` (operation) on each `Value` |\n",
|
||
"| Gradient | *If I change this one ingredient a tiny bit, how much does the final score change?* | `.grad` on each `Value` |\n",
|
||
"| Backprop | Walk backward along the tags from the final score to every ingredient | `loss.backward()` |\n",
|
||
"\n",
|
||
"A normal float is like a score on a napkin: **4**. You cannot tell where it came from.\n",
|
||
"A **`Value`** is the same score **on a tagged box** — you can always walk back.\n",
|
||
"\n",
|
||
"**Blame traveling backward (the chain rule)**\n",
|
||
"\n",
|
||
"The final taste score is off. At each station you ask: *if your output had been 1 point\n",
|
||
"sweeter, how much would the final score have moved?*\n",
|
||
"\n",
|
||
"- **`+` (mix two bowls)** — nudge either bowl; the mix moves the same amount. Blame passes\n",
|
||
" through to both inputs equally.\n",
|
||
"- **`*` (recipe ratio, e.g. 2 cups × 3 batches)** — change flour a little → effect depends\n",
|
||
" on batch count; change batches → effect depends on flour. Each side's blame depends on\n",
|
||
" the **other** side's amount.\n",
|
||
"\n",
|
||
"**Backprop** starts at the output (\"downstream moved by 1\"), walks **backward** along the\n",
|
||
"tags, and at each station multiplies by that station's local rule. Small effect here ×\n",
|
||
"small effect there = total effect on the end.\n",
|
||
"\n",
|
||
"**Why gradients add (`+=`)**\n",
|
||
"\n",
|
||
"One bag of sugar might go into **both** dough and icing. Both paths affect the final taste,\n",
|
||
"so sugar's total blame is the **sum** of both paths — same when one `Value` is used twice\n",
|
||
"in an expression.\n",
|
||
"\n",
|
||
"**Training the MLP (section 6)**\n",
|
||
"\n",
|
||
"Same factory, but the knobs are **weights and biases** (how much each input counts):\n",
|
||
"\n",
|
||
"1. **Forward** — run points through the line; get predictions.\n",
|
||
"2. **Loss** — how wrong were we? (distance from the target).\n",
|
||
"3. **Backward** — `loss.backward()` sends blame to every knob.\n",
|
||
"4. **Update** — turn each knob a tiny step **against** its gradient (downhill = less wrong).\n",
|
||
"\n",
|
||
"The **two moons** dataset is two swirls of red and blue beads. The network learns a curved\n",
|
||
"boundary — not by memorizing dots, but by turning knobs until wrong guesses hurt (high loss)\n",
|
||
"and right ones don't.\n",
|
||
"\n",
|
||
"> **One line to keep:** Forward = tagged boxes down the line. Backward = follow tags from\n",
|
||
"> the final score back to each ingredient and assign fair blame. PyTorch does the same thing\n",
|
||
"> at scale — millions of tagged boxes, fast hardware."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "84448625",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 0.1 Imports\n",
|
||
"\n",
|
||
"Pure Python plus a little NumPy/Matplotlib for the toy dataset and pictures. No deep-learning\n",
|
||
"library is needed to build the engine itself — that is the whole point.\n",
|
||
"\n",
|
||
"**-> Training:** `math` gives us `exp`/`tanh` for activations; `random` initialises weights;\n",
|
||
"NumPy and Matplotlib are only for the demo dataset and plots."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 1,
|
||
"id": "e31a52ef",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"ready\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"import math # exp, tanh used inside Value activations\n",
|
||
"import random # random weight init for Neuron\n",
|
||
"import numpy as np # dataset + optional plots only (not the autograd engine)\n",
|
||
"\n",
|
||
"random.seed(1337) # same random weights every run\n",
|
||
"np.random.seed(1337) # same noisy moon dots every run\n",
|
||
"\n",
|
||
"# Plots are optional: the notebook runs fine without matplotlib (it just skips pictures).\n",
|
||
"try:\n",
|
||
" import matplotlib.pyplot as plt\n",
|
||
" HAS_PLT = True # flag so later cells can skip plotting gracefully\n",
|
||
"except Exception:\n",
|
||
" HAS_PLT = False\n",
|
||
" print(\"matplotlib not installed -> plots will be skipped (everything else still works)\")\n",
|
||
"\n",
|
||
"print(\"ready\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "ace0626a",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"from IPython.display import HTML, display\n",
|
||
"import json\n",
|
||
"\n",
|
||
"_GRAPH_UID = 0 # unique id per render so multiple cells don't clash\n",
|
||
"\n",
|
||
"\n",
|
||
"def show_graph(root, title=\"Computation graph — hover a node\"):\n",
|
||
" \"\"\"Draw an interactive SVG graph for a Value expression.\n",
|
||
"\n",
|
||
" Hover (or click) any node to see its value, operation, and parents.\n",
|
||
" After `.backward()`, gradients appear on each node automatically.\n",
|
||
" \"\"\"\n",
|
||
" global _GRAPH_UID\n",
|
||
" _GRAPH_UID += 1\n",
|
||
" gid = f\"mg{_GRAPH_UID}\"\n",
|
||
"\n",
|
||
" nodes, seen = [], set()\n",
|
||
"\n",
|
||
" def build_topo(v):\n",
|
||
" if v not in seen:\n",
|
||
" seen.add(v)\n",
|
||
" for ch in v._prev:\n",
|
||
" build_topo(ch)\n",
|
||
" nodes.append(v)\n",
|
||
"\n",
|
||
" build_topo(root)\n",
|
||
" id_map = {v: i for i, v in enumerate(nodes)}\n",
|
||
"\n",
|
||
" depth = {}\n",
|
||
"\n",
|
||
" def mark_depth(v, d=0):\n",
|
||
" depth[v] = max(depth.get(v, 0), d)\n",
|
||
" for ch in v._prev:\n",
|
||
" mark_depth(ch, d + 1)\n",
|
||
"\n",
|
||
" mark_depth(root, 0)\n",
|
||
" max_depth = max(depth.values()) if depth else 0\n",
|
||
" layer_of = {v: max_depth - depth[v] for v in nodes}\n",
|
||
"\n",
|
||
" from collections import defaultdict\n",
|
||
"\n",
|
||
" layers = defaultdict(list)\n",
|
||
" for v in nodes:\n",
|
||
" layers[layer_of[v]].append(v)\n",
|
||
"\n",
|
||
" NODE_W, NODE_H, GAP_X, GAP_Y = 108, 58, 36, 72\n",
|
||
" positions = {}\n",
|
||
" max_w = 0\n",
|
||
" for ly in sorted(layers.keys()):\n",
|
||
" row = layers[ly]\n",
|
||
" row_w = len(row) * NODE_W + max(0, len(row) - 1) * GAP_X\n",
|
||
" max_w = max(max_w, row_w)\n",
|
||
" x0 = (max_w - row_w) / 2\n",
|
||
" for i, v in enumerate(row):\n",
|
||
" positions[v] = (x0 + i * (NODE_W + GAP_X), ly * (NODE_H + GAP_Y))\n",
|
||
"\n",
|
||
" svg_w = int(max_w + 48)\n",
|
||
" svg_h = int((max(layers.keys()) + 1) * (NODE_H + GAP_Y) + 36)\n",
|
||
" show_grad = any(getattr(v, \"grad\", 0.0) for v in nodes)\n",
|
||
"\n",
|
||
" def node_name(v):\n",
|
||
" lbl = getattr(v, \"label\", \"\") or \"\"\n",
|
||
" return lbl if lbl else f\"n{id_map[v]}\"\n",
|
||
"\n",
|
||
" def esc(s):\n",
|
||
" return (\n",
|
||
" str(s)\n",
|
||
" .replace(\"&\", \"&\")\n",
|
||
" .replace(\"<\", \"<\")\n",
|
||
" .replace(\">\", \">\")\n",
|
||
" .replace('\"', \""\")\n",
|
||
" )\n",
|
||
"\n",
|
||
" edge_lines = []\n",
|
||
" for v in nodes:\n",
|
||
" for ch in v._prev:\n",
|
||
" x1, y1 = positions[ch]\n",
|
||
" x2, y2 = positions[v]\n",
|
||
" cx1, cy1 = x1 + NODE_W / 2, y1 + NODE_H\n",
|
||
" cx2, cy2 = x2 + NODE_W / 2, y2\n",
|
||
" eid = f\"{gid}-e{id_map[ch]}-{id_map[v]}\"\n",
|
||
" edge_lines.append(\n",
|
||
" f'<line class=\"edge\" data-edge=\"{eid}\" data-from=\"{id_map[ch]}\" data-to=\"{id_map[v]}\" '\n",
|
||
" f'x1=\"{cx1:.1f}\" y1=\"{cy1:.1f}\" '\n",
|
||
" f'x2=\"{cx2:.1f}\" y2=\"{cy2:.1f}\" stroke=\"#94a3b8\" stroke-width=\"2\" '\n",
|
||
" f'marker-end=\"url(#{gid}-arrow)\"/>'\n",
|
||
" )\n",
|
||
"\n",
|
||
" node_groups = []\n",
|
||
" meta_nodes = {}\n",
|
||
" for v in nodes:\n",
|
||
" x, y = positions[v]\n",
|
||
" nid = id_map[v]\n",
|
||
" node_id = f\"{gid}-n{nid}\"\n",
|
||
" nm = esc(node_name(v))\n",
|
||
" op = esc(v._op or \"leaf\")\n",
|
||
" val = f\"{v.data:.4f}\"\n",
|
||
" grad = f\"{getattr(v, 'grad', 0.0):.4f}\"\n",
|
||
" fill = \"#dcfce7\" if not v._prev else \"#dbeafe\"\n",
|
||
" grad_svg = (\n",
|
||
" f'<text x=\"{x + 8:.1f}\" y=\"{y + 50:.1f}\" font-size=\"11\" fill=\"#c2410c\">'\n",
|
||
" f\"grad = {grad}</text>\"\n",
|
||
" if show_grad\n",
|
||
" else \"\"\n",
|
||
" )\n",
|
||
" node_groups.append(\n",
|
||
" f'<g class=\"node\" data-node=\"{node_id}\" tabindex=\"0\" role=\"button\" '\n",
|
||
" f'aria-label=\"{nm} value {val}\">'\n",
|
||
" f'<rect x=\"{x:.1f}\" y=\"{y:.1f}\" width=\"{NODE_W}\" height=\"{NODE_H}\" rx=\"8\" '\n",
|
||
" f'fill=\"{fill}\" stroke=\"#64748b\" stroke-width=\"1.5\"/>'\n",
|
||
" f'<text x=\"{x + 8:.1f}\" y=\"{y + 18:.1f}\" font-size=\"13\" font-weight=\"600\" '\n",
|
||
" f'fill=\"#0f172a\">{nm}</text>'\n",
|
||
" f'<text x=\"{x + 8:.1f}\" y=\"{y + 34:.1f}\" font-size=\"11\" fill=\"#475569\">'\n",
|
||
" f\"op: {op} val: {val}</text>\"\n",
|
||
" f\"{grad_svg}\"\n",
|
||
" f'<title>{nm}: {val} ({op})</title></g>'\n",
|
||
" )\n",
|
||
" meta_nodes[node_id] = {\n",
|
||
" \"name\": node_name(v),\n",
|
||
" \"op\": v._op or \"leaf\",\n",
|
||
" \"val\": val,\n",
|
||
" \"grad\": grad,\n",
|
||
" \"parents\": \", \".join(node_name(p) for p in v._prev) or \"—\",\n",
|
||
" }\n",
|
||
"\n",
|
||
" meta_json = json.dumps({\"gid\": gid, \"show_grad\": show_grad, \"nodes\": meta_nodes})\n",
|
||
"\n",
|
||
" html = f\"\"\"\n",
|
||
"<div class=\"mg-wrap\" style=\"font-family:system-ui,sans-serif;max-width:{svg_w + 20}px\">\n",
|
||
" <div style=\"font-weight:600;margin-bottom:6px;color:#334155\">{esc(title)}</div>\n",
|
||
" <svg width=\"{svg_w}\" height=\"{svg_h}\" style=\"background:#f8fafc;border:1px solid #e2e8f0;border-radius:8px\">\n",
|
||
" <defs>\n",
|
||
" <marker id=\"{gid}-arrow\" markerWidth=\"8\" markerHeight=\"8\" refX=\"6\" refY=\"3\" orient=\"auto\">\n",
|
||
" <path d=\"M0,0 L6,3 L0,6 Z\" fill=\"#94a3b8\"/>\n",
|
||
" </marker>\n",
|
||
" </defs>\n",
|
||
" {''.join(edge_lines)}\n",
|
||
" {''.join(node_groups)}\n",
|
||
" </svg>\n",
|
||
" <div id=\"{gid}-info\" style=\"margin-top:8px;padding:8px 10px;background:#fff;border:1px solid #e2e8f0;\n",
|
||
" border-radius:6px;font-size:13px;color:#334155;min-height:1.2em\">\n",
|
||
" Hover a node to inspect it. Arrows flow from inputs (bottom) to output (top).\n",
|
||
" </div>\n",
|
||
"</div>\n",
|
||
"<style>\n",
|
||
" .mg-wrap .node {{ cursor: pointer; }}\n",
|
||
" .mg-wrap .node rect {{ transition: stroke 0.12s, filter 0.12s; }}\n",
|
||
" .mg-wrap .node:hover rect, .mg-wrap .node:focus rect {{ stroke: #f59e0b; stroke-width: 2.5; filter: drop-shadow(0 1px 3px rgba(0,0,0,.15)); }}\n",
|
||
" .mg-wrap .edge.dim {{ opacity: 0.15; }}\n",
|
||
" .mg-wrap .edge.hl {{ stroke: #f59e0b; stroke-width: 3; opacity: 1; }}\n",
|
||
"</style>\n",
|
||
"<script>\n",
|
||
"(function() {{\n",
|
||
" const meta = {meta_json};\n",
|
||
" const info = document.getElementById(meta.gid + \"-info\");\n",
|
||
" const edges = Array.from(document.querySelectorAll('[data-edge^=\"' + meta.gid + '-\"]'));\n",
|
||
" function clearHL() {{\n",
|
||
" document.querySelectorAll('.node[data-node^=\"' + meta.gid + '-\"]').forEach(n => n.classList.remove('pinned'));\n",
|
||
" edges.forEach(e => {{ e.classList.remove('hl', 'dim'); }});\n",
|
||
" }}\n",
|
||
" function highlight(nodeId) {{\n",
|
||
" clearHL();\n",
|
||
" edges.forEach(e => e.classList.add('dim'));\n",
|
||
" const nid = nodeId.split('-n')[1];\n",
|
||
" edges.forEach(e => {{\n",
|
||
" if (e.getAttribute('data-from') === nid || e.getAttribute('data-to') === nid)\n",
|
||
" e.classList.remove('dim'), e.classList.add('hl');\n",
|
||
" }});\n",
|
||
" const m = meta.nodes[nodeId];\n",
|
||
" if (!m) return;\n",
|
||
" let txt = '<b>' + m.name + '</b> op=' + m.op + ' value=' + m.val;\n",
|
||
" if (meta.show_grad) txt += ' grad=' + m.grad;\n",
|
||
" txt += '<br>parents: ' + m.parents;\n",
|
||
" info.innerHTML = txt;\n",
|
||
" }}\n",
|
||
" document.querySelectorAll('.node[data-node^=\"' + meta.gid + '-\"]').forEach(el => {{\n",
|
||
" el.addEventListener('mouseenter', () => highlight(el.getAttribute('data-node')));\n",
|
||
" el.addEventListener('focus', () => highlight(el.getAttribute('data-node')));\n",
|
||
" el.addEventListener('click', () => {{\n",
|
||
" el.classList.toggle('pinned');\n",
|
||
" highlight(el.getAttribute('data-node'));\n",
|
||
" }});\n",
|
||
" }});\n",
|
||
"}})();\n",
|
||
"</script>\n",
|
||
"\"\"\"\n",
|
||
" display(HTML(html))\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"interactive graph viewer ready\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "3315510f",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 0.2 A tiny dataset — two interleaving moons\n",
|
||
"\n",
|
||
"Our goal at the end is to train a small network to separate two groups of dots that curl\n",
|
||
"around each other (\"two moons\"). Real-life picture: two handfuls of red and blue beads\n",
|
||
"mixed in a swirl — can the network learn to draw the boundary between them?\n",
|
||
"\n",
|
||
"We try scikit-learn's `make_moons`; if it is not installed we generate the same shape with\n",
|
||
"NumPy so the notebook stays dependency-light."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 2,
|
||
"id": "507b4df3",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"sklearn not available, using NumPy fallback: ModuleNotFoundError\n",
|
||
"X shape: (100, 2) y shape: (100,)\n",
|
||
"first 3 points: [[0.9296812690137589, -0.04902823627877147], [0.965763959785321, -0.11143765173168577], [1.0124564608446291, -0.07324929559695742]]\n",
|
||
"first 3 labels: [0, 0, 0]\n"
|
||
]
|
||
},
|
||
{
|
||
"data": {
|
||
"image/png": "iVBORw0KGgoAAAANSUhEUgAAAdUAAAF2CAYAAAA4MQK3AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQAAaAZJREFUeJztnQd4FNXXxk9ooYgUqUE6CiJVmiBKlSJSFJGiNBGkKoLwBwuIqFQBQRQbzUJTioKCSBGVIgYQKfoBUqWXhCogzPe89zrJ7GZ2s5vsZtv7e56B7OzszJ075dxz7ilRhmEYQgghhJBUky71uyCEEEIIhSohhBDiQ6ipEkIIIT6CQpUQQgjxERSqhBBCiI+gUCWEEEJ8BIUqIYQQ4iMoVAkhhBAfQaFKCCGE+AgKVUKCiAMHDkhUVJTMnDkz0E0hYQDuI9xPv/76q4QSUVFR8uqrr0ooEpZCdf369eqCxMXFBbophNjy+eefy6RJk8K6d44ePaqew23btgW6KUHDu+++65cBk7/2G2l888038tBDD0nBggUlZ86c8uCDD8pvv/3m1T7CVqiOGDGCQpWEnFAtWrSoXLlyRTp27CjhIFTxHFKoJkKhGtw8/PDDkjFjRhk2bJgMHTpUduzYoQTrqVOnPN5HBr+2kBDitdkrc+bM7LUQALVI/vnnH8mSJUugm0LccOnSJcmWLZt4woYNG6RGjRoJnytXriyNGzeW7777Tp544onI1FRhbho0aJD6u3jx4uolhQVzVY8++qjcc889Dts3b95cff/VV18lrNu0aZNa9+233yas++uvv6RNmzaSO3duyZo1q9x7772ybNkyj9qEffXt21cWLFggZcuWVQ9hzZo15ffff1ffv//++1KqVCn1Mq1bt65qqzP4bZUqVdRv8+TJI08++aT8/fffSbZbvXq13H///eomgvmiZcuWsnv37iR9hDbt3btXunTporbLkSOHdO3aVS5fvuyw7cqVK6V27dpqm1tuuUVKly4tL774ogSKmzdvyttvvy3ly5dX/ZU3b15p0qSJw5zRjBkzpH79+pIvXz6Jjo5Wff7ee+8l2VexYsXUyPSnn36S6tWrq/2VKFFCZs+e7VFbxo8fL7Vq1ZLbbrtNXRdcny+++CLZ3+Ea4945ePBgwv2JtriaU8U1Qt/jerdq1Ur9jfN+4YUX5MaNGwkveOwD19sZvPhxfZ955hmP7tPFixdLuXLlVN/dfffdsnz58iTboi1PPfWU5M+fP2G76dOnJ3y/du1aqVatmvob95V5nu5MlBcuXJD+/fur88A+cf2gJWzZssVhOzyfuOY4JzyLderUkZ9//tn2Hv/jjz/k8ccfl1tvvVVdp+eee071hxVv75cVK1ZI1apV1TXHs+vpPvD7nTt3yg8//JDQH7gXTDBdhfMvXLiw2gfeCWPGjFH3vDuS2y+4evWqDBgwQN03eDc88sgjttoX3nnm+yN79uzSrFkztW9PQPuff/75hOt3++23S6dOneT06dMJ25w8eVK6deum7hs8bxUrVpRZs2Z5tP+tW7dK06ZN1bXEM9CgQQPZuHGj7Rwy+qJ3797qeqAdnt5fVoEKzAHutWvXxGOMMOO3334z2rdvj3J2xsSJE41PPvlELRcvXjQmTJhgpEuXzoiPj1fb3rx508iVK5da98ILLyTsY9y4cQ7bHT9+3MifP7+RPXt246WXXlL7qVixotpm4cKFybYJbalQoYJRuHBhY/To0WrJkSOHUaRIEeOdd94xypYta7z11lvGyy+/bGTKlMmoV6+ew+9nzJih9lGtWjV1TkOGDDGyZMliFCtWzDh37lzCditXrjQyZMhg3HnnncbYsWONESNGGHny5FHnuH///oTthg8frvZXuXJl49FHHzXeffdd4+mnn1brBg8enLDdjh07VHuqVq1qvP3228a0adNUPz3wwANGoOjSpYtqZ9OmTY1JkyYZ48ePN1q2bGlMmTIlYRv0E7ZDX2F9o0aN1G/Q11aKFi1qlC5dWl3bF198UX1/zz33GFFRUerck+P22283evfurX6He6J69erqOEuXLnX7u++++86oVKmSujbm/blo0SL1Ha4T9oFrbtK5c2cjc+bMxt1332089dRTxnvvvWe0bt1abYdrZ4J7M2PGjMaZM2ccjjd//ny17bp169y2C9vgvi5YsKAxcuRI1b8lSpQwsmbNapw+fTphOzwPOHfcz6+99ppqT4sWLRKeOXMbfId1PXr0SDjPffv2uTx+hw4d1P02YMAA46OPPjLGjBljNG/e3Pj0008Ttlm1apXapmbNmuqZwfHwbGHdpk2bktzj5cuXV/vANXryySfVuo4dOzoc15v7pVSpUup5wjOI52HNmjUe7wPXGP1WpkyZhP7AvQAuXbqk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",
|
||
"text/plain": [
|
||
"<Figure size 500x400 with 1 Axes>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"def make_moons_fallback(n_samples=100, noise=0.1):\n",
|
||
" n = n_samples // 2 # half the points per moon\n",
|
||
" t = np.linspace(0, np.pi, n) # angle along each crescent\n",
|
||
" # outer moon — upper arc (cos, sin)\n",
|
||
" x1 = np.stack([np.cos(t), np.sin(t)], axis=1)\n",
|
||
" # inner moon — lower arc, shifted down so the two curves interleave\n",
|
||
" x2 = np.stack([1 - np.cos(t), 1 - np.sin(t) - 0.5], axis=1)\n",
|
||
" X = np.concatenate([x1, x2], axis=0) # stack into one (100, 2) array\n",
|
||
" X += noise * np.random.randn(*X.shape) # jitter dots so it's not perfectly clean\n",
|
||
" y = np.array([0] * n + [1] * n) # label 0 = outer moon, 1 = inner moon\n",
|
||
" return X, y\n",
|
||
"\n",
|
||
"try:\n",
|
||
" from sklearn.datasets import make_moons\n",
|
||
" X, y = make_moons(n_samples=100, noise=0.1, random_state=1337)\n",
|
||
" print(\"using sklearn make_moons\")\n",
|
||
"except Exception as e:\n",
|
||
" X, y = make_moons_fallback(100, noise=0.1) # same shape, no sklearn needed\n",
|
||
" print(\"sklearn not available, using NumPy fallback:\", type(e).__name__)\n",
|
||
"\n",
|
||
"print(\"X shape:\", X.shape, \" y shape:\", y.shape) # expect (100, 2) and (100,)\n",
|
||
"print(\"first 3 points:\", X[:3].tolist())\n",
|
||
"print(\"first 3 labels:\", y[:3].tolist())\n",
|
||
"\n",
|
||
"if HAS_PLT:\n",
|
||
" plt.figure(figsize=(5, 4))\n",
|
||
" plt.scatter(X[:, 0], X[:, 1], c=y, s=20, cmap=\"bwr\") # red vs blue by class\n",
|
||
" plt.title(\"two moons — can a tiny net separate the colors?\")\n",
|
||
" plt.show()"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0d2fe98b",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 1.1 The `Value` object — a number that remembers\n",
|
||
"\n",
|
||
"A normal Python float forgets where it came from: once you compute `3.0`, nobody knows it\n",
|
||
"was `1.0 + 2.0`. Our `Value` keeps that memory: the numbers that made it (`_prev`) and the\n",
|
||
"operation (`_op`).\n",
|
||
"\n",
|
||
"We start with a **minimal** version that can only do the **forward pass** (no gradients yet),\n",
|
||
"just so you can see the \"remembering\" working.\n",
|
||
"\n",
|
||
"**-> Training:** `_prev` is the list of parent numbers; later, gradients flow backward along\n",
|
||
"exactly these links."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 3,
|
||
"id": "eea9bc18",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"d = Value(data=4.0)\n",
|
||
"d was made by op: '+'\n",
|
||
"d's parents: {Value(data=-6.0), Value(data=10.0)}\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"class Value:\n",
|
||
" def __init__(self, data, _children=(), _op=''):\n",
|
||
" self.data = data # the number itself (forward pass result)\n",
|
||
" # HISTORY: who made me? _prev = parent Value objects; _op = the operation (+, *, ...)\n",
|
||
" # Forward pass builds a linked tree/DAG automatically — no separate \"tape\" step.\n",
|
||
" self._prev = set(_children) # set of input Values that fed into this one\n",
|
||
" self._op = _op # string tag: '+', '*', or '' for a leaf (input)\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return f\"Value(data={self.data})\" # pretty print when you print(a)\n",
|
||
"\n",
|
||
" def __add__(self, other):\n",
|
||
" # New number = sum; we pass (self, other) so this node remembers both parents.\n",
|
||
" return Value(self.data + other.data, (self, other), '+')\n",
|
||
"\n",
|
||
" def __mul__(self, other):\n",
|
||
" # Same idea: result remembers it came from self * other.\n",
|
||
" return Value(self.data * other.data, (self, other), '*')\n",
|
||
"\n",
|
||
"\n",
|
||
"# --- tiny demo: build d = a*b + c and inspect the graph tags ---\n",
|
||
"a = Value(2.0) # leaf — no parents, _op is ''\n",
|
||
"b = Value(-3.0)\n",
|
||
"c = Value(10.0)\n",
|
||
"e = a * b # e.data = -6; e._prev = {a, b}; e._op = '*'\n",
|
||
"d = e + c # d.data = 4; d._prev = {e, c}; d._op = '+'\n",
|
||
"print(\"d =\", d)\n",
|
||
"print(\"d was made by op:\", repr(d._op))\n",
|
||
"print(\"d's parents:\", d._prev)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "e65b3200",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 1.2 Reading the expression graph\n",
|
||
"\n",
|
||
"`d = a*b + c` is really a little tree: `d` points back to `e` and `c`; `e` points back to\n",
|
||
"`a` and `b`. Let's print it as an indented tree so the structure is visible. (No graphviz\n",
|
||
"dependency — just recursion.) Section **1.3** draws the same wiring as an interactive picture."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 4,
|
||
"id": "3a436597",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"name": "stdout",
|
||
"output_type": "stream",
|
||
"text": [
|
||
"4.0000 (op=+)\n",
|
||
" -6.0000 (op=*)\n",
|
||
" 2.0000 (op=leaf)\n",
|
||
" -3.0000 (op=leaf)\n",
|
||
" 10.0000 (op=leaf)\n"
|
||
]
|
||
}
|
||
],
|
||
"source": [
|
||
"def show(v, indent=0):\n",
|
||
" # print this node's value and which op created it ('leaf' = raw input)\n",
|
||
" print(\" \" * indent + f\"{v.data:.4f} (op={v._op or 'leaf'})\")\n",
|
||
" for child in v._prev: # follow parent links (deeper indent = further back in time)\n",
|
||
" show(child, indent + 1) # recurse until we hit leaves with no _prev\n",
|
||
"\n",
|
||
"show(d) # prints the tree: d (+) -> e (*) -> a, b and c"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "f4e9da6b",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 1.3 Interactive graph — see the wiring\n",
|
||
"\n",
|
||
"The text tree above is correct but flat. The same expression as a **picture**: green boxes are\n",
|
||
"raw inputs (leaves), blue boxes are operations. **Hover** a node to highlight its connections;\n",
|
||
"**click** to pin the detail panel. Arrows run from ingredients (bottom) up to the final score (top)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "f149d4da",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Re-use d = a*b + c from section 1.1 (run that cell first if you restarted the kernel)\n",
|
||
"show_graph(d, title=\"Forward pass: d = a × b + c\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "5ee07490",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 2.1 What a gradient means — nudge and measure\n",
|
||
"\n",
|
||
"Before any clever math, here is the *definition* of a gradient, done the dumb way: change an\n",
|
||
"input by a tiny amount `h`, recompute the output, and see how much it moved.\n",
|
||
"\n",
|
||
"slope = (output after nudge - output before) / h\n",
|
||
"\n",
|
||
"Real-life picture: to feel how steep a hill is, take one small step and notice how much your\n",
|
||
"height changed. That ratio is the slope."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "fbfbee18",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def f_out(a_, b_, c_):\n",
|
||
" return (a_ * b_) + c_ # plain floats — no graph, just the math\n",
|
||
"\n",
|
||
"h = 1e-6 # tiny nudge size (like a small step on a hill)\n",
|
||
"base = f_out(2.0, -3.0, 10.0) # output before any nudge = 4.0\n",
|
||
"\n",
|
||
"# how sensitive is the output to a? slope ≈ (f(a+h) - f(a)) / h\n",
|
||
"da = (f_out(2.0 + h, -3.0, 10.0) - base) / h\n",
|
||
"# to b?\n",
|
||
"db = (f_out(2.0, -3.0 + h, 10.0) - base) / h\n",
|
||
"# to c?\n",
|
||
"dc = (f_out(2.0, -3.0, 10.0 + h) - base) / h\n",
|
||
"\n",
|
||
"print(f\"output = {base}\")\n",
|
||
"print(f\"d(out)/da = {da:.4f} (equals b = -3)\") # derivative of a*b w.r.t. a is b\n",
|
||
"print(f\"d(out)/db = {db:.4f} (equals a = 2)\") # derivative w.r.t. b is a\n",
|
||
"print(f\"d(out)/dc = {dc:.4f} (equals 1)\") # derivative of +c w.r.t. c is 1"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "d3d3c6e2",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 3.1 The chain rule — multiply the little ratios\n",
|
||
"\n",
|
||
"The nudge-and-measure trick is correct but slow: one re-run per input. Real networks have\n",
|
||
"millions of inputs. The **chain rule** gets every sensitivity in *one* backward pass.\n",
|
||
"\n",
|
||
"#### The one idea (relay race, not one long jump)\n",
|
||
"\n",
|
||
"When one thing changes another thing, which then changes a third, you **multiply the two\n",
|
||
"\"sensitivity\" steps**.\n",
|
||
"\n",
|
||
"- **a** — starting point (e.g. how many projects you take)\n",
|
||
"- **e** — middle step (hours you work)\n",
|
||
"- **d** — final result (your pay)\n",
|
||
"\n",
|
||
"**d** does not feel **a** directly. It only feels **e**. So ask two smaller questions:\n",
|
||
"\n",
|
||
"1. If **a** moves a little, how much does **e** move? → `d(e)/d(a)`\n",
|
||
"2. If **e** moves a little, how much does **d** move? → `d(d)/d(e)`\n",
|
||
"\n",
|
||
"The full effect is those two multiplied:\n",
|
||
"\n",
|
||
"`d(d)/d(a) = d(d)/d(e) × d(e)/d(a)`\n",
|
||
"\n",
|
||
"> **One line to keep:** If **d** depends on **e**, and **e** depends on **a**, then \"how fast\n",
|
||
"> **d** responds to **a**\" = \"how fast **d** responds to **e**\" × \"how fast **e** responds to **a**.\"\n",
|
||
"\n",
|
||
"#### Units make it click (the middle cancels)\n",
|
||
"\n",
|
||
"Like converting units:\n",
|
||
"\n",
|
||
"- Pay per hour → dollars/hour\n",
|
||
"- Hours per project → hours/project\n",
|
||
"\n",
|
||
"Multiply: `(dollars/hour) × (hours/project) = dollars/project`\n",
|
||
"\n",
|
||
"The **hour** in the middle cancels. You get \"how pay changes when projects change.\" The formula\n",
|
||
"looks like fraction cancellation — that is the story, not magic algebra.\n",
|
||
"\n",
|
||
"#### Tiny wiggle picture\n",
|
||
"\n",
|
||
"Bump **a** by a tiny amount Δa.\n",
|
||
"\n",
|
||
"- **e** moves about `(d(e)/d(a)) × Δa`\n",
|
||
"- **d** moves about `(d(d)/d(e)) ×` that amount\n",
|
||
"\n",
|
||
"So total change in **d** is `(d(d)/d(e)) × (d(e)/d(a)) × Δa`. The chain rule is just: the\n",
|
||
"multiplier on Δa is the **product of the two hops**.\n",
|
||
"\n",
|
||
"#### Worked example\n",
|
||
"\n",
|
||
"Let `a = 2`, `e = a²`, `d = 3e`.\n",
|
||
"\n",
|
||
"- `d(e)/d(a) = 2a = 4` (e changes 4 units per unit of a)\n",
|
||
"- `d(d)/d(e) = 3` (d changes 3 units per unit of e)\n",
|
||
"\n",
|
||
"So `d(d)/d(a) = 3 × 4 = 12` — when **a** goes up by 1, **d** goes up by about 12.\n",
|
||
"\n",
|
||
"Check directly: `d = 3a²`, so `d(d)/d(a) = 6a = 12` at `a = 2`. Same answer.\n",
|
||
"\n",
|
||
"#### Why college notation felt confusing\n",
|
||
"\n",
|
||
"Textbooks often write `f(g(x))` and say \"derivative of outside × derivative of inside.\" That\n",
|
||
"is correct, but it hides the story: **indirect influence through a middle variable**. The relay\n",
|
||
"race picture is the same thing with names you can follow.\n",
|
||
"\n",
|
||
"#### Local rules (what each station does)\n",
|
||
"\n",
|
||
"Each operation knows its own **local** derivative:\n",
|
||
"\n",
|
||
"- for `+`: output changes 1-for-1 with each input, so the gradient just **passes through**.\n",
|
||
"- for `*`: each input's local derivative is **the other input**.\n",
|
||
"\n",
|
||
"**Backprop** = start with gradient 1.0 at the output, then walk backward multiplying by each\n",
|
||
"local derivative."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0da2822d",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 4.1 Adding gradients and local backward rules\n",
|
||
"\n",
|
||
"#### What is autograd?\n",
|
||
"\n",
|
||
"**Autograd** = **automatic differentiation** — the computer figures out gradients for you\n",
|
||
"after you write the forward math.\n",
|
||
"\n",
|
||
"Real-life picture: like a GPS that logged every turn on your drive. When you ask \"how do I get\n",
|
||
"back?\", it does not guess — it rewinds those turns and tells you how each step affected the\n",
|
||
"final destination.\n",
|
||
"\n",
|
||
"**Is it a library?** Two meanings:\n",
|
||
"\n",
|
||
"| Meaning | What it is | In this notebook |\n",
|
||
"|---|---|---|\n",
|
||
"| **The idea** | Any system that records ops, then runs `backward()` to fill `.grad` | Our homemade **micrograd** `Value` class |\n",
|
||
"| **A PyPI package** | A library literally named [`autograd`](https://github.com/HIPS/autograd) | We do **not** use it — we build from scratch |\n",
|
||
"\n",
|
||
"Later, **PyTorch** ships its own autograd (`loss.backward()` in notebooks `01` and `06`). Same\n",
|
||
"idea, industrial scale.\n",
|
||
"\n",
|
||
"When code says \"analytic gradients via autograd,\" it means: run forward through `Value` objects,\n",
|
||
"call `.backward()` once, read `.grad` — instead of nudging each input by hand with\n",
|
||
"`numeric_grad`.\n",
|
||
"\n",
|
||
"---\n",
|
||
"\n",
|
||
"Now the full `Value`. Each operation also defines a `_backward()` that says how to push the\n",
|
||
"output's gradient onto its inputs. Note we **accumulate** (`+=`) gradients, because one value\n",
|
||
"can feed into several places.\n",
|
||
"\n",
|
||
"We add `+`, `*`, `**` (power), `tanh`, and `exp` — enough to build a neural net. We also add\n",
|
||
"convenience operators (`-`, `/`, right-hand versions) so expressions read naturally.\n",
|
||
"\n",
|
||
"#### `_op` and `_prev` — the tags on every box\n",
|
||
"\n",
|
||
"Every `Value` stores two pieces of history in `__init__`:\n",
|
||
"\n",
|
||
"- **`_op`** — *what happened at this station?* A stamp on the box: `'+'` (mixed), `'*'` (scaled),\n",
|
||
" `'tanh'` (squashed), or `''` for a raw leaf input (flour, sugar — nothing done yet).\n",
|
||
"- **`_prev`** — *who gave me ingredients?* The set of parent `Value`s that fed into this one.\n",
|
||
" Like \"received from\" arrows on the tag.\n",
|
||
"\n",
|
||
"**Cookie-factory picture for `d = a * b + c`:**\n",
|
||
"\n",
|
||
"1. `e = a * b` → `e._op = '*'`, `e._prev = {a, b}` — \"made by multiplying; parents are a and b.\"\n",
|
||
"2. `d = e + c` → `d._op = '+'`, `d._prev = {e, c}` — \"made by adding; parents are e and c.\"\n",
|
||
"\n",
|
||
"A normal float is a score on a napkin: **4**. You cannot tell how you got it. A `Value` is the\n",
|
||
"same score **on a tagged box** — you can always walk back.\n",
|
||
"\n",
|
||
"**Why this matters:** forward pass moves ingredients down the line. Backprop starts at the final\n",
|
||
"score and walks **backward** along `_prev`. At each stop, `_op` tells you which local rule to\n",
|
||
"apply (add passes blame through; multiply splits blame using the other input). Without these\n",
|
||
"tags there is no map from the final number back to the knobs you need to turn.\n",
|
||
"\n",
|
||
"> **One line:** `_op` = what was done here; `_prev` = who fed into it. Together they are the\n",
|
||
"> breadcrumb trail from output back to inputs.\n",
|
||
"\n",
|
||
"**-> Training:** `tanh` is the neuron's \"squashing\" activation; its local derivative is\n",
|
||
"`1 - tanh(x)^2`."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "9433f6b8",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Value:\n",
|
||
" def __init__(self, data, _children=(), _op='', label=''):\n",
|
||
" self.data = data # the actual number (forward value)\n",
|
||
" self.grad = 0.0 # how much the final loss changes if THIS number nudges +1\n",
|
||
" self._backward = lambda: None # filled in by each op: \"push grad to my parents\"\n",
|
||
" self._prev = set(_children) # graph edges pointing backward to parents\n",
|
||
" self._op = _op # which op created this node ('+', '*', 'tanh', ...)\n",
|
||
" self.label = label # optional name for debugging / graph drawing\n",
|
||
"\n",
|
||
" def __repr__(self):\n",
|
||
" return f\"Value(data={self.data:.4f}, grad={self.grad:.4f})\"\n",
|
||
"\n",
|
||
" def __add__(self, other):\n",
|
||
" other = other if isinstance(other, Value) else Value(other) # allow Value + 3\n",
|
||
" out = Value(self.data + other.data, (self, other), '+') # forward: add numbers\n",
|
||
" def _backward():\n",
|
||
" # local rule for +: d(a+b)/da = 1, d(a+b)/db = 1 → grad flows 1:1 to both\n",
|
||
" self.grad += 1.0 * out.grad\n",
|
||
" other.grad += 1.0 * out.grad\n",
|
||
" out._backward = _backward # closure captures self/other/out — runs later in backward()\n",
|
||
" return out\n",
|
||
"\n",
|
||
" def __mul__(self, other):\n",
|
||
" other = other if isinstance(other, Value) else Value(other)\n",
|
||
" out = Value(self.data * other.data, (self, other), '*') # forward: multiply\n",
|
||
" def _backward():\n",
|
||
" # local rule for *: d(a*b)/da = b, d(a*b)/db = a (chain rule: × out.grad)\n",
|
||
" self.grad += other.data * out.grad\n",
|
||
" other.grad += self.data * out.grad\n",
|
||
" out._backward = _backward\n",
|
||
" return out\n",
|
||
"\n",
|
||
" def __pow__(self, other):\n",
|
||
" assert isinstance(other, (int, float)), \"only int/float powers\" # exponent is constant\n",
|
||
" out = Value(self.data ** other, (self,), f'**{other}') # forward: x^n\n",
|
||
" def _backward():\n",
|
||
" # d(x^n)/dx = n * x^(n-1), then chain rule × upstream grad\n",
|
||
" self.grad += other * (self.data ** (other - 1)) * out.grad\n",
|
||
" out._backward = _backward\n",
|
||
" return out\n",
|
||
"\n",
|
||
" def tanh(self):\n",
|
||
" x = self.data\n",
|
||
" t = (math.exp(2 * x) - 1) / (math.exp(2 * x) + 1) # forward tanh (squash to -1..1)\n",
|
||
" out = Value(t, (self,), 'tanh')\n",
|
||
" def _backward():\n",
|
||
" # d tanh(x)/dx = 1 - tanh(x)^2 (use saved t, not recompute from self.data)\n",
|
||
" self.grad += (1 - t ** 2) * out.grad\n",
|
||
" out._backward = _backward\n",
|
||
" return out\n",
|
||
"\n",
|
||
" def exp(self):\n",
|
||
" out = Value(math.exp(self.data), (self,), 'exp') # forward: e^x\n",
|
||
" def _backward():\n",
|
||
" # d e^x / dx = e^x — out.data IS e^x at forward time\n",
|
||
" self.grad += out.data * out.grad\n",
|
||
" out._backward = _backward\n",
|
||
" return out\n",
|
||
"\n",
|
||
" # --- convenience ops so expressions like 3 + a or a / b work naturally ---\n",
|
||
" def __neg__(self):\n",
|
||
" return self * -1 # unary minus via multiply by -1\n",
|
||
"\n",
|
||
" def __sub__(self, other):\n",
|
||
" return self + (-other if isinstance(other, Value) else Value(-other)) # a - b = a + (-b)\n",
|
||
"\n",
|
||
" def __radd__(self, other):\n",
|
||
" return self + other # handles 3 + a (Python calls __radd__ on a)\n",
|
||
"\n",
|
||
" def __rmul__(self, other):\n",
|
||
" return self * other # handles 3 * a\n",
|
||
"\n",
|
||
" def __truediv__(self, other):\n",
|
||
" other = other if isinstance(other, Value) else Value(other)\n",
|
||
" return self * other ** -1 # a / b = a * b^(-1)\n",
|
||
"\n",
|
||
" def backward(self):\n",
|
||
" # Step 1: list every node in the graph, parents before children (topological sort).\n",
|
||
" # We walk _prev links; reversed(topo) then visits output → inputs.\n",
|
||
" topo = []\n",
|
||
" visited = set()\n",
|
||
" def build_topo(v):\n",
|
||
" if v not in visited:\n",
|
||
" visited.add(v)\n",
|
||
" for child in v._prev: # visit all ancestors first\n",
|
||
" build_topo(child)\n",
|
||
" topo.append(v) # post-order: children before parent\n",
|
||
" build_topo(self) # start from the output node (e.g. loss)\n",
|
||
"\n",
|
||
" # Step 2: seed — \"if output goes up 1, output goes up 1\" (dL/dL = 1).\n",
|
||
" self.grad = 1.0\n",
|
||
"\n",
|
||
" # Step 3: reverse sweep; each node runs its local _backward (chain rule × parents).\n",
|
||
" for node in reversed(topo): # output first, leaves last\n",
|
||
" node._backward() # each op pushes blame to its _prev parents\n",
|
||
"\n",
|
||
"\n",
|
||
"print(\"full Value class ready\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "7f14c265",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 4.2 One backward pass on the tiny expression\n",
|
||
"\n",
|
||
"Rebuild `d = a*b + c` with the full class, call `d.backward()` once, and read the gradients.\n",
|
||
"They should match the nudge-and-measure numbers from section 2.1 (`a.grad = b = -3`,\n",
|
||
"`b.grad = a = 2`, `c.grad = 1`)."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "9d1e739c",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"a = Value(2.0, label='a') # leaf inputs — knobs with no parents yet\n",
|
||
"b = Value(-3.0, label='b')\n",
|
||
"c = Value(10.0, label='c')\n",
|
||
"e = a * b # builds multiply node; graph remembers a, b\n",
|
||
"d = e + c # builds add node; graph remembers e, c\n",
|
||
"\n",
|
||
"d.backward() # one backward pass fills .grad on every node in the graph\n",
|
||
"\n",
|
||
"print(\"d =\", d.data) # forward answer still 4.0\n",
|
||
"print(\"a.grad =\", a.grad, \" (expect -3)\") # ∂d/∂a = b\n",
|
||
"print(\"b.grad =\", b.grad, \" (expect 2)\") # ∂d/∂b = a\n",
|
||
"print(\"c.grad =\", c.grad, \" (expect 1)\") # ∂d/∂c = 1"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "0083fec0",
|
||
"metadata": {},
|
||
"source": [
|
||
"Same graph **after backprop** — orange gradient numbers show how much each node would nudge the\n",
|
||
"final output. Hover `a` and notice its grad equals `b` (−3): the multiply rule in action."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "c14c82ca",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"show_graph(d, title=\"After backward() — gradients filled in\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "1ac38e12",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 4.3 Trust but verify — numeric gradient check\n",
|
||
"\n",
|
||
"A good habit: confirm the analytic gradient (from `backward()`) matches the slow\n",
|
||
"nudge-and-measure gradient. If they agree, the engine is correct.\n",
|
||
"\n",
|
||
"**Real-life picture:** change one recipe ingredient by a tiny pinch, cook again, and see\n",
|
||
"how much the final score moves. That ratio — *change in score ÷ size of pinch* — is the\n",
|
||
"gradient for that ingredient. Repeat for each ingredient, one at a time. Slow, but it is\n",
|
||
"the definition of \"sensitivity.\"\n",
|
||
"\n",
|
||
"**Two ways to measure the same hill**\n",
|
||
"\n",
|
||
"| | Nudge-and-measure (numeric) | `backward()` (analytic) |\n",
|
||
"|---|---|---|\n",
|
||
"| Idea | Tiny change in one input → re-run forward | Chain rule walks the graph once |\n",
|
||
"| Cost | One forward pass **per input** | One backward pass for **all** inputs |\n",
|
||
"| Result | slope ≈ `(f(x+h) − f(x)) / h` | `.grad` on each leaf `Value` |\n",
|
||
"\n",
|
||
"**Outline — nudge-and-measure (what `numeric_grad` does)**\n",
|
||
"\n",
|
||
"1. Run the same math as the graph, but on plain floats (no `Value` objects).\n",
|
||
"2. Pick a tiny step `h` (e.g. `1e-6`).\n",
|
||
"3. Save the baseline output at the current inputs.\n",
|
||
"4. For **each** input `i`, one at a time:\n",
|
||
" - copy the inputs;\n",
|
||
" - add `h` only to input `i`;\n",
|
||
" - re-run forward;\n",
|
||
" - slope = (new output − baseline) / `h`.\n",
|
||
"5. That slope is the numeric gradient for input `i`.\n",
|
||
"\n",
|
||
"**Outline — analytic check (what we trust after verifying)**\n",
|
||
"\n",
|
||
"1. Build the expression with `Value` objects (the graph remembers parents via `_prev`).\n",
|
||
"2. Call `.backward()` once on the final output.\n",
|
||
"3. Read `.grad` on each leaf — chain rule applied in one reverse sweep.\n",
|
||
"\n",
|
||
"**Compare:** print analytic vs numeric side by side; they **match** when\n",
|
||
"`|analytic − numeric| < 1e-4`. Same hill, two rulers — if they agree, the engine's local\n",
|
||
"`_backward()` rules are wired correctly."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "fe4b711e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"def numeric_grad(f, inputs, i, h=1e-6):\n",
|
||
" base = f(inputs) # output at current inputs\n",
|
||
" bumped = list(inputs) # copy so we don't mutate the original list\n",
|
||
" bumped[i] += h # nudge only input i by a tiny amount\n",
|
||
" return (f(bumped) - base) / h # slope = rise / run\n",
|
||
"\n",
|
||
"f = lambda v: (v[0] * v[1]) + v[2] # same math as a*b+c but on plain floats\n",
|
||
"inputs = [2.0, -3.0, 10.0]\n",
|
||
"\n",
|
||
"a = Value(2.0); b = Value(-3.0); c = Value(10.0)\n",
|
||
"(a * b + c).backward() # analytic gradients via autograd\n",
|
||
"\n",
|
||
"for i, (name, val) in enumerate(zip(\"abc\", (a, b, c))):\n",
|
||
" ng = numeric_grad(f, inputs, i) # slow finite-difference gradient for input i\n",
|
||
" print(f\"{name}: analytic={val.grad:+.4f} numeric={ng:+.4f} match={abs(val.grad-ng)<1e-4}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "6518159c",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Your turn 4** — Build `g = (a*b + c).tanh()` with `a=0.5, b=2.0, c=-1.0`, call\n",
|
||
"`g.backward()`, and check `a.grad` against a numeric nudge. (Hint: reuse `numeric_grad`\n",
|
||
"with `f = lambda v: math.tanh(v[0]*v[1] + v[2])`.)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "d6bea75d",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# Your turn 4 — fill in and run\n",
|
||
"a = Value(0.5); b = Value(2.0); c = Value(-1.0) # inputs\n",
|
||
"g = (a * b + c).tanh() # forward: multiply, add, squash\n",
|
||
"g.backward() # backprop through tanh, +, *\n",
|
||
"\n",
|
||
"f = lambda v: math.tanh(v[0]*v[1] + v[2]) # same formula on plain floats\n",
|
||
"ng_a = numeric_grad(f, [0.5, 2.0, -1.0], 0) # numeric ∂g/∂a (nudge index 0)\n",
|
||
"print(f\"a.grad analytic = {a.grad:+.4f} numeric = {ng_a:+.4f}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "fef7ef8b",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 5.1 Neuron -> Layer -> MLP, all from `Value`\n",
|
||
"\n",
|
||
"Everything below is still built from plain `Value` objects — the same tagged boxes and\n",
|
||
"`backward()` trail from section 4. Here we stack them into a small **neural network**.\n",
|
||
"\n",
|
||
"#### One story to hold in your head: renting an apartment\n",
|
||
"\n",
|
||
"You apply with two numbers on the form: **monthly income** and **credit score**.\n",
|
||
"The landlord wants one answer: *likely good tenant* vs *likely risky*.\n",
|
||
"\n",
|
||
"**One neuron = one reviewer on the panel**\n",
|
||
"\n",
|
||
"A single neuron looks at your whole application and gives **one opinion**, squashed to a\n",
|
||
"number between -1 and +1:\n",
|
||
"\n",
|
||
"`tanh(w · x + b)` means:\n",
|
||
"\n",
|
||
"- **`w`** — how much this reviewer cares about each field (income vs credit). Big weight =\n",
|
||
" \"this matters a lot to me.\"\n",
|
||
"- **`b`** — this reviewer's baseline strictness before seeing you (always picky, or always\n",
|
||
" lenient).\n",
|
||
"- **`w · x + b`** — weighted score: multiply each input by its importance, add bias.\n",
|
||
"- **`tanh(...)`** — squash the score so it cannot run away to infinity; strong yes or strong no,\n",
|
||
" but bounded (like capping enthusiasm at \"very pro\" or \"very against\").\n",
|
||
"\n",
|
||
"So one neuron is not magic — it is **one weighted vote**, softened at the edges.\n",
|
||
"\n",
|
||
"**One layer = a whole panel, all reading the same application**\n",
|
||
"\n",
|
||
"A **layer** is several neurons in **parallel**. Each sees the **same** inputs `x`, but each\n",
|
||
"has its **own** weights and bias — different reviewers, different priorities:\n",
|
||
"\n",
|
||
"- Reviewer A might care mostly about income.\n",
|
||
"- Reviewer B might care mostly about credit.\n",
|
||
"- Reviewer C might look for a balance of both.\n",
|
||
"\n",
|
||
"The layer's output is a **list of opinions** (one per neuron). Eight neurons → eight numbers\n",
|
||
"describing the application from eight angles.\n",
|
||
"\n",
|
||
"**An MLP = panels stacked in stages**\n",
|
||
"\n",
|
||
"An **MLP** (multi-layer perceptron) feeds one layer's opinions into the next:\n",
|
||
"\n",
|
||
"1. **First hidden layer** — junior reviewers turn raw form fields into rough themes (\"cash-flow\n",
|
||
" looks fine\", \"credit is shaky\", …).\n",
|
||
"2. **Second hidden layer** — senior reviewers debate those themes, not the raw form.\n",
|
||
"3. **Output layer** — one final judge reads the debate and emits a single score (here: one\n",
|
||
" neuron → one number for the two-moons task).\n",
|
||
"\n",
|
||
"That is why the code uses shapes like `MLP(2, [8, 8, 1)`:\n",
|
||
"\n",
|
||
"| Piece | In the story | In code |\n",
|
||
"|---|---|---|\n",
|
||
"| 2 inputs | income + credit (here: x, y on the plot) | `nin = 2` |\n",
|
||
"| 8 + 8 hidden | two panels of 8 reviewers | two `Layer`s with `nout=8` |\n",
|
||
"| 1 output | final hire / no-hire score | last layer with `nout=1` |\n",
|
||
"\n",
|
||
"Data flows **forward** only: form → junior panel → senior panel → final judge.\n",
|
||
"Every multiply, add, and `tanh` is still a `Value` node, so `loss.backward()` can later\n",
|
||
"blame the right weight when the answer was wrong.\n",
|
||
"\n",
|
||
"#### The three building blocks (code ↔ picture)\n",
|
||
"\n",
|
||
"| Class | What it is | Real-life |\n",
|
||
"|---|---|---|\n",
|
||
"| **`Neuron`** | one weighted vote + squash | one reviewer |\n",
|
||
"| **`Layer`** | list of neurons, same inputs | one panel sitting together |\n",
|
||
"| **`MLP`** | layers wired in sequence | whole hiring process, stage by stage |\n",
|
||
"\n",
|
||
"**-> Training:** `parameters()` collects every weight and bias in the network — all the knobs\n",
|
||
"on all reviewers' score sheets. Section 6 nudges those knobs downhill until the final judge\n",
|
||
"gets the moons right."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": 6,
|
||
"id": "93e51095",
|
||
"metadata": {},
|
||
"outputs": [
|
||
{
|
||
"data": {
|
||
"text/html": [
|
||
"\n",
|
||
" <iframe\n",
|
||
" width=\"100%\"\n",
|
||
" height=\"640\"\n",
|
||
" src=\"data:text/html;base64,PCFET0NUWVBFIGh0bWw+CjxodG1sIGxhbmc9ImVuIj4KPGhlYWQ+CjxtZXRhIGNoYXJzZXQ9IlVURi04IiAvPgo8bWV0YSBuYW1lPSJ2aWV3cG9ydCIgY29udGVudD0id2lkdGg9ZGV2aWNlLXdpZHRoLCBpbml0aWFsLXNjYWxlPTEuMCIgLz4KPHRpdGxlPk1MUCBQbGF5Z3JvdW5kIOKAlCBOZXVyb24g4oaSIExheWVyIOKGkiBNTFAgKHNlY3Rpb24gNS4xKTwvdGl0bGU+CjxzY3JpcHQgc3JjPSJodHRwczovL2Nkbi5qc2RlbGl2ci5uZXQvbnBtL2QzQDciPjwvc2NyaXB0Pgo8c3R5bGU+CiAgOnJvb3QgewogICAgLS1iZzogIzBmMTQyMDsKICAgIC0tcGFuZWw6ICMxNjFkMmU7CiAgICAtLXBhbmVsLTI6ICMxZDI5NDA7CiAgICAtLWluazogI2U4ZWRmNjsKICAgIC0tbXV0ZWQ6ICM5NGEzYjg7CiAgICAtLWxpbmU6ICMyYTM1NTA7CiAgICAtLXBvczogIzRhZGU4MDsgICAvKiBwb3NpdGl2ZSB3ZWlnaHQgLyBhY3RpdmF0aW9uICovCiAgICAtLW5lZzogI2Y4NzE3MTsgICAvKiBuZWdhdGl2ZSB3ZWlnaHQgLyBhY3RpdmF0aW9uICovCiAgICAtLWFjY2VudDogIzYwYTVmYTsKICAgIC0tYWNjZW50LTI6ICNmYmJmMjQ7CiAgfQogICogeyBib3gtc2l6aW5nOiBib3JkZXItYm94OyB9CiAgaHRtbCwgYm9keSB7IG1hcmdpbjogMDsgcGFkZGluZzogMDsgfQogIGJvZHkgewogICAgYmFja2dyb3VuZDogcmFkaWFsLWdyYWRpZW50KDEyMDBweCA2MDBweCBhdCA3MCUgLTEwJSwgIzFiMjc0MCAwJSwgdmFyKC0tYmcpIDU1JSk7CiAgICBjb2xvcjogdmFyKC0taW5rKTsKICAgIGZvbnQtZmFtaWx5OiAiU2Vnb2UgVUkiLCBzeXN0ZW0tdWksIC1hcHBsZS1zeXN0ZW0sIFJvYm90bywgc2Fucy1zZXJpZjsKICAgIGxpbmUtaGVpZ2h0OiAxLjU7CiAgICBwYWRkaW5nOiAyMnB4IGNsYW1wKDE0cHgsIDN2dywgNDBweCkgNjBweDsKICB9CiAgaDEgeyBmb250LXNpemU6IGNsYW1wKDIwcHgsIDIuNnZ3LCAyOHB4KTsgbWFyZ2luOiAwIDAgNHB4OyB9CiAgLnN1YiB7IGNvbG9yOiB2YXIoLS1tdXRlZCk7IG1hcmdpbjogMCAwIDE4cHg7IG1heC13aWR0aDogNzBjaDsgfQogIC5zdWIgYiB7IGNvbG9yOiB2YXIoLS1pbmspOyB9CiAgLmxheW91dCB7IGRpc3BsYXk6IGdyaWQ7IGdyaWQtdGVtcGxhdGUtY29sdW1uczogMzAwcHggMWZyOyBnYXA6IDE4cHg7IGFsaWduLWl0ZW1zOiBzdGFydDsgfQogIEBtZWRpYSAobWF4LXdpZHRoOiA5MDBweCkgeyAubGF5b3V0IHsgZ3JpZC10ZW1wbGF0ZS1jb2x1bW5zOiAxZnI7IH0gfQoKICAucGFuZWwgewogICAgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KDE4MGRlZywgdmFyKC0tcGFuZWwpIDAlLCB2YXIoLS1wYW5lbC0yKSAxMDAlKTsKICAgIGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWxpbmUpOwogICAgYm9yZGVyLXJhZGl1czogMTRweDsKICAgIHBhZGRpbmc6IDE2cHggMTZweCAxOHB4OwogICAgYm94LXNoYWRvdzogMCAxMHB4IDMwcHggcmdiYSgwLDAsMCwuMzUpOwogIH0KICAucGFuZWwgaDIgeyBmb250LXNpemU6IDE0cHg7IHRleHQtdHJhbnNmb3JtOiB1cHBlcmNhc2U7IGxldHRlci1zcGFjaW5nOiAuMDhlbTsgY29sb3I6IHZhcigtLW11dGVkKTsgbWFyZ2luOiAwIDAgMTJweDsgfQoKICAuZmllbGQgeyBtYXJnaW4tYm90dG9tOiAxNnB4OyB9CiAgLmZpZWxkIGxhYmVsIHsgZGlzcGxheTogZmxleDsganVzdGlmeS1jb250ZW50OiBzcGFjZS1iZXR3ZWVuOyBmb250LXNpemU6IDEzcHg7IG1hcmdpbi1ib3R0b206IDZweDsgfQogIC5maWVsZCBsYWJlbCAudmFsIHsgZm9udC12YXJpYW50LW51bWVyaWM6IHRhYnVsYXItbnVtczsgY29sb3I6IHZhcigtLWFjY2VudCk7IGZvbnQtd2VpZ2h0OiA2MDA7IH0KICBpbnB1dFt0eXBlPXJhbmdlXSB7IHdpZHRoOiAxMDAlOyBhY2NlbnQtY29sb3I6IHZhcigtLWFjY2VudCk7IH0KCiAgLmJ0biB7CiAgICBhcHBlYXJhbmNlOiBub25lOyBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1saW5lKTsgY3Vyc29yOiBwb2ludGVyOwogICAgYmFja2dyb3VuZDogdmFyKC0tcGFuZWwtMik7IGNvbG9yOiB2YXIoLS1pbmspOwogICAgcGFkZGluZzogOXB4IDEycHg7IGJvcmRlci1yYWRpdXM6IDlweDsgZm9udC1zaXplOiAxM3B4OyBmb250LXdlaWdodDogNjAwOwogICAgdHJhbnNpdGlvbjogLjE1czsgd2lkdGg6IDEwMCU7IG1hcmdpbi1ib3R0b206IDlweDsKICB9CiAgLmJ0bjpob3ZlciB7IGJvcmRlci1jb2xvcjogdmFyKC0tYWNjZW50KTsgY29sb3I6ICNmZmY7IH0KICAuYnRuLnByaW1hcnkgeyBiYWNrZ3JvdW5kOiBsaW5lYXItZ3JhZGllbnQoMTgwZGVnLCAjM2I4MmY2LCAjMjU2M2ViKTsgYm9yZGVyLWNvbG9yOiAjMjU2M2ViOyB9CiAgLmJ0bi5wcmltYXJ5OmhvdmVyIHsgYmFja2dyb3VuZDogbGluZWFyLWdyYWRpZW50KDE4MGRlZywgIzRmOTNmZiwgIzJmNmZlMCk7IH0KCiAgLmxlZ2VuZCB7IGRpc3BsYXk6IGZsZXg7IGZsZXgtZGlyZWN0aW9uOiBjb2x1bW47IGdhcDogN3B4OyBmb250LXNpemU6IDEyLjVweDsgY29sb3I6IHZhcigtLW11dGVkKTsgfQogIC5sZWdlbmQgLnJvdyB7IGRpc3BsYXk6IGZsZXg7IGFsaWduLWl0ZW1zOiBjZW50ZXI7IGdhcDogOHB4OyB9CiAgLnN3YXRjaCB7IHdpZHRoOiAyMnB4OyBoZWlnaHQ6IDRweDsgYm9yZGVyLXJhZGl1czogM3B4OyB9CgogIC5zdGFnZSB7IGJhY2tncm91bmQ6IHJnYmEoMCwwLDAsLjE4KTsgYm9yZGVyLXJhZGl1czogMTRweDsgfQogIHN2ZyB7IGRpc3BsYXk6IGJsb2NrOyB3aWR0aDogMTAwJTsgaGVpZ2h0OiBhdXRvOyB9CgogIC5vdXQtY2FyZCB7CiAgICBtYXJnaW4tdG9wOiAxNHB4OyBwYWRkaW5nOiAxNHB4IDE2cHg7IGJvcmRlci1yYWRpdXM6IDEycHg7CiAgICBib3JkZXI6IDFweCBzb2xpZCB2YXIoLS1saW5lKTsgYmFja2dyb3VuZDogcmdiYSgwLDAsMCwuMik7CiAgICBkaXNwbGF5OiBmbGV4OyBhbGlnbi1pdGVtczogY2VudGVyOyBqdXN0aWZ5LWNvbnRlbnQ6IHNwYWNlLWJldHdlZW47IGdhcDogMTRweDsKICB9CiAgLm91dC1jYXJkIC5sYWJlbCB7IGNvbG9yOiB2YXIoLS1tdXRlZCk7IGZvbnQtc2l6ZTogMTNweDsgfQogIC5vdXQtY2FyZCAudmVyZGljdCB7IGZvbnQtc2l6ZTogMjZweDsgZm9udC13ZWlnaHQ6IDgwMDsgZm9udC12YXJpYW50LW51bWVyaWM6IHRhYnVsYXItbnVtczsgfQoKICAvKiB0b29sdGlwICovCiAgLnRpcCB7CiAgICBwb3NpdGlvbjogZml4ZWQ7IHBvaW50ZXItZXZlbnRzOiBub25lOyB6LWluZGV4OiA1MDsgb3BhY2l0eTogMDsKICAgIGJhY2tncm91bmQ6ICMwYjEzMjI7IGJvcmRlcjogMXB4IHNvbGlkIHZhcigtLWFjY2VudCk7IGNvbG9yOiB2YXIoLS1pbmspOwogICAgcGFkZGluZzogOHB4IDEwcHg7IGJvcmRlci1yYWRpdXM6IDlweDsgZm9udC1zaXplOiAxMi41cHg7IG1heC13aWR0aDogMjQwcHg7CiAgICBib3gtc2hhZG93OiAwIDhweCAyNHB4IHJnYmEoMCwwLDAsLjUpOyB0cmFuc2l0aW9uOiBvcGFjaXR5IC4xMnM7CiAgfQogIC50aXAgYiB7IGNvbG9yOiB2YXIoLS1hY2NlbnQpOyB9CiAgLnRpcCAubW9ubyB7IGZvbnQtZmFtaWx5OiB1aS1tb25vc3BhY2UsICJDYXNjYWRpYSBDb2RlIiwgQ29uc29sYXMsIG1vbm9zcGFjZTsgfQoKICAvKiBub2RlICsgZWRnZSBiYXNlIHN0eWxlcyAqLwogIC5lZGdlIHsgdHJhbnNpdGlvbjogc3Ryb2tlLW9wYWNpdHkgLjJzOyB9CiAgLm5vZGUgY2lyY2xlIHsgc3Ryb2tlOiAjMGIxMzIyOyBzdHJva2Utd2lkdGg6IDEuNXB4OyBjdXJzb3I6IHBvaW50ZXI7IH0KICAubm9kZS1sYWJlbCB7IGZvbnQtc2l6ZTogMTFweDsgZmlsbDogdmFyKC0tbXV0ZWQpOyB9CiAgLmxheWVyLXRpdGxlIHsgZm9udC1zaXplOiAxMnB4OyBmaWxsOiB2YXIoLS1pbmspOyBmb250LXdlaWdodDogNzAwOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyB9CiAgLmxheWVyLWNhcCB7IGZvbnQtc2l6ZTogMTFweDsgZmlsbDogdmFyKC0tbXV0ZWQpOyB0ZXh0LWFuY2hvcjogbWlkZGxlOyB9CiAgLmhpbnQgeyBjb2xvcjogdmFyKC0tbXV0ZWQpOyBmb250LXNpemU6IDEycHg7IG1hcmdpbi10b3A6IDEwcHg7IH0KICAucGlsbCB7IGRpc3BsYXk6aW5saW5lLWJsb2NrOyBwYWRkaW5nOjJweCA3cHg7IGJvcmRlcjoxcHggc29saWQgdmFyKC0tbGluZSk7IGJvcmRlci1yYWRpdXM6OTk5cHg7IGZvbnQtc2l6ZToxMXB4OyBjb2xvcjp2YXIoLS1tdXRlZCk7IH0KPC9zdHlsZT4KPC9oZWFkPgo8Ym9keT4KICA8aDE+TUxQIFBsYXlncm91bmQg4oCUIDxzcGFuIHN0eWxlPSJjb2xvcjp2YXIoLS1hY2NlbnQpIj5OZXVyb24g4oaSIExheWVyIOKGkiBNTFA8L3NwYW4+PC9oMT4KICA8cCBjbGFzcz0ic3ViIj4KICAgIFRoZSByZXZpZXdlci1wYW5lbCBzdG9yeSBmcm9tIDxiPnNlY3Rpb24gNS4xPC9iPiwgbWFkZSBsaXZlLiBUd28gaW5wdXRzCiAgICAoPGI+aW5jb21lPC9iPiwgPGI+Y3JlZGl0PC9iPikgZmxvdyB0aHJvdWdoIHR3byBwYW5lbHMgb2YgOCByZXZpZXdlcnMgaW50byBvbmUgZmluYWwganVkZ2U6CiAgICB0aGUgPHNwYW4gY2xhc3M9InBpbGwiPjIg4oaSIDgg4oaSIDgg4oaSIDE8L3NwYW4+IG5ldHdvcmsuIEV2ZXJ5IGxpbmUgaXMgYSB3ZWlnaHQsIGV2ZXJ5IGRvdCBpcyBhCiAgICA8c3BhbiBjbGFzcz0ibW9ubyI+dGFuaCh3wrd4ICsgYik8L3NwYW4+IG9waW5pb24uIERyYWcgdGhlIGlucHV0cywgaG92ZXIgYW55dGhpbmcsIG9yIHByZXNzIHBsYXkuCiAgPC9wPgoKICA8ZGl2IGNsYXNzPSJsYXlvdXQiPgogICAgPGRpdiBjbGFzcz0icGFuZWwiPgogICAgICA8aDI+SW5wdXRzICh0aGUgYXBwbGljYXRpb24pPC9oMj4KICAgICAgPGRpdiBjbGFzcz0iZmllbGQiPgogICAgICAgIDxsYWJlbD5JbmNvbWUgKHjigoEpIDxzcGFuIGNsYXNzPSJ2YWwiIGlkPSJ4MXYiPjAuNTA8L3NwYW4+PC9sYWJlbD4KICAgICAgICA8aW5wdXQgdHlwZT0icmFuZ2UiIGlkPSJ4MSIgbWluPSItMSIgbWF4PSIxIiBzdGVwPSIwLjAxIiB2YWx1ZT0iMC41IiAvPgogICAgICA8L2Rpdj4KICAgICAgPGRpdiBjbGFzcz0iZmllbGQiPgogICAgICAgIDxsYWJlbD5DcmVkaXQgKHjigoIpIDxzcGFuIGNsYXNzPSJ2YWwiIGlkPSJ4MnYiPi0wLjMwPC9zcGFuPjwvbGFiZWw+CiAgICAgICAgPGlucHV0IHR5cGU9InJhbmdlIiBpZD0ieDIiIG1pbj0iLTEiIG1heD0iMSIgc3RlcD0iMC4wMSIgdmFsdWU9Ii0wLjMiIC8+CiAgICAgIDwvZGl2PgoKICAgICAgPGgyIHN0eWxlPSJtYXJnaW4tdG9wOjZweCI+Q29udHJvbHM8L2gyPgogICAgICA8YnV0dG9uIGNsYXNzPSJidG4gcHJpbWFyeSIgaWQ9InBsYXkiPuKWtiBBbmltYXRlIGZvcndhcmQgcGFzczwvYnV0dG9uPgogICAgICA8YnV0dG9uIGNsYXNzPSJidG4iIGlkPSJyZXJvbGwiPvCfjrIgTmV3IHJhbmRvbSB3ZWlnaHRzPC9idXR0b24+CiAgICAgIDxidXR0b24gY2xhc3M9ImJ0biIgaWQ9InJlc2V0Ij7ihrogUmVzZXQgaW5wdXRzPC9idXR0b24+CgogICAgICA8aDIgc3R5bGU9Im1hcmdpbi10b3A6MThweCI+UmVhZCB0aGUgY29sb3JzPC9oMj4KICAgICAgPGRpdiBjbGFzcz0ibGVnZW5kIj4KICAgICAgICA8ZGl2IGNsYXNzPSJyb3ciPjxzcGFuIGNsYXNzPSJzd2F0Y2giIHN0eWxlPSJiYWNrZ3JvdW5kOnZhcigtLXBvcykiPjwvc3Bhbj4gcG9zaXRpdmUgd2VpZ2h0IC8gYWN0aXZhdGlvbjwvZGl2PgogICAgICAgIDxkaXYgY2xhc3M9InJvdyI+PHNwYW4gY2xhc3M9InN3YXRjaCIgc3R5bGU9ImJhY2tncm91bmQ6dmFyKC0tbmVnKSI+PC9zcGFuPiBuZWdhdGl2ZSB3ZWlnaHQgLyBhY3RpdmF0aW9uPC9kaXY+CiAgICAgICAgPGRpdiBjbGFzcz0icm93Ij48c3BhbiBjbGFzcz0ic3dhdGNoIiBzdHlsZT0iYmFja2dyb3VuZDp2YXIoLS1tdXRlZCk7aGVpZ2h0OjJweCI+PC9zcGFuPiB0aGluIGxpbmUgPSB3ZWFrLCB0aGljayA9IHN0cm9uZzwvZGl2PgogICAgICA8L2Rpdj4KCiAgICAgIDxkaXYgY2xhc3M9Im91dC1jYXJkIj4KICAgICAgICA8c3BhbiBjbGFzcz0ibGFiZWwiPkZpbmFsIGp1ZGdlPGJyPnNjb3JlICh0YW5oKTwvc3Bhbj4KICAgICAgICA8c3BhbiBjbGFzcz0idmVyZGljdCIgaWQ9InZlcmRpY3QiPuKAlDwvc3Bhbj4KICAgICAgPC9kaXY+CiAgICAgIDxwIGNsYXNzPSJoaW50IiBpZD0idmVyZGljdC10ZXh0Ij5DbG9zZXIgdG8gPGIgc3R5bGU9ImNvbG9yOnZhcigtLXBvcykiPisxPC9iPiA9IGxpa2VseSBnb29kIHRlbmFudCwgY2xvc2VyIHRvIDxiIHN0eWxlPSJjb2xvcjp2YXIoLS1uZWcpIj7iiJIxPC9iPiA9IGxpa2VseSByaXNreS48L3A+CiAgICA8L2Rpdj4KCiAgICA8ZGl2IGNsYXNzPSJwYW5lbCBzdGFnZSI+CiAgICAgIDxzdmcgaWQ9Im5ldCIgdmlld0JveD0iMCAwIDk4MCA1NjAiIHByZXNlcnZlQXNwZWN0UmF0aW89InhNaWRZTWlkIG1lZXQiIGFyaWEtbGFiZWw9Ik1MUCBkaWFncmFtIj48L3N2Zz4KICAgICAgPHAgY2xhc3M9ImhpbnQiIHN0eWxlPSJwYWRkaW5nOjAgNnB4IDRweCI+CiAgICAgICAgSG92ZXIgYSA8Yj5saW5lPC9iPiB0byBzZWUgaXRzIHdlaWdodCwgb3IgYSA8Yj5kb3Q8L2I+IHRvIHNlZSB0aGF0IHJldmlld2VyJ3MKICAgICAgICA8c3BhbiBjbGFzcz0ibW9ubyI+d8K3eCArIGI8L3NwYW4+IGFuZCBzcXVhc2hlZCBvcGluaW9uLiBMYXllcnMgbGVmdOKGknJpZ2h0OiBmb3JtIOKGkiBqdW5pb3IgcGFuZWwg4oaSIHNlbmlvciBwYW5lbCDihpIgZmluYWwganVkZ2UuCiAgICAgIDwvcD4KICAgIDwvZGl2PgogIDwvZGl2PgoKICA8ZGl2IGNsYXNzPSJ0aXAiIGlkPSJ0aXAiPjwvZGl2PgoKPHNjcmlwdD4KLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBUaW55IE1MUCwgZmFpdGhmdWwgdG8gc2VjdGlvbiA1LjE6IE5ldXJvbiA9IHRhbmgod8K3eCArIGIpLCBMYXllciA9IG5ldXJvbnMgaW4KLy8gcGFyYWxsZWwsIE1MUCA9IGxheWVycyBpbiBzZXF1ZW5jZS4gU2hhcGU6IDIgLT4gOCAtPiA4IC0+IDEuCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KY29uc3QgU0laRVMgPSBbMiwgOCwgOCwgMV07CmNvbnN0IExBWUVSX1RJVExFUyA9IFsiSW5wdXRzIiwgIkhpZGRlbiBwYW5lbCAxIiwgIkhpZGRlbiBwYW5lbCAyIiwgIkZpbmFsIGp1ZGdlIl07CmNvbnN0IExBWUVSX0NBUFMgICA9IFsiaW5jb21lLCBjcmVkaXQiLCAiOCBqdW5pb3IgcmV2aWV3ZXJzIiwgIjggc2VuaW9yIHJldmlld2VycyIsICIxIHNjb3JlIl07Cgpjb25zdCByYW5kID0gKCkgPT4gTWF0aC5yYW5kb20oKSAqIDIgLSAxOyAgICAgICAgICAvLyB1bmlmb3JtKC0xLCAxKSwgbGlrZSBtaWNyb2dyYWQKY29uc3QgdGFuaCA9ICh6KSA9PiBNYXRoLnRhbmgoeik7CgovLyBCdWlsZCBuZXR3b3JrOiB3ZWlnaHRzW2xdIGlzIG1hdHJpeCBbbm91dCB4IG5pbl0sIGJpYXNlc1tsXSBpcyBbbm91dF0uCmxldCB3ZWlnaHRzLCBiaWFzZXM7CmZ1bmN0aW9uIGJ1aWxkTmV0d29yaygpIHsKICB3ZWlnaHRzID0gW107IGJpYXNlcyA9IFtdOwogIGZvciAobGV0IGwgPSAwOyBsIDwgU0laRVMubGVuZ3RoIC0gMTsgbCsrKSB7CiAgICBjb25zdCBuaW4gPSBTSVpFU1tsXSwgbm91dCA9IFNJWkVTW2wgKyAxXTsKICAgIHdlaWdodHMucHVzaChBcnJheS5mcm9tKHtsZW5ndGg6IG5vdXR9LCAoKSA9PiBBcnJheS5mcm9tKHtsZW5ndGg6IG5pbn0sIHJhbmQpKSk7CiAgICBiaWFzZXMucHVzaChBcnJheS5mcm9tKHtsZW5ndGg6IG5vdXR9LCByYW5kKSk7CiAgfQp9CmJ1aWxkTmV0d29yaygpOwoKLy8gRm9yd2FyZCBwYXNzLiBSZXR1cm5zIGFjdGl2YXRpb25zIHBlciBsYXllciAobGF5ZXIgMCA9IHJhdyBpbnB1dHMpIGFuZCB0aGUKLy8gcHJlLWFjdGl2YXRpb24gKHfCt3grYikgZm9yIGVhY2ggbmV1cm9uIHNvIHRoZSB0b29sdGlwIGNhbiBzaG93IGJvdGguCmZ1bmN0aW9uIGZvcndhcmQoeCkgewogIGNvbnN0IGFjdHMgPSBbeC5zbGljZSgpXTsKICBjb25zdCBwcmVhY3RzID0gW3guc2xpY2UoKV07ICAgLy8gaW5wdXRzIGhhdmUgbm8gcHJlLWFjdGl2YXRpb247IG1pcnJvciBmb3IgaW5kZXhpbmcKICBsZXQgY3VyID0geC5zbGljZSgpOwogIGZvciAobGV0IGwgPSAwOyBsIDwgd2VpZ2h0cy5sZW5ndGg7IGwrKykgewogICAgY29uc3QgeiA9IFtdLCBhID0gW107CiAgICBmb3IgKGxldCBqID0gMDsgaiA8IHdlaWdodHNbbF0ubGVuZ3RoOyBqKyspIHsKICAgICAgbGV0IHMgPSBiaWFzZXNbbF1bal07CiAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgY3VyLmxlbmd0aDsgaSsrKSBzICs9IHdlaWdodHNbbF1bal1baV0gKiBjdXJbaV07CiAgICAgIHoucHVzaChzKTsgYS5wdXNoKHRhbmgocykpOwogICAgfQogICAgcHJlYWN0cy5wdXNoKHopOyBhY3RzLnB1c2goYSk7IGN1ciA9IGE7CiAgfQogIHJldHVybiB7IGFjdHMsIHByZWFjdHMgfTsKfQoKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQovLyBMYXlvdXQgZ2VvbWV0cnkKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpjb25zdCBXID0gOTgwLCBIID0gNTYwLCBQQURfWCA9IDkwLCBQQURfVE9QID0gNzAsIFBBRF9CT1QgPSA1MDsKY29uc3Qgc3ZnID0gZDMuc2VsZWN0KCIjbmV0Iik7CmNvbnN0IHRpcCA9IGQzLnNlbGVjdCgiI3RpcCIpOwoKZnVuY3Rpb24gbGF5ZXJYKGwpIHsgcmV0dXJuIFBBRF9YICsgKFcgLSAyICogUEFEX1gpICogKGwgLyAoU0laRVMubGVuZ3RoIC0gMSkpOyB9CmZ1bmN0aW9uIG5vZGVZKGwsIGopIHsKICBjb25zdCBuID0gU0laRVNbbF07CiAgY29uc3QgdG9wID0gUEFEX1RPUCwgYm90ID0gSCAtIFBBRF9CT1QsIHNwYW4gPSBib3QgLSB0b3A7CiAgaWYgKG4gPT09IDEpIHJldHVybiAodG9wICsgYm90KSAvIDI7CiAgcmV0dXJuIHRvcCArIHNwYW4gKiAoaiAvIChuIC0gMSkpOwp9CgovLyBjb2xvciBzY2FsZXM6IHdlaWdodHMgJiBhY3RpdmF0aW9ucyBtYXBwZWQgdG8gcmVkKC0pL2dyZWVuKCspCmZ1bmN0aW9uIHdlaWdodENvbG9yKHcpIHsgcmV0dXJuIHcgPj0gMCA/ICJ2YXIoLS1wb3MpIiA6ICJ2YXIoLS1uZWcpIjsgfQpmdW5jdGlvbiBhY3RDb2xvcihhKSAgICB7IHJldHVybiBhID49IDAgPyAidmFyKC0tcG9zKSIgOiAidmFyKC0tbmVnKSI7IH0KZnVuY3Rpb24gd2VpZ2h0V2lkdGgodykgeyByZXR1cm4gMC42ICsgTWF0aC5taW4oNS41LCBNYXRoLmFicyh3KSAqIDMuMik7IH0KCmNvbnN0IGdFZGdlcyAgPSBzdmcuYXBwZW5kKCJnIikuYXR0cigiY2xhc3MiLCAiZWRnZXMiKTsKY29uc3QgZ05vZGVzICA9IHN2Zy5hcHBlbmQoImciKS5hdHRyKCJjbGFzcyIsICJub2RlcyIpOwpjb25zdCBnVGl0bGVzID0gc3ZnLmFwcGVuZCgiZyIpLmF0dHIoImNsYXNzIiwgInRpdGxlcyIpOwoKLy8gbGF5ZXIgdGl0bGVzICsgY2FwdGlvbnMKZnVuY3Rpb24gZHJhd1RpdGxlcygpIHsKICBnVGl0bGVzLnNlbGVjdEFsbCgiKiIpLnJlbW92ZSgpOwogIFNJWkVTLmZvckVhY2goKG4sIGwpID0+IHsKICAgIGdUaXRsZXMuYXBwZW5kKCJ0ZXh0IikuYXR0cigiY2xhc3MiLCAibGF5ZXItdGl0bGUiKQogICAgICAuYXR0cigieCIsIGxheWVyWChsKSkuYXR0cigieSIsIDI4KS50ZXh0KExBWUVSX1RJVExFU1tsXSk7CiAgICBnVGl0bGVzLmFwcGVuZCgidGV4dCIpLmF0dHIoImNsYXNzIiwgImxheWVyLWNhcCIpCiAgICAgIC5hdHRyKCJ4IiwgbGF5ZXJYKGwpKS5hdHRyKCJ5IiwgNDYpLnRleHQoTEFZRVJfQ0FQU1tsXSk7CiAgfSk7Cn0KCi8vIGJ1aWxkIGVkZ2VzICsgbm9kZXMgb25jZSAoZ2VvbWV0cnkgaXMgc3RhdGljOyBvbmx5IGNvbG9yL3dpZHRoL2FuaW0gdXBkYXRlKQpsZXQgZWRnZVNlbCwgbm9kZVNlbDsKZnVuY3Rpb24gZHJhd1N0cnVjdHVyZSgpIHsKICAvLyBlZGdlcwogIGNvbnN0IGVkZ2VzID0gW107CiAgZm9yIChsZXQgbCA9IDA7IGwgPCB3ZWlnaHRzLmxlbmd0aDsgbCsrKSB7CiAgICBmb3IgKGxldCBqID0gMDsgaiA8IFNJWkVTW2wgKyAxXTsgaisrKSB7CiAgICAgIGZvciAobGV0IGkgPSAwOyBpIDwgU0laRVNbbF07IGkrKykgewogICAgICAgIGVkZ2VzLnB1c2goeyBsLCBpLCBqLAogICAgICAgICAgeDE6IGxheWVyWChsKSwgICAgIHkxOiBub2RlWShsLCBpKSwKICAgICAgICAgIHgyOiBsYXllclgobCArIDEpLCB5Mjogbm9kZVkobCArIDEsIGopIH0pOwogICAgICB9CiAgICB9CiAgfQogIGVkZ2VTZWwgPSBnRWRnZXMuc2VsZWN0QWxsKCJsaW5lIikuZGF0YShlZGdlcykuam9pbigibGluZSIpCiAgICAuYXR0cigiY2xhc3MiLCAiZWRnZSIpCiAgICAuYXR0cigieDEiLCBkID0+IGQueDEpLmF0dHIoInkxIiwgZCA9PiBkLnkxKQogICAgLmF0dHIoIngyIiwgZCA9PiBkLngyKS5hdHRyKCJ5MiIsIGQgPT4gZC55MikKICAgIC5vbigibW91c2Vtb3ZlIiwgKGUsIGQpID0+IHsKICAgICAgY29uc3QgdyA9IHdlaWdodHNbZC5sXVtkLmpdW2QuaV07CiAgICAgIHNob3dUaXAoZSwgYDxiPldlaWdodDwvYj4gwrcgcGFuZWwgbGluZTxicj4KICAgICAgICA8c3BhbiBjbGFzcz0ibW9ubyI+bGF5ZXIgJHtkLmx9IOKGkiAke2QubCArIDF9PC9zcGFuPjxicj4KICAgICAgICBmcm9tIG5ldXJvbiAke2QuaX0g4oaSIG5ldXJvbiAke2Quan08YnI+CiAgICAgICAgPHNwYW4gY2xhc3M9Im1vbm8iPncgPSAke3cudG9GaXhlZCgzKX08L3NwYW4+PGJyPgogICAgICAgIDxzcGFuIHN0eWxlPSJjb2xvcjoke3c+PTA/J3ZhcigtLXBvcyknOid2YXIoLS1uZWcpJ30iPiR7dz49MD8nYm9vc3RzJzonZGFtcHMnfSB0aGlzIHNpZ25hbDwvc3Bhbj5gKTsKICAgIH0pCiAgICAub24oIm1vdXNlbGVhdmUiLCBoaWRlVGlwKTsKCiAgLy8gbm9kZXMKICBjb25zdCBub2RlcyA9IFtdOwogIFNJWkVTLmZvckVhY2goKG4sIGwpID0+IHsKICAgIGZvciAobGV0IGogPSAwOyBqIDwgbjsgaisrKSBub2Rlcy5wdXNoKHsgbCwgaiwgeDogbGF5ZXJYKGwpLCB5OiBub2RlWShsLCBqKSB9KTsKICB9KTsKICBjb25zdCBnID0gZ05vZGVzLnNlbGVjdEFsbCgiZy5ub2RlIikuZGF0YShub2Rlcykuam9pbigiZyIpLmF0dHIoImNsYXNzIiwgIm5vZGUiKQogICAgLmF0dHIoInRyYW5zZm9ybSIsIGQgPT4gYHRyYW5zbGF0ZSgke2QueH0sJHtkLnl9KWApOwogIGcuc2VsZWN0QWxsKCJjaXJjbGUiKS5kYXRhKGQgPT4gW2RdKS5qb2luKCJjaXJjbGUiKS5hdHRyKCJyIiwgZCA9PiBkLmwgPT09IDAgfHwgZC5sID09PSBTSVpFUy5sZW5ndGggLSAxID8gMTMgOiAxMSk7CiAgbm9kZVNlbCA9IGc7CgogIC8vIGlucHV0IHJvdyBsYWJlbHMKICBnTm9kZXMuc2VsZWN0QWxsKCJ0ZXh0LmluLWxhYiIpLnJlbW92ZSgpOwogIFsieOKCgSBpbmNvbWUiLCAieOKCgiBjcmVkaXQiXS5mb3JFYWNoKCh0LCBpKSA9PiB7CiAgICBnTm9kZXMuYXBwZW5kKCJ0ZXh0IikuYXR0cigiY2xhc3MiLCAibm9kZS1sYWJlbCBpbi1sYWIiKQogICAgICAuYXR0cigieCIsIGxheWVyWCgwKSAtIDIyKS5hdHRyKCJ5Iiwgbm9kZVkoMCwgaSkgKyA0KQogICAgICAuYXR0cigidGV4dC1hbmNob3IiLCAiZW5kIikudGV4dCh0KTsKICB9KTsKCiAgZy5vbigibW91c2Vtb3ZlIiwgKGUsIGQpID0+IHsKICAgIGlmIChkLmwgPT09IDApIHsKICAgICAgc2hvd1RpcChlLCBgPGI+SW5wdXQ8L2I+PGJyPjxzcGFuIGNsYXNzPSJtb25vIj54JHtkLmorMX0gPSAke3N0YXRlLmFjdHNbMF1bZC5qXS50b0ZpeGVkKDMpfTwvc3Bhbj48YnI+JHtkLmo9PT0wPydtb250aGx5IGluY29tZSc6J2NyZWRpdCBzY29yZSd9IChyYXcgZm9ybSBmaWVsZClgKTsKICAgIH0gZWxzZSB7CiAgICAgIGNvbnN0IHogPSBzdGF0ZS5wcmVhY3RzW2QubF1bZC5qXSwgYSA9IHN0YXRlLmFjdHNbZC5sXVtkLmpdOwogICAgICBjb25zdCByb2xlID0gZC5sID09PSBTSVpFUy5sZW5ndGggLSAxID8gImZpbmFsIGp1ZGdlIiA6IGByZXZpZXdlciAke2Quan0gaW4gcGFuZWwgJHtkLmx9YDsKICAgICAgc2hvd1RpcChlLCBgPGI+TmV1cm9uPC9iPiDCtyAke3JvbGV9PGJyPgogICAgICAgIDxzcGFuIGNsYXNzPSJtb25vIj53wrd4ICsgYiA9ICR7ei50b0ZpeGVkKDMpfTwvc3Bhbj48YnI+CiAgICAgICAgPHNwYW4gY2xhc3M9Im1vbm8iPnRhbmgoLi4uKSA9IDxiIHN0eWxlPSJjb2xvcjoke2E+PTA/J3ZhcigtLXBvcyknOid2YXIoLS1uZWcpJ30iPiR7YS50b0ZpeGVkKDMpfTwvYj48L3NwYW4+PGJyPgogICAgICAgICR7YT49MD8nbGVhbmluZyB5ZXMnOidsZWFuaW5nIG5vJ31gKTsKICAgIH0KICB9KS5vbigibW91c2VsZWF2ZSIsIGhpZGVUaXApOwp9CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIFBhaW50IGN1cnJlbnQgc3RhdGUgKGNvbG9ycyArIHdpZHRocykgb250byBlZGdlcyBhbmQgbm9kZXMKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpsZXQgc3RhdGUgPSB7IGFjdHM6IFtdLCBwcmVhY3RzOiBbXSB9OwoKZnVuY3Rpb24gcmVjb21wdXRlKCkgewogIGNvbnN0IHgxID0gK2QzLnNlbGVjdCgiI3gxIikucHJvcGVydHkoInZhbHVlIik7CiAgY29uc3QgeDIgPSArZDMuc2VsZWN0KCIjeDIiKS5wcm9wZXJ0eSgidmFsdWUiKTsKICBzdGF0ZSA9IGZvcndhcmQoW3gxLCB4Ml0pOwogIHBhaW50KCk7CiAgdXBkYXRlVmVyZGljdCgpOwp9CgpmdW5jdGlvbiBwYWludCgpIHsKICBlZGdlU2VsCiAgICAuYXR0cigic3Ryb2tlIiwgZCA9PiB3ZWlnaHRDb2xvcih3ZWlnaHRzW2QubF1bZC5qXVtkLmldKSkKICAgIC5hdHRyKCJzdHJva2Utd2lkdGgiLCBkID0+IHdlaWdodFdpZHRoKHdlaWdodHNbZC5sXVtkLmpdW2QuaV0pKQogICAgLmF0dHIoInN0cm9rZS1vcGFjaXR5IiwgMC40NSk7CgogIG5vZGVTZWwuc2VsZWN0KCJjaXJjbGUiKQogICAgLmF0dHIoImZpbGwiLCBkID0+IHsKICAgICAgY29uc3QgYSA9IHN0YXRlLmFjdHNbZC5sXSA/IHN0YXRlLmFjdHNbZC5sXVtkLmpdIDogMDsKICAgICAgY29uc3QgbWFnID0gTWF0aC5taW4oMSwgTWF0aC5hYnMoYSkpOwogICAgICBjb25zdCBiYXNlID0gYSA+PSAwID8gWzc0LDIyMiwxMjhdIDogWzI0OCwxMTMsMTEzXTsKICAgICAgLy8gYmxlbmQgdG93YXJkIGRhcmsgcGFuZWwgZm9yIGxvdyBtYWduaXR1ZGUKICAgICAgY29uc3QgbWl4ID0gKGMpID0+IE1hdGgucm91bmQoNDAgKyAoYyAtIDQwKSAqICgwLjI1ICsgMC43NSAqIG1hZykpOwogICAgICByZXR1cm4gYHJnYigke21peChiYXNlWzBdKX0sJHttaXgoYmFzZVsxXSl9LCR7bWl4KGJhc2VbMl0pfSlgOwogICAgfSk7Cn0KCmZ1bmN0aW9uIHVwZGF0ZVZlcmRpY3QoKSB7CiAgY29uc3Qgb3V0ID0gc3RhdGUuYWN0c1tzdGF0ZS5hY3RzLmxlbmd0aCAtIDFdWzBdOwogIGNvbnN0IGVsID0gZDMuc2VsZWN0KCIjdmVyZGljdCIpOwogIGVsLnRleHQoKG91dCA+PSAwID8gIisiIDogIiIpICsgb3V0LnRvRml4ZWQoMykpCiAgICAuc3R5bGUoImNvbG9yIiwgb3V0ID49IDAgPyAidmFyKC0tcG9zKSIgOiAidmFyKC0tbmVnKSIpOwogIGQzLnNlbGVjdCgiI3ZlcmRpY3QtdGV4dCIpLmh0bWwoCiAgICBvdXQgPj0gMAogICAgICA/IGBTY29yZSA8YiBzdHlsZT0iY29sb3I6dmFyKC0tcG9zKSI+JHtvdXQudG9GaXhlZCgyKX08L2I+IOKGkiBsZWFuaW5nIDxiPmdvb2QgdGVuYW50PC9iPi5gCiAgICAgIDogYFNjb3JlIDxiIHN0eWxlPSJjb2xvcjp2YXIoLS1uZWcpIj4ke291dC50b0ZpeGVkKDIpfTwvYj4g4oaSIGxlYW5pbmcgPGI+cmlza3k8L2I+LmAKICApOwp9CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEFuaW1hdGVkIGZvcndhcmQgcGFzczogbGlnaHQgdXAgbGF5ZXIgYnkgbGF5ZXIsIHB1bHNlIGVkZ2VzIGJldHdlZW4gdGhlbQovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmxldCBwbGF5aW5nID0gZmFsc2U7CmZ1bmN0aW9uIGFuaW1hdGUoKSB7CiAgaWYgKHBsYXlpbmcpIHJldHVybjsKICBwbGF5aW5nID0gdHJ1ZTsKICAvLyBkaW0gZXZlcnl0aGluZyBmaXJzdAogIGVkZ2VTZWwuYXR0cigic3Ryb2tlLW9wYWNpdHkiLCAwLjA4KTsKICBub2RlU2VsLnNlbGVjdCgiY2lyY2xlIikuYXR0cigib3BhY2l0eSIsIDAuMjUpOwogIGQzLnNlbGVjdCgiI3BsYXkiKS5hdHRyKCJkaXNhYmxlZCIsIHRydWUpLnRleHQoIuKWtiBwbGF5aW5n4oCmIik7CgogIGNvbnN0IFNURVAgPSA2MjA7CiAgLy8gbGlnaHQgaW5wdXQgbGF5ZXIKICBsaWdodExheWVyKDAsIDApOwogIGZvciAobGV0IGwgPSAwOyBsIDwgd2VpZ2h0cy5sZW5ndGg7IGwrKykgewogICAgcHVsc2VFZGdlcyhsLCBTVEVQICogKGwgKyAwLjE1KSk7CiAgICBsaWdodExheWVyKGwgKyAxLCBTVEVQICogKGwgKyAxKSk7CiAgfQogIHNldFRpbWVvdXQoKCkgPT4gewogICAgZWRnZVNlbC50cmFuc2l0aW9uKCkuZHVyYXRpb24oMzAwKS5hdHRyKCJzdHJva2Utb3BhY2l0eSIsIDAuNDUpOwogICAgbm9kZVNlbC5zZWxlY3QoImNpcmNsZSIpLnRyYW5zaXRpb24oKS5kdXJhdGlvbigzMDApLmF0dHIoIm9wYWNpdHkiLCAxKTsKICAgIHBsYXlpbmcgPSBmYWxzZTsKICAgIGQzLnNlbGVjdCgiI3BsYXkiKS5hdHRyKCJkaXNhYmxlZCIsIG51bGwpLnRleHQoIuKWtiBBbmltYXRlIGZvcndhcmQgcGFzcyIpOwogIH0sIFNURVAgKiAod2VpZ2h0cy5sZW5ndGggKyAxKSArIDI1MCk7Cn0KCmZ1bmN0aW9uIGxpZ2h0TGF5ZXIobCwgZGVsYXkpIHsKICBub2RlU2VsLmZpbHRlcihkID0+IGQubCA9PT0gbCkuc2VsZWN0KCJjaXJjbGUiKQogICAgLnRyYW5zaXRpb24oKS5kZWxheShkZWxheSkuZHVyYXRpb24oMjgwKQogICAgLmF0dHIoIm9wYWNpdHkiLCAxKQogICAgLmF0dHIoInIiLCBkID0+IChkLmwgPT09IDAgfHwgZC5sID09PSBTSVpFUy5sZW5ndGggLSAxID8gMTMgOiAxMSkgKyA0KQogICAgLnRyYW5zaXRpb24oKS5kdXJhdGlvbigyMjApCiAgICAuYXR0cigiciIsIGQgPT4gZC5sID09PSAwIHx8IGQubCA9PT0gU0laRVMubGVuZ3RoIC0gMSA/IDEzIDogMTEpOwp9CgpmdW5jdGlvbiBwdWxzZUVkZ2VzKGwsIGRlbGF5KSB7CiAgZWRnZVNlbC5maWx0ZXIoZCA9PiBkLmwgPT09IGwpCiAgICAudHJhbnNpdGlvbigpLmRlbGF5KGRlbGF5KS5kdXJhdGlvbigzMDApLmF0dHIoInN0cm9rZS1vcGFjaXR5IiwgMC45KQogICAgLnRyYW5zaXRpb24oKS5kdXJhdGlvbigzMjApLmF0dHIoInN0cm9rZS1vcGFjaXR5IiwgMC4xOCk7Cn0KCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gVG9vbHRpcCBoZWxwZXJzCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KZnVuY3Rpb24gc2hvd1RpcChlLCBodG1sKSB7CiAgdGlwLmh0bWwoaHRtbCkuc3R5bGUoIm9wYWNpdHkiLCAxKQogICAgLnN0eWxlKCJsZWZ0IiwgKGUuY2xpZW50WCArIDE0KSArICJweCIpCiAgICAuc3R5bGUoInRvcCIsICAoZS5jbGllbnRZICsgMTQpICsgInB4Iik7Cn0KZnVuY3Rpb24gaGlkZVRpcCgpIHsgdGlwLnN0eWxlKCJvcGFjaXR5IiwgMCk7IH0KCi8vIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0KLy8gV2lyZSB1cCBjb250cm9scwovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCmZ1bmN0aW9uIHN5bmNMYWJlbHMoKSB7CiAgZDMuc2VsZWN0KCIjeDF2IikudGV4dCgoK2QzLnNlbGVjdCgiI3gxIikucHJvcGVydHkoInZhbHVlIikpLnRvRml4ZWQoMikpOwogIGQzLnNlbGVjdCgiI3gydiIpLnRleHQoKCtkMy5zZWxlY3QoIiN4MiIpLnByb3BlcnR5KCJ2YWx1ZSIpKS50b0ZpeGVkKDIpKTsKfQpkMy5zZWxlY3QoIiN4MSIpLm9uKCJpbnB1dCIsICgpID0+IHsgc3luY0xhYmVscygpOyByZWNvbXB1dGUoKTsgfSk7CmQzLnNlbGVjdCgiI3gyIikub24oImlucHV0IiwgKCkgPT4geyBzeW5jTGFiZWxzKCk7IHJlY29tcHV0ZSgpOyB9KTsKZDMuc2VsZWN0KCIjcGxheSIpLm9uKCJjbGljayIsIGFuaW1hdGUpOwpkMy5zZWxlY3QoIiNyZXJvbGwiKS5vbigiY2xpY2siLCAoKSA9PiB7IGJ1aWxkTmV0d29yaygpOyBkcmF3U3RydWN0dXJlKCk7IHJlY29tcHV0ZSgpOyB9KTsKZDMuc2VsZWN0KCIjcmVzZXQiKS5vbigiY2xpY2siLCAoKSA9PiB7CiAgZDMuc2VsZWN0KCIjeDEiKS5wcm9wZXJ0eSgidmFsdWUiLCAwLjUpOwogIGQzLnNlbGVjdCgiI3gyIikucHJvcGVydHkoInZhbHVlIiwgLTAuMyk7CiAgc3luY0xhYmVscygpOyByZWNvbXB1dGUoKTsKfSk7CgovLyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tCi8vIEluaXQKLy8gLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLQpkcmF3VGl0bGVzKCk7CmRyYXdTdHJ1Y3R1cmUoKTsKc3luY0xhYmVscygpOwpyZWNvbXB1dGUoKTsKPC9zY3JpcHQ+CjwvYm9keT4KPC9odG1sPgo=\"\n",
|
||
" frameborder=\"0\"\n",
|
||
" allowfullscreen\n",
|
||
" \n",
|
||
" ></iframe>\n",
|
||
" "
|
||
],
|
||
"text/plain": [
|
||
"<IPython.lib.display.IFrame at 0x236e4d71ac0>"
|
||
]
|
||
},
|
||
"metadata": {},
|
||
"output_type": "display_data"
|
||
}
|
||
],
|
||
"source": [
|
||
"# Interactive companion to section 5.1 — drag the inputs, hover lines/dots, press play.\n",
|
||
"# Shows the same 2 -> 8 -> 8 -> 1 reviewer-panel network as the code below.\n",
|
||
"import base64\n",
|
||
"from pathlib import Path\n",
|
||
"from IPython.display import IFrame, display\n",
|
||
"\n",
|
||
"html_path = Path(\"mlp_playground.html\")\n",
|
||
"if not html_path.exists():\n",
|
||
" html_path = Path.cwd() / \"mlp_playground.html\"\n",
|
||
"\n",
|
||
"if not html_path.exists():\n",
|
||
" raise FileNotFoundError(\n",
|
||
" \"mlp_playground.html not found. Keep it next to this notebook, then re-run.\"\n",
|
||
" )\n",
|
||
"\n",
|
||
"# Notebook UIs often cannot load local files via IFrame src=\"file.html\".\n",
|
||
"# Pack the page into a data URI so the browser runs it inline with scripts intact.\n",
|
||
"b64 = base64.b64encode(html_path.read_bytes()).decode(\"ascii\")\n",
|
||
"display(IFrame(src=f\"data:text/html;base64,{b64}\", width=\"100%\", height=640))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "1d260369",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"class Neuron:\n",
|
||
" def __init__(self, nin):\n",
|
||
" # one weight per input + one bias — all are Value objects (trainable knobs)\n",
|
||
" self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]\n",
|
||
" self.b = Value(random.uniform(-1, 1))\n",
|
||
"\n",
|
||
" def __call__(self, x):\n",
|
||
" # weighted sum: w1*x1 + w2*x2 + ... + b (each * and + builds the graph)\n",
|
||
" act = sum((wi * xi for wi, xi in zip(self.w, x)), self.b)\n",
|
||
" return act.tanh() # squash activation to (-1, 1)\n",
|
||
"\n",
|
||
" def parameters(self):\n",
|
||
" return self.w + [self.b] # every knob this neuron owns\n",
|
||
"\n",
|
||
"\n",
|
||
"class Layer:\n",
|
||
" def __init__(self, nin, nout):\n",
|
||
" self.neurons = [Neuron(nin) for _ in range(nout)] # nout parallel neurons\n",
|
||
"\n",
|
||
" def __call__(self, x):\n",
|
||
" outs = [n(x) for n in self.neurons] # each neuron sees the same input vector x\n",
|
||
" return outs[0] if len(outs) == 1 else outs # scalar if one neuron, else list\n",
|
||
"\n",
|
||
" def parameters(self):\n",
|
||
" return [p for n in self.neurons for p in n.parameters()] # flatten all weights\n",
|
||
"\n",
|
||
"\n",
|
||
"class MLP:\n",
|
||
" def __init__(self, nin, nouts):\n",
|
||
" sizes = [nin] + nouts # e.g. [2, 8, 8, 1] for 2→8→8→1\n",
|
||
" # layer i connects sizes[i] inputs to sizes[i+1] outputs\n",
|
||
" self.layers = [Layer(sizes[i], sizes[i + 1]) for i in range(len(nouts))]\n",
|
||
"\n",
|
||
" def __call__(self, x):\n",
|
||
" for layer in self.layers: # data flows forward through each layer\n",
|
||
" x = layer(x)\n",
|
||
" return x\n",
|
||
"\n",
|
||
" def parameters(self):\n",
|
||
" return [p for layer in self.layers for p in layer.parameters()] # all trainable Values\n",
|
||
"\n",
|
||
"\n",
|
||
"net = MLP(2, [8, 8, 1]) # 2 inputs -> 8 hidden -> 8 hidden -> 1 output\n",
|
||
"print(\"parameter count:\", len(net.parameters()))\n",
|
||
"print(\"one prediction:\", net([Value(0.5), Value(-0.2)]))"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "fc0b5b7b",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 6.1 Train the MLP on the two moons\n",
|
||
"\n",
|
||
"Loop the four familiar steps:\n",
|
||
"\n",
|
||
"1. **forward** — predict on every point\n",
|
||
"2. **loss** — how wrong (here: mean-squared error against labels mapped to -1 / +1)\n",
|
||
"3. **backward** — `loss.backward()` fills every parameter's `.grad`\n",
|
||
"4. **update** — nudge each parameter a little **against** its gradient (downhill)\n",
|
||
"\n",
|
||
"Watch the loss fall. (Pure-Python micrograd is slow, so we keep the net and step count small.)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "de95ef11",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"net = MLP(2, [8, 8, 1]) # fresh network for training\n",
|
||
"\n",
|
||
"# map labels {0,1} -> {-1,+1} to match tanh output range (-1 to +1)\n",
|
||
"ys = [1.0 if yi == 1 else -1.0 for yi in y]\n",
|
||
"# wrap each (x, y) coordinate pair as Value objects so forward builds a graph\n",
|
||
"Xv = [[Value(float(xi[0])), Value(float(xi[1]))] for xi in X]\n",
|
||
"\n",
|
||
"lr = 0.1 # learning rate — how big each knob turn per step\n",
|
||
"for step in range(60): # 60 gradient-descent steps (small count: micrograd is slow)\n",
|
||
"\n",
|
||
" # --- 1. FORWARD: predict every point, build one big graph ending in loss ---\n",
|
||
" preds = [net(xrow) for xrow in Xv]\n",
|
||
" loss = sum(((p - yt) ** 2 for p, yt in zip(preds, ys)), Value(0.0)) # sum of squared errors\n",
|
||
" loss = loss * (1.0 / len(ys)) # average MSE so scale doesn't grow with dataset size\n",
|
||
"\n",
|
||
" # --- 2. BACKWARD: zero old grads, then propagate blame from loss to every weight ---\n",
|
||
" for p in net.parameters():\n",
|
||
" p.grad = 0.0 # must reset — backward() accumulates with +=\n",
|
||
" loss.backward() # fills p.grad for every parameter\n",
|
||
"\n",
|
||
" # --- 3. UPDATE: walk downhill — turn each knob opposite its gradient ---\n",
|
||
" for p in net.parameters():\n",
|
||
" p.data -= lr * p.grad # new value = old value - lr * (how loss rises if knob rises)\n",
|
||
"\n",
|
||
" if step % 10 == 0 or step == 59:\n",
|
||
" # sign match: prediction > 0 same as target > 0 counts as correct\n",
|
||
" acc = sum((1 if (p.data > 0) == (yt > 0) else 0) for p, yt in zip(preds, ys)) / len(ys)\n",
|
||
" print(f\"step {step:3d} loss {loss.data:.4f} acc {acc:.2%}\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "9cb30391",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 6.2 See the boundary it learned\n",
|
||
"\n",
|
||
"Evaluate the trained net across a grid of points and color the regions. The swirl between the\n",
|
||
"two moons should now be split by a curved boundary — the network \"drew the line.\"\n",
|
||
"\n",
|
||
"(Coarse grid for speed; each grid point is a full forward pass through micrograd.)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "ee39626a",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"if HAS_PLT:\n",
|
||
" # pad plot area a little beyond the data points\n",
|
||
" xmin, xmax = X[:, 0].min() - 0.5, X[:, 0].max() + 0.5\n",
|
||
" ymin, ymax = X[:, 1].min() - 0.5, X[:, 1].max() + 0.5\n",
|
||
" xs = np.linspace(xmin, xmax, 30) # 30 grid columns\n",
|
||
" ys_grid = np.linspace(ymin, ymax, 30) # 30 grid rows\n",
|
||
"\n",
|
||
" Z = np.zeros((len(ys_grid), len(xs))) # store net output at each grid cell\n",
|
||
" for i, gy in enumerate(ys_grid):\n",
|
||
" for j, gx in enumerate(xs):\n",
|
||
" out = net([Value(float(gx)), Value(float(gy))]) # forward only (no training)\n",
|
||
" Z[i, j] = out.data # >0 one side of boundary, <0 other side\n",
|
||
"\n",
|
||
" plt.figure(figsize=(5, 4))\n",
|
||
" plt.contourf(xs, ys_grid, Z, levels=[-1e9, 0, 1e9], cmap=\"bwr\", alpha=0.3) # colored regions\n",
|
||
" plt.scatter(X[:, 0], X[:, 1], c=y, s=20, cmap=\"bwr\") # training dots on top\n",
|
||
" plt.title(\"decision boundary learned by the micrograd MLP\")\n",
|
||
" plt.show()\n",
|
||
"else:\n",
|
||
" print(\"matplotlib missing - skipping the boundary plot (training above still ran)\")"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "b00ac0fd",
|
||
"metadata": {},
|
||
"source": [
|
||
"**Your turn 6** — Try `MLP(2, [16, 16, 1])` or a different learning rate / step count and\n",
|
||
"re-run 6.1 and 6.2. Does a bigger net reach higher accuracy? Does too-large a learning rate\n",
|
||
"make the loss bounce instead of fall?"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "48623e88",
|
||
"metadata": {},
|
||
"source": [
|
||
"### 7.1 Bridge to PyTorch — same gradients, industrial engine\n",
|
||
"\n",
|
||
"PyTorch is micrograd scaled up to fast tensors. The *idea* is identical: build an expression,\n",
|
||
"call `.backward()`, read `.grad`. Here we redo the tiny `a*b + c` (then a tanh) in PyTorch and\n",
|
||
"confirm the gradients match what our engine produced.\n",
|
||
"\n",
|
||
"**-> Training:** in notebooks `01` and `06` we let PyTorch's autograd do exactly this — now you\n",
|
||
"know what it is doing under the hood."
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "code",
|
||
"execution_count": null,
|
||
"id": "e6c3140e",
|
||
"metadata": {},
|
||
"outputs": [],
|
||
"source": [
|
||
"# On some Windows setups torch and numpy ship duplicate OpenMP DLLs; this avoids a clash.\n",
|
||
"import os\n",
|
||
"os.environ.setdefault(\"KMP_DUPLICATE_LIB_OK\", \"TRUE\")\n",
|
||
"\n",
|
||
"try:\n",
|
||
" import torch\n",
|
||
"\n",
|
||
" # requires_grad=True tells PyTorch to track history (like our Value graph)\n",
|
||
" at = torch.tensor([0.5], requires_grad=True)\n",
|
||
" bt = torch.tensor([2.0], requires_grad=True)\n",
|
||
" ct = torch.tensor([-1.0], requires_grad=True)\n",
|
||
"\n",
|
||
" gt = torch.tanh(at * bt + ct) # same expression as micrograd Your turn 4\n",
|
||
" gt.backward() # PyTorch autograd — same idea as Value.backward()\n",
|
||
" print(\"PyTorch: a.grad =\", at.grad.item())\n",
|
||
"except OSError as e:\n",
|
||
" print(\"PyTorch could not load in this environment:\", e)\n",
|
||
" print(\"(That is fine here - the micrograd result below is the point.)\")\n",
|
||
"\n",
|
||
"# our engine, same expression — should match PyTorch's a.grad\n",
|
||
"a = Value(0.5); b = Value(2.0); c = Value(-1.0)\n",
|
||
"(a * b + c).tanh().backward()\n",
|
||
"print(\"micrograd: a.grad =\", a.grad)"
|
||
]
|
||
},
|
||
{
|
||
"cell_type": "markdown",
|
||
"id": "49c1ebf8",
|
||
"metadata": {},
|
||
"source": [
|
||
"## What's next\n",
|
||
"\n",
|
||
"You built the engine that powers all of deep learning:\n",
|
||
"\n",
|
||
"- a **number that remembers** its history (`Value`)\n",
|
||
"- **backpropagation** via the chain rule (`backward()`)\n",
|
||
"- a small **MLP** trained by nudging weights downhill\n",
|
||
"\n",
|
||
"Where this leads in the series:\n",
|
||
"\n",
|
||
"- `01_build_gpt.ipynb` — language modeling basics (the bigram), now that backprop is no longer\n",
|
||
" mysterious.\n",
|
||
"- `02`-`05` — scale these same ideas up with PyTorch on real character data (`names.txt`).\n",
|
||
"- `06_build_gpt_attention.ipynb` — the transformer, where `loss.backward()` is doing exactly\n",
|
||
" what you built here, just across millions of `Value`-like nodes.\n",
|
||
"\n",
|
||
"**Checklist**\n",
|
||
"- [ ] A `Value` records data, parents, and operation\n",
|
||
"- [ ] Each op defines a local `_backward`\n",
|
||
"- [ ] `backward()` = topological order + chain rule\n",
|
||
"- [ ] Neuron / Layer / MLP built from `Value`\n",
|
||
"- [ ] Trained on two moons; boundary learned\n",
|
||
"- [ ] Confirmed gradients match PyTorch"
|
||
]
|
||
}
|
||
],
|
||
"metadata": {
|
||
"kernelspec": {
|
||
"display_name": ".venv (3.12.10)",
|
||
"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.12.10"
|
||
}
|
||
},
|
||
"nbformat": 4,
|
||
"nbformat_minor": 5
|
||
}
|