{
 "cells": [
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "306f3472",
   "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": "4c677c9e",
   "metadata": {},
   "source": [
    "# Как обучаются нейросети"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a10c55fb",
   "metadata": {},
   "source": [
    "## 1. Где мы остановились: производная без матана"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "56c5ecc3",
   "metadata": {},
   "outputs": [],
   "source": [
    "def slope(f, c, eps=1e-4):\n",
    "    \"\"\"Численная производная: чуть-чуть вправо, чуть-чуть влево, поделить на разницу.\"\"\"\n",
    "    return (f(c + eps) - f(c - eps)) / (2 * eps)\n",
    "\n",
    "\n",
    "E = lambda c: (c - 30) ** 2\n",
    "\n",
    "print(\"   c    по формуле 2(c−30)    «чуть-чуть повернуть»\")\n",
    "for c in [10, 20, 25, 30, 35, 50]:\n",
    "    print(\"  %2d    %18.3f    %20.3f\" % (c, 2 * (c - 30), slope(E, c)))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9beeaacf",
   "metadata": {},
   "outputs": [],
   "source": [
    "def gradient_descent(f, c0, eta, n_steps):\n",
    "    cs = [c0]\n",
    "    for _ in range(n_steps):\n",
    "        cs.append(cs[-1] - eta * slope(f, cs[-1]))\n",
    "    return np.array(cs)\n",
    "\n",
    "\n",
    "trace = gradient_descent(E, 20., eta=0.1, n_steps=10)\n",
    "cs = np.linspace(10, 50, 200)\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "ax.plot(cs, E(cs), linewidth=2.5, color=\"0.4\", label=\"E(c) = (c − 30)²\")\n",
    "ax.plot(trace, E(trace), \"o-\", color=palette[1], linewidth=2, markersize=8,\n",
    "        label=\"шаги c ← c − 0,1 · (чуть-чуть повернуть)\")\n",
    "ax.set_xlabel(\"предполагаемая цена страницы c\"); ax.set_ylabel(\"квадрат промаха\")\n",
    "ax.set_title(\"Тот же спуск, что в прошлый раз, — но производная посчитана без формулы\")\n",
    "ax.legend(fontsize=11)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "014a2cc1",
   "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",
    "\n",
    "\n",
    "def predict2(w, x):\n",
    "    \"\"\"Правило с двумя настройками: плата за заказ b = w[0] и цена страницы c = w[1].\"\"\"\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",
    "print(\"Дно ландшафта по формуле: плата за заказ %.2f ₽, страница %.2f ₽, ошибка %.1f\"\n",
    "      % (w_star[0], w_star[1], mean_sq2(w_star)))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "67aab900",
   "metadata": {},
   "source": [
    "## 2. Обещание первое: почему квадрат, а не модуль"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fcc3af72",
   "metadata": {},
   "outputs": [],
   "source": [
    "def mean_abs_c(c):\n",
    "    return np.mean(np.abs(paid - c * pages))\n",
    "\n",
    "\n",
    "def mean_sq_c(c):\n",
    "    return np.mean((paid - c * pages) ** 2)\n",
    "\n",
    "\n",
    "tr_abs = gradient_descent(mean_abs_c, 10., eta=1.0, n_steps=25)\n",
    "tr_sq = gradient_descent(mean_sq_c, 10., eta=0.005, n_steps=25)\n",
    "print(\"Спуск по среднему промаху, последние шаги: %s\" % np.round(tr_abs[-6:], 1))\n",
    "print(\"Спуск по квадрату промаха, последние шаги: %s\" % np.round(tr_sq[-6:], 2))\n",
    "\n",
    "cs = np.linspace(5, 50, 300)\n",
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5.5))\n",
    "for ax, f, tr, title in [(ax1, mean_abs_c, tr_abs, \"средний промах: наклон постоянный, спуск колеблется\"),\n",
    "                         (ax2, mean_sq_c, tr_sq, \"средний квадрат промаха: наклон тает у дна, спуск садится\")]:\n",
    "    ax.plot(cs, [f(c) for c in cs], linewidth=2.5, color=\"0.4\")\n",
    "    ax.plot(tr, [f(c) for c in tr], \"o-\", color=palette[1], linewidth=1.5, markersize=5, label=\"25 шагов из c = 10\")\n",
    "    ax.set_xlabel(\"цена страницы c\"); ax.set_title(title, fontsize=12)\n",
    "    ax.legend(fontsize=11)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "374c0ab1",
   "metadata": {},
   "outputs": [],
   "source": [
    "misses_A = np.array([2., 2., 2., 2.])\n",
    "misses_B = np.array([0., 0., 0., 6.])\n",
    "print(\"              модель А      модель Б\")\n",
    "print(\"средний промах   %5.2f         %5.2f   → лучше Б\" % (misses_A.mean(), misses_B.mean()))\n",
    "print(\"средний квадрат  %5.2f         %5.2f   → лучше А\" % ((misses_A ** 2).mean(), (misses_B ** 2).mean()))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ef11c9df",
   "metadata": {},
   "source": [
    "## 3. Обещание второе: как далеко шагать"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "40227ba0",
   "metadata": {},
   "outputs": [],
   "source": [
    "etas = [0.001, 0.008, 0.0155, 0.0175]\n",
    "titles = [\"слишком маленький\", \"в самый раз\", \"великоват\",\n",
    "          \"слишком большой\"]\n",
    "grid = np.linspace(10, 60, 400)\n",
    "\n",
    "fig, axes = plt.subplots(1, 4, figsize=(14, 6), sharey=True)\n",
    "for ax, eta, title in zip(axes, etas, titles):\n",
    "    tr = gradient_descent(mean_sq_c, 25., eta=eta, n_steps=12)\n",
    "    ax.plot(grid, [mean_sq_c(c) for c in grid], linewidth=2, color=\"0.4\")\n",
    "    ax.plot(tr, [mean_sq_c(c) for c in tr], \"o-\", color=palette[1], linewidth=1.5, markersize=5, clip_on=True)\n",
    "    ax.scatter([tr[0]], [mean_sq_c(tr[0])], s=80, color=palette[0], zorder=5)\n",
    "    if tr.max() > 65 or tr.min() < 5:\n",
    "        title += \"\\nпосле 12 шагов c = %.0f, ошибка ×%.0f\" % (tr[-1], mean_sq_c(tr[-1]) / mean_sq_c(tr[0]))\n",
    "    ax.set_title(r\"$\\eta = %g$: %s\" % (eta, title), fontsize=11)\n",
    "    ax.set_xlabel(\"c\")\n",
    "    ax.set_xlim(10, 60); ax.set_ylim(0, 12000)\n",
    "axes[0].set_ylabel(\"средний квадрат промаха\")\n",
    "plt.suptitle(\"12 шагов из одной и той же точки с разным размером шага (оси одинаковые)\", y=1.02)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "49a2ba92",
   "metadata": {},
   "source": [
    "## 4. Две настройки: градиент без формулы Тейлора"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4a44a38f",
   "metadata": {},
   "outputs": [],
   "source": [
    "w0 = np.array([0., 40.])                                        # стартовая точка на карте\n",
    "\n",
    "slope_b = slope(lambda b: mean_sq2([b, w0[1]]), w0[0])          # крутим b, c заморожен\n",
    "slope_c = slope(lambda c: mean_sq2([w0[0], c]), w0[1])          # крутим c, b заморожен\n",
    "print(\"В точке (b = %g, c = %g):\" % tuple(w0))\n",
    "print(\"  чуть-чуть повернуть b  →  ошибка меняется со скоростью %8.1f\" % slope_b)\n",
    "print(\"  чуть-чуть повернуть c  →  ошибка меняется со скоростью %8.1f\" % slope_c)\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",
    "g0 = gradient(mean_sq2, w0)\n",
    "print(\"\\ngradient(mean_sq2, w0) = %s  — те же два числа, собранные вместе\" % np.round(g0, 1))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5576f77f",
   "metadata": {},
   "outputs": [],
   "source": [
    "angles = np.linspace(0, 2 * np.pi, 361)\n",
    "step = 1e-3\n",
    "change = np.array([(mean_sq2(w0 + step * np.array([np.cos(a), np.sin(a)])) - mean_sq2(w0)) / step\n",
    "                   for a in angles])\n",
    "best, worst = angles[np.argmax(change)], angles[np.argmin(change)]\n",
    "grad_angle = np.arctan2(g0[1], g0[0])\n",
    "print(\"Самый крутой подъём — направление %.0f°, самый крутой спуск — %.0f°\" % (np.degrees(best), np.degrees(worst)))\n",
    "print(\"Направление стрелки из двух производных: %.1f°\" % np.degrees(grad_angle))\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",
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n",
    "ax1.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "ch_max = np.abs(change).max()\n",
    "for a in np.linspace(0, 2 * np.pi, 24, endpoint=False):\n",
    "    ch = (mean_sq2(w0 + step * np.array([np.cos(a), np.sin(a)])) - mean_sq2(w0)) / step\n",
    "    length = abs(ch) / ch_max                                    # длина стрелки — размер изменения\n",
    "    ax1.annotate(\"\", xy=(w0[0] + 12 * length * np.cos(a), w0[1] + 3 * length * np.sin(a)), xytext=(w0[0], w0[1]),\n",
    "                 arrowprops=dict(arrowstyle=\"->\", color=palette[3] if ch > 0 else palette[0], lw=1.5))\n",
    "gn = g0 / np.linalg.norm(g0)\n",
    "ax1.annotate(\"\", xy=(w0[0] + 14 * gn[0], w0[1] + 3.5 * gn[1]), xytext=(w0[0], w0[1]),\n",
    "             arrowprops=dict(arrowstyle=\"->\", color=\"black\", lw=3))\n",
    "ax1.scatter([w0[0]], [w0[1]], s=120, color=\"black\", zorder=5)\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(\"24 пробных направления: длина — насколько изменилась ошибка,\\nкрасные — выросла, синие — упала; чёрная стрелка — из двух производных\", fontsize=11)\n",
    "ax1.legend(fontsize=11, loc=\"lower right\")\n",
    "\n",
    "ax2.plot(np.degrees(angles), change, linewidth=2.5, color=palette[0])\n",
    "ax2.axvline(np.degrees(grad_angle) % 360, color=\"black\", linestyle=\"--\", linewidth=2, label=\"направление стрелки из двух производных\")\n",
    "ax2.axvline((np.degrees(grad_angle) + 180) % 360, color=palette[3], linestyle=\"--\", linewidth=2, label=\"противоположное ей\")\n",
    "ax2.axhline(0, color=\"0.6\", linewidth=1)\n",
    "ax2.set_xlabel(\"направление шага, градусы\"); ax2.set_ylabel(\"изменение ошибки на единицу шага\")\n",
    "ax2.set_title(\"Изменение ошибки как функция от направления\", fontsize=12)\n",
    "ax2.legend(fontsize=10, loc=\"lower left\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "46442a52",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(10, 6.5))\n",
    "ax.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "bb, cc = np.meshgrid(np.linspace(-15, 65, 9), np.linspace(22, 44, 7))\n",
    "U, V = np.zeros_like(bb), np.zeros_like(cc)\n",
    "for i in range(bb.shape[0]):\n",
    "    for j in range(bb.shape[1]):\n",
    "        g = -gradient(mean_sq2, np.array([bb[i, j], cc[i, j]]))\n",
    "        g = g / np.linalg.norm(g)\n",
    "        U[i, j], V[i, j] = g[0] * 4, g[1]                       # оси в разном масштабе\n",
    "ax.quiver(bb, cc, U, V, color=palette[1], angles=\"xy\", scale_units=\"xy\", scale=1, width=0.004)\n",
    "ax.scatter([w_star[0]], [w_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5)\n",
    "ax.set_xlabel(\"b — плата за заказ, ₽\"); ax.set_ylabel(\"c — цена страницы, ₽\")\n",
    "ax.set_title(\"В каждой точке карты — своя стрелка «вниз»; все они поперёк линий уровня\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "002a5f6b",
   "metadata": {},
   "source": [
    "## 5. Вопрос из прошлой лекции: по очереди или вместе?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "dcffb4dd",
   "metadata": {},
   "outputs": [],
   "source": [
    "def descent_together(f, w0, eta, n_steps):\n",
    "    ws = [np.array(w0, dtype=float)]\n",
    "    for _ in range(n_steps):\n",
    "        ws.append(ws[-1] - eta * gradient(f, ws[-1]))\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "def best_value_of_one_knob(f, w, i, grid_width=200., n_grid=4001):\n",
    "    \"\"\"Лучшее значение ручки i при замороженных остальных: одномерная задача, решаем перебором.\"\"\"\n",
    "    candidates = np.linspace(w[i] - grid_width, w[i] + grid_width, n_grid)\n",
    "    errors = [f(np.r_[w[:i], v, w[i + 1:]]) for v in candidates]\n",
    "    w_new = w.copy()\n",
    "    w_new[i] = candidates[int(np.argmin(errors))]\n",
    "    return w_new\n",
    "\n",
    "\n",
    "def descent_one_at_a_time(f, w0, n_rounds):\n",
    "    ws = [np.array(w0, dtype=float)]\n",
    "    for k in range(n_rounds):\n",
    "        ws.append(best_value_of_one_knob(f, ws[-1], 0))          # лучшее b при этом c\n",
    "        ws.append(best_value_of_one_knob(f, ws[-1], 1))          # лучшее c при этом b\n",
    "    return np.array(ws)\n",
    "\n",
    "\n",
    "n_rounds = 20\n",
    "tr_t = descent_together(mean_sq2, [0., 40.], eta=0.0155, n_steps=400)\n",
    "tr_o = descent_one_at_a_time(mean_sq2, [0., 40.], n_rounds)\n",
    "print(\"Спуск по стрелке, 400 шагов:          ошибка %.1f,  вычислений ошибки %6d\" % (mean_sq2(tr_t[-1]), 400 * 4))\n",
    "print(\"По очереди, %d кругов (40 подборов):  ошибка %.1f,  вычислений ошибки %6d\" % (n_rounds, mean_sq2(tr_o[-1]), 40 * 4001))\n",
    "print(\"Дно:                                   ошибка %.1f\" % mean_sq2(w_star))\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 6.5))\n",
    "ax.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "ax.plot(tr_t[:, 0], tr_t[:, 1], \"-\", color=palette[1], linewidth=2, label=\"обе ручки сразу (по стрелке), 400 шагов\")\n",
    "ax.plot(tr_o[:, 0], tr_o[:, 1], \"o-\", color=palette[2], linewidth=2, markersize=4,\n",
    "        label=\"по очереди: лучшее b, потом лучшее c, ... (%d кругов)\" % n_rounds)\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_xlabel(\"b — плата за заказ, ₽\"); ax.set_ylabel(\"c — цена страницы, ₽\")\n",
    "ax.set_title(\"Два способа добраться до дна\")\n",
    "ax.legend(fontsize=11, loc=\"upper right\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "f0370d74",
   "metadata": {},
   "source": [
    "## 6. Спуск по карте: узкая долина и масштаб"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4e702a1e",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(1, 2, figsize=(15, 6))\n",
    "for ax, eta, n in zip(axes, [0.003, 0.016], [200, 200]):\n",
    "    tr = descent_together(mean_sq2, [0., 40.], eta, n)\n",
    "    ax.contour(B, C, Z, levels=levels, cmap=\"Blues_r\", linewidths=1)\n",
    "    ax.plot(tr[:, 0], tr[:, 1], \"o-\", color=palette[1], linewidth=1, markersize=2,\n",
    "            label=r\"спуск, $\\eta = %g$, %d шагов\" % (eta, n))\n",
    "    ax.scatter([tr[0, 0]], [tr[0, 1]], 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_xlabel(\"b — плата за заказ, ₽\"); ax.set_ylabel(\"c — цена страницы, ₽\")\n",
    "    ax.set_title(\"ошибка %.1f после %d шагов\" % (mean_sq2(tr[-1]), n))\n",
    "    ax.legend(fontsize=10, loc=\"upper right\")\n",
    "plt.suptitle(\"Ландшафт ошибки для двух настроек — узкая долина\", y=1.01)\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "05c2798a",
   "metadata": {},
   "outputs": [],
   "source": [
    "x_mean, x_std = pages.mean(), pages.std()\n",
    "z_pages = (pages - x_mean) / x_std\n",
    "\n",
    "\n",
    "def mean_sq2_scaled(w):\n",
    "    return mean_sq2(w, x=z_pages)\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",
    "tr_raw = descent_together(mean_sq2, [0., 0.], eta=0.015, n_steps=3000)\n",
    "tr_scaled = descent_together(mean_sq2_scaled, [0., 0.], eta=0.3, n_steps=3000)\n",
    "print(\"Сырое число страниц,   100 шагов:  b = %.2f, c = %.2f\" % tuple(tr_raw[100]))\n",
    "print(\"Сырое число страниц,  3000 шагов:  b = %.2f, c = %.2f\" % tuple(tr_raw[-1]))\n",
    "b_s, c_s = tr_scaled[100]\n",
    "print(\"Отмасштабированный,    100 шагов:  b = %.2f, c = %.2f  (пересчитано обратно в страницы)\"\n",
    "      % (b_s - c_s * x_mean / x_std, c_s / x_std))\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, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 6))\n",
    "ax1.contour(Bz, Cz, Zz, levels=levels_z, cmap=\"Blues_r\", linewidths=1)\n",
    "ax1.plot(tr_scaled[:30, 0], tr_scaled[:30, 1], \"o-\", color=palette[1], linewidth=1.5, markersize=4, label=r\"спуск, $\\eta = 0.3$, 30 шагов\")\n",
    "ax1.scatter([wz_star[0]], [wz_star[1]], marker=\"*\", s=300, color=palette[3], zorder=5, label=\"дно\")\n",
    "ax1.set_xlabel(\"настройка при единице\"); ax1.set_ylabel(\"настройка при z\")\n",
    "ax1.set_title(\"После масштабирования долина круглая\")\n",
    "ax1.legend(fontsize=10)\n",
    "\n",
    "steps = np.arange(1, 3001)\n",
    "floor = mean_sq2(w_star)\n",
    "ax2.plot(steps, [mean_sq2(w) - floor + 1e-3 for w in tr_raw[1:]], linewidth=2.5, color=palette[0],\n",
    "         label=r\"сырое число страниц, $\\eta = 0.015$ (больше нельзя — разлетится)\")\n",
    "ax2.plot(steps, [mean_sq2_scaled(w) - floor + 1e-3 for w in tr_scaled[1:]], linewidth=2.5, color=palette[2],\n",
    "         label=r\"отмасштабированный вход, $\\eta = 0.3$\")\n",
    "ax2.set_xscale(\"log\"); 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": "89d6d816",
   "metadata": {},
   "source": [
    "## 7. Больше настроек — та же функция: перелом тарифа"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6db79450",
   "metadata": {},
   "outputs": [],
   "source": [
    "pages_d = np.arange(1, 13, dtype=float)\n",
    "paid_d = 30 * pages_d - 20 * np.maximum(0, pages_d - 3) + rng.normal(0, 4, len(pages_d))\n",
    "\n",
    "Xd = np.vstack([np.ones_like(pages_d), pages_d]).T\n",
    "line_d = np.linalg.solve(Xd.T @ Xd, Xd.T @ paid_d)              # лучшая прямая по формуле\n",
    "print(\"Лучшая прямая: %.1f + %.2f·x, средний квадрат промаха %.1f ₽²\" % (line_d[0], line_d[1], mean_sq2(line_d, pages_d, paid_d)))\n",
    "\n",
    "xs_d = np.linspace(0.5, 12.5, 300)\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "ax.scatter(pages_d, paid_d, marker=\"*\", s=200, color=\"black\", zorder=5, label=\"чеки четвёртого копицентра\")\n",
    "ax.plot(xs_d, predict2(line_d, xs_d), linewidth=2.5, color=palette[0], label=\"лучшая прямая\")\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": "3d6c8538",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, axes = plt.subplots(1, 3, figsize=(14, 6))\n",
    "axes[0].plot(xs_d, np.maximum(0, xs_d - 3), linewidth=2.5, color=palette[1])\n",
    "axes[0].set_title(\"блок max(0, x − 3): ноль, потом включается\", fontsize=12)\n",
    "axes[1].plot(xs_d, -20 * np.maximum(0, xs_d - 3), linewidth=2.5, color=palette[1])\n",
    "axes[1].set_title(\"−20 · max(0, x − 3): блок с весом\", fontsize=12)\n",
    "axes[2].plot(xs_d, 30 * xs_d, linewidth=1.5, color=palette[0], linestyle=\"--\", label=\"30·x\")\n",
    "axes[2].plot(xs_d, -20 * np.maximum(0, xs_d - 3), linewidth=1.5, color=palette[1], linestyle=\"--\", label=\"−20·max(0, x − 3)\")\n",
    "axes[2].plot(xs_d, 30 * xs_d - 20 * np.maximum(0, xs_d - 3), linewidth=3, color=palette[3], label=\"сумма\")\n",
    "axes[2].scatter(pages_d, paid_d, marker=\"*\", s=120, color=\"black\", zorder=5)\n",
    "axes[2].set_title(\"прямая + блок = ломаная с переломом\", fontsize=12)\n",
    "axes[2].legend(fontsize=10, loc=\"upper left\")\n",
    "for ax in axes:\n",
    "    ax.set_xlabel(\"число страниц\"); ax.axvline(3, color=\"0.7\", linestyle=\":\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9af4ec19",
   "metadata": {},
   "outputs": [],
   "source": [
    "def predict_kink(p, x):\n",
    "    return p[0] * x + p[1] * np.maximum(0, x - p[2])\n",
    "\n",
    "\n",
    "def mean_sq_kink(p):\n",
    "    return np.mean((paid_d - predict_kink(p, pages_d)) ** 2)\n",
    "\n",
    "\n",
    "p = np.array([10., 0., 6.])             # старт: «10 ₽ за страницу, перелома нет, где-то на 6»\n",
    "snapshots_kink = {0: p.copy()}\n",
    "for step in range(1, 5001):\n",
    "    p = p - 1e-3 * gradient(mean_sq_kink, p)\n",
    "    if step in (100, 1000, 5000):\n",
    "        snapshots_kink[step] = p.copy()\n",
    "\n",
    "for step, q in snapshots_kink.items():\n",
    "    print(\"шаг %4d:  c = %5.2f,  d = %6.2f,  перелом t = %4.2f,  ошибка %7.1f ₽²\" % (step, q[0], q[1], q[2], mean_sq_kink(q)))\n",
    "print(\"\\nПравда: c = 30, d = −20, t = 3\")\n",
    "\n",
    "fig, ax = plt.subplots(figsize=figsize)\n",
    "ax.scatter(pages_d, paid_d, marker=\"*\", s=200, color=\"black\", zorder=5, label=\"чеки\")\n",
    "for i, (step, q) in enumerate(snapshots_kink.items()):\n",
    "    ax.plot(xs_d, predict_kink(q, xs_d), linewidth=2.5, color=palette[i], label=\"после %d шагов\" % step)\n",
    "ax.set_xlabel(\"число страниц\"); ax.set_ylabel(\"стоимость, ₽\")\n",
    "ax.set_title(\"Три ручки, одна из них — положение перелома; спуск находит его сам\")\n",
    "ax.legend(fontsize=11, loc=\"upper left\")\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31855174",
   "metadata": {},
   "source": [
    "### Переломов несколько — блоков несколько"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "64ee0e67",
   "metadata": {},
   "outputs": [],
   "source": [
    "fig, ax = plt.subplots(figsize=(11, 6))\n",
    "ax.set_xlim(-0.5, 3.5); ax.set_ylim(-0.7, 8.7); ax.axis(\"off\")\n",
    "ys_hidden = np.arange(8)\n",
    "ax.scatter([0], [3.5], s=900, color=palette[0], zorder=5); ax.text(0, 3.5, \"x\", ha=\"center\", va=\"center\", fontsize=14, color=\"white\")\n",
    "ax.scatter(np.ones(8), ys_hidden, s=900, color=palette[1], zorder=5)\n",
    "for j in ys_hidden:\n",
    "    ax.text(1, j, \"max(0,·)\", ha=\"center\", va=\"center\", fontsize=8, color=\"white\")\n",
    "    ax.annotate(\"\", xy=(0.88, j), xytext=(0.12, 3.5), arrowprops=dict(arrowstyle=\"->\", color=\"0.5\", lw=1))\n",
    "    ax.text(0.84, j + 0.3, \"w%d·x + b%d\" % (j + 1, j + 1), fontsize=8, color=\"0.3\", ha=\"right\")\n",
    "    ax.annotate(\"\", xy=(1.88, 3.5), xytext=(1.12, j), arrowprops=dict(arrowstyle=\"->\", color=\"0.5\", lw=1))\n",
    "    ax.text(1.16, j + 0.3, \"× v%d\" % (j + 1), fontsize=8, color=\"0.3\", ha=\"left\")\n",
    "ax.scatter([2], [3.5], s=900, color=palette[3], zorder=5); ax.text(2, 3.5, \"Σ + b\", ha=\"center\", va=\"center\", fontsize=11, color=\"white\")\n",
    "ax.annotate(\"\", xy=(2.88, 3.5), xytext=(2.12, 3.5), arrowprops=dict(arrowstyle=\"->\", color=\"0.5\", lw=1.5))\n",
    "ax.text(3.0, 3.5, \"стоимость\", ha=\"left\", va=\"center\", fontsize=13)\n",
    "ax.text(0, 8.3, \"вход\", ha=\"center\", fontsize=12); ax.text(1, 8.3, \"восемь блоков — «нейронов»\", ha=\"center\", fontsize=12); ax.text(2, 8.3, \"сумма\", ha=\"center\", fontsize=12)\n",
    "ax.set_title(\"Это и есть нейросеть: один вход, восемь нейронов с ReLU, один линейный выход\", fontsize=13)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7529468c",
   "metadata": {},
   "outputs": [],
   "source": [
    "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()   # урок раздела 6\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",
    "    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):\n",
    "    return np.mean((u_e - net(p, z_e)) ** 2)\n",
    "\n",
    "\n",
    "p_net = rng.normal(0, .5, 3 * H + 1)\n",
    "eta, snapshots = 0.2, {}\n",
    "for step in range(12001):\n",
    "    if step in (0, 50, 500, 12000):\n",
    "        snapshots[step] = p_net.copy()\n",
    "    p_net = p_net - eta * gradient(mean_sq_net, p_net)\n",
    "\n",
    "for step, q in snapshots.items():\n",
    "    print(\"шаг %4d:  средний квадрат промаха %7.1f ₽²\" % (step, mean_sq_net(q) * y_s ** 2))"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "012854b1",
   "metadata": {},
   "outputs": [],
   "source": [
    "xs = np.linspace(0.5, 20.5, 400)\n",
    "zs = (xs - x_m) / x_s\n",
    "W1, b1, W2, b2 = p_net[:H], p_net[H:2 * H], p_net[2 * H:3 * H], p_net[3 * H]\n",
    "kinks = x_m - b1 / W1 * x_s                                  # где включается каждый блок, в страницах\n",
    "\n",
    "fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5.5))\n",
    "ax1.scatter(pages_e, paid_e, marker=\"*\", s=120, color=\"black\", zorder=5, label=\"чеки\")\n",
    "ax1.plot(xs, tariff(xs), linewidth=1.5, color=\"0.5\", linestyle=\"--\", label=\"настоящий тариф\")\n",
    "for i, (step, q) in enumerate(snapshots.items()):\n",
    "    ax1.plot(xs, predict_net(q, xs), linewidth=2.5, color=palette[i], label=\"после %d шагов\" % step)\n",
    "ax1.set_ylim(0, 350)\n",
    "ax1.set_xlabel(\"число страниц\"); ax1.set_ylabel(\"стоимость, ₽\")\n",
    "ax1.set_title(\"Что выдаёт сеть по ходу обучения\")\n",
    "ax1.legend(fontsize=10, loc=\"upper left\")\n",
    "\n",
    "for j in range(H):\n",
    "    ax2.plot(xs, W2[j] * relu(W1[j] * zs + b1[j]) * y_s, color=\"0.4\", linewidth=1,\n",
    "             label=\"один блок v·max(0, w·x + b)\" if j == 0 else None)\n",
    "ax2.axhline(b2 * y_s + y_m, color=\"0.6\", linewidth=1, label=\"свободный член\")\n",
    "ax2.plot(xs, predict_net(p_net, xs), linewidth=2.5, color=palette[1], label=\"сумма: предсказание сети\")\n",
    "ax2.scatter(pages_e, paid_e, marker=\"*\", s=120, color=\"black\", zorder=5)\n",
    "ax2.set_xlabel(\"число страниц\"); ax2.set_ylabel(\"вклад в стоимость, ₽\")\n",
    "ax2.set_title(\"Из чего складывается предсказание: восемь блоков, у каждого свой перелом\")\n",
    "ax2.legend(fontsize=10, loc=\"upper left\")\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0a5fd9d1",
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"Перелом каждого из восьми блоков (в страницах), по какую сторону он включён и сколько добавляет к цене страницы:\")\n",
    "for j in np.argsort(kinks):\n",
    "    inside = 0.5 <= kinks[j] <= 20.5\n",
    "    print(\"  блок %d: перелом на %6.1f стр., включён %s, добавляет %6.2f ₽/стр. %s\"\n",
    "          % (j + 1, kinks[j], \"правее\" if W1[j] > 0 else \"левее \", W2[j] * W1[j] * y_s / x_s,\n",
    "             \"\" if inside else \"(вне диапазона чеков)\"))\n",
    "\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, predict_net(p_net, xs), linewidth=2.5, color=palette[1], label=\"предсказание сети\")\n",
    "for j in range(H):\n",
    "    if 0.5 <= kinks[j] <= 20.5:\n",
    "        ax.axvline(kinks[j], color=palette[1], linestyle=\":\", linewidth=1.5,\n",
    "                   label=\"переломы, найденные сетью\" if j == np.argsort(kinks)[0] else None)\n",
    "for t_true in (3, 10):\n",
    "    ax.axvline(t_true, color=palette[3], linestyle=\"--\", linewidth=2, label=\"настоящие переломы тарифа\" if t_true == 3 else None)\n",
    "ax.set_ylim(0, 350)\n",
    "ax.set_xlabel(\"число страниц\"); ax.set_ylabel(\"стоимость, ₽\")\n",
    "ax.set_title(\"Переломы, которые сеть расставила сама, против настоящих переломов тарифа\")\n",
    "ax.legend(fontsize=10, loc=\"upper left\")\n",
    "plt.show()"
   ]
  }
 ],
 "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
}
