{
"cells": [
{
"cell_type": "markdown",
"id": "599f6bb6",
"metadata": {},
"source": "# 01 - The math you actually need\n\nAlmost anyone who believes they are \"not a maths person\" can follow this notebook. A neural network relies on only a handful of small ideas, and each one has a plain-English picture sitting behind the symbols. For every idea you will get the picture first, then a small example worked out by hand, and only then the code.\n\nHere is the whole toolbox, six ideas in total:\n\n- **vectors and matrices**, which are just grids of numbers, and the single operation that matters most: **matrix multiply**\n- **the dot product**, a score for how similar two lists of numbers are (this is exactly what attention later uses)\n- **broadcasting**, the rule that lets NumPy do arithmetic on grids of different sizes without slow loops\n- **softmax**, which turns raw scores into probabilities that add up to 1\n- **cross-entropy**, a single number measuring how wrong a guess was\n- **gradients and gradient descent**, the method by which a model actually learns\n\nNone of this is harder than \"multiply some numbers together and add them up.\" We will build each piece in turn."
},
{
"cell_type": "code",
"execution_count": 1,
"id": "01fc47ae",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:02.701573Z",
"iopub.status.busy": "2026-06-11T07:12:02.701375Z",
"iopub.status.idle": "2026-06-11T07:12:02.817698Z",
"shell.execute_reply": "2026-06-11T07:12:02.817145Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"scalar shape=()\n",
"vector shape=(3,)\n",
"matrix shape=(2, 3)\n",
"tensor shape=(2, 3, 4)\n"
]
}
],
"source": [
"import numpy as np\n",
"np.random.seed(0)\n",
"\n",
"# Scalars, vectors, matrices, tensors are all just arrays with different numbers of axes.\n",
"scalar = np.array(3.0)\n",
"vector = np.array([1.0, 2.0, 3.0])\n",
"matrix = np.array([[1.0, 2.0, 3.0],\n",
" [4.0, 5.0, 6.0]])\n",
"tensor = np.random.randn(2, 3, 4) # e.g. (batch, time, features) - the shape we'll use for text\n",
"\n",
"for name, a in [(\"scalar\", scalar), (\"vector\", vector), (\"matrix\", matrix), (\"tensor\", tensor)]:\n",
" print(f\"{name:7} shape={a.shape}\")"
]
},
{
"id": "333dac06",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
import numpy as np: load NumPy, the toolbox for fast math on grids of numbers, and nickname it np so we can type np.something instead of numpy.something.
\n
np.random.seed(0): fix the random-number generator to a known starting point, so \"random\" values come out identical every run. That makes the results reproducible.
\n
scalar = np.array(3.0): a scalar is just a single number (here 3.0). No rows, no columns -- one value. (0 axes.)
\n
vector = np.array([1.0, 2.0, 3.0]): a vector is a row of numbers, like one line of a spreadsheet. The square brackets make a list. (1 axis.)
\n
matrix = np.array([[...], [...]]): a matrix is a grid: rows and columns, like a full spreadsheet. The double brackets mean \"a list of rows\"; this one is 2 rows × 3 columns. (2 axes.)
\n
tensor = np.random.randn(2, 3, 4): a tensor is the general word for a block of numbers with any number of axes. This is a 2×3×4 block of random values -- picture 2 stacked spreadsheets, each 3 rows by 4 columns. It's exactly the shape we'll use for text (sequences × positions × features).
\n
for name, a in [(\"scalar\", scalar), ...]:: a loop that walks the four items, pairing a label name with the thing itself a, so we can print them the same way.
\n
print(f\"{name:7} shape={a.shape}\"): print each one's name and its .shape (NumPy's \"how many along each axis\"). :7 pads the name to 7 characters so the columns line up. You'll see (), (3,), (2, 3), (2, 3, 4): the axis-counts going up.
\n
\n"
},
{
"cell_type": "markdown",
"id": "668abb4e",
"metadata": {},
"source": "## Matrix multiply is the central operation\n\nA **matrix** is simply a grid of numbers arranged in rows and columns. Matrix multiply, written `A @ B` in code, is the operation that almost all of a neural network's work comes down to.\n\nThe picture to hold onto: each number in the answer is one row of `A` combined with one column of `B`. You line them up, multiply the matching entries, and add the results. That \"multiply matching entries and add them\" step is the dot product from the next section, so a matrix multiply is really a whole grid of dot products worked out at once.\n\nThe shape rule is `(n, k) @ (k, m) -> (n, m)`. The two inner numbers (the `k` values) must be equal, because they are the length of the lists being dotted together; they cancel, leaving as many rows as `A` had and as many columns as `B` had.\n\nWorked by hand, the entry `C[0,1]` is row 0 of `A` dotted with column 1 of `B`. Row 0 of `A` is `[1, 2, 3]` and column 1 of `B` is `[0, 1, 1]`, so the result is `1*0 + 2*1 + 3*1 = 5`. The code confirms it."
},
{
"cell_type": "code",
"execution_count": 2,
"id": "f7abd66e",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:02.818985Z",
"iopub.status.busy": "2026-06-11T07:12:02.818876Z",
"iopub.status.idle": "2026-06-11T07:12:02.821568Z",
"shell.execute_reply": "2026-06-11T07:12:02.821122Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"A (2, 3) @ B (3, 2) -> C (2, 2)\n",
"[[ 4. 5.]\n",
" [10. 11.]]\n",
"check C[0,1] = 5.0\n"
]
}
],
"source": [
"A = np.array([[1., 2., 3.],\n",
" [4., 5., 6.]]) # (2, 3)\n",
"B = np.array([[1., 0.],\n",
" [0., 1.],\n",
" [1., 1.]]) # (3, 2)\n",
"C = A @ B # (2, 2)\n",
"print(\"A\", A.shape, \"@ B\", B.shape, \"-> C\", C.shape)\n",
"print(C)\n",
"\n",
"# C[0,1] is row 0 of A dotted with column 1 of B:\n",
"print(\"check C[0,1] =\", float(np.dot(A[0], B[:, 1])))"
]
},
{
"id": "a71fe5a4",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
A = np.array([[1., 2., 3.], [4., 5., 6.]]): a 2-row, 3-column grid. The dot in 1. just means \"treat it as a decimal,\" not a whole number.
\n
B = np.array([[1., 0.], [0., 1.], [1., 1.]]): a 3-row, 2-column grid.
\n
C = A @ B: the star of the show: @ is matrix multiply. NumPy checks the shapes line up ((2,3) and (3,2) share the inner 3) and produces a (2,2) grid. Each output number is one row of A blended with one column of B.
\n
print(\"A\", A.shape, ...): print the shapes so you can watch (2,3) @ (3,2) collapse to (2,2).
\n
A[0]: grab row 0 of A (counting starts at 0), i.e. [1., 2., 3.].
\n
B[:, 1]: grab column 1 of B: the : means \"every row,\" the 1 picks the second column.
\n
np.dot(A[0], B[:, 1]): the dot product of that row and column: multiply matching slots, add them up. We print it to prove it equals C[0,1]: a matrix multiply really is a grid of dot products.
\n
float(...): unwrap NumPy's one-number result into a plain Python number so it prints cleanly.
\n
\n"
},
{
"cell_type": "markdown",
"id": "cb7e6256",
"metadata": {},
"source": "## The dot product: a measure of similarity\n\nTake two lists of numbers of the same length, multiply them entry by entry, and add up the results. That single number is the **dot product**.\n\nThe picture: think of each list as an arrow. The dot product is large and positive when the two arrows point in much the same direction, near zero when they are at right angles (that is, unrelated), and negative when they point in opposite directions. In other words, it is a similarity score that answers \"how aligned are these two?\"\n\nWorked by hand:\n- `[1, 0] . [0.9, 0.1] = 1*0.9 + 0*0.1 = 0.9`. Both arrows point mostly to the right, so the score is large and positive: they are similar.\n- `[1, 0] . [0, 1] = 1*0 + 0*1 = 0`. One points right and the other straight up, a right angle, so the score is zero: they are unrelated.\n\nThis is the heart of attention. A token works out which earlier tokens are relevant to it by taking dot products with them, and then leans on the ones that score highly. (The `cosine` function below is just the dot product rescaled to ignore the length of the arrows, so its value always falls between -1 and 1.)"
},
{
"cell_type": "code",
"execution_count": 3,
"id": "15b316bd",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:02.822761Z",
"iopub.status.busy": "2026-06-11T07:12:02.822669Z",
"iopub.status.idle": "2026-06-11T07:12:02.825117Z",
"shell.execute_reply": "2026-06-11T07:12:02.824702Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"a . b = 0.9 cosine: 0.99 -> similar\n",
"a . c = 0.0 cosine: 0.0 -> unrelated\n"
]
}
],
"source": [
"def cosine(u, v):\n",
" return float(u @ v / (np.linalg.norm(u) * np.linalg.norm(v)))\n",
"\n",
"a = np.array([1., 0.])\n",
"b = np.array([0.9, 0.1]) # similar direction to a\n",
"c = np.array([0., 1.]) # orthogonal to a\n",
"print(\"a . b =\", round(float(a @ b), 2), \" cosine:\", round(cosine(a, b), 2), \"-> similar\")\n",
"print(\"a . c =\", round(float(a @ c), 2), \" cosine:\", round(cosine(a, c), 2), \"-> unrelated\")"
]
},
{
"id": "3de7bad5",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
def cosine(u, v):: define a small reusable function that takes two vectors and returns how aligned they are.
\n
u @ v: for two flat vectors, @ is just the dot product: multiply matching slots and sum.
\n
np.linalg.norm(u): the length of vector u (how long the arrow is). Dividing the dot product by both lengths strips out size and leaves pure direction agreement: that's the cosine, always between -1 and 1.
\n
a = np.array([1., 0.]): an arrow pointing right (along the x-axis).
\n
b = np.array([0.9, 0.1]): an arrow pointing almost the same way as a.
\n
c = np.array([0., 1.]): an arrow pointing straight up -- a right angle to a.
\n
round(float(a @ b), 2): compute the dot product, unwrap it, round to 2 decimals. a·b is large (similar directions); a·c is 0 (unrelated).
\n
\n"
},
{
"cell_type": "markdown",
"id": "96387872",
"metadata": {},
"source": "## Broadcasting: reuse one row everywhere, without a loop\n\nOften two grids are not the same size. Suppose you have a 3-by-4 grid and want to add the same four numbers to each of its three rows. Rather than writing a loop, NumPy handles this by **broadcasting**: it quietly treats the smaller item (here the list of four numbers) as if it were repeated across the missing rows. Nothing is actually copied, so it stays fast; it is only bookkeeping.\n\nThe picture is a single set of settings applied to every row at once, like turning the same four dials on every sample. This comes up constantly, for example when adding a fixed set of numbers (called a **bias**) to every row, or rescaling every column. In the code below, the list `[10, 20, 30, 40]` is added to each of the three rows."
},
{
"cell_type": "code",
"execution_count": 4,
"id": "ec474b7d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:02.826295Z",
"iopub.status.busy": "2026-06-11T07:12:02.826202Z",
"iopub.status.idle": "2026-06-11T07:12:02.828195Z",
"shell.execute_reply": "2026-06-11T07:12:02.827888Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"[[11. 21. 31. 41.]\n",
" [11. 21. 31. 41.]\n",
" [11. 21. 31. 41.]]\n",
"shapes: (3, 4) + (4,) -> (3, 4)\n"
]
}
],
"source": [
"X = np.ones((3, 4)) # 3 rows, 4 features\n",
"bias = np.array([10., 20., 30., 40.]) # one value per feature\n",
"print((X + bias)) # bias is added to every row, no loop needed\n",
"print(\"shapes:\", X.shape, \"+\", bias.shape, \"->\", (X + bias).shape)"
]
},
{
"id": "2b5d0e8a",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
X = np.ones((3, 4)): a 3-row, 4-column grid filled entirely with 1s. (The size is passed as one pair (3, 4).)
\n
bias = np.array([10., 20., 30., 40.]): a single row of 4 numbers, one per column.
\n
X + bias: the shapes don't match ((3,4) vs just (4,)), so NumPy broadcasts: it quietly reuses those 4 bias numbers on every one of the 3 rows. You wrote no loop, and none runs -- it's just bookkeeping.
\n
print((X + bias).shape): confirms the result is still (3, 4): same grid, each row shifted by the bias.
\n
\n"
},
{
"cell_type": "markdown",
"id": "14eaeb3b",
"metadata": {},
"source": "## Softmax: turning numbers into probabilities\n\nA model produces raw scores called **logits**, which can be any real numbers, such as `2.0, 1.0, 0.1, -1.0`. We need to convert these into **probabilities**: values that are all positive and add up to 1 (that is, to 100%). Converting scores into probabilities is the whole job of softmax.\n\n$$\\text{softmax}(x)_i = \\frac{e^{x_i}}{\\sum_j e^{x_j}}$$\n\nReading the formula slowly: $e^{x_i}$ means the fixed number $e$ (about 2.718) raised to the power of score $i$. The symbol $\\sum_j$ (a capital Greek letter sigma) is shorthand for \"add the following up over every score $j$.\" So each output is one score's $e$-value divided by the total of all the $e$-values, which is precisely a share of the whole; and a share of the whole is what a probability is.\n\nWhy use $e$ at all? It does two useful things at once. It makes every value positive, so there are no negative probabilities, and it widens the gaps, so a clearly higher score ends up with a clearly larger share.\n\nWorked by hand for `[2.0, 1.0, 0.1, -1.0]`: the $e$-values are about `7.39, 2.72, 1.11, 0.37`, which add up to `11.58`. Dividing each by `11.58` gives `0.64, 0.24, 0.10, 0.03`, all positive and summing to 1. The top score of 2.0 ends up with 64% of the total.\n\n(The line `x - x.max()` in the code is a safety step. Subtracting the same number from every score does not change the result, but it stops $e$ raised to a large power from producing a number too big for the computer to handle.)"
},
{
"cell_type": "code",
"execution_count": 5,
"id": "bdcd7982",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:02.829513Z",
"iopub.status.busy": "2026-06-11T07:12:02.829424Z",
"iopub.status.idle": "2026-06-11T07:12:02.831745Z",
"shell.execute_reply": "2026-06-11T07:12:02.831302Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"probs : [0.638 0.235 0.095 0.032]\n",
"sum : 1.0 (always 1)\n"
]
}
],
"source": [
"def softmax(x, axis=-1):\n",
" x = x - x.max(axis=axis, keepdims=True) # numerical stability\n",
" e = np.exp(x)\n",
" return e / e.sum(axis=axis, keepdims=True)\n",
"\n",
"logits = np.array([2.0, 1.0, 0.1, -1.0])\n",
"p = softmax(logits)\n",
"print(\"probs :\", np.round(p, 3))\n",
"print(\"sum :\", round(float(p.sum()), 6), \"(always 1)\")"
]
},
{
"id": "55123ed3",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
def softmax(x, axis=-1):: define softmax. axis=-1 is a default: \"if the caller doesn't say otherwise, work along the last axis\" (across each row of scores).
\n
x = x - x.max(axis=axis, keepdims=True): subtract the biggest score from every score. This doesn't change the final answer, but it stops exp of a big number from overflowing. keepdims=True keeps the shape so the subtraction broadcasts cleanly.
\n
e = np.exp(x): raise e (~2.718) to each score, all at once. This makes everything positive and exaggerates the gaps.
\n
return e / e.sum(...): divide each value by the total of all of them, so the results add up to 1: a share of the whole, which is exactly a probability.
\n
logits = np.array([2.0, 1.0, 0.1, -1.0]): four raw scores to feed in.
\n
np.round(p, 3) / p.sum(): print the probabilities (rounded) and their sum, which is always 1.
import matplotlib.pyplot as plt: load the plotting toolbox, nicknamed plt.
\n
fig, ax = plt.subplots(1, 2, figsize=(8, 3)): make a figure with 1 row of 2 side-by-side panels; ax[0] is the left panel, ax[1] the right.
\n
ax[0].bar(labels, logits): a bar chart on the left: one bar per label, heights = the raw logits.
\n
ax[1].bar(labels, p): the same labels on the right, but heights = the softmax probabilities.
\n
ax[1].set_ylim(0, 1): pin the right axis to 0..1, the range probabilities live in.
\n
plt.tight_layout(); plt.show(): tidy the spacing and display. The two panels let you see raw scores become probabilities.
\n
\n"
},
{
"cell_type": "markdown",
"id": "9114de87",
"metadata": {},
"source": "## Cross-entropy: how wrong was the guess?\n\nThe model gives a probability to every possible next token, and reality then reveals the one token that was actually correct. **Cross-entropy** turns that into a single number, and it looks only at the probability the model gave to the correct answer:\n\n$$\\text{loss} = -\\log p_{\\text{correct}}$$\n\nThe picture is the model's *surprise*. If it gave the correct token a probability of 99%, it had essentially predicted it, so the loss is tiny. If it gave the correct token only 1%, it was caught out, so the loss is large. Training amounts to making the model less surprised by the truth, over and over.\n\nWhy the $-\\log$, the negative logarithm? A logarithm answers the question \"what power must I raise a fixed base number to?\", and the key fact here is that $\\log(1) = 0$, so a perfect 100% guess costs nothing. As the probability falls towards 0 the logarithm becomes a larger and larger negative number, and the minus sign in front turns that into a larger and larger positive loss. So a confident and correct guess is cheap, a confident but wrong guess is expensive, and everything in between sits smoothly on that scale.\n\nWorked by hand with logits `[2.0, 1.0, 0.1, -1.0]` (which softmax turned into `0.64, 0.24, 0.10, 0.03`):\n- if the correct token is number 0 (probability 0.64), the loss is `-log(0.64)`, about `0.45`: small, because it was right.\n- if the correct token is number 3 (probability 0.03), the loss is `-log(0.03)`, about `3.45`: large, because it was confidently wrong.\n\nThat single number is the entire training signal used throughout the course."
},
{
"cell_type": "code",
"execution_count": 7,
"id": "fec5afd4",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:03.268094Z",
"iopub.status.busy": "2026-06-11T07:12:03.267972Z",
"iopub.status.idle": "2026-06-11T07:12:03.270215Z",
"shell.execute_reply": "2026-06-11T07:12:03.269798Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"loss if correct token is 0 (confident, right): 0.449\n",
"loss if correct token is 3 (confident, wrong): 3.449\n"
]
}
],
"source": [
"def cross_entropy(logits, target_idx):\n",
" p = softmax(logits)\n",
" return -np.log(p[target_idx])\n",
"\n",
"logits = np.array([2.0, 1.0, 0.1, -1.0]) # model likes token 0 most\n",
"print(\"loss if correct token is 0 (confident, right):\", round(float(cross_entropy(logits, 0)), 3))\n",
"print(\"loss if correct token is 3 (confident, wrong):\", round(float(cross_entropy(logits, 3)), 3))"
]
},
{
"id": "ddeccd8e",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
def cross_entropy(logits, target_idx):: define the loss. It takes the raw scores and the position of the correct answer.
\n
p = softmax(logits): reuse the function from above: scores become probabilities.
\n
return -np.log(p[target_idx]): pick out the probability the model gave the correct token, then take its negative log: near 0 when that probability is high, large when it's small. That single number is the loss.
\n
cross_entropy(logits, 0): the correct answer is token 0, the model's favorite → small loss (confident and right).
\n
cross_entropy(logits, 3): the correct answer is token 3, the model's least favorite → big loss (confident and wrong).
\n
\n"
},
{
"cell_type": "markdown",
"id": "a004fdb0",
"metadata": {},
"source": "## Gradients and gradient descent: how it learns\n\nThis is the method that drives every model in the course, so it is worth seeing in full.\n\nAn analogy. Imagine standing on a hillside in thick fog, trying to reach the lowest point. You cannot see, but you can feel which way the ground slopes under your feet. So you take a small step downhill, feel again, and step again. Repeat this enough times and you arrive at the bottom. That is gradient descent.\n\n- the **loss** is your height on the hill, and we want it low,\n- the **gradient** is the slope under your feet, telling you which way is uphill and how steep it is,\n- a **step** nudges the model's numbers a little in the downhill direction, which is the direction opposite the gradient.\n\nA function we can picture easily is $f(x) = x^2$, a smooth bowl shape with its lowest point at $x = 0$. Its slope at any point is given by the formula $f'(x) = 2x$ (this slope formula is called the *derivative*). At $x = 8$ the slope is $2 \\times 8 = 16$: steep, and tilting upward to the right, so downhill is to the left.\n\nOne step worked by hand, using a step size (called the **learning rate**) of `0.1`. The rule is: new x = old x minus learning rate times slope, which gives `8 - 0.1 * 16 = 6.4`. Starting again from `6.4`, the slope is `12.8`, so the next point is `6.4 - 0.1 * 12.8 = 5.12`, then `4.096`, and so on. Each step lands at `0.8` times the previous value, marching steadily towards 0. After 40 steps it has essentially arrived (about `0.00106`).\n\nThe learning rate is the size of each step, and the choice matters. Too large, and you leap straight over the bottom and bounce out of the valley, so the loss grows instead of shrinking; too small, and you inch along and take far too long to arrive. A value of `0.1` is a comfortable stride for this example.\n\nThe code also shows a *numerical* way to find the slope, so you need not take the calculus on trust. Nudge `x` by a tiny amount each way, measure how much `f` rises or falls, and divide the rise by the distance covered; that ratio is the slope, the same `16` the formula gives. That is all the function `grad_numerical` does. In a real network the computer works out these slopes automatically, which is the subject of notebook 04."
},
{
"cell_type": "code",
"execution_count": 8,
"id": "0e482cd9",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:03.271415Z",
"iopub.status.busy": "2026-06-11T07:12:03.271324Z",
"iopub.status.idle": "2026-06-11T07:12:03.273746Z",
"shell.execute_reply": "2026-06-11T07:12:03.273243Z"
}
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"started at 8.0, ended at x = 0.00106 (true minimum is 0)\n",
"numerical grad at x=8 was 16.0 (exact 2*8 = 16)\n"
]
}
],
"source": [
"def f(x): return x**2\n",
"def grad_numerical(f, x, h=1e-5):\n",
" return (f(x + h) - f(x - h)) / (2*h) # slope of a tiny secant line\n",
"\n",
"x = 8.0 # start far from the minimum\n",
"lr = 0.1 # learning rate: how big a step we take\n",
"history = [x]\n",
"for step in range(40):\n",
" g = grad_numerical(f, x) # which way is downhill (and how steep)\n",
" x = x - lr * g # step downhill\n",
" history.append(x)\n",
"print(\"started at 8.0, ended at x =\", round(x, 5), \" (true minimum is 0)\")\n",
"print(\"numerical grad at x=8 was\", round(grad_numerical(f, 8.0), 4), \" (exact 2*8 = 16)\")"
]
},
{
"id": "5429fd34",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
def f(x): return x**2: the bowl-shaped function to minimize. ** means \"to the power of,\" so this is x², lowest at x=0.
\n
def grad_numerical(f, x, h=1e-5):: estimate the slope without calculus.
\n
(f(x + h) - f(x - h)) / (2*h): nudge x a hair (1e-5 = 0.00001) each way, see how much f rises, divide by the distance: that's rise-over-run, the slope.
\n
x = 8.0: start far from the bottom.
\n
lr = 0.1: the learning rate: how big a step we take each time.
\n
history = [x]: a list recording every position visited (so the next cell can draw the path). history.append(x) adds each new spot to the end.
\n
for step in range(40):: repeat 40 times.
\n
g = grad_numerical(f, x): measure the downhill direction and steepness at the current x.
\n
x = x - lr * g: step against the slope (downhill). The right side is computed first, then stored back in x.
\n
round(x, 5): after 40 steps x has marched from 8.0 to nearly 0, the minimum.
\n
\n"
},
{
"cell_type": "code",
"execution_count": 9,
"id": "8e4b611d",
"metadata": {
"execution": {
"iopub.execute_input": "2026-06-11T07:12:03.274916Z",
"iopub.status.busy": "2026-06-11T07:12:03.274821Z",
"iopub.status.idle": "2026-06-11T07:12:03.321703Z",
"shell.execute_reply": "2026-06-11T07:12:03.321083Z"
}
},
"outputs": [
{
"data": {
"image/png": "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7LPPqvchK5fKSqfbtm3Djh07DK0f2ZHnS/l/+OEH1KxZU91et24dOnTokOPySADl7++PrVu3omTJkggICEB8fLx6bkxMjHpMmTJlVP16e3urupUARezdu1cFaFInrVu3xqVLlwyBWsZ6lgDj008/xezZs7FgwQIVvFy9elWtm7J27Vr069dPBVMS6HFtFMuQlJKGtceuG23ipYHOzOV0LfaCFhIRp6v02qZCX2ueHq/4+Hidv7+/utZLjYnTBZRspclF/nZOREVF6ezs7HSrV6827IuIiNA5OTnpXn31VcO+ChUq6Pr06fPI15s9e7aucePGhtt+fn66l156KdNjfH19dfXr1zfcHjFihK53795qOyEhQf3tffv2ZXrO6NGjdYMGDVLbu3btUv93d+zYYbh/8+bNap++/t95551Mf+NBSpUqpfv0008Nt5OTk3Vly5bNVXl69uypGzVqVLav/8033+hcXV11d+7cyfb+jh076mbOnJlp388//6zKpSfv68033zTcjomJUfu2bt2aqT7Cw8Nz9fkk07b5ZIg6jzT9cLsuOSXVaM+hbJkoJKXciqgmqZ3nbqomqte71SysP0Uas3JyVC0EuXHtyReQfP7K/1sm1AtZwb5GJZTZuihXfzsnLl++rLolmjVrZtgnv+KrV69+32OleT6rVatWqV/V8otafoVL87z8OtY7e/asajXIyM/PD7t27cq2PPKrPi4uDk888cR9ORoNGzbMtK9evXqGbemeETdv3kT58jlblyAyMhI3btyAr6+vYZ+tra16n/qujpyU58UXX1QtA9Li0LlzZ9VlI90u4vjx4+px0sWRHemKkdaUjz76KFPXi3QFyd+Vbo2s71W6SqSO5b2S5VpxL5G/f5OysLUx3swEBhOFaGCz8iqYWHv0GqZ0rq5mLSPzo/rrc9jVoFd8+miVIyEBhAoo7l0Xmz4a1rl8rYImJ7GM9u/fr5rbpQm+S5cuKghZuXKl6irIKwlIxObNm1XXQEYODg6ZbtvZ2Rm29dMH67sPCkpOytO1a1fV5bBlyxZs374dHTt2xLhx41S30KO6HOT1pf769u17332SQ5Hde9W/34J+r2Q6gu7E4Z+A22p7QBPjWdQrOzy7FaL21T3gVdQBd2KTsN0/rDD/FJkYlx5t4bXkQ9jX8oGVg7269lr6EVy6tymUvye5C3KiOnz4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",
"text/plain": [
""
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"import matplotlib.pyplot as plt\n",
"xs = np.linspace(-9, 9, 200)\n",
"plt.figure(figsize=(6, 4))\n",
"plt.plot(xs, f(xs), label=\"f(x) = x^2\")\n",
"plt.plot(history, [f(v) for v in history], \"o-\", color=\"crimson\", ms=4, label=\"gradient descent\")\n",
"plt.legend(); plt.title(\"rolling downhill to the minimum\"); plt.xlabel(\"x\"); plt.ylabel(\"loss\")\n",
"plt.show()"
]
},
{
"id": "de1417f8",
"cell_type": "markdown",
"metadata": {},
"source": "Line by line: what each line does\n
\n
import matplotlib.pyplot as plt: the plotting toolbox (numpy is already imported earlier in the notebook).
\n
np.linspace(-9, 9, 200): 200 evenly spaced x-values from -9 to 9, for drawing a smooth curve.
\n
plt.plot(xs, f(xs)): draw the bowl itself; f(xs) squares all 200 values at once.
\n
plt.plot(history, [f(v) for v in history], \"o-\", ...): draw the path descent took. [f(v) for v in history] computes the height at each visited x; \"o-\" means dots joined by lines.
\n
plt.legend(); plt.show(): show the labels and render -- you see the red dots rolling down into the valley.
\n
\n"
},
{
"cell_type": "markdown",
"id": "c866da63",
"metadata": {},
"source": "## Recap\n\nThe whole toolbox, in plain words:\n\n- **matrix multiply**: combine rows and columns (a whole grid of dot products at once)\n- **dot product**: one number for \"how aligned, or similar, are these two lists?\"\n- **broadcasting**: apply one small list to every row, without a loop\n- **softmax**: turn raw scores into probabilities that add up to 1\n- **cross-entropy**: one number for \"how surprised was the model by the truth?\"\n- **gradient descent**: feel the downhill slope, take a step, and repeat until the loss is low\n\nThat really is the maths you need. Everything more advanced later in the course is built out of these six ideas.\n\nNext, notebook 02 turns raw text into the numbers a model can actually work with."
}
],
"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
}