{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Jacobiana y hessiana, lo justo\n",
    "\n",
    "Qué son las derivadas de segundo orden, qué mide la curvatura, y por qué saberlo explica que el entrenamiento vaya lento.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 16 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/jacobiana-y-hessiana/\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": [
    "## Este capítulo es corto a propósito 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Jacobiana y hessiana tienen fama de tema avanzado, y para lo que tú necesitas\n",
    "son dos ideas simples. Te las doy y seguimos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Volvemos a abrir el archivo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada capítulo arranca de cero, así que estas líneas se repiten. Son las\n",
    "mismas del capítulo 1 y las vas a ver en todos."
   ]
  },
  {
   "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",
    "print('X.shape:', X.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La jacobiana: cuando la función devuelve varios números"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El gradiente del capítulo 14 es de una función\n",
    "que devuelve **un** número, el error. Si la función devuelve varios,\n",
    "hace falta una derivada por cada entrada y cada salida. Eso es una matriz, y se\n",
    "llama **jacobiana**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = X.std(axis=0)\n",
    "J = np.diag(1 / s)\n",
    "print('desviaciones:', np.round(s, 4))\n",
    "print('jacobiana del escalado:')\n",
    "print(np.round(J, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa es la jacobiana de escalar tus columnas: entran tres números y salen tres.\n",
    "Sale diagonal porque cada columna se escala sola, sin mirar a las otras.\n",
    "\n",
    "Y hay un caso que ya conoces: la jacobiana de multiplicar por una matriz\n",
    "**es esa misma matriz**. Si tu función es Wx, su jacobiana es W. Por\n",
    "eso en una red los pesos aparecen tal cual en la retropropagación del capítulo 15: la cadena va multiplicando jacobianas, y las\n",
    "jacobianas de las capas lineales son los propios pesos 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La hessiana: la curvatura del valle"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La derivada dice la pendiente. La **derivada segunda** dice\n",
    "cuánto cambia esa pendiente, o sea si el valle es cerrado y empinado o abierto y\n",
    "plano."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "A = np.column_stack([np.ones(len(y)), X[:, 0]])\n",
    "H = 2 * (A.T @ A) / len(y)\n",
    "print('hessiana:')\n",
    "print(np.round(H, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Hij=∂2E∂wi∂wj\n",
    "\n",
    "la hessiana guarda las derivadas segundas: cuánto cambia la pendiente en una dirección cuando te mueves en otra, o sea la curvatura del valle"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es simétrica, como X⊤X en el capítulo 6, y por el mismo motivo. Y eso quiere decir que le\n",
    "podemos sacar autovalores con `eigh`, como en el capítulo 10."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sus autovalores dicen dos cosas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vals = np.linalg.eigvalsh(H)\n",
    "print('autovalores        :', np.round(vals, 4))\n",
    "print('todos positivos    :', bool(np.all(vals > 0)))\n",
    "print('numero de condicion:', round(float(vals.max() / vals.min()), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Todos positivos** quiere decir que en todas las direcciones la\n",
    "superficie sube. O sea que ahí hay un fondo de verdad y no una silla de montar.\n",
    "Cuando eso pasa en todo el espacio, el problema se llama\n",
    "**convexo**, y convexo quiere decir que hay un solo mínimo y no te\n",
    "puedes quedar atrapada.\n",
    "\n",
    "**El cociente entre el mayor y el menor** es el número de\n",
    "condición, el mismo del capítulo 8. Aquí vale 869, y traducido\n",
    "quiere decir que el valle es 869 veces más empinado en una dirección que en la\n",
    "otra.\n",
    "\n",
    "Imagínate un cañón largo y estrecho. Si caminas siempre en la dirección de\n",
    "máxima pendiente, rebotas de una pared a la otra y avanzas poquito hacia el\n",
    "fondo. Eso es el zigzag del descenso de gradiente, y ese 869 es el número que\n",
    "lo predice antes de que pase 🙃"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y por eso existe el método de Newton"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "w = np.array([500.0, 30.0])\n",
    "g = -2 * (A.T @ (y - A @ w)) / len(y)\n",
    "paso = np.linalg.solve(H, g)\n",
    "nuevo = w - paso\n",
    "optimo = np.linalg.solve(A.T @ A, A.T @ y)\n",
    "\n",
    "print('gradiente    :', np.round(g, 4))\n",
    "print('un paso da   :', round(float(nuevo[0]), 4), round(float(nuevo[1]), 4))\n",
    "print('el optimo es :', round(float(optimo[0]), 4), round(float(optimo[1]), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝐰nuevo=𝐰−H−1∇E\n",
    "\n",
    "el paso de Newton corrige el gradiente con la curvatura, y por eso llega al fondo mucho más rápido, aunque calcular la hessiana cuesta caro"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Un paso.** No cien, ni mil: uno, y clavado.\n",
    "\n",
    "Newton corrige el gradiente con la curvatura, así que sabe estirar el paso\n",
    "donde el valle es plano y acortarlo donde es empinado. Sin zigzag.\n",
    "\n",
    "Y entonces la pregunta obvia: si esto es tan bueno, ¿por qué las redes se\n",
    "entrenan con descenso de gradiente y no con Newton?\n",
    "\n",
    "Por el tamaño. La hessiana de un modelo con un millón de pesos tiene un billón\n",
    "de casillas, y además hay que resolver un sistema con ella en cada paso. No cabe\n",
    "ni en la memoria ni en el presupuesto.\n",
    "\n",
    "Por eso existen los métodos que están en medio, como L-BFGS, que se inventan\n",
    "una hessiana aproximada, o como Adam, que guarda un resumen barato de la\n",
    "curvatura por cada peso. Todos intentan lo mismo: algo de Newton sin pagar\n",
    "Newton 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- La jacobiana es la matriz de derivadas primeras cuando salen varios\n",
    "números.\n",
    "\n",
    "- La jacobiana de multiplicar por W es W.\n",
    "\n",
    "- La hessiana son las derivadas segundas y mide la curvatura.\n",
    "\n",
    "- Autovalores todos positivos: hay un mínimo de verdad y el problema es\n",
    "convexo.\n",
    "\n",
    "- El número de condición dice cuánto vas a zigzaguear.\n",
    "\n",
    "- Newton llega en un paso y no se usa porque la hessiana no cabe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Los autovalores de la hessiana de tu error son [0.39, 341.22]. ¿Qué esperas que le pase al descenso de gradiente?\n",
    "\n",
    "a) Que zigzaguee, porque el valle es 869 veces más empinado en una dirección que en la otra\n",
    "\n",
    "b) Que vaya derecho al fondo, porque los dos son positivos\n",
    "\n",
    "c) Que no converja nunca, porque uno es muy grande\n",
    "\n",
    "d) Que se quede en un mínimo local\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Positivos quiere decir que hay un fondo y no una silla de montar, que es buena noticia. Pero no dice nada de lo rápido que se llega.\n",
    "\n",
    "*c)* Converger va a converger. Lo que va a hacer es tardar muchísimo, que es distinto de no llegar.\n",
    "\n",
    "*d)* Con los dos autovalores positivos en todo el espacio, el problema es convexo y solo hay un mínimo. Los locales aparecen cuando la hessiana cambia de signo según dónde estés.\n",
    "\n",
    "El cociente entre el autovalor mayor y el menor es el número de condición, y es el que te dice cuánto vas a zigzaguear 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. La hessiana a lo bruto\n",
    "\n",
    "Sácala midiendo, como hicimos con la derivada."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "A = np.column_stack([np.ones(len(y)), X[:, 0]])\n",
    "\n",
    "def error(w):\n",
    "    return float(((y - A @ w) ** 2).mean())\n",
    "\n",
    "w = np.array([500.0, 30.0])\n",
    "h = 1e-4\n",
    "Hn = np.zeros((2, 2))\n",
    "for i in range(2):\n",
    "    for j in range(2):\n",
    "        pp, pm, mp, mm = w.copy(), w.copy(), w.copy(), w.copy()\n",
    "        pp[i] += h; pp[j] += h\n",
    "        pm[i] += h; pm[j] -= h\n",
    "        mp[i] -= h; mp[j] += h\n",
    "        mm[i] -= h; mm[j] -= h\n",
    "        Hn[i, j] = (error(pp) - error(pm) - error(mp) + error(mm)) / (4 * h * h)\n",
    "\n",
    "print(np.round(Hn, 4))\n",
    "print('coincide con la exacta:',\n",
    "      np.allclose(Hn, 2 * (A.T @ A) / len(y), rtol=1e-3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "[[  1.9994  23.35  ]\n",
    " [ 23.35   339.6097]]\n",
    "coincide con la exacta: True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro evaluaciones por casilla. Para dos pesos son dieciséis evaluaciones y\n",
    "se aguanta; para un millón de pesos, saca la cuenta. Ahí tienes la razón\n",
    "práctica de que Newton no se use en redes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Escala y mira cómo se arregla el cañón\n",
    "\n",
    "El número de condición no es del problema: es de cómo lo\n",
    "escribiste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "u = X[:, 0]\n",
    "A = np.column_stack([np.ones(len(y)), u])\n",
    "uz = (u - u.mean()) / u.std()\n",
    "Az = np.column_stack([np.ones(len(y)), uz])\n",
    "\n",
    "for nombre, M in (('sin escalar', A), ('escalado  ', Az)):\n",
    "    H = 2 * (M.T @ M) / len(y)\n",
    "    v = np.linalg.eigvalsh(H)\n",
    "    print(nombre, '-> autovalores', np.round(v, 4),\n",
    "          '| condicion', round(float(v.max() / v.min()), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin escalar -> autovalores [  0.3924 341.2153] | condicion 869.55\n",
    "escalado   -> autovalores [2. 2.] | condicion 1.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Condición 1. El cañón se convirtió en un tazón redondo, y en un tazón redondo\n",
    "el descenso de gradiente va derecho al fondo sin rebotar. La misma pregunta, los\n",
    "mismos datos, y el entrenamiento pasa de lento a inmediato solo por escalar. Es\n",
    "la tercera vez en este libro que escalar arregla algo, y no es casualidad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Una silla de montar\n",
    "\n",
    "Cuando los autovalores tienen signos distintos, no hay ni\n",
    "mínimo ni máximo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Silla = np.array([[2.0, 0.0], [0.0, -2.0]])\n",
    "print('autovalores:', np.linalg.eigvalsh(Silla))\n",
    "print('hay minimo :', bool(np.all(np.linalg.eigvalsh(Silla) > 0)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "autovalores: [-2.  2.]\n",
    "hay minimo : False\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sube en una dirección y baja en la otra: es la forma de una silla de montar.\n",
    "El gradiente ahí vale cero y no estás en ningún fondo. En redes profundas esto\n",
    "pasa muchísimo más que los mínimos locales, y es la razón de que el gradiente se\n",
    "quede quieto sin que el modelo haya terminado de aprender."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 16 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/jacobiana-y-hessiana/\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
}
