{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Derivadas parciales y el gradiente: hacia dónde subir\n",
    "\n",
    "Qué es una derivada parcial, qué es el gradiente, y por qué apunta hacia arriba cuando lo que quieres es bajar.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 14 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/derivadas-parciales-y-el-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": [
    "## Un peso era fácil. Ahora son dos 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 13 había un solo peso, así que el valle era una\n",
    "curva y la pregunta era izquierda o derecha.\n",
    "\n",
    "Con dos pesos el valle es una superficie, como un terreno con lomas. Y la\n",
    "pregunta ya no es izquierda o derecha: es **en qué dirección de todas las\n",
    "posibles**."
   ]
  },
  {
   "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": [
    "## Dos pesos: intercepto y unidades"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "A = np.column_stack([np.ones(X.shape[0]), X[:, 0]])\n",
    "\n",
    "def error(w):\n",
    "    return float(((y - A @ w) ** 2).mean())\n",
    "\n",
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / len(y)\n",
    "\n",
    "w = np.array([0.0, 0.0])\n",
    "print('error en (0, 0):', round(error(w), 2))\n",
    "print('gradiente      :', np.round(gradiente(w), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "∂E∂wj\n",
    "\n",
    "la derivada parcial respecto a un peso es cuánto cambia el error si mueves ese peso y dejas todos los demás quietos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese vector tiene dos números y cada uno contesta una pregunta distinta:\n",
    "\n",
    "- **-1606.77**: si muevo el intercepto y dejo el peso de las\n",
    "unidades quieto, el error baja con esta inclinación.\n",
    "\n",
    "- **-18794.46**: lo mismo para el peso de las unidades.\n",
    "\n",
    "Eso es una **derivada parcial**: la derivada de siempre, pero\n",
    "mirando un solo peso y congelando los demás. La palabra parcial no esconde nada\n",
    "raro, quiere decir \"de a uno\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "∇E=(∂E∂w1,…,∂E∂wd)\n",
    "\n",
    "el gradiente es el vector con todas las derivadas parciales juntas, y como vector apunta hacia donde el error sube más rápido"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Símbolo | Qué es |\n",
    "|---|---|\n",
    "| **∇E** | el gradiente, y ese triangulito al revés se llama nabla |\n",
    "| **∂E/∂wj** | la parcial respecto al peso j |\n",
    "| **∂** | la de es curva y no recta solo para avisar que hay más variables |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El gradiente **es un vector**, con todo lo que eso quiere decir\n",
    "desde el capítulo 2: tiene componentes, tiene una dirección y tiene\n",
    "un largo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora lo importante: apunta hacia arriba"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "w = np.array([500.0, 30.0])\n",
    "g = gradiente(w)\n",
    "paso = 1e-5\n",
    "print('error donde estoy      :', round(error(w), 2))\n",
    "print('error restando el grad :', round(error(w - paso * g), 2))\n",
    "print('error sumando el grad  :', round(error(w + paso * g), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Restando baja, sumando sube. **El gradiente apunta cuesta\n",
    "arriba.**\n",
    "\n",
    "Y no es un capricho de la definición: el gradiente apunta hacia donde la\n",
    "función **crece** más rápido, de todas las direcciones posibles. Es\n",
    "la dirección de máxima subida.\n",
    "\n",
    "Como nosotras queremos bajar, vamos exactamente al revés. De ahí sale el signo\n",
    "menos que vas a ver en todas las fórmulas de entrenamiento, y de ahí sale el\n",
    "nombre del algoritmo 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## En el fondo vale cero"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "optimo = np.linalg.solve(A.T @ A, A.T @ y)\n",
    "print('intercepto   :', round(float(optimo[0]), 4))\n",
    "print('peso unidades:', round(float(optimo[1]), 4))\n",
    "print('gradiente ahi:', np.round(gradiente(optimo), 6))\n",
    "print('error ahi    :', round(error(optimo), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Gradiente cero en las dos componentes. Ninguna dirección baja: estás en el\n",
    "fondo.\n",
    "\n",
    "Y ese punto es el mismo que salió por las ecuaciones normales del capítulo 6. Tercera vez que dos caminos distintos llegan al\n",
    "mismo sitio en este libro, y no va a ser la última 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuidado con leer los pesos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Fíjate en el gradiente de partida: -1606 contra -18794. El segundo es once\n",
    "veces más grande.\n",
    "\n",
    "La tentación es decir \"las unidades importan once veces más\". Y no. Ese\n",
    "número es grande porque **la columna de unidades tiene números\n",
    "grandes** comparada con la columna de unos, exactamente el mismo problema\n",
    "de escala del capítulo 7.\n",
    "\n",
    "El gradiente no mide importancia, mide inclinación en unidades de esa columna.\n",
    "Y esa diferencia de escala entre componentes es la que hace que el descenso de\n",
    "gradiente zigzaguee. Eso lo trabajamos en el capítulo 16."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Una parcial es la derivada de siempre moviendo un peso y congelando los\n",
    "demás.\n",
    "\n",
    "- El gradiente es el vector con todas las parciales juntas.\n",
    "\n",
    "- Apunta hacia donde la función SUBE más rápido.\n",
    "\n",
    "- Por eso el descenso de gradiente resta, y de ahí sale el menos de las\n",
    "fórmulas.\n",
    "\n",
    "- Gradiente cero en todas las componentes quiere decir que estás en el\n",
    "fondo.\n",
    "\n",
    "- Componentes grandes no quiere decir columnas importantes: quiere decir\n",
    "escalas distintas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "El gradiente del error en tu punto actual es [93.79, 3069.82]. ¿Hacia dónde te mueves para bajar el error?\n",
    "\n",
    "a) Restando el gradiente: hacia [-93.79, -3069.82]\n",
    "\n",
    "b) Sumando el gradiente, porque apunta al mínimo\n",
    "\n",
    "c) Solo hacia el segundo peso, porque su número es mucho mayor\n",
    "\n",
    "d) Hacia [0, 0], que es donde el gradiente vale cero\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Apunta justo al revés: el gradiente señala hacia donde el error SUBE más rápido. Por eso se resta.\n",
    "\n",
    "*c)* Los dos se mueven a la vez y cada uno en proporción a su parcial. Que uno sea mayor quiere decir que ese se mueve más, no que el otro se quede.\n",
    "\n",
    "*d)* El gradiente vale cero en el mínimo, y el mínimo no tiene por qué estar en el origen. Aquí está en [797.51, 0.5].\n",
    "\n",
    "El gradiente apunta cuesta arriba. Por eso el algoritmo se llama descenso de gradiente y por eso la fórmula lleva un menos 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cada parcial, una por una\n",
    "\n",
    "Comprueba que el gradiente son dos derivadas separadas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "A = np.column_stack([np.ones(X.shape[0]), 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",
    "for j in range(2):\n",
    "    mas, menos = w.copy(), w.copy()\n",
    "    mas[j] += h\n",
    "    menos[j] -= h\n",
    "    print('parcial', j, ':', round((error(mas) - error(menos)) / (2 * h), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "parcial 0 : 93.7905\n",
    "parcial 1 : 3069.8176\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mover un peso y congelar el otro, dos veces. El gradiente no es un objeto\n",
    "misterioso: es el resultado de hacer eso para cada peso y guardar los números en\n",
    "una lista."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Ninguna otra dirección baja más\n",
    "\n",
    "Compara el gradiente contra direcciones al azar."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "A = np.column_stack([np.ones(X.shape[0]), X[:, 0]])\n",
    "\n",
    "def error(w):\n",
    "    return float(((y - A @ w) ** 2).mean())\n",
    "\n",
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / len(y)\n",
    "\n",
    "w = np.array([500.0, 30.0])\n",
    "g = gradiente(w)\n",
    "d = -g / np.linalg.norm(g)\n",
    "paso = 0.01\n",
    "print('bajando por el gradiente :', round(error(w + paso * d), 4))\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "peor = 0\n",
    "for _ in range(200):\n",
    "    r = rng.normal(size=2)\n",
    "    r = r / np.linalg.norm(r)\n",
    "    if error(w + paso * r) < error(w + paso * d):\n",
    "        peor += 1\n",
    "print('direcciones al azar que bajaron mas:', peor, 'de 200')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "bajando por el gradiente : 599298.6806\n",
    "direcciones al azar que bajaron mas: 0 de 200\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Doscientas direcciones y ninguna gana. Eso es lo que quiere decir \"dirección\n",
    "de máxima pendiente\", y es la razón de que el algoritmo se llame así y no de\n",
    "otra manera."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Escala las columnas y mira el gradiente\n",
    "\n",
    "El mismo problema, con las columnas puestas en la misma\n",
    "escala."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "y = X[:, 1]\n",
    "u = X[:, 0]\n",
    "uz = (u - u.mean()) / u.std()\n",
    "Az = np.column_stack([np.ones(len(u)), uz])\n",
    "\n",
    "def grad_z(w):\n",
    "    return -2 * (Az.T @ (y - Az @ w)) / len(y)\n",
    "\n",
    "print('sin escalar:', np.round(-2 * (np.column_stack([np.ones(len(u)), u]).T\n",
    "                                     @ (y - np.column_stack([np.ones(len(u)), u])\n",
    "                                        @ np.array([0.0, 0.0]))) / len(y), 2))\n",
    "print('escalado   :', np.round(grad_z(np.array([0.0, 0.0])), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin escalar: [ -1606.77 -18794.46]\n",
    "escalado   : [-1606.77    -5.83]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin escalar las dos componentes se llevaban once veces de diferencia. Con la\n",
    "columna escalada el segundo número se desploma a -5.83, porque ahora está medido\n",
    "en desviaciones y no en unidades sueltas. Los datos son los mismos: lo único que\n",
    "cambió es en qué se miden. Por eso escalar antes de entrenar no es cosmética."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Preguntas frecuentes"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "¿Qué son las derivadas parciales?La derivada respecto de una variable dejando las otras quietas. Si tu error depende de cinco parámetros, hay cinco derivadas parciales, una por parámetro.\n",
    "\n",
    "¿Qué es una derivada parcial, con un ejemplo?Si el error depende del precio y del descuento, la parcial respecto del precio dice cuánto cambia el error al mover el precio un poquito y no tocar el descuento.\n",
    "\n",
    "¿Qué es el gradiente?El vector con todas las derivadas parciales juntas. Apunta hacia donde la función sube más rápido, y por eso entrenar es caminar en contra de él.\n",
    "\n",
    "¿Para qué sirve el gradiente en machine learning?Para saber en qué dirección mover cada parámetro para que el error baje. Es lo único que un modelo sabe hacer mientras entrena."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 14 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/derivadas-parciales-y-el-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
}
