{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Descenso de gradiente, a mano y después con NumPy\n",
    "\n",
    "El algoritmo con el que se entrena casi todo, escrito en cuatro líneas y corrido sobre la tabla de ventas hasta llegar al óptimo exacto.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 17 de **Matemática para machine learning**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/matematica-para-machine-learning/descenso-de-gradiente/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# Este capitulo abre el archivo por su nombre, sin URL, que es lo que se hace\n",
    "# cuando lo tienes al lado. Aqui se baja primero para que exista.\n",
    "for nombre in ['ventas-miss-yera.csv']:\n",
    "    urllib.request.urlretrieve(f\"https://missyera.com/static/datasets/{nombre}\", nombre)\n",
    "    print(nombre, \"lista\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuatro líneas y con eso se entrena casi todo 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ya tienes las dos piezas. En el capítulo 14 viste\n",
    "que el gradiente apunta cuesta arriba. En el capítulo 13 bajaste\n",
    "un valle a mano sin saber que lo estabas haciendo.\n",
    "\n",
    "Esto es juntarlas y ponerle nombre."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝐰←𝐰−η∇E(𝐰)\n",
    "\n",
    "el descenso de gradiente resta el gradiente multiplicado por la tasa de aprendizaje, o sea da un paso en contra de la cuesta arriba y lo repite"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Símbolo | Qué es |\n",
    "|---|---|\n",
    "| **w** | los pesos |\n",
    "| **η** | la tasa de aprendizaje, la letra eta, que decide cuánto avanzas |\n",
    "| **∇E** | el gradiente, que apunta hacia donde el error sube |\n",
    "| **←** | quiere decir \"el nuevo valor de w es esto\" |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Volvemos a abrir el archivo, escalado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Escalo las columnas desde el principio, y en el capítulo que viene vas a ver\n",
    "por qué no es opcional."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "df = pd.read_csv('ventas-miss-yera.csv')\n",
    "for c in ('unidades', 'monto', 'satisfaccion'):\n",
    "    df[c] = pd.to_numeric(df[c], errors='coerce')\n",
    "X = df[['unidades', 'monto', 'satisfaccion']].dropna().to_numpy()\n",
    "\n",
    "y = X[:, 1]\n",
    "F = X[:, [0, 2]]\n",
    "Zf = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "A = np.column_stack([np.ones(len(y)), Zf])\n",
    "n = len(y)\n",
    "print('A.shape:', A.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Quiero predecir el monto con las unidades y la satisfacción. Ojo con esto:\n",
    "el monto sale de las columnas que predicen, **no** está dentro de\n",
    "ellas. Meter la respuesta entre las preguntas es de los errores más caros que\n",
    "hay, y en mi libro de machine learning tiene un capítulo entero."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El algoritmo entero"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def error(w):\n",
    "    return float(((y - A @ w) ** 2).mean())\n",
    "\n",
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / n\n",
    "\n",
    "w = np.zeros(3)\n",
    "for i in range(1, 51):\n",
    "    w = w - 0.1 * gradiente(w)\n",
    "    if i in (1, 2, 5, 10, 25, 50):\n",
    "        print('  iter', str(i).rjust(2), '| error', round(error(w), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es. Una resta dentro de un bucle.\n",
    "\n",
    "Y mira cómo baja: al principio a saltos enormes, después cada vez menos.\n",
    "Tiene sentido, porque cerca del fondo el gradiente es chiquito, así que el paso\n",
    "también. El algoritmo **frena solo** al llegar 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Llegó al sitio correcto?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimo = np.linalg.solve(A.T @ A, A.T @ y)\n",
    "print('descenso :', np.round(w, 4))\n",
    "print('exacto   :', np.round(optimo, 4))\n",
    "print('distancia:', round(float(np.linalg.norm(w - optimo)), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cincuenta iteraciones y está a una centésima del óptimo que el capítulo 6 calcula de una sola vez con una fórmula.\n",
    "\n",
    "Y aquí la pregunta obvia: si la fórmula da el resultado exacto de un tirón,\n",
    "¿para qué el bucle?\n",
    "\n",
    "Porque la fórmula **solo existe para modelos lineales**. Para una\n",
    "red neuronal, para una regresión logística o para casi cualquier otra cosa, no\n",
    "hay fórmula. Solo queda buscar, y buscar es esto 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La tasa de aprendizaje, que es lo único que hay que elegir"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for tasa in (0.001, 0.01, 0.1, 0.5, 0.9, 1.01):\n",
    "    w = np.zeros(3)\n",
    "    for _ in range(200):\n",
    "        w = w - tasa * gradiente(w)\n",
    "    print('  tasa', tasa, '-> error', round(error(w), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lee esa tabla de arriba abajo, porque es el capítulo entero:\n",
    "\n",
    "- **0.001**: doscientas iteraciones y todavía va por 857.000.\n",
    "Llega, pero de aquí a mañana.\n",
    "\n",
    "- **0.01**: casi. Se queda a doscientos del fondo.\n",
    "\n",
    "- **0.1 a 0.9**: perfecto. Hay un rango cómodo y bastante\n",
    "ancho.\n",
    "\n",
    "- **1.01**: veintinueve mil millones. Explotó.\n",
    "\n",
    "Entre 0.9 y 1.01 no hay casi nada de diferencia, y una funciona y la otra\n",
    "revienta. Por qué existe ese borde tan filoso es el capítulo 18."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las tres variantes que vas a oír nombrar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo que hicimos usa **todas** las filas en cada paso. Se llama\n",
    "descenso de gradiente por lotes, y con 2.238 filas va sobrado. Con veinte\n",
    "millones no.\n",
    "\n",
    "- **Estocástico**, o SGD: usa una fila cada vez. Cada paso es\n",
    "baratísimo y va dando tumbos, porque una sola fila es una opinión muy parcial\n",
    "sobre hacia dónde está el fondo.\n",
    "\n",
    "- **Por minilotes**: usa un puñado, típicamente entre 32 y 256.\n",
    "Es el que se usa de verdad en todas partes.\n",
    "\n",
    "- **Con momento, Adam y compañía**: le añaden memoria de los pasos\n",
    "anteriores para no rebotar tanto. Adam además le pone tasa propia a cada peso, y\n",
    "eso es lo que intenta arreglar lo que veremos de la curvatura en el capítulo 16.\n",
    "\n",
    "Todas son la misma línea. Lo único que cambia es con cuántas filas se calcula\n",
    "el gradiente y qué se recuerda entre pasos 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- El descenso de gradiente es restar el gradiente por la tasa, en un\n",
    "bucle.\n",
    "\n",
    "- Frena solo al acercarse, porque el gradiente se hace chiquito.\n",
    "\n",
    "- Llega al mismo sitio que la fórmula exacta, pero funciona donde no hay\n",
    "fórmula.\n",
    "\n",
    "- La tasa es lo único que eliges, y tiene un rango cómodo ancho con un borde\n",
    "filoso.\n",
    "\n",
    "- SGD, minilotes y Adam son la misma línea con distinta cantidad de filas y\n",
    "distinta memoria."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Entrenas con tasa 0.001 y el error baja muy despacio pero baja. Subes a 1.01 y el error se dispara a mil millones. ¿Qué pasó?\n",
    "\n",
    "a) El paso se pasó del fondo y cayó más arriba en la otra ladera, y eso se realimenta cada iteración\n",
    "\n",
    "b) El modelo se sobreajustó\n",
    "\n",
    "c) Los datos tienen valores extremos\n",
    "\n",
    "d) Hay que entrenar más iteraciones\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* El sobreajuste es que aprenda de memoria los datos de entrenamiento, y eso baja el error, no lo dispara. Esto es un problema de paso.\n",
    "\n",
    "*c)* Los mismos datos funcionaban con 0.001. Lo único que cambió es cuánto se mueve en cada paso.\n",
    "\n",
    "*d)* Más iteraciones con una tasa que diverge solo hacen el número más grande. El problema no es el tiempo, es la dirección de a cuánto avanza.\n",
    "\n",
    "Si el error sube en vez de bajar, lo primero que se toca es la tasa, y se divide entre diez 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Mira el gradiente apagarse\n",
    "\n",
    "La prueba de que el algoritmo frena solo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "F = X[:, [0, 2]]\n",
    "Zf = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "A = np.column_stack([np.ones(len(y)), Zf])\n",
    "n = len(y)\n",
    "\n",
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / n\n",
    "\n",
    "w = np.zeros(3)\n",
    "for i in range(1, 41):\n",
    "    g = gradiente(w)\n",
    "    w = w - 0.1 * g\n",
    "    if i in (1, 5, 10, 20, 40):\n",
    "        print('iter', str(i).rjust(2), '| largo del gradiente',\n",
    "              round(float(np.linalg.norm(g)), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "iter  1 | largo del gradiente 1607.1372\n",
    "iter  5 | largo del gradiente 658.282\n",
    "iter 10 | largo del gradiente 215.7054\n",
    "iter 20 | largo del gradiente 23.1611\n",
    "iter 40 | largo del gradiente 0.267\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De mil seiscientos a dos décimas. No hace falta ninguna regla de parada\n",
    "inteligente: cerca del fondo los pasos se hacen solos diminutos. Y por eso mismo,\n",
    "si tu entrenamiento se queda quieto, mirar el largo del gradiente te dice si es\n",
    "que llegó o es que se atascó."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Arranca desde otro sitio\n",
    "\n",
    "En un problema convexo da igual dónde empieces."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "F = X[:, [0, 2]]\n",
    "Zf = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "A = np.column_stack([np.ones(len(y)), Zf])\n",
    "n = len(y)\n",
    "\n",
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / n\n",
    "\n",
    "for inicio in (np.zeros(3), np.array([5000.0, -300.0, 900.0])):\n",
    "    w = inicio.copy()\n",
    "    for _ in range(300):\n",
    "        w = w - 0.1 * gradiente(w)\n",
    "    print('desde', np.round(inicio, 1), '-> llega a', np.round(w, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "desde [0. 0. 0.] -> llega a [803.3863   2.679  -16.8305]\n",
    "desde [5000. -300.  900.] -> llega a [803.3863   2.679  -16.8305]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El mismo punto hasta el cuarto decimal. Eso pasa porque este problema es\n",
    "convexo, que es lo que decían los autovalores positivos de la hessiana en el capítulo 16. En una red neuronal esto no se cumple, y por eso\n",
    "ahí sí importa desde dónde arrancas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Estocástico contra por lotes\n",
    "\n",
    "Una fila por paso, y mira el temblor."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "F = X[:, [0, 2]]\n",
    "Zf = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "A = np.column_stack([np.ones(len(y)), Zf])\n",
    "n = len(y)\n",
    "\n",
    "def error(w):\n",
    "    return float(((y - A @ w) ** 2).mean())\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "w = np.zeros(3)\n",
    "for paso in range(1, 4001):\n",
    "    i = int(rng.integers(n))\n",
    "    g = -2 * A[i] * (y[i] - A[i] @ w)\n",
    "    w = w - 0.01 * g\n",
    "    if paso in (100, 500, 1000, 4000):\n",
    "        print('paso', str(paso).rjust(4), '| error', round(error(w), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "paso  100 | error 593507.79\n",
    "paso  500 | error 584273.73\n",
    "paso 1000 | error 596225.78\n",
    "paso 4000 | error 585674.45\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en que el error no baja de forma limpia: en el paso 1000 está peor que\n",
    "en el 500, y en el 4000 vuelve a mejorar sin llegar nunca al fondo.\n",
    "Eso es el temblor del estocástico, que nunca se queda quieto porque cada fila lo\n",
    "empuja para su lado. En un problema chico es un defecto; en uno enorme es lo que\n",
    "lo hace posible, y de paso ese ruido ayuda a salirse de sitios malos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 17 de **Matemática para machine learning**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/matematica-para-machine-learning/descenso-de-gradiente/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
