{
"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'