{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# La convolución\n",
    "\n",
    "Un filtro de nueve números que recorre la imagen. Escrito a mano y medido contra los píxeles sueltos, moviendo los dígitos.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 11 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/convolucion/\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": [
    "Ocho capítulos diciendo que la red pierde. Aquí cambiamos de tipo de dato y\n",
    "por fin se entiende para qué existe todo esto 🖼️\n",
    "\n",
    "Una pregunta para arrancar: **¿qué tienen los píxeles de una foto que no tienen las columnas de un Excel?** Ahí está la respuesta entera de por qué existe el deep learning 🧩\n",
    "\n",
    "Y vamos a usar imágenes de verdad, que vienen dentro de scikit-learn: 1.797\n",
    "dígitos escritos a mano en 8 por 8 píxeles."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Una imagen es una matriz de números"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "from sklearn.datasets import load_digits\n",
    "from sklearn.linear_model import LogisticRegression\n",
    "from sklearn.metrics import accuracy_score\n",
    "from sklearn.model_selection import train_test_split\n",
    "\n",
    "digitos = load_digits()\n",
    "imagenes = digitos.images / 16.0          # de 0 a 1\n",
    "etiquetas = digitos.target\n",
    "\n",
    "print('tenemos', imagenes.shape[0], 'imágenes de', imagenes.shape[1], 'por',\n",
    "      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": [
    "Ahí lo tienes: un cero dibujado con números. Se ve el agujero del medio en la\n",
    "franja de ceros de las filas 3, 4 y 5 👀\n",
    "\n",
    "Y aquí está la propiedad que lo cambia todo, la que anuncié en el capítulo 1:\n",
    "en esta matriz **la posición significa algo**. El píxel de arriba a\n",
    "la izquierda y el de abajo a la derecha no son dos columnas intercambiables como\n",
    "`monto` y `ciudad`: son vecinos o no lo son, y eso es\n",
    "información."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La convolución, escrita"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "S(i,j)=∑u∑vI(i+u,j+v)K(u,v)\n",
    "\n",
    "deslizas un filtro chiquito por toda la imagen y en cada posición sumas el producto punto a punto, que es cómo se detecta un borde sin decirle dónde mirar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un filtro es una matriz chiquita. Se apoya en una esquina de la imagen, se\n",
    "multiplican los números que coinciden, se suman, y ese número va a la salida.\n",
    "Luego se corre una posición y se repite."
   ]
  },
  {
   "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",
    "            trozo = imagen[i:i + alto, j:j + ancho]\n",
    "            salida[i, j] = (trozo * filtro).sum()\n",
    "    return salida\n",
    "\n",
    "VERTICAL = np.array([[-1., 0, 1],\n",
    "                     [-1., 0, 1],\n",
    "                     [-1., 0, 1]])\n",
    "HORIZONTAL = VERTICAL.T\n",
    "\n",
    "print('el filtro vertical sobre el primer dígito:')\n",
    "print(np.round(convolucion(imagenes[0], VERTICAL), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira lo que hizo ese filtro sin que nadie le explicara nada 🤯\n",
    "\n",
    "La primera columna sale toda positiva (2,1 2,5 2,2 1,9 2,1 1,9) y la última\n",
    "toda negativa. Positivo significa \"aquí la imagen se aclara de izquierda a\n",
    "derecha\" y negativo lo contrario.\n",
    "\n",
    "O sea que **encontró los dos bordes verticales del cero**: el de\n",
    "la izquierda, donde el trazo empieza, y el de la derecha, donde termina. Con\n",
    "nueve números y una resta.\n",
    "\n",
    "Ese filtro se llama detector de bordes y lleva inventado desde antes que las\n",
    "redes neuronales. Lo que hace una red convolucional es **aprender los\n",
    "filtros en vez de que se los escribas tú**, con la retropropagación del\n",
    "capítulo 6 exactamente igual que cualquier otro peso."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué un filtro y no una capa normal"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Podrías conectar cada píxel a cada neurona, como venimos haciendo. La cuenta\n",
    "explica por qué no se hace:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for lado in [8, 28, 224]:\n",
    "    pixeles = lado * lado\n",
    "    densa = pixeles * 100                    # 100 neuronas conectadas a todo\n",
    "    convo = 9 * 100                          # 100 filtros de 3 por 3\n",
    "    print(f'imagen de {lado}x{lado}: capa densa {densa:9,} pesos   '\n",
    "          f'convolucional {convo:6,} pesos')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Para una foto de 224 por 224, que es el tamaño típico, una capa densa\n",
    "necesitaría cinco millones de pesos y la convolucional novecientos. **Cinco\n",
    "mil veces menos** 😳\n",
    "\n",
    "Y no es solo ahorro. Los mismos nueve números se usan en las 50.000 posiciones\n",
    "de la imagen, así que **lo que el filtro aprende en una esquina sirve en\n",
    "todas**. Una capa densa tendría que aprender por separado a reconocer un\n",
    "borde arriba a la izquierda y el mismo borde abajo a la derecha.\n",
    "\n",
    "Eso se llama compartir parámetros y es la idea central del capítulo 🔑"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## ¿Y funciona mejor?"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Vamos a construir rasgos con los dos filtros y compararlos contra los píxeles\n",
    "crudos. Después de convolucionar, se resume cada zona con su valor máximo, que se\n",
    "llama **pooling**:"
   ]
  },
  {
   "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",
    "    salida = []\n",
    "    for im in imgs:\n",
    "        v = np.abs(convolucion(im, VERTICAL))\n",
    "        h = np.abs(convolucion(im, HORIZONTAL))\n",
    "        salida.append(np.concatenate([agrupa(v), agrupa(h)]))\n",
    "    return np.array(salida)\n",
    "\n",
    "indices = np.arange(len(imagenes))\n",
    "i_tr, i_te = train_test_split(indices, test_size=0.25, random_state=42,\n",
    "                              stratify=etiquetas)\n",
    "I_tr, I_te = imagenes[i_tr], imagenes[i_te]\n",
    "y_tr, y_te = etiquetas[i_tr], etiquetas[i_te]\n",
    "\n",
    "pixeles_tr = I_tr.reshape(len(I_tr), -1)\n",
    "conv_tr = rasgos(I_tr)\n",
    "print('columnas con píxeles     :', pixeles_tr.shape[1])\n",
    "print('columnas con convolución :', conv_tr.shape[1])\n",
    "\n",
    "m_pix = LogisticRegression(max_iter=2000).fit(pixeles_tr, y_tr)\n",
    "m_conv = LogisticRegression(max_iter=2000).fit(conv_tr, y_tr)\n",
    "print('acierto con píxeles    :', round(accuracy_score(\n",
    "    y_te, m_pix.predict(I_te.reshape(len(I_te), -1))), 4))\n",
    "print('acierto con convolución:', round(accuracy_score(\n",
    "    y_te, m_conv.predict(rasgos(I_te))), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Pues no: 0,9622 con los píxeles y 0,9156 con la convolución 🙃\n",
    "\n",
    "Y tiene una explicación aburrida y correcta: 64 píxeles llevan más información\n",
    "que 18 rasgos. Al agrupar tiré datos, y en imágenes tan chiquitas y tan\n",
    "centraditas eso no compensa.\n",
    "\n",
    "Así que hasta aquí la convolución tampoco gana. Pero espera al siguiente\n",
    "apartado, que es donde se ve para qué existe de verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La prueba de verdad: mover la imagen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las fotos reales no vienen centradas. Vamos a desplazar los dígitos de prueba\n",
    "uno o dos píxeles y ver quién aguanta:"
   ]
  },
  {
   "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} {\"píxeles\":>9} {\"convolución\":>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(y_te, m_pix.predict(movidas.reshape(len(movidas), -1)))\n",
    "    a_conv = accuracy_score(y_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": [
    "Ahí está 🎯\n",
    "\n",
    "Con un píxel de desplazamiento a la derecha, el modelo de píxeles se desploma\n",
    "de 0,9622 a **0,4089**, o sea que pierde más de la mitad de sus\n",
    "aciertos. La convolución baja a 0,5689, que también es una caída pero\n",
    "**dieciséis puntos por encima**.\n",
    "\n",
    "La razón es directa: para el modelo de píxeles, la columna \"píxel número 27\"\n",
    "pasó a contener lo que antes estaba en la 26. Todo lo que aprendió sobre esa\n",
    "columna dejó de valer.\n",
    "\n",
    "Para la convolución, el borde vertical *sigue estando ahí*, un poquito\n",
    "más allá, y como después agrupamos por zonas, muchas veces cae en la misma\n",
    "casilla. El filtro no pregunta dónde: pregunta si 🔍\n",
    "\n",
    "Y ahora la parte honesta: las dos se desploman. Con dos píxeles, 0,0756 y\n",
    "0,1067, o sea que las dos son inservibles.\n",
    "\n",
    "Dos filtros escritos a mano y un pooling de 2 por 2 son un juguete. Una red\n",
    "convolucional de verdad tiene decenas de filtros aprendidos por capa, varias\n",
    "capas apiladas y encima aumento de datos (el freno 3 del capítulo 8, que aquí\n",
    "sería entrenar con las imágenes ya movidas). Con eso sí se consigue aguantar de\n",
    "verdad."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El compromiso del pooling"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si agrupar da resistencia al movimiento, ¿por qué no agrupar más?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def agrupa3(mapa):\n",
    "    return np.array([mapa[i:i + 3, j:j + 3].max()\n",
    "                     for i in range(0, 6, 3) for j in range(0, 6, 3)])\n",
    "\n",
    "def rasgos3(imgs):\n",
    "    return np.array([np.concatenate([agrupa3(np.abs(convolucion(im, VERTICAL))),\n",
    "                                     agrupa3(np.abs(convolucion(im, HORIZONTAL)))])\n",
    "                     for im in imgs])\n",
    "\n",
    "m_grueso = LogisticRegression(max_iter=2000).fit(rasgos3(I_tr), y_tr)\n",
    "print(f'{\"desplazamiento\":16} {\"pool 2x2\":>9} {\"pool 3x3\":>9}')\n",
    "for dx, dy in [(0, 0), (1, 0), (1, 1), (2, 0)]:\n",
    "    movidas = desplaza(I_te, dx, dy)\n",
    "    print(f'({dx},{dy}){\"\":11} '\n",
    "          f'{accuracy_score(y_te, m_conv.predict(rasgos(movidas))):9.4f} '\n",
    "          f'{accuracy_score(y_te, m_grueso.predict(rasgos3(movidas))):9.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Porque se paga. Con pooling de 3 por 3, en las imágenes quietas el acierto cae\n",
    "de 0,9156 a 0,7067, y a cambio con dos píxeles de desplazamiento sube de 0,1067 a\n",
    "**0,2844**, casi el triple.\n",
    "\n",
    "Ese es el compromiso de todo el asunto: **agrupar da resistencia y quita\n",
    "precisión**. Cuánto agrupar es la decisión de diseño más repetida en las\n",
    "redes de imágenes, y no tiene una respuesta buena, tiene la respuesta que\n",
    "corresponda a tus datos 🎚️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Inventar tus propios filtros\n",
    "\n",
    "Prueba un filtro que detecte diagonales y otro que\n",
    "promedie."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DIAGONAL = np.array([[-1., -1, 0],\n",
    "                     [-1., 0, 1],\n",
    "                     [0., 1, 1]])\n",
    "PROMEDIO = np.ones((3, 3)) / 9\n",
    "\n",
    "for nombre, f in [('diagonal', DIAGONAL), ('promedio', PROMEDIO)]:\n",
    "    r = convolucion(imagenes[0], f)\n",
    "    print(f'{nombre}: va de {r.min():.2f} a {r.max():.2f}')\n",
    "print()\n",
    "print('el promedio sobre el primer dígito, que es desenfocarlo:')\n",
    "print(np.round(convolucion(imagenes[0], PROMEDIO), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "diagonal: va de -2.69 a 2.12\n",
    "promedio: va de 0.21 a 0.57\n",
    "\n",
    "el promedio sobre el primer dígito, que es desenfocarlo:\n",
    "[[0.25 0.46 0.57 0.53 0.41 0.28]\n",
    " [0.33 0.44 0.47 0.42 0.45 0.38]\n",
    " [0.33 0.34 0.26 0.21 0.36 0.36]\n",
    " [0.31 0.31 0.22 0.21 0.37 0.36]\n",
    " [0.31 0.34 0.34 0.34 0.41 0.33]\n",
    " [0.26 0.38 0.49 0.44 0.36 0.22]]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El filtro de promedio no detecta nada: **desenfoca**. Es lo que\n",
    "hace el \"difuminar\" de cualquier editor de fotos, y son nueve números.\n",
    "\n",
    "Cambiar los números del filtro cambia lo que busca, y esa es toda la magia. En\n",
    "una red de verdad, esos nueve números empiezan aleatorios y la retropropagación\n",
    "los va moviendo hasta que detectan lo que haga falta 🎨"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánto encoge la imagen\n",
    "\n",
    "Mira qué tamaño sale con filtros de distinto tamaño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for lado in [3, 5, 7]:\n",
    "    f = np.ones((lado, lado))\n",
    "    print(f'filtro {lado}x{lado} sobre 8x8 deja',\n",
    "          convolucion(imagenes[0], f).shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "filtro 3x3 sobre 8x8 deja (6, 6)\n",
    "filtro 5x5 sobre 8x8 deja (4, 4)\n",
    "filtro 7x7 sobre 8x8 deja (2, 2)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada filtro se come el borde: un 3 por 3 quita una fila por cada lado y un 7\n",
    "por 7 quita tres.\n",
    "\n",
    "Apila diez capas y no queda imagen. Por eso existe el *padding*, que es\n",
    "rodear la imagen de ceros antes de convolucionar para que salga del mismo\n",
    "tamaño 🖼️"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El padding, en tres líneas\n",
    "\n",
    "Escribe el relleno de ceros y comprueba que conserva el\n",
    "tamaño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def con_borde(imagen, cuanto=1):\n",
    "    return np.pad(imagen, cuanto, mode='constant')\n",
    "\n",
    "print('sin borde:', convolucion(imagenes[0], VERTICAL).shape)\n",
    "print('con borde:', convolucion(con_borde(imagenes[0]), VERTICAL).shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin borde: (6, 6)\n",
    "con borde: (8, 8)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 6 por 6 a 8 por 8, igual que la entrada. Una línea de numpy.\n",
    "\n",
    "Con esto ya puedes apilar todas las capas que quieras sin quedarte sin\n",
    "imagen, que es lo que hacen las redes profundas de visión 🧱"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 4. Entrenar con las imágenes movidas\n",
    "\n",
    "Aplica el aumento de datos del capítulo 8: entrena con\n",
    "copias desplazadas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "aumentadas = np.concatenate([I_tr, desplaza(I_tr, 1, 0), desplaza(I_tr, 0, 1)])\n",
    "y_aumentadas = np.concatenate([y_tr, y_tr, y_tr])\n",
    "\n",
    "m_aum = LogisticRegression(max_iter=2000).fit(\n",
    "    aumentadas.reshape(len(aumentadas), -1), y_aumentadas)\n",
    "\n",
    "print(f'{\"desplazamiento\":16} {\"normal\":>9} {\"con aumento\":>12}')\n",
    "for dx, dy in [(0, 0), (1, 0), (1, 1)]:\n",
    "    movidas = desplaza(I_te, dx, dy).reshape(len(I_te), -1)\n",
    "    print(f'({dx},{dy}){\"\":11} {accuracy_score(y_te, m_pix.predict(movidas)):9.4f} '\n",
    "          f'{accuracy_score(y_te, m_aum.predict(movidas)):12.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "desplazamiento      normal  con aumento\n",
    "(0,0)               0.9622       0.9067\n",
    "(1,0)               0.4089       0.8822\n",
    "(1,1)               0.1156       0.4600\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con un píxel de desplazamiento pasa de 0,4089 a **0,8822**. Más\n",
    "del doble, y usando el mismo modelo de píxeles de siempre.\n",
    "\n",
    "Y tiene su precio, que también hay que decirlo: en las imágenes quietas baja\n",
    "de 0,9622 a 0,9067. Le pediste que sirviera para tres situaciones en vez de una,\n",
    "y con los mismos pesos algo tenía que soltar.\n",
    "\n",
    "Aun así, para este problema **el aumento de datos resuelve más que la\n",
    "convolución**. En imágenes de verdad se usan los dos a la vez, y este\n",
    "ejercicio explica por qué el aumento nunca es opcional 💪"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 5. Dónde se activa el filtro\n",
    "\n",
    "Busca en qué zona del dígito responde más fuerte cada\n",
    "filtro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for indice in [0, 1, 2]:\n",
    "    v = np.abs(convolucion(imagenes[indice], VERTICAL))\n",
    "    h = np.abs(convolucion(imagenes[indice], HORIZONTAL))\n",
    "    print(f'dígito {etiquetas[indice]}: vertical máx {v.max():.2f}  '\n",
    "          f'horizontal máx {h.max():.2f}  '\n",
    "          f'{\"más vertical\" if v.max() > h.max() else \"más horizontal\"}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "dígito 0: vertical máx 2.50  horizontal máx 2.00  más vertical\n",
    "dígito 1: vertical máx 3.00  horizontal máx 1.69  más vertical\n",
    "dígito 2: vertical máx 2.62  horizontal máx 2.38  más vertical\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los tres responden más al vertical, y el que más es el 1 (3,00 contra 1,69),\n",
    "que tiene sentido porque un uno es prácticamente una raya vertical.\n",
    "\n",
    "Pero fíjate en que los tres dan el mismo veredicto: con solo dos filtros no se\n",
    "distingue casi nada.\n",
    "\n",
    "Ahí se ve por qué las redes de verdad usan decenas de filtros por capa: cada\n",
    "uno se especializa en un trocito distinto, y la combinación de todos es lo que\n",
    "identifica 🔬"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 6. Convolución sobre una fila de datos, que también existe\n",
    "\n",
    "Aplica un filtro de 1 dimensión a las ventas mensuales de\n",
    "la distribuidora, que son las del CSV de los otros capítulos."
   ]
  },
  {
   "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",
    "v = pd.read_csv(URL).drop_duplicates()\n",
    "v['monto'] = pd.to_numeric(v['monto'].str.replace(',', '.'))\n",
    "f = pd.to_datetime(v['fecha'], format='%Y-%m-%d', errors='coerce')\n",
    "falta = f.isna() & v['fecha'].notna()\n",
    "f[falta] = pd.to_datetime(v.loc[falta, 'fecha'], format='%d/%m/%Y', errors='coerce')\n",
    "v['fecha'] = f\n",
    "\n",
    "por_mes = v.set_index('fecha')['monto'].resample('ME').sum() / 1000\n",
    "ventas_mes = por_mes.values\n",
    "\n",
    "suaviza = np.ones(3) / 3\n",
    "bordes = np.array([-1., 0, 1])\n",
    "\n",
    "print('miles de soles por mes:', np.round(ventas_mes, 1))\n",
    "print('suavizada             :', np.round(np.convolve(ventas_mes, suaviza, mode='valid'), 1))\n",
    "print('cambios               :', np.round(np.convolve(ventas_mes, bordes[::-1], mode='valid'), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "miles de soles por mes: [128.8 129.5 139.9 115.5 155.5 138.1 141.6 122.4 142.4 139.6 132.4 125.9\n",
    " 129.8 121.5 164.3 127.4 153.4 103.5]\n",
    "suavizada             : [132.7 128.3 136.9 136.3 145.  134.  135.4 134.8 138.1 132.6 129.4 125.7\n",
    " 138.5 137.7 148.3 128.1]\n",
    "cambios               : [ 11.1 -14.   15.5  22.6 -13.8 -15.7   0.7  17.3 -10.  -13.7  -2.5  -4.5\n",
    "  34.5   5.9 -10.9 -23.9]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El filtro que suaviza es la media móvil de toda la vida: la serie deja de dar\n",
    "tumbos y se ve la tendencia.\n",
    "\n",
    "Y el de bordes marca dónde hubo un salto de un mes al siguiente. Los picos\n",
    "grandes de esa fila son los meses donde las ventas se movieron de verdad, y los\n",
    "valores cerca de cero son los meses aburridos 📊\n",
    "\n",
    "Una media móvil *es* una convolución, solo que en una dimensión. Por\n",
    "eso las convolucionales también se usan en series de tiempo y en audio, y no solo\n",
    "en fotos 📈"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 7. El filtro más grande que la imagen, que no avisa\n",
    "\n",
    "Pásale a una imagen de 8 por 8 un filtro de 9 por 9."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vacio = convolucion(imagenes[0], np.ones((9, 9)))\n",
    "print('forma que devuelve:', vacio.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "forma que devuelve: (0, 0)\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "No revienta: devuelve una matriz de **0 por 0** tan tranquila. Un\n",
    "filtro de 9 no cabe en una imagen de 8, así que el bucle no entra ni una vez y la\n",
    "función retorna la matriz vacía que creó al principio.\n",
    "\n",
    "El error llega después, cuando alguien intenta usar eso:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vacio.max()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "ValueError: zero-size array to reduction operation maximum which has no identity\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y el mensaje no habla de filtros ni de imágenes: habla de un array vacío, en\n",
    "una línea que está lejos de donde nació el problema.\n",
    "\n",
    "Esto pasa de verdad al apilar capas, porque la imagen se encoge dos píxeles\n",
    "por capa y en algún momento una recibe algo más chico que su propio filtro.\n",
    "Cuando montes una red convolucional, **haz la cuenta de los tamaños antes\n",
    "de entrenar** 📐"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Sobre dígitos limpios los píxeles crudos ganan a la convolución (0,9622 contra 0,9156), y al mover la imagen un píxel se invierte. ¿Qué demuestra?\n",
    "\n",
    "a) Que la convolución no sirve para ver mejor, sirve para no depender de la posición\n",
    "\n",
    "b) Que la convolución está mal implementada\n",
    "\n",
    "c) Que los dígitos son demasiado fáciles\n",
    "\n",
    "d) Que hacen falta más filtros\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Está bien: con las imágenes desplazadas gana ella.\n",
    "\n",
    "*c)* Fáciles son, pero eso no explica por qué al moverlos cambia el ganador.\n",
    "\n",
    "*d)* Con más filtros sobre imágenes perfectamente centradas sigue sin haber ventaja que explotar.\n",
    "\n",
    "Una capa normal aprende dónde está el borde; una convolución aprende qué es un borde."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La normalización que se adelanta"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### La trampa\n",
    "\n",
    "Normalizas las imágenes antes de partir, que es lo que se hace siempre con píxeles, y luego repartes en entrenamiento y prueba.\n",
    "\n",
    "```\n",
    "X = (X - X.mean()) / X.std()\n",
    "\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",
    "La media y la desviación se calcularon con **todas las imágenes**, incluidas las de la prueba 📸 Cuando examines el modelo, esas imágenes ya habrán participado en decidir con qué vara se miden todas.\n",
    "\n",
    "Con imágenes de un mismo conjunto limpio infla poquísimo, y eso es lo que lo hace peligroso: pasa la revisión y se copia al proyecto siguiente. En el proyecto siguiente las imágenes vienen de dos cámaras distintas, la media global tapa esa diferencia, y el modelo sale mucho mejor de lo que va a ser.\n",
    "\n",
    "La media y la desviación se calculan **solo con el trozo de entrenamiento** y se guardan junto al modelo, porque son parte de él: el día que llegue una imagen nueva hay que aplicarle exactamente esos mismos dos números."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- 🖼️ En una imagen la posición significa algo, y por eso no se trata como\n",
    "columnas sueltas.\n",
    "\n",
    "- 🔍 Un filtro de nueve números encuentra los bordes verticales del dígito sin\n",
    "que nadie le explique qué es un borde.\n",
    "\n",
    "- 🔑 Comparte los mismos nueve números en toda la imagen: para 224 por 224 son\n",
    "900 pesos contra 5 millones de una capa densa.\n",
    "\n",
    "- 🙃 En dígitos limpios y centrados los píxeles crudos ganan (0,9622 contra\n",
    "0,9156), porque agrupar tira información.\n",
    "\n",
    "- 🎯 Con un píxel de desplazamiento los píxeles caen a 0,4089 y la convolución\n",
    "aguanta en 0,5689. Ahí está para qué existe.\n",
    "\n",
    "- 😅 Y las dos se desploman con dos píxeles. Dos filtros a mano son un juguete;\n",
    "las redes de verdad aprenden decenas por capa.\n",
    "\n",
    "- 🎚️ Agrupar más da resistencia y quita precisión: pool 3x3 baja el acierto\n",
    "limpio a 0,7067 y triplica el aguante al desplazamiento.\n",
    "\n",
    "- 💪 Y el aumento de datos resuelve aquí más que la convolución: de 0,4089 a\n",
    "0,8822, a cambio de bajar de 0,9622 a 0,9067 en las imágenes quietas.\n",
    "\n",
    "Y si de todo el capítulo te llevas una sola frase, que sea esta:"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La convolución no es una capa más lista. Es una capa que sabe algo del dato antes de verlo."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y si quieres el vocabulario de todo esto suelto y en dos líneas por término, lo tengo en el [glosario de IA](https://missyera.com/glosario-ia/) 📖\n",
    "\n",
    "En el capítulo 13 pasamos al texto, que tiene otra forma de estructura y otra\n",
    "manera de convertirse en números.\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 11 de **Deep learning desde cero**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/deep-learning-desde-cero/convolucion/\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
}
