{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Deep learning: la red escrita a mano\n",
    "\n",
    "Cuaderno de practica de la guia **Deep learning desde cero** de Miss Yera.\n",
    "Corre de arriba abajo. No necesitas instalar nada si lo abres en Google Colab.\n",
    "\n",
    "Los datos son ventas de una distribuidora peruana, con los defectos que traen\n",
    "los datos reales: fechas en dos formatos, la ciudad escrita de cuatro maneras,\n",
    "montos como texto, duplicados y tres tipos distintos de dato faltante.\n",
    "\n",
    "Guia completa: https://missyera.com/guias/deep-learning-desde-cero/"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aqui no se importa TensorFlow ni PyTorch. Todo sale de numpy, porque una red que escribes es una red que entiendes.\n",
    "\n",
    "Y el cuaderno empieza por lo incomodo: midiendo cuando **no** usarla."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Sobre datos en tabla, la red pierde\n",
    "\n",
    "Mismos datos y mismo preprocesamiento que el cuaderno de machine learning. Se comparan tres modelos y se mira la columna de entrenamiento con la de prueba."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import pandas as pd\n",
    "from sklearn.compose import ColumnTransformer\n",
    "from sklearn.impute import SimpleImputer\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import roc_auc_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "from sklearn.neural_network import MLPClassifier\n",
    "from sklearn.pipeline import Pipeline\n",
    "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n",
    "\n",
    "URL = \"https://missyera.com/static/datasets/ventas-miss-yera.csv\"\n",
    "\n",
    "def carga_limpia(url):\n",
    "    v = pd.read_csv(url).drop_duplicates()\n",
    "    v[\"ciudad\"] = (v[\"ciudad\"].str.strip().str.lower()\n",
    "                   .str.normalize(\"NFKD\")\n",
    "                   .str.encode(\"ascii\", \"ignore\").str.decode(\"utf-8\"))\n",
    "    v[\"monto\"] = pd.to_numeric(v[\"monto\"].str.replace(\",\", \".\"))\n",
    "    for col in [\"fecha\", \"fecha_ultima_compra\"]:\n",
    "        f = pd.to_datetime(v[col], format=\"%Y-%m-%d\", errors=\"coerce\")\n",
    "        falta = f.isna() & v[col].notna()\n",
    "        f[falta] = pd.to_datetime(v.loc[falta, col], format=\"%d/%m/%Y\", errors=\"coerce\")\n",
    "        v[col] = f\n",
    "    return v\n",
    "\n",
    "def prepara(v):\n",
    "    v = v.sort_values([\"cliente_id\", \"fecha\"]).copy()\n",
    "    v[\"sin_compra_previa\"] = v[\"fecha_ultima_compra\"].isna().astype(int)\n",
    "    v[\"sin_descuento\"] = v[\"descuento\"].isna().astype(int)\n",
    "    v[\"sin_satisfaccion\"] = v[\"satisfaccion\"].isna().astype(int)\n",
    "    v[\"precio_unitario\"] = v[\"monto\"] / v[\"unidades\"]\n",
    "    v[\"visita_numero\"] = v.groupby(\"cliente_id\").cumcount() + 1\n",
    "    return v\n",
    "\n",
    "NUMERICAS = [\"unidades\", \"monto\", \"descuento\", \"satisfaccion\", \"precio_unitario\",\n",
    "             \"sin_compra_previa\", \"sin_descuento\", \"sin_satisfaccion\", \"visita_numero\"]\n",
    "CATEGORICAS = [\"ciudad\", \"segmento\", \"canal\", \"categoria\"]\n",
    "\n",
    "def arma(modelo):\n",
    "    return Pipeline([\n",
    "        (\"pre\", ColumnTransformer([\n",
    "            (\"num\", Pipeline([(\"r\", SimpleImputer(strategy=\"median\")),\n",
    "                              (\"e\", StandardScaler())]), NUMERICAS),\n",
    "            (\"cat\", Pipeline([(\"r\", SimpleImputer(strategy=\"most_frequent\")),\n",
    "                              (\"c\", OneHotEncoder(handle_unknown=\"ignore\"))]), CATEGORICAS),\n",
    "        ])),\n",
    "        (\"mod\", modelo),\n",
    "    ])\n",
    "\n",
    "datos = prepara(carga_limpia(URL))\n",
    "X = datos[NUMERICAS + CATEGORICAS]\n",
    "y = datos[\"compro\"]\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(X, y, test_size=0.25,\n",
    "                                          random_state=42, stratify=y)\n",
    "print(\"filas:\", len(datos), \" columnas que entran:\", X.shape[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "competidores = [\n",
    "    (\"regresion logistica\", LogisticRegression(max_iter=1000, random_state=42)),\n",
    "    (\"red de 16\", MLPClassifier(hidden_layer_sizes=(16,), max_iter=300, random_state=42)),\n",
    "    (\"red de 64 y 32\", MLPClassifier(hidden_layer_sizes=(64, 32), max_iter=300,\n",
    "                                     random_state=42)),\n",
    "]\n",
    "\n",
    "for nombre, modelo in competidores:\n",
    "    m = arma(modelo).fit(X_tr, y_tr)\n",
    "    entrena = roc_auc_score(y_tr, m.predict_proba(X_tr)[:, 1])\n",
    "    prueba = roc_auc_score(y_te, m.predict_proba(X_te)[:, 1])\n",
    "    print(f\"{nombre:20} entrena={entrena:.4f}  prueba={prueba:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres cosas en esa salida.\n",
    "\n",
    "**La red pierde**, y no por poco. **Cuanto mas grande, peor**: la de dos capas tiene mas de tres mil pesos que ajustar y 2.250 filas para hacerlo. Y mira la columna de entrenamiento de la red grande: memorizo las filas enteras, por eso en prueba se hunde.\n",
    "\n",
    "Una red gana cuando el dato tiene **forma**: pixeles vecinos, palabras en orden, sonido. En una tabla puedes intercambiar dos columnas y no se pierde informacion; en una foto, si intercambias dos pixeles, la rompes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. La neurona son tres cosas\n",
    "\n",
    "Multiplica cada entrada por un peso, suma con un sesgo, y aplasta el resultado. No hay nada mas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def sigmoide(z):\n",
    "    return 1 / (1 + np.exp(-z))\n",
    "\n",
    "entradas = np.array([2.0, -0.5, 1.0])      # tres cosas que sabemos del cliente\n",
    "pesos    = np.array([0.8, -0.3, 0.2])      # cuanto importa cada una\n",
    "sesgo    = -0.1\n",
    "\n",
    "z = entradas @ pesos + sesgo\n",
    "print(\"la suma da   :\", round(z, 4))\n",
    "print(\"aplastada da :\", round(sigmoide(z), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la comprobacion que quita el misterio: la regresion logistica de arriba **es** una neurona. Le sacamos los pesos y hacemos la cuenta a mano."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "logistica = arma(LogisticRegression(max_iter=1000, random_state=42)).fit(X_tr, y_tr)\n",
    "\n",
    "W = logistica.named_steps[\"mod\"].coef_[0]\n",
    "B = logistica.named_steps[\"mod\"].intercept_[0]\n",
    "T = logistica.named_steps[\"pre\"].transform(X_te)\n",
    "\n",
    "a_mano   = sigmoide(T @ W + B)\n",
    "libreria = logistica.predict_proba(X_te)[:, 1]\n",
    "\n",
    "print(\"pesos:\", W.shape, \"  sesgo:\", round(B, 4))\n",
    "print(\"diferencia maxima entre las dos:\", float(np.abs(a_mano - libreria).max()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. El XOR, que paro el campo veinte anos\n",
    "\n",
    "Tres problemas de cuatro filas. Los tres suenan igual de razonables y solo dos se pueden aprender con una neurona."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "X4 = np.array([[0., 0.], [0., 1.], [1., 0.], [1., 1.]])\n",
    "Y_AND = np.array([0., 0., 0., 1.])     # los dos\n",
    "Y_OR  = np.array([0., 1., 1., 1.])     # alguno de los dos\n",
    "Y_XOR = np.array([0., 1., 1., 0.])     # uno pero no los dos\n",
    "\n",
    "def una_neurona(objetivo, vueltas=5000, paso=0.5):\n",
    "    rng = np.random.default_rng(0)\n",
    "    w, b = rng.normal(0, 0.5, 2), 0.0\n",
    "    for _ in range(vueltas):\n",
    "        error = sigmoide(X4 @ w + b) - objetivo\n",
    "        w -= paso * X4.T @ error / len(X4)\n",
    "        b -= paso * error.mean()\n",
    "    return sigmoide(X4 @ w + b)\n",
    "\n",
    "for nombre, objetivo in [(\"AND\", Y_AND), (\"OR\", Y_OR), (\"XOR\", Y_XOR)]:\n",
    "    p = una_neurona(objetivo)\n",
    "    print(f\"{nombre:4} {np.round(p, 4)}  acierta {int(((p >= 0.5) == objetivo).sum())} de 4\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El XOR devuelve 0,5 en los cuatro casos, que es la forma que tiene una neurona de decir \"no tengo ni idea\". Y no le faltaron vueltas: puedes dejarla un millon.\n",
    "\n",
    "Una neurona traza una recta. Dibuja los cuatro puntos del XOR: los que valen 1 estan en diagonal, y no hay recta que deje esas dos de un lado y las otras dos del otro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Dos capas, escritas enteras\n",
    "\n",
    "Veinte lineas de numpy: hacia adelante, hacia atras y corregir. Nada mas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def red_dos_capas(semilla, ocultas=4, vueltas=5000, paso=0.5):\n",
    "    rng = np.random.default_rng(semilla)\n",
    "    W1 = rng.normal(0, 1, (2, ocultas)); b1 = np.zeros(ocultas)\n",
    "    W2 = rng.normal(0, 1, (ocultas, 1)); b2 = np.zeros(1)\n",
    "    objetivo = Y_XOR.reshape(-1, 1)\n",
    "\n",
    "    for _ in range(vueltas):\n",
    "        h = np.tanh(X4 @ W1 + b1)                  # hacia adelante\n",
    "        p = sigmoide(h @ W2 + b2)\n",
    "\n",
    "        d2 = (p - objetivo) / len(X4)              # hacia atras\n",
    "        dW2, db2 = h.T @ d2, d2.sum(axis=0)\n",
    "        d1 = (d2 @ W2.T) * (1 - h ** 2)\n",
    "        dW1, db1 = X4.T @ d1, d1.sum(axis=0)\n",
    "\n",
    "        W2 -= paso * dW2; b2 -= paso * db2         # corregir\n",
    "        W1 -= paso * dW1; b1 -= paso * db1\n",
    "\n",
    "    perdida = -np.mean(objetivo * np.log(p) + (1 - objetivo) * np.log(1 - p))\n",
    "    return p.ravel(), float(perdida)\n",
    "\n",
    "p, perdida = red_dos_capas(semilla=1)\n",
    "print(\"la red dice  :\", np.round(p, 4))\n",
    "print(\"lo que queria:\", Y_XOR)\n",
    "print(\"perdida      :\", round(perdida, 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con cuatro neuronas en medio, resuelto. Lo que hicieron esas cuatro es **doblar el espacio** hasta que los cuatro puntos si se puedan separar con una recta.\n",
    "\n",
    "Y ahora lo que casi nadie cuenta: ese resultado salio con la semilla 1. Prueba diez."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for semilla in range(10):\n",
    "    p, perdida = red_dos_capas(semilla)\n",
    "    aciertos = int(((p >= 0.5).astype(int) == Y_XOR).sum())\n",
    "    print(f\"semilla {semilla}: perdida {perdida:.4f}  acierta {aciertos}/4\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No siempre converge. La arquitectura **puede** representar el XOR, y aun asi el entrenamiento a veces no lo encuentra.\n",
    "\n",
    "Poder representar algo y llegar a encontrarlo son dos cosas distintas, y esa distincion es medio deep learning. Cambia `ocultas=4` por `ocultas=2` y vuelve a correr la celda: se cae mucho mas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. La convolucion, y por que existe todo esto\n",
    "\n",
    "Cambiamos de tipo de dato. Digitos escritos a mano, 8 por 8 pixeles, que vienen dentro de scikit-learn."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_digits\n",
    "from sklearn.metrics import accuracy_score\n",
    "\n",
    "digitos = load_digits()\n",
    "imagenes = digitos.images / 16.0\n",
    "etiquetas = digitos.target\n",
    "\n",
    "print(\"tenemos\", imagenes.shape[0], \"imagenes de\", imagenes.shape[1], \"por\", imagenes.shape[2])\n",
    "print(\"la primera es un\", etiquetas[0], \"y por dentro es esto:\")\n",
    "print((imagenes[0] * 16).astype(int))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahi esta la propiedad que lo cambia todo: en esa matriz **la posicion significa algo**. Un filtro es una matriz chiquita que se apoya en cada esquina, multiplica, suma, y se corre una posicion."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def convolucion(imagen, filtro):\n",
    "    alto, ancho = filtro.shape\n",
    "    salida = np.zeros((imagen.shape[0] - alto + 1, imagen.shape[1] - ancho + 1))\n",
    "    for i in range(salida.shape[0]):\n",
    "        for j in range(salida.shape[1]):\n",
    "            salida[i, j] = (imagen[i:i + alto, j:j + ancho] * filtro).sum()\n",
    "    return salida\n",
    "\n",
    "VERTICAL = np.array([[-1., 0, 1], [-1., 0, 1], [-1., 0, 1]])\n",
    "HORIZONTAL = VERTICAL.T\n",
    "\n",
    "print(\"el filtro vertical sobre el primer digito:\")\n",
    "print(np.round(convolucion(imagenes[0], VERTICAL), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin que nadie le explicara nada: la primera columna sale positiva y la ultima negativa, o sea que **encontro los dos bordes verticales del cero**. Con nueve numeros y una resta.\n",
    "\n",
    "Y una red convolucional no escribe esos nueve numeros: los aprende, con el mismo descenso de gradiente que cualquier otro peso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. La prueba de verdad: mover la imagen\n",
    "\n",
    "Aqui se ve para que existe la convolucion. Primero los dos modelos, y despues desplazamos los digitos un pixel."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def agrupa(mapa):\n",
    "    return np.array([mapa[i:i + 2, j:j + 2].max()\n",
    "                     for i in range(0, 6, 2) for j in range(0, 6, 2)])\n",
    "\n",
    "def rasgos(imgs):\n",
    "    return np.array([np.concatenate([agrupa(np.abs(convolucion(im, VERTICAL))),\n",
    "                                     agrupa(np.abs(convolucion(im, HORIZONTAL)))])\n",
    "                     for im in imgs])\n",
    "\n",
    "i_tr, i_te = train_test_split(np.arange(len(imagenes)), test_size=0.25,\n",
    "                              random_state=42, stratify=etiquetas)\n",
    "I_tr, I_te = imagenes[i_tr], imagenes[i_te]\n",
    "d_tr, d_te = etiquetas[i_tr], etiquetas[i_te]\n",
    "\n",
    "m_pix = LogisticRegression(max_iter=2000).fit(I_tr.reshape(len(I_tr), -1), d_tr)\n",
    "m_conv = LogisticRegression(max_iter=2000).fit(rasgos(I_tr), d_tr)\n",
    "\n",
    "print(\"columnas con pixeles    :\", I_tr.reshape(len(I_tr), -1).shape[1])\n",
    "print(\"columnas con convolucion:\", rasgos(I_tr[:1]).shape[1])"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def desplaza(imgs, dx, dy):\n",
    "    return np.array([np.roll(np.roll(im, dy, axis=0), dx, axis=1) for im in imgs])\n",
    "\n",
    "print(f'{\"desplazamiento\":16} {\"pixeles\":>9} {\"convolucion\":>12}')\n",
    "for dx, dy in [(0, 0), (1, 0), (0, 1), (1, 1), (2, 0)]:\n",
    "    movidas = desplaza(I_te, dx, dy)\n",
    "    a_pix = accuracy_score(d_te, m_pix.predict(movidas.reshape(len(movidas), -1)))\n",
    "    a_conv = accuracy_score(d_te, m_conv.predict(rasgos(movidas)))\n",
    "    print(f'({dx},{dy}){\"\":11} {a_pix:9.4f} {a_conv:12.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin mover nada, los pixeles ganan: 64 pixeles llevan mas informacion que 18 rasgos.\n",
    "\n",
    "Con un pixel de desplazamiento, el modelo de pixeles se desploma y la convolucion aguanta bastante mas. Para el modelo de pixeles, la columna \"pixel numero 27\" paso a contener lo que antes estaba en la 26, y todo lo que aprendio sobre esa columna dejo de valer. Para la convolucion, el borde vertical **sigue estando ahi**, un poquito mas alla. El filtro no pregunta donde: pregunta si.\n",
    "\n",
    "Y la parte honesta: con dos pixeles las dos son inservibles. Dos filtros escritos a mano son un juguete."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. La atencion, en seis lineas\n",
    "\n",
    "Cada palabra mira a todas las demas y se queda con una mezcla de lo que le sirve. De aqui sale ChatGPT."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def softmax(z):\n",
    "    z = z - z.max(axis=-1, keepdims=True)\n",
    "    e = np.exp(z)\n",
    "    return e / e.sum(axis=-1, keepdims=True)\n",
    "\n",
    "frase = [\"la\", \"bodega\", \"de\", \"lima\", \"no\", \"pago\"]\n",
    "rng = np.random.default_rng(7)\n",
    "d = 8\n",
    "\n",
    "E = rng.normal(0, 1, (len(frase), d))     # los embeddings\n",
    "Wq = rng.normal(0, 0.5, (d, d))\n",
    "Wk = rng.normal(0, 0.5, (d, d))\n",
    "Wv = rng.normal(0, 0.5, (d, d))\n",
    "\n",
    "Q, K, V = E @ Wq, E @ Wk, E @ Wv          # consultas, claves, valores\n",
    "puntajes = Q @ K.T / np.sqrt(d)           # quien le interesa a quien\n",
    "A = softmax(puntajes)                     # convertidos en pesos que suman 1\n",
    "salida = A @ V                            # la mezcla\n",
    "\n",
    "print(\"matriz de atencion:\", A.shape, \" salida:\", salida.shape)\n",
    "print(\"cada fila suma:\", np.round(A.sum(axis=1), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Eso es todo. Y ojo con un detalle que parece decorativo: **dividir por la raiz de d**. Sin eso los puntajes salen enormes, el softmax se satura y los pesos quedan en 1 y 0.\n",
    "\n",
    "Aviso honesto: las matrices Wq, Wk y Wv son aleatorias, asi que los numeros de esa matriz de atencion no significan nada. Lo real aqui es la maquinaria."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la mascara, que es lo que hace que un modelo **escriba** en vez de solo leer. Si tiene que predecir la palabra siguiente, no puede dejar que cada palabra mire a las que vienen despues: seria copiarse la respuesta."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mascara = np.triu(np.ones((len(frase), len(frase))), 1) * -1e9\n",
    "A_causal = softmax(puntajes + mascara)\n",
    "\n",
    "print(\"        \" + \"  \".join(f\"{p:>8}\" for p in frase))\n",
    "for p, fila in zip(frase, A_causal):\n",
    "    print(f\"{p:8}\" + \"  \".join(f\"{x:8.3f}\" for x in fila))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la forma de triangulo. La primera palabra solo puede mirarse a si misma; la ultima las ve todas. Ese triangulo es la diferencia entre un modelo que lee y uno que escribe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Lo que la atencion arregla, medido\n",
    "\n",
    "La misma palabra en dos frases que dicen lo contrario. Sin atencion, `llego` es el mismo vector siempre."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vocabulario = {\"el\": 0, \"pedido\": 1, \"llego\": 2, \"completo\": 3, \"no\": 4}\n",
    "tabla = rng.normal(0, 1, (5, d))\n",
    "\n",
    "def atiende(palabras):\n",
    "    Emb = tabla[[vocabulario[p] for p in palabras]]\n",
    "    Qa, Ka, Va = Emb @ Wq, Emb @ Wk, Emb @ Wv\n",
    "    return softmax(Qa @ Ka.T / np.sqrt(d)) @ Va\n",
    "\n",
    "def coseno(a, b):\n",
    "    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b)))\n",
    "\n",
    "f1 = [\"el\", \"pedido\", \"llego\", \"completo\"]\n",
    "f2 = [\"el\", \"pedido\", \"no\", \"llego\"]\n",
    "\n",
    "fijo = coseno(tabla[vocabulario[\"llego\"]], tabla[vocabulario[\"llego\"]])\n",
    "tras = coseno(atiende(f1)[f1.index(\"llego\")], atiende(f2)[f2.index(\"llego\")])\n",
    "print(\"el vector fijo de 'llego' en las dos frases:\", round(fijo, 4))\n",
    "print(\"despues de la atencion                     :\", round(tras, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahi esta. Los dos `llego` pasaron a ser vectores distintos, porque cada uno se mezclo con las palabras que lo rodean. Eso se llama **representacion contextual** y es literalmente el motivo de que los modelos de lenguaje de hoy funcionen."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Y ahora al reves: generar un digito que no existe\n",
    "\n",
    "Todo lo anterior fue mirar algo y ponerle nombre. Esta celda hace lo contrario: le pides un 7 y te dibuja un 7 que nadie escribio.\n",
    "\n",
    "Es un modelo de difusion, la misma familia que los modelos que generan imagenes. La idea: aprender a **quitar** ruido, y despues hacerlo 60 veces seguidas partiendo de ruido puro. Tarda medio minuto."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Xg = (digitos.data / 16.0) * 2 - 1        # de 0..16 a -1..1\n",
    "yg = digitos.target\n",
    "T = 60\n",
    "betas = np.linspace(1e-4, 0.10, T)\n",
    "alfas = 1 - betas\n",
    "abarra = np.cumprod(alfas)\n",
    "\n",
    "FREQ = np.array([1., 2., 4., 8.])\n",
    "P, Hn = 64, 256\n",
    "ENTRADA = P + 8 + 10                      # la imagen, el reloj y el digito pedido\n",
    "\n",
    "def entrada(xt, t, cual):\n",
    "    ang = (t[:, None] / T) * FREQ * np.pi\n",
    "    reloj = np.concatenate([np.sin(ang), np.cos(ang)], axis=1)\n",
    "    return np.concatenate([xt, reloj, np.eye(10)[cual]], axis=1)\n",
    "\n",
    "def adivina(p, xt, t, cual):\n",
    "    z = entrada(xt, t, cual)\n",
    "    h1 = np.maximum(0, z @ p[\"W1\"] + p[\"b1\"])\n",
    "    h2 = np.maximum(0, h1 @ p[\"W2\"] + p[\"b2\"])\n",
    "    return h2 @ p[\"W3\"] + p[\"b3\"], (z, h1, h2)\n",
    "\n",
    "def entrena_difusion(vueltas=6000, lote=256, paso=0.002, semilla=0):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    p = dict(W1=r.normal(0, np.sqrt(2 / ENTRADA), (ENTRADA, Hn)), b1=np.zeros(Hn),\n",
    "             W2=r.normal(0, np.sqrt(2 / Hn), (Hn, Hn)), b2=np.zeros(Hn),\n",
    "             W3=r.normal(0, np.sqrt(2 / Hn), (Hn, P)), b3=np.zeros(P))\n",
    "    m = {k: np.zeros_like(v) for k, v in p.items()}\n",
    "    v = {k: np.zeros_like(w) for k, w in p.items()}\n",
    "    for it in range(1, vueltas + 1):\n",
    "        i = r.integers(0, len(Xg), lote)\n",
    "        t = r.integers(0, T, lote)\n",
    "        ruido = r.normal(size=(lote, P))\n",
    "        a = abarra[t][:, None]\n",
    "        xt = np.sqrt(a) * Xg[i] + np.sqrt(1 - a) * ruido\n",
    "        pred, (z, h1, h2) = adivina(p, xt, t, yg[i])\n",
    "        d3 = 2 * (pred - ruido) / lote\n",
    "        g = {\"W3\": h2.T @ d3, \"b3\": d3.sum(0)}\n",
    "        d2 = (d3 @ p[\"W3\"].T) * (h2 > 0)\n",
    "        g[\"W2\"] = h1.T @ d2; g[\"b2\"] = d2.sum(0)\n",
    "        d1 = (d2 @ p[\"W2\"].T) * (h1 > 0)\n",
    "        g[\"W1\"] = z.T @ d1; g[\"b1\"] = d1.sum(0)\n",
    "        for k in p:\n",
    "            m[k] = 0.9 * m[k] + 0.1 * g[k]\n",
    "            v[k] = 0.999 * v[k] + 0.001 * g[k] ** 2\n",
    "            p[k] -= paso * (m[k] / (1 - 0.9 ** it)) / (np.sqrt(v[k] / (1 - 0.999 ** it)) + 1e-8)\n",
    "    return p\n",
    "\n",
    "modelo_dif = entrena_difusion()\n",
    "print(\"entrenado\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def genera(p, pedido, semilla=7):\n",
    "    r = np.random.default_rng(semilla)\n",
    "    x = r.normal(size=(len(pedido), P))            # se arranca de ruido puro\n",
    "    for t in range(T - 1, -1, -1):\n",
    "        ruido, _ = adivina(p, x, np.full(len(pedido), t), pedido)\n",
    "        x0 = ((x - np.sqrt(1 - abarra[t]) * ruido) / np.sqrt(abarra[t])).clip(-1, 1)\n",
    "        antes = abarra[t - 1] if t > 0 else 1.0\n",
    "        media = (np.sqrt(antes) * betas[t] / (1 - abarra[t])) * x0 \\\n",
    "            + (np.sqrt(alfas[t]) * (1 - antes) / (1 - abarra[t])) * x\n",
    "        if t > 0:\n",
    "            x = media + np.sqrt(betas[t] * (1 - antes) / (1 - abarra[t])) * r.normal(size=x.shape)\n",
    "        else:\n",
    "            x = media\n",
    "    return x\n",
    "\n",
    "pedido = np.repeat(np.arange(10), 30)              # 30 de cada digito\n",
    "inventados = genera(modelo_dif, pedido)\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "fig, ejes = plt.subplots(2, 5, figsize=(8, 3.5))\n",
    "for k, eje in enumerate(ejes.ravel()):\n",
    "    eje.imshow(inventados[k * 30].reshape(8, 8), cmap=\"gray_r\")\n",
    "    eje.set_title(f\"le pedi un {k}\", fontsize=8)\n",
    "    eje.axis(\"off\")\n",
    "plt.tight_layout(); plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ninguno de esos dibujos existia. La red arranco de 64 numeros al azar y los fue limpiando.\n",
    "\n",
    "Y como tu ojo no es una medicion, ponle un juez: una regresion logistica entrenada con digitos de verdad, a ver si les pone el numero que pediste."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.neighbors import NearestNeighbors\n",
    "\n",
    "juez = LogisticRegression(max_iter=2000).fit(Xg, yg)\n",
    "print(\"le pone al inventado el digito que pedi:\",\n",
    "      round(float((juez.predict(inventados) == pedido).mean()), 4))\n",
    "\n",
    "vecino = NearestNeighbors(n_neighbors=2).fit(Xg)\n",
    "print(\"distancia del inventado al real mas parecido:\",\n",
    "      round(float(vecino.kneighbors(inventados, n_neighbors=1)[0][:, 0].mean()), 4))\n",
    "print(\"distancia de un real al real mas parecido   :\",\n",
    "      round(float(vecino.kneighbors(Xg, n_neighbors=2)[0][:, 1].mean()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las dos mediciones que hay que hacerle siempre a un modelo generativo.\n",
    "\n",
    "La primera dice si se le entiende. La segunda dice si **inventa o copia**: si los inventados estuvieran a distancia casi cero de los reales, seria un fotocopiadora carisima. Aqui estan mas lejos entre si que los propios digitos reales, asi que no copia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas\n",
    "\n",
    "Una neurona son tres operaciones. Una capa oculta dobla el espacio. Una convolucion comparte los mismos nueve numeros en todas las posiciones. Una atencion deja que cada palabra se arme segun su frase. Todo lo demas es apilar eso y entrenar mucho.\n",
    "\n",
    "Y la que hay que recordar antes de proponer una red en un proyecto: **sobre datos en tabla, casi siempre pierde**. La primera celda de este cuaderno esta puesta ahi a proposito."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}