{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Convertir palabras en números que signifiquen algo\n",
    "\n",
    "One-hot dice que bodega y minimarket no se parecen en nada. Los embeddings arreglan eso, y traen su propio problema.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 13 de **Deep learning desde cero**, 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/deep-learning-desde-cero/texto-y-embeddings/\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": [
    "En el capítulo 11 el dato tenía forma de rejilla. Aquí tiene forma de\n",
    "**secuencia**, y antes de meterla en cualquier red hay un problema\n",
    "que resolver: una red multiplica números y \"bodega\" no se multiplica 🔤\n",
    "\n",
    "Antes de seguir, piénsalo tú: **¿cómo le explicarías a una máquina que bodega y minimarket se parecen, y que arroz no tiene nada que ver con ninguna de las dos?** 🔤"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que veníamos haciendo, y por qué no basta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En todo el libro de machine learning convertimos las palabras con\n",
    "`OneHotEncoder`: una columna por valor, un 1 en la que toca y ceros en\n",
    "las demás."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "\n",
    "palabras = ['bodega', 'minimarket', 'mayorista', 'arroz', 'aceite']\n",
    "posicion = {p: i for i, p in enumerate(palabras)}\n",
    "\n",
    "def one_hot(p):\n",
    "    v = np.zeros(len(palabras))\n",
    "    v[posicion[p]] = 1\n",
    "    return v\n",
    "\n",
    "print('bodega    :', one_hot('bodega'))\n",
    "print('minimarket:', one_hot('minimarket'))\n",
    "print()\n",
    "print('parecido bodega y minimarket:', float(one_hot('bodega') @ one_hot('minimarket')))\n",
    "print('parecido bodega y arroz     :', float(one_hot('bodega') @ one_hot('arroz')))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los dos parecidos dan **0,0**. Exactamente el mismo número.\n",
    "\n",
    "O sea que para el modelo, una bodega se parece a un minimarket lo mismo que se\n",
    "parece a un saco de arroz: nada. Cada palabra es una isla 🏝️\n",
    "\n",
    "Con cuatro segmentos eso se aguanta, porque el modelo aprende cada uno por\n",
    "separado y hay datos de sobra. Con un vocabulario de 50.000 palabras de texto es\n",
    "imposible: no vas a tener ejemplos suficientes de cada una, y lo que aprendas\n",
    "sobre \"retraso\" no le servirá de nada a \"tardanza\"."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La idea: mirar con quién anda"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un **embedding** es una lista corta de números que representa una\n",
    "palabra. Y la idea para construirlo es de 1957, mucho antes que las redes: una\n",
    "palabra se define por las palabras que la rodean.\n",
    "\n",
    "Vamos con un puñado de comentarios de clientes. Los escribí yo para este\n",
    "capítulo, con la forma de los que llegan a una distribuidora:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COMENTARIOS = [\n",
    "    'el pedido llego completo y a tiempo',\n",
    "    'el pedido llego incompleto y con retraso',\n",
    "    'la bodega pidio arroz y azucar',\n",
    "    'el minimarket pidio aceite y azucar',\n",
    "    'el mayorista pidio arroz en cantidad',\n",
    "    'el pedido de la bodega llego tarde',\n",
    "    'el pedido del minimarket llego tarde',\n",
    "    'la bodega reclamo por el retraso del pedido',\n",
    "    'el minimarket reclamo por el aceite roto',\n",
    "    'el mayorista reclamo por la factura',\n",
    "    'llego todo completo sin reclamo',\n",
    "    'el arroz llego en buen estado',\n",
    "    'el aceite llego en buen estado',\n",
    "    'el azucar llego roto y con retraso',\n",
    "    'la bodega pago la factura a tiempo',\n",
    "    'el minimarket pago la factura con retraso',\n",
    "    'el mayorista pago la factura a tiempo',\n",
    "    'el vendedor visito la bodega el lunes',\n",
    "    'el vendedor visito el minimarket el martes',\n",
    "    'el vendedor visito al mayorista el lunes',\n",
    "    'la bodega compro arroz el lunes',\n",
    "    'el minimarket compro aceite el martes',\n",
    "    'el mayorista compro arroz y aceite',\n",
    "    'nadie visito la bodega esta semana',\n",
    "    'el pedido de arroz llego completo',\n",
    "    'el pedido de aceite llego incompleto',\n",
    "]\n",
    "\n",
    "vocabulario = sorted({p for f in COMENTARIOS for p in f.split()})\n",
    "lugar = {p: i for i, p in enumerate(vocabulario)}\n",
    "print('frases:', len(COMENTARIOS), ' palabras distintas:', len(vocabulario))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contar quién aparece al lado de quién"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "V = len(vocabulario)\n",
    "juntas = np.zeros((V, V))\n",
    "VENTANA = 2                      # dos palabras a cada lado\n",
    "\n",
    "for f in COMENTARIOS:\n",
    "    ps = f.split()\n",
    "    for i, p in enumerate(ps):\n",
    "        for j in range(max(0, i - VENTANA), min(len(ps), i + VENTANA + 1)):\n",
    "            if i != j:\n",
    "                juntas[lugar[p], lugar[ps[j]]] += 1\n",
    "\n",
    "for p in ['bodega', 'arroz']:\n",
    "    vecinas = np.argsort(-juntas[lugar[p]])[:4]\n",
    "    print(f'{p:10} aparece más al lado de:',\n",
    "          ', '.join(f'{vocabulario[i]} ({int(juntas[lugar[p], i])})' for i in vecinas))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa matriz es todo lo que sabemos del idioma: quién anda con quién. No hay\n",
    "gramática, ni diccionario, ni nadie que haya explicado qué es una bodega 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## De la matriz a los vectores cortos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La matriz tiene 41 por 41. Lo que queremos son listas de cinco números, y la\n",
    "forma clásica de comprimir una matriz conservando lo que importa es la\n",
    "**descomposición en valores singulares**, que numpy trae hecha:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "U, S, _ = np.linalg.svd(np.log1p(juntas))     # log1p suaviza las cuentas grandes\n",
    "E = U[:, :5] * S[:5]                          # cinco números por palabra\n",
    "\n",
    "def parecido(a, b):\n",
    "    x, y = E[lugar[a]], E[lugar[b]]\n",
    "    return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12))\n",
    "\n",
    "print('bodega    :', np.round(E[lugar['bodega']], 3))\n",
    "print('arroz     :', np.round(E[lugar['arroz']], 3))\n",
    "print()\n",
    "for a, b in [('bodega', 'minimarket'), ('arroz', 'aceite'), ('bodega', 'arroz'),\n",
    "             ('lunes', 'martes'), ('completo', 'incompleto')]:\n",
    "    print(f'{a:12} y {b:12}: {parecido(a, b):+.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está lo que queríamos 🎉\n",
    "\n",
    "**bodega y minimarket dan 0,849**, cuando con one-hot daban 0,0.\n",
    "Y arroz y aceite 0,945, y lunes y martes 0,968.\n",
    "\n",
    "Y lo importante: **bodega y arroz dan 0,300**, o sea bajo. El\n",
    "método distinguió solo que hay palabras de \"tipo de cliente\" y palabras de\n",
    "\"producto\", sin que nadie se lo dijera. Solo mirando con quién andan.\n",
    "\n",
    "Ese número, el `parecido`, es la **similitud coseno**:\n",
    "mide si dos vectores apuntan en la misma dirección, sin importar su tamaño. Es la\n",
    "medida estándar para embeddings y va de -1 a 1."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora el problema, que es famoso"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vuelve a mirar la última línea: **completo e incompleto dan\n",
    "0,975**. Casi idénticos.\n",
    "\n",
    "Y no es un fallo de mi corpus ni de mi código. Es que *completo* e\n",
    "*incompleto* aparecen exactamente en los mismos sitios: después de\n",
    "\"llego\", antes de \"y\". Su compañía es la misma, así que su vector es el mismo.\n",
    "\n",
    "Este es el agujero conocido de esta idea: **los embeddings de\n",
    "coocurrencia no distinguen antónimos**. Bueno y malo, subir y bajar, con y\n",
    "sin. Todos salen parecidísimos, y si construyes un detector de quejas encima,\n",
    "va a confundir el elogio con el reclamo 😬\n",
    "\n",
    "La solución no llegó hasta el capítulo 16: en vez de un vector fijo por\n",
    "palabra, un vector **que cambia según la frase**. Eso es la atención,\n",
    "y es la idea que hizo posible todo lo que usas hoy."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Buscar lo más parecido"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for p in ['bodega', 'arroz', 'retraso']:\n",
    "    puntajes = sorted(((parecido(p, w), w) for w in vocabulario if w != p),\n",
    "                      reverse=True)[:4]\n",
    "    print(f'{p:9} se parece a: ' +\n",
    "          ', '.join(f'{w} ({s:+.2f})' for s, w in puntajes))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para `bodega` saca mayorista, nadie, minimarket y vendedor. Tres de\n",
    "las cuatro son exactamente lo que esperarías 👏\n",
    "\n",
    "Y la cuarta explica el método: `nadie` aparece porque escribí\n",
    "\"nadie visito la bodega esta semana\", y en esa frase *nadie* ocupa el\n",
    "mismo hueco que ocuparía un vendedor. El método no sabe qué significa nadie: sabe\n",
    "en qué hueco cae.\n",
    "\n",
    "Con `arroz` pasa lo mismo: sale aceite (perfecto) y también\n",
    "`del` y `tarde`, que son ruido de tener solo 26 frases. Los\n",
    "embeddings de verdad se entrenan con miles de millones de palabras, y ahí el\n",
    "ruido se diluye 📚"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y si en vez de palabras fueran fotos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acá está el salto que explica la palabra **multimodal**, que vas\n",
    "a oír mucho y que suena a más de lo que es.\n",
    "\n",
    "Un embedding no sabe que está hecho de palabras. Es una lista de números y un\n",
    "parecido que se mide con un ángulo. Nada de eso pide que el origen sea texto.\n",
    "**Si consigues meter una foto en el mismo espacio, la foto y la palabra se\n",
    "pueden comparar entre ellas.**\n",
    "\n",
    "Vamos con tres productos y unas fotos de mentira, que acá son rejillas de 6\n",
    "por 6. Lo que hace falta es aprender a pasar de los 36 números de la foto a los 5\n",
    "números de la palabra, y para eso basta un ajuste por mínimos cuadrados:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rng = np.random.default_rng(0)\n",
    "\n",
    "def foto(fila, columna):\n",
    "    im = np.zeros((6, 6))\n",
    "    im[fila] = 1.0\n",
    "    im[:, columna] = 1.0\n",
    "    return im\n",
    "\n",
    "FOTOS = {'arroz': foto(1, 1), 'aceite': foto(3, 3), 'azucar': foto(4, 0)}\n",
    "Xi = np.array([FOTOS[p].ravel() for p in FOTOS])\n",
    "Yt = np.array([E[lugar[p]] for p in FOTOS])\n",
    "W, *_ = np.linalg.lstsq(Xi, Yt, rcond=None)\n",
    "print('cada foto son', Xi.shape[1], 'numeros y cada palabra', Yt.shape[1])\n",
    "\n",
    "def coseno(x, y):\n",
    "    return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12))\n",
    "\n",
    "for p in FOTOS:\n",
    "    sucia = (FOTOS[p] + rng.normal(0, .15, (6, 6))).ravel()\n",
    "    v = sucia @ W\n",
    "    print(f'foto de {p:8}',\n",
    "          sorted(((round(coseno(v, E[lugar[q]]), 3), q) for q in FOTOS), reverse=True))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada foto, y encima ensuciada con ruido, cae más cerca de su propia palabra\n",
    "que de las otras dos. Nadie le enseñó a la foto qué es el arroz: se le enseñó a\n",
    "**aterrizar donde ya estaba la palabra** 🔤\n",
    "\n",
    "Mira los márgenes antes de emocionarte: 1,0 contra 0,943 es ganar por poco.\n",
    "Con tres ejemplos y una transformación lineal no da para más, y eso también es\n",
    "el mecanismo de verdad. Los modelos que hacen esto en serio usan una red por cada\n",
    "lado y millones de pares foto-texto, pero la idea que los sostiene es esta y cabe\n",
    "en veinte líneas.\n",
    "\n",
    "De ahí sale todo lo que llaman multimodal: buscar fotos escribiendo, describir\n",
    "una imagen, encontrar el producto del catálogo con una foto del cliente. No son\n",
    "tres inventos, es el mismo espacio compartido tres veces 📋"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cuántas dimensiones conviene\n",
    "\n",
    "Prueba con 2, 5 y 10 números por palabra."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for dims in [2, 5, 10]:\n",
    "    Ed = U[:, :dims] * S[:dims]\n",
    "\n",
    "    def par(a, b):\n",
    "        x, y = Ed[lugar[a]], Ed[lugar[b]]\n",
    "        return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12))\n",
    "\n",
    "    print(f'{dims:2d} dimensiones: bodega/minimarket {par(\"bodega\", \"minimarket\"):+.3f}  '\n",
    "          f'bodega/arroz {par(\"bodega\", \"arroz\"):+.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    " 2 dimensiones: bodega/minimarket +0.955  bodega/arroz +0.693\n",
    " 5 dimensiones: bodega/minimarket +0.849  bodega/arroz +0.300\n",
    "10 dimensiones: bodega/minimarket +0.686  bodega/arroz +0.346\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 2 dimensiones todo se parece a todo (bodega y arroz dan 0,693). Con 10, la\n",
    "distinción entre tipos es más nítida.\n",
    "\n",
    "Es el mismo compromiso de siempre: pocas dimensiones aplastan las diferencias\n",
    "y muchas se aprenden el ruido. Los embeddings de verdad usan entre 100 y 1.000, y\n",
    "salen de probar 📏"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. La ventana también decide\n",
    "\n",
    "Cuenta la coocurrencia con una ventana de 1 y de 5."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def con_ventana(v):\n",
    "    M = np.zeros((V, V))\n",
    "    for f in COMENTARIOS:\n",
    "        ps = f.split()\n",
    "        for i, p in enumerate(ps):\n",
    "            for j in range(max(0, i - v), min(len(ps), i + v + 1)):\n",
    "                if i != j:\n",
    "                    M[lugar[p], lugar[ps[j]]] += 1\n",
    "    Uu, Ss, _ = np.linalg.svd(np.log1p(M))\n",
    "    Ev = Uu[:, :5] * Ss[:5]\n",
    "\n",
    "    def par(a, b):\n",
    "        x, y = Ev[lugar[a]], Ev[lugar[b]]\n",
    "        return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12))\n",
    "    return par('bodega', 'minimarket'), par('bodega', 'arroz')\n",
    "\n",
    "for v in [1, 2, 5]:\n",
    "    a, b = con_ventana(v)\n",
    "    print(f'ventana {v}: bodega/minimarket {a:+.3f}  bodega/arroz {b:+.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ventana 1: bodega/minimarket +0.609  bodega/arroz +0.433\n",
    "ventana 2: bodega/minimarket +0.849  bodega/arroz +0.300\n",
    "ventana 5: bodega/minimarket +0.823  bodega/arroz +0.781\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lo que hay que mirar es la *distancia* entre las dos columnas, o sea\n",
    "cuánto distingue.\n",
    "\n",
    "Con ventana 5, bodega y arroz suben a 0,781: mirando casi la frase entera,\n",
    "todo aparece con todo y la distinción se pierde. Con ventana 1 tampoco va bien,\n",
    "porque con solo la palabra pegada hay tan pocas cuentas que el resultado es\n",
    "ruidoso (0,609 contra 0,433, casi sin separación).\n",
    "\n",
    "La ventana 2 es la que más separa: 0,849 contra 0,300. **Ventana grande\n",
    "agrupa por tema y ventana chica se queda sin datos**, y el punto bueno\n",
    "está en medio, que es donde suele estar todo 🔍"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Sumar vectores para representar una frase\n",
    "\n",
    "La forma más simple de convertir una frase en números: sumar\n",
    "los de sus palabras."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def vector_de(frase):\n",
    "    return np.mean([E[lugar[p]] for p in frase.split() if p in lugar], axis=0)\n",
    "\n",
    "def parecido_frases(f1, f2):\n",
    "    a, b = vector_de(f1), vector_de(f2)\n",
    "    return float(a @ b / (np.linalg.norm(a) * np.linalg.norm(b) + 1e-12))\n",
    "\n",
    "print('dos quejas parecidas:', round(parecido_frases(\n",
    "    'la bodega reclamo por el retraso', 'el minimarket reclamo por el retraso'), 3))\n",
    "print('queja contra elogio :', round(parecido_frases(\n",
    "    'la bodega reclamo por el retraso', 'llego todo completo sin reclamo'), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "dos quejas parecidas: 0.969\n",
    "queja contra elogio : 0.731\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las dos quejas dan 0,969 y la queja contra el elogio 0,731. Distingue, y\n",
    "mejor de lo que yo esperaba con 26 frases 👏\n",
    "\n",
    "Pero mira el límite que tiene sumar: **se pierde el orden**.\n",
    "\"llego completo\" y \"completo llego\" dan exactamente el mismo vector, porque la\n",
    "media no sabe en qué orden venían. Y \"no llego el pedido\" se parece muchísimo a\n",
    "\"llego el pedido\", que es justo lo contrario.\n",
    "\n",
    "El capítulo 16 arregla también esto 🔀"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. La aritmética famosa, a ver si sale\n",
    "\n",
    "Lo de \"rey menos hombre más mujer da reina\", con nuestras\n",
    "palabras."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "objetivo = E[lugar['bodega']] - E[lugar['arroz']] + E[lugar['aceite']]\n",
    "\n",
    "puntajes = []\n",
    "for w in vocabulario:\n",
    "    x = E[lugar[w]]\n",
    "    puntajes.append((float(objetivo @ x / (np.linalg.norm(objetivo) * np.linalg.norm(x) + 1e-12)), w))\n",
    "\n",
    "for s, w in sorted(puntajes, reverse=True)[:4]:\n",
    "    print(f'  {w:12} {s:+.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "  bodega       +0.971\n",
    "  nadie        +0.897\n",
    "  mayorista    +0.877\n",
    "  minimarket   +0.786\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sale bodega, que es hacer trampa: como arroz y aceite son casi idénticos, se\n",
    "cancelan y queda bodega.\n",
    "\n",
    "La aritmética de vectores es de las cosas más citadas de este campo y también\n",
    "de las más exageradas. Con 26 frases no sale nada, y con corpus enormes sale a\n",
    "veces y con muchos ejemplos elegidos a mano 🎩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Por qué esto no sirve para la tabla de ventas\n",
    "\n",
    "Arma el mismo procedimiento tratando cada fila del CSV como\n",
    "una frase y mira qué sale."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "\n",
    "URL = 'https://missyera.com/static/datasets/ventas-miss-yera.csv'\n",
    "ventas = pd.read_csv(URL).drop_duplicates()\n",
    "ventas['ciudad'] = (ventas['ciudad'].str.strip().str.lower()\n",
    "                    .str.normalize('NFKD')\n",
    "                    .str.encode('ascii', 'ignore').str.decode('utf-8'))\n",
    "\n",
    "filas = [f'{r.segmento} {r.ciudad} {r.canal} {r.categoria}'.lower()\n",
    "         for r in ventas.itertuples()]\n",
    "voc2 = sorted({p for f in filas for p in f.split()})\n",
    "lug2 = {p: i for i, p in enumerate(voc2)}\n",
    "\n",
    "M = np.zeros((len(voc2), len(voc2)))\n",
    "for f in filas:\n",
    "    ps = f.split()\n",
    "    for i, p in enumerate(ps):\n",
    "        for j, q in enumerate(ps):\n",
    "            if i != j:\n",
    "                M[lug2[p], lug2[q]] += 1\n",
    "\n",
    "U2, S2, _ = np.linalg.svd(np.log1p(M))\n",
    "E2 = U2[:, :5] * S2[:5]\n",
    "\n",
    "def par2(a, b):\n",
    "    x, y = E2[lug2[a]], E2[lug2[b]]\n",
    "    return float(x @ y / (np.linalg.norm(x) * np.linalg.norm(y) + 1e-12))\n",
    "\n",
    "for a, b in [('bodega', 'minimarket'), ('bodega', 'lima'), ('lima', 'arequipa')]:\n",
    "    print(f'{a:12} y {b:12}: {par2(a, b):+.3f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "bodega       y minimarket  : +1.000\n",
    "bodega       y lima        : +0.676\n",
    "lima         y arequipa    : +1.000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Bodega y minimarket dan **1,000** clavado, y lima y arequipa\n",
    "también. O sea que dentro de una misma columna, el método no distingue nada.\n",
    "\n",
    "Y bodega con lima da 0,676, que sigue siendo alto. Lo único que aprendió es\n",
    "\"esta palabra es de la misma columna o de otra\", que es exactamente lo que el\n",
    "`OneHotEncoder` ya te da gratis.\n",
    "\n",
    "La razón es que en esta tabla **cada fila tiene una palabra de cada tipo\n",
    "y todas se cruzan con todas**. Bodega aparece con las seis ciudades, con\n",
    "los cuatro canales y con las cinco categorías, igual que mayorista. No hay ningún\n",
    "patrón de compañía que separar.\n",
    "\n",
    "Por eso el libro de machine learning usó `OneHotEncoder` y no\n",
    "embeddings: **los embeddings necesitan un contexto que varíe**, y en\n",
    "una tabla con columnas independientes no lo hay. Es una herramienta de texto y\n",
    "conviene no forzarla 🧰"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Cuántos números ocupa cada cosa\n",
    "\n",
    "Compara la memoria de one-hot contra embeddings para\n",
    "vocabularios reales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for palabras_n in [41, 1_000, 50_000]:\n",
    "    print(f'{palabras_n:7,} palabras: one-hot {palabras_n * palabras_n:14,} números   '\n",
    "          f'embeddings de 100 {palabras_n * 100:12,}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "     41 palabras: one-hot          1,681 números   embeddings de 100        4,100\n",
    "  1,000 palabras: one-hot      1,000,000 números   embeddings de 100      100,000\n",
    " 50,000 palabras: one-hot  2,500,000,000 números   embeddings de 100    5,000,000\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con 50.000 palabras, one-hot necesitaría 2.500 millones de números y los\n",
    "embeddings 5 millones. Quinientas veces menos.\n",
    "\n",
    "Y lo que ahorra no es solo memoria: con one-hot, cada palabra hay que\n",
    "aprenderla por separado. Con embeddings, lo que el modelo aprende sobre \"retraso\"\n",
    "aprovecha para \"tardanza\", porque están cerca 🤝"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El error de la palabra que no está\n",
    "\n",
    "Pídele el vector de una palabra que no salió en ninguna\n",
    "frase."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "E[lugar['huancayo']]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "KeyError: 'huancayo'\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y este error es el problema real de los embeddings clásicos, no una anécdota:\n",
    "**solo saben las palabras que vieron**. Una ciudad nueva, un nombre\n",
    "de producto nuevo, un error de tipeo, y no hay vector.\n",
    "\n",
    "Es la misma avería silenciosa de la ciudad nueva del capítulo 21 del libro de\n",
    "machine learning, y aquí al menos revienta en vez de callarse.\n",
    "\n",
    "Se resuelve partiendo las palabras en trozos más chicos, y eso es lo que hacen\n",
    "los modelos de hoy: \"huancayo\" se parte en pedacitos que sí conocen. Lo vemos en\n",
    "el capítulo 17 🧩"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Los embeddings de coocurrencia dan a completo e incompleto un parecido de 0,975. ¿Qué está pasando?\n",
    "\n",
    "a) Que aparecen en los mismos contextos, y eso es todo lo que el método mira\n",
    "\n",
    "b) Que el cálculo está mal\n",
    "\n",
    "c) Que hacen falta más dimensiones\n",
    "\n",
    "d) Que faltan datos de entrenamiento\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Está bien: es el agujero conocido de la idea.\n",
    "\n",
    "*c)* Con más dimensiones siguen apareciendo rodeados de las mismas palabras.\n",
    "\n",
    "*d)* Con más texto el problema empeora, porque los contextos se parecen todavía más.\n",
    "\n",
    "Mirar con quién anda una palabra no distingue una cosa de su contrario."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y los vectores que vieron demasiado"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Entrenas los embeddings con todo el texto que tienes, que es lo lógico porque cuanto más texto mejor, y después repartes para evaluar.\n",
    "\n",
    "```\n",
    "vectores = entrena_embeddings(todo_el_texto)\n",
    "\n",
    "X = [vectores[p] for p in frases]\n",
    "X_tr, X_te, y_tr, y_te = train_test_split(\n",
    "    X, y, test_size=0.25)\n",
    "```\n",
    "\n",
    "**Qué está mal**\n",
    "\n",
    "Los vectores se construyeron mirando **con qué palabras aparece cada palabra**, y en ese conteo entraron las frases de la prueba 🔤 No copiaste la respuesta, pero sí metiste en el modelo información que el día de mañana no vas a tener.\n",
    "\n",
    "Y hay un caso donde esto deja de ser sutil y se vuelve grave: si tus frases de prueba se parecen mucho a las de entrenamiento, los vectores aprenden esa coincidencia y el modelo la aprovecha. El número sube y nadie sabe explicar por qué.\n",
    "\n",
    "La regla es la de siempre y no cambia porque el dato sea texto: **todo lo que aprenda algo de los datos se aprende solo con el trozo de entrenamiento**. Vale para la media, para las categorías y también para los embeddings."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🏝️ Con one-hot, bodega y minimarket se parecen 0,0, igual que bodega y\n",
    "arroz. Cada palabra es una isla.\n",
    "\n",
    "- 👀 Un embedding se construye contando con qué palabras aparece cada una. No\n",
    "hay gramática ni diccionario.\n",
    "\n",
    "- 🎉 Con cinco dimensiones, bodega y minimarket dan 0,849 y bodega y arroz\n",
    "0,300: distinguió tipos de palabra solo.\n",
    "\n",
    "- 😬 Y completo con incompleto dan 0,975. Los antónimos aparecen en los mismos\n",
    "sitios, así que salen iguales.\n",
    "\n",
    "- 🔍 La ventana decide qué agrupa: chica junta palabras que se sustituyen,\n",
    "grande junta palabras del mismo tema.\n",
    "\n",
    "- 🔀 Sumar los vectores de una frase pierde el orden: \"no llego el pedido\" se\n",
    "parece muchísimo a \"llego el pedido\".\n",
    "\n",
    "- 🧰 En la tabla de ventas, bodega y minimarket dan 1,000 y no se distinguen:\n",
    "cada columna se cruza con todas. Los embeddings necesitan contexto que varíe.\n",
    "\n",
    "- 🧩 Y solo saben las palabras que vieron: pedir una nueva revienta.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para una máquina, dos palabras del mismo mundo son tan distintas como dos que no tienen nada que ver. Salvo que se lo enseñes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los embeddings son también lo que hay debajo de un buscador de documentos. Si eso es lo que te toca montar en tu empresa, en [soluciones de IA](https://missyera.com/consultoria-ia/) cuento cómo se aterriza 🏭\n",
    "\n",
    "En el capítulo 16 llega la atención, que arregla las tres cosas que quedaron\n",
    "mal aquí: los antónimos, el orden y el vector fijo.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 13 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/texto-y-embeddings/\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
}
