{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dceec757",
   "metadata": {},
   "outputs": [],
   "source": [
    "%matplotlib inline\n",
    "import time\n",
    "import numpy as np\n",
    "import matplotlib as mpl\n",
    "import matplotlib.pyplot as plt\n",
    "import seaborn as sns\n",
    "\n",
    "sns.set_style(\"whitegrid\")\n",
    "sns.set_palette(\"colorblind\")\n",
    "palette = sns.color_palette()\n",
    "figsize = (10, 6)\n",
    "legend_fontsize = 13\n",
    "\n",
    "from matplotlib import rc\n",
    "rc('font', **{'family': 'sans-serif'})\n",
    "rc('figure', **{'dpi': 120})\n",
    "\n",
    "rng = np.random.default_rng(4)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "546c2102",
   "metadata": {},
   "source": [
    "# От нейросети к оптимизатору"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "69016728",
   "metadata": {},
   "source": [
    "## Данные и модели"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9c4c0267",
   "metadata": {},
   "outputs": [],
   "source": [
    "pages = np.array([1., 2., 3., 4., 5., 6., 7., 8., 8., 9., 10., 11., 12., 12.])\n",
    "color_pages = rng.integers(0, 3, len(pages))               # третий копицентр, этого в чеке нет\n",
    "paid = 20 + 30 * pages + 15 * color_pages\n",
    "_ = rng.normal(0, 4, 12)                                   # четвёртый копицентр был здесь\n",
    "\n",
    "\n",
    "def predict2(w, x):\n",
    "    return w[0] + w[1] * x\n",
    "\n",
    "\n",
    "def mean_sq2(w, x=pages, y=paid):\n",
    "    return np.mean((y - predict2(w, x)) ** 2)\n",
    "\n",
    "\n",
    "X = np.vstack([np.ones_like(pages), pages]).T\n",
    "w_star = np.linalg.solve(X.T @ X, X.T @ paid)\n",
    "x_mean, x_std = pages.mean(), pages.std()\n",
    "z_pages = (pages - x_mean) / x_std                          # отмасштабированный вход из раздела 3\n",
    "\n",
    "\n",
    "def mean_sq2_scaled(w, x=z_pages, y=paid):\n",
    "    return mean_sq2(w, x=x, y=y)\n",
    "\n",
    "\n",
    "Xz = np.vstack([np.ones_like(z_pages), z_pages]).T\n",
    "wz_star = np.linalg.solve(Xz.T @ Xz, Xz.T @ paid)\n",
    "\n",
    "\n",
    "def slope(f, c, eps=1e-4):\n",
    "    \"\"\"Численная производная: чуть-чуть вправо, чуть-чуть влево, поделить на разницу.\"\"\"\n",
    "    return (f(c + eps) - f(c - eps)) / (2 * eps)\n",
    "\n",
    "\n",
    "def gradient(f, w, eps=1e-4):\n",
    "    \"\"\"По одному ответу «чуть-чуть повернуть» на каждую ручку.\"\"\"\n",
    "    g = np.zeros_like(w)\n",
    "    for i in range(len(w)):\n",
    "        dw = np.zeros_like(w)\n",
    "        dw[i] = eps\n",
    "        g[i] = (f(w + dw) - f(w - dw)) / (2 * eps)\n",
    "    return g\n",
    "\n",
    "\n",
    "def descent(f, w0, eta, n_steps, grad=None):\n",
    "    \"\"\"Градиентный спуск; градиент — численный, если не дали готовый.\"\"\"\n",
    "    grad = grad or (lambda w: gradient(f, w))\n",
    "    ws = [np.array(w0, dtype=float)]\n",
    "    for _ in range(n_steps):\n",
    "        ws.append(ws[-1] - eta * grad(ws[-1]))\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "def tariff(x):\n",
    "    return 30 * np.minimum(x, 3) + 20 * np.clip(x - 3, 0, 7) + 10 * np.maximum(x - 10, 0)\n",
    "\n",
    "\n",
    "pages_e = np.arange(1, 21, dtype=float)\n",
    "paid_e = tariff(pages_e) + rng.normal(0, 5, len(pages_e))\n",
    "\n",
    "x_m, x_s, y_m, y_s = pages_e.mean(), pages_e.std(), paid_e.mean(), paid_e.std()\n",
    "z_e, u_e = (pages_e - x_m) / x_s, (paid_e - y_m) / y_s\n",
    "\n",
    "H = 14\n",
    "relu = lambda z: np.maximum(0, z)\n",
    "\n",
    "\n",
    "def net(p, z):\n",
    "    \"\"\"Один вход, H включаемых блоков, линейный выход.\"\"\"\n",
    "    W1, b1, W2, b2 = p[:H], p[H:2 * H], p[2 * H:3 * H], p[3 * H]\n",
    "    return relu(np.outer(z, W1) + b1) @ W2 + b2\n",
    "\n",
    "\n",
    "def predict_net(p, x):                       # в рублях и страницах\n",
    "    return net(p, (x - x_m) / x_s) * y_s + y_m\n",
    "\n",
    "\n",
    "def mean_sq_net(p, z=z_e, u=u_e):\n",
    "    return np.mean((u - net(p, z)) ** 2)\n",
    "\n",
    "xs = np.linspace(0.5, 20.5, 400)\n",
    "\n",
    "\n",
    "def make_net(H_try):\n",
    "    def net_H(p, z):\n",
    "        W1, b1, W2, b2 = p[:H_try], p[H_try:2 * H_try], p[2 * H_try:3 * H_try], p[3 * H_try]\n",
    "        return relu(np.outer(z, W1) + b1) @ W2 + b2\n",
    "    return lambda p: np.mean((u_e - net_H(p, z_e)) ** 2)\n",
    "\n",
    "\n",
    "# те же вызовы генератора, что в ноутбуке 07: старт сети, посылки, замеры времени\n",
    "_ = rng.normal(0, .5, 3 * H + 1)\n",
    "_ = rng.uniform(0, 10, 200), rng.uniform(10, 100, 200), rng.normal(0, 1, 3), rng.normal(0, 1, 25)\n",
    "for H_try in [8, 32, 128, 512]:\n",
    "    _ = rng.normal(0, 1, 3 * H_try + 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "48a7680a",
   "metadata": {},
   "source": [
    "## 1. Обратное распространение ошибки"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d411403d",
   "metadata": {},
   "outputs": [],
   "source": [
    "def forward(p, z):\n",
    "    \"\"\"Прямой проход: считаем предсказание и запоминаем всё, что было внутри.\"\"\"\n",
    "    W1, b1, W2, b2 = p[:H], p[H:2 * H], p[2 * H:3 * H], p[3 * H]\n",
    "    s = np.outer(z, W1) + b1            # вход каждого блока\n",
    "    h = relu(s)                         # выход каждого блока\n",
    "    out = h @ W2 + b2                   # предсказание\n",
    "    return s, h, out\n",
    "\n",
    "\n",
    "def backward(p, z=z_e, u=u_e):\n",
    "    \"\"\"Обратный проход: от ошибки назад, перемножая усиления, складывая на развилках.\"\"\"\n",
    "    W1, b1, W2, b2 = p[:H], p[H:2 * H], p[2 * H:3 * H], p[3 * H]\n",
    "    s, h, out = forward(p, z)\n",
    "    n = len(z)\n",
    "    d_out = 2 * (out - u) / n           # усиление звена «квадрат промаха, усреднить»\n",
    "    g_b2 = d_out.sum()                  # b2 прибавляется: усиление 1, складываем по чекам\n",
    "    g_W2 = h.T @ d_out                  # умножение на W2: усиление по W2 — то, что умножали (h)\n",
    "    d_h = np.outer(d_out, W2)           # ... а назад к h — усиление W2\n",
    "    d_s = d_h * (s > 0)                 # ReLU: пропускает 1, если блок был включён, иначе 0\n",
    "    g_b1 = d_s.sum(axis=0)              # b1 прибавляется: усиление 1\n",
    "    g_W1 = z @ d_s                      # умножение на W1: усиление по W1 — то, что умножали (z)\n",
    "    return np.concatenate([g_W1, g_b1, g_W2, [g_b2]])\n",
    "\n",
    "\n",
    "p_test = rng.normal(0, 1, 3 * H + 1)\n",
    "g_numeric = gradient(mean_sq_net, p_test)\n",
    "g_backprop = backward(p_test)\n",
    "print(\"Наибольшее расхождение между «чуть-чуть повернуть» и обратным проходом: %.1e\" % np.abs(g_numeric - g_backprop).max())\n",
    "print(\"\\nПервые пять производных:\")\n",
    "print(\"  чуть-чуть повернуть: \", np.round(g_numeric[:5], 4))\n",
    "print(\"  обратный проход:     \", np.round(g_backprop[:5], 4))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f4c6b3ff",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "       512       1537               0.1985 с        0.01091 с           18\n"
     ]
    }
   ],
   "source": [
    "def make_backprop(H_try):\n",
    "    def backward_H(p):\n",
    "        W1, b1, W2, b2 = p[:H_try], p[H_try:2 * H_try], p[2 * H_try:3 * H_try], p[3 * H_try]\n",
    "        s = np.outer(z_e, W1) + b1; h = relu(s); out = h @ W2 + b2\n",
    "        d_out = 2 * (out - u_e) / len(z_e)\n",
    "        d_s = np.outer(d_out, W2) * (s > 0)\n",
    "        return np.concatenate([z_e @ d_s, d_s.sum(axis=0), h.T @ d_out, [d_out.sum()]])\n",
    "    return backward_H\n",
    "\n",
    "\n",
    "print(\"  нейронов   настроек   «чуть-чуть повернуть»   обратный проход   во сколько раз быстрее\")\n",
    "for H_try in [8, 32, 128, 512]:\n",
    "    p = rng.normal(0, 1, 3 * H_try + 1)\n",
    "    f, bp = make_net(H_try), make_backprop(H_try)\n",
    "    t0 = time.perf_counter(); gradient(f, p); t_num = time.perf_counter() - t0\n",
    "    t0 = time.perf_counter()\n",
    "    for _ in range(20):\n",
    "        bp(p)\n",
    "    t_bp = (time.perf_counter() - t0) / 20\n",
    "    print(\"  %8d   %8d   %18.4f с   %12.5f с   %10.0f\" % (H_try, len(p), t_num, t_bp, t_num / t_bp))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00f81e32",
   "metadata": {},
   "source": [
    "## 2. Разные старты — разные ямы"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "87e0f626",
   "metadata": {},
   "outputs": [],
   "source": [
    "solutions = []\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "ax.scatter(pages_e, paid_e, marker=\"*\", s=120, color=\"black\", zorder=5, label=\"чеки\")\n",
    "ax.plot(xs, tariff(xs), linewidth=1.5, color=\"0.5\", linestyle=\"--\", label=\"настоящий тариф\")\n",
    "for i, seed in enumerate([1, 2, 3]):\n",
    "    p = np.random.default_rng(seed).normal(0, 1, 3 * H + 1)\n",
    "    for step in range(3000):\n",
    "        p = p - 0.05 * backward(p)\n",
    "    solutions.append(p)\n",
    "    ax.plot(xs, predict_net(p, xs), linewidth=2.5, color=palette[i],\n",
    "            label=\"старт №%d: ошибка %.1f ₽²\" % (seed, mean_sq_net(p) * y_s ** 2))\n",
    "ax.set_ylim(0, 350)\n",
    "ax.set_xlabel(\"число страниц\"); ax.set_ylabel(\"стоимость, ₽\")\n",
    "ax.set_title(\"Три спуска из трёх случайных точек приходят в три разные ямы\")\n",
    "ax.legend(fontsize=11, loc=\"upper left\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1b1974f4",
   "metadata": {},
   "outputs": [],
   "source": [
    "p1, p2, p3 = solutions\n",
    "e1 = (p2 - p1) / np.linalg.norm(p2 - p1)                      # первая ось плоскости: от ямы 1 к яме 2\n",
    "v = (p3 - p1) - ((p3 - p1) @ e1) * e1\n",
    "e2 = v / np.linalg.norm(v)                                    # вторая ось: к яме 3, поперёк первой\n",
    "pts = np.array([[0, 0], [(p2 - p1) @ e1, 0], [(p3 - p1) @ e1, (p3 - p1) @ e2]])\n",
    "\n",
    "a_grid = np.linspace(pts[:, 0].min() - 3, pts[:, 0].max() + 3, 90)\n",
    "b_grid = np.linspace(pts[:, 1].min() - 3, pts[:, 1].max() + 3, 90)\n",
    "Zp = np.array([[mean_sq_net(p1 + a * e1 + b * e2) * y_s ** 2 for a in a_grid] for b in b_grid])\n",
    "\n",
    "ts = np.linspace(0, 1, 60)\n",
    "paths = {\"из ямы 1 в яму 2\": [mean_sq_net((1 - t) * p1 + t * p2) * y_s ** 2 for t in ts],\n",
    "         \"из ямы 1 в яму 3\": [mean_sq_net((1 - t) * p1 + t * p3) * y_s ** 2 for t in ts],\n",
    "         \"из ямы 2 в яму 3\": [mean_sq_net((1 - t) * p2 + t * p3) * y_s ** 2 for t in ts]}\n",
    "\n",
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6.5))\n",
    "levels_p = np.logspace(np.log10(Zp.min()), np.log10(Zp.max()), 30)\n",
    "ax1.contourf(a_grid, b_grid, np.log10(Zp), levels=30, cmap=\"Blues_r\", alpha=0.6)\n",
    "ax1.contour(a_grid, b_grid, Zp, levels=levels_p, cmap=\"Blues_r\", linewidths=0.8)\n",
    "for i, (a, b) in enumerate(pts):\n",
    "    ax1.scatter([a], [b], marker=\"*\", s=350, color=palette[i], edgecolors=\"black\", zorder=5,\n",
    "                label=\"яма %d: ошибка %.1f ₽²\" % (i + 1, mean_sq_net(solutions[i]) * y_s ** 2))\n",
    "ax1.set_xlabel(\"вдоль плоскости, ось 1\"); ax1.set_ylabel(\"вдоль плоскости, ось 2\")\n",
    "ax1.set_title(\"Срез %d-мерного ландшафта плоскостью через три решения\" % len(p1))\n",
    "ax1.legend(fontsize=10, loc=\"upper right\")\n",
    "\n",
    "for i, (name, L) in enumerate(paths.items()):\n",
    "    ax2.plot(ts, L, linewidth=2.5, color=palette[i], label=name)\n",
    "ax2.set_yscale(\"log\")\n",
    "ax2.set_xlabel(\"доля пути по прямой\"); ax2.set_ylabel(\"ошибка, ₽² (лог. шкала)\")\n",
    "ax2.set_title(\"Ошибка вдоль прямой из одной ямы в другую: между ними перевал\")\n",
    "ax2.legend(fontsize=10)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a107281a",
   "metadata": {},
   "source": [
    "## 3. Градиентный спуск против стохастического"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b0290b9c",
   "metadata": {},
   "outputs": [],
   "source": [
    "def grad2_exact(w, x=z_pages, y=paid):\n",
    "    \"\"\"Стрелка для правила b + c·z по формуле (обратный проход для двух ручек).\"\"\"\n",
    "    r = predict2(w, x) - y\n",
    "    return np.array([2 * r.mean(), 2 * (r * x).mean()])\n",
    "\n",
    "\n",
    "w0 = np.array([100., 0.])\n",
    "arrows = np.array([grad2_exact(w0, z_pages[i:i + 1], paid[i:i + 1]) for i in range(len(pages))])\n",
    "print(\"Стрелка по всем чекам: %s\" % np.round(grad2_exact(w0), 1))\n",
    "print(\"Среднее четырнадцати стрелок по одному чеку: %s\" % np.round(arrows.mean(axis=0), 1))\n",
    "\n",
    "Bz, Cz = np.meshgrid(np.linspace(-50, 450, 200), np.linspace(-150, 350, 200))\n",
    "Zz = np.array([[mean_sq2_scaled((b, c)) for b in Bz[0]] for c in Cz[:, 0]])\n",
    "levels_z = np.logspace(np.log10(Zz.min() + 1), np.log10(Zz.max()), 25)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 7))\n",
    "ax.contour(Bz, Cz, Zz, levels=levels_z, cmap=\"Blues_r\", linewidths=1)\n",
    "scale = 0.25\n",
    "for i, g in enumerate(arrows):\n",
    "    ax.annotate(\"\", xy=(w0[0] - scale * g[0], w0[1] - scale * g[1]), xytext=(w0[0], w0[1]),\n",
    "                arrowprops=dict(arrowstyle=\"->\", color=\"red\", lw=1, alpha=0.7))\n",
    "g_all = grad2_exact(w0)\n",
    "ax.annotate(\"\", xy=(w0[0] - scale * g_all[0], w0[1] - scale * g_all[1]), xytext=(w0[0], w0[1]),\n",
    "            arrowprops=dict(arrowstyle=\"->\", color=\"black\", lw=3))\n",
    "ax.scatter([w0[0]], [w0[1]], s=120, color=\"black\", zorder=5)\n",
    "ax.scatter([wz_star[0]], [wz_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5, label=\"дно\")\n",
    "ax.plot([], [], color=\"red\", label=\"стрелка «вниз» по одному чеку (14 штук)\")\n",
    "ax.plot([], [], color=\"black\", linewidth=3, label=\"стрелка по всем чекам = их среднее\")\n",
    "ax.set_xlabel(\"настройка при единице\"); ax.set_ylabel(\"настройка при z\")\n",
    "ax.set_title(\"У каждого чека своё мнение, куда идти; общая стрелка — среднее\")\n",
    "ax.legend(fontsize=11, loc=\"lower right\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2c379343",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sgd(w0, eta, n_steps, batch_size, grad=grad2_exact, n_data=len(pages), seed=0):\n",
    "    r = np.random.default_rng(seed)\n",
    "    ws = [np.array(w0, dtype=float)]\n",
    "    for _ in range(n_steps):\n",
    "        idx = r.choice(n_data, batch_size, replace=False)                # случайная горстка чеков\n",
    "        ws.append(ws[-1] - eta * grad(ws[-1], z_pages[idx], paid[idx]))\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "batch_sizes = [14, 4, 2, 1]\n",
    "traces = {B: sgd([0., 0.], eta=0.3, n_steps=60, batch_size=B) for B in batch_sizes}\n",
    "\n",
    "fig, axes = plt.subplots(1, 4, figsize=(18, 7.8), sharey=True)\n",
    "for ax, B in zip(axes, batch_sizes):\n",
    "    tr = traces[B]\n",
    "    ax.contour(Bz, Cz, Zz, levels=levels_z, cmap=\"Blues_r\", linewidths=0.8)\n",
    "    ax.plot(tr[:, 0], tr[:, 1], \"o-\", color=palette[3], linewidth=1.2, markersize=3)\n",
    "    ax.scatter([wz_star[0]], [wz_star[1]], marker=\"*\", s=250, color=palette[3], zorder=5)\n",
    "    ax.set_xlim(150, 350); ax.set_ylim(0, 200)\n",
    "    ax.set_title(\"%s за шаг\\nошибка на всех чеках после 60 шагов: %.1f\" % (\"все 14 чеков\" if B == 14 else \"%d случайных чека\" % B if B > 1 else \"1 случайный чек\",\n",
    "                                                                            mean_sq2_scaled(tr[-1])), fontsize=11)\n",
    "    ax.set_xlabel(\"настройка при единице\")\n",
    "axes[0].set_ylabel(\"настройка при z\")\n",
    "plt.suptitle(\"60 шагов с одним и тем же η = 0,3: чем меньше горстка, тем дешевле шаг и тем сильнее дрожит дорога\", y=1.03)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "930e6587",
   "metadata": {},
   "outputs": [],
   "source": [
    "def sgd_schedule(w0, eta_of_step, n_steps, batch_size, seed=0):\n",
    "    r = np.random.default_rng(seed)\n",
    "    ws = [np.array(w0, dtype=float)]\n",
    "    for k in range(n_steps):\n",
    "        idx = r.choice(len(pages), batch_size, replace=False)\n",
    "        ws.append(ws[-1] - eta_of_step(k) * grad2_exact(ws[-1], z_pages[idx], paid[idx]))\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "n_steps = 300\n",
    "tr_const = sgd_schedule([0., 0.], lambda k: 0.3, n_steps, batch_size=2)\n",
    "tr_decay = sgd_schedule([0., 0.], lambda k: 0.3 / (1 + k / 10), n_steps, batch_size=2)\n",
    "floor = mean_sq2(w_star)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "ax.plot([mean_sq2_scaled(w) - floor + 1e-2 for w in tr_const], linewidth=2, color=palette[1], label=r\"постоянный шаг $\\eta = 0.3$\")\n",
    "ax.plot([mean_sq2_scaled(w) - floor + 1e-2 for w in tr_decay], linewidth=2, color=palette[2], label=r\"убывающий шаг $\\eta_k = 0.3 / (1 + k/10)$\")\n",
    "ax.set_yscale(\"log\")\n",
    "ax.set_xlabel(\"номер шага\"); ax.set_ylabel(\"ошибка минус её минимум (лог. шкала)\")\n",
    "ax.set_title(\"SGD по 2 чекам: с постоянным шагом дрожит у дна, с убывающим — садится\")\n",
    "ax.legend(fontsize=11)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a330bd22",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_net(p0, eta, n_looks, batch_size, seed=0):\n",
    "    \"\"\"Обучение сети обратным проходом; batch_size = сколько чеков смотрим за шаг.\"\"\"\n",
    "    r = np.random.default_rng(seed)\n",
    "    p, looks, history = p0.copy(), 0, [(0, mean_sq_net(p0) * y_s ** 2)]\n",
    "    while looks < n_looks:\n",
    "        idx = r.choice(len(z_e), batch_size, replace=False) if batch_size < len(z_e) else np.arange(len(z_e))\n",
    "        p = p - eta * backward(p, z_e[idx], u_e[idx])\n",
    "        looks += batch_size\n",
    "        history.append((looks, mean_sq_net(p) * y_s ** 2))\n",
    "    return p, np.array(history)\n",
    "\n",
    "\n",
    "def smooth(hist, window_looks=200):\n",
    "    \"\"\"Скользящее среднее ошибки по окну в window_looks просмотров — чтобы шум не прятал кривые.\"\"\"\n",
    "    looks, err = hist[:, 0], hist[:, 1]\n",
    "    out = np.array([err[(looks > l - window_looks) & (looks <= l)].mean() for l in looks])\n",
    "    return looks, out\n",
    "\n",
    "\n",
    "p0 = np.random.default_rng(7).normal(0, 0.5, 3 * H + 1)\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "for i, (B, eta) in reversed(list(enumerate([(20, 0.1), (4, 0.1), (1, 0.1)]))):\n",
    "    p_B, hist = train_net(p0, eta, n_looks=20 * 600, batch_size=B)\n",
    "    looks, err_s = smooth(hist)\n",
    "    ax.plot(hist[:, 0], hist[:, 1], linewidth=0.8, color=palette[i], alpha=0.25)\n",
    "    ax.plot(looks, err_s, linewidth=2.5, color=palette[i],\n",
    "            label=\"%s за шаг: ошибка в конце %.1f ₽²\" % (\"все 20 чеков\" if B == 20 else \"%d чек%s\" % (B, \"а\" if B == 4 else \"\"), err_s[-1]))\n",
    "ax.axhline(25, color=\"0.5\", linestyle=\"--\", linewidth=1.5, label=\"уровень шума в чеках\")\n",
    "ax.set_yscale(\"log\"); ax.set_ylim(10, 2e4)\n",
    "ax.set_xlabel(\"сколько раз сеть посмотрела на чек\"); ax.set_ylabel(\"ошибка на всех чеках, ₽² (лог. шкала)\")\n",
    "ax.set_title(\"Одинаковый бюджет просмотров: горстки учатся быстрее, хотя каждый их шаг хуже\\n(тонкие линии — ошибка после каждого шага, толстые — её скользящее среднее)\")\n",
    "ax.legend(fontsize=11)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c203e8c2",
   "metadata": {},
   "source": [
    "## 4. Метод моментов: шарик с инерцией"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7772e366",
   "metadata": {},
   "outputs": [],
   "source": [
    "def grad2_raw(w):\n",
    "    return grad2_exact(w, pages, paid)\n",
    "\n",
    "\n",
    "B, C = np.meshgrid(np.linspace(-20, 70, 200), np.linspace(20, 45, 200))\n",
    "Z = np.array([[mean_sq2((b, c)) for b in B[0]] for c in C[:, 0]])\n",
    "levels = np.logspace(np.log10(Z.min() + 1), np.log10(Z.max()), 25)\n",
    "\n",
    "tr_gd = descent(mean_sq2, [0., 40.], eta=0.01, n_steps=400, grad=grad2_raw)\n",
    "steps_b = np.diff(tr_gd[:, 0]); steps_c = np.diff(tr_gd[:, 1])\n",
    "print(\"Первые 8 шагов по c (поперёк долины): %s\" % np.round(steps_c[:8], 2))\n",
    "print(\"Первые 8 шагов по b (вдоль долины):   %s\" % np.round(steps_b[:8], 3))\n",
    "print(\"Поперёк долины шаги меняют знак и быстро затухают; вдоль — всегда одного знака, но по 0,2 ₽ при %.0f ₽ до дна.\" % (w_star[0] - tr_gd[8, 0]))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12491be0",
   "metadata": {},
   "outputs": [],
   "source": [
    "beta, eta, g = 0.9, 1.0, 1.0            # постоянная стрелка величины 1\n",
    "v, speeds = 0., []\n",
    "for k in range(60):\n",
    "    v = beta * v - eta * g\n",
    "    speeds.append(-v)\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 5))\n",
    "ax.plot(speeds, \"o-\", color=palette[1], linewidth=2, markersize=4, label=r\"скорость шарика при $\\beta = 0.9$\")\n",
    "ax.axhline(eta * g, color=palette[0], linestyle=\"--\", linewidth=2, label=\"шаг обычного спуска\")\n",
    "ax.axhline(eta * g / (1 - beta), color=palette[3], linestyle=\"--\", linewidth=2, label=r\"предел: $\\eta \\cdot$стрелка$\\,/\\,(1-\\beta)$ = 10 шагов\")\n",
    "ax.set_xlabel(\"номер шага\"); ax.set_ylabel(\"длина шага\")\n",
    "ax.set_title(\"Постоянный наклон: толчки складываются, и шарик разгоняется до 1/(1 − β) обычных шагов\")\n",
    "ax.legend(fontsize=11, loc=\"lower right\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "00690107",
   "metadata": {},
   "outputs": [],
   "source": [
    "def momentum(grad, w0, eta, beta, n_steps):\n",
    "    w, v = np.array(w0, dtype=float), np.zeros(len(w0))\n",
    "    ws = [w.copy()]\n",
    "    for _ in range(n_steps):\n",
    "        v = beta * v - eta * grad(w)\n",
    "        w = w + v\n",
    "        ws.append(w.copy())\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "def steps_until(tr, f, tol=1.0):\n",
    "    \"\"\"Через сколько шагов ошибка впервые оказалась в пределах tol от дна.\"\"\"\n",
    "    for k, w in enumerate(tr):\n",
    "        if f(w) - floor < tol:\n",
    "            return k\n",
    "    return None\n",
    "\n",
    "\n",
    "n = 400\n",
    "tr_mom = momentum(grad2_raw, [0., 40.], eta=0.01, beta=0.9, n_steps=n)\n",
    "tr_gd_long = descent(mean_sq2, [0., 40.], eta=0.01, n_steps=3000, grad=grad2_raw)\n",
    "tr_mom_long = momentum(grad2_raw, [0., 40.], eta=0.01, beta=0.9, n_steps=3000)\n",
    "print(\"Шагов до дна (ошибка в пределах 1 ₽² от минимума):\")\n",
    "print(\"  обычный спуск, η = 0.01:            %s\" % steps_until(tr_gd_long, mean_sq2))\n",
    "print(\"  метод моментов, η = 0.01, β = 0.9:  %s\" % steps_until(tr_mom_long, mean_sq2))\n",
    "\n",
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))\n",
    "ax1.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "ax1.plot(tr_gd[:, 0], tr_gd[:, 1], \"-\", color=palette[1], linewidth=1.5, label=\"обычный спуск, %d шагов\" % n)\n",
    "ax1.plot(tr_mom[:, 0], tr_mom[:, 1], \"-\", color=palette[2], linewidth=1.5, label=r\"метод моментов, $\\beta = 0.9$, %d шагов\" % n)\n",
    "ax1.scatter([0], [40], s=100, color=palette[0], zorder=5, label=\"старт\")\n",
    "ax1.scatter([w_star[0]], [w_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5, label=\"дно\")\n",
    "ax1.set_xlabel(\"b — плата за заказ, ₽\"); ax1.set_ylabel(\"c — цена страницы, ₽\")\n",
    "ax1.set_title(\"Та же долина, тот же η = 0.01\")\n",
    "ax1.legend(fontsize=10, loc=\"upper right\")\n",
    "\n",
    "ax2.plot([mean_sq2(w) - floor + 1e-2 for w in tr_gd], linewidth=2, color=palette[1], label=\"обычный спуск\")\n",
    "ax2.plot([mean_sq2(w) - floor + 1e-2 for w in tr_mom], linewidth=2, color=palette[2], label=r\"метод моментов, $\\beta = 0.9$\")\n",
    "ax2.set_yscale(\"log\")\n",
    "ax2.set_xlabel(\"номер шага\"); ax2.set_ylabel(\"ошибка минус её минимум (лог. шкала)\")\n",
    "ax2.set_title(\"Шарик с инерцией доходит на порядок быстрее\")\n",
    "ax2.legend(fontsize=11)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "57ffd651",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(16, 6))\n",
    "ax1.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "for i, beta in enumerate([0.5, 0.9, 0.99]):\n",
    "    tr = momentum(grad2_raw, [0., 40.], eta=0.01, beta=beta, n_steps=n)\n",
    "    ax1.plot(tr[:, 0], tr[:, 1], \"-\", color=palette[i], linewidth=1.5, label=r\"$\\beta = %g$\" % beta)\n",
    "    ax2.plot([mean_sq2(w) - floor + 1e-2 for w in tr], linewidth=2, color=palette[i], label=r\"$\\beta = %g$\" % beta)\n",
    "ax1.scatter([w_star[0]], [w_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5)\n",
    "ax1.set_xlim(-20, 110); ax1.set_ylim(20, 45)\n",
    "ax1.set_xlabel(\"b — плата за заказ, ₽\"); ax1.set_ylabel(\"c — цена страницы, ₽\")\n",
    "ax1.set_title(\"Метод моментов при разной инерции, %d шагов\" % n)\n",
    "ax1.legend(fontsize=11, loc=\"upper right\")\n",
    "ax2.set_yscale(\"log\")\n",
    "ax2.set_xlabel(\"номер шага\"); ax2.set_ylabel(\"ошибка минус её минимум (лог. шкала)\")\n",
    "ax2.set_title(r\"$\\beta = 0.99$: шарик пролетает дно и долго качается\")\n",
    "ax2.legend(fontsize=11)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fbb8b0da",
   "metadata": {},
   "source": [
    "## 5. Ускорение Нестерова: сначала посмотреть, потом шагнуть"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "74c920d6",
   "metadata": {},
   "outputs": [],
   "source": [
    "def nesterov(grad, w0, eta, beta, n_steps):\n",
    "    w, v = np.array(w0, dtype=float), np.zeros(len(w0))\n",
    "    ws = [w.copy()]\n",
    "    for _ in range(n_steps):\n",
    "        v = beta * v - eta * grad(w + beta * v)          # стрелка в точке, куда шарик вот-вот прилетит\n",
    "        w = w + v\n",
    "        ws.append(w.copy())\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "fig, axes = plt.subplots(1, 2, figsize=(16, 6))\n",
    "for ax, beta in zip(axes, [0.9, 0.99]):\n",
    "    tr_m = momentum(grad2_raw, [0., 40.], eta=0.01, beta=beta, n_steps=n)\n",
    "    tr_n = nesterov(grad2_raw, [0., 40.], eta=0.01, beta=beta, n_steps=n)\n",
    "    ax.plot([mean_sq2(w) - floor + 1e-2 for w in tr_gd], linewidth=2, color=palette[1], label=\"обычный спуск\")\n",
    "    ax.plot([mean_sq2(w) - floor + 1e-2 for w in tr_m], linewidth=2, color=palette[2], label=\"метод моментов\")\n",
    "    ax.plot([mean_sq2(w) - floor + 1e-2 for w in tr_n], linewidth=2, color=palette[4], label=\"Нестеров\")\n",
    "    ax.set_yscale(\"log\")\n",
    "    ax.set_xlabel(\"номер шага\"); ax.set_ylabel(\"ошибка минус её минимум (лог. шкала)\")\n",
    "    ax.set_title(r\"$\\eta = 0.01$, $\\beta = %g$\" % beta)\n",
    "    ax.legend(fontsize=11)\n",
    "plt.suptitle(\"Нестеров гасит перелёты: при большой инерции разница особенно заметна\", y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.show()\n",
    "\n",
    "tr_m_long = momentum(grad2_raw, [0., 40.], eta=0.01, beta=0.99, n_steps=3000)\n",
    "tr_n_long = nesterov(grad2_raw, [0., 40.], eta=0.01, beta=0.99, n_steps=3000)\n",
    "print(\"Шагов до дна при β = 0.99:  метод моментов %s,  Нестеров %s\" % (steps_until(tr_m_long, mean_sq2), steps_until(tr_n_long, mean_sq2)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64a3495e",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(11, 6.5))\n",
    "ax.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "tr_m = momentum(grad2_raw, [0., 40.], eta=0.01, beta=0.99, n_steps=n)\n",
    "tr_n = nesterov(grad2_raw, [0., 40.], eta=0.01, beta=0.99, n_steps=n)\n",
    "ax.plot(tr_m[:, 0], tr_m[:, 1], \"-\", color=palette[2], linewidth=1.5, label=r\"метод моментов, $\\beta = 0.99$\")\n",
    "ax.plot(tr_n[:, 0], tr_n[:, 1], \"-\", color=palette[4], linewidth=1.5, label=r\"Нестеров, $\\beta = 0.99$\")\n",
    "ax.scatter([0], [40], s=100, color=palette[0], zorder=5, label=\"старт\")\n",
    "ax.scatter([w_star[0]], [w_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5, label=\"дно\")\n",
    "ax.set_xlim(-20, 130); ax.set_ylim(15, 45)\n",
    "ax.set_xlabel(\"b — плата за заказ, ₽\"); ax.set_ylabel(\"c — цена страницы, ₽\")\n",
    "ax.set_title(\"Та же инерция, %d шагов: шарик Нестерова смотрит вперёд и тормозит раньше\" % n)\n",
    "ax.legend(fontsize=11, loc=\"upper right\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "33f21e15",
   "metadata": {},
   "outputs": [],
   "source": [
    "def train_net_optimizer(p0, method, eta, beta, n_looks, batch_size, seed=0):\n",
    "    r = np.random.default_rng(seed)\n",
    "    p, v, looks = p0.copy(), np.zeros_like(p0), 0\n",
    "    history = [(0, mean_sq_net(p0) * y_s ** 2)]\n",
    "    while looks < n_looks:\n",
    "        idx = r.choice(len(z_e), batch_size, replace=False)\n",
    "        if method == \"sgd\":\n",
    "            p = p - eta * backward(p, z_e[idx], u_e[idx])\n",
    "        elif method == \"momentum\":\n",
    "            v = beta * v - eta * backward(p, z_e[idx], u_e[idx]); p = p + v\n",
    "        elif method == \"nesterov\":\n",
    "            v = beta * v - eta * backward(p + beta * v, z_e[idx], u_e[idx]); p = p + v\n",
    "        looks += batch_size\n",
    "        history.append((looks, mean_sq_net(p) * y_s ** 2))\n",
    "    return p, np.array(history)\n",
    "\n",
    "\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "for i, (method, eta, beta, label) in enumerate([(\"sgd\", 0.1, 0.0, \"SGD\"),\n",
    "                                                 (\"momentum\", 0.02, 0.9, r\"SGD + момент, $\\beta = 0.9$\"),\n",
    "                                                 (\"nesterov\", 0.02, 0.9, r\"SGD + Нестеров, $\\beta = 0.9$\")]):\n",
    "    _, hist = train_net_optimizer(p0, method, eta, beta, n_looks=20 * 600, batch_size=4)\n",
    "    ax.plot(hist[:, 0], hist[:, 1], linewidth=1.5, color=palette[i], label=\"%s: ошибка в конце %.1f ₽²\" % (label, hist[-1, 1]))\n",
    "ax.axhline(25, color=\"0.5\", linestyle=\"--\", linewidth=1.5, label=\"уровень шума в чеках\")\n",
    "ax.set_yscale(\"log\"); ax.set_ylim(10, 2e4)\n",
    "ax.set_xlabel(\"сколько раз сеть посмотрела на чек\"); ax.set_ylabel(\"ошибка на всех чеках, ₽² (лог. шкала)\")\n",
    "ax.set_title(\"Сеть из %d блоков, горстки по 4 чека: инерция работает и со стохастическими стрелками\" % H)\n",
    "ax.legend(fontsize=11)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "a2d4f81c-bf12-4353-9a74-578421ebf084",
   "metadata": {},
   "outputs": [],
   "source": []
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3 (ipykernel)",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.13.5"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
