{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Por qué a veces no converge: tasa, mínimos locales y convexidad\n",
    "\n",
    "Los tres motivos por los que un entrenamiento no llega, con el número exacto que separa la tasa que funciona de la que explota.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 18 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/por-que-no-converge/\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": [
    "## El borde filoso del capítulo anterior 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 17 la tasa 0.9 funcionaba y la 1.01 mandaba el\n",
    "error a veintinueve mil millones. Ese borde no es aleatorio: tiene un número\n",
    "exacto y se puede calcular."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Volvemos a abrir el archivo"
   ]
  },
  {
   "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": [
    "## Motivo uno: la tasa se pasó del límite"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "H = 2 * (A.T @ A) / n\n",
    "vals = np.linalg.eigvalsh(H)\n",
    "print('autovalores de la hessiana:', np.round(vals, 4))\n",
    "print('limite de la tasa         :', round(float(2 / vals.max()), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "η<2λmax\n",
    "\n",
    "la tasa tiene que ser menor que dos partido por el autovalor mayor de la hessiana, y si te pasas de ahí el error crece en vez de bajar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0.9863. Ahí está el borde. La tasa 0.9 estaba justo debajo y la 1.01 justo\n",
    "encima, y por eso una funcionaba y la otra no.\n",
    "\n",
    "La intuición es la de una pelota en un tazón: si el paso es más del doble de\n",
    "lo que hace falta para llegar al fondo, acabas más arriba que donde\n",
    "empezaste. Y como el gradiente ahí es mayor, el siguiente paso es peor\n",
    "todavía. Se realimenta 🙃"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Motivo dos, y es el que te va a pasar: no escalaste"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "An = np.column_stack([np.ones(len(y)), F])\n",
    "Hn = 2 * (An.T @ An) / n\n",
    "vn = np.linalg.eigvalsh(Hn)\n",
    "print('sin escalar, autovalores:', np.round(vn, 4))\n",
    "print('limite de la tasa       :', float(2 / vn.max()))\n",
    "print('numero de condicion     :', round(float(vn.max() / vn.min()), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El límite pasó de 0.9863 a **0.0056**. Ciento setenta y seis\n",
    "veces más chico.\n",
    "\n",
    "Y ahora míralo pasar:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def error_n(w):\n",
    "    return float(((y - An @ w) ** 2).mean())\n",
    "\n",
    "for tasa in (0.1, 0.01, 0.001):\n",
    "    w = np.zeros(3)\n",
    "    roto = False\n",
    "    for _ in range(200):\n",
    "        w = w - tasa * (-2 * (An.T @ (y - An @ w)) / n)\n",
    "        if not np.all(np.isfinite(w)):\n",
    "            roto = True\n",
    "            break\n",
    "    print('  tasa', tasa, '->', 'exploto' if roto else round(error_n(w), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La tasa 0.1, que escalado iba perfecta, acá revienta. La 0.01 devuelve diez\n",
    "elevado a 169, que técnicamente no es infinito y para efectos prácticos lo es. Y\n",
    "la 0.001, que sí está por debajo del límite, llega a 633.065 cuando el óptimo es\n",
    "567.723.\n",
    "\n",
    "Ninguna de las tres sirve. Los mismos datos, el mismo modelo, la misma\n",
    "fórmula, y lo único que cambia es en qué unidades están medidas las\n",
    "columnas.\n",
    "\n",
    "**Escalar no es limpieza de datos.** Es lo que convierte un cañón\n",
    "estrecho en un tazón redondo, y esa es la diferencia entre poder entrenar y no\n",
    "poder. Es la cuarta vez en este libro que escalar arregla algo y ya no es\n",
    "coincidencia: es que la mitad de los problemas de este oficio son problemas de\n",
    "escala 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Motivo tres: la superficie no tiene un solo fondo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los dos motivos de arriba son de configuración y se arreglan. El tercero es\n",
    "de la forma del problema."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('autovalores escalado:', np.round(vals, 4))\n",
    "print('todos positivos     :', bool(np.all(vals > 0)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todos positivos en todo el espacio quiere decir **convexo**: un\n",
    "solo fondo, y llegas siempre, vengas de donde vengas. Eso lo comprobaste en el\n",
    "ejercicio 2 del capítulo 17.\n",
    "\n",
    "Toda regresión lineal con error cuadrático es convexa. La logística también.\n",
    "Y por eso con esos modelos el entrenamiento o converge o está mal configurado,\n",
    "no hay tercera opción.\n",
    "\n",
    "Una red neuronal **no** es convexa, y ahí aparecen dos cosas:\n",
    "\n",
    "- **Mínimos locales**: hoyos que no son el hoyo más hondo. Tienen\n",
    "mucha peor fama de la que merecen, porque en dimensiones altas son raros.\n",
    "\n",
    "- **Puntos de silla**: sube en unas direcciones y baja en otras,\n",
    "como el ejercicio 3 del capítulo 16. El gradiente ahí vale cero y\n",
    "el entrenamiento se queda quieto sin haber terminado. Estos sí son el problema\n",
    "de verdad, y son muchísimo más frecuentes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La lista para cuando no converja"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- **¿Escalaste?** Nueve de cada diez veces es esto.\n",
    "\n",
    "- **Baja la tasa entre diez.** Si con eso baja el error, era la\n",
    "tasa.\n",
    "\n",
    "- **Mira el largo del gradiente.** Si es cero pero el error sigue\n",
    "alto, es un punto de silla, no el fondo.\n",
    "\n",
    "- **Comprueba el gradiente contra el numérico**, como en el capítulo 13. Si no coinciden, la derivada está mal programada\n",
    "y no hay tasa que lo arregle.\n",
    "\n",
    "- **Mira si hay nan.** Un solo nan en los datos se propaga a todos\n",
    "los pesos en una iteración."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- La tasa tiene un límite exacto: dos partido por el autovalor mayor de la\n",
    "hessiana.\n",
    "\n",
    "- Sin escalar ese límite se hace diminuto, y ninguna tasa razonable\n",
    "funciona.\n",
    "\n",
    "- Escalar cambia la forma del valle, no los datos.\n",
    "\n",
    "- Los modelos lineales son convexos: convergen o están mal\n",
    "configurados.\n",
    "\n",
    "- En redes, los puntos de silla dan más problemas que los mínimos\n",
    "locales.\n",
    "\n",
    "- Cuando no converja: escalar, bajar la tasa, mirar el gradiente, buscar\n",
    "nan."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Con las columnas escaladas tu entrenamiento converge con tasa 0.1. Sin escalar, la misma tasa explota. ¿Por qué?\n",
    "\n",
    "a) Porque sin escalar el autovalor mayor de la hessiana es enorme, y el límite de la tasa se hace diminuto\n",
    "\n",
    "b) Porque sin escalar hay más ruido en los datos\n",
    "\n",
    "c) Porque escalar quita los valores extremos\n",
    "\n",
    "d) Porque escalar hace el modelo más simple\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Los datos son exactamente los mismos números. Lo único que cambió es en qué unidades están medidos.\n",
    "\n",
    "*c)* Escalar no quita nada: resta la media y divide por la desviación. Los extremos siguen siendo extremos, solo que en otra escala.\n",
    "\n",
    "*d)* El modelo es el mismo y tiene los mismos pesos. Lo que cambia es la forma del valle por el que hay que bajar.\n",
    "\n",
    "Escalar no es limpieza de datos: es cambiarle la forma al valle para que se pueda bajar 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cruza el límite y míralo explotar\n",
    "\n",
    "Justo debajo y justo encima del número exacto."
   ]
  },
  {
   "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",
    "limite = float(2 / np.linalg.eigvalsh(2 * (A.T @ A) / n).max())\n",
    "print('limite:', round(limite, 6))\n",
    "for tasa in (limite * 0.99, limite * 1.01):\n",
    "    w = np.zeros(3)\n",
    "    for _ in range(300):\n",
    "        w = w - tasa * (-2 * (A.T @ (y - A @ w)) / n)\n",
    "    print('  tasa', round(tasa, 6), '-> error', error(w))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "limite: 0.986287\n",
    "  tasa 0.976424 -> error 567723.3915457682\n",
    "  tasa 0.99615 -> error 28471165.52758426\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un 2% de diferencia en la tasa, y el error pasa de 567.723 a veintiocho\n",
    "millones. No es una zona gris: es un acantilado, y ahora sabes en qué coordenada\n",
    "está. Y ojo con que del lado malo el número todavía se puede imprimir: si dejas\n",
    "correr más iteraciones se va a infinito, pero en el paso 300 aún parece un\n",
    "número."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Un nan se come el modelo entero\n",
    "\n",
    "Mete un hueco y mira cuánto tarda en contaminarlo todo."
   ]
  },
  {
   "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",
    "Aroto = A.copy()\n",
    "Aroto[7, 1] = np.nan\n",
    "w = np.zeros(3)\n",
    "for i in range(1, 4):\n",
    "    w = w - 0.1 * (-2 * (Aroto.T @ (y - Aroto @ w)) / n)\n",
    "    print('iter', i, '| pesos', w, '| cuantos nan:', int(np.isnan(w).sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "iter 1 | pesos [nan nan nan] | cuantos nan: 3\n",
    "iter 2 | pesos [nan nan nan] | cuantos nan: 3\n",
    "iter 3 | pesos [nan nan nan] | cuantos nan: 3\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Una sola casilla mala, y en **una** iteración los tres pesos son\n",
    "nan. Ni siquiera hizo falta la segunda: el producto punto del capítulo 4 suma toda la columna, así que un nan en cualquier fila\n",
    "contamina el resultado entero, y de ahí pasa a los tres pesos.\n",
    "\n",
    "Y encima no da error: sigue corriendo tan tranquilo hasta el final. Por eso\n",
    "`np.isnan(X).any()` va antes de entrenar y no después de que algo\n",
    "salga raro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El número de condición predice cuántas iteraciones\n",
    "\n",
    "Compara escalado contra a medio escalar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "F = X[:, [0, 2]]\n",
    "n = len(y)\n",
    "Zf = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "\n",
    "for nombre, M in (('escalado ', Zf), ('a medias ', Zf * np.array([1.0, 50.0]))):\n",
    "    A_ = np.column_stack([np.ones(n), M])\n",
    "    H = 2 * (A_.T @ A_) / n\n",
    "    v = np.linalg.eigvalsh(H)\n",
    "    opt = np.linalg.solve(A_.T @ A_, A_.T @ y)\n",
    "    w = np.zeros(3)\n",
    "    tasa = 0.9 * 2 / v.max()\n",
    "    iters = 0\n",
    "    while np.linalg.norm(w - opt) > 1e-3 and iters < 200000:\n",
    "        w = w - tasa * (-2 * (A_.T @ (y - A_ @ w)) / n)\n",
    "        iters += 1\n",
    "    print(nombre, '| condicion', round(float(v.max() / v.min()), 1),\n",
    "          '| iteraciones', iters)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "escalado  | condicion 1.0 | iteraciones 54\n",
    "a medias  | condicion 2500.5 | iteraciones 18878\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Multipliqué una columna por cincuenta y el entrenamiento pasó de 54\n",
    "iteraciones a 18.878. Trescientas cincuenta veces más lento por una decisión de\n",
    "unidades que nadie tomó a propósito. Ese es el precio real de no escalar, y no\n",
    "sale en ningún mensaje de error."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 18 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/por-que-no-converge/\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
}
