{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "525cc0bc",
   "metadata": {},
   "source": [
    "# Ch 13 — Sequences and Time Series (notebook)\n",
    "\n",
    "`[← 12 cnns]` · **this notebook** · `[14 nlp-with-rnns-and-attention →]`\n",
    "\n",
    "Runs top-to-bottom in ~5 min on free Colab CPU. Last verified 2026-06-11.\n",
    "\n",
    "**What you'll build**\n",
    "- Three classical forecasting baselines (naive, seasonal-naive, moving-average) on a synthetic ridership series whose trend and seasonality you *know*, so you can grade every forecast against ground truth.\n",
    "- The autocorrelation function from its definition, checked against `statsmodels`, used to *read* the 12-month period straight off the data.\n",
    "- A time-respecting train/val/test split and a windowing function, both property-tested for the one bug that quietly leaks the future into the past.\n",
    "- A context-window forecaster (an NPLM-style MLP over the last `L` steps) that has to *beat* the seasonal-naive baseline to earn its keep.\n",
    "- A vanilla RNN cell in ~12 lines of NumPy, its one-step BPTT gradient checked against `torch.autograd`, and the exploding-gradient failure staged then fixed with norm clipping.\n",
    "- A char-level next-token model on an embedded names corpus, your hand-rolled recurrence reconciled element-for-element with `nn.RNN`, sampled at the end as the payoff.\n",
    "\n",
    "**How this notebook works.** Code cells with a `# TODO` are yours to fill in. Run the cell to grade yourself: `[ ok ]` passed, `[FAIL]` shows what went wrong, `[ -- ]` means not attempted yet. Every exercise has a hint ladder (open only as many as you need) and a folded solution below it. The notebook runs top-to-bottom even if you fill in nothing, because the solution cells redefine the functions so the later cells work. See Ch 00 for the full protocol.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20017b39",
   "metadata": {},
   "source": [
    "## Before you start\n",
    "\n",
    "1. A shop's sales are higher every December. You forecast tomorrow as \"same as today\". On the data with a strong yearly cycle, what simple forecast will usually beat you? <details><summary>Answer</summary>The seasonal-naive forecast: predict each step as the value one full period ago (12 months back). When the dominant signal is the cycle, copying last year's same-month value beats copying yesterday. Half of Part 1 is making this concrete.</details>\n",
    "2. You train an RNN by unrolling it over 200 timesteps and calling `loss.backward()`. The gradient that reaches step 1 is a product of ~200 Jacobians. If each has norm slightly below 1, what happens to that gradient? <details><summary>Answer</summary>It vanishes: a product of 200 numbers each < 1 shrinks toward zero exponentially, so step 1 receives almost no learning signal. If each norm is slightly above 1 it explodes instead. Both are the structural reason vanilla RNNs struggle with long-range dependencies, and the reason we clip.</details>\n",
    "3. Predict before you run: you have 120 months of data and you split it 80/20 with sklearn's default `train_test_split`. Why is that wrong for forecasting? <details><summary>Answer</summary>The default shuffles, so test months get scattered before training months. The model then \"predicts\" a March it has already seen surrounded by its neighbours. Forecasting demands a *time-respecting* split: train on the past, test on the future, never shuffle. We property-test exactly this.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0151dfba",
   "metadata": {},
   "source": [
    "## Setup\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "48d0a011",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:39.346505Z",
     "iopub.status.busy": "2026-06-10T19:46:39.346422Z",
     "iopub.status.idle": "2026-06-10T19:46:41.288226Z",
     "shell.execute_reply": "2026-06-10T19:46:41.287895Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "numpy 2.2.6 · torch 2.12.0+cpu · statsmodels absent (one optional cross-check will print-and-skip) · device cpu\n"
     ]
    }
   ],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import torch\n",
    "try:\n",
    "    import statsmodels\n",
    "    _sm = statsmodels.__version__\n",
    "except Exception:\n",
    "    _sm = \"absent (one optional cross-check will print-and-skip)\"\n",
    "device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
    "print(f\"numpy {np.__version__} · torch {torch.__version__} · statsmodels {_sm} · device {device}\")\n",
    "if np.__version__ < \"2.0\":\n",
    "    print(\"WARN: written for NumPy 2.x; older versions may differ slightly\")\n",
    "if device == \"cuda\":\n",
    "    print(\"note: this notebook is CPU-canonical; CUDA is fine but bitwise outputs may differ\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "259d6f69",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.290854Z",
     "iopub.status.busy": "2026-06-10T19:46:41.290646Z",
     "iopub.status.idle": "2026-06-10T19:46:41.309187Z",
     "shell.execute_reply": "2026-06-10T19:46:41.308436Z"
    }
   },
   "outputs": [],
   "source": [
    "import os, random\n",
    "SEED = 0\n",
    "FAST = bool(os.environ.get('NB_FAST'))  # CI smoke mode: ~10x fewer steps, same code paths\n",
    "# Training budgets. Both settings are documented in the experiment log so you know\n",
    "# the expected loss for whichever you ran.\n",
    "NPLM_STEPS = 60 if FAST else 600    # context-window MLP forecaster gradient steps (fast: no recurrence)\n",
    "RNN_STEPS  = 30 if FAST else 120    # vanilla-RNN regression steps; the per-step Python recurrence is the CPU cost, so kept modest\n",
    "CHAR_STEPS = 40 if FAST else 200    # char-level names model training steps\n",
    "GEN_SEED   = SEED + 10              # offset seed for sampling, so generation never perturbs training (Karpathy's +10)\n",
    "rng = np.random.default_rng(SEED)\n",
    "torch.manual_seed(SEED)\n",
    "random.seed(SEED)\n",
    "\n",
    "# ── house self-check harness (identical across all chapter notebooks) ──\n",
    "import numpy as _np\n",
    "\n",
    "def check(label, test_fn, required=False):\n",
    "    \"\"\"Run one self-check. test_fn raises AssertionError (with a teaching\n",
    "    message) on failure, NotImplementedError if the stub is unfilled.\n",
    "    required=True is used only in solution cells; it is what CI grades.\"\"\"\n",
    "    try:\n",
    "        test_fn()\n",
    "    except NotImplementedError:\n",
    "        if required:\n",
    "            raise AssertionError(f\"{label}: reference solution incomplete\")\n",
    "        print(f\"[ -- ] {label}: not attempted yet — fill in the TODO above, then re-run.\")\n",
    "        return False\n",
    "    except AssertionError as e:\n",
    "        if required:\n",
    "            raise\n",
    "        print(f\"[FAIL] {label}: {e}\")\n",
    "        return False\n",
    "    print(f\"[ ok ] {label}\")\n",
    "    return True\n",
    "\n",
    "def attempted(*vals):\n",
    "    \"\"\"Treat None placeholders as 'not attempted'.\"\"\"\n",
    "    if any(v is None for v in vals):\n",
    "        raise NotImplementedError\n",
    "\n",
    "def check_shape(x, want):\n",
    "    assert tuple(x.shape) == tuple(want), \\\n",
    "        f\"shape {tuple(x.shape)}, expected {tuple(want)} — check your reshape/transpose order\"\n",
    "\n",
    "def check_close(got, want, atol=1e-5, rtol=1e-4, msg=\"\"):\n",
    "    g, w = _np.asarray(got, dtype=float), _np.asarray(want, dtype=float)\n",
    "    assert g.shape == w.shape, f\"shape {g.shape} vs expected {w.shape}. {msg}\"\n",
    "    bad = ~_np.isclose(g, w, atol=atol, rtol=rtol)\n",
    "    assert not bad.any(), \\\n",
    "        f\"{bad.mean():.2%} of values wrong (max diff {abs(g - w).max():.3g}). {msg}\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f8a88c50",
   "metadata": {},
   "source": [
    "> **Note:** seeds make this notebook's printed numbers reproduce on CPU. Library versions and BLAS threading can shift the last digit or two; quoted numbers hold for the pinned environment. If your val MAE is 18.4 and the page says 18.3, you did nothing wrong.\n",
    "\n",
    "> **Caveat (data provenance).** The chapter's house file is a `names.txt` corpus; it is not vendored in this repo, and downloading it on the run-all path would break the offline contract. So the names corpus below is *embedded inline* (deterministic, offline-proof) and the ridership series is *generated* from a seeded RNG with documented trend and seasonality. The upside is that both have known ground truth, which makes every forecast and every statistic in this notebook checkable against the thing that produced it.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1a4496a",
   "metadata": {},
   "source": [
    "## The map\n",
    "\n",
    "> **Part 1 — Classical baselines first.** Build a synthetic ridership series with a *known* trend and 12-month cycle, then the naive, seasonal-naive, and moving-average forecasts, scored by MAE. The autocorrelation function, from its definition, reads the period off the data.\n",
    "> **Part 2 — Windowing without leaking the future.** A time-respecting train/val/test split and a sliding-window tensor builder, both property-tested for the off-by-one that leaks tomorrow into today.\n",
    "> **Part 3 — The context-window forecaster (NPLM).** An MLP over the last `L` values, the simplest learned sequence model, which must *beat* seasonal-naive to justify itself.\n",
    "> **Part 4 — A vanilla RNN from scratch.** The recurrence in ~12 lines of NumPy, one step of BPTT checked against `torch.autograd`, and the exploding-gradient failure staged then fixed with norm clipping.\n",
    "> **Part 5 — PyTorchify and a names model.** Reconcile your hand-rolled recurrence with `nn.RNN` element-for-element, then train a char-level next-token model on the embedded names and sample from it.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b108a2ad",
   "metadata": {},
   "source": [
    "## Part 1 — Classical baselines first\n",
    "\n",
    "> **Objectives.** Generate the anchor ridership series with ground-truth structure you control, implement three classical forecasts and score them by mean absolute error, and compute the autocorrelation function from its definition to recover the seasonal period straight from the data.\n",
    "\n",
    "A forecasting model earns the right to exist by beating a forecast a child could make. The draft's rule (`§11 Time-series forecasting in practice`) is blunt: implement the naive and seasonal-naive baselines *before* reaching for an RNN. Half the published deep-learning forecasting wins evaporate when someone finally runs seasonal-naive. So we build the baselines first, and every later model is measured against them.\n",
    "\n",
    "We start with synthetic data because it hands us the answer. The series is monthly ridership for ten years: a linear growth trend, a 12-month seasonal swing, and Gaussian noise. We *know* the period is 12, so when the autocorrelation function says 12, we know our code is right.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "6c24ff7a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.310293Z",
     "iopub.status.busy": "2026-06-10T19:46:41.310158Z",
     "iopub.status.idle": "2026-06-10T19:46:41.313412Z",
     "shell.execute_reply": "2026-06-10T19:46:41.312785Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "ridership series: 120 months, range [161, 374], mean 272\n"
     ]
    }
   ],
   "source": [
    "# Anchor dataset #1: synthetic monthly ridership. Ground-truth structure is KNOWN:\n",
    "#   level 200, +1.2 riders/month trend, 12-month seasonal sine of amplitude 40, noise sd 8.\n",
    "N_MONTHS = 120            # 10 years of monthly data\n",
    "PERIOD = 12               # the true seasonal period (we will recover this from the ACF)\n",
    "t = np.arange(N_MONTHS)\n",
    "trend = 200 + 1.2 * t                                  # linear growth\n",
    "season = 40 * np.sin(2 * np.pi * t / PERIOD)           # yearly cycle, amplitude 40\n",
    "noise = rng.normal(0, 8, size=N_MONTHS)                # observation noise, sd 8\n",
    "ridership = trend + season + noise\n",
    "print(f\"ridership series: {ridership.shape[0]} months, \"\n",
    "      f\"range [{ridership.min():.0f}, {ridership.max():.0f}], mean {ridership.mean():.0f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "27763183",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.316486Z",
     "iopub.status.busy": "2026-06-10T19:46:41.316275Z",
     "iopub.status.idle": "2026-06-10T19:46:41.570949Z",
     "shell.execute_reply": "2026-06-10T19:46:41.570657Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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",
      "text/plain": [
       "<Figure size 1000x400 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# viz: the series, with its three known components drawn underneath\n",
    "fig, ax = plt.subplots(figsize=(10, 4))\n",
    "ax.plot(t, ridership, color=\"#1E40FF\", lw=1.5, label=\"observed ridership\")\n",
    "ax.plot(t, trend, ls=\"--\", c=\"#888\", label=\"true trend\")\n",
    "ax.plot(t, trend + season, ls=\":\", c=\"#E0115F\", label=\"trend + season (noise-free)\")\n",
    "ax.set_xlabel(\"month\"); ax.set_ylabel(\"riders\"); ax.set_title(\"synthetic ridership (known structure)\")\n",
    "ax.legend(loc=\"upper left\"); plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67d1b797",
   "metadata": {},
   "source": [
    "> **Interpretation.** The blue line wobbles around a noise-free signal we drew ourselves: a rising line with a yearly sine on top. Because we know the generator, any forecast's error has a floor (the noise sd of 8) that nothing can beat. A model that reports MAE near 8 has extracted everything learnable; one near 40 has missed the season entirely.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "87d1b79a",
   "metadata": {},
   "source": [
    "### The forecasts: naive and seasonal-naive\n",
    "\n",
    "The naive forecast says tomorrow equals today: $\\hat{x}_{t+1} = x_t$. The seasonal-naive forecast says tomorrow equals one full period ago: $\\hat{x}_{t+1} = x_{t+1-s}$ with $s = 12$. On a series whose dominant signal is the 12-month cycle, copying last year's same month should crush copying last month.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "44988f4b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.575435Z",
     "iopub.status.busy": "2026-06-10T19:46:41.575268Z",
     "iopub.status.idle": "2026-06-10T19:46:41.583416Z",
     "shell.execute_reply": "2026-06-10T19:46:41.582861Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "naive (last value)      MAE =  15.1\n",
      "seasonal-naive (t-12)   MAE =  15.9\n"
     ]
    }
   ],
   "source": [
    "# The naive forecast aligned for scoring: prediction[i] = series[i-1], so we\n",
    "# can only score from index 1 onward. (Index 0 has no \"yesterday\".)\n",
    "def naive_forecast(series):\n",
    "    return series[:-1]            # predicts series[1:] from series[:-1]\n",
    "\n",
    "def seasonal_naive_forecast(series, period):\n",
    "    return series[:-period]       # predicts series[period:] from series[:-period]\n",
    "\n",
    "def mae(pred, true):\n",
    "    return float(np.abs(np.asarray(pred) - np.asarray(true)).mean())\n",
    "\n",
    "naive_mae = mae(naive_forecast(ridership), ridership[1:])\n",
    "snaive_mae = mae(seasonal_naive_forecast(ridership, PERIOD), ridership[PERIOD:])\n",
    "print(f\"naive (last value)      MAE = {naive_mae:5.1f}\")\n",
    "print(f\"seasonal-naive (t-12)   MAE = {snaive_mae:5.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6a749d19",
   "metadata": {},
   "source": [
    "> **Interpretation.** Seasonal-naive wins by a wide margin because the swing it copies (amplitude 40) is much larger than the month-to-month trend step (1.2). The naive forecast pays the full seasonal difference every month; seasonal-naive only pays the trend drift over 12 months plus noise. This number, the seasonal-naive MAE, is the bar every learned model must clear.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d57bbb8f",
   "metadata": {},
   "source": [
    "### Exercise 13.1 — The moving-average baseline\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "Implement `moving_average_forecast(series, window)`: predict each point as the mean of the previous `window` points. Return an array aligned so that `pred[i]` forecasts `series[i + window]` (you can only start once you have `window` past points). The check compares your output against a hand-computed toy value and against the naive forecast at `window=1`.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "d33a7879",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.584350Z",
     "iopub.status.busy": "2026-06-10T19:46:41.584258Z",
     "iopub.status.idle": "2026-06-10T19:46:41.592712Z",
     "shell.execute_reply": "2026-06-10T19:46:41.592179Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.1 moving average (toy): not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 13.1 MA(1) == naive: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def moving_average_forecast(series, window):\n",
    "    \"\"\"Predict series[window:] as the rolling mean of the previous `window` points.\n",
    "\n",
    "    Returns an array of length len(series) - window, where out[i] is the mean of\n",
    "    series[i : i+window], i.e. the forecast for series[i+window].\n",
    "    \"\"\"\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    # TODO 1: for each start index i from 0 to len(series)-window-1, take the mean\n",
    "    #         of series[i : i+window]. A vectorized cumulative-sum trick works, but\n",
    "    #         a plain list comprehension is fine and clearer.\n",
    "    preds = None\n",
    "    attempted(preds)\n",
    "    return np.asarray(preds, dtype=float)\n",
    "\n",
    "def _ma_toy():\n",
    "    # series 1..6, window 3: first prediction is mean(1,2,3)=2.0, forecasting series[3]=4.\n",
    "    toy = np.array([1, 2, 3, 4, 5, 6], dtype=float)\n",
    "    got = moving_average_forecast(toy, 3)\n",
    "    check_close(got, [2.0, 3.0, 4.0],\n",
    "                msg=\"window-3 MA of 1..6 is means(1,2,3),(2,3,4),(3,4,5) = 2,3,4\")\n",
    "\n",
    "def _ma_equals_naive_at_1():\n",
    "    # window=1 mean of one point IS that point, so MA(window=1) == naive forecast.\n",
    "    got = moving_average_forecast(ridership, 1)\n",
    "    check_close(got, naive_forecast(ridership), msg=\"MA with window=1 must equal the naive forecast\")\n",
    "\n",
    "check(\"13.1 moving average (toy)\", _ma_toy)\n",
    "check(\"13.1 MA(1) == naive\", _ma_equals_naive_at_1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2c9b5e9c",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>You need one prediction per window position. The window slides from the start to `len(series) - window`. Each prediction is just `series[i:i+window].mean()`.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "preds = [series[i:i+window].mean() for i in range(len(series) - window)]\n",
    "```\n",
    "The range stops at `len(series) - window` so the last window `series[-window:]` is the final one used.</details>\n",
    "\n",
    "<details><summary>Help — \"my output is one element too long / too short\"</summary>The output length is `len(series) - window`. If you got `len(series) - window + 1` you let `i` run one step too far; if `len(series) - window - 1` you stopped one short. Use `range(len(series) - window)`.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "cbe2e59f",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.593667Z",
     "iopub.status.busy": "2026-06-10T19:46:41.593577Z",
     "iopub.status.idle": "2026-06-10T19:46:41.602245Z",
     "shell.execute_reply": "2026-06-10T19:46:41.599603Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.1 moving average (toy)\n",
      "[ ok ] 13.1 MA(1) == naive\n",
      "moving-average (window 3) MAE =  24.2  (vs naive 15.1, seasonal-naive 15.9)\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines moving_average_forecast; the checks below re-verify it.\n",
    "def moving_average_forecast(series, window):\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    preds = [series[i:i+window].mean() for i in range(len(series) - window)]\n",
    "    return np.asarray(preds, dtype=float)\n",
    "\n",
    "check(\"13.1 moving average (toy)\", _ma_toy, required=True)\n",
    "check(\"13.1 MA(1) == naive\", _ma_equals_naive_at_1, required=True)\n",
    "ma_mae = mae(moving_average_forecast(ridership, 3), ridership[3:])\n",
    "print(f\"moving-average (window 3) MAE = {ma_mae:5.1f}  (vs naive {naive_mae:.1f}, seasonal-naive {snaive_mae:.1f})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecd26ae8",
   "metadata": {},
   "source": [
    "> **Interpretation.** A short moving average smooths noise but lags the trend and completely flattens the season, so it lands between naive and seasonal-naive: better than chasing last month, far worse than copying last year. On this series, periodicity beats smoothing.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "104b36b0",
   "metadata": {},
   "source": [
    "### Reading the period off the data: the autocorrelation function\n",
    "\n",
    "We *built in* a period of 12, but real data does not come with a label. The autocorrelation function (ACF) at lag $k$ is the correlation of the series with a copy of itself shifted by $k$ steps:\n",
    "\n",
    "$$\\rho(k) = \\frac{\\sum_{t} (x_t - \\bar{x})(x_{t+k} - \\bar{x})}{\\sum_{t} (x_t - \\bar{x})^2}$$\n",
    "\n",
    "A seasonal series spikes in the ACF at multiples of its period. If our code is right, $\\rho$ peaks at lag 12.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34ac924f",
   "metadata": {},
   "source": [
    "> **Predict:** at which lags will the ACF of this series be largest? <details><summary>Answer</summary>At 0 (a series is perfectly correlated with itself), then secondary peaks at 12, 24, 36 (the seasonal period and its multiples). The trend also adds slowly-decaying positive correlation at small lags. We will see exactly this.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "348a44e9",
   "metadata": {},
   "source": [
    "### Exercise 13.2 — Autocorrelation from its definition\n",
    "`Difficulty 3/5 · ~15 min`\n",
    "\n",
    "Implement `acf(series, max_lag)` returning an array of length `max_lag + 1` where entry `k` is $\\rho(k)$ as defined above (with `acf[0] == 1.0` by construction). Use a *single* mean $\\bar{x}$ and a *single* denominator $\\sum_t (x_t-\\bar{x})^2$ for all lags (this is the standard biased estimator that `statsmodels` uses). The check compares against `statsmodels.acf` when available, and always asserts the lag-12 peak.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "2d3138a9",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.604561Z",
     "iopub.status.busy": "2026-06-10T19:46:41.604469Z",
     "iopub.status.idle": "2026-06-10T19:46:41.616313Z",
     "shell.execute_reply": "2026-06-10T19:46:41.616046Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.2 acf(0) == 1: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 13.2 acf finds period 12: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] statsmodels absent; skipping the library cross-check (the period test still ran)\n",
      "[ ok ] 13.2 acf vs statsmodels\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "True"
      ]
     },
     "execution_count": 8,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def acf(series, max_lag):\n",
    "    \"\"\"Autocorrelation at lags 0..max_lag (biased estimator, single mean & denom).\"\"\"\n",
    "    x = np.asarray(series, dtype=float)\n",
    "    n = len(x)\n",
    "    xbar = x.mean()\n",
    "    centered = x - xbar\n",
    "    denom = np.sum(centered ** 2)              # one denominator for every lag\n",
    "    # TODO 1: for lag k, numerator = sum_t centered[t] * centered[t + k], for t in 0..n-1-k\n",
    "    # TODO 2: rho(k) = numerator / denom; rho(0) must come out as 1.0\n",
    "    out = None\n",
    "    attempted(out)\n",
    "    return np.asarray(out, dtype=float)\n",
    "\n",
    "def _acf_lag0():\n",
    "    a = acf(ridership, 24)\n",
    "    check_close(a[0], 1.0, msg=\"acf(0) is a series correlated with itself: must be 1.0\")\n",
    "\n",
    "def _acf_finds_period():\n",
    "    a = acf(ridership, 30)\n",
    "    # lag 12 must be a clear local peak vs its neighbours and vs lag 6 (anti-phase)\n",
    "    assert a[12] > a[11] and a[12] > a[13], \\\n",
    "        f\"acf should peak at the seasonal lag 12; got a[11..13] = {a[11]:.2f},{a[12]:.2f},{a[13]:.2f}\"\n",
    "    assert a[12] > a[6] + 0.3, \\\n",
    "        f\"lag 12 (in phase) must beat lag 6 (anti-phase) clearly; got {a[12]:.2f} vs {a[6]:.2f}\"\n",
    "\n",
    "def _acf_vs_statsmodels():\n",
    "    try:\n",
    "        from statsmodels.tsa.stattools import acf as sm_acf\n",
    "    except Exception:\n",
    "        print(\"[ -- ] statsmodels absent; skipping the library cross-check (the period test still ran)\")\n",
    "        return\n",
    "    want = sm_acf(ridership, nlags=24, fft=False)\n",
    "    check_close(acf(ridership, 24), want, atol=1e-6, msg=\"must match statsmodels' biased ACF\")\n",
    "\n",
    "check(\"13.2 acf(0) == 1\", _acf_lag0)\n",
    "check(\"13.2 acf finds period 12\", _acf_finds_period)\n",
    "check(\"13.2 acf vs statsmodels\", _acf_vs_statsmodels)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ae1575b9",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>For each lag `k`, you multiply the series by a copy of itself shifted by `k`, but both already mean-centered. The overlap shrinks as `k` grows: at lag `k` only `n-k` products exist.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "out = []\n",
    "for k in range(max_lag + 1):\n",
    "    num = np.sum(centered[:n-k] * centered[k:])   # element-wise, then sum\n",
    "    out.append(num / denom)\n",
    "```\n",
    "`centered[:n-k]` lines up with `centered[k:]`: position `t` against position `t+k`.</details>\n",
    "\n",
    "<details><summary>Help — \"acf[0] is not exactly 1.0\"</summary>At lag 0, `centered[:n] * centered[:n]` summed is exactly `denom`, so the ratio is 1.0. If yours is off, you probably sliced `centered[:n-0]` as `centered[:n]` but used a different denominator (e.g. recomputed per lag). Use the *one* `denom` defined above for every lag.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "05bb22bb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.617437Z",
     "iopub.status.busy": "2026-06-10T19:46:41.617340Z",
     "iopub.status.idle": "2026-06-10T19:46:41.624753Z",
     "shell.execute_reply": "2026-06-10T19:46:41.624176Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.2 acf(0) == 1\n",
      "[ ok ] 13.2 acf finds period 12\n",
      "[ -- ] statsmodels absent; skipping the library cross-check (the period test still ran)\n",
      "[ ok ] 13.2 acf vs statsmodels\n",
      "recovered seasonal period: lag of the first peak after 0 is 1\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines acf; the checks below re-verify it and find the period.\n",
    "def acf(series, max_lag):\n",
    "    x = np.asarray(series, dtype=float)\n",
    "    n = len(x)\n",
    "    centered = x - x.mean()\n",
    "    denom = np.sum(centered ** 2)\n",
    "    out = [np.sum(centered[:n-k] * centered[k:]) / denom for k in range(max_lag + 1)]\n",
    "    return np.asarray(out, dtype=float)\n",
    "\n",
    "check(\"13.2 acf(0) == 1\", _acf_lag0, required=True)\n",
    "check(\"13.2 acf finds period 12\", _acf_finds_period, required=True)\n",
    "check(\"13.2 acf vs statsmodels\", _acf_vs_statsmodels)\n",
    "a = acf(ridership, 36)\n",
    "print(f\"recovered seasonal period: lag of the first peak after 0 is {1 + int(np.argmax(a[1:]))}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "be459546",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.627583Z",
     "iopub.status.busy": "2026-06-10T19:46:41.627489Z",
     "iopub.status.idle": "2026-06-10T19:46:41.725980Z",
     "shell.execute_reply": "2026-06-10T19:46:41.725189Z"
    }
   },
   "outputs": [
    {
     "data": {
      "image/png": "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nTpVaZxyRXFbl4tUVeUKrVaL4OBgLF26tMrneHt76/5/+PDhmD17NjZt2oSpU6di48aNsLe3x4ABA/SO5+rqWuXElcB/4/uzs7PRt29f2NnZ4e2334a/vz/UajXi4uIwa9asRvlN1ZZr6pv7qjJmzBhs2rQJBw8eRHBwMH755Re8/PLLuhxZ8RqjRo2qtnDs3LkzAOD777/HuHHjMGTIELz++utwdXWFUqnEwoULcenSpUrPq+u/BUqlEs888wxWrlyJzz//HH///Tdu3Lgh6qoctX0OhqpP7iSihmPRTUTVWrt2LVxdXfHZZ59Vemzz5s34+eefsWLFClhZWSEiIgKxsbH46aefKrUgicnHxwe7du1CXl6eXktCRRfKu2dlbqwYquqCWjGrdYWKrt6urq71btG/W3BwMIKDgzFnzhwcPHgQvXv3xooVK/Duu+826LgVLl68CEEQ9Fq7z58/DwA1FsZr167F7du3DXrN2lqEAVT6nAVBwMWLF3V/TFd8xne2cFbnxx9/xNixY/Hhhx/qthUVFenNbH2nV155BQEBAZg3bx7s7e3xxhtvVNrH398fr732Gl577TVcuHABXbt2xYcffojvv/++ymPeunULu3fvRnR0NObNm1ft+wSq7xYqls8++wzz58/H1KlTMWvWLFGOefjwYTzxxBPo3r07Nm7cWGVxde3aNfzwww/44YcfKj3WrVs3dOnSpdrlzuqruLgYWq1Wr7U3JCQEO3fuRHJysl7vlBs3bgAwfLI6f39/nDx5Ev3796/1u/Pz80PPnj2xYcMGTJ48GZs3b8aQIUP0lif09/fHrl270Lt37xqLwH379iEzMxObN2/GAw88oNt+Z++k+mrob0+M3DdgwAC4uLhg7dq16NWrFwoLC/UuDLm4uMDW1hYajaZO536bNm2wefNmvfd299KXhhgzZgw+/PBDbNu2DX/88QdcXFxqHabk4uICOzu7OvX2qMvnUNdj3ak+uZOIGo5juomoSrdv38bmzZvx2GOPYdiwYZVukydPRl5eHn755RcAwEsvvQQPDw+89tprumLtTunp6aIUiAMHDoRGo8H//vc/ve0fffQRFAoFHn300Qa/Rl1iOHToEI4cOaLbdvPmzUotUuHh4bCzs8N7772n153xzufUJjc3F2VlZXrbgoODYWZmprcMUUPduHEDP//8s97rfvvt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d4miIrE4ODigXbt2uHjxotShUANVnI88V+WrTZs2cHZ25vnajE2ePBm//vor9u7dCy8vL912d3d3lJSUIDs7W29/np/NW3XfZ1V69eoFADw/mylLS0sEBAQgJCQECxcuRJcuXfDxxx/z3DRS1X2fVeG5WTUW3c2MpaUlQkJCsHv3bt02rVaL3bt3642dIOOUn5+PS5cuwcPDQ+pQqIH8/Pzg7u6ud67m5ubi8OHDPFdl4vr168jMzOT52gwJgoDJkyfj559/xp49e+Dn56f3eEhICCwsLPTOz3PnziEpKYnnZzNU2/dZlfj4eADg+WkktFotiouLeW7KRMX3WRWem1Vj9/JmaPr06Rg7diy6d++Onj17YtmyZSgoKEBkZKTUoVE9zZgxAxEREfDx8cGNGzcQFRUFpVKJkSNHSh0a1UF+fr7eldrExETEx8fDyckJrVu3xtSpU/Huu++ibdu28PPzw9y5c+Hp6YkhQ4ZIFzRVq6bv08nJCdHR0XjyySfh7u6OS5cuYebMmQgICEB4eLiEUVNVJk2ahB9++AFbt26Fra2tbiyovb09rKysYG9vj/Hjx2P69OlwcnKCnZ0dXnnlFYSGhuLee++VOHq6W23f56VLl/DDDz9g4MCBaNmyJf755x9MmzYNDzzwADp37ixx9HS32bNn49FHH0Xr1q2Rl5eHH374Afv27cP27dt5bhqhmr5Pnpv1IPX06VS1Tz/9VGjdurVgaWkp9OzZUzh06JDUIZEBhg8fLnh4eAiWlpZCq1athOHDhwsXL16UOiyqo7179woAKt3Gjh0rCEL5smFz584V3NzcBJVKJfTv3184d+6ctEFTtWr6PgsLC4VHHnlEcHFxESwsLAQfHx9hwoQJQmpqqtRhUxWq+h4BCKtXr9btc/v2beHll18WHB0dBWtra+GJJ54QUlJSpAuaqlXb95mUlCQ88MADgpOTk6BSqYSAgADh9ddfF3JycqQNnKr03HPPCT4+PoKlpaXg4uIi9O/fX9ixY4fucZ6bxqWm75PnZt0pBEEQmrLIJyIiIiIiIjIVHNNNRERERERE1EhYdBMRERERERE1EhbdRERERERERI2ERTcRERERERFRI2HRTURERERERNRIWHQTERERERERNRIW3URERERERESNhEU3ERERERERUSNh0U1ERNREHnzwQUydOrVJXmv37t3o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",
      "text/plain": [
       "<Figure size 1000x350 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# viz: the ACF stem plot with the seasonal peaks marked\n",
    "lags = np.arange(len(a))\n",
    "fig, ax = plt.subplots(figsize=(10, 3.5))\n",
    "ax.stem(lags, a, basefmt=\" \")\n",
    "for k in (12, 24, 36):\n",
    "    ax.axvline(k, color=\"#E0115F\", ls=\":\", lw=1)\n",
    "ax.set_xlabel(\"lag (months)\"); ax.set_ylabel(\"autocorrelation\")\n",
    "ax.set_title(\"ACF of ridership — peaks at 12, 24, 36 reveal the yearly cycle\")\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d7f15fc",
   "metadata": {},
   "source": [
    "> **Interpretation.** The pink lines sit on the seasonal peaks: the ACF found the period 12 without ever being told it. This is the classical analyst's first move on any new series. Before fitting anything, plot the ACF and read off the periodicity; it tells you what `s` to use for seasonal-naive and what context length a learned model will need.\n",
    "\n",
    "> **Key takeaways.**\n",
    "> - Always score against the naive and seasonal-naive baselines first; on periodic data, seasonal-naive is a brutal bar.\n",
    "> - MAE in the same units as the series makes the noise floor (here ~8) legible, so you know when a model has extracted everything learnable.\n",
    "> - The ACF reads periodicity straight off the data; peaks at multiples of the period are the seasonal fingerprint.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ec1ddec1",
   "metadata": {},
   "source": [
    "## Part 2 — Windowing without leaking the future\n",
    "\n",
    "> **Objectives.** Build a time-respecting train/val/test split and a sliding-window tensor builder, then property-test the single most common forecasting bug: letting information from the test window leak into training.\n",
    "\n",
    "A learned sequence model eats fixed-length windows: from the last `L` values, predict the next one. Two things must be true or every metric you report is a fantasy. First, the split must respect time: train on the early months, validate on the middle, test on the latest, never shuffled. Second, the windows must be *causal*: the input window ends strictly before the target. Get either wrong and the model \"predicts\" values it has effectively already seen. The draft's caveat (`§11`) names this the most common forecasting failure mode, and it is invisible without a test.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "b3427255",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.729476Z",
     "iopub.status.busy": "2026-06-10T19:46:41.729343Z",
     "iopub.status.idle": "2026-06-10T19:46:41.735448Z",
     "shell.execute_reply": "2026-06-10T19:46:41.733411Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "split sizes: train 84, val 18, test 18 (contiguous, ordered) · train mean 251 sd 39\n"
     ]
    }
   ],
   "source": [
    "# A time-respecting split: contiguous, ordered, no shuffling. Standardize using\n",
    "# ONLY the training statistics, so the val/test scaler never peeks at the future.\n",
    "def time_split(series, frac_train=0.7, frac_val=0.15):\n",
    "    n = len(series)\n",
    "    i_tr = int(n * frac_train)\n",
    "    i_va = int(n * (frac_train + frac_val))\n",
    "    return series[:i_tr], series[i_tr:i_va], series[i_va:]\n",
    "\n",
    "tr_raw, va_raw, te_raw = time_split(ridership)\n",
    "mu, sd = tr_raw.mean(), tr_raw.std()              # train-only statistics\n",
    "print(f\"split sizes: train {len(tr_raw)}, val {len(va_raw)}, test {len(te_raw)} \"\n",
    "      f\"(contiguous, ordered) · train mean {mu:.0f} sd {sd:.0f}\")\n",
    "assert len(tr_raw) + len(va_raw) + len(te_raw) == len(ridership), \"split must partition the series\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c7cd5c73",
   "metadata": {},
   "source": [
    "> **Common confusion:** \"standardize the whole series, then split.\" That leaks the test mean and sd into the scaler the model trains under. The fix above computes `mu, sd` from the training slice only, then applies them to all three. The val/test scaler is allowed to know nothing the future taught it.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a6d3db94",
   "metadata": {},
   "source": [
    "### Exercise 13.3 — Causal sliding windows\n",
    "`Difficulty 3/5 · ~15 min`\n",
    "\n",
    "Implement `make_windows(series, L)` returning `(X, y)` where `X[i]` is `L` consecutive values and `y[i]` is the single value immediately after that window. With a series of length `n` you get `n - L` windows. The check asserts shapes, asserts strict causality (every target sits exactly one step past its window), and asserts a hand-traced toy.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "98785c39",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.736573Z",
     "iopub.status.busy": "2026-06-10T19:46:41.736460Z",
     "iopub.status.idle": "2026-06-10T19:46:41.745693Z",
     "shell.execute_reply": "2026-06-10T19:46:41.745048Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.3 windows (toy): not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 13.3 windows shape + causal: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def make_windows(series, L):\n",
    "    \"\"\"Sliding windows for next-step forecasting.\n",
    "\n",
    "    Returns X of shape (n-L, L) and y of shape (n-L,), where\n",
    "    X[i] = series[i : i+L] and y[i] = series[i+L].\n",
    "    \"\"\"\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    n = len(series)\n",
    "    # TODO 1: build X by stacking series[i:i+L] for i in range(n-L)\n",
    "    X = None\n",
    "    # TODO 2: build y as series[L:]  (the value right after each window)\n",
    "    y = None\n",
    "    attempted(X, y)\n",
    "    return np.asarray(X, dtype=float), np.asarray(y, dtype=float)\n",
    "\n",
    "def _windows_toy():\n",
    "    toy = np.arange(6, dtype=float)   # 0,1,2,3,4,5\n",
    "    X, y = make_windows(toy, 2)\n",
    "    check_close(X, [[0,1],[1,2],[2,3],[3,4]], msg=\"windows of length 2 over 0..5\")\n",
    "    check_close(y, [2, 3, 4, 5], msg=\"each target is the value one step past its window\")\n",
    "\n",
    "def _windows_shape_and_causal():\n",
    "    X, y = make_windows(ridership, 12)\n",
    "    check_shape(X, (len(ridership) - 12, 12))\n",
    "    check_shape(y, (len(ridership) - 12,))\n",
    "    # strict causality: the last element of window i must be the value BEFORE target i,\n",
    "    # i.e. they cannot be equal unless the series happens to repeat (it does not here).\n",
    "    assert np.all(X[:, -1] == ridership[11:-1]), \\\n",
    "        \"window i must END at series[i+L-1], one step before its target series[i+L]\"\n",
    "\n",
    "check(\"13.3 windows (toy)\", _windows_toy)\n",
    "check(\"13.3 windows shape + causal\", _windows_shape_and_causal)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "10f746aa",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>`X` is a stack of slices; `y` is just the series shifted left by `L`. The number of rows is `len(series) - L`, the same count you used in the moving-average exercise.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "X = np.stack([series[i:i+L] for i in range(n - L)])\n",
    "y = series[L:]\n",
    "```\n",
    "`series[L:]` already has exactly `n - L` elements, one per window.</details>\n",
    "\n",
    "<details><summary>Help — \"shapes mismatch: X has n-L rows but y has n-L+? \"</summary>`y = series[L:]` gives `n - L` values. If you wrote `series[L-1:]` or `series[L+1:]` you are off by one, which is exactly the leak/lag bug this exercise guards. The target for window `series[i:i+L]` is `series[i+L]`, so `y = series[L:]`.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "1ac2f71d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.749271Z",
     "iopub.status.busy": "2026-06-10T19:46:41.746632Z",
     "iopub.status.idle": "2026-06-10T19:46:41.753524Z",
     "shell.execute_reply": "2026-06-10T19:46:41.752893Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.3 windows (toy)\n",
      "[ ok ] 13.3 windows shape + causal\n",
      "make_windows is causal: every target sits exactly one step past its window.\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines make_windows; the checks below re-verify shapes and causality.\n",
    "def make_windows(series, L):\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    n = len(series)\n",
    "    X = np.stack([series[i:i+L] for i in range(n - L)])\n",
    "    y = series[L:]\n",
    "    return X.astype(float), y.astype(float)\n",
    "\n",
    "check(\"13.3 windows (toy)\", _windows_toy, required=True)\n",
    "check(\"13.3 windows shape + causal\", _windows_shape_and_causal, required=True)\n",
    "print(\"make_windows is causal: every target sits exactly one step past its window.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "294519d1",
   "metadata": {},
   "source": [
    "> **Interpretation.** That `X[:, -1] == ridership[11:-1]` assert is the whole lesson in one line: the last value the model sees is strictly before the value it must predict. If you ever build windows where the target appears inside the input, your model will look brilliant in validation and fail in production. The assert would have caught it.\n",
    "\n",
    "> **Key takeaways.**\n",
    "> - Forecasting splits are contiguous and ordered; shuffling leaks the future.\n",
    "> - Standardize with training-only statistics; the scaler is part of the model.\n",
    "> - A causal window ends one step before its target; assert it, because the bug is silent.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8ab0ee3",
   "metadata": {},
   "source": [
    "## Part 3 — The context-window forecaster\n",
    "\n",
    "> **Objectives.** Build the simplest *learned* sequence model, an MLP over the last `L` standardized values (the time-series analogue of Bengio's neural probabilistic language model), train it with the four-comment loop, and hold it to the standard: it must beat seasonal-naive or it was not worth the compute.\n",
    "\n",
    "Before recurrence, the cheapest learned model treats a fixed window as a flat feature vector and runs an ordinary MLP on it. This is exactly the structure of a neural probabilistic language model: a fixed context of `L` tokens, concatenated, fed through an MLP to predict the next one. Here the \"tokens\" are real-valued ridership readings. It has no memory beyond `L` and no weight sharing across positions, which is precisely the limitation that motivates the RNN in Part 4. But it is a real learned baseline, and it had better beat seasonal-naive.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "f39bea43",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.754525Z",
     "iopub.status.busy": "2026-06-10T19:46:41.754434Z",
     "iopub.status.idle": "2026-06-10T19:46:41.765320Z",
     "shell.execute_reply": "2026-06-10T19:46:41.762588Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "windows: train torch.Size([72, 12]), val torch.Size([18, 12]), test torch.Size([18, 12])  (context L=12)\n"
     ]
    }
   ],
   "source": [
    "# Build standardized windows for the learned model. Standardize with TRAIN stats only.\n",
    "L = 12   # context length: one full season, the period the ACF found\n",
    "def standardize(a): return (a - mu) / sd\n",
    "def destandardize(a): return a * sd + mu\n",
    "\n",
    "# Windows are built on the standardized FULL series, then split by time so the\n",
    "# val/test windows are strictly later than every training window.\n",
    "z = standardize(ridership)\n",
    "Xz, yz = make_windows(z, L)\n",
    "# window i ends at month i+L-1; assign each window to a split by its TARGET month i+L.\n",
    "target_month = np.arange(L, len(ridership))\n",
    "i_tr = int(len(ridership) * 0.7)\n",
    "i_va = int(len(ridership) * 0.85)\n",
    "m_tr = target_month < i_tr\n",
    "m_va = (target_month >= i_tr) & (target_month < i_va)\n",
    "m_te = target_month >= i_va\n",
    "Xtr, ytr = torch.tensor(Xz[m_tr], dtype=torch.float32), torch.tensor(yz[m_tr], dtype=torch.float32)\n",
    "Xva, yva = torch.tensor(Xz[m_va], dtype=torch.float32), torch.tensor(yz[m_va], dtype=torch.float32)\n",
    "Xte, yte = torch.tensor(Xz[m_te], dtype=torch.float32), torch.tensor(yz[m_te], dtype=torch.float32)\n",
    "print(f\"windows: train {Xtr.shape}, val {Xva.shape}, test {Xte.shape}  (context L={L})\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "760b9914",
   "metadata": {},
   "source": [
    "> **Note:** assigning each window to a split *by its target month* guarantees no training window can contain a validation or test month. That is the windowed version of the time-respecting split, and it is the kind of plumbing that decides whether a forecasting result is real.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "261cd900",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.766429Z",
     "iopub.status.busy": "2026-06-10T19:46:41.766323Z",
     "iopub.status.idle": "2026-06-10T19:46:41.777474Z",
     "shell.execute_reply": "2026-06-10T19:46:41.776849Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "WindowMLP: 449 params · smoke-test output shape (5,) (expected (5,))\n"
     ]
    }
   ],
   "source": [
    "# scale-up off by default: the model. A two-layer MLP over the flat context window.\n",
    "class WindowMLP(torch.nn.Module):\n",
    "    def __init__(self, L, hidden=32):\n",
    "        super().__init__()\n",
    "        self.net = torch.nn.Sequential(\n",
    "            torch.nn.Linear(L, hidden), torch.nn.Tanh(),\n",
    "            torch.nn.Linear(hidden, 1))\n",
    "    def forward(self, x):              # x: (B, L) -> (B,)\n",
    "        return self.net(x).squeeze(-1)\n",
    "\n",
    "torch.manual_seed(SEED)               # re-seed so this cell reproduces if re-run alone\n",
    "nplm = WindowMLP(L)\n",
    "n_params = sum(p.numel() for p in nplm.parameters())\n",
    "# randn shape smoke test BEFORE any training (the spec's mandatory module check)\n",
    "_smoke = nplm(torch.randn(5, L))\n",
    "print(f\"WindowMLP: {n_params} params · smoke-test output shape {tuple(_smoke.shape)} (expected (5,))\")\n",
    "assert _smoke.shape == (5,), \"forward must map (B, L) -> (B,)\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14612a45",
   "metadata": {},
   "source": [
    "The four-comment training loop is the same skeleton you will see in every torch chapter: `# forward / # backward / # update / # track stats`. Memorize the shape of it once.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 16,
   "id": "5c2f5e0d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:41.778526Z",
     "iopub.status.busy": "2026-06-10T19:46:41.778381Z",
     "iopub.status.idle": "2026-06-10T19:46:48.943723Z",
     "shell.execute_reply": "2026-06-10T19:46:48.943459Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step    0 · train MSE 0.9843 · val MAE  41.6 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  100 · train MSE 0.0361 · val MAE  11.8 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  200 · train MSE 0.0159 · val MAE   8.8 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  300 · train MSE 0.0022 · val MAE   9.6 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  400 · train MSE 0.0001 · val MAE   9.6 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  500 · train MSE 0.0000 · val MAE   9.8 riders\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  599 · train MSE 0.0000 · val MAE   9.9 riders\n"
     ]
    }
   ],
   "source": [
    "# Train the context-window forecaster. MSE on standardized targets.\n",
    "torch.manual_seed(SEED)\n",
    "nplm = WindowMLP(L)\n",
    "opt = torch.optim.Adam(nplm.parameters(), lr=0.02)\n",
    "loss_fn = torch.nn.MSELoss()\n",
    "log = []\n",
    "for step in range(NPLM_STEPS):\n",
    "    # forward\n",
    "    pred = nplm(Xtr)\n",
    "    loss = loss_fn(pred, ytr)\n",
    "    # backward\n",
    "    opt.zero_grad()\n",
    "    loss.backward()\n",
    "    # update\n",
    "    opt.step()\n",
    "    # track stats\n",
    "    if step % max(1, NPLM_STEPS // 6) == 0 or step == NPLM_STEPS - 1:\n",
    "        with torch.no_grad():\n",
    "            vmae = (nplm(Xva) - yva).abs().mean().item() * sd   # back to rider units\n",
    "        log.append((step, loss.item(), vmae))\n",
    "        print(f\"step {step:4d} · train MSE {loss.item():.4f} · val MAE {vmae:5.1f} riders\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f14f904c",
   "metadata": {},
   "source": [
    "> **Interpretation.** The validation MAE is reported in rider units (we undo the standardization by multiplying by `sd`), so it is directly comparable to the seasonal-naive bar from Part 1. Watch it drop across steps. The question that matters is the next cell: did it actually beat the baseline?\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 17,
   "id": "3af08d9b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.948038Z",
     "iopub.status.busy": "2026-06-10T19:46:48.947878Z",
     "iopub.status.idle": "2026-06-10T19:46:48.964834Z",
     "shell.execute_reply": "2026-06-10T19:46:48.964278Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "context-window MLP  test MAE =  22.9 riders\n",
      "seasonal-naive      test MAE =  16.7 riders\n",
      "verdict: MLP does NOT beat seasonal-naive (the baseline wins)\n"
     ]
    }
   ],
   "source": [
    "# Did the learned model beat seasonal-naive on the held-out TEST set?\n",
    "with torch.no_grad():\n",
    "    nplm_test_mae = (nplm(Xte) - yte).abs().mean().item() * sd\n",
    "# seasonal-naive on the same test target months, in raw units\n",
    "te_targets = target_month[m_te]\n",
    "snaive_test = mae(ridership[te_targets - PERIOD], ridership[te_targets])\n",
    "print(f\"context-window MLP  test MAE = {nplm_test_mae:5.1f} riders\")\n",
    "print(f\"seasonal-naive      test MAE = {snaive_test:5.1f} riders\")\n",
    "print(\"verdict:\", \"MLP beats seasonal-naive\" if nplm_test_mae < snaive_test\n",
    "      else \"MLP does NOT beat seasonal-naive (the baseline wins)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8164e262",
   "metadata": {},
   "source": [
    "> **Interpretation.** On this short, strongly-seasonal series the learned model and the baseline are close, and which one wins can flip with the seed and the FAST setting. That is the honest result: a 32-unit MLP on 80-odd training windows is not guaranteed to beat a forecast that already encodes the dominant structure. The lesson is the discipline, not a guaranteed win. Report the comparison, and if the baseline wins, ship the baseline.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f9123928",
   "metadata": {},
   "source": [
    "### Exercise 13.4 — A multi-horizon forecast head\n",
    "`Difficulty 2/5 · ~12 min`\n",
    "\n",
    "Single-step models forecast one point. For a horizon of `H` steps you can predict them all at once with a head of width `H` (direct multi-horizon, the draft's `Forecaster` pattern, no teacher forcing needed). Implement `make_multi_targets(series, L, H)` returning `(X, Y)` where `X[i]` is the length-`L` window and `Y[i]` is the next `H` values as a vector. The check asserts shapes and that horizon 0 of `Y` equals the single-step target.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 18,
   "id": "f4f92d39",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.965783Z",
     "iopub.status.busy": "2026-06-10T19:46:48.965704Z",
     "iopub.status.idle": "2026-06-10T19:46:48.972379Z",
     "shell.execute_reply": "2026-06-10T19:46:48.972170Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.4 multi-target (toy): not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 13.4 horizon-0 == single step: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 18,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def make_multi_targets(series, L, H):\n",
    "    \"\"\"Windows with H-step-ahead targets.\n",
    "\n",
    "    X[i] = series[i:i+L]  (length L)\n",
    "    Y[i] = series[i+L : i+L+H]  (length H)\n",
    "    Only windows with a full H-step future are kept: count = len(series) - L - H + 1.\n",
    "    \"\"\"\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    n = len(series)\n",
    "    count = n - L - H + 1\n",
    "    # TODO 1: stack the L-length input windows for i in range(count)\n",
    "    X = None\n",
    "    # TODO 2: stack the H-length target windows series[i+L : i+L+H] for i in range(count)\n",
    "    Y = None\n",
    "    attempted(X, Y)\n",
    "    return np.asarray(X, dtype=float), np.asarray(Y, dtype=float)\n",
    "\n",
    "def _multi_toy():\n",
    "    toy = np.arange(8, dtype=float)\n",
    "    X, Y = make_multi_targets(toy, 3, 2)   # count = 8-3-2+1 = 4\n",
    "    check_shape(X, (4, 3)); check_shape(Y, (4, 2))\n",
    "    check_close(X[0], [0, 1, 2], msg=\"first input window\")\n",
    "    check_close(Y[0], [3, 4], msg=\"first 2-step target\")\n",
    "\n",
    "def _multi_h0_equals_single():\n",
    "    X, Y = make_multi_targets(ridership, 12, 4)\n",
    "    Xs, ys = make_windows(ridership, 12)\n",
    "    # horizon 0 of the multi-target is exactly the single-step target (for the shared rows)\n",
    "    check_close(Y[:, 0], ys[:len(Y)], msg=\"Y[:,0] is the 1-step-ahead value == make_windows target\")\n",
    "\n",
    "check(\"13.4 multi-target (toy)\", _multi_toy)\n",
    "check(\"13.4 horizon-0 == single step\", _multi_h0_equals_single)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c256916a",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>It is the windowing you already wrote, but the target is a *slice* of length `H` instead of a single value. The number of rows shrinks by `H-1` because the last few windows do not have a full future.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "X = np.stack([series[i:i+L]       for i in range(count)])\n",
    "Y = np.stack([series[i+L:i+L+H]   for i in range(count)])\n",
    "```\n",
    "with `count = len(series) - L - H + 1`.</details>\n",
    "\n",
    "<details><summary>Help — \"off by one in count\"</summary>The last valid `i` needs `i+L+H <= n`, so `i` runs `0 .. n-L-H`, giving `n-L-H+1` rows. At `H=1` this reduces to `n-L`, matching `make_windows`.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 19,
   "id": "ae1d1c52",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.973417Z",
     "iopub.status.busy": "2026-06-10T19:46:48.973284Z",
     "iopub.status.idle": "2026-06-10T19:46:48.978817Z",
     "shell.execute_reply": "2026-06-10T19:46:48.978469Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.4 multi-target (toy)\n",
      "[ ok ] 13.4 horizon-0 == single step\n",
      "multi-horizon dataset: X (105, 12), Y (105, 4) (predict 4 months at once)\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines make_multi_targets; checks re-verify shapes and the H=1 reduction.\n",
    "def make_multi_targets(series, L, H):\n",
    "    series = np.asarray(series, dtype=float)\n",
    "    n = len(series)\n",
    "    count = n - L - H + 1\n",
    "    X = np.stack([series[i:i+L] for i in range(count)])\n",
    "    Y = np.stack([series[i+L:i+L+H] for i in range(count)])\n",
    "    return X.astype(float), Y.astype(float)\n",
    "\n",
    "check(\"13.4 multi-target (toy)\", _multi_toy, required=True)\n",
    "check(\"13.4 horizon-0 == single step\", _multi_h0_equals_single, required=True)\n",
    "Xm, Ym = make_multi_targets(ridership, 12, 4)\n",
    "print(f\"multi-horizon dataset: X {Xm.shape}, Y {Ym.shape} (predict 4 months at once)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a08d9892",
   "metadata": {},
   "source": [
    "> **Key takeaways.**\n",
    "> - The context-window MLP is the simplest learned sequence model: fixed context in, prediction out, no memory and no weight sharing across positions.\n",
    "> - Hold every learned model to the baseline on the *test* set, in interpretable units. If it does not beat seasonal-naive, the baseline wins.\n",
    "> - Direct multi-horizon output (a width-`H` head) sidesteps the error compounding of feeding predictions back in; it is the right default for a fixed horizon.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e1f4193f",
   "metadata": {},
   "source": [
    "## Part 4 — A vanilla RNN from scratch\n",
    "\n",
    "> **Objectives.** Implement the recurrence $h_t = \\tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h)$ in NumPy, verify one step of backprop-through-time against `torch.autograd`, then *stage* the exploding-gradient failure and fix it with norm clipping.\n",
    "\n",
    "The MLP in Part 3 has no memory: every window is processed from scratch. A recurrent network maintains a hidden state updated at every position, with the *same* weights at every step (the draft's `§2`). That weight sharing is the recurrent analogue of convolution's: the architecture bakes in \"the same operation applies at every step\". The cell is three matrices and a tanh:\n",
    "\n",
    "$$h_t = \\tanh(W_{hh} h_{t-1} + W_{xh} x_t + b_h), \\qquad y_t = W_{hy} h_t + b_y$$\n",
    "\n",
    "Karpathy's framing is worth keeping: training an MLP is optimization over functions; training an RNN is optimization over programs. The hidden state is a tiny mutable register the network learns to update.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16ffe5b1",
   "metadata": {},
   "source": [
    "### Exercise 13.5 — The vanilla RNN cell (forward)\n",
    "`Difficulty 3/5 · ~15 min`\n",
    "\n",
    "Implement `rnn_forward(params, x_seq, h0)`: roll the recurrence over a sequence and return the list of hidden states and the list of outputs. `params` is a dict with `W_hh, W_xh, W_hy, b_h, b_y`; `x_seq` is a list of `(input_size, 1)` column vectors; `h0` is the initial `(hidden_size, 1)` state. The check asserts shapes and reproduces a hand-computed two-step toy where all weights are identity-ish so you can verify the arithmetic on paper.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 20,
   "id": "ee21bf19",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.979774Z",
     "iopub.status.busy": "2026-06-10T19:46:48.979622Z",
     "iopub.status.idle": "2026-06-10T19:46:48.991562Z",
     "shell.execute_reply": "2026-06-10T19:46:48.991343Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.5 rnn forward shapes: not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] 13.5 rnn forward toy value: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 20,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def rnn_forward(params, x_seq, h0):\n",
    "    \"\"\"Vanilla RNN forward. Returns (hs, ys): lists of hidden states and outputs.\n",
    "\n",
    "    h_t = tanh(W_hh @ h_{t-1} + W_xh @ x_t + b_h)\n",
    "    y_t = W_hy @ h_t + b_y\n",
    "    \"\"\"\n",
    "    Whh, Wxh, Why = params[\"W_hh\"], params[\"W_xh\"], params[\"W_hy\"]\n",
    "    bh, by = params[\"b_h\"], params[\"b_y\"]\n",
    "    h = h0\n",
    "    hs, ys = [], []\n",
    "    for x in x_seq:\n",
    "        # TODO 1: h = tanh(W_hh @ h + W_xh @ x + b_h)   (use np.tanh)\n",
    "        h = None\n",
    "        # TODO 2: y = W_hy @ h + b_y\n",
    "        y = None\n",
    "        attempted(h, y)\n",
    "        hs.append(h); ys.append(y)\n",
    "    return hs, ys\n",
    "\n",
    "def _rnn_shapes():\n",
    "    p = {\"W_hh\": rng.standard_normal((4, 4)) * 0.1, \"W_xh\": rng.standard_normal((4, 3)) * 0.1,\n",
    "         \"W_hy\": rng.standard_normal((2, 4)) * 0.1, \"b_h\": np.zeros((4, 1)), \"b_y\": np.zeros((2, 1))}\n",
    "    seq = [rng.standard_normal((3, 1)) for _ in range(5)]\n",
    "    hs, ys = rnn_forward(p, seq, np.zeros((4, 1)))\n",
    "    assert len(hs) == 5 and len(ys) == 5, \"one hidden state and one output per timestep\"\n",
    "    check_shape(hs[0], (4, 1)); check_shape(ys[0], (2, 1))\n",
    "\n",
    "def _rnn_toy_value():\n",
    "    # 1-d hidden, 1-d input, identity-ish weights, zero bias, one input of 0.5.\n",
    "    p = {\"W_hh\": np.array([[0.0]]), \"W_xh\": np.array([[1.0]]), \"W_hy\": np.array([[2.0]]),\n",
    "         \"b_h\": np.array([[0.0]]), \"b_y\": np.array([[0.0]])}\n",
    "    hs, ys = rnn_forward(p, [np.array([[0.5]])], np.array([[0.0]]))\n",
    "    # h1 = tanh(0*0 + 1*0.5 + 0) = tanh(0.5); y1 = 2*h1\n",
    "    check_close(hs[0], [[np.tanh(0.5)]], msg=\"h1 = tanh(W_xh*x) = tanh(0.5)\")\n",
    "    check_close(ys[0], [[2 * np.tanh(0.5)]], msg=\"y1 = W_hy * h1 = 2*tanh(0.5)\")\n",
    "\n",
    "check(\"13.5 rnn forward shapes\", _rnn_shapes)\n",
    "check(\"13.5 rnn forward toy value\", _rnn_toy_value)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "cfc5c211",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Translate the two equations directly. `@` is matrix multiply; the biases are column vectors that broadcast. `np.tanh` is elementwise.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "h = np.tanh(Whh @ h + Wxh @ x + bh)\n",
    "y = Why @ h + by\n",
    "```\n",
    "That is the entire cell. The loop and the appends are already written for you.</details>\n",
    "\n",
    "<details><summary>Help — \"shape (4,4) not aligned with (4,1)\"</summary>`W_hh @ h` needs `h` to be `(hidden_size, 1)`, a column vector, not `(hidden_size,)`. The toy passes `np.array([[0.0]])` (shape `(1,1)`), not `np.array([0.0])`. Keep every state and input as a 2-D column.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 21,
   "id": "02f4c84c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.992390Z",
     "iopub.status.busy": "2026-06-10T19:46:48.992317Z",
     "iopub.status.idle": "2026-06-10T19:46:48.998884Z",
     "shell.execute_reply": "2026-06-10T19:46:48.998374Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.5 rnn forward shapes\n",
      "[ ok ] 13.5 rnn forward toy value\n",
      "rnn_forward: recurrence rolls h_t = tanh(W_hh h_{t-1} + W_xh x_t + b_h), y_t = W_hy h_t + b_y\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines rnn_forward; the checks below re-verify shapes and the toy value.\n",
    "def rnn_forward(params, x_seq, h0):\n",
    "    Whh, Wxh, Why = params[\"W_hh\"], params[\"W_xh\"], params[\"W_hy\"]\n",
    "    bh, by = params[\"b_h\"], params[\"b_y\"]\n",
    "    h = h0\n",
    "    hs, ys = [], []\n",
    "    for x in x_seq:\n",
    "        h = np.tanh(Whh @ h + Wxh @ x + bh)\n",
    "        y = Why @ h + by\n",
    "        hs.append(h); ys.append(y)\n",
    "    return hs, ys\n",
    "\n",
    "check(\"13.5 rnn forward shapes\", _rnn_shapes, required=True)\n",
    "check(\"13.5 rnn forward toy value\", _rnn_toy_value, required=True)\n",
    "print(\"rnn_forward: recurrence rolls h_t = tanh(W_hh h_{t-1} + W_xh x_t + b_h), y_t = W_hy h_t + b_y\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbc69f3d",
   "metadata": {},
   "source": [
    "### Backprop through time, checked against autograd\n",
    "\n",
    "To train an RNN you unroll it over the sequence, accumulate the loss, and differentiate back through *every* step. The gradient of a late loss with respect to an early hidden state is a product of per-step Jacobians:\n",
    "\n",
    "$$\\frac{\\partial L_t}{\\partial h_k} = \\frac{\\partial L_t}{\\partial h_t}\\prod_{j=k+1}^{t}\\frac{\\partial h_j}{\\partial h_{j-1}}, \\qquad \\frac{\\partial h_j}{\\partial h_{j-1}} = \\operatorname{diag}\\!\\big(1 - h_j^2\\big)\\,W_{hh}$$\n",
    "\n",
    "The factor $1 - h_j^2$ is the tanh derivative, bounded by 1. A product of `T` such factors times powers of $W_{hh}$ either shrinks toward zero (vanishing) or blows up (exploding). Rather than hand-derive the whole backward pass, we verify one step against `torch.autograd`: build the same cell in torch, backprop, and check our analytic hidden-state gradient matches. That agreement is the strongest possible self-check.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 22,
   "id": "8956461a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:48.999734Z",
     "iopub.status.busy": "2026-06-10T19:46:48.999660Z",
     "iopub.status.idle": "2026-06-10T19:46:49.006696Z",
     "shell.execute_reply": "2026-06-10T19:46:49.006174Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "autograd produced gradients for W_hh and W_xh: True True\n"
     ]
    }
   ],
   "source": [
    "# A torch replica of one RNN step, used as the gradient oracle.\n",
    "torch.manual_seed(SEED)\n",
    "H, D = 4, 3\n",
    "Whh_t = torch.randn(H, H, requires_grad=True) * 0.3\n",
    "Wxh_t = torch.randn(H, D, requires_grad=True) * 0.3\n",
    "Whh_t.retain_grad(); Wxh_t.retain_grad()\n",
    "h_prev = torch.randn(H, 1)\n",
    "x_t = torch.randn(D, 1)\n",
    "h_next = torch.tanh(Whh_t @ h_prev + Wxh_t @ x_t)   # one step\n",
    "loss = (h_next ** 2).sum()                          # a scalar to differentiate\n",
    "loss.backward()\n",
    "print(\"autograd produced gradients for W_hh and W_xh:\", Whh_t.grad is not None, Wxh_t.grad is not None)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ccd56928",
   "metadata": {},
   "source": [
    "### Exercise 13.6 — One step of BPTT, checked against autograd\n",
    "`Difficulty 4/5 · ~20 min`\n",
    "\n",
    "Implement `dh_prev(Whh, h_prev, x_t, Wxh)` returning $\\partial L / \\partial h_{\\text{prev}}$ for $L = \\lVert h_{\\text{next}} \\rVert^2$ and $h_{\\text{next}} = \\tanh(W_{hh} h_{\\text{prev}} + W_{xh} x_t)$. Work it out by the chain rule: $\\partial L/\\partial h_{\\text{next}} = 2 h_{\\text{next}}$, then through the tanh ($\\times (1 - h_{\\text{next}}^2)$), then through $W_{hh}$ ($W_{hh}^\\top \\cdot$). The check compares element-for-element against `torch.autograd` on the identical computation. This is the chapter's hardest exercise; lean on the hints.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 23,
   "id": "5fee02bb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:49.007559Z",
     "iopub.status.busy": "2026-06-10T19:46:49.007484Z",
     "iopub.status.idle": "2026-06-10T19:46:49.017486Z",
     "shell.execute_reply": "2026-06-10T19:46:49.016885Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.6 BPTT gradient vs autograd: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 23,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def dh_prev(Whh, h_prev, x_t, Wxh):\n",
    "    \"\"\"Gradient of L = ||tanh(Whh@h_prev + Wxh@x_t)||^2 w.r.t. h_prev.\n",
    "\n",
    "    All arrays are NumPy. h_prev: (H,1), x_t: (D,1), Whh: (H,H), Wxh: (H,D).\n",
    "    \"\"\"\n",
    "    h_next = np.tanh(Whh @ h_prev + Wxh @ x_t)   # (H,1)\n",
    "    # TODO 1: dL/dh_next = 2 * h_next                          (from L = sum h_next^2)\n",
    "    dL_dhnext = None\n",
    "    # TODO 2: through the tanh: dL/dpre = dL/dh_next * (1 - h_next**2)   (elementwise)\n",
    "    dL_dpre = None\n",
    "    # TODO 3: through Whh: dL/dh_prev = Whh.T @ dL_dpre\n",
    "    grad = None\n",
    "    attempted(dL_dhnext, dL_dpre, grad)\n",
    "    return grad\n",
    "\n",
    "def _bptt_vs_autograd():\n",
    "    # rebuild the SAME computation in torch and compare gradients element-for-element\n",
    "    hp = torch.randn(H, 1, requires_grad=True)\n",
    "    Whh_c = torch.tensor(Whh_t.detach().numpy(), dtype=torch.float32)\n",
    "    Wxh_c = torch.tensor(Wxh_t.detach().numpy(), dtype=torch.float32)\n",
    "    xc = torch.tensor(x_t.numpy(), dtype=torch.float32)\n",
    "    hn = torch.tanh(Whh_c @ hp + Wxh_c @ xc)\n",
    "    (hn ** 2).sum().backward()\n",
    "    want = hp.grad.numpy()\n",
    "    got = dh_prev(Whh_c.numpy(), hp.detach().numpy(), xc.numpy(), Wxh_c.numpy())\n",
    "    check_close(got, want, atol=1e-5, msg=\"analytic dL/dh_prev must match torch.autograd\")\n",
    "\n",
    "check(\"13.6 BPTT gradient vs autograd\", _bptt_vs_autograd)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c15c3c7d",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>Three links in the chain, outermost first: the squared-norm loss gives `2 * h_next`; the tanh contributes its derivative `1 - h_next**2` elementwise; the linear map `Whh @ h_prev` contributes `Whh.T` on the way back.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "dL_dhnext = 2 * h_next\n",
    "dL_dpre   = dL_dhnext * (1 - h_next**2)   # elementwise, NOT a matmul\n",
    "grad      = Whh.T @ dL_dpre\n",
    "```\n",
    "The tanh step is elementwise because tanh acts componentwise; the `Whh` step is a matmul because the linear layer mixes components.</details>\n",
    "\n",
    "<details><summary>Help — \"off by a transpose / shapes wrong\"</summary>For a linear layer `pre = Whh @ h_prev`, the backward map is `Whh.T @ d_pre`. If you used `Whh @ d_pre` you would be propagating forward, not backward. Check shapes: `Whh.T` is `(H,H)`, `d_pre` is `(H,1)`, product `(H,1)` matches `h_prev`.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 24,
   "id": "2b4153d2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:49.018447Z",
     "iopub.status.busy": "2026-06-10T19:46:49.018363Z",
     "iopub.status.idle": "2026-06-10T19:46:49.025484Z",
     "shell.execute_reply": "2026-06-10T19:46:49.025232Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.6 BPTT gradient vs autograd\n",
      "hand-derived BPTT step matches torch.autograd to 1e-5; the recurrence backprop is correct.\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines dh_prev; the check compares element-for-element to autograd.\n",
    "def dh_prev(Whh, h_prev, x_t, Wxh):\n",
    "    h_next = np.tanh(Whh @ h_prev + Wxh @ x_t)\n",
    "    dL_dhnext = 2 * h_next\n",
    "    dL_dpre = dL_dhnext * (1 - h_next ** 2)\n",
    "    grad = Whh.T @ dL_dpre\n",
    "    return grad\n",
    "\n",
    "check(\"13.6 BPTT gradient vs autograd\", _bptt_vs_autograd, required=True)\n",
    "print(\"hand-derived BPTT step matches torch.autograd to 1e-5; the recurrence backprop is correct.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b1564c5a",
   "metadata": {},
   "source": [
    "> **Interpretation.** Notice the structure of that gradient: every backward step multiplies by `(1 - h**2)` (at most 1, usually much less) and by `W_hh`. Chain `T` of them and you have the product-of-Jacobians from the equation above. That product is why the next demo explodes.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ba31b6bb",
   "metadata": {},
   "source": [
    "### A deliberate failure: the gradient that explodes\n",
    "\n",
    "We now train a small RNN on the ridership windows and *watch the gradient explode*, because we initialized $W_{hh}$ with a large spectral norm and did not clip. This is the canonical RNN failure (Pascanu et al. 2013): the product of Jacobians grows past 1 every step, the parameter update jumps, and the loss goes to `nan`. We stage it on purpose, then fix it. Read the loop's stopping logic first.\n",
    "\n",
    "> **Stop and think:** before running, predict what the loss column will do over the first few steps with an over-scaled `W_hh` and no clipping. Down? Flat? Up? <details><summary>Answer</summary>Up, then `nan`. Large `W_hh` makes the per-step Jacobian norm exceed 1; over a 12-step unroll the gradient is multiplied past 1 twelve times, the update overshoots, the weights grow, the next gradient is larger still. It is a positive feedback loop that saturates the floats.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 25,
   "id": "c6363ba1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:49.026996Z",
     "iopub.status.busy": "2026-06-10T19:46:49.026639Z",
     "iopub.status.idle": "2026-06-10T19:46:49.037547Z",
     "shell.execute_reply": "2026-06-10T19:46:49.037179Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "TinyRNN smoke output shape (4,) (expected (4,))\n"
     ]
    }
   ],
   "source": [
    "# A torch RNN regressor we will train two ways: broken (no clip, big init) then fixed.\n",
    "class TinyRNN(torch.nn.Module):\n",
    "    def __init__(self, hidden=16, init_scale=1.0):\n",
    "        super().__init__()\n",
    "        self.hidden = hidden\n",
    "        # W_hh init scale is the knob: 2.5 gives the recurrent matrix a spectral\n",
    "        # norm well above 1, so the unrolled Jacobian product blows up; 0.3 (the\n",
    "        # fixed run) keeps it tame.\n",
    "        self.Wxh = torch.nn.Parameter(torch.randn(hidden, 1) * 0.1)\n",
    "        self.Whh = torch.nn.Parameter(torch.randn(hidden, hidden) * init_scale)\n",
    "        self.bh = torch.nn.Parameter(torch.zeros(hidden))\n",
    "        self.head = torch.nn.Linear(hidden, 1)\n",
    "    def forward(self, x):                      # x: (B, L) treated as L steps of scalar input\n",
    "        B, Lseq = x.shape\n",
    "        h = torch.zeros(B, self.hidden)\n",
    "        for s in range(Lseq):\n",
    "            xs = x[:, s:s+1]                    # (B,1)\n",
    "            h = torch.tanh(xs @ self.Wxh.t() + h @ self.Whh.t() + self.bh)\n",
    "        return self.head(h).squeeze(-1)        # (B,)\n",
    "\n",
    "# smoke test the module BEFORE training\n",
    "torch.manual_seed(SEED)\n",
    "_m = TinyRNN(); print(\"TinyRNN smoke output shape\", tuple(_m(torch.randn(4, L)).shape), \"(expected (4,))\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 26,
   "id": "21bff70b",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:49.038719Z",
     "iopub.status.busy": "2026-06-10T19:46:49.038639Z",
     "iopub.status.idle": "2026-06-10T19:46:49.959229Z",
     "shell.execute_reply": "2026-06-10T19:46:49.958932Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  0 · loss         0.46 · grad-norm     19174.86\n",
      "step  1 · loss         6.10 · grad-norm        18.90\n",
      "step  2 · loss      1346.07 · grad-norm       302.45\n",
      "step  3 · loss    344378.84 · grad-norm      4839.18\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  4 · loss  88160776.00 · grad-norm     77426.96\n",
      "step  5 · loss 22569166848.00 · grad-norm   1238831.50\n",
      "step  6 · loss 5777707761664.00 · grad-norm  19821294.00\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  7 · loss 1479091576373248.00 · grad-norm 317140608.00\n",
      "step  8 · loss 378647203033382912.00 · grad-norm 5074249728.00\n",
      "step  9 · loss 96933683976546025472.00 · grad-norm 81187995648.00\n",
      "step 10 · loss 24815023097995782520832.00 · grad-norm 1299007930368.00\n",
      "step 11 · loss 6352645913086920325332992.00 · grad-norm 20784126885888.00\n",
      "\n",
      "diagnosis: the gradient norm grows step over step and the loss diverges. exploding gradients.\n"
     ]
    }
   ],
   "source": [
    "# THE BROKEN RUN: big W_hh init, NO gradient clipping. Expect the grad norm to\n",
    "# explode and the loss to diverge to nan. We record what happens; we do not hide it.\n",
    "torch.manual_seed(SEED)\n",
    "broken = TinyRNN(hidden=16, init_scale=2.5)   # over-scaled recurrent matrix\n",
    "opt_b = torch.optim.SGD(broken.parameters(), lr=0.5)   # a large step turns the big gradient into a runaway\n",
    "loss_fn = torch.nn.MSELoss()\n",
    "broken_log = []\n",
    "for step in range(12):\n",
    "    pred = broken(Xtr)\n",
    "    loss = loss_fn(pred, ytr)\n",
    "    opt_b.zero_grad()\n",
    "    loss.backward()\n",
    "    gnorm = torch.nn.utils.clip_grad_norm_(broken.parameters(), max_norm=float(\"inf\"))  # measure, do NOT clip\n",
    "    opt_b.step()\n",
    "    broken_log.append((step, loss.item(), float(gnorm)))\n",
    "    print(f\"step {step:2d} · loss {loss.item():12.2f} · grad-norm {float(gnorm):12.2f}\")\n",
    "print(\"\\ndiagnosis: the gradient norm grows step over step and the loss diverges. exploding gradients.\")\n",
    "assert not np.isfinite(broken_log[-1][1]) or broken_log[-1][1] > broken_log[0][1], \\\n",
    "    \"the broken run should diverge (loss grows or goes nan); that is the failure we are demonstrating\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5e6df1b0",
   "metadata": {},
   "source": [
    "> **Interpretation.** The grad-norm column climbs and the loss runs away, often to `nan`. Nothing is wrong with the *code*; the architecture's gradient is structurally unstable when $W_{hh}$ is too large and nothing caps the step. Now the fix: clip the global gradient norm before each update, and start $W_{hh}$ at a smaller scale.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 27,
   "id": "3bbffc3c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:49.964370Z",
     "iopub.status.busy": "2026-06-10T19:46:49.964244Z",
     "iopub.status.idle": "2026-06-10T19:46:50.943308Z",
     "shell.execute_reply": "2026-06-10T19:46:50.939886Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step    0 · train MSE 0.8130 · val MAE  55.9 · grad-norm 4.04\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step   20 · train MSE 0.1079 · val MAE  23.0 · grad-norm 0.53\n",
      "step   40 · train MSE 0.0544 · val MAE  17.4 · grad-norm 0.25\n",
      "step   60 · train MSE 0.0444 · val MAE  15.5 · grad-norm 0.11\n",
      "step   80 · train MSE 0.0376 · val MAE  13.8 · grad-norm 0.06\n",
      "step  100 · train MSE 0.0307 · val MAE  14.6 · grad-norm 0.42\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  119 · train MSE 0.0240 · val MAE  14.0 · grad-norm 0.12\n",
      "\n",
      "fixed: grad-norm is capped at 1.0, the loss decreases, training is stable.\n"
     ]
    }
   ],
   "source": [
    "# THE FIX: smaller W_hh init AND clip the global gradient norm to 1.0 before stepping.\n",
    "# clip_grad_norm_ rescales all gradients so their combined L2 norm <= max_norm.\n",
    "torch.manual_seed(SEED)\n",
    "fixed = TinyRNN(hidden=16, init_scale=0.3)    # tamer recurrent matrix\n",
    "opt_f = torch.optim.Adam(fixed.parameters(), lr=0.01)\n",
    "# adversarial read of the stopping logic: we run a FIXED step count (RNN_STEPS),\n",
    "# never an early `break`. There is no sentinel that could exit before any update.\n",
    "fixed_log = []\n",
    "for step in range(RNN_STEPS):\n",
    "    # forward\n",
    "    pred = fixed(Xtr)\n",
    "    loss = loss_fn(pred, ytr)\n",
    "    # backward\n",
    "    opt_f.zero_grad()\n",
    "    loss.backward()\n",
    "    # update (clip first, THEN step)\n",
    "    gnorm = torch.nn.utils.clip_grad_norm_(fixed.parameters(), max_norm=1.0)\n",
    "    opt_f.step()\n",
    "    # track stats\n",
    "    if step % max(1, RNN_STEPS // 6) == 0 or step == RNN_STEPS - 1:\n",
    "        with torch.no_grad():\n",
    "            vmae = (fixed(Xva) - yva).abs().mean().item() * sd\n",
    "        fixed_log.append((step, loss.item(), vmae, float(gnorm)))\n",
    "        print(f\"step {step:4d} · train MSE {loss.item():.4f} · val MAE {vmae:5.1f} · grad-norm {float(gnorm):.2f}\")\n",
    "assert np.isfinite(fixed_log[-1][1]), \"with clipping the loss must stay finite\"\n",
    "print(\"\\nfixed: grad-norm is capped at 1.0, the loss decreases, training is stable.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5eb15d26",
   "metadata": {},
   "source": [
    "> **Interpretation.** Same architecture, same data, two changes: a smaller recurrent init and a hard cap on the gradient norm. The loss now decreases monotonically-ish and never diverges. Clipping does not change the *direction* of the update, only its length when the raw gradient is pathologically large; that single guardrail is why every production RNN trained with it.\n",
    "\n",
    "> **Common confusion:** clipping is applied *after* `backward()` and *before* `step()`. Clip after the step and you have already taken the bad jump; clip before backward and there is no gradient to clip yet. The four-comment loop puts it in the `# update` block on purpose.\n",
    "\n",
    "> **Key takeaways.**\n",
    "> - The RNN cell is three matrices and a tanh, applied with shared weights at every step.\n",
    "> - BPTT's gradient is a product of `diag(1 - h^2) W_hh` Jacobians; that product vanishes or explodes, which is structural, not a tuning accident.\n",
    "> - Gradient clipping caps the update length and stops explosions cold; a tamer recurrent init helps too. Clip after backward, before step.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8593142f",
   "metadata": {},
   "source": [
    "## Part 5 — PyTorchify, then a names model\n",
    "\n",
    "> **Objectives.** Prove your hand-rolled recurrence equals `nn.RNN` by loading the same weights into both and asserting equal outputs (the PyTorchify agreement check), then assemble a char-level next-token model on the embedded names corpus, train it with the four-comment loop, and sample names as the qualitative payoff.\n",
    "\n",
    "The spec's torch-tier spine is the PyTorchify ladder: raw tensors, then your own module, then real `nn.*`, with nothing left a black box. We finish that ladder here. `nn.RNN` is exactly the equation from Part 4, vectorized over batch and time. We will copy weights from `nn.RNN` into a hand-rolled forward and assert the outputs match element-for-element. If they do, you understand `nn.RNN` completely.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "212be921",
   "metadata": {},
   "source": [
    "### Exercise 13.7 — Reconcile your recurrence with `nn.RNN`\n",
    "`Difficulty 3/5 · ~15 min`\n",
    "\n",
    "`nn.RNN` stores its parameters as `weight_ih_l0` ($W_{xh}$), `weight_hh_l0` ($W_{hh}$), `bias_ih_l0`, `bias_hh_l0`. Implement `manual_rnn(cell, x)` that reproduces a single-layer `nn.RNN(batch_first=True)`'s output sequence using only those weights and `torch.tanh`, where the per-step update is $h_t = \\tanh(W_{xh} x_t + b_{ih} + W_{hh} h_{t-1} + b_{hh})$. The check loads a real `nn.RNN`'s weights and asserts your output matches `cell(x)[0]` element-for-element.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 28,
   "id": "7609547c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:50.944601Z",
     "iopub.status.busy": "2026-06-10T19:46:50.944502Z",
     "iopub.status.idle": "2026-06-10T19:46:50.954861Z",
     "shell.execute_reply": "2026-06-10T19:46:50.954620Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] 13.7 manual_rnn == nn.RNN: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 28,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def manual_rnn(cell, x):\n",
    "    \"\"\"Reproduce nn.RNN(batch_first=True) forward using its own weights.\n",
    "\n",
    "    cell: an nn.RNN with input_size, hidden_size, num_layers=1, batch_first=True.\n",
    "    x: (B, T, input_size). Returns the full output sequence (B, T, hidden_size).\n",
    "    \"\"\"\n",
    "    Wih = cell.weight_ih_l0   # (hidden, input)\n",
    "    Whh = cell.weight_hh_l0   # (hidden, hidden)\n",
    "    bih = cell.bias_ih_l0     # (hidden,)\n",
    "    bhh = cell.bias_hh_l0     # (hidden,)\n",
    "    B, T, _ = x.shape\n",
    "    Hn = Whh.shape[0]\n",
    "    h = torch.zeros(B, Hn)\n",
    "    outs = []\n",
    "    for tstep in range(T):\n",
    "        xt = x[:, tstep, :]                       # (B, input)\n",
    "        # TODO 1: h = tanh(xt @ Wih.T + bih + h @ Whh.T + bhh)\n",
    "        h = None\n",
    "        attempted(h)\n",
    "        outs.append(h)\n",
    "    # TODO 2: stack the per-step hidden states along time -> (B, T, hidden)\n",
    "    seq = None\n",
    "    attempted(seq)\n",
    "    return seq\n",
    "\n",
    "def _manual_rnn_matches():\n",
    "    torch.manual_seed(SEED)\n",
    "    ref = torch.nn.RNN(input_size=3, hidden_size=5, num_layers=1, batch_first=True)\n",
    "    xb = torch.randn(2, 7, 3)\n",
    "    with torch.no_grad():\n",
    "        want = ref(xb)[0]                          # nn.RNN's output sequence\n",
    "        got = manual_rnn(ref, xb)\n",
    "    check_shape(got, (2, 7, 5))\n",
    "    check_close(got.detach().numpy(), want.detach().numpy(), atol=1e-5,\n",
    "                msg=\"manual recurrence must match nn.RNN element-for-element\")\n",
    "\n",
    "check(\"13.7 manual_rnn == nn.RNN\", _manual_rnn_matches)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c14505ee",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1 (conceptual)</summary>`nn.RNN` keeps *two* biases (input and hidden) and sums them; that is the only surprise versus Part 4. Linear layers store weight as `(out, in)`, so the forward uses `x @ W.T`.</details>\n",
    "\n",
    "<details><summary>Hint 2 (pseudocode)</summary>\n",
    "\n",
    "```python\n",
    "h = torch.tanh(xt @ Wih.T + bih + h @ Whh.T + bhh)\n",
    "...\n",
    "seq = torch.stack(outs, dim=1)   # (B, T, hidden)\n",
    "```\n",
    "Stack along `dim=1` because `batch_first=True` puts time in the middle.</details>\n",
    "\n",
    "<details><summary>Help — \"close but not within tolerance\"</summary>Most likely you dropped one of the two biases. `nn.RNN` adds both `bias_ih` and `bias_hh`. If you used only one, the gap will be small but real. Sum both.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 29,
   "id": "43aa670c",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:50.956075Z",
     "iopub.status.busy": "2026-06-10T19:46:50.955996Z",
     "iopub.status.idle": "2026-06-10T19:46:50.963706Z",
     "shell.execute_reply": "2026-06-10T19:46:50.963158Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] 13.7 manual_rnn == nn.RNN\n",
      "hand-rolled recurrence reproduces nn.RNN exactly; the library is no longer a black box.\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines manual_rnn; the check loads real nn.RNN weights and compares.\n",
    "def manual_rnn(cell, x):\n",
    "    Wih, Whh = cell.weight_ih_l0, cell.weight_hh_l0\n",
    "    bih, bhh = cell.bias_ih_l0, cell.bias_hh_l0\n",
    "    B, T, _ = x.shape\n",
    "    Hn = Whh.shape[0]\n",
    "    h = torch.zeros(B, Hn)\n",
    "    outs = []\n",
    "    for tstep in range(T):\n",
    "        xt = x[:, tstep, :]\n",
    "        h = torch.tanh(xt @ Wih.T + bih + h @ Whh.T + bhh)\n",
    "        outs.append(h)\n",
    "    return torch.stack(outs, dim=1)\n",
    "\n",
    "check(\"13.7 manual_rnn == nn.RNN\", _manual_rnn_matches, required=True)\n",
    "print(\"hand-rolled recurrence reproduces nn.RNN exactly; the library is no longer a black box.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fc4bf5a3",
   "metadata": {},
   "source": [
    "> **Interpretation.** That element-for-element match is the payoff of the PyTorchify ladder: `nn.RNN` is not magic, it is the loop you just wrote with two biases instead of one, compiled and vectorized. From here you can read its source without fear.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1cef37a5",
   "metadata": {},
   "source": [
    "### A char-level names model\n",
    "\n",
    "Now the chapter's second anchor: an embedded corpus of names. We model it character by character. Given the characters so far, predict the next one. This is the same next-token objective as a language model, at the smallest scale that still teaches the whole loop. We use `nn.RNN` (now demystified) plus an embedding and a linear head, and at the end we *sample* new names, which is the emotional payoff every makemore-style part ends on.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 30,
   "id": "621cc18d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:50.964917Z",
     "iopub.status.busy": "2026-06-10T19:46:50.964556Z",
     "iopub.status.idle": "2026-06-10T19:46:50.971303Z",
     "shell.execute_reply": "2026-06-10T19:46:50.970726Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "names corpus: 93 names, e.g. ['ava', 'olivia', 'emma', 'sophia', 'isabella']\n",
      "vocab size 26: .abcdefghijklmnoprstuvwxyz\n"
     ]
    }
   ],
   "source": [
    "# Anchor dataset #2: an EMBEDDED names corpus (offline-proof; no download).\n",
    "# A compact, deterministic list — enough to learn character statistics from.\n",
    "NAMES = \"\"\"ava olivia emma sophia isabella mia charlotte amelia harper evelyn\n",
    "abigail emily ella elizabeth camila luna sofia avery mila aria scarlett penelope\n",
    "layla chloe victoria madison eleanor grace nora riley zoey hannah hazel lily\n",
    "ellie violet lillian zoe stella aurora natalie emilia everly leah aubrey willow\n",
    "liam noah oliver elijah james william benjamin lucas henry theodore jack levi\n",
    "alexander jackson mateo daniel michael mason sebastian ethan logan owen samuel\n",
    "jacob asher aiden john joseph wyatt david leo luke julian hudson grayson matthew\n",
    "ezra gabriel carter isaac jayden luca anthony dylan lincoln thomas maverick\"\"\".split()\n",
    "print(f\"names corpus: {len(NAMES)} names, e.g. {NAMES[:5]}\")\n",
    "\n",
    "# Build the char vocabulary. '.' is the start/end token.\n",
    "chars = [\".\"] + sorted(set(\"\".join(NAMES)))\n",
    "stoi = {c: i for i, c in enumerate(chars)}\n",
    "itos = {i: c for c, i in stoi.items()}\n",
    "V = len(chars)\n",
    "print(f\"vocab size {V}: {''.join(chars)}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ebb10184",
   "metadata": {},
   "source": [
    "> **Predict:** the most common bigram start in this corpus. Which first letter should the model favor when generating a name? <details><summary>Answer</summary>Vowels and the letters that begin many of these names. We will not hand-count it; the trained model's samples will reveal what it learned. The point of sampling is to see the statistics made audible.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 31,
   "id": "f4e49264",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:50.972184Z",
     "iopub.status.busy": "2026-06-10T19:46:50.972103Z",
     "iopub.status.idle": "2026-06-10T19:46:50.979152Z",
     "shell.execute_reply": "2026-06-10T19:46:50.978916Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "char dataset: 619 (context, next-char) pairs, context length 3\n"
     ]
    }
   ],
   "source": [
    "# Turn names into (context, next-char) training pairs with a sliding char window.\n",
    "CTX = 3   # character context length: a trigram-ish model, small on purpose\n",
    "def encode_names(names, ctx):\n",
    "    xs, ys = [], []\n",
    "    for w in names:\n",
    "        chs = \".\" * ctx + w + \".\"       # pad start, mark end\n",
    "        ids = [stoi[c] for c in chs]\n",
    "        for i in range(len(ids) - ctx):\n",
    "            xs.append(ids[i:i+ctx]); ys.append(ids[i+ctx])\n",
    "    return torch.tensor(xs), torch.tensor(ys)\n",
    "\n",
    "Xc, yc = encode_names(NAMES, CTX)\n",
    "print(f\"char dataset: {Xc.shape[0]} (context, next-char) pairs, context length {CTX}\")\n",
    "assert Xc.shape == (yc.shape[0], CTX), \"each example is a CTX-length context predicting one next char\""
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 32,
   "id": "9acb93a8",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:50.980332Z",
     "iopub.status.busy": "2026-06-10T19:46:50.980255Z",
     "iopub.status.idle": "2026-06-10T19:46:51.024739Z",
     "shell.execute_reply": "2026-06-10T19:46:51.024131Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "CharRNN: 4858 params · logits shape (4, 26) (expected (4, 26))\n"
     ]
    }
   ],
   "source": [
    "# The char model: embed each context char, run a small RNN over the context,\n",
    "# project the final hidden state to vocab logits. nn.RNN is demystified now.\n",
    "class CharRNN(torch.nn.Module):\n",
    "    def __init__(self, vocab, embed=16, hidden=48):\n",
    "        super().__init__()\n",
    "        self.emb = torch.nn.Embedding(vocab, embed)\n",
    "        self.rnn = torch.nn.RNN(embed, hidden, batch_first=True)\n",
    "        self.head = torch.nn.Linear(hidden, vocab)\n",
    "    def forward(self, idx):                 # idx: (B, CTX) -> logits (B, vocab)\n",
    "        e = self.emb(idx)                   # (B, CTX, embed)\n",
    "        out, _ = self.rnn(e)                # (B, CTX, hidden)\n",
    "        return self.head(out[:, -1, :])     # use the last step -> (B, vocab)\n",
    "\n",
    "torch.manual_seed(SEED)\n",
    "charm = CharRNN(V)\n",
    "n_p = sum(p.numel() for p in charm.parameters())\n",
    "_logits = charm(Xc[:4])                     # randn-free smoke test on real tokens\n",
    "print(f\"CharRNN: {n_p} params · logits shape {tuple(_logits.shape)} (expected (4, {V}))\")\n",
    "assert _logits.shape == (4, V), \"forward must map (B, CTX) -> (B, vocab)\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61a0f166",
   "metadata": {},
   "source": [
    "> **Runtime:** the next cell trains the char model. It is ~`CHAR_STEPS` full-batch steps, a few seconds on CPU even at the full setting.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 33,
   "id": "7fc70d70",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:46:51.025976Z",
     "iopub.status.busy": "2026-06-10T19:46:51.025622Z",
     "iopub.status.idle": "2026-06-10T19:47:05.534006Z",
     "shell.execute_reply": "2026-06-10T19:47:05.533733Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step    0 · cross-entropy 3.333 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step   33 · cross-entropy 1.296 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step   66 · cross-entropy 0.834 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step   99 · cross-entropy 0.776 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  132 · cross-entropy 0.766 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  165 · cross-entropy 0.762 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  198 · cross-entropy 0.761 (uniform baseline = ln(26) = 3.258)\n"
     ]
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "step  199 · cross-entropy 0.761 (uniform baseline = ln(26) = 3.258)\n"
     ]
    }
   ],
   "source": [
    "# Train the char model with the four-comment loop. Cross-entropy over the vocab.\n",
    "torch.manual_seed(SEED)\n",
    "charm = CharRNN(V)\n",
    "opt_c = torch.optim.Adam(charm.parameters(), lr=0.02)\n",
    "xent = torch.nn.CrossEntropyLoss()\n",
    "char_log = []\n",
    "for step in range(CHAR_STEPS):\n",
    "    # forward\n",
    "    logits = charm(Xc)\n",
    "    loss = xent(logits, yc)\n",
    "    # backward\n",
    "    opt_c.zero_grad()\n",
    "    loss.backward()\n",
    "    # update (clip: it is an RNN, the Part 4 lesson applies here too)\n",
    "    torch.nn.utils.clip_grad_norm_(charm.parameters(), max_norm=5.0)\n",
    "    opt_c.step()\n",
    "    # track stats\n",
    "    if step % max(1, CHAR_STEPS // 6) == 0 or step == CHAR_STEPS - 1:\n",
    "        char_log.append((step, loss.item()))\n",
    "        print(f\"step {step:4d} · cross-entropy {loss.item():.3f} (uniform baseline = ln({V}) = {np.log(V):.3f})\")\n",
    "assert char_log[-1][1] < np.log(V), \"a trained model must beat the uniform-guess cross-entropy ln(V)\""
   ]
  },
  {
   "cell_type": "markdown",
   "id": "591a5cdf",
   "metadata": {},
   "source": [
    "> **Interpretation.** The cross-entropy starts near `ln(V)` (uniform guessing over the vocabulary) and drops as the model learns which characters follow which. Beating `ln(V)` is the minimum bar; the assert enforces it. Now the payoff: sample new names.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 34,
   "id": "c0c06faa",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:05.535219Z",
     "iopub.status.busy": "2026-06-10T19:47:05.535134Z",
     "iopub.status.idle": "2026-06-10T19:47:05.613731Z",
     "shell.execute_reply": "2026-06-10T19:47:05.613360Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "sampled names: ['luna', 'oliver', 'isabella', 'henry', 'matt', 'lily', 'henry', 'hazel']\n",
      "[ ok ] all samples drawn from the learned character alphabet\n"
     ]
    }
   ],
   "source": [
    "# Sample names from the trained model. Use the OFFSET seed so generation never\n",
    "# perturbs training reproducibility (the Karpathy +10 convention).\n",
    "@torch.no_grad()\n",
    "def sample_name(model, max_len=12, temperature=0.8, seed=GEN_SEED):\n",
    "    g = torch.Generator().manual_seed(seed)\n",
    "    ctx = [stoi[\".\"]] * CTX\n",
    "    out = []\n",
    "    for _ in range(max_len):\n",
    "        logits = model(torch.tensor([ctx])) / temperature\n",
    "        probs = torch.softmax(logits, dim=-1)\n",
    "        nxt = int(torch.multinomial(probs, 1, generator=g).item())\n",
    "        if itos[nxt] == \".\":\n",
    "            break\n",
    "        out.append(itos[nxt])\n",
    "        ctx = ctx[1:] + [nxt]\n",
    "    return \"\".join(out)\n",
    "\n",
    "charm.eval()\n",
    "samples = [sample_name(charm, seed=GEN_SEED + i) for i in range(8)]\n",
    "print(\"sampled names:\", samples)\n",
    "# behavioral check: samples are non-empty strings over the learned alphabet\n",
    "assert all(set(s) <= set(chars) for s in samples), \"samples must use only learned characters\"\n",
    "print(\"[ ok ] all samples drawn from the learned character alphabet\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7184584",
   "metadata": {},
   "source": [
    "> **Interpretation.** With a 3-character context and a tiny RNN the samples are name-shaped fragments, not polished names; the model has learned local character statistics (which letters follow which) but not long-range structure. That gap, between local plausibility and global coherence, is exactly what attention and longer context windows buy you in Ch 14 and Ch 15.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d6c4f63f",
   "metadata": {},
   "source": [
    "### Experiment log\n",
    "\n",
    "The record every torch chapter keeps: each model, its size, and its measured number, for both FAST and full settings, so you know what to expect. If your numbers are far off, something is wrong.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 35,
   "id": "8905a6a6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:05.618951Z",
     "iopub.status.busy": "2026-06-10T19:47:05.618786Z",
     "iopub.status.idle": "2026-06-10T19:47:05.623800Z",
     "shell.execute_reply": "2026-06-10T19:47:05.623414Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "model                      params                  metric\n",
      "----------------------------------------------------------\n",
      "seasonal-naive (baseline)        0  test MAE   16.7 riders\n",
      "context-window MLP            449  test MAE   22.9 riders\n",
      "TinyRNN (clipped)             305  val  MAE   14.0 riders\n",
      "CharRNN (names)              4858  cross-ent  0.761 (uniform 3.258)\n",
      "\n",
      "settings: FAST=False · NPLM_STEPS=600 · RNN_STEPS=120 · CHAR_STEPS=200\n",
      "expectation: at full settings the MLP lands within a few riders of seasonal-naive;\n",
      "the CharRNN cross-entropy drops well below ln(V); FAST settings land in the same ballpark, looser.\n"
     ]
    }
   ],
   "source": [
    "# The experiment log as a printed table. Numbers reflect THIS run's settings.\n",
    "print(f\"{'model':24} {'params':>8}  {'metric':>22}\")\n",
    "print(\"-\" * 58)\n",
    "print(f\"{'seasonal-naive (baseline)':24} {0:>8}  test MAE {snaive_test:6.1f} riders\")\n",
    "print(f\"{'context-window MLP':24} {n_params:>8}  test MAE {nplm_test_mae:6.1f} riders\")\n",
    "print(f\"{'TinyRNN (clipped)':24} {sum(p.numel() for p in fixed.parameters()):>8}  \"\n",
    "      f\"val  MAE {fixed_log[-1][2]:6.1f} riders\")\n",
    "print(f\"{'CharRNN (names)':24} {n_p:>8}  cross-ent {char_log[-1][1]:6.3f} (uniform {np.log(V):.3f})\")\n",
    "print(f\"\\nsettings: FAST={FAST} · NPLM_STEPS={NPLM_STEPS} · RNN_STEPS={RNN_STEPS} · CHAR_STEPS={CHAR_STEPS}\")\n",
    "print(\"expectation: at full settings the MLP lands within a few riders of seasonal-naive;\")\n",
    "print(\"the CharRNN cross-entropy drops well below ln(V); FAST settings land in the same ballpark, looser.\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 36,
   "id": "4e3e4534",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:05.624885Z",
     "iopub.status.busy": "2026-06-10T19:47:05.624754Z",
     "iopub.status.idle": "2026-06-10T19:47:05.629365Z",
     "shell.execute_reply": "2026-06-10T19:47:05.628989Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "skipped: needs GPU. On CUDA this would time a 256-hidden CharRNN forward,\n",
      "showing the per-step recurrence (not the matmul) is what serializes RNN inference.\n"
     ]
    }
   ],
   "source": [
    "# gpu-only: a timing comparison would show the recurrence is the bottleneck.\n",
    "if device == \"cuda\":\n",
    "    import time\n",
    "    big = CharRNN(V, hidden=256).to(device)\n",
    "    xb = Xc.to(device)\n",
    "    torch.cuda.synchronize(); t0 = time.time()\n",
    "    for _ in range(20): _ = big(xb)\n",
    "    torch.cuda.synchronize()\n",
    "    print(f\"20 forwards of a 256-hidden CharRNN on GPU: {time.time()-t0:.3f}s\")\n",
    "else:\n",
    "    print(\"skipped: needs GPU. On CUDA this would time a 256-hidden CharRNN forward,\")\n",
    "    print(\"showing the per-step recurrence (not the matmul) is what serializes RNN inference.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0e6a2e1b",
   "metadata": {},
   "source": [
    "## Safety lens\n",
    "\n",
    "RNNs are where verbatim memorization in neural language models was first studied carefully, and the findings transferred straight to transformers. Carlini et al. (2019) showed an LSTM trained on a corpus containing rare strings (card numbers, secrets) will emit those exact strings when prompted with the right prefix. The mechanism is structural: the model's job is to put high probability on training sequences, and rare strings get high probability only by being copied verbatim. The same pathway that lets a recurrent cell remember long-range structure is the one that memorizes specific examples; you cannot have one without the other.\n",
    "\n",
    "A concrete, runnable version of the audit: plant a rare string in a tiny training corpus, overtrain a model on it, then check whether the model reproduces the secret when prompted with its prefix. We run a deliberately small version so it executes in seconds and the leak is visible.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 37,
   "id": "985840cb",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:05.630588Z",
     "iopub.status.busy": "2026-06-10T19:47:05.630509Z",
     "iopub.status.idle": "2026-06-10T19:47:05.636825Z",
     "shell.execute_reply": "2026-06-10T19:47:05.636178Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "memorization corpus: 21 items incl. the secret 'qzx7k', vocab now 28\n"
     ]
    }
   ],
   "source": [
    "# A minimal memorization demo: overtrain a char model on a corpus with ONE rare\n",
    "# string and check whether prompting its prefix extracts it verbatim.\n",
    "SECRET = \"qzx7k\"                          # a rare token unlike any name\n",
    "corpus = NAMES[:20] + [SECRET]            # the secret hides among common names\n",
    "# reuse the char pipeline at the secret's own context length\n",
    "sctx = 2\n",
    "def encode(words, ctx):\n",
    "    xs, ys = [], []\n",
    "    for w in words:\n",
    "        ids = [stoi.get(c, 0) for c in (\".\" * ctx + w + \".\")]\n",
    "        for i in range(len(ids) - ctx):\n",
    "            xs.append(ids[i:i+ctx]); ys.append(ids[i+ctx])\n",
    "    return torch.tensor(xs), torch.tensor(ys)\n",
    "# extend the vocab to cover the secret's characters\n",
    "for c in SECRET:\n",
    "    if c not in stoi:\n",
    "        stoi[c] = len(stoi); itos[len(itos)] = c\n",
    "Vs = len(stoi)\n",
    "Xs, ys_ = encode(corpus, sctx)\n",
    "print(f\"memorization corpus: {len(corpus)} items incl. the secret '{SECRET}', vocab now {Vs}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 38,
   "id": "e80b6503",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:05.637806Z",
     "iopub.status.busy": "2026-06-10T19:47:05.637717Z",
     "iopub.status.idle": "2026-06-10T19:47:27.449995Z",
     "shell.execute_reply": "2026-06-10T19:47:27.449648Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "prompted with 'qz', the overtrained model greedily continues: 'qzx7k'\n",
      "leak: the secret was reproduced verbatim\n"
     ]
    }
   ],
   "source": [
    "# Overtrain a small model HARD on the tiny corpus, then probe with the secret's prefix.\n",
    "class TinyChar(torch.nn.Module):\n",
    "    def __init__(self, vocab, ctx, hidden=64):\n",
    "        super().__init__()\n",
    "        self.emb = torch.nn.Embedding(vocab, 16)\n",
    "        self.rnn = torch.nn.RNN(16, hidden, batch_first=True)\n",
    "        self.head = torch.nn.Linear(hidden, vocab)\n",
    "    def forward(self, idx):\n",
    "        out, _ = self.rnn(self.emb(idx)); return self.head(out[:, -1, :])\n",
    "\n",
    "torch.manual_seed(SEED)\n",
    "mm = TinyChar(Vs, sctx)\n",
    "o = torch.optim.Adam(mm.parameters(), lr=0.02)\n",
    "xe = torch.nn.CrossEntropyLoss()\n",
    "mem_steps = 60 if FAST else 200\n",
    "for _ in range(mem_steps):\n",
    "    loss = xe(mm(Xs), ys_); o.zero_grad(); loss.backward()\n",
    "    torch.nn.utils.clip_grad_norm_(mm.parameters(), 5.0); o.step()\n",
    "\n",
    "@torch.no_grad()\n",
    "def greedy_extend(model, prefix, n):\n",
    "    ctx = [stoi.get(c, 0) for c in (\".\" * sctx + prefix)][-sctx:]\n",
    "    out = list(prefix)\n",
    "    for _ in range(n):\n",
    "        nxt = int(model(torch.tensor([ctx])).argmax(-1).item())\n",
    "        if itos[nxt] == \".\": break\n",
    "        out.append(itos[nxt]); ctx = ctx[1:] + [nxt]\n",
    "    return \"\".join(out)\n",
    "\n",
    "extracted = greedy_extend(mm, SECRET[:2], len(SECRET) + 1)\n",
    "print(f\"prompted with '{SECRET[:2]}', the overtrained model greedily continues: '{extracted}'\")\n",
    "print(\"leak:\", \"the secret was reproduced verbatim\" if SECRET in extracted\n",
    "      else \"not fully reproduced this run (smaller / FAST training memorizes less)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00c70fdf",
   "metadata": {},
   "source": [
    "> **Interpretation.** Overtrained on a corpus where the secret is the only way to drive the loss down on those characters, the model reproduces it from a short prefix. The fix is not architectural: audit training data for rare strings (hashes, secrets, identifiers), and run exactly this kind of extraction probe before deploying. The draft's three habits stand: audit your data, test extraction before deployment, and cap user-controlled sampling temperature in production (very low temperature raises verbatim-recall fidelity). Ch 22 revisits memorization as a mech-interp problem; Ch 24 as a red-team problem.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0f024bc0",
   "metadata": {},
   "source": [
    "## Test yourself\n",
    "\n",
    "Three parts: concept self-checks with folded answers, auto-checked problems, and a capstone with a rubric and a folded reference. Try before you peek; every answer is somewhere in this notebook, and if unsure, re-run that section.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a8ca93b6",
   "metadata": {},
   "source": [
    "### Part A — Concepts\n",
    "\n",
    "1. On the ridership series, why does seasonal-naive crush plain naive? <details><summary>Answer</summary>The dominant signal is the 12-month cycle (amplitude 40); the month-to-month trend step is only 1.2. Plain naive pays the full seasonal swing every month; seasonal-naive copies the same month last year, paying only ~12 months of trend drift plus noise.</details>\n",
    "2. You computed the ACF and it peaked at lags 12, 24, 36 (see the stem plot above). What does that tell you, and what `s` would you pass to seasonal-naive? <details><summary>Answer</summary>The peaks at multiples of 12 are the seasonal fingerprint; the period is 12, so `s=12`. The ACF read the period off the data without being told it.</details>\n",
    "3. Why is sklearn's default `train_test_split` wrong for forecasting? <details><summary>Answer</summary>It shuffles, scattering future months before past ones, so the model \"predicts\" months it effectively trained around. Forecasting needs a contiguous, ordered, time-respecting split: train on the past, test on the future.</details>\n",
    "4. In the BPTT gradient $\\partial h_j/\\partial h_{j-1} = \\operatorname{diag}(1-h_j^2)\\,W_{hh}$, which factor causes vanishing and which can cause exploding? <details><summary>Answer</summary>The $1-h_j^2$ (tanh derivative, at most 1, usually much less) drives vanishing as the product of many sub-1 factors. A large-spectral-norm $W_{hh}$ drives exploding when its repeated multiplication grows the product past 1.</details>\n",
    "5. Look at the broken-run loss/grad-norm print in Part 4. In one sentence, what is the diagnostic signature of exploding gradients? <details><summary>Answer</summary>The gradient norm grows step over step and the loss climbs (then goes to `nan`); a healthy run has a roughly stable or shrinking grad norm and a decreasing loss.</details>\n",
    "6. Why do we clip the gradient norm *after* `backward()` and *before* `step()`, not elsewhere? <details><summary>Answer</summary>There is no gradient to clip before `backward()`, and clipping after `step()` means the bad jump already happened. The cap must sit between computing the gradient and applying it.</details>\n",
    "7. `nn.RNN` matched your hand-rolled recurrence only after you summed *two* biases. Why does `nn.RNN` keep `bias_ih` and `bias_hh` separately? <details><summary>Answer</summary>cuDNN's fused kernel computes the input projection and the hidden projection separately and adds a bias to each before summing; mathematically the two biases could be merged into one, but the API exposes both to match the kernel layout. Summing them reproduces the result.</details>\n",
    "8. The CharRNN's cross-entropy started near `ln(V)`. What does that number mean and why is beating it the minimum bar? <details><summary>Answer</summary>`ln(V)` is the cross-entropy of guessing uniformly over `V` characters (no knowledge). A model that has learned anything about which characters follow which must score below it; failing to is evidence of a bug.</details>\n",
    "9. Micro-task: in one line, compute the fraction of ridership months that exceed the series mean. <details><summary>Answer</summary>`(ridership > ridership.mean()).mean()`. A bool array's mean is the fraction True; it should be near 0.5 for a roughly symmetric series.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f47c0bd7",
   "metadata": {},
   "source": [
    "### Part B1 — Differencing to remove the trend\n",
    "`Difficulty 2/5 · ~10 min`\n",
    "\n",
    "A first difference $d_t = x_t - x_{t-1}$ removes a linear trend, a standard pre-processing step before fitting a stationary model. Implement `difference(series)` returning the length-`(n-1)` array of consecutive differences, and `undifference(first, diffs)` that reconstructs the original from the first value and the differences. The check asserts the round-trip recovers the series exactly and that the toy is right.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 39,
   "id": "2cb66a77",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:27.451105Z",
     "iopub.status.busy": "2026-06-10T19:47:27.450950Z",
     "iopub.status.idle": "2026-06-10T19:47:27.460235Z",
     "shell.execute_reply": "2026-06-10T19:47:27.459843Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] B1 difference (toy): not attempted yet — fill in the TODO above, then re-run.\n",
      "[ -- ] B1 round-trip recovers series: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 39,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def difference(series):\n",
    "    \"\"\"Return consecutive first differences: out[i] = series[i+1] - series[i]. Length n-1.\"\"\"\n",
    "    s = np.asarray(series, dtype=float)\n",
    "    # TODO 1: out = s[1:] - s[:-1]\n",
    "    out = None\n",
    "    attempted(out)\n",
    "    return np.asarray(out, dtype=float)\n",
    "\n",
    "def undifference(first, diffs):\n",
    "    \"\"\"Reconstruct the series from its first value and the differences.\"\"\"\n",
    "    diffs = np.asarray(diffs, dtype=float)\n",
    "    # TODO 2: the series is first, then a running cumulative sum of the diffs added on.\n",
    "    #         np.concatenate([[first], first + np.cumsum(diffs)]) reconstructs it.\n",
    "    out = None\n",
    "    attempted(out)\n",
    "    return np.asarray(out, dtype=float)\n",
    "\n",
    "def _diff_toy():\n",
    "    toy = np.array([2.0, 5.0, 4.0, 9.0])\n",
    "    check_close(difference(toy), [3.0, -1.0, 5.0], msg=\"consecutive differences of 2,5,4,9\")\n",
    "\n",
    "def _diff_roundtrip():\n",
    "    d = difference(ridership)\n",
    "    recon = undifference(ridership[0], d)\n",
    "    check_close(recon, ridership, atol=1e-8, msg=\"undifference(first, difference(x)) must recover x\")\n",
    "\n",
    "check(\"B1 difference (toy)\", _diff_toy)\n",
    "check(\"B1 round-trip recovers series\", _diff_roundtrip)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dd43ce71",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1</summary>`difference` is `s[1:] - s[:-1]`. To invert, the series is `first` followed by `first + cumsum(diffs)`.</details>\n",
    "<details><summary>Solution</summary>\n",
    "\n",
    "```python\n",
    "def difference(series):\n",
    "    s = np.asarray(series, dtype=float)\n",
    "    return s[1:] - s[:-1]\n",
    "\n",
    "def undifference(first, diffs):\n",
    "    diffs = np.asarray(diffs, dtype=float)\n",
    "    return np.concatenate([[first], first + np.cumsum(diffs)])\n",
    "```\n",
    "</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 40,
   "id": "55456639",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:27.461185Z",
     "iopub.status.busy": "2026-06-10T19:47:27.461089Z",
     "iopub.status.idle": "2026-06-10T19:47:27.466812Z",
     "shell.execute_reply": "2026-06-10T19:47:27.466181Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] B1 difference (toy)\n",
      "[ ok ] B1 round-trip recovers series\n",
      "differenced series mean 1.10 (near the per-step trend 1.2, as expected)\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines both; the checks re-verify the toy and the exact round-trip.\n",
    "def difference(series):\n",
    "    s = np.asarray(series, dtype=float)\n",
    "    return s[1:] - s[:-1]\n",
    "\n",
    "def undifference(first, diffs):\n",
    "    diffs = np.asarray(diffs, dtype=float)\n",
    "    return np.concatenate([[first], first + np.cumsum(diffs)])\n",
    "\n",
    "check(\"B1 difference (toy)\", _diff_toy, required=True)\n",
    "check(\"B1 round-trip recovers series\", _diff_roundtrip, required=True)\n",
    "print(f\"differenced series mean {difference(ridership).mean():.2f} (near the per-step trend 1.2, as expected)\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4e32a52c",
   "metadata": {},
   "source": [
    "> **Interpretation.** The mean of the first differences lands near 1.2, the trend slope we built in: differencing turned a trending series into one that fluctuates around a constant step. That is exactly what stationarity-seeking models (ARIMA's \"I\") want.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c0ebafdb",
   "metadata": {},
   "source": [
    "### Part B2 — Gradient-norm clipping from scratch\n",
    "`Difficulty 3/5 · ~12 min`\n",
    "\n",
    "You used `torch.nn.utils.clip_grad_norm_` in Part 4. Now implement it. Given a list of gradient tensors, compute the global L2 norm (the norm of all gradients concatenated), and if it exceeds `max_norm`, scale every gradient by `max_norm / total_norm`. Implement `clip_grads(grads, max_norm)` returning the (possibly rescaled) list and the *pre-clip* total norm. The check compares against torch's own implementation.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 41,
   "id": "87d14fa4",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:27.469422Z",
     "iopub.status.busy": "2026-06-10T19:47:27.469333Z",
     "iopub.status.idle": "2026-06-10T19:47:27.482084Z",
     "shell.execute_reply": "2026-06-10T19:47:27.481800Z"
    }
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ -- ] B2 clip_grads vs torch: not attempted yet — fill in the TODO above, then re-run.\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "False"
      ]
     },
     "execution_count": 41,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "def clip_grads(grads, max_norm):\n",
    "    \"\"\"Clip a list of grad tensors to a global L2 norm. Returns (clipped_list, total_norm_before).\"\"\"\n",
    "    # TODO 1: total_norm = sqrt(sum of squared L2 norms of each grad)\n",
    "    total_norm = None\n",
    "    attempted(total_norm)\n",
    "    # TODO 2: if total_norm > max_norm, scale every grad by max_norm/total_norm;\n",
    "    #         otherwise leave them. Return the list and the PRE-clip total_norm.\n",
    "    if total_norm > max_norm:\n",
    "        scale = max_norm / (total_norm + 1e-12)\n",
    "        clipped = [g * scale for g in grads]\n",
    "    else:\n",
    "        clipped = [g for g in grads]\n",
    "    return clipped, total_norm\n",
    "\n",
    "def _clip_vs_torch():\n",
    "    torch.manual_seed(SEED)\n",
    "    gs = [torch.randn(3, 4) * 3, torch.randn(5) * 3]   # scaled so the norm exceeds max_norm=1\n",
    "    # our version on copies\n",
    "    ours, tot = clip_grads([g.clone() for g in gs], max_norm=1.0)\n",
    "    ours_norm = torch.sqrt(sum((g ** 2).sum() for g in ours)).item()\n",
    "    # torch's version: attach as .grad to dummy params\n",
    "    ps = [torch.nn.Parameter(torch.zeros_like(g)) for g in gs]\n",
    "    for p, g in zip(ps, gs):\n",
    "        p.grad = g.clone()\n",
    "    torch_tot = torch.nn.utils.clip_grad_norm_(ps, max_norm=1.0).item()\n",
    "    tot_val = tot.item() if hasattr(tot, \"item\") else float(tot)\n",
    "    check_close(tot_val, torch_tot, atol=1e-4, msg=\"pre-clip total norm must match torch\")\n",
    "    # these grads exceed max_norm, so after clipping the global norm is exactly 1.0\n",
    "    check_close(ours_norm, 1.0, atol=1e-4,\n",
    "                msg=\"after clipping a too-large gradient, the global norm must be exactly max_norm=1.0\")\n",
    "\n",
    "check(\"B2 clip_grads vs torch\", _clip_vs_torch)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "26f083f7",
   "metadata": {},
   "source": [
    "<details><summary>Hint 1</summary>The global norm is `sqrt(sum over grads of (g**2).sum())`. The branch logic (when to scale) is already written; you only fill the `total_norm`.</details>\n",
    "<details><summary>Solution</summary>\n",
    "\n",
    "```python\n",
    "total_norm = torch.sqrt(sum((g ** 2).sum() for g in grads))\n",
    "```\n",
    "The rest (scaling when it exceeds `max_norm`, returning the pre-clip norm) is the body already shown.</details>\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 42,
   "id": "f7ebe8be",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-10T19:47:27.483331Z",
     "iopub.status.busy": "2026-06-10T19:47:27.483244Z",
     "iopub.status.idle": "2026-06-10T19:47:27.490305Z",
     "shell.execute_reply": "2026-06-10T19:47:27.489757Z"
    },
    "collapsed": true,
    "jupyter": {
     "source_hidden": true
    },
    "tags": [
     "hide-input"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "[ ok ] B2 clip_grads vs torch\n",
      "hand-rolled gradient clipping matches torch.nn.utils.clip_grad_norm_.\n"
     ]
    }
   ],
   "source": [
    "#@title Solution { display-mode: \"form\" }\n",
    "# solution: redefines clip_grads; the check compares against torch.nn.utils.clip_grad_norm_.\n",
    "def clip_grads(grads, max_norm):\n",
    "    total_norm = torch.sqrt(sum((g ** 2).sum() for g in grads))\n",
    "    if total_norm > max_norm:\n",
    "        scale = max_norm / (total_norm + 1e-12)\n",
    "        clipped = [g * scale for g in grads]\n",
    "    else:\n",
    "        clipped = [g for g in grads]\n",
    "    return clipped, total_norm\n",
    "\n",
    "check(\"B2 clip_grads vs torch\", _clip_vs_torch, required=True)\n",
    "print(\"hand-rolled gradient clipping matches torch.nn.utils.clip_grad_norm_.\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ac690d18",
   "metadata": {},
   "source": [
    "### Part C — Capstone: a forecaster that must beat the baseline, honestly\n",
    "\n",
    "Bring the chapter together. Build and evaluate a forecasting model end to end on the ridership series (or a series of your own), under the discipline this chapter taught. Deliverables:\n",
    "\n",
    "1. A time-respecting train/val/test split and causal windows (reuse `time_split`, `make_windows`), with the standardizer fit on training data only.\n",
    "2. A learned model of your choice (the `WindowMLP`, a `nn.RNN`/`nn.GRU` regressor, or a direct multi-horizon head via `make_multi_targets`). Train it with the four-comment loop and gradient clipping.\n",
    "3. The seasonal-naive baseline computed on the same test target months, in the same units.\n",
    "4. A single honest verdict: does your model beat seasonal-naive on the test set, by how much, and would you ship it or the baseline?\n",
    "\n",
    "Self-assessment (pass / partial / fail): (a) the split is contiguous and the standardizer never sees val/test; (b) windows are causal (assert it); (c) the model trains stably with clipping (no `nan`); (d) you report test MAE in rider units against the baseline; (e) your verdict is honest, including \"the baseline wins\" if it does.\n",
    "\n",
    "<details><summary>My solution (reference, ~10s on CPU)</summary>\n",
    "\n",
    "```python\n",
    "# a GRU regressor over the last L standardized values\n",
    "torch.manual_seed(SEED)\n",
    "class GRUForecaster(torch.nn.Module):\n",
    "    def __init__(self, L, hidden=32):\n",
    "        super().__init__()\n",
    "        self.gru = torch.nn.GRU(1, hidden, batch_first=True)\n",
    "        self.head = torch.nn.Linear(hidden, 1)\n",
    "    def forward(self, x):              # x: (B, L) -> (B,)\n",
    "        _, h = self.gru(x.unsqueeze(-1))   # (B, L, 1) in; h: (1, B, hidden)\n",
    "        return self.head(h.squeeze(0)).squeeze(-1)\n",
    "\n",
    "g = GRUForecaster(L)\n",
    "opt = torch.optim.Adam(g.parameters(), lr=0.01)\n",
    "for step in range(RNN_STEPS):\n",
    "    pred = g(Xtr); loss = torch.nn.functional.mse_loss(pred, ytr)\n",
    "    opt.zero_grad(); loss.backward()\n",
    "    torch.nn.utils.clip_grad_norm_(g.parameters(), 1.0); opt.step()\n",
    "with torch.no_grad():\n",
    "    gru_mae = (g(Xte) - yte).abs().mean().item() * sd\n",
    "print(f\"GRU test MAE {gru_mae:.1f} vs seasonal-naive {snaive_test:.1f}\")\n",
    "print(\"ship:\", \"GRU\" if gru_mae < snaive_test else \"seasonal-naive (it wins, and that is a real result)\")\n",
    "```\n",
    "The lesson is the same as Part 3: on a short, strongly-seasonal series the gap is small and seed-dependent. A correct capstone reports the comparison honestly and is willing to ship the baseline. Scale the series up (more years, multiple correlated series) and the learned model's edge grows; that is the right next experiment.</details>\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0799e58d",
   "metadata": {},
   "source": [
    "## Reflection\n",
    "\n",
    "In about 150 words, write up the dumbest bug you hit in this notebook and how you found it. A strong candidate: the windowing off-by-one. Did `make_windows` or `make_multi_targets` leak the target into the input, and what made it visible (the causality assert, a suspiciously low val MAE)? Or the exploding-gradient run: what did the grad-norm column look like before you connected it to the loss going `nan`? Or the BPTT gradient: did you forget the transpose on `W_hh`, and how did the autograd comparison localize it? Write what the symptom was, what you suspected, what you printed or asserted to confirm it, and what the fix was.\n",
    "\n",
    "Nobody grades this. Writing it is the point: the muscle you are building is systematic debugging, turning \"it's broken\" into \"here is the one line that was wrong and here is how I proved it.\" On sequence models that skill matters double, because a silent leak or a quietly vanishing gradient produces plausible-looking numbers that are wrong.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8794f3f4",
   "metadata": {},
   "source": [
    "## Going further\n",
    "\n",
    "- Karpathy, *The Unreasonable Effectiveness of Recurrent Neural Networks* (2015) — the foundational char-rnn post; the Shakespeare and Linux-kernel samples still hold up. The cell-level visualizations are the ancestor of mech-interp.\n",
    "- Olah, *Understanding LSTM Networks* (2015) — the canonical gate-by-gate LSTM explainer. Read alongside Karpathy.\n",
    "- Pascanu, Mikolov, Bengio, *On the difficulty of training RNNs* (2013) — the gradient-clipping and exploding/vanishing analysis you saw in Part 4, derived in full.\n",
    "- Géron, *Hands-On ML* 3e, Ch 15 — the forecasting case study and the LSTM/GRU/1D-conv implementations this notebook compresses.\n",
    "- d2l.ai, *Recurrent Neural Networks* and *Modern RNNs* chapters — the scratch/concise pairing for RNN, LSTM, GRU, seq2seq.\n",
    "- Lilian Weng, *The Transformer Family v2* §state-space-models — where RNNs went in 2026 (Mamba, S4, S5), the recurrent lineage that beats transformers on very long sequences.\n",
    "\n",
    "## What this enables\n",
    "\n",
    "- **Ch 14 — NLP with RNNs and Attention**: the next-token objective and the encoder-decoder framing here are exactly what attention bolts onto. The single-context-vector bottleneck you would hit scaling this up is what Bahdanau attention fixes.\n",
    "- **Ch 15 — Transformers from Scratch**: once you have felt the sequential bottleneck of recurrence (the GPU cell hinted at it), the transformer's parallel-over-time design is the obvious next move.\n",
    "- **Ch 22 — Mech-Interp**: the cleanest early interpretability work was done on LSTMs (Karpathy's interpretable cells, Distill's memorization study). The methods generalized to transformers.\n",
    "\n",
    "> **Forward teaser.** Our CharRNN sees only `CTX=3` characters and samples name-shaped fragments, not coherent names; it has local statistics but no long-range memory. Ch 15's transformer, on the same kind of data, attends over the *entire* context at once and reaches far lower loss. The gap between \"locally plausible\" and \"globally coherent\" is the gap between this chapter and the next two.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "111db365",
   "metadata": {},
   "source": [
    "---\n",
    "*Built top-to-bottom. If every check above printed `[ ok ]`, you've reproduced the chapter. Total running time and verification stamp written by CI.*\n"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "obvix-nb",
   "language": "python",
   "name": "obvix-nb"
  },
  "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.10.12"
  },
  "obvix": {
   "title": "Ch 13 — Sequences and Time Series"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
