{ "cells": [ { "cell_type": "markdown", "id": "2b5d5fd0", "metadata": {}, "source": [ "# 04 - Autograd from scratch\n", "\n", "In notebook 03 we worked out the gradient of a one-layer model by hand. That approach does not scale, because a real network is a deep chain of thousands of operations, and differentiating all of it by hand is hopeless. The solution is **automatic differentiation**, usually shortened to **autograd**. It is the machinery behind the `loss.backward()` call in PyTorch.\n", "\n", "The idea is small and rather elegant: build an expression out of tiny operations, have each operation remember how it was computed, then sweep backward through the chain applying a single rule, the chain rule, to obtain the gradient of everything at once. We will build a working engine for ordinary numbers in about 40 lines. After this, `.backward()` will no longer feel mysterious." ] }, { "cell_type": "markdown", "id": "e8823265", "metadata": {}, "source": [ "## The chain rule, the only rule\n", "\n", "Everything below rests on one fact from calculus, and it has a friendly picture.\n", "\n", "Think of exchange rates. Suppose 1 dollar is worth 3 euros, and 1 euro is worth 2 pesos. How many pesos is a dollar worth? You multiply the rates: `3 * 2 = 6`. The chain rule is that same move applied to \"sensitivities.\" If nudging `a` changes `b` at a rate written `db/da`, and nudging `b` changes the loss `L` at a rate written `dL/db`, then nudging `a` changes `L` at the product of the two:\n", "\n", "$$\\frac{dL}{da} = \\frac{dL}{db}\\cdot\\frac{db}{da}$$\n", "\n", "(The notation `dL/da` simply means \"the rate at which `L` changes when `a` changes,\" which is the slope, or derivative, from notebook 01.) So to find how the loss responds to some number buried deep in the network, you multiply the small rates along the path from that number up to the loss.\n", "\n", "How autograd uses this: every operation knows its own small rate, called its **local derivative**, with respect to its inputs. The code comments spell each one out; for `a + b` the rate is 1, and for `a * b` it is the other input. Backprop starts at the output with `dL/dL = 1` and walks backward, and at each step it multiplies the gradient coming in from above by that operation's local rate, handing the correct gradient down to its inputs. That per-operation step is stored as a small `_backward` function.\n", "\n", "One subtlety: gradients add up. If a value is used in two places, nudging it affects the loss through both paths, so its total gradient is the sum of what comes back from each. This is why every `grad` starts at 0 and we always add to it, and why `backward()` first sorts the operations into order (a \"topological order\"), so that each one is finalized only after everything that depends on it has already contributed its gradient." ] }, { "cell_type": "code", "execution_count": 1, "id": "bf7b1509", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.895360Z", "iopub.status.busy": "2026-07-01T08:57:57.895193Z", "iopub.status.idle": "2026-07-01T08:57:57.904715Z", "shell.execute_reply": "2026-07-01T08:57:57.904075Z" } }, "outputs": [], "source": [ "import math\n", "\n", "class Value:\n", " \"\"\"A scalar that records how it was computed, so we can backprop through it.\"\"\"\n", " def __init__(self, data, _children=(), _op=''):\n", " self.data = data\n", " self.grad = 0.0 # dL/d(self), filled in during backward()\n", " self._backward = lambda: None # how to send gradient to our inputs\n", " self._prev = set(_children)\n", " self._op = _op\n", "\n", " def __add__(self, other):\n", " other = other if isinstance(other, Value) else Value(other)\n", " out = Value(self.data + other.data, (self, other), '+')\n", " def _backward(): # d(a+b)/da = 1, d(a+b)/db = 1\n", " self.grad += out.grad\n", " other.grad += out.grad\n", " out._backward = _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), '*')\n", " def _backward(): # d(a*b)/da = b, d(a*b)/db = a\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, k): # only constant powers\n", " out = Value(self.data ** k, (self,), f'**{k}')\n", " def _backward(): # d(a**k)/da = k * a**(k-1)\n", " self.grad += k * self.data ** (k - 1) * out.grad\n", " out._backward = _backward\n", " return out\n", "\n", " def tanh(self):\n", " t = math.tanh(self.data)\n", " out = Value(t, (self,), 'tanh')\n", " def _backward(): # d(tanh)/dx = 1 - tanh^2\n", " self.grad += (1 - t ** 2) * out.grad\n", " out._backward = _backward\n", " return out\n", "\n", " def backward(self):\n", " # topological order so every node is processed after the things that use it\n", " topo, visited = [], set()\n", " def build(v):\n", " if v not in visited:\n", " visited.add(v)\n", " for child in v._prev: build(child)\n", " topo.append(v)\n", " build(self)\n", " self.grad = 1.0 # dL/dL = 1\n", " for v in reversed(topo):\n", " v._backward()\n", "\n", " # niceties so we can write normal math\n", " def __neg__(self): return self * -1\n", " def __radd__(self, o): return self + o\n", " def __sub__(self, o): return self + (-o)\n", " def __rsub__(self, o): return o + (-self)\n", " def __rmul__(self, o): return self * o\n", " def __truediv__(self, o): return self * o ** -1\n", " def __repr__(self): return f\"Value(data={self.data:.4f}, grad={self.grad:.4f})\"" ] }, { "cell_type": "markdown", "id": "83f82f93", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "m4c1a", "metadata": {}, "source": [ "### A `Value` is a number that remembers where it came from\n", "\n", "The class above is short but easy to read past. The one idea that makes autograd possible is that a `Value` stores more than a number: it keeps the operation and the input `Value`s that produced it. Let's build a tiny expression and look." ] }, { "cell_type": "code", "execution_count": 2, "id": "m4c1b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.906533Z", "iopub.status.busy": "2026-07-01T08:57:57.906381Z", "iopub.status.idle": "2026-07-01T08:57:57.909268Z", "shell.execute_reply": "2026-07-01T08:57:57.908747Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "expr.data = 7.0 (the forward value: 2*3 + 1)\n", "expr._op = '+' (the operation that produced expr)\n", "expr._prev -- the inputs it was built from:\n", " Value(data=1) from op ''\n", " Value(data=6.0) from op '*'\n", "\n", "that stored link from each node back to its inputs IS the graph\n", "backward() will later walk in reverse.\n" ] } ], "source": [ "# A Value is not just a number -- it remembers the op and inputs that made it.\n", "n_a = Value(2.0)\n", "n_b = Value(3.0)\n", "expr = n_a * n_b + 1 # a small expression\n", "\n", "print(\"expr.data =\", expr.data, \" (the forward value: 2*3 + 1)\")\n", "print(\"expr._op =\", repr(expr._op), \" (the operation that produced expr)\")\n", "print(\"expr._prev -- the inputs it was built from:\")\n", "for child in expr._prev:\n", " print(f\" Value(data={child.data}) from op {child._op!r}\")\n", "print(\"\\nthat stored link from each node back to its inputs IS the graph\")\n", "print(\"backward() will later walk in reverse.\")" ] }, { "cell_type": "markdown", "id": "m4c1c", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "9e8ed600", "metadata": {}, "source": [ "## Does it work? Check against numerical gradients\n", "\n", "The honest test of an autograd engine is whether it produces the same gradients as brute force. We build a small expression, `L = (a*b + b**2) * tanh(c)`, and call `.backward()` once, which fills in `a.grad`, `b.grad`, and `c.grad` by the chain rule. We then compare these against the numerical slope from notebook 01: nudge each input by a tiny amount, see how much `L` moves, and divide the change by the distance covered. The two agree to five decimal places, so the engine is correct. (These are the same three numbers PyTorch will reproduce in notebook 07.)" ] }, { "cell_type": "code", "execution_count": 3, "id": "e59d583b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.910979Z", "iopub.status.busy": "2026-07-01T08:57:57.910875Z", "iopub.status.idle": "2026-07-01T08:57:57.914124Z", "shell.execute_reply": "2026-07-01T08:57:57.913669Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "autograd grads: a=-1.38635 b=-1.84847 c=2.35934\n", "numerical grads: a=-1.38635 b=-1.84847 c=2.35934\n", "match -> True\n" ] } ], "source": [ "# L = (a*b + b**2) * tanh(c)\n", "a, b, c = Value(2.0), Value(-3.0), Value(0.5)\n", "L = (a*b + b**2) * c.tanh()\n", "L.backward()\n", "print(\"autograd grads: a=%.5f b=%.5f c=%.5f\" % (a.grad, b.grad, c.grad))\n", "\n", "def numeric(fn, x, h=1e-6):\n", " return (fn(x + h) - fn(x - h)) / (2*h)\n", "da = numeric(lambda x: (x*(-3.0) + (-3.0)**2) * math.tanh(0.5), 2.0)\n", "db = numeric(lambda x: (2.0*x + x**2) * math.tanh(0.5), -3.0)\n", "dc = numeric(lambda x: (2.0*(-3.0) + (-3.0)**2) * math.tanh(x), 0.5)\n", "print(\"numerical grads: a=%.5f b=%.5f c=%.5f\" % (da, db, dc))\n", "print(\"match ->\", all(abs(x-y) < 1e-4 for x, y in [(a.grad,da),(b.grad,db),(c.grad,dc)]))" ] }, { "cell_type": "markdown", "id": "d81ff6a6", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "m4c2a", "metadata": {}, "source": [ "### The whole graph, as a table\n", "\n", "`.backward()` filled in a `.grad` on every node in the expression, not just the three inputs. Here is the entire graph of `L = (a*b + b**2) * tanh(c)` laid out -- each node's operation, its forward `data`, and its `grad`, which is `dL/dnode`. Read it bottom-up: `L.grad` starts at 1, and the chain rule carries a gradient into every node below it." ] }, { "cell_type": "code", "execution_count": 4, "id": "m4c2b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.915695Z", "iopub.status.busy": "2026-07-01T08:57:57.915620Z", "iopub.status.idle": "2026-07-01T08:57:57.918358Z", "shell.execute_reply": "2026-07-01T08:57:57.917937Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " node op data grad = dL/dnode\n", " c input +0.5000 +2.3593\n", " tanh +0.4621 +3.0000\n", " b input -3.0000 -1.8485\n", " a input +2.0000 -1.3864\n", " * -6.0000 +0.4621\n", " **2 +9.0000 +0.4621\n", " + +3.0000 +0.4621\n", " L * +1.3864 +1.0000\n" ] } ], "source": [ "# Walk the graph L was built from and print every node's data and grad.\n", "names = {id(a): \"a\", id(b): \"b\", id(c): \"c\"}\n", "topo, seen = [], set()\n", "def walk(v):\n", " if id(v) not in seen:\n", " seen.add(id(v))\n", " for ch in v._prev:\n", " walk(ch)\n", " topo.append(v)\n", "walk(L)\n", "\n", "print(f\"{'node':>6} {'op':>6} {'data':>10} {'grad = dL/dnode':>18}\")\n", "for v in topo:\n", " tag = names.get(id(v), \"L\" if v is L else \"\")\n", " print(f\"{tag:>6} {(v._op or 'input'):>6} {v.data:>+10.4f} {v.grad:>+18.4f}\")" ] }, { "cell_type": "markdown", "id": "m4c2c", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "9b7805a9", "metadata": {}, "source": [ "## A tiny neural net, trained with our engine\n", "\n", "Now we put the engine to real use. We will stack `Value` objects into a small **neural network** and train it on a toy problem, with no PyTorch, using only the autograd we just wrote.\n", "\n", "The building blocks:\n", "- a **neuron** takes its inputs, multiplies each by a learned **weight**, adds the results together along with one extra learned number called a **bias** (so the whole step is a weighted vote), and then passes the result through `tanh`, a standard \"squashing\" function that gently forces any number into the range -1 to 1. The weights and the bias are the knobs the neuron learns.\n", "- a **layer** is simply several neurons side by side, each looking at the same inputs.\n", "- an **MLP**, short for multi-layer perceptron, stacks layers, feeding the outputs of one layer in as the inputs of the next.\n", "\n", "`MLP(2, [8, 8, 1])` means 2 inputs, then a layer of 8 neurons, then another 8, then a single output. That comes to 105 individual knobs (the `parameters`) for the engine to tune. Training is the same loop as always: a forward pass to predict, a measure of how far off we are (the *squared error* this time, which is the gap between prediction and target, squared so that every miss counts as a positive amount, since these outputs are plain numbers rather than probabilities), then `loss.backward()` to fill in every knob's `.grad`, then the **update** -- the line that actually changes the knobs. Repeat, and the loss falls until the predictions settle onto the +1 and -1 targets.\n", "\n", "**The update step, and what \"SGD\" means.** All the learning happens in one line: `p.data -= 0.05 * p.grad`. Each knob's `.grad` says which way to move it to *raise* the loss, so we step the opposite way -- subtract a small slice of the gradient -- to *lower* it. The size of that slice is the **learning rate** (here `0.05`): too small and training crawls, too large and it overshoots the bottom and bounces. Doing this over and over is **gradient descent** -- the same rolling-downhill picture from notebook 01, now running on all 105 knobs at once.\n", "\n", "The code comment calls it an **SGD step**, short for *stochastic gradient descent*. The only extra idea in the word \"stochastic\" is *randomness*: real training does not measure the gradient on the whole dataset each step, it uses a fresh **random handful of examples** (a *mini-batch*) -- far cheaper, and the little bit of noise even helps it generalize. Our toy here has just 8 points, so we use all of them every step (technically *full-batch* gradient descent), but the update line is identical. The bigram in notebook 03 and the GPT in notebook 09 both take the stochastic route, drawing a new random batch on every step." ] }, { "cell_type": "code", "execution_count": 5, "id": "826f107b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.919644Z", "iopub.status.busy": "2026-07-01T08:57:57.919564Z", "iopub.status.idle": "2026-07-01T08:57:57.923448Z", "shell.execute_reply": "2026-07-01T08:57:57.923045Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "parameters: 105\n" ] } ], "source": [ "import random\n", "random.seed(42)\n", "\n", "class Neuron:\n", " def __init__(self, nin):\n", " self.w = [Value(random.uniform(-1, 1)) for _ in range(nin)]\n", " self.b = Value(0.0)\n", " def __call__(self, x):\n", " act = sum((wi*xi for wi, xi in zip(self.w, x)), self.b)\n", " return act.tanh()\n", " def parameters(self): return self.w + [self.b]\n", "\n", "class Layer:\n", " def __init__(self, nin, nout): self.neurons = [Neuron(nin) for _ in range(nout)]\n", " def __call__(self, x):\n", " outs = [n(x) for n in self.neurons]\n", " return outs[0] if len(outs) == 1 else outs\n", " def parameters(self): return [p for n in self.neurons for p in n.parameters()]\n", "\n", "class MLP:\n", " def __init__(self, nin, nouts):\n", " sizes = [nin] + nouts\n", " self.layers = [Layer(sizes[i], sizes[i+1]) for i in range(len(nouts))]\n", " def __call__(self, x):\n", " for layer in self.layers: x = layer(x)\n", " return x\n", " def parameters(self): return [p for layer in self.layers for p in layer.parameters()]\n", "\n", "model = MLP(2, [8, 8, 1]) # 2 inputs -> 8 -> 8 -> 1 output\n", "print(\"parameters:\", len(model.parameters()))" ] }, { "cell_type": "markdown", "id": "7d807b97", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "m4c3a", "metadata": {}, "source": [ "### Where the 105 knobs live, and one neuron by hand\n", "\n", "`MLP(2, [8, 8, 1])` reports 105 parameters. That number is not magic -- it is just every weight and bias added up. Let's break it down layer by layer, then compute a single neuron the long way so \"a weighted vote, then tanh\" stops being words." ] }, { "cell_type": "code", "execution_count": 6, "id": "m4c3b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.924495Z", "iopub.status.busy": "2026-07-01T08:57:57.924429Z", "iopub.status.idle": "2026-07-01T08:57:57.927158Z", "shell.execute_reply": "2026-07-01T08:57:57.926819Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "layer 0: 8 neurons x (2 weights + 1 bias) = 24 params\n", "layer 1: 8 neurons x (8 weights + 1 bias) = 72 params\n", "layer 2: 1 neurons x (8 weights + 1 bias) = 9 params\n", "total = 105 (equals len(model.parameters()) = 105)\n", "\n", "neuron[0][0] on input [1.0, 1.0]:\n", " weights = [0.279, -0.95], bias = 0.0\n", " weighted vote = -0.671 -> tanh = -0.586 (the neuron's output)\n" ] } ], "source": [ "# How the 105 parameters are distributed, and one neuron computed by hand.\n", "total = 0\n", "for li, layer in enumerate(model.layers):\n", " nout = len(layer.neurons); nin = len(layer.neurons[0].w)\n", " pc = nout * (nin + 1); total += pc\n", " print(f\"layer {li}: {nout} neurons x ({nin} weights + 1 bias) = {pc} params\")\n", "print(f\"total = {total} (equals len(model.parameters()) = {len(model.parameters())})\")\n", "\n", "nrn = model.layers[0].neurons[0]; xin = [1.0, 1.0] # one neuron, first layer\n", "s = sum(w.data * xi for w, xi in zip(nrn.w, xin)) + nrn.b.data\n", "print(f\"\\nneuron[0][0] on input {xin}:\")\n", "print(f\" weights = {[round(w.data, 3) for w in nrn.w]}, bias = {nrn.b.data}\")\n", "print(f\" weighted vote = {s:+.3f} -> tanh = {math.tanh(s):+.3f} (the neuron's output)\")" ] }, { "cell_type": "markdown", "id": "m4c3c", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 7, "id": "8bbbeaa3", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:57.928298Z", "iopub.status.busy": "2026-07-01T08:57:57.928235Z", "iopub.status.idle": "2026-07-01T08:57:58.322147Z", "shell.execute_reply": "2026-07-01T08:57:58.321833Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "loss: 0.0328 -> 0.0013\n", "predictions: [0.99, 0.99, 0.99, 0.99, -0.99, -0.99, -0.99, -0.99]\n", "targets : [1.0, 1.0, 1.0, 1.0, -1.0, -1.0, -1.0, -1.0]\n" ] } ], "source": [ "# toy dataset: two interleaved blobs (label +1 / -1)\n", "xs = [[ 1.0, 1.0], [ 1.5, 0.5], [ 0.5, 1.5], [ 2.0, 1.0],\n", " [-1.0, -1.0], [-1.5, -0.5], [-0.5, -1.5], [-2.0, -1.0]]\n", "ys = [1.0]*4 + [-1.0]*4\n", "\n", "losses = []\n", "for step in range(120):\n", " preds = [model(x) for x in xs] # forward\n", " loss = sum((p - y)**2 for p, y in zip(preds, ys)) # sum of squared errors\n", " for p in model.parameters(): p.grad = 0.0 # reset grads\n", " loss.backward() # backprop (our engine!)\n", " for p in model.parameters(): p.data -= 0.05 * p.grad # SGD step\n", " losses.append(loss.data)\n", "\n", "print(\"loss: %.4f -> %.4f\" % (losses[0], losses[-1]))\n", "print(\"predictions:\", [round(model(x).data, 2) for x in xs])\n", "print(\"targets :\", ys)" ] }, { "cell_type": "markdown", "id": "61c15dea", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "code", "execution_count": 8, "id": "68bd0c83", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:58.323481Z", "iopub.status.busy": "2026-07-01T08:57:58.323403Z", "iopub.status.idle": "2026-07-01T08:57:58.502294Z", "shell.execute_reply": "2026-07-01T08:57:58.501994Z" } }, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "import matplotlib.pyplot as plt\n", "plt.figure(figsize=(6,3)); plt.plot(losses)\n", "plt.title(\"training a neural net on a hand-written autograd engine\"); plt.xlabel(\"step\"); plt.ylabel(\"loss\"); plt.show()" ] }, { "cell_type": "markdown", "id": "61394f6c", "metadata": {}, "source": [ "
Line by line: what each line does\n", "\n", "
" ] }, { "cell_type": "markdown", "id": "m4c4a", "metadata": {}, "source": [ "### See what it learned: the decision boundary\n", "\n", "The loss curve tells you training worked, but not *what* the net now believes. So feed a whole grid of points through the trained model and colour each region by the sign of the output. The eight training points fall on the right sides of a boundary the engine bent to fit them -- learned with nothing but the autograd we wrote at the top of this notebook." ] }, { "cell_type": "code", "execution_count": 9, "id": "m4c4b", "metadata": { "execution": { "iopub.execute_input": "2026-07-01T08:57:58.503391Z", "iopub.status.busy": "2026-07-01T08:57:58.503318Z", "iopub.status.idle": "2026-07-01T08:57:58.852705Z", "shell.execute_reply": "2026-07-01T08:57:58.852301Z" } }, "outputs": [ { "data": { "image/png": "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", "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "# Feed a grid of points through the trained net; colour each by the sign of its output.\n", "xr = [-3 + 6 * i / 43 for i in range(44)]\n", "yr = [-2.5 + 5 * j / 35 for j in range(36)]\n", "Z = [[model([X, Y]).data for X in xr] for Y in yr] # 1584 forward passes\n", "\n", "plt.figure(figsize=(5.2, 4.2))\n", "plt.contourf(xr, yr, Z, levels=[-100, 0, 100], colors=[\"#f4b7b7\", \"#b7c9f4\"])\n", "plt.contour(xr, yr, Z, levels=[0], colors=[\"#333\"], linewidths=1)\n", "plt.scatter([p[0] for p in xs[:4]], [p[1] for p in xs[:4]], c=\"#1c4fd6\", edgecolors=\"k\", s=90, label=\"+1\")\n", "plt.scatter([p[0] for p in xs[4:]], [p[1] for p in xs[4:]], c=\"#d61c1c\", edgecolors=\"k\", s=90, marker=\"s\", label=\"-1\")\n", "plt.legend(); plt.title(\"the decision boundary our engine learned\")\n", "plt.xlabel(\"x1\"); plt.ylabel(\"x2\"); plt.show()" ] }, { "cell_type": "markdown", "id": "m4c4c", "metadata": {}, "source": [ "
Line by line: what each line does\n", "
    \n", "
  • xr, yr: a grid of x and y coordinates covering the plot area.
  • \n", "
  • Z = [[model([X, Y]).data ...]]: run the trained net on every grid point -- 1584 forward passes -- and keep each output number.
  • \n", "
  • plt.contourf(..., levels=[-100, 0, 100]): paint the plane in two colours, split at output 0 -- the model's decision.
  • \n", "
  • plt.contour(..., levels=[0]): draw the boundary line itself, where the net switches from -1 to +1.
  • \n", "
  • plt.scatter(...): the eight training points on top; each sits in the correctly coloured region, so the net separated the two classes.
  • \n", "
\n", "
" ] }, { "cell_type": "markdown", "id": "fbcfa2b1", "metadata": {}, "source": [ "## Recap\n", "\n", "You have built **autograd**: a graph of operations that differentiates itself by multiplying local rates along the chain, and you trained a real, if tiny, neural network on top of it. PyTorch's `.backward()` is exactly this idea, generalized from single numbers to whole **tensors** (a tensor is just a grid of numbers, as in notebook 01) and written in fast, low-level code for speed. From here on we let the framework handle the bookkeeping, but you now know precisely what it is doing underneath.\n", "\n", "Next, notebook 05 covers **self-attention**, the mechanism that lets a token look back over the whole context rather than just one character." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (.venv)", "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.12" } }, "nbformat": 4, "nbformat_minor": 5 }