Redes Neuronales y Deep Learning

Contents

10. Redes Neuronales y Deep Learning#

10.1. Gradiente descendente#

  • El método de gradiente descendente es uno de los mas ampliamente usados para la minimización iterativa de una función de costo diferenciable, \(J(\boldsymbol{\theta}),~\boldsymbol{\theta}\in\mathbb{R}^{l}\). Como cualquier otra técnica iterativa, el método parte de una estimación inicial, \(\boldsymbol{\theta}^{(0)}\), y genera una sucesión \(\boldsymbol{\theta}^{(i)},~i=1,2,\dots,\) tal que:

    \[ \boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}+\mu_{i}\Delta\boldsymbol{\theta}^{(i)},~ i >0,~\mu_{i}>0. \]
  • La diferencia entre cada método radica en la forma que \(\mu_{i}\) y \(\Delta\boldsymbol{\theta}^{(i)}\) son seleccionados. \(\Delta\boldsymbol{\theta}^{(i)}\) es conocido como la dirección de actualización o de búsqueda. La sucesión \(\mu_{i}\) es conocida como el tamaño o longitud de paso en la \(i\)-ésima iteración, estos valores pueden ser constantes o cambiar. En el método de gradiente descendente, la selección de \(\Delta\boldsymbol{\theta}^{(i)}\) es realizada para garantizar que \(J(\boldsymbol{\theta}^{(i)})<J(\boldsymbol{\theta}^{(i-1)})\), excepto en el minimizador \(\boldsymbol{\theta}_{\star}\).

_images/curva_nivel.png

Fig. 10.1 Función de coste en el espacio de parámetros bidimensional.#

  • Suponga que en la iteración \(i-1\) el valor \(\boldsymbol{\theta}^{(i-1)}\) ha sido obtenido

\[ J(\boldsymbol{\theta}^{(i)})=J(\boldsymbol{\theta}^{(i-1)}+\mu_{i}\Delta\boldsymbol{\theta}^{(i)})\approx J(\boldsymbol{\theta}^{(i-1)})+\mu_{i}\cdot\nabla^{T}J(\boldsymbol{\theta}^{(i-1)})\Delta\boldsymbol{\theta}^{(i-1)}. \]
  • Nótese que seleccionando la dirección tal que \(\nabla^{T}J(\boldsymbol{\theta}^{(i-1)})\Delta\boldsymbol{\theta}^{(i)}<0\), garantizará que \(J(\boldsymbol{\theta}^{(i-1)}+\mu_{i}\Delta\boldsymbol{\theta}^{(i)})<J(\boldsymbol{\theta}^{(i-1)})\). Tal selección de \(\Delta\boldsymbol{\theta}^{(i)}\) y \(\nabla J(\boldsymbol{\theta}^{(i-1)})\) debe formar un ángulo obtuso. Las curvas de nivel asociadas a \(J(\boldsymbol{\theta})\) pueden tomar cualquier forma, la cual va a depender de como está definido \(J(\boldsymbol{\theta})\).

  • \(J(\boldsymbol{\theta})\) se supone diferenciable, por lo tanto, las curvas de nivel o contornos deben ser suaves y aceptar un plano tangente en cualquier punto. Además, de los cursos de cálculo sabemos que el vector gradiente \(\nabla J(\boldsymbol{\theta})\) es perpendicular al plano tangente (recta tangente) a la correspondiente curva de nivel en el punto \(\boldsymbol{\theta}\). Nótese que seleccionando la dirección de búsqueda \(\Delta\boldsymbol{\theta}^{(i)}\) que forma un angulo obtuso con el gradiente, se coloca a \(\boldsymbol{\theta}^{(i-1)}+\mu_{i}\Delta\boldsymbol{\theta}^{(i)}\) en un punto sobre el contorno el cual corresponde a un valor menor que \(J(\boldsymbol{\theta})\).

  • Dos problemas surgen ahora:

    1. Escoger la mejor dirección de búsqueda

    2. Calcular que tan lejos es aceptable un movimiento a traves de esta dirección.

_images/maximun_dec_cost_function.png

Fig. 10.2 El vector gradiente en un punto \(\boldsymbol{\theta}\) es perpendicular al plano tangente (línea punteada) en la curva de nivel que cruza \(\boldsymbol{\theta}\). La dirección de descenso forma un ángulo obtuso, \(\phi\), con el vector gradiente.#

  • Nótese que si \(\mu_{i}\|\Delta\boldsymbol{\theta}^{(i)}\|\) es demasiado grande, entonces el nuevo punto puede ser colocado en un contorno correspondiente a un valor mayor al del actual contorno.

_images/curva_nivel_cost_function.png

Fig. 10.3 Las correspondientes curvas de nivel para la función de coste, en el plano bidimensional. Nótese que a medida que nos alejamos del valor óptimo, \(\boldsymbol{\theta}_{\star}\), los valores de \(c\) aumentan.#

  • Para abordar (1), supongamos que \(\mu_{i}=1\) y buscamos todos los vectores \(\boldsymbol{z}\) con norma Euclidiana unitaria, con inicio (cola) en \(\boldsymbol{\theta}^{(i-1)}\). Entonces, para todas las posibles direcciones, la que entrega el valor más negativo del producto interno, \(\nabla^{T}J(\boldsymbol{\theta}^{(i-1)})z\), es aquella de gradiente negativo

\[ z=-\frac{\nabla J(\boldsymbol{\theta}^{(i-1)})}{\|\nabla J(\boldsymbol{\theta}^{(i-1)}\|} \]
  • Centrando \(\boldsymbol{\theta}^{(i-1)}\) en la bola con norma Euclideana uno. De todos los vectores con norma unitaria y origen en \(\boldsymbol{\theta}^{(i-1)}\), seleccionamos aquel que apunta en la dirección negativa del gradiente. Por lo tanto, para todos los vectores con norma Euclidiana 1, la dirección de descenso mas pronunciada coincide con la dirección del gradiente descendente, negativo, y la correspondiente actualización recursiva se convierte en

\[ \boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}-\mu_{i}\nabla J(\boldsymbol{\theta}^{(i-1)}),\quad\text{Gradiente descendente}. \]
_images/desc_gradient.png

Fig. 10.4 Representación del gradiente negativo el cual conduce a la máxima disminución de la función de coste.#

  • La selección de \(\mu_{i}\) debe ser realizada de tal forma que garantice convergencia de la secuencia de minimización. Nótese que el algoritmo puede oscilar en torno al mínimo sin converger, si no seleccionamos la dirección correcta. La selección de \(\mu_{i}\) dependerá de la convergencia a cero del error entre \(\boldsymbol{\theta}^{(i)}\) y el mínimo real en forma de serie geométrica.

Ejercicio

  • Por ejemplo, para el caso de la función de coste del error cuadrático medio, la longitud de paso está dada por: \(0<\mu<2/\lambda_{\max}\), donde \(\lambda_{\max}\) el máximo eigenvalor de la matriz de covarianza \(\Sigma_{x}=\mathbb{E}[\boldsymbol{x}\boldsymbol{x}^{T}]\), donde \(J(\boldsymbol{\theta})=E[(y-\boldsymbol{\theta}^{T}\boldsymbol{x})^{2}]\) (ver Sección 5.3 [Theodoridis, 2020]).

10.2. Redes neuronales#

Introducción

  • Las redes neuronales son sistemas de aprendizaje compuestos por neuronas conectadas en capas que ajustan sus conexiones para aprender. Tras un período de 25 años desde su inicio, las redes neuronales se convirtieron en la norma en el aprendizaje automático. En un principio, dominaron durante una década, pero luego fueron superadas por máquinas de vectores de soporte.

  • Sin embargo, desde 2010, las redes neuronales profundas se han vuelto populares gracias a mejoras en la tecnología y la disponibilidad de grandes conjuntos de datos, impulsando el campo del aprendizaje automático.

10.3. El perceptrón#

  • Nuestro punto de partida será considerar el problema simple de una tarea de clasificación conformada por dos clases linealmente separables. En otras palabras, dado un conjunto de muestras de entrenamiento, \((y_{n}, \boldsymbol{x}_{n})\), \(n=1,2,\dots,N\), con \(y_{n}\in\{-1,+1\},~\boldsymbol{x}_{n}\in\mathbb{R}^{l}\), suponemos que existe un hiperplano

    \[ \boldsymbol{\theta}_{\star}^{T}\boldsymbol{x}=0, \]

    tal que,

    \[\begin{split} \begin{cases} \boldsymbol{\theta}_{\star}^{T}\boldsymbol{x}&>0,\quad\text{si}\quad\boldsymbol{x}\in\omega_{1}\\ \boldsymbol{\theta}_{\star}^{T}\boldsymbol{x}&<0,\quad\text{si}\quad\boldsymbol{x}\in\omega_{2} \end{cases} \end{split}\]

    En otras palabras, dicho hiperplano clasifica correctamente todos los puntos del conjunto de entrenamiento. Para simplificar, el término de sesgo del hiperplano ha sido absorbido en \(\boldsymbol{\theta}_{\star}\) después de extender la dimensionalidad del espacio de entrada en uno. El objetivo ahora es desarrollar un algoritmo que calcule iterativamente un hiperplano que clasifique correctamente todos los patrones de ambas clases. Para ello, se adopta una función de costo.

  • Sea \(\boldsymbol{\theta}\) la estimación del vector de parámetros desconocidos, disponible en la actual iteración. Entonces hay dos posibilidades. La primera es que todos los puntos estén clasificados correctamente; esto significa que se ha obtenido una solución. La otra alternativa es que \(\boldsymbol{\theta}\) clasifique correctamente algunos de los puntos y el resto estén mal clasificados.

Costo perceptrón

Sea \(\mathcal{Y}\) el conjunto de todas las muestras mal clasificadas. La función de costo, perceptrón se define como

\[ J(\boldsymbol{\theta})=-\sum_{n:\boldsymbol{x}_{n}\in\mathcal{Y}}y_{n}\boldsymbol{\theta}^{T}\boldsymbol{x}_{n}:\quad\textsf{Costo perceptrón}, \]

donde

\[\begin{split} y_{n}= \begin{cases} +1,&\quad\text{si}~\boldsymbol{x}\in\omega_{1}\\ -1,&\quad\text{si}~\boldsymbol{x}\in\omega_{2}. \end{cases} \end{split}\]
  • Nótese que la función la función de costo es no negativa. En efecto, dado que la suma es sobre los puntos mal clasificados, si \(\boldsymbol{x}_{n}\in\omega_{1}~(\omega_{2}),~\) entonces \(\boldsymbol{\theta}^{T}\boldsymbol{x}_{n}\leq (\geq)~0\), entregando así un producto \(-y_{n}\boldsymbol{\theta}^{T}\boldsymbol{x}_{n}\geq0\).

  • La función de costo es cero, si no existen puntos mal clasificados, esto es, \(\mathcal{Y}=\emptyset\). La función de costo perceptrón no es diferenciable en todos los puntos, es lineal por tramos. Si reescribimos \(J(\boldsymbol{\theta})\) en una forma ligeramente diferente:

\[ J(\boldsymbol{\theta})=\left(-\sum_{n:\boldsymbol{x}_{n}\in\mathcal{Y}}y_{n}\boldsymbol{x}_{n}^{T}\right)\boldsymbol{\theta}. \]
  • Nótese que esta es una función lineal con respeto a \(\boldsymbol{\theta}\), siempre que el conjunto de puntos mal clasificados permanezca igual. Además, nótese que ligeros cambios del valor \(\boldsymbol{\theta}\) corresponden a cambios de posición del respectivo hiperplano. Como consecuencia, existirá un punto donde el número de muestras mal clasificadas en \(\mathcal{Y}\), repentinamente cambia; este es el tiempo donde una muestra en el conjunto de entrenamiento cambia su posición relativa con respecto a el hiperplano en movimiento, y en consecuencia, el conjunto \(\mathcal{Y}\) es modificado. Después de este cambio, el conjunto, \(J(\boldsymbol{\theta})\), corresponderá a una nueva función lineal.

El algoritmo perceptrón

A partir del método de subgradientes se puede verificar fácilmente que, iniciando desde un punto arbitrario, \(\boldsymbol{\theta}^{(0)}\), el siguiente método iterativo,

\[ \boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}+\mu_{i}\sum_{n:\boldsymbol{x}_{n}\in\mathcal{Y}}y_{n}\boldsymbol{x}_{n}:\quad\text{Regla perceptrón}, \]

converge después de un número finito de pasos. La sucesión de parámetros \(\mu_{i}\) es seleccionada adecuadamente para garantizar convergencia.

  • Nótese que usando el método de subgradiente (ver apéndice) o gradiente descendente se tiene que

\[\begin{split} \begin{align*} \boldsymbol{\theta}^{(i)}&=\boldsymbol{\theta}^{(i-1)}-\mu_{i}J'(\boldsymbol{\theta}^{(i-1)})\\ &=\boldsymbol{\theta}^{(i-1)}-\mu_{i}\left(-\sum_{n:\boldsymbol{x}_{n}\in\mathcal{Y}}y_{n}\boldsymbol{x}_{n}^{T}\right)\\ &=\boldsymbol{\theta}^{(i-1)}+\mu_{i}\sum_{n:\boldsymbol{x}_{n}\in\mathcal{Y}}y_{n}\boldsymbol{x}_{n}. \end{align*} \end{split}\]
  • Otra versión del algoritmo considera una muestra por iteración en un esquema cíclico, hasta que el algoritmo converge. Denotemos por \(y_{(i)}\), \(\boldsymbol{x}_{i},~i\in\{1,2,\dots,N\}\) los pares de entrenamiento presentados al algoritmo en la iteración \(i\)-ésima. Entonces, la iteración de actualización se convierte en:

(10.1)#\[\begin{split} \boldsymbol{\theta}^{(i)}= \begin{cases} \boldsymbol{\theta}^{(i-1)}+\mu_{i}y_{(i)}\boldsymbol{x}_{(i)},&\quad\text{si}\,\boldsymbol{x}_{(i)}\,\text{es mal clasificado por}\,\boldsymbol{\theta}^{(i-1)},\\ \boldsymbol{\theta}^{(i-1)},&\quad\text{otro caso}. \end{cases} \end{split}\]
  • Esto es, partiendo de una estimación inicial de forma random, inicializando \(\boldsymbol{\theta}^{(0)}\) con algunos valores pequeños, testeamos cada una de las muestras, \(\boldsymbol{x}_{n},~n=1,2,\dots,N\). Cada vez que una muestra es mal clasificada, se toma acción por medio de la regla perceptrón para una corrección. En otro caso, ninguna acción es requerida.

  • Una vez que todas las muestras han sido consideradas, decimos que una época (epoch) ha sido completada. Si no se obtiene convergencia, todas las muestras son reconsideradas en una segunda época, y así sucesivamente. La versión de este algoritmo es conocida como esquema pattern-by-pattern. Algunas veces también es referido como el algoritmo online. Nótese que el número total de datos muestrales es fijo, y que el algoritmo las considera en forma cíclica, época por época (epoch-by-epoch).

Observación

  • Después de un número finito de épocas, se garantiza que el algoritmo es convergente (convexidad de \(J\) y clases linealmente separables). Nótese que para obtener dicha convergencia, la sucesión \(\mu_{i}\) debe ser seleccionada apropiadamente. Sin embargo, para el caso del algoritmo perceptrón, la convergencia es garantizada, aun cuando \(\mu_{i}\) es una constante positiva, \(\mu_{i}=\mu>0\), usualmente tomado igual a uno.

  • La formulación en (10.1) es conocida también como la filosofía de aprendizaje reward-punishment. Si la actual estimación es exitosa en la predicción de la clase del respectivo patrón, ninguna acción es tomada (reward), en otro caso, el algoritmo es obligado a realizar una actualización (punishment).

_images/perceptron_rule.png

Fig. 10.5 El punto \(x\) está mal clasificado por la línea roja. La regla perceptrón gira el hiperplano hacia el punto \(x\), para intentar incluirlo en el lado correcto del nuevo hiperplano y clasificarlo correctamente.#

  • La Fig. 10.5 ofrece una interpretación geométrica de la regla del perceptrón. Supongamos que la muestra \(\boldsymbol{x}\) está mal clasificada por el hiperplano, \(\boldsymbol{\theta}^{(i-1)}\). Como sabemos, por geometría analítica, \(\boldsymbol{\theta}^{(i-1)}\) corresponde a un vector que es perpendicular al hiperplano que está definido por este vector. Como \(\boldsymbol{x}\) se encuentra en el lado \((-)\) del hiperplano y está mal clasificado, pertenece a la clase \(\omega_{1}\); asumiendo \(\mu = 1\), la corrección aplicada por el algoritmo es

    \[ \boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}+\boldsymbol{x}, \]

    y su efecto es girar el hiperplano en dirección a \(\boldsymbol{x}\) para colocarlo en el lado \((+)\) del nuevo hiperplano, que está definido por la estimación actualizada \(\boldsymbol{\theta^{(i)}}\). El algoritmo perceptrón en su modo de funcionamiento patrón por patrón (pattern-by-pattern) se resume en el siguiente algoritmo:

Algorithm 10.1 (Algoritmo perceptrón pattern-by-pattern)

Inicialización

  1. Inicializar \(\boldsymbol{\theta}^{(0)}\); usualmente, de forma random, pequeño

  2. Seleccionar \(\mu\); usualmente establecido como uno

  3. \(i=1\)

Repeat Cada iteración corresponde a un epoch

  1. counter = 0; Contador del número de actualizaciones por epoch

  2. For \(n=1,2,\dots,N\) Do Para cada epoch, todas las muestras son presentadas una vez

    If(\(y_{n}\boldsymbol{x}_{n}^{T}\leq0\)) Then

    1. \(\boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}+\mu y_{n}\boldsymbol{x}_{n}\)

    2. \(i=i+1\)

    3. counter = counter + 1

    End For

  3. Until counter = 0

  • Una vez que el algoritmo perceptrón se ha ejecutado y converge, tenemos los pesos, \(\theta_{i},~i = 1,2,\dots,l\), de las sinapsis de la neurona/perceptrón asociada, así como el término de sesgo \(\theta_{0}\). Ahora se pueden utilizar para clasificar patrones desconocidos. Las características \(x_{i}, i = 1, 2,\dots,l\), se aplican a los nodos de entrada. A su vez, cada característica se multiplica por la sinapsis respectiva (peso), y luego se añade el término de sesgo en su combinación lineal.

  • El resultado de esta operación pasa por una función no lineal, \(f\), conocida como función de activación (ver Activation function). Dependiendo de la forma de la no linealidad, se producen diferentes tipos de neuronas. La mas clásica conocida como neurona McCulloch-Pitts, la función de activación es la de Heaviside, es decir,

\[\begin{split} f(z)= \begin{cases} 1,&\quad\text{si}~z>0,\\ 0,&\quad\text{si}~z\leq0. \end{cases} \end{split}\]
_images/mcculloch_pitts.png

Fig. 10.6 Arquitectura básica de neuronas/perceptrones.#

  • En la arquitectura básica de neuronas/perceptrones, las características de entrada se aplican a los nodos de entrada y se ponderan por los respectivos pesos que definen las sinapsis. A continuación se añade el término de sesgo en su combinación lineal y el resultado es empujado a través de la no linealidad. En la neurona McCulloch-Pitts, la salida es 1 para los patrones de la clase \(\omega_{1}\) o 0 para la clase \(\omega_{2}\). La suma y la operación no lineal se unen para simplificar el gráfico.

_images/hidden_layer_activationf_function.png

Fig. 10.7 Selección de función de activación para hidden layers. (Fuente [Brownlee and Mastery, 2017]).#

  • Para las capas ocultas (input layer), la función de activación tangente hiperbólica suele funcionar mejor que la sigmoidea logística. Tanto la función sigmoid como tanh pueden hacer que el modelo sea más susceptible a los problemas durante el entrenamiento, a través del llamado problema de los gradientes desvanecientes. Los modelos modernos de redes neuronales con arquitecturas comunes, como MLP y CNN, harán uso de la función de activación ReLU, o extensiones.

  • Las redes recurrentes suelen utilizar funciones de activación tanh o sigmoid, o incluso ambas. Por ejemplo, la LSTM suele utilizar la activación sigmoid para las conexiones recurrentes y la activación tanh para la salida.

_images/output_layer_activation_function.png

Fig. 10.8 Selección de función de activación para hidden layers. (Fuente [Brownlee and Mastery, 2017]).#

  • Si su problema es de regresión, debería utilizar una función de activación lineal en el output layer. Si su problema es de clasificación, el modelo predice la probabilidad de pertenencia a una clase, que se puede convertir en una etiqueta de clase mediante redondeo (para sigmoid) o argmax (para softmax). Usualmente, softmax es usada en clasificación múltiple cuando las clases son mutuamente excluyentes en caso contrario puede usar la función de activación sigmoid para cada output.

10.4. Redes Neuronales Multicapa Feed-Forward#

  • Una sola neurona está asociada a un hiperplano

    \[ H: \theta_{1}x_{1}+\theta_{2}x_{2}+\cdots+\theta_{l}x_{l}+\theta_{0}=0, \]

    en el espacio de entrada, y la clasificación se basa en una función no lineal que produce un resultado de uno o cero según en qué lado del hiperplano \(H\) se encuentre un punto. A continuación, se mostrará cómo combinar varias neuronas en capas para crear clasificadores no lineales. Seguiremos un enfoque constructivo simple que será útil al abordar aspectos de las redes neuronales, especialmente en el contexto de arquitecturas profundas de aprendizaje profundo (deep learning).

  • Como punto de partida, consideramos el caso en el que las clases del espacio de características están formadas por uniones de regiones poliédricas

_images/regiones_poliedricas.png

Fig. 10.9 Clases formadas por uniones de regiones poliédricas. Las regiones se etiquetan según el lado en el que se encuentran, con respecto a las tres líneas, \(H_{1}, H_{2}\) y \(H_{3}\).#

  • Las regiones poliédricas se forman como intersecciones de semiespacios, cada uno de ellos asociado a un hiperplano. En la Fig. 10.9, hay tres hiperplanos (líneas rectas en \(\mathbb{R}^2\)), indicados como \(H_{1}, H_{2}, H_{3}\), que dan lugar a siete regiones poliédricas. Para cada hiperplano se indican los lados \((+)\) y \((-)\) (semiespacios).

  • Cada una de las regiones se etiqueta con un triplete de números binarios, según el lado que se encuentra con respecto a \(H_{1}, H_{2}, H_{3}\). Por ejemplo, la región etiquetada como \((101)\) se encuentra en el lado \((+)\) de \(H_{1}\), el lado \((-)\) de \(H_{2}\) y el lado \((+)\) de \(H_{3}\).

_images/poliedros_neurons.png

Fig. 10.10 (A) Las neuronas de la primera capa oculta son activadas por los valores de las características aplicadas en los nodos de entrada y forman las regiones poliédricas. (B) Las neuronas de la segunda capa tienen como entradas las salidas de la primera capa, y así forman las clases.#

  • La Fig. 10.10 muestra tres neuronas, correspondientes a los tres hiperplanos, \(H_{1}, H_{2}\) y \(H_{3}\), de la Fig. 10.9, respectivamente. Las salidas asociadas, denotadas como \(y_{1}, y_{2}\), y \(y_{3}\), forman la etiqueta de la región a la que el patrón de entrada correspondiente pertenece. De hecho, si los pesos de las sinapsis se han fijado adecuadamente, entonces, si un patrón se origina en la región, digamos, \((010)\), la primera neurona de la izquierda asigna un cero \((y_{1} = 0)\), la del medio un uno \((y_{2} = 1)\), y la de la derecha un cero \((y_{3} = 0)\).

  • En otras palabras, combinando las tres neuronas, hemos logrado un mapeo del espacio de características de entrada en el espacio tridimensional. Más concretamente, el mapeo se realiza en los vértices del cubo unitario en \(\mathbb{R}^{3}\), como se muestra en la Fig. 10.11

_images/mapping_input_feature.png

Fig. 10.11 Las neuronas de la primera capa oculta realizan un mapeo del espacio de características de entrada a los vértices de un hipercubo unitario. El vértice 110, denotado como un círculo sin sombrear, no corresponde a ninguna región.#

  • Ahora utilizaremos esta nueva representación, proporcionada por las salidas de las neuronas de la primera capa oculta, como entrada que alimenta las neuronas de una segunda capa oculta, la cual se construye de la siguiente forma. Elegimos todas las regiones que pertenecen a una clase. Para nuestro ejemplo de la Fig. 10.11, seleccionamos las dos regiones que corresponden a la clase \(\omega_{1}\), es decir, \((000)\) y \((111)\). Recordemos que todos los puntos de estas regiones se mapean a los respectivos vértices del cubo unitario en \(\mathbb{R}^{3}\). Sin embargo, en este nuevo espacio transformado, cada uno de los vértices es linealmente separable del resto.

  • Observe que la salida \(z_{1}\) de la neurona izquierda arroja un 1 sólo si el patrón de entrada se origina en la región \(000\) y 0 para todos los demás patrones. Para la neurona de la derecha, la salida \(z_{2}\) será 1 para todos los patrones procedentes de la región \((111)\) y cero para el resto (ver Fig. 10.11). Nótese que esta segunda capa de neuronas ha realizado un segundo mapeo, esta vez a los vértices del rectángulo unitario en \(\mathbb{R}^{2}\).

_images/map_layer_intor2.png

Fig. 10.12 Los patrones de la clase \(\omega_{1}\) se asignan a (01) o a (10) y los patrones de la clase \(\omega_{2}\) se asignan a (00). Clases ahora linealmente separables a través de una línea recta realizada por una neurona.#

  • Este mapeo proporciona una nueva representación de los patrones de entrada, y esta representación codifica la información relacionada con las clases de las regiones. La Fig. 10.12 muestra el mapeo a los vértices del rectángulo unitario en el espacio \((z_{1}, z_{2})\). Nótese que todos los puntos procedentes de la clase \(\omega_{2}\) están mapeados a \((00)\) y los puntos de la clase \(\omega_{1}\) están asignados a \((10)\) o a \((01)\). Esto es, mediante mapeos sucesivos, hemos transformado nuestra tarea, originalmente, linealmente no separable a una que es linealmente separable.

  • En efecto, el punto \((00)\) puede separarse linealmente de \((01)\) y \((10)\), y esto puede realizarse mediante una neurona adicional que opera en el espacio \((z_{1}, z_{2})\); la cual se conoce como la neurona de salida, porque proporciona la decisión final de clasificación.

_images/final_neural_network.png

Fig. 10.13 Red neuronal feed-forward de tres capas. Comprende la capa de entrada (no de procesamiento), dos capas ocultas y una capa de salida de neuronas#

  • La red final resultante se muestra en la Fig. 10.13. Llamamos a esta red feed-forward, porque la información fluye hacia adelante desde la capa de entrada a la de salida. Se compone de la capa de entrada, que es una capa no procesadora, dos capas ocultas, y una capa de salida. Llamamos a esta red neuronal, red de tres capas, sin contar la capa de entrada de nodos no procesadores. Esta red neuronal de tres capas puede resolver cualquier tarea de clasificación, en la que las clases están formadas por uniones de regiones poliédricas.

  • Considerando esta estructura de la red multicapa. Nuestro interés ahora se centrará en buscar formas de estimar los pesos desconocidos de las sinapsis y los sesgos de las neuronas. Sin embargo, desde un punto de vista conceptual, debemos recordar que cada capa realiza un mapeo en un nuevo espacio, y cada mapeo proporciona una representación diferente de los datos de entrada, hasta la última capa, donde la tarea se ha transformado en una que es fácil de resolver.

10.5. Redes Totalmente Conectadas#

  • Para resumir de manera más formal el tipo de operaciones que tienen lugar en una red totalmente conectada, centrémonos en, por ejemplo, la capa \(r\) de una red neuronal multicapa y supongamos que está formada por \(k_{r}\) neuronas. El vector de entrada a esta capa está formado por las salidas de los nodos de la capa anterior, que se denomina \(\boldsymbol{y}^{r-1}\).

  • Sea \(\boldsymbol{\theta}_{j}^{r}\) el vector de los pesos sinápticos, incluido el término de sesgo, asociado a la neurona \(j\) de la capa \(r\), donde \(j = 1,2,\dots, k_{r}\). La dimensión respectiva de este vector es \(k_{r-1} + 1\), donde \(k_{r-1}\) es el número de neuronas de la capa anterior, \(r-1\), y el aumento en 1 representa el término de sesgo. Entonces las operaciones realizadas, antes de la no linealidad, son los productos internos

\[ z_{j}^{r}=\boldsymbol{\theta}_{j}^{rT}\boldsymbol{y}^{r-1},\quad j=1,2,\dots,k_{r}. \]
  • Colocando todos los valores de salida en un vector \(\boldsymbol{z}^{r}=[z_{1}^{r}, z_{2}^{r},\dots,z_{k_{r}}^{r}]^{T}\), y agrupando todos los vectores sinápticos como filas, una debajo de la otra, en una matriz, podemos escribir colectivamente

\[ \boldsymbol{z}^{r}=\Theta\boldsymbol{y}^{r-1},\quad\text{donde}\quad\Theta:=[\boldsymbol{\theta}_{1}^{r}, \boldsymbol{\theta}_{2}^{r},\dots, \boldsymbol{\theta}_{k_{r}}^{r}]. \]
  • El vector de las salidas de la \(r\) th capa oculta, después de empujar cada \(z_{i}^{r}\) a través de la no linealidad \(f\), está finalmente dado por

\[\begin{split} \boldsymbol{y}^{r}= \begin{bmatrix} 1\\ f(\boldsymbol{z}^{r}) \end{bmatrix} \end{split}\]
  • La siguiente figura describe como es creada la \(j\)-ésima capa de la red neuronal full conectada

_images/fully_connected_net.png

Fig. 10.14 \(j\)-ésimo elemento de la capa \(r\) de la red totalmente conectada.#

Observación

  • La notación anterior significa que \(f\) actúa sobre cada uno de los respectivos componentes del vector, individualmente, y la extensión del vector en uno es para dar cuenta de los términos de sesgo en la práctica estándar. Para redes grandes, con muchas capas y muchos nodos por capa, este tipo de conectividad resulta ser muy costoso en términos del número de parámetros (pesos), que es del orden de \(k_{r}k_{r-1}\).

  • Por ejemplo, si \(k_{r-1} = 1000\) y \(k_{r} = 1000\), esto equivale a un orden de 1 millón de parámetros. Tenga en cuenta que este número es la contribución de los parámetros de una sola de las capas. Sin embargo, un gran número de parámetros hace que una red sea vulnerable al sobreajuste, cuando se trata de entrenamiento

  • Se pueden emplear las llamadas técnicas de reparto de pesos, en las que un conjunto de parámetros es compartido entre un número de conexiones, a través de restricciones adecuadamente incorporadas. Las redes neuronales recurrentes y convolucionales pertenecen a esta familia de redes de peso compartido. En una red convolucional, las convoluciones sustituyen a las operaciones de producto interno, lo que permite un reparto de pesos importante que conduce a una reducción sustancial del número de parámetros.

10.6. El Algoritmo De Backpropagation#

  • Una red neuronal considera una función paramétrica no lineal, \(\hat{y} = f_{\boldsymbol{\theta}}(\boldsymbol{x})\), donde \(\boldsymbol{\theta}\) representa todos los pesos/sesgo presentes en la red. Por lo tanto, el entrenamiento de una red neuronal no parece ser diferente del entrenamiento de cualquier otro modelo de predicción paramétrica.

  • Todo lo que se necesita es (a) un conjunto de muestras de entrenamiento, (b) una función de pérdida \(\mathcal{L}(y, \hat{y})\), y (c) un esquema iterativo, por ejemplo, el gradiente descendente, para realizar la optimización de la función de coste asociada (pérdida empírica).

\[ J(\boldsymbol{\theta})=\sum_{n=1}^{N}\mathcal{L}(y_{n}, f_{\boldsymbol{\theta}}(\boldsymbol{x}_{n})). \]
  • La dificultad del entrenamiento de las redes neuronales radica en su estructura multicapa que complica el cálculo de los gradientes, que intervienen en la optimización. Además, la neurona McCulloch-Pitts se basa en la función de activación discontinua Heaviside no diferenciable.

  • Neurona sigmoidea logística: Una posibilidad es adoptar la función sigmoidea logística, es decir,

\[ f(z)=\sigma(z):=\frac{1}{1+\exp(-az)}. \]
  • Nótese que cuanto mayor sea el valor del parámetro \(a\), la gráfica correspondiente se acerca más a la de la función de Heaviside (ver Fig. 10.15).

_images/sigmoid_act_function.png

Fig. 10.15 Función sigmoidea logística para diferentes valores del parámetro \(a\).#

  • Otra posibilidad sería utilizar la función,

    \[ f(z)=a\tanh\left(\frac{cz}{2}\right), \]

    donde \(c\) y \(a\) son parámetros de control. El gráfico de esta función se muestra en la Fig. 10.16. Nótese que a diferencia de la sigmoidea logística, esta es una función no simétrica, es decir, \(f(-z)=-f(z)\). Ambas son también conocidas como funciones de reducción, porque limitan la salida a un rango finito de valores.

_images/tanh_act_function.png

Fig. 10.16 Función de reducción de la tangente hiperbólica para \(a = 1.7\) y \(c = 4/3\).#

  • Recordemos que la regla de actualización del algoritmo gradiente descendente, en su versión unidimensional se convierte en

    \[ \theta(new)=\theta(old)-\mu\left.\frac{d J}{d\theta}\right|_{\theta(old)}, \]

    y las iteraciones parten de un punto inicial arbitrario, \(\theta^{(0)}\).

  • Si en la iteración actual el algoritmo está digamos, en el punto \(\theta(old) = \theta_{1}\), entonces se moverá hacia el mínimo local, \(\theta_{l}\). Esto se debe a que la derivada del coste en \(\theta_{1}\) es igual a la tangente \(\phi_{1}\) (ver Fig. 10.17), que es negativa (el ángulo es obtuso) y la actualización, \(\theta(new)\), se moverá a la derecha, hacia el mínimo local, \(\theta_{l}\).

_images/convex_function_saddle_point.png

Fig. 10.17 Función no convexa global, con mínimos locales y puntos de silla.#

Observación

  • La elección del tamaño del paso, \(\mu\), es crítica para la convergencia del algoritmo. En problemas reales en espacios multidimensionales, el número de mínimos locales puede ser grande, por lo que el algoritmo puede converger a uno local. Sin embargo, esto no es necesariamente una mala noticia. Si este mínimo local es lo suficientemente profundo, es decir, si el valor de la función de coste en este punto, por ejemplo, \(J(\theta_{l})\), no es mucho mayor que el alcanzado en el mínimo global, es decir, \(J(\theta_{g})\), la convergencia a dicho mínimo local puede corresponder a una buena solución.

  • Una opción para evitar caer en mínimos locales es utilizar un método de optimización diferente, por ejemplo, Adam, RMSprop o AdaGrad que pueden adaptarse mejor a la topografía del error y ayudar a escapar de los mínimos locales. Explorar otras funciones de coste también es otra opción, o utilizar diferentes métodos de regularización (\(L1, L2\)), dado que, si el modelo es muy complejo, puede ser más propenso a caer en mínimos locales.

10.7. El Esquema De Backpropagation Para Gradiente Descendente#

Habiendo adoptado una función de activación diferenciable, estamos listos para proceder a desarrollar el esquema iterativo de gradiente descendente para la minimización de la función de coste. Formularemos la tarea en un marco general.

  • Sea \((\boldsymbol{y}_{n}, \boldsymbol{x}_{n}), n = 1, 2,\dots, N\), el conjunto de muestras de entrenamiento. Nótese que hemos asumido múltiples variables output, como vectores. Suponemos que la red consta de \(L\) capas, \(L-1\) capas ocultas y una capa de salida. Cada capa consta de \(k_{r}, r = 1, 2,\dots, L\), neuronas. Así, los vectores de salida (objetivo/deseado) son

\[ \boldsymbol{y}_{n}=[y_{n1}, y_{n2},\dots, y_{nk_{L}}]^{T}\in\mathbb{R}^{K_{L}},\quad n=1,2,\dots,N. \]
  • Para ciertas derivaciones matemáticas, también denotamos el número de nodos de entrada como \(k_{0}\); es decir, \(k_{0} = l\), donde \(l\) es la dimensionalidad del espacio de características de entrada.

  • Sea \(\boldsymbol{\theta}_{j}^{r}\) el vector de los pesos sinápticos asociados a la \(j\)-th neurona de la \(r\)-th capa, con \(j = 1, 2,\dots, k_{r}\) y \(r = 1, 2,\dots,L\), donde el término de sesgo se incluye en \(\boldsymbol{\theta}_{j}^{r}\), es decir,

(10.2)#\[ \boldsymbol{\theta}_{j}^{r}:=[\theta_{j0}^{r}, \theta_{j1}^{r},\dots, \theta_{jk_{r-1}}^{r}]^{T}. \]
  • Los pesos sinápticos en la capa \(r\) enlazan la neurona respectiva con todas las neuronas de la capa \(k_{r-1}\) (véase la Fig. 10.18). El paso iterativo básico para el esquema de gradiente descendente se escribe como

(10.3)#\[ \boldsymbol{\theta}_{j}^{r}(\text{new})=\boldsymbol{\theta}_{j}^{r}(old)+\Delta\boldsymbol{\theta}_{j}^{r},\quad \Delta\boldsymbol{\theta}_{j}^{r}:=-\mu\left.\frac{\partial J}{\partial\boldsymbol{\theta}_{j}^{r}}\right|_{\boldsymbol{\theta}_{j}^{r}(old)}. \]
  • El parámetro \(\mu\) es el tamaño de paso definido por el usuario (también puede depender de la iteración) y \(J\) denota la función de coste.

  • Las ecuaciones de actualización (10.3) comprenden el par del esquema de gradiente descendente para la optimización. Como se ha dicho anteriormente, la dificultad de las redes neuronales feed-forward surge de su estructura multicapa. Para calcular los gradientes en la Ecuación (10.3), para todas las neuronas, en todas las capas, se deben seguir dos pasos en su cálculo

    • Forward computations: Para un vector de entrada dado \(\boldsymbol{x}_{n}, n = 1, 2,\dots, N\), se utilizan las estimaciones actuales de los parámetros (pesos sinápticos) (\(\boldsymbol{\theta}_{j}^{r}(old)\)) y calcula todas las salidas de todas las neuronas en todas las capas, denotadas como \(y_{nj}^{r}\); en la Fig. 10.18, se ha suprimido el índice \(n\) para no afectar la notación.

    • Backward computations: Utilizando las salidas neuronales calculadas anteriormente junto con los valores objetivos conocidos, \(y_{nk}\), de la capa de salida, se calculan los gradientes de la función de coste. Esto implica \(L\) pasos, es decir, tantos como el número de capas. La secuencia de los pasos algorítmicos se indica a continuación:

      • Calcular el gradiente de la función de coste con respecto a los parámetros de las neuronas de la última capa, es decir, \(\partial J/\partial\boldsymbol{\theta}_{j}^{L}, j = 1, 2,\dots, k_{L}\).

      • For \(r = L-1\) to \(1\), Do

        Calcular los gradientes con respecto a los parámetros asociados a las neuronas de la \(r\) th capa, es decir, \(\partial J/\partial\boldsymbol{\theta}_{k}^{r}, k= 1, 2,\dots, k_{r}\) basado en todos los gradientes \(\partial J/\partial\boldsymbol{\theta}_{j}^{r+1}, j= 1, 2,\dots, k_{r+1}\), con respecto a los parámetros de la capa \(r + 1\) que se han calculado en el paso anterior.

      • End For

Ejemplo

  • Analicemos un ejemplo concreto de forward computations, seguido de un enfoque backward computations. El ejemplo de entrenamiento será (\(x=2.1\), \(y=4\)), el peso inicial será \(w:=\theta_{1}=1\), el sesgo será \(b:=\theta_{0}=0\) y \(\textsf{Loss}=L\). \(\hat{y}\) representa la salida de la neurona y el valor predicho, mientras que la pérdida se refiere a la pérdida por error cuadrático

_images/backpropagation_example.png

Fig. 10.19 Ejemplo de red neuronal para backpropagation#

  • Forward computaion:

_images/forwardcomputation_example.png
  • Backward computation: Ahora podemos retroceder (backpropagation) y calcular las derivadas parciales aplicando la regla de la cadena para obtener

\[\begin{split} \begin{align*} \frac{\partial\textsf{Loss}}{\partial w} &= \frac{\partial\hat{y}}{\partial w}\frac{\partial\textsf{Loss}}{\partial\hat{y}}=x(2(\hat{y}-y)),~\text{entonces, para}~w=1, b=0\\ \left.\frac{\partial\textsf{Loss}}{\partial w}\right|_{x=2.1,y=4} &= \left.x(2(\hat{y}-y))\right|_{x=2.1,y=4}=(2.1)(2(2.1-4))=-7.98 \end{align*} \end{split}\]
\[\begin{split} \begin{align*} \frac{\partial\textsf{Loss}}{\partial b} &= \frac{\partial\hat{y}}{\partial b}\frac{\partial\textsf{Loss}}{\partial\hat{y}}=1\cdot(2(\hat{y}-y)),~\text{entonces, para}~w=1, b=0\\ \left.\frac{\partial\textsf{Loss}}{\partial b}\right|_{x=2.1,y=4} &= \left.1\cdot(2(\hat{y}-y))\right|_{x=2.1,y=4}=(1)(2(2.1-4))=-3.8 \end{align*} \end{split}\]
  • Luego, actualizamos los parámetros en la dirección opuesta al gradiente para disminuir la pérdida, con una tasa de aprendizaje de \(\textsf{lr}:=\mu=0.01\)

\[\begin{split} \begin{align*} w &= w-\textsf{lr}\cdot\frac{\partial\textsf{Loss}}{\partial w}=(1)-(0.01)(-7.98)=1.0798\\ b &= b-\textsf{lr}\cdot\frac{\partial\textsf{Loss}}{\partial b}=(0)-(0.01)(-3.8)=0.038 \end{align*} \end{split}\]
  • Para evaluar el rendimiento de nuestros parámetros ajustados, realizaremos una pasada adicional

\[ \textsf{Loss}=((1.0798)(2.1)+0.038-4)^{2}=2.87 \]
  • La pérdida total disminuyó de 3.61 a 2.87, lo que representa una reducción de 0.74. ¡La pérdida ha bajado!

10.8. Entrenamiento de MLP#

Observación

  • Los pesos de una red neuronal se ajustan mediante algoritmos de optimización basados en gradiente, como el gradiente descendente estocástico, que minimiza iterativamente la función de pérdida \(L\). Para tareas de regresión, se emplean el error cuadrático medio (MSE) y el error absoluto medio (MAE); para clasificación, se utilizan pérdidas logarítmicas binarias o categóricas.

  • El algoritmo backpropagation se comprende a través de grafos computacionales, útiles para calcular en redes neuronales. En una red simple con una capa oculta de dos neuronas con activación sigmoidea y salida lineal, alimentada por dos entradas \([y_1, y_2]\), los pesos se distribuyen en los bordes de la red.

_images/twoinput_single_hidden_layer.png

Fig. 10.20 Red neuronal con una sola capa oculta y dos entradas.#

  • La Fig. 10.21 ilustra este proceso mediante un grafo computacional para la entrada \([-1, 2]\), generando salidas intermedias \(p_i\). En particular, \(p_7\) y \(p_8\) corresponden a las salidas de las neuronas ocultas \(g_1\) y \(g_2\). Durante este paso también se calcula la función de pérdida \(L\) usada en el entrenamiento.

_images/forwardprop_single_hidden_layer.png

Fig. 10.21 Gráfico computacional de un perceptrón con una capa y dos neuronas ocultas.#

  • En este punto, se aplica el algoritmo de backpropagation para calcular las derivadas parciales entre dos nodos conectados por una arista. El recorrido hacia atrás en el grafo para calcular la derivada parcial también se conoce como paso hacia atrás (backward pass). El operador de diferenciación parcial se aplica en cada nodo y las derivadas parciales se asignan a las respectivas aristas que conectan el nodo descendente a lo largo del grafo computacional.

  • Siguiendo la regla de la cadena la derivada parcial \(\partial_{\omega} L\) se calcula multiplicando las derivadas parciales en todas las aristas que conectan el nodo de peso y el nodo de pérdida. Si existen varios caminos entre un nodo de peso y el nodo de pérdida, las derivadas parciales a lo largo de cada camino se suman para obtener la derivada parcial total de la pérdida con respecto al peso. Esta técnica gráfica de implementar los pasos hacia delante y hacia atrás es la técnica computacional subyacente utilizada en potentes bibliotecas de aprendizaje profundo. El paso hacia atrás se ilustra en Fig. 10.22

_images/partialderivates_single_hidden_layer.png

Fig. 10.22 Cálculo de derivadas parciales en un grafo computacional.#

  • Las derivadas parciales de la función de pérdida con respecto a los pesos se obtienen aplicando la regla de la cadena:

\[\begin{split} \begin{align*} \frac{\partial L}{\partial\omega_{5}} &= -2(y-\hat{y})\times 1\times p_{7}\\ \frac{\partial L}{\partial\omega_{6}} &= -2(y-\hat{y})\times 1\times p_{8}\\ \frac{\partial L}{\partial\omega_{1}} = \frac{\partial L}{\partial\hat{y}}\frac{\partial\hat{y}}{\partial p_{9}}\frac{\partial p_{9}}{\partial p_{7}}\frac{\partial p_{7}}{\partial p_{5}}\frac{\partial p_{5}}{\partial p_{1}}\frac{\partial p_{1}}{\partial\omega_{1}} &= -2(y-\hat{y})\times 1\times\omega_{5}\times p_{7}^{2}e^{-p_{5}}\times 1\times -1\\ \frac{\partial L}{\partial\omega_{2}} = \frac{\partial L}{\partial\hat{y}}\frac{\partial\hat{y}}{\partial p_{9}}\frac{\partial p_{9}}{\partial p_{7}}\frac{\partial p_{7}}{\partial p_{5}}\frac{\partial p_{5}}{\partial p_{2}}\frac{\partial p_{2}}{\partial\omega_{2}} &= -2(y-\hat{y})\times 1\times\omega_{5}\times p_{7}^{2}e^{-p_{5}}\times 1\times 2\\ \frac{\partial L}{\partial\omega_{3}} = \frac{\partial L}{\partial\hat{y}}\frac{\partial\hat{y}}{\partial p_{10}}\frac{\partial p_{10}}{\partial p_{8}}\frac{\partial p_{8}}{\partial p_{6}}\frac{\partial p_{6}}{\partial p_{3}}\frac{\partial p_{3}}{\partial\omega_{3}} &= -2(y-\hat{y})\times 1\times\omega_{6}\times p_{8}^{2}e^{-p_{6}}\times 1\times -1\\ \frac{\partial L}{\partial\omega_{4}} = \frac{\partial L}{\partial\hat{y}}\frac{\partial\hat{y}}{\partial p_{10}}\frac{\partial p_{10}}{\partial p_{8}}\frac{\partial p_{8}}{\partial p_{6}}\frac{\partial p_{6}}{\partial p_{4}}\frac{\partial p_{4}}{\partial\omega_{4}} &= -2(y-\hat{y})\times 1\times\omega_{6}\times p_{8}^{2}e^{-p_{6}}\times 1\times 2. \end{align*} \end{split}\]
  • Durante el entrenamiento, los pesos se inicializan con números aleatorios comúnmente muestreados a partir de una distribución uniforme con límites superior e inferior en \([-1, 1]\) o una distribución normal que tiene media cero y varianza unitaria. Estos esquemas de inicialización aleatoria tienen algunas variantes que mejoran la convergencia de la optimización. En este caso, vamos a suponer que los pesos son inicializados a partir de una distribución aleatoria uniforme y, por tanto, \(w_{1} = -0.33, w_{2} = -0.33, w_{3} = 0.57, w_{4} = -0.01, w_{5}=0.07\), y \(w_{6} = 0.82\).

  • Con estos valores, vamos a realizar pasos hacia adelante y hacia atrás sobre el grafo computacional. Actualizamos la figura anterior con los valores calculados durante la pasada hacia adelante en azul y los gradientes calculados durante la pasada hacia atrás en rojo. Para este ejemplo, fijamos el valor real de la variable objetivo como \(y = 1\)

_images/forward_backward_single_hidden_layer.png

Fig. 10.23 Pasos hacia delante (en azul) y hacia atrás (en rojo) sobre un grafo computacional.#

  • Una vez calculados los gradientes a lo largo de las aristas, las derivadas parciales con respecto a los pesos no son más que una aplicación de la regla de la cadena, de la que ya hemos hablado. Los valores de las derivadas parciales son los siguientes:

\[\begin{split} \begin{align*} \frac{\partial L}{\partial\omega_{5}} &= -0.919\times 1\times 0.418 = -0.384\\ \frac{\partial L}{\partial\omega_{6}} &= -0.919\times 1\times 0.357 = -0.328\\ \frac{\partial L}{\partial\omega_{1}} &= -0.919\times 1\times 0.07\times 0.243\times 1\times -1 = 0.016\\ \frac{\partial L}{\partial\omega_{2}} &= -0.919\times 1\times 0.07\times 0.243\times 1\times 2 = -0.032\\ \frac{\partial L}{\partial\omega_{3}} &= -0.919\times 1\times 0.82\times 0.229\times 1\times -1 = 0.173\\ \frac{\partial L}{\partial\omega_{4}} &= -0.919\times 1\times 0.82\times 0.229\times 1\times 2 = -0.346\\ \end{align*} \end{split}\]
  • El siguiente paso consiste en actualizar los pesos mediante el algoritmo de gradiente descendiente. Así, con una tasa de aprendizaje de \(\alpha = 0.01\), el nuevo valor de

\[ w_{5} = 0.07 - 0.01\times -0.384 = 0.0738. \]
  • El resto de pesos también pueden actualizarse utilizando una regla de actualización similar. El proceso de actualización iterativa de los pesos se repite varias veces. El número de veces que se actualizan los pesos se conoce como número de épocas o pasadas sobre los datos de entrenamiento. Normalmente, un criterio de tolerancia sobre el cambio de la función de pérdida en comparación con la época anterior controla el número de épocas.

  • Para determinar los pesos de una red neuronal se utiliza el algoritmo backpropagation junto con un optimizador basado en el gradiente. Afortunadamente, existen potentes bibliotecas de aprendizaje profundo, como Tensorflow, Theano y CNTK que implementan gráficos computacionales para entrenar redes neuronales de cualquier arquitectura y complejidad. Estas bibliotecas vienen con soporte para ejecutar los cálculos como operaciones matemáticas en matrices multidimensionales y también pueden aprovechar las GPU para realizar cálculos más rápidos.

Observación

  • El esquema de cálculo hacia atrás backpropagation es una aplicación directa de la regla de la cadena para las derivadas, y comienza con el paso inicial de calcular las derivadas asociadas a la última capa (de salida), que resulta ser sencillo.

  • A continuación, el algoritmo “fluye” hacia atrás en la jerarquía de capas. Esto se debe a la naturaleza de la red multicapa, donde las salidas, capa tras capa, se forman como funciones de funciones.

  • El gradiente en la última capa es simplemente la derivada de la función de pérdida con respecto a la salida de esa capa. Como no hay capas posteriores que influyan en su gradiente, el cálculo es más directo y no requiere aplicar la regla de la cadena a través de varias capas.

  • En efecto, centrémonos en la salida \(y_{k}^{r}\) de la neurona \(k\) en la capa \(r\). Entonces tenemos

    \[ y_{k}^{r}=f(\boldsymbol{\theta}_{k}^{r^T}\boldsymbol{y}^{r-1}),\quad k=1,2,\dots, k_{r}, \]

    donde \(\boldsymbol{y}^{r-1}\) es el vector (ampliado) que comprende todas las salidas de la capa anterior, \(r-1\), y \(f\) denota la no-linealidad.

  • De acuerdo con lo anterior, la salida de la \(j\)-th neurona en la siguiente capa viene dada por

    \[\begin{split} y_{j}^{r+1}=f(\boldsymbol{\theta}_{j}^{r+1^T}\boldsymbol{y}^{r})= f\left(\boldsymbol{\theta}_{j}^{r+1^{T}} \begin{bmatrix} 1\\ f(\Theta^{r}\boldsymbol{y}^{r-1}) \end{bmatrix} \right)= f\left(\boldsymbol{\theta}_{j}^{r+1^{T}} \begin{bmatrix} 1\\ f(\boldsymbol{y}^{r}) \end{bmatrix} \right), \end{split}\]

    donde \(\Theta^{r}:=[\boldsymbol{\theta}_{1}^{r}, \boldsymbol{\theta}_{2}^{r},\dots,\boldsymbol{\theta}_{k_{r}}]^{T}\) denota la matriz cuyas columnas corresponden al vector de pesos en el layer \(r\).

  • Nótese que obtuvimos evaluación de “una función interna bajo una función externa”. Claramente, esto continúa a medida que avanzamos en la jerarquía. Esta estructura de evaluación de funciones internas por funciones externas, es el subproducto de la naturaleza multicapa de las redes neuronales, la cual es una operación altamente no lineal, que da lugar a la dificultad de calcular los gradientes, a diferencia de otros modelos, como por ejemplo SVM.

  • Sin embargo, se puede observar fácilmente que el cálculo de los gradientes con respecto a los parámetros que definen la capa de salida no plantea ninguna dificultad. En efecto, la salida de la \(j\)-th neurona de la última capa (que es en realidad la respectiva estimación de salida actual) se escribe como:

\[ \hat{y}_{j}:=y_{j}^{L}=f(\boldsymbol{\theta}_{j}^{L^{T}}\boldsymbol{y}^{L-1}). \]
  • Dado que \(\boldsymbol{y}^{L-1}\) es conocido, después de los cálculos durante el paso adelante (forward computaion), tomando la derivada con respecto a \(\boldsymbol{\theta}_{j}^{L}\) es sencillo; no hay ninguna operación de función sobre función. Por esto es que empezamos por la capa superior y luego nos movemos hacia atrás. Debido a su importancia histórica, se dará la derivación completa del algoritmo backpropagation.

  • Para la derivación detallada del algoritmo backpropagation, se adopta como ejemplo la función de pérdida del error cuadrático, es decir

    (10.4)#\[ J(\boldsymbol{\theta})=\sum_{n=1}^{N}J_{n}(\boldsymbol{\theta})\quad\text{y}\quad J_{n}(\boldsymbol{\theta})=\frac{1}{2}\sum_{k=1}^{k_{L}}(\hat{y}_{nk}-y_{nk})^{2}, \]

    donde \(\hat{y}_{nk},~k=1,2,\dots,k_{L}\), son las estimaciones proporcionadas en los correspondientes nodos de salida de la red. Las consideraremos como los elementos de un vector correspondiente, \(\hat{\boldsymbol{y}}_{n}\).

10.9. Cálculo de gradientes#

  • Sea \(z_{nj}^{r}\) la salida del combinador lineal de la \(j\)-th neurona en la capa \(r\) en el instante de tiempo \(n\), cuando se aplica el patrón \(\boldsymbol{x}_{n}\) en los nodos de entrada (véase la Fig. 10.18). Entonces, para \(n, j\) fijos, podemos escribir

    (10.5)#\[ z_{nj}^{r}=\sum_{m=1}^{k_{r-1}}\theta_{jm}^{r}y_{nm}^{r-1}+\theta_{j0}^{r}=\sum_{m=0}^{k_{r-1}}\theta_{jm}^{r}y_{nm}^{r-1}=\boldsymbol{\theta}_{j}^{r^{T}}\boldsymbol{y}_{n}^{r-1}, \]

    donde por definición

    \[ \boldsymbol{y}_{n}^{r-1}:=[1, y_{n1}^{r-1},\dots, y_{nk_{r-1}}^{r-1}]^{T}, \]

    y \(y_{n0}^{r}\equiv 1,~\forall~r, n\) y \(\theta_{j}^{r}\) ha sido definido en la Ecuación (10.2).

  • Para las neuronas de la capa de salida \(r=L,~y_{nm}^{L}=\hat{y}_{nm},~m=1,2,\dots, k_{L}\), y para \(r=1\), tenemos \(y_{nm}^{1}=x_{nm},~m=1,2,\dots, k_{1}\); esto es, \(y_{nm}^{1}\) se fija igual a los valores de las características de entrada.

  • Por lo tanto, teniendo en cuenta que \(z_{j}^{r}=\boldsymbol{\theta}_{j}^{rT}\boldsymbol{y}^{r-1},~j=1,2,\dots,k_{r}\) y \(\hat{y}_{j}:=y_{j}^{r}=f(\boldsymbol{\theta}_{j}^{r^{T}}\boldsymbol{y}^{r-1})=f(z_{j}^{r})\), con base en la Ecuación (10.4) podemos escribir las derivadas parciales

\[ \frac{\partial J_{n}}{\partial\boldsymbol{\theta}_{j}^{r}}=\frac{\partial J_{n}}{\partial z_{nj}^{r}}\frac{\partial z_{nj}^{r}}{\partial\boldsymbol{\theta}_{j}^{r}}=\frac{\partial J_{n}}{\partial z_{nj}^{r}}\boldsymbol{y}_{n}^{r-1}. \]
  • Definamos

\[ \delta_{nj}^{r}:=\frac{\partial J_{n}}{\partial z_{nj}^{r}}. \]
  • Entonces la Ecuación (10.3) puede escribirse como

(10.6)#\[ \Delta\boldsymbol{\theta}_{j}^{r}=\left.-\mu\frac{\partial J}{\partial\boldsymbol{\theta}_{j}^{r}}\right|_{\boldsymbol{\theta}_{j}^{r}(\text{old})}=-\mu\frac{\partial}{\partial\boldsymbol{\theta}_{j}^{r}}\left.\sum_{n=1}^{N}J_{n}\right|_{\boldsymbol{\theta}_{j}^{r}(\text{old})}=-\mu\sum_{n=1}^{N}\delta_{nj}^{r}\boldsymbol{y}_{n}^{r-1},\quad r=1,2,\dots,L. \]

10.10. Cálculo de \(\delta_{nj}^{r}\)#

  • Este es el cálculo principal del algoritmo backpropagation. Para el cálculo de los gradientes, \(\delta_{nj}^{r}\), se comienza en la última capa, \(r = L\), y se procede hacia atrás, hacia \(r = 1\); esta “filosofía” justifica el nombre dado al algoritmo.

  1. \(r=L\): Tenemos que

    \[ \delta_{nj}^{L}:=\frac{\partial J_{n}}{\partial z_{nj}^{L}}. \]

    Para la función de pérdida del error al cuadrado,

    \[ J_{n}=\frac{1}{2}\sum_{k=1}^{k_{L}}\left(\hat{y}_{nk}-y_{nk}\right)^{2}=\frac{1}{2}\sum_{k=1}^{k_{L}}\left(f(z_{nk}^{L})-y_{nk}\right)^{2}. \]

    Por lo tanto,

    (10.7)#\[\begin{split} \begin{align*} \delta_{nj}^{L}=\frac{\partial}{\partial z_{nj}^{L}}\left(\frac{1}{2}\sum_{k=1}^{k_{L}}\left(f(z_{nk}^{L})-y_{nk}\right)^{2}\right)&=(f(z_{nj}^{L})-y_{nj})f'(z_{nj}^{L})\\ &=(\hat{y}_{nj}-y_{nj})f'(z_{nj}^{L})=e_{nj}f'(z_{nj}^{L}), \end{align*} \end{split}\]

    donde \(j=1,2,\dots, k_{L}\), \(f'\) denota la derivada de \(f\) y \(e_{nj}\) es el error asociado con el \(j\)-th output en el tiempo \(n\). Nótese que para el último layer, el cálculo del gradiente, \(\delta_{nj}^{L}\) es sencillo.

  1. \(r<L\): Debido a la dependencia sucesiva entre las capas, el valor de \(z_{nj}^{r-1}\) influye en todos los valores \(z_{nk}^{r},~k = 1, 2,\dots, k_{r}\), de la capa siguiente. Empleando la regla de la cadena para la diferenciación, obtenemos, para \(r=L, L-1,\dots, 2\):

    (10.8)#\[ \delta_{nj}^{r-1}=\frac{\partial J_{n}}{\partial z_{nj}^{r-1}}=\sum_{k=1}^{k_{r}}\frac{\partial J_{n}}{\partial z_{nk}^{r}}\frac{\partial z_{nk}^{r}}{\partial z_{nj}^{r-1}}, \]

    o

    (10.9)#\[ \delta_{nj}^{r-1}=\sum_{k=1}^{k_{r}}\delta_{nk}^{r}\frac{\partial z_{nk}^{r}}{\partial z_{nj}^{r-1}}. \]

    Además, usando la Ecuación (10.5) se tiene,

    \[ \frac{\partial z_{nk}^{r}}{\partial z_{nj}^{r-1}}=\frac{\partial}{\partial z_{nj}^{r-1}}\left(\sum_{m=0}^{k_{r-1}}\theta_{km}^{r}y_{nm}^{r-1}\right), \]

    donde \(\textcolor{blue}{y_{nm}^{r-1}=f(z_{nm}^{r-1})}=f(\boldsymbol{\theta}_{m}^{r-1^{T}}\boldsymbol{y}_{n}^{r-2})\). Entonces,

    \[ \frac{\partial z_{nk}^{r}}{\partial z_{nj}^{r-1}}=\theta_{kj}^{r}f'(z_{nj}^{r-1}), \]

    y combinando las Ecuaciones (10.8) y (10.9), obtenemos la regla recursiva

    \[ \delta_{nj}^{r-1}=\left(\sum_{k=1}^{k_{r}}\delta_{nk}^{r}\theta_{kj}^{r}\right)f'(z_{nj}^{r-1}). \]
  • Manteniendo la misma notación en la Ecuación (10.7), definimos

    \[ e_{nj}^{r-1}:=\sum_{k=1}^{k_{r}}\delta_{nk}^{r}\theta_{kj}^{r}, \]

    y finalmente obtenemos,

    (10.10)#\[ \delta_{nj}^{r-1}=e_{nj}^{r-1}f'(z_{nj}^{r-1}). \]
  • El único cálculo que queda es la derivada de \(f\). Para el caso de la función sigmoidea logística se demuestra fácilmente que es igual a (verifíquelo)

\[ f'(z)=af(z)(1-f(z)). \]
  • La derivación se ha completado y el esquema backpropagation neural network se resume en el siguiente algoritmo

Algorithm 10.2 (Algoritmo Backpropagation Gradiente Descendente)

Inicialización

  1. Inicializar todos los pesos y sesgos sinápticos al azar con valores pequeños, pero no muy pequeños.

  2. Seleccione el tamaño del paso \(\mu\).

  3. Fije \(y_{nj}^{1}=x_{nj},\quad j=1,2,\dots,k_{1}:=l,\quad n=1,2,\dots,N\)

Repeat Cada repetición completa un epoch

  1. For \(n=1,2,\dots,N\) Do

    1. For \(r=1,2,\dots,L\) Do Cálculo Forward

      1. For \(j=1,2,\dots,k_{r}\) Do

        Calcule \(z_{nj}^{r}\) a partir de la Ecuación (10.5) Calcule \(y_{nj}^{r}=f(z_{nj}^{r})\)

      2. End For

    2. End For

    3. For \(j = 1, 2,\dots, k_{L}\), Do; Cálculo Backward (output layer)

      Calcule \(\delta_{nj}^{L}\) a partir de la Ecuación (10.10)

    4. End For

    5. For \(r=L, L-1,\dots, 2\), Do; Cálculo Backward (hidden layers)

      1. For \(j=1,2,\dots, k_{r}\), Do

        Calcule \(\delta_{nj}^{r-1}\) a partir de la Ecuación (10.10)

      2. End For

    6. End For

  2. End For

  3. For \(r=1,2,\dots,L\), Do: Actualice los pesos

    1. For \(j=1,2,\dots,k_{r}\), Do

      Calcule \(\Delta\boldsymbol{\theta}_{j}^{r}\) a partir de la Ecuación (10.6)

      \(\boldsymbol{\theta}_{j}^{r}=\boldsymbol{\theta}_{j}^{r}+\Delta\boldsymbol{\theta}_{j}^{r}\)

    2. End For

  4. End For

  5. Until Un criterio de parada se cumpla.

  • El algoritmo de backpropagation puede reivindicar una serie de padres. La popularización del algoritmo se asocia con el artículo clásico [Rumelhart et al., 1986], donde se proporciona la derivación del algoritmo. La idea de backpropagation también aparece en [Bryson Jr et al., 1963] en el contexto del control óptimo.

  • Existen diferentes variaciones del algoritmo backpropagation, tales como: Gradiende descendente con término de momento, Algoritmo de momentos de Nesterov’s, Algoritmo AdaGrad, RMSProp con momento de Nesterov, Algortimo de estimación de momentos adaptativo los cuales pueden ser utlizados para resolver la tarea de optimización (ver [Theodoridis, 2020]).

10.11. Tuning de Redes Neuronales#

  • Profundizaremos en la mecánica del Perceptrón Multicapa (MLP) empleando el MLPClassifier() en el conjunto de datos two_moons que se utilizó anteriormente en esta sección

from sklearn.neural_network import MLPClassifier
from sklearn.datasets import make_moons
from sklearn.model_selection import train_test_split
import mglearn
import matplotlib.pyplot as plt
X, y = make_moons(n_samples=100, noise=0.25, random_state=3)
X_train, X_test, y_train, y_test = train_test_split(X, y, stratify=y, random_state=42)
mlp = MLPClassifier(solver='adam', random_state=0, max_iter=10000).fit(X_train, y_train)
  • adam Optimizer: adam es un acrónimo de Adaptive Moment Estimation y es uno de los algoritmos de optimización más populares y efectivos utilizados en el entrenamiento de redes neuronales (ver [Kingma and Ba, 2014]). Es una variante avanzada del gradiente descendente que combina las ideas de los métodos de momentum y RMSProp para adaptarse mejor a diferentes problemas de optimización.

Características de Adam

  • Adaptative Learning Rate: Calcula tasas de aprendizaje adaptativas para cada parámetro utilizando estimaciones de primer y segundo orden de los momentos (media y varianza) de los gradientes.

  • Momentum:Utiliza el concepto de momentum para acumular una media móvil exponencial de los gradientes pasados (primer momento) y una media móvil exponencial de los cuadrados de los gradientes pasados (segundo momento).

mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0");
plt.ylabel("Feature 1");
_images/1868535f75ae2a969bab12c376908528656a191882bc1d8640d31beb96033220.png
  • Evidentemente, la red neuronal ha adquirido un límite de decisión decisivamente no lineal pero relativamente gradual

mlp = MLPClassifier(solver='lbfgs', random_state=0, max_iter=400, hidden_layer_sizes=[10])
  • Se empleó el algoritmo 'lbfgs' (ver Broyden–Fletcher–Goldfarb–Shanno algorithm (L-BFGS), optimizador de la familia de los métodos cuasi-Newton.). En particular, el MLP emplea 100 nodos ocultos por defecto, un número considerable para este conjunto de datos compacto. Sin embargo, incluso con un número reducido de nodos, se puede lograr un resultado satisfactorio

Optimizador L-BFGS

  • L-BFGS es una variante del algoritmo BFGS, que es un método de optimización de segundo orden. A diferencia de los métodos de primer orden, que utilizan únicamente el gradiente de la función de pérdida, los métodos de segundo orden también utilizan la información de la segunda derivada (la matriz Hessiana) para mejorar la dirección de los pasos de actualización. L-BFGS ajusta internamente las tasas de aprendizaje basadas en la información de segundo orden.

mlp.fit(X_train, y_train)
MLPClassifier(hidden_layer_sizes=[10], max_iter=400, random_state=0,
              solver='lbfgs')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0");
plt.ylabel("Feature 1");
_images/dd2bd6982dafeb7432f378d4aad6425efd5f862dd5fdebf08a6ac351bbb4bd8d.png
  • Al reducir las unidades ocultas a 10 hace que el límite de decisión tenga un aspecto algo irregular. La no linealidad por defecto empleada es la "relu"

import numpy as np
line = np.linspace(-3, 3, 100)
plt.plot(line, np.tanh(line), label="tanh")
plt.plot(line, np.maximum(line, 0), label="relu")
plt.legend(loc="best")
plt.xlabel("x")
plt.ylabel("relu(x), tanh(x)");
_images/3c26314ef087f3a6c368e94182cff437ca31af95e28ca767daca3e718a44cae0.png
  • En el caso de una sola capa oculta, esto implica que la función de decisión comprende 10 segmentos de línea recta. Para conseguir un límite de decisión más gradual, las alternativas incluyen aumentar las unidades ocultas, introducir una segunda capa oculta o emplear la no linealidad "tanh".

  • Utilizando dos capas ocultas, con 10 unidades cada una

mlp = MLPClassifier(solver='lbfgs', random_state=0, hidden_layer_sizes=[10, 10])
mlp.fit(X_train, y_train)
MLPClassifier(hidden_layer_sizes=[10, 10], random_state=0, solver='lbfgs')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1");
_images/6985cde3fd8482a21615d9e6cbe5e787ddd77fd3aa8228d20220e9df01b5c3da.png
mlp = MLPClassifier(solver='lbfgs', activation='tanh', max_iter=400, random_state=0, hidden_layer_sizes=[10, 10])
mlp.fit(X_train, y_train)
MLPClassifier(activation='tanh', hidden_layer_sizes=[10, 10], max_iter=400,
              random_state=0, solver='lbfgs')
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3)
mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train)
plt.xlabel("Feature 0")
plt.ylabel("Feature 1");
_images/82e32a023aaa1255f7ea8db5ac791b0fa38444a258dbcdff445ddb77ef6cdf89.png
  • Además, podemos influir en la complejidad de una red neuronal incorporando una penalización \(L2\), similar al enfoque de la regresión de ridge y los clasificadores lineales. El parámetro responsable de esto en el MLPClassifier es 'alpha', análogo a los modelos de regresión lineal. Por defecto, asume un valor mínimo (regularización limitada). El siguiente experimento incluye dos capas ocultas, cada una de ellas compuesta por 10 o 100 unidades y diferentes valores de alpha

fig, axes = plt.subplots(2, 4, figsize=(20, 8))

for axx, n_hidden_nodes in zip(axes, [10, 100]):
    for ax, alpha in zip(axx, [0.0001, 0.01, 0.1, 1]):
        mlp = MLPClassifier(solver='lbfgs', random_state=0, max_iter=1000, hidden_layer_sizes=[n_hidden_nodes, n_hidden_nodes], alpha=alpha)
        mlp.fit(X_train, y_train)
        mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
        mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
        ax.set_title("n_hidden=[{}, {}]\nalpha={:.4f}".format(n_hidden_nodes, n_hidden_nodes, alpha))
_images/dab81b971453635ecb4abe5ac76e581048cc187b2799014519820f7f39e49d3f.png

Observación

  • Como habrá podido comprobar, existen multitud de métodos para gestionar la complejidad de una red neuronal, como el número de capas ocultas, las unidades en cada capa oculta y la regularización (alpha), entre otros. Un rasgo fundamental de las redes neuronales reside en su configuración aleatoria inicial de pesos antes de comenzar el aprendizaje.

  • Esta aleatoriedad influye en el modelo aprendido resultante. Así, el empleo de parámetros idénticos puede dar lugar a modelos distintos debido a semillas aleatorias diferentes. Mientras que este efecto es más pronunciado para redes pequeñas, es menos significativo para redes de tamaño adecuado donde la complejidad se elige apropiadamente.

fig, axes = plt.subplots(2, 4, figsize=(20, 8))

for i, ax in enumerate(axes.ravel()):
    mlp = MLPClassifier(solver='lbfgs', random_state=i, max_iter=2000, hidden_layer_sizes=[100, 100])
    mlp.fit(X_train, y_train)
    mglearn.plots.plot_2d_separator(mlp, X_train, fill=True, alpha=.3, ax=ax)
    mglearn.discrete_scatter(X_train[:, 0], X_train[:, 1], y_train, ax=ax)
_images/b4a1d6f24acb30184784b60f1745e36e70aeb1606cb263500d408e617ae1aa74.png
  • Para una comprensión más completa de las redes neuronales en escenarios prácticos, implementaremos el MLPClassifier en el conjunto de datos Breast Cancer. Nuestro paso inicial consiste en utilizar los parámetros por defecto. Queda como tarea para el estudiante, utilizar GridSearchCV y Pipeline.

from sklearn.datasets import load_breast_cancer
cancer = load_breast_cancer()
print("Cancer data per-feature maxima:\n{}".format(cancer.data.max(axis=0)))
Cancer data per-feature maxima:
[2.811e+01 3.928e+01 1.885e+02 2.501e+03 1.634e-01 3.454e-01 4.268e-01
 2.012e-01 3.040e-01 9.744e-02 2.873e+00 4.885e+00 2.198e+01 5.422e+02
 3.113e-02 1.354e-01 3.960e-01 5.279e-02 7.895e-02 2.984e-02 3.604e+01
 4.954e+01 2.512e+02 4.254e+03 2.226e-01 1.058e+00 1.252e+00 2.910e-01
 6.638e-01 2.075e-01]
X_train, X_test, y_train, y_test = train_test_split(cancer.data, cancer.target, random_state=0)
mlp = MLPClassifier(random_state=42)
mlp.fit(X_train, y_train)
MLPClassifier(random_state=42)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print("Accuracy on training set: {:.2f}".format(mlp.score(X_train, y_train)))
print("Accuracy on test set: {:.2f}".format(mlp.score(X_test, y_test)))
Accuracy on training set: 0.94
Accuracy on test set: 0.92
  • El MLP muestra una precisión notable, aunque no a la par de otros modelos. Al igual que en el caso anterior del SVC, esta discrepancia se debe probablemente al escalado de los datos. Las redes neuronales exigen una varianza uniforme entre las características de entrada, idealmente con una media de 0 y una varianza de 1.

  • Para satisfacer estos criterios, los datos deben reescalarse. Mientras que aquí estamos implementando este proceso manualmente, en el capítulo Evaluación de modelos y Pipelines se usa StandardScaler para el manejo automatizado de este procedimiento

  • Calculamos el valor medio por característica en el conjunto de entrenamiento

mean_on_train = X_train.mean(axis=0)
  • Calculamos la desviación estándar de cada característica en el conjunto de entrenamiento

std_on_train = X_train.std(axis=0)
  • Procedemos con el proceso de estandarización a media 0 y desviación 1

X_train_scaled = (X_train - mean_on_train) / std_on_train
X_test_scaled  = (X_test - mean_on_train) / std_on_train
mlp = MLPClassifier(random_state=0, max_iter=400)
mlp.fit(X_train_scaled, y_train)
MLPClassifier(max_iter=400, random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print("Accuracy on training set: {:.3f}".format(mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
Accuracy on training set: 1.000
Accuracy on test set: 0.972
  • Dado que se aprecia una disparidad entre el rendimiento de entrenamiento y el de prueba, se puede intentar mejorar el rendimiento de generalización reduciendo la complejidad del modelo. En este escenario, optamos por intensificar el parámetro "alpha" (de 0.0001 a 1) para imponer una regularización más potente de los pesos

mlp = MLPClassifier(max_iter=1000, alpha=1, random_state=0)
mlp.fit(X_train_scaled, y_train)
MLPClassifier(alpha=1, max_iter=1000, random_state=0)
In a Jupyter environment, please rerun this cell to show the HTML representation or trust the notebook.
On GitHub, the HTML representation is unable to render, please try loading this page with nbviewer.org.
print("Accuracy on training set: {:.3f}".format(mlp.score(X_train_scaled, y_train)))
print("Accuracy on test set: {:.3f}".format(mlp.score(X_test_scaled, y_test)))
Accuracy on training set: 0.988
Accuracy on test set: 0.972
  • Descubrir lo que ha aprendido una red neuronal suele ser todo un reto. Una técnica para comprender mejor los conocimientos adquiridos consiste en analizar los pesos del modelo. Puede ver un ejemplo en la galería de scikit-learn. Sin embargo, para el conjunto de datos de Cáncer de Mama, la comprensión puede ser algo desafiante.

  • La siguiente rutina muestra los pesos aprendidos, conectando la entrada a la primera capa oculta. Las filas corresponden a las 30 características de entrada, mientras que las columnas corresponden a las 100 unidades ocultas.

import seaborn as sns
plt.figure(figsize=(20, 15))
sns.set(font_scale=1.8)
plt.imshow(mlp.coefs_[0], interpolation='none', cmap='viridis', aspect='auto')
plt.yticks(range(30), cancer.feature_names)
plt.xlabel("Columns in weight matrix")
plt.ylabel("Input feature")
plt.colorbar();
_images/277e080f05fc51092dd4841cb293c2c1612df7b878b52a92822dbd3fbee2fdce.png
  • Una posible inferencia que podemos hacer es que las características que tienen pesos muy pequeños para todas las unidades ocultas son "menos importantes" para el modelo. Podemos ver que “mean smoothness” y “mean compactness”, además de las características encontradas entre “smoothness error” y “fractal dimension error”, tienen pesos relativamente bajos comparados con otras características. Esto podría significar que se trata de rasgos menos relevantes o, posiblemente, que no las representamos de forma que la red neuronal pudiera utilizarlas.

  • También podemos visualizar los pesos que conectan la capa oculta con la capa de salida, pero son aún más difíciles de interpretar. Aunque MLPClassifier y MLPRegressor proporcionan interfaces fáciles de usar para las arquitecturas de redes neuronales más comunes, solo capturan un pequeño subconjunto de lo que es posible con las redes neuronales. Si está interesado en trabajar con modelos más flexibles o modelos más grandes, se recomienda mirar más allá de scikit-learn en las fantásticas bibliotecas de aprendizaje profundo.

  • Para los usuarios de Python, las más conocidas son keras, lasagna y `tensor-flow. Estas bibliotecas proporcionan una interfaz mucho más flexible para construir redes neuronales y seguir el rápido progreso en la investigación del aprendizaje profundo, también permiten el uso de unidades de procesamiento gráfico (GPU) de alto rendimiento que scikit-learn no soporta.

10.12. Análisis de Malware por API calls#

  • El siguiente conjunto de datos forma parte de una investigación sobre la detección y clasificación de Malware mediante Deep Learning (ver Oliveira, Angelo; Sassi, Renato José (2019)). Contiene 42.797 secuencias de llamadas a la API de malware y 1.079 secuencias de llamadas a la API de goodware. Cada secuencia de llamadas a la API se compone de las 100 primeras llamadas a la API consecutivas no repetidas asociadas al proceso principal, extraídas de los elementos “calls” de los informes de Cuckoo Sandbox.

  • Características

      • Nombre de la columna: hash

      • Descripción: El hash MD5 del ejemplo

      • Tipo: Cadena de 32 bytes

      • Nombre de columna: t_0, t_1,…, t99

      • Descripción: Llamada a la API

      • Tipo: Entero (0-306)

      • Nombre de columna: malware

      • Descripción: Clase

      • Tipo: Entero: 0 (Goodware) o 1 (Malware)

_images/trojan_horse_malware.png

Fig. 10.24 Gráfico de comportamiento del malware troyano con hash MD5 ac65ce897a1f0dc273e8dc54fe3768ec.#

import warnings
warnings.filterwarnings('ignore')
import numpy as np
import pylab as pl
import pandas as pd
import matplotlib.pyplot as plt 
import seaborn as sns
import random
from sklearn.utils import shuffle
from sklearn.svm import SVC
from sklearn.metrics import confusion_matrix,classification_report
from sklearn.model_selection import cross_val_score, GridSearchCV
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.neural_network import MLPClassifier
from sklearn.model_selection import train_test_split
import pickle
import glob
from sklearn.metrics import f1_score
from sklearn.metrics import classification_report
from sklearn.metrics import roc_curve
from sklearn.metrics import precision_recall_curve
from time import process_time
data = pd.read_csv("https://raw.githubusercontent.com/lihkir/Data/main/api_call_sequence_per_malware.csv")
data.shape
(43876, 102)
  • La siguiente es una construida en el presente curso, la cual puede ser útil para quienes solo desean presentar unas cuantas columnas al inicio y al final de cada pandas. Solo requiere del nombre del pandas y el número de columnas que deseamos visualziar al inicio y al final. Es bastante util, para utilizar en los Jupyter Books, donde el espacio horizontal es reducido.

def idx_lr(data, n_new_cols):
    n_data_cols = len(data.columns)
    idx_l  = sorted(list(range(n_new_cols - 1, -1, -1)))
    idx_r  = sorted(list(range(n_data_cols - 1, n_data_cols - n_new_cols - 1, -1)))
    return idx_l + idx_r
  • Para imprimir por ejemplo las tres primeras columnas al inicio y al final del pandas data utilizamos la orden data.iloc[:, idx_lr(data, 4)]. Nótese que no contamos con datos faltantes que requieran de un procedimiento de imputación (ver Análisis con datos faltantes). Además, nótese que todas las columnas con la excepción de hash corresponde a datos numericos discretos.

data.iloc[:, idx_lr(data, 4)].head()
hash t_0 t_1 t_2 t_97 t_98 t_99 malware
0 071e8c3f8922e186e57548cd4c703a5d 112 274 158 208 56 71 1
1 33f8e6d08a6aae939f25a8e0d63dd523 82 208 187 171 215 35 1
2 b68abd064e975e1c6d5f25e748663076 16 110 240 65 113 112 1
3 72049be7bd30ea61297ea624ae198067 82 208 187 302 228 302 1
4 c9b3700a77facf29172f32df6bc77f48 82 240 117 260 141 260 1
print("# NaN values:", data.isna().sum().sum())
# NaN values: 0
data.dtypes
hash       object
t_0         int64
t_1         int64
t_2         int64
t_3         int64
            ...  
t_96        int64
t_97        int64
t_98        int64
t_99        int64
malware     int64
Length: 102, dtype: object
  • Pasamos ahora a eliminar aquellas variables (columnas) irrelevantes en el análisis predictivo, a saber la columna hash del pandas data y creamos uno nuevo al cual nombraremos data_new.

data_new = data.drop(columns=['hash'], axis=1)
data_new.shape
(43876, 101)
  • Verifiquemos si nuestros datos están desbalanceados respecto a las clases de interés 0 (Goodware) o 1 (Malware)

import matplotlib
matplotlib.rc_file_defaults()
cnt_pro = data_new['malware'].value_counts()
sns.barplot(x=cnt_pro.index, y=cnt_pro.values, alpha=0.8)
plt.ylabel('Number of data', fontsize=10)
plt.xlabel('Malware Type', fontsize=10);
_images/a397b2a059e1eaf76e02b3f57721844d8e6cd469ff82f714bd2fff1d1ed85b27.png
  • Claramente, existe un desbalance entre los tipos de Malware. Por otro lado, verifiquemos para algunos Hash, como se distribuyen la frecuencia de cada una de sus API calls. Las columnas representan las frecuencias de cada API call representado por los valores t_0, t_1,..., t99.

fig, axs = plt.subplots(nrows=2, ncols=2, figsize=(15, 12))
plt.subplots_adjust(hspace=0.2)
fig.suptitle("Malware API Calls by Hash", fontsize=14, y=0.92)

for i, ax in zip(range(1, 5), axs.ravel()):
    plt.subplot(2, 2, i)
    n_hash = random.randint(0, len(data_new))
    data_new.iloc[n_hash,:-1].value_counts().plot(kind='bar', xlabel='Hash: '+ data['hash'][n_hash], ylabel='API Call');
_images/18cde9be2d27606a6bb9ca0683ecc9d63c42ac685bdd4e44e774e7408ab1aee6.png
  • Procedemos a hacer uso de make_pipeline y GridSearchCV para obtener parámetros del mejor modelo. En este caso utilizaremos el clasificador MLPClassifier, el cual se estudió en clase de forma analítica.

\[ \boldsymbol{\theta}^{(i)}=\boldsymbol{\theta}^{(i-1)}-\mu_{i}\nabla J(\boldsymbol{\theta}^{(i-1)}),\quad\text{Gradiente descendente}. \]
  • El algoritmo clasificador MLPClassifier denota el valor de la longitud de paso \(\mu_{i}\) como learning_rate, el cual asume los valores constant, invscaling, adaptive. En este ejemplo se ha probado con los tres parámetros, obteniendo siempre el mejor score cuando learning_rate = constant. Por otro lado, hidden_layer_sizes representa el número de neuronas en el \(i\)th layer. En este problema de clasificación optamos por seleccionar desde una a tres capas escondidas (hidden layers), pero siempre el mejor score entregado por GridSearchCV, fue obtenido considerando una sola capa. Por lo tanto, en este problema hacemos solo un grid search sobre el número de neuronas sobre una sola capa.

  • Dado que nuestro problema es de clasificación binaria, también consideramos en el param_grid, solo activation = logistic. Las opciones del clasificador son identity, logistic, tanh, relu, pero GridSearchCV entregó el mejor score usando logistic como era de esperarse. El parámetro alpha es la fuerza del término de regularización \(L^2\), similar al utilizado en la regresión ridge cuando deseamos reducir la complejidad de un modelo. Puede revisar la documentación del clasificador para ver todos los parámetros asociados, y aquellos que son por defecto (ver MLPClassifier).

_images/final_neural_network.png

Fig. 10.25 Red neuronal feed-forward de tres capas.#

La capa de entrada

  • En general las ANNs tienen exactamente una capa de entrada. Con respecto al número de neuronas que componen esta capa, este parámetro se determina de forma completa y única, una vez que se conoce la forma de los datos de entrenamiento. En concreto, el número de neuronas que componen esa capa es igual al número de características (columnas) de sus datos. Algunas configuraciones de ANNs añaden un nodo adicional para un término de sesgo.

La capa de salida

  • Al igual que la capa de entrada, cada ANN tiene exactamente una capa de salida. Determinar su tamaño (número de neuronas) es sencillo; está completamente determinado por la configuración del modelo elegido. Si la ANN es un regresor, la capa de salida tiene un solo nodo (Time Series Forecasting). Si la ANN es un clasificador, entonces también tiene un solo nodo, a menos que se utilice softmax, en cuyo caso la capa de salida tiene un nodo por cada etiqueta de clase en su modelo.

Las capas ocultas

  • ¿Cuántas capas ocultas?. Si los datos son linealmente separables (lo que a menudo se sabe cuando se empieza a codificar una ANN, SVM puede servir de test), entonces no se necesita ninguna capa oculta. Por supuesto, tampoco se necesita una ANN para resolver los datos, pero está seguirá haciendo su trabajo.

  • Sobre la configuración de las capas ocultas en las ANNs, existe un consenso dentro de este tema, y es la diferencia de rendimiento al añadir capas ocultas adicionales: las situaciones en las que el rendimiento mejora con una segunda (o tercera, etc.) capa oculta son muy pocas. Una capa oculta es suficiente para la gran mayoría de los problemas.

  • Entonces, ¿qué pasa con el tamaño de la(s) capa(s) oculta(s), cuántas neuronas?. Existen algunas reglas empíricas; de ellas, la más utilizada es ‘The optimal size of the hidden layer is usually between the size of the input and size of the output layers’. Jeff Heaton, the author of Introduction to Neural Networks in Java.

  • Hay una regla empírica adicional que ayuda en los problemas de aprendizaje supervisado. Normalmente se puede evitar el sobreajuste si se mantiene el número de neuronas por debajo de:

    \[ N_{h}=\frac{N_{s}}{(\alpha\cdot(N_{i}+N_{o}))} \]
    • \(N_{i}=\) número de neuronas de entrada

    • \(N_{o}=\) número de neuronas de salida

    • \(N_{s}=\) número de muestras en el conjunto de datos de entrenamiento

    • \(\alpha=\) un factor de escala arbitrario, normalmente 2-10

  • Un valor de \(\alpha=2\) suele funcionar sin sobreajustar. Se puede pensar en \(\alpha\) como el factor de ramificación efectivo o el número de pesos distintos de cero para cada neurona. Las capas de salida harán que el factor de ramificación “efectivo” sea muy inferior al factor de ramificación medio real de la red. Para profundizar mas en el diseño de redes neuronales, ver el siguiente texto de Martin Hagan.

  • En resumen, para la mayoría de los problemas, probablemente se podría obtener un rendimiento decente (incluso sin un segundo paso de optimización) estableciendo la configuración de la capa oculta utilizando sólo dos reglas:

    • el número de capas ocultas es igual a uno

    • el número de neuronas de esa capa es la media de las neuronas de las capas de entrada y salida. ¡Nótese que en el ejemplo de esta sección el número de columnas para \(X\) es 100!.

  • Pasamos ahora a implementar un modelo de clasificación para el conjunto de datos relacionados con: Análisis de Malware por API calls. Utilizaremos la clase MLPClassifier y como preprocesamiento, estandarizaremos nuestros datos usando la clase StandardScaler. Para encontrar los mejores parámetros y evita problemas de data leakage, utilizaremos Pipeline y GridSearchCV tal como se explicó en secciones anteriores.

pipe = make_pipeline(StandardScaler(), MLPClassifier(max_iter=10000, random_state=42))
param_grid = {'mlpclassifier__alpha': [0.1, 0.01, 0.001],
              'mlpclassifier__hidden_layer_sizes': [(10,), (100,), (1000,)],
              'mlpclassifier__activation': ['logistic'],
              'mlpclassifier__learning_rate': ['constant']}
  • Construimos nuestra matriz de caracteristicas, o variable explicativa X y el vector de clases o la variable respuesta y

y = data_new['malware']
X = data_new.drop(['malware'], axis=1)
  • Realizamos la división de nuestros datos a priori, con el objetivo de evitar data leakage. Consideramos el mismo random_state utilizado en el clasificador, con el objetivo de que los resutlados sean reproducibles.

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
  • El siguiente ciclo if ha sido implementado en este curso para evitar reentrenar nuestro modelo clasificador. Nótese que si el algoritmo no detecta que existe un archivo de extensión .pkl, reentrena el modelo, de lo contrario, utiliza el modelo entrenado a priori, el cual ha sido guardado en el archivo .pkl. Los archivo Pickle de extensión .pkl en simples palabras se encargan de serializar un objeto, esto es transformar el mismo en una cadena de bytes única que puede ser guardada en un archivo (de extensión .pkl), archivo que podemos desempaquetar después y trabajar con su contenido. Para poder utlizarlo necesitamos importarlo con la orden import pickle, e instalar la librería (Pickel viene por defecto desde Python 3.9) si así se requiere utilizando:

pip install pickle
from joblib import dump, load
grid = None

if (len(glob.glob("grid_mlp.joblib")) != 0):
    grid = load('grid_mlp.joblib')
else:
    time_start = process_time()
    grid = GridSearchCV(pipe, param_grid, cv=5, n_jobs=-1, scoring='roc_auc')
    grid.fit(X_train, y_train)
    time_stop = process_time()
    str_cpu_time = "GridSearchCV CPU time: " + str((time_stop-time_start)*0.6) + " minutes"
    print(str_cpu_time) 
    with open('cpu_time.txt', 'w', encoding='utf-8') as f:
        f.write(str_cpu_time)
    dump(grid, 'grid_mlp.joblib')

print("Best CV score = %0.3f:" % grid.best_score_)
print("Best parameters:\n{}".format(grid.best_params_))
Best CV score = 0.958:
Best parameters:
{'mlpclassifier__activation': 'logistic', 'mlpclassifier__alpha': 0.01, 'mlpclassifier__hidden_layer_sizes': (100,), 'mlpclassifier__learning_rate': 'constant'}
  • Utilizamos el f1_score, el cual, de acuerdo a lo estudiado en secciones previas, calcula la media armonica entre precision y recall

print(f"F1 Score of the classifier is: {f1_score(y_test, grid.predict(X_test))}")
F1 Score of the classifier is: 0.9937633808061063
  • Además podemos usar classification_report y la matriz de confusión, para identificar aquellas clases que fueron incorrectamente clasificadas por nuestro modelo

print(classification_report(y_test, grid.predict(X_test)))
              precision    recall  f1-score   support

           0       0.94      0.56      0.70       283
           1       0.99      1.00      0.99     10686

    accuracy                           0.99     10969
   macro avg       0.96      0.78      0.85     10969
weighted avg       0.99      0.99      0.99     10969
mlp_cm = confusion_matrix(y_test, grid.predict(X_test))
f, ax = plt.subplots(figsize=(5,5))
sns.heatmap(mlp_cm, annot=True, linewidth=0.7, linecolor='black', fmt='g', ax=ax, cmap="BuPu")
plt.title('MLP Confusion Matrix')
plt.xlabel('Y predict')
plt.ylabel('Y test')
plt.show()
_images/98e9c819a3f79440e7ffc629470627a7e5aa39f58b08fb1deb64613295fa2688.png
  • Nótese que la clase 0, presenta el mayor número de clasificaciones erroneas (FP), obtenidas por nuestra ANN. Justamente esto se debe al desbalance notorio en nuestros datos, el cual se inclina hacia la clases 1, de mayor frecuencia. Si se tiene clara una métrica de negocio o un punto de operación, podríamos ajustar el número de prediccion incorrectas a favor de este punto de operación, tal como en el problema de detección de cancer, donde el interés era reducir (FN). Para analizar el comportamiento del clasificador a diferentes umbrales, utilizamos la curva ROC y precision-recall

fpr_mlp, tpr_mlp, thresholds_mlp = roc_curve(y_test, grid.predict_proba(X_test)[:, 1])
plt.plot(fpr_mlp, tpr_mlp, label="ROC Curve MLP")
plt.xlabel("FPR")
plt.ylabel("TPR (recall)")
close_default_mlp = np.argmin(np.abs(thresholds_mlp - 0.5))
plt.plot(fpr_mlp[close_default_mlp], tpr_mlp[close_default_mlp], '^', 
         markersize=10, label="threshold 0.5 MLP", fillstyle="none", c='k', mew=2)
plt.legend(loc=4);
_images/5d32aa09e306c54377c2dad6a66311f83f79a86f386c042211e4db8af122507a.png
  • Nótese que la tasa TPR se mantiene aproximadamente contante para valores de FPR>0.2, esto es, para esta tasas de FPR no se está sacrificando la tasa recall. Si deseamos mantener un recall alto, aproximadamente, mayor que 0.8, podemos considerar por ejemplo una tasa FPR en torno a 0.1, con la que no estaríamos sacrificando recall y además, obtendriamos un tasa de falsos positivos FPR relativamente baja.

precision_mlp, recall_mlp, thresholds_mlp = precision_recall_curve(y_test, grid.predict_proba(X_test)[:, 1])
plt.plot(precision_mlp, recall_mlp, label="MLP")
close_default_mlp = np.argmin(np.abs(thresholds_mlp - 0.5))
plt.plot(precision_mlp[close_default_mlp], recall_mlp[close_default_mlp], '^', c='k',
         markersize=10, label="threshold 0.5 MLP", fillstyle="none", mew=2)
plt.xlabel("Precision")
plt.ylabel("Recall")
plt.legend(loc="best");
_images/859905d45090955a392bb04e073feb8f84b406d55a534585babfb9cfd7261e0f.png
  • Nótese que con el umbral por defecto, la curva precision-recall muestra una clasificación con scores bastante buenos para cada una de estas métricas, incluso, si movemos el umbral un poco, para favorecer la clase 0 que es la menos frecuente, vamos a obtener de igual manera un rango de valores altos para precision y recall. Sólo valores de precision muy elevados, esto es, valores cuya distancia 1 tiende a cero, estaríamos sacrificando recall.

10.13. Aplicación: Predicción de series temporales#

Series de tiempo

  • Uno de los enfoques clásicos para la predicción de series temporales es el modelo autorregresivo de orden \(p\), denotado como \(AR(p)\), el cual modela el valor actual \(y_t\) como una combinación lineal de sus \(p\) valores pasados:

    \[ y_t = \omega_0 + \sum_{i=1}^{p} \omega_i y_{t-i} + \varepsilon_t, \]

    donde \(\omega_i\) son coeficientes del modelo y \(\varepsilon_t\) es un término de error con media cero y varianza constante.

  • Este modelo puede generalizarse mediante una función no lineal \(f\):

    \[ y_t = f(y_{t-1}, y_{t-2}, \dots, y_{t-p}), \]

    donde \(f\) puede ser aprendida utilizando redes neuronales artificiales. En este capítulo se consideran tres arquitecturas neuronales para aproximar \(f\), cada una definida por su número de capas ocultas, neuronas por capa y función de activación. El entrenamiento de los modelos se realiza mediante el algoritmo de backpropagation del error o sus variantes.

  • En esta sección, utilizaremos MLP para desarrollar modelos de predicción de series temporales. El conjunto de datos utilizado para estos ejemplos es sobre la contaminación atmosférica medida por la concentración de partículas (PM) de diámetro inferior o igual a 2.5 micrómetros.

  • Hay otras variables, como la presión atmosférica, temperatura del aire, punto de rocío, etc. Se han desarrollado un par de modelos de series temporales, uno sobre la presión atmosférica PRES y otro sobre pm 2.5. El conjunto de datos se ha descargado del repositorio de aprendizaje automático de la UCI.

import warnings
from matplotlib import pyplot as plt
import seaborn as sns

warnings.filterwarnings("ignore")
sns.set_style("darkgrid")
import sys
import pandas as pd
import numpy as np
import datetime
from sklearn.preprocessing import MinMaxScaler
import tensorflow as tf
from tensorflow import keras
from keras.layers import Dense, Input, Dropout
from keras.optimizers import SGD
from keras.models import Model
from keras.models import load_model
from keras.callbacks import ModelCheckpoint
import os
from sklearn.metrics import r2_score
from sklearn.metrics import mean_absolute_error
2025-04-05 19:28:06.564687: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2025-04-05 19:28:06.860171: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2025-04-05 19:28:06.972003: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2025-04-05 19:28:06.992936: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2025-04-05 19:28:07.141852: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-04-05 19:28:08.310045: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
df = pd.read_csv('https://raw.githubusercontent.com/lihkir/Data/refs/heads/main/PRSA_data_2010.1.1-2014.12.31.csv', usecols=lambda column: column not in ['DEWP', 'TEMP', 'cbwd', 'Iws',	'Is', 'Ir'])
devices = tf.config.list_physical_devices()
print("Available devices:")
for device in devices:
    print(device)

gpus = tf.config.list_physical_devices('GPU')
if gpus:
    print("TensorFlow is using GPU.")
    for gpu in gpus:
        gpu_details = tf.config.experimental.get_device_details(gpu)
        print(f"GPU details: {gpu_details}")
else:
    print("TensorFlow is not using GPU.")
Available devices:
PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU')
PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')
TensorFlow is using GPU.
GPU details: {'compute_capability': (8, 9), 'device_name': 'NVIDIA GeForce RTX 4070'}
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1743899291.679266    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899292.553202    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899292.553247    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899292.554421    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
print('Shape of the dataframe:', df.shape)
Shape of the dataframe: (43824, 7)
df.head()
No year month day hour pm2.5 PRES
0 1 2010 1 1 0 NaN 1021.0
1 2 2010 1 1 1 NaN 1020.0
2 3 2010 1 1 2 NaN 1019.0
3 4 2010 1 1 3 NaN 1019.0
4 5 2010 1 1 4 NaN 1018.0
  • Para asegurarse de que las filas están en el orden correcto de fecha y hora de las observaciones, se crea una nueva columna datetime a partir de las columnas relacionadas con la fecha y la hora del DataFrame. La nueva columna se compone de objetos datetime.datetime de Python. El DataFrame se ordena en orden ascendente sobre esta columna

df['datetime'] = df[['year', 'month', 'day', 'hour']].apply(lambda row: datetime.datetime(year=row['year'], month=row['month'], day=row['day'],
                                                                                          hour=row['hour']), axis=1)
df.sort_values('datetime', ascending=True, inplace=True)
df.head()
No year month day hour pm2.5 PRES datetime
0 1 2010 1 1 0 NaN 1021.0 2010-01-01 00:00:00
1 2 2010 1 1 1 NaN 1020.0 2010-01-01 01:00:00
2 3 2010 1 1 2 NaN 1019.0 2010-01-01 02:00:00
3 4 2010 1 1 3 NaN 1019.0 2010-01-01 03:00:00
4 5 2010 1 1 4 NaN 1018.0 2010-01-01 04:00:00
  • Dibujamos un diagrama de cajas para visualizar la tendencia central y la dispersión de por ejemplo la columna PRES

plot_fontsize = 18;
import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1)  
g1 = sns.boxplot(x=df['PRES'], orient="h")  # Removed palette
g1.set_title('Box plot of Air Pressure', fontsize=plot_fontsize)
g1.set_xlabel('Air Pressure readings in hPa', fontsize=plot_fontsize)

plt.subplot(1, 2, 2) 
g2 = sns.boxplot(x=df['pm2.5'], orient="h")  # Removed palette
g2.set_title('Box plot of PM2.5', fontsize=plot_fontsize)
g2.set_xlabel('PM2.5 readings in µg/m³', fontsize=plot_fontsize)

plt.show()
_images/75f93cf24e52982e1061e3e9dac518591a3f497e8ed8be2e97e1ad278c2dc241.png
plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1) 
g1 = sns.lineplot(x=df.index, y=df['PRES'])  # Using x and y parameters instead of data
g1.set_title('Time series of Air Pressure', fontsize=plot_fontsize)
g1.set_xlabel('Index', fontsize=plot_fontsize)
g1.set_ylabel('Air Pressure readings in hPa', fontsize=plot_fontsize)

plt.subplot(1, 2, 2)  
g2 = sns.lineplot(x=df.index, y=df['pm2.5'])  # Using x and y parameters instead of data
g2.set_title('Time series of PM2.5', fontsize=plot_fontsize)
g2.set_xlabel('Index', fontsize=plot_fontsize)
g2.set_ylabel('PM2.5 readings in µg/m³', fontsize=plot_fontsize)

plt.show()
_images/6699c78f1a132a300c9a158cf8b2ae9e4241ca030e8416bc0ecfcbe12722ef42.png
  • Los algoritmos de gradiente descendente funcionan mejor (por ejemplo, convergen más rápido) si las variables están dentro del intervalo \([-1, 1]\). Muchas fuentes relajan el límite hasta \([-3, 3]\). La variable PRES es escalada con minmax para limitar la variable transformada dentro de \([0,1]\).

Observación

  • Antes de entrenar el modelo, el conjunto de datos se divide en dos partes: el conjunto de entrenamiento y el conjunto de validación. La red neuronal se entrena en el conjunto de entrenamiento. Esto significa que el cálculo de la función de pérdida, la propagación hacia atrás y los pesos actualizados mediante un algoritmo de gradiente descendente se realizan en el conjunto de entrenamiento.

  • El conjunto de validación se utiliza para evaluar el modelo y determinar el número de épocas en su entrenamiento. Aumentar el número de épocas reducirá aún más la función de pérdida en el conjunto de entrenamiento, pero no necesariamente tendrá el mismo efecto en el conjunto de validación debido al sobreajuste en el conjunto de entrenamiento, por lo que el número de épocas se controla manteniendo una evaluación y verificación sobre la función de pérdida calculada para el conjunto de validación.

  • Utilizamos Keras con el backend Tensorflow para definir y entrenar el modelo. Todos los pasos implicados en el entrenamiento y validación del modelo se realizan llamando a las funciones apropiadas de la API de Keras.

  • Los cuatro primeros años, de 2010 a 2013, se utilizan como entrenamiento y 2014 se utiliza para la validación

from sklearn.preprocessing import MinMaxScaler
import datetime

split_date = datetime.datetime(year=2014, month=1, day=1, hour=0)
df_train = df.loc[df['datetime'] < split_date].copy()
df_val = df.loc[df['datetime'] >= split_date].copy()

scaler_pres = MinMaxScaler(feature_range=(0, 1))
df_train['scaled_PRES'] = scaler_pres.fit_transform(df_train[['PRES']])
df_val['scaled_PRES'] = scaler_pres.transform(df_val[['PRES']])

print('Shape of train:', df_train.shape)
print('Shape of val:', df_val.shape)
Shape of train: (35064, 9)
Shape of val: (8760, 9)
df_train.head()
No year month day hour pm2.5 PRES datetime scaled_PRES
0 1 2010 1 1 0 NaN 1021.0 2010-01-01 00:00:00 0.545455
1 2 2010 1 1 1 NaN 1020.0 2010-01-01 01:00:00 0.527273
2 3 2010 1 1 2 NaN 1019.0 2010-01-01 02:00:00 0.509091
3 4 2010 1 1 3 NaN 1019.0 2010-01-01 03:00:00 0.509091
4 5 2010 1 1 4 NaN 1018.0 2010-01-01 04:00:00 0.490909
df_val.head()
No year month day hour pm2.5 PRES datetime scaled_PRES
35064 35065 2014 1 1 0 24.0 1014.0 2014-01-01 00:00:00 0.418182
35065 35066 2014 1 1 1 53.0 1013.0 2014-01-01 01:00:00 0.400000
35066 35067 2014 1 1 2 65.0 1013.0 2014-01-01 02:00:00 0.400000
35067 35068 2014 1 1 3 70.0 1013.0 2014-01-01 03:00:00 0.400000
35068 35069 2014 1 1 4 79.0 1012.0 2014-01-01 04:00:00 0.381818
  • Restablecemos los índices del conjunto de validación

df_val.reset_index(drop=True, inplace=True)
df_val.head()
No year month day hour pm2.5 PRES datetime scaled_PRES
0 35065 2014 1 1 0 24.0 1014.0 2014-01-01 00:00:00 0.418182
1 35066 2014 1 1 1 53.0 1013.0 2014-01-01 01:00:00 0.400000
2 35067 2014 1 1 2 65.0 1013.0 2014-01-01 02:00:00 0.400000
3 35068 2014 1 1 3 70.0 1013.0 2014-01-01 03:00:00 0.400000
4 35069 2014 1 1 4 79.0 1012.0 2014-01-01 04:00:00 0.381818
  • También se grafican las series temporales de entrenamiento y validación normalizadas para PRES.

plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1) 
g1 = sns.lineplot(df_train['PRES'], color='b')
g1.set_title('Time series of scaled Air Pressure in train set', fontsize=plot_fontsize)
g1.set_xlabel('Index', fontsize=plot_fontsize)
g1.set_ylabel('Scaled Air Pressure readings', fontsize=plot_fontsize);

plt.subplot(1, 2, 2)
g2 = sns.lineplot(df_val['PRES'], color='r')
g2.set_title('Time series of scaled Air Pressure in validation set', fontsize=plot_fontsize)
g2.set_xlabel('Index', fontsize=plot_fontsize)
g2.set_ylabel('Scaled Air Pressure readings', fontsize=plot_fontsize);

plt.show()
_images/27879440dfd76fc0279ce3405b17b58fe966be5d05ebe66e10a372298adea97f.png

Regresores y Variable Objetivo

  • Ahora necesitamos generar los regresores (\(X\)) y la variable objetivo (\(y\)) para el entrenamiento y la validación. La matriz bidimensional de regresores y la matriz unidimensional objetivo se crean a partir de la matriz unidimensional original de la columna standardized_PRES en el DataFrame.

  • Para el modelo de predicción de series temporales, de este ejemplo, se utilizan las observaciones de los últimos siete días para predecir el día siguiente. Esto equivale a un modelo \(AR(7)\). Definimos una función que toma la serie temporal original y el número de pasos temporales en regresores como entrada para generar las matrices \(X\) e \(y\)

def makeXy(ts, nb_timesteps):
    """
    Input: 
           ts: original time series
           nb_timesteps: number of time steps in the regressors
    Output: 
           X: 2-D array of regressors
           y: 1-D array of target 
    """
    X = []
    y = []
    for i in range(nb_timesteps, ts.shape[0]):
        if i-nb_timesteps <= 4:
            print(i-nb_timesteps, i-1, i)
        X.append(list(ts.loc[i-nb_timesteps:i-1])) #Regressors
        y.append(ts.loc[i]) #Target
    X, y = np.array(X), np.array(y)
    return X, y
X_train, y_train = makeXy(df_train['scaled_PRES'], 7)
0 6 7
1 7 8
2 8 9
3 9 10
4 10 11
print('Shape of train arrays:', X_train.shape, y_train.shape)
Shape of train arrays: (35057, 7) (35057,)
X_val, y_val = makeXy(df_val['scaled_PRES'], 7)
0 6 7
1 7 8
2 8 9
3 9 10
4 10 11
print('Shape of validation arrays:', X_val.shape, y_val.shape)
Shape of validation arrays: (8753, 7) (8753,)
  • Ahora definimos la red MLP utilizando la API funcional de Keras. En este enfoque una capa puede ser declarada como la entrada de la siguiente capa en el momento de definir la siguiente

input_layer = Input(shape=(7,), dtype='float32')
  • En este caso, Input es una función que se utiliza para crear una capa de entrada en un modelo de red neuronal. shape=(7,) específica la forma de los datos de entrada. En este caso, significa que los datos de entrada tendrán 7 dimensiones. dtype='float32' especifica el tipo de datos de los elementos de la capa de entrada. En este caso, son números de punto flotante de 32 bits.

  • Las capas densas las definimos en esta caso con activación tanh. Puede utilizar un GridSearch tal como se hizo en el curso de Machine Learning para encontrar hiperparámetros adecuados minimizando las métricas de regresión.

dense1 = Dense(32, activation='tanh')(input_layer)
dense2 = Dense(16, activation='tanh')(dense1)
dense3 = Dense(16, activation='tanh')(dense2)
I0000 00:00:1743899294.415427    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899294.415492    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899294.415514    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899294.573292    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743899294.573359    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-04-05 19:28:14.573367: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2112] Could not identify NUMA node of platform GPU id 0, defaulting to 0.  Your kernel may not have been built with NUMA support.
I0000 00:00:1743899294.573404    1786 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-04-05 19:28:14.573961: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2021] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 9558 MB memory:  -> device: 0, name: NVIDIA GeForce RTX 4070, pci bus id: 0000:01:00.0, compute capability: 8.9

Dense e input_layer

  • Dense: Correspone a una capa totalmente conectada (fully connected).

  • Unidades (Neurons): 32 neuronas.

  • dense1: Esta capa toma como entrada input_layer, que puede ser la capa de entrada del modelo u otra capa anterior.

  • dense2: Esta capa toma como entrada la salida de dense1. Esto significa que los 32 valores de salida de dense1 se usan como entrada para dense2. Similarmente, ocurre con dense3

Observación

  • Las múltiples capas ocultas y el gran número de neuronas en cada capa oculta le dan a las redes neuronales la capacidad de modelar la compleja no linealidad de las relaciones subyacentes entre los regresores y el objetivo. Sin embargo, las redes neuronales profundas también pueden sobreajustar los datos de entrenamiento y dar malos resultados en el conjunto de validación o prueba. La función Dropout se ha utilizado eficazmente para regularizar las redes neuronales profundas.

  • En este ejemplo, se añade una capa Dropout antes de la capa de salida. Dropout aleatoriamente establece \(p\) fracción de neuronas de entrada a cero antes de pasar a la siguiente capa. La eliminación aleatoria de entradas actúa esencialmente como un tipo de ensamblaje de modelos de agregación bootstrap. Al apagar neuronas aleatoriamente, Dropout está forzando a la red a no depender excesivamente de ninguna unidad específica, mejorando la generalización.

  • Por ejemplo, el bosque aleatorio utiliza el ensamblaje mediante la construcción de árboles en subconjuntos aleatorios de características de entrada. Utilizamos \(p=0.2\) para descartar el 20% de las características de entrada seleccionadas aleatoriamente.

dropout_layer = Dropout(0.2)(dense3)
  • Por último, la capa de salida predice la presión atmosférica del día siguiente

output_layer = Dense(1, activation='linear')(dropout_layer)
  • Las capas de entrada, densa y de salida se empaquetarán ahora dentro de un modelo, que es una clase envolvente para entrenar y hacer predicciones. Como función de pérdida se utiliza el error cuadrático medio (MSE). Los pesos de la red se optimizan mediante el algoritmo Adam. Adam significa Estimación Adaptativa de Momentos y ha sido una opción popular para el entrenamiento de redes neuronales profundas.

  • A diferencia del Gradiente descendente Estocástico, Adam utiliza diferentes tasas de aprendizaje para cada peso y las actualiza por separado a medida que avanza el entrenamiento. La tasa de aprendizaje de un peso se actualiza basándose en medias móviles ponderadas exponencialmente de los gradientes del peso y los gradientes al cuadrado.

ts_model = Model(inputs=input_layer, outputs=output_layer)
ts_model.compile(loss='mean_squared_error', optimizer='adam')
ts_model.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, 7)              │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 32)             │           256 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_1 (Dense)                 │ (None, 16)             │           528 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_2 (Dense)                 │ (None, 16)             │           272 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 16)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_3 (Dense)                 │ (None, 1)              │            17 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 1,073 (4.19 KB)
 Trainable params: 1,073 (4.19 KB)
 Non-trainable params: 0 (0.00 B)

Observación

  • En este caso, Params # es calculado mediante la fórmula: Param # = #Entradas x #Neuronas + #Neuronas. Esto incluye los pesos de cada conexión entre las entradas y las neuronas y un bias para cada neurona. En este caso Params # = 7x32 + 32 = 256.

  • El modelo se entrena llamando a la función fit() en el objeto modelo y pasándole X_train y y_train. El entrenamiento se realiza para un número predefinido de épocas. Además, batch_size define el número de muestras del conjunto de entrenamiento que se utilizarán para una instancia de backpropagation.

  • El conjunto de datos de validación también se pasa para evaluar el modelo después de cada epoch completa. Un objeto ModelCheckpoint rastrea la función de pérdida en el conjunto de validación y guarda el modelo para la época en la que la función de pérdida ha sido mínima.

save_weights_at = os.path.join('keras_models', 'PRSA_data_Air_Pressure_MLP_weights.{epoch:02d}-{val_loss:.4f}.keras')
save_best = ModelCheckpoint(save_weights_at, monitor='val_loss', verbose=0,
                            save_best_only=True, save_weights_only=False, mode='min', save_freq='epoch');
  • Aquí val_loss es el valor de la función de coste para los datos de validación cruzada y loss es el valor de la función de coste para los datos de entrenamiento. Con verbose=0, no se imprime ningún mensaje en la consola durante el proceso de guardado del modelo. period=1 indica que el modelo se evaluará y potencialmente se guardará después de cada época.

import os
from joblib import dump, load
history_airp = None

if os.path.exists('history_airp.joblib'):
    history_airp = load('history_airp.joblib')
    print("El archivo 'history_airp.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_airp = ts_model.fit(x=X_train, y=y_train, batch_size=16, epochs=20,
             verbose=2, callbacks=[save_best], validation_data=(X_val, y_val),
             shuffle=True);
    dump(history_airp.history, 'history_airp.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_airp.joblib'.")
El archivo 'history_airp.joblib' ya existe. Se ha cargado el historial del entrenamiento.
  • En este caso, el modo verbose=2 muestra una barra de progreso por cada época. Los modos posibles son: 0 para no mostrar nada, 1 para mostrar la barra de progreso, 2 para mostrar una línea por época. Las muestras se mezclan aleatoriamente antes de cada época (shuffle=True).

  • Se hacen predicciones para la presión atmosférica a partir del mejor modelo guardado. Las predicciones del modelo sobre la presión atmosférica escalada, se transforman inversamente para obtener predicciones sobre la presión atmosférica original. También se calcula la bondad de ajuste o R cuadrado.

import os
import re
from tensorflow.keras.models import load_model
model_dir = 'keras_models'
files = os.listdir(model_dir)
pattern = r"PRSA_data_Air_Pressure_MLP_weights\.(\d+)-([\d\.]+)\.keras"

best_val_loss = float('inf')
best_model_file = None
best_model = None

for file in files:
    match = re.match(pattern, file)
    if match:
        epoch = int(match.group(1))
        val_loss = float(match.group(2))
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_model_file = file

if best_model_file:
    best_model_path = os.path.join(model_dir, best_model_file)
    print(f"Cargando el mejor modelo: {best_model_file} con val_loss: {best_val_loss}")
    best_model = load_model(best_model_path)
else:
    print("No se encontraron archivos de modelos que coincidan con el patrón.")
Cargando el mejor modelo: PRSA_data_Air_Pressure_MLP_weights.03-0.0001.keras con val_loss: 0.0001
preds = best_model.predict(X_val)
pred_PRES = scaler_pres.inverse_transform(preds)
pred_PRES = np.squeeze(pred_PRES)
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1743899295.688420    4510 service.cc:146] XLA service 0x7f473c005c30 initialized for platform CUDA (this does not guarantee that XLA will be used). Devices:
I0000 00:00:1743899295.688447    4510 service.cc:154]   StreamExecutor device (0): NVIDIA GeForce RTX 4070, Compute Capability 8.9
2025-04-05 19:28:15.700143: I tensorflow/compiler/mlir/tensorflow/utils/dump_mlir_util.cc:268] disabling MLIR crash reproducer, set env var `MLIR_CRASH_REPRODUCER_DIRECTORY` to enable.
2025-04-05 19:28:15.853743: I external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:531] Loaded cuDNN version 8907
257/274 ━━━━━━━━━━━━━━━━━━━━ 0s 590us/step
I0000 00:00:1743899296.107701    4510 device_compiler.h:188] Compiled cluster using XLA!  This line is logged at most once for the lifetime of the process.
274/274 ━━━━━━━━━━━━━━━━━━━━ 1s 1ms/step
r2 = r2_score(df_val['PRES'].loc[7:], pred_PRES)
print('R-squared for the validation set:', round(r2,4))
R-squared for the validation set: 0.9955
plt.figure(figsize=(5.5, 5.5))
plt.plot(range(50), df_val['PRES'].loc[7:56], linestyle='-', marker='*', color='r')
plt.plot(range(50), pred_PRES[:50], linestyle='-', marker='.', color='b')
plt.legend(['Actual','Predicted'], loc=2)
plt.title('Actual vs Predicted Air Pressure')
plt.ylabel('Air Pressure')
plt.xlabel('Index');
_images/086a245125ab09436fc2ba43a14d8b72c022237f90ecf67b483a07f1a10ce56d.png

10.14. Redes Neuronales Convolucionales#

Introducción

  • Hasta ahora, las redes neuronales se han considerado como receptoras de vectores de características en su capa de entrada, similar a otros clasificadores discutidos anteriormente. Estos vectores se derivan de datos brutos para condensar información relevante para la tarea de aprendizaje automático. Sin embargo, a finales de los años 80, surgió un enfoque alternativo: integrar la generación de características en el entrenamiento de la red neuronal.

  • Así nacieron las Redes Neuronales Convolucionales (CNN), que aprenden características directamente de los datos junto con los parámetros de la red. Su éxito inicial fue en el reconocimiento de dígitos OCR [LeCun et al., 1989]. El término “convolucional” se refiere a que la primera capa realiza convoluciones en lugar de productos internos, usados en redes totalmente conectadas.

10.14.1. La necesidad de convoluciones#

  • Asegurémonos primero de que entendemos la razón por la que no podemos, en la práctica, alimentar la entrada de una red neuronal directamente con datos brutos, por ejemplo, una matriz de imágenes o las muestras de una versión digitalizada de un segmento de voz, y por qué es necesario un preprocesamiento para generar características. Lo mismo ocurre para cualquier predictor/aprendiz y no sólo para las redes neuronales. En muchas aplicaciones, trabajar directamente con los datos en bruto hace que la tarea sea sencillamente inmanejable.

_images/hex.jpg
  • Una imagen de \(256 \times 256\) se vectoriza como \(\boldsymbol{x} \in \mathbb{R}^{65000}\). Si la primera capa tiene \(k_1 = 1000\) nodos, una red totalmente conectada requeriría aproximadamente 65 millones de parámetros \(\theta_{jk}\), con \(j = 1, \dots, 65000\) y \(k = 1, \dots, 1000\). Esta cantidad crece considerablemente con imágenes de mayor resolución (\(1000 \times 1000\)) o a color, donde la entrada se triplica al emplear el modelo RGB.

  • A medida que se incrementan las capas ocultas, también crece el número de parámetros, lo que eleva la carga computacional y compromete la capacidad de generalización, requiriendo grandes volúmenes de datos para evitar el sobreajuste. Además, vectorizar matrices de imágenes implica pérdida de información, al ignorar relaciones espaciales entre píxeles. Por ello, los métodos de generación de características buscan extraer dependencias estadísticas que capturen dichas correlaciones y permitan codificar eficazmente la información relevante para el aprendizaje.

  • Al emplear convoluciones, se pueden abordar simultáneamente ambos problemas, es decir, el de la explosión de parámetros y el de la extracción de información estadística útil. Los pasos básicos de cualquier red convolucional son:

    • la etapa de convolución,

    • el paso de no linealidad,

    • el paso de agrupación.

10.14.2. La etapa de convolución#

  • Una forma de reducir el número de parámetros es mediante el reparto de pesos, como se ha comentado al final de la sección Redes Totalmente Conectadas. Ahora tomaremos prestada esta idea del reparto de pesos y la utilizaremos de una forma más sofisticada. Para ello, nos centraremos en el caso en que la entrada de la red esté formada por imágenes. La imagen matricial de entrada se denomina \(I\).

_images/image_input_conv.png

Fig. 10.26 (A) Imagen matricial de entrada de \(3\times 3\). (B) Nodos de la capa oculta.#

  • Para el caso de la Fig. 10.26, se trata de una matriz de 3 × 3; nótese que la entrada es no vectorizada. Para enfatizar que nos apartaremos de la lógica de las operaciones de multiplicar-añadir (producto interno) de las redes feed-forward completamente conectadas, utilizaremos un símbolo diferente \(h\) en lugar de \(\theta\), para denotar los parámetros asociados.

  • Introducimos ahora el concepto de reparto de pesos. Recordemos que en una red totalmente conectada, cada nodo está asociado con un vector de parámetros, \(\boldsymbol{\theta}_{i}\), para el nodo \(i\)th cuya dimensionalidad es igual al número de nodos de la capa anterior. En cambio, ahora cada nodo estará asociado a un único parámetro. Para ello, dispondremos los nodos en forma de matriz bidimensional, como se muestra en Fig. 10.26 (B). Para el caso de la figura, hemos supuesto cuatro nodos dispuestos en una matriz de 2 × 2, \(H\).

Observación

  • El primer nodo se caracteriza por \(h(1, 1)\), el segundo por \(h(1, 2)\), y así sucesivamente. En otras palabras, cualquier conexión que termine en el primer nodo se multiplicará por el mismo peso \(h(1, 1)\), y un argumento similar es válido para el resto de los nodos. Este razonamiento reduce drásticamente el número de parámetros. Sin embargo, para que esto tenga sentido, tenemos que alejarnos de la lógica de las operaciones de producto interior de las redes totalmente conectadas.

  • Para entender por qué, supongamos que utilizamos un único parámetro por nodo en una red totalmente conectada. Entonces, la salida del combinador lineal asociado al primer nodo sería \(O(1, 1) = h(1, 1)a\), donde \(a\) es la suma de todas las entradas al nodo recibidas de la capa anterior. La salida respectiva del segundo nodo sería \(O(1, 2) = h(1, 2)a\), y así sucesivamente. Por lo tanto, todos los nodos proporcionarían básicamente la misma información con respecto a los valores de entrada; la única diferencia serían los distintos pesos que actúan sobre la misma información de entrada de la capa anterior.

  • Pasemos ahora a introducir un concepto diferente, en el que mantenemos un único parámetro por nodo, aunque cada una de las salidas de una capa oculta transmite información diferente con respecto a las distintas entradas que se reciben de la capa anterior.

  • Para ello, introduciremos las convoluciones. En este contexto, los nodos de la capa oculta se interpretan como elementos de una matriz \(H\), y convolucionamos \(H\) con la matriz de entrada entrada \(I\). El primer valor de salida de la capa oculta será

\[ O(1, 1)=h(1, 1)I(1, 1)+h(1, 2)I(1, 2)+h(2, 1)I(2, 1)+h(2, 2)I(2, 2) \]
  • El resultado anterior se obtiene si colocamos la matriz \(H\) de \(2\times2\) sobre \(I\), empezando por la esquina superior izquierda (el cuadrado rojo de Fig. 10.26(A) indica la posición de la matriz \(H\)). A continuación, multiplicamos los elementos en las partes superpuestas de las dos matrices y los sumamos. Desde un punto de vista físico, el valor \(O(1, 1)\) resultante es una media ponderada sobre un área local dentro de la matriz \(I\).

  • En la operación anterior, el área correspondiente de la imagen está formada por los píxeles de la parte superior izquierda 2 × 2 de la matriz \(I\). Para obtener el segundo valor de salida, \(O(1, 2)\), deslizamos \(H\) un píxel hacia la derecha, como indica el recuadro rojo punteado, y repetimos las operaciones, es decir

\[ O(1, 2) = h(1, 1)I(1, 2) + h(1, 2)I (1, 3) + h(2, 1)I(2, 2) + h(2, 2)I(2, 3). \]
  • Siguiendo el mismo razonamiento, deslizamos \(H\) para “escanear” toda la imagen matricial; así, se obtienen otros dos valores de salida \(O(2, 1)\) y \(O(2, 2)\). Las cuatro posiciones posibles de \(H\) encima de \(I\) se indican en la Fig. 10.26(A) por los cuadros cuadrados de color rojo completo, rojo punteado, gris oscuro y gris punteado. Para cada posición se obtiene un valor de salida. Por lo tanto, bajo el escenario descrito anteriormente, las salidas de la primera capa oculta forman una matriz de \(2\times 2\), \(O\). Cada uno de los elementos de la matriz de salida codifica información de un área diferente de la imagen de entrada.

Convolución

  • En el entorno más general, la operación de convolución entre dos matrices, \(H\in R^{m\times m}\) e \(I\in\mathbb{R}^{l\times l}\), es otra matriz, definida como

(10.11)#\[ O(i, j)=\sum_{t=1}^{m}\sum_{r=1}^{m}h(t, r)I(i+t-1, j+r-1),~\text{donde en este caso}~m<l. \]
  • En otras palabras, \(O(i, j)\) contiene información en un área de ventana de la matriz de entrada. De acuerdo con la definición de la Eq. (10.11) el elemento \(I(i, j)\) es el elemento superior izquierdo de esta área de la ventana. El tamaño de la ventana depende del valor de \(m\). El tamaño de la matriz de salida depende de las suposiciones que se adopten sobre cómo tratar los elementos/píxeles en los bordes de \(I\).

  • En sentido estricto, en la jerga del procesamiento de señales, la Eq. (10.11) se conoce como operación de correlación cruzada. Para la operación de convolución, primero hay que invertir los índices. Sin embargo, este es el nombre que ha “sobrevivido” en la comunidad del aprendizaje automático y bien nos adherimos a él. Al fin y al cabo, ambas son operaciones ponderadas sobre los píxeles dentro de un área de ventana de una imagen.

Observation 10.1

  • La discusión anterior nos “obliga” a pensar en una capa oculta como una colección de nodos uno al lado del otro. En cambio, en una CNN, cada capa oculta corresponde a una (o a más de una, como pronto veremos) matriz \(H\). Además, \(H\) se utiliza para realizar convoluciones.

  • Desde el punto de vista del tratamiento de señales, esta matriz es un filtro que actúa sobre la entrada para proporcionar la salida. En la jerga del aprendizaje de maquinas, también se denomina matriz kernel en lugar de filtro. La matriz de salida suele denominarse la matriz del mapa de características.

  • En resumen, al realizar convoluciones, en lugar de operaciones de producto interno, hemos conseguido

    1. Los parámetros que componen la capa oculta son compartidos por todos los píxeles de entrada y no tenemos un conjunto dedicado de parámetros por elemento de entrada (píxel)

    2. Las salidas de la capa oculta codifican la información de correlación de vecindad local de las distintas zonas de la imagen de entrada.

    3. Además, como la salida de la capa oculta también es una matriz de imágenes, se puede considerar como la entrada a una segunda capa oculta y construir así una red con muchas capas, cada una de las cuales realiza convoluciones.

  • De hecho, estas operaciones de filtrado se han utilizado tradicionalmente para generar características a partir de imágenes. La diferencia era que los elementos de la matriz de filtrado se preseleccionaban. Tomemos ejemplo, la siguiente matriz:

(10.12)#\[\begin{split} H= \begin{bmatrix} -1 & -1 & -1\\ -1 & 8 & -1\\ -1 & -1 & -1 \end{bmatrix} \end{split}\]
  • El filtro anterior se conoce como detector de bordes. Convolucionando una matriz de imagen, \(I\) , con la matriz anterior, \(H\),detecta los bordes de una imagen. La Fig. 10.27A muestra la imagen de un barco y la Fig. 10.27B muestra la salida después del filtrado de la imagen de la izquierda con la matriz de filtrado \(H\) anterior.

_images/edge_detect_convH.png

Fig. 10.27 (A) Imagen original y (B) Bordes de la imagen extraídos tras filtrar la matriz de la imagen original con el filtro \(H\) de la ecuación Eq. (10.12). Fuente [Theodoridis, 2020].#

  • La detección de bordes es de gran importancia en la comprensión de imágenes. Además, cambiando adecuadamente los valores en \(H\), se pueden detectar bordes en diferentes orientaciones, por ejemplo, diagonal, vertical, horizontal; en otras palabras, cambiando los valores de \(H\) se pueden generar diferentes tipos de características.

from skimage.color import rgb2gray
from skimage.io import imread
import numpy as np
from scipy import signal
import matplotlib.pylab as pylab
im = rgb2gray(imread('imgs/cameraman.jpg')).astype(float)
print(im.shape, np.max(im))
(256, 256) 0.9921568627450982
blur_box_kernel = np.ones((3,3)) / 9
edge_laplace_kernel = np.array([[0,1,0],[1,-4,1],[0,1,0]])
edge_laplace_kernel
array([[ 0,  1,  0],
       [ 1, -4,  1],
       [ 0,  1,  0]])
im_blurred = signal.convolve2d(im, blur_box_kernel)
im_edges = np.clip(signal.convolve2d(im, edge_laplace_kernel), 0, 1)
fig, axes = pylab.subplots(ncols=3, sharex=True, sharey=True, figsize=(18, 6))
axes[0].imshow(im, cmap=pylab.cm.gray)
axes[0].set_title('Original Image', size=20)
axes[1].imshow(im_blurred, cmap=pylab.cm.gray)
axes[1].set_title('Box Blur', size=20)
axes[2].imshow(im_edges, cmap=pylab.cm.gray)
axes[2].set_title('Laplace Edge Detection', size=20)
for ax in axes:
    ax.axis('off')
_images/397bb7041d9e3f106a40d873b19a934a19e75f63d15ff63bb39dfb18545d2a19.png
  • Nos hemos acercado a la idea que subyace a las CNN

    1. En lugar de utilizar una matriz de filtro/núcleo fija, como en el ejemplo del detector de bordes, deje el cálculo de los valores de la matriz de filtro, \(H\) , para la fase de entrenamiento. En otras palabras, hacemos que \(H\) se adapte a los datos y no preseleccionada.

    2. En lugar de utilizar una única matriz de filtros, empleamos más de una. Cada una de ellas generará un tipo diferente de características. Por ejemplo, una puede generar bordes diagonales, la otra horizontales, etc. Por lo tanto, cada capa oculta comprenderá más de una matriz de filtrado. Los valores de los elementos de cada una de las matrices de filtrado se calcularán durante la fase de entrenamiento, optimizando algún criterio. En otras palabras, cada capa oculta de una CNN genera un conjunto de características de forma óptima.

_images/feature_maps_inp_img.png

Fig. 10.28 Ilustración de tres filtros/núcleos. Profundidad de la capa oculta (número de filtro) es tres. Fuente [Theodoridis, 2020].#

  • La Fig. 10.28 ilustra la entrada y la primera capa oculta de una CNN. La entrada comprende una matriz imagen. La capa oculta consta de tres matrices de filtrado, a saber, \(H_{1}, H_{2}, H_{3}\). Nótese que cada matriz es el resultado de convolucionar una matriz de filtros diferente sobre la imagen de entrada. Cuantos más filtros se empleen, más mapas de características se extraerán y, en principio, mejor será el rendimiento de la red.

  • Sin embargo, cuantos más filtros utilicemos, más parámetros habrá que aprender, lo que plantea problemas computacionales y de sobreajuste. Nótese que cada píxel de una matriz de mapeo de características de salida codifica información dentro del área de la ventana que está definida por la posición correspondiente de la respectiva matriz de filtro.

10.14.3. Ejemplo de uso de Múltiples Kernels en una CNN#

En una Red Neuronal Convolucional (CNN), cada kernel se aplica simultáneamente a la imagen de entrada, generando un mapa de características (feature map).

Proceso de Aplicación

  1. Cada kernel detecta distintos patrones como bordes o texturas.

  2. Cada filtro genera su propio mapa de características, reduciendo dimensiones según el tamaño del kernel, stride y padding.

  3. El resultado es un volumen de salida de tamaño \(H' \times W' \times N\), donde \(N\) es el número de filtros (kernels).

Ejemplo con Tres Kernels: Si una imagen RGB de \(32 \times 32 \times 3\) se procesa con tres kernels de \(3 \times 3 \times 3\), la salida será: \(30 \times 30 \times 3\)

Conclusión: Cada kernel opera en paralelo, generando su propio mapa de características. El número de filtros define la profundidad de la salida. Las dimensiones dependen de los hiperparámetros de convolución.

Observation 10.2

  • Una característica importante de una CNN es que la invarianza de traslación (reconoce patrones en una imagen sin importar dónde se encuentren esos patrones) está integrada de forma natural en la red y es un subproducto de las circunvoluciones implicadas. De hecho, estas últimas se realizan deslizando la misma matriz de filtros sobre toda la imagen.

  • Así, si un objeto presente en una imagen se coloca en otra posición, la única diferencia sería que la contribución de este objeto a la salida también se desplazará la misma cantidad en el número de píxeles.

  • Es interesante señalar que existen pruebas sólidas en el campo de la neurociencia visual de que en el ser humano se realizan cálculos similares. (ver [Hubel and Wiesel, 1962, Serre et al., 2007]). La noción de convoluciones fue usada inicialmente por [Fukushima et al., 1983] en el contexto del aprendizaje no supervisado.

  • A continuación, presentamos algunos términos de la jerga utilizada en relación con las CNN:

  1. Profundidad: La profundidad de una capa es el número de matrices de filtro que se emplean en esta capa. No debe confundirse profundidad de la red, que corresponde al número total de capas ocultas utilizadas. A veces, nos referimos al número de filtros como el número de canales.

  2. Campo receptivo: Cada píxel de una matriz de características de salida resulta como una media ponderada de los píxeles dentro de un área específica de la matriz imagen de entrada (o de la salida de la capa anterior). El área específica que corresponde a un píxel se conoce como su campo receptivo (ver Fig. 10.28).

  3. Deslizamiento (stride): En la práctica, en lugar de deslizar la matriz de filtros de píxel en píxel, se puede deslizar, por ejemplo, \(s\) píxeles. Este valor se conoce como stride. Para valores de \(s > 1\), se obtienen matrices de mapas de características de menor tamaño. Esto se ilustra en Fig. 10.29A y B.

_images/stride_conv_prop.png

Fig. 10.29 Matriz de entrada y filtro de tamaños 5 × 5 y 3 × 3 respectivamente. En (A), el paso es igual a \(s = 1\) y en (B) es igual a \(s = 2\). En (A) la salida es una matriz de tamaño 3 × 3 y en (B) de tamaño 2 × 2. Fuente [Theodoridis, 2020].#

  1. Relleno de ceros (padding): A veces, se utilizan ceros para rellenar la matriz de entrada alrededor de los píxeles del borde. De esta forma, la dimensión de la matriz aumenta. Si la matriz original tiene dimensiones \(l\times l\), después de expandirla con \(p\) columnas y filas, las nuevas dimensiones pasan a ser (\(l + 2p\)). Esto se muestra en la Fig. 10.30.

_images/zero_fill_conv.png

Fig. 10.30 Ejemplo en el que la matriz original es de 5 × 5 y tras rellenarla con \(p = 2\) filas y columnas, su tamaño pasa a ser de 9 × 9. Fuente [Theodoridis, 2020].#

  1. Término de sesgo: Tras cada convolución que genera un píxel en el mapa de características, se añade un sesgo común a todos los píxeles de dicho mapa, calculado durante el entrenamiento. Esto sigue la lógica de compartir parámetros, como ocurre con los filtros en la imagen de entrada..

  • Se puede ajustar el tamaño de una matriz de mapa de características de salida ajustando el valor del stride, \(s\), y el número de columnas y filas cero adicionales en el relleno. En general, se puede comprobar fácilmente que si \(I\in\mathbb{R}^{l\times l}, H\in\mathbb{R}^{m\times m}\), y \(p\) es el número de filas y columnas adicionales para el relleno, entonces el mapa de características tiene dimensiones \(k\times k\), donde

    (10.13)#\[ k=\lfloor\frac{l+2p-m}{s}+1\rfloor, \]

    y \(\lfloor\cdot\rfloor\) es el operador suelo, i.e \(\lfloor2.7\rfloor=2\).

  • Por ejemplo, si \(l = 5, m = 3, p = 0\) y \(s = 1\), entonces \(k = 3\). Por otro lado, si \(l = 5, m = 3, p = 0\) y \(s = 2\), entonces \(k = 2\) (ver Fig. 10.29A y B). Nótese que si los valores de \(l, m, p\) y \(s\) son tales que la matriz de filtrado, al deslizarse sobre \(I\), cae fuera de \(I\), estas operaciones no se realizan. Solo realizamos operaciones mientras la matriz filtro esté contenida dentro de \(I\).

10.14.4. El paso de la no linealidad#

  • Una vez que se han realizado las convoluciones y se ha añadido el término de sesgo a todos los valores del mapa de características, el siguiente paso es aplicar una no linealidad (función de activación) a cada uno de los píxeles de cada matriz de mapas de características.

  • Se puede emplear cualquiera de las no linealidades que se han comentado anteriormente. Actualmente, la función de activación lineal rectificada, ReLU, es la más popular.

Hide code cell source
import numpy as np
import matplotlib.pyplot as plt

def relu(x):
    return np.maximum(0, x)

x = np.linspace(-10, 10, 100)
y = relu(x)

plt.plot(x, y, label='ReLU', color='b')
plt.axhline(0, color='black', linewidth=0.5)
plt.axvline(0, color='black', linewidth=0.5)
plt.grid(True, linestyle='--', alpha=0.6)
plt.legend()
plt.show()
_images/a66828ac77cefdeb4effe6a0e93ee176abc80505ebf7a6cc313c76510c39e8d4.png

Importancia de ReLU

La función de activación más utilizada en CNN es ReLU (Rectified Linear Unit) por las siguientes razones:

  1. Evita el gradiente desvaneciente: A diferencia de sigmoide o tangente hiperbólica, mantiene gradientes significativos en redes profundas, facilitando el entrenamiento.

  2. Eficiencia computacional: Su cálculo \(\max(0, x)\) es más simple que funciones exponenciales, reduciendo la carga de procesamiento.

  3. Favorece la esparsidad: Al asignar cero a valores negativos, desactiva neuronas, mejorando eficiencia y generalización.

  4. Acelera el entrenamiento: Su linealidad en valores positivos y bajo costo computacional permiten una convergencia más rápida.

No obstante, puede generar neuronas muertas cuando ciertos pesos anulan permanentemente la activación. Para evitarlo, existen variantes como Leaky ReLU y PReLU.

  • La Fig. 10.31A muestra la imagen obtenida después de filtrar la imagen original del barco con el detector de bordes en la (10.12) y la Fig. 10.31B muestra el resultado que se obtiene tras la aplicación de la no linealidad en cada píxel individual.

_images/relu_activ_fn.png

Fig. 10.31 (A) Imagen donde se han extraído los bordes y (B) imagen resultante tras aplicar el en cada uno de los píxeles. Fuente [Theodoridis, 2020].#

10.14.5. La etapa de agrupación#

  • Pooling espacial: Este proceso reduce la dimensionalidad de las matrices de mapas de características. Se emplea una ventana deslizante, cuyo desplazamiento se define mediante un parámetro stride. La operación selecciona un único valor que representa todos los píxeles dentro de la ventana.

  • La operación más común es la agrupación máxima (max pooling), donde se selecciona el píxel de mayor valor dentro de la ventana. Otra opción es la agrupación promedio (average pooling), que toma el valor medio de los píxeles, también conocida como pooling de suma. En la ilustración, una matriz de 6 × 6 se reduce aplicando una ventana de 2 × 2 con un stride de 2.

_images/pooling_step_conv.png

Fig. 10.32 (A) Matriz original de tamaño de 6 × 6. Agrupación mediante una ventana de 2 × 2 y un stride \(s = 2\). Valor máximo por ubicación de la ventana se indica en negrita. (B) Matriz 3 × 3 resultante tras la agrupación máxima. Fuente [Theodoridis, 2020].#

  • La misma fórmula de la Eq. (10.13) permite calcular el tamaño de la matriz resultante en el caso general. La agrupación reduce la dimensionalidad y el tamaño de las matrices, lo que es crucial, ya que cada salida se convierte en la entrada de la siguiente capa. Controlar el tamaño es esencial para gestionar el número de parámetros sin comprometer excesivamente la información.

_images/pooling_invariant_conv.png

Fig. 10.33 (A) Bordes de la imagen del barco tras la aplicación de ReLu (B) Imagen resultante tras aplicar max-pooling utilizando una ventana de 8 × 8. Fuente [Theodoridis, 2020].#

Observación

  • Fig. 10.33 muestra el efecto de aplicar el pooling a la imagen de la izquierda. Sin duda, los bordes se vuelven más gruesos, pero la información relacionada con los bordes puede extraerse. Nótese que después de la agrupación, el tamaño de la matriz imagen es reducido.

  • Desde otro punto de vista, el polling resume las estadísticas dentro del área pooling. El pooling puede considerarse un tipo especial de filtrado, en el que, en lugar de la convolución, se selecciona el valor máximo (o medio) de la imagen. El pooling ayuda a que la representación sea aproximadamente invariante a pequeñas traslaciones de la entrada.

10.14.6. Convolución sobre volúmenes#

Observación

  • En la Fig. 10.28, la primera capa oculta genera tres matrices de imágenes como entrada para la siguiente capa, similar a la representación RGB de imágenes en color, donde cada matriz indica la intensidad de un color en cada píxel con valores de 0 a 255.

Rojo (R)

Verde (G)

Azul (B)

Color resultante

255

0

0

🔴 Rojo puro

0

255

0

🟢 Verde puro

0

0

255

🔵 Azul puro

255

255

0

🟡 Amarillo (Rojo + Verde)

0

255

255

🔵 Cian (Verde + Azul)

255

0

255

🟣 Magenta (Rojo + Azul)

255

255

255

⚪ Blanco (Máxima intensidad)

0

0

0

⚫ Negro (Sin intensidad)

  • Las entradas no son matrices bidimensionales, sino conjuntos de matrices bidimensionales, conocidos en matemáticas como tensores tridimensionales o volúmenes. Se adopta este último término por su relación con la geometría. El desafío radica en definir cómo realizar convoluciones en estos volúmenes.

import numpy as np
import matplotlib.pyplot as plt

imagen = np.array([
    [[255, 0, 0], [0, 255, 0], [0, 0, 255]],  # Rojo, Verde, Azul
    [[255, 255, 0], [0, 255, 255], [255, 0, 255]],  # Amarillo, Cian, Magenta
    [[255, 255, 255], [128, 128, 128], [0, 0, 0]]  # Blanco, Gris, Negro
], dtype=np.uint8)

canal_rojo  = imagen[:, :, 0]  # Matriz de valores R
canal_verde = imagen[:, :, 1]  # Matriz de valores G
canal_azul  = imagen[:, :, 2]  # Matriz de valores B

plt.imshow(imagen)
plt.show()
_images/ddcce1e9cb25d0268ee5d848916449a8ce2b668b3a7b567ad4b277a170a09667.png
print("Matriz del canal Rojo (R):\n", canal_rojo)
print("Matriz del canal Verde (G):\n", canal_verde)
print("Matriz del canal Azul (B):\n", canal_azul)
Matriz del canal Rojo (R):
 [[255   0   0]
 [255   0 255]
 [255 128   0]]
Matriz del canal Verde (G):
 [[  0 255   0]
 [255 255   0]
 [255 128   0]]
Matriz del canal Azul (B):
 [[  0   0 255]
 [  0 255 255]
 [255 128   0]]
_images/matrix_to_volume_conv.png

Fig. 10.34 Se apilan \(d\) matrices de tamaño \(h\times w\) cada una para formar un volumen de tamaño \(h \times w\times d\). En este caso, \(h = w = 5\) y \(d = 3\). Fuente [Theodoridis, 2020].#

  • Por convención, las tres dimensiones de un volumen se representarán como \(h\) para la altura, \(w\) para la anchura y \(d\) para la profundidad. Nótese que la profundidad \(d\) corresponde al número de imágenes implicadas. Así pues, si tenemos tres imágenes de 256 × 256, entonces \(h = w = 256\) y \(d = 3\) y diremos que el volumen es de tamaño (dimensión) 256 × 256 × 3. Fig. 10.34 ilustra la geometría asociada a las respectivas definiciones.

Convolución de volúmenes

  • En una capa con entrada de dimensiones \(h \times w \times d\), los filtros de las capas ocultas también son volúmenes, pero deben tener la misma profundidad \(d\) que la entrada, aunque su altura y anchura pueden variar.

  • Si la entrada es un volumen \(\boldsymbol{I}\) de \(l \times l \times d\), este consta de \(d\) imágenes \(I_r\) de \(l \times l\). Un filtro \(\boldsymbol{H}\) de \(m \times m \times d\) también se compone de \(d\) imágenes \(H_r\) de \(m \times m\). La convolución se define a partir de estos elementos.

  1. Convolucionar las correspondientes matrices de imágenes bidimensionales para generar \(d\) matrices bidimensionales de salida, es decir

\[ O_{r}=I_{r}\star H_{r},\quad r=1,2,\dots,d. \]
  1. La convolución de los dos volúmenes, \(\boldsymbol{I}\) y \(\boldsymbol{H}\), se define como

    \[ O=\sum_{r=1}^{d}O_{r}. \]

    En otras palabras, la convolución (denotada por \(\star\)) de dos volúmenes es una matriz bidimensional, es decir,

    \[ \text{3D volume}\star\text{3D volume}=\text{2D array}. \]
_images/volume_conv_3Dto2D.png

Fig. 10.35 Convolución \(\text{3D volume}\star\text{3D volume}=\text{2D array}\). \(h = w = l, h = w = m\) y \(d = 3\).#

  • La operación se ilustra en la Fig. 10.35. Las matrices correspondientes (mostradas por diferentes colores y tipos de líneas). Las tres matrices de salida (\(d = 3\)) se suman posteriormente para formar la convolución de los dos volúmenes. La dimensión \(k\) de la salida depende de los valores de \(l\) y \(m\), del stride \(s\) y del padding \(p\), si se utiliza, según la Eq. (10.13).

Observación

  • En la práctica, cada capa de una CNN comprende varios de estos volúmenes de filtrado. Por ejemplo, si la entrada a una capa es un volumen \(l\times l\times d\), y hay, digamos, \(c\) volúmenes de núcleo, cada uno de dimensiones \(m\times m\times d\), la salida de la capa será un volumen \(k\times k\times c\), donde \(k\) se determina la Eq. (10.13).

10.14.7. Red en red y convolución 1 × 1#

Observación

  • La convolución 1 × 1 no tiene sentido cuando se trata de matrices bidimensionales. En efecto una matriz de filtro 1 × 1 es un escalar. Convolucionar una matriz \(I\) de \(l\times l\) con un escalar \(a\) equivale a deslizar el valor escalar sobre todos los píxeles y multiplicar cada uno de ellos por \(a\). El resultado es la trivial \(aI\) .

  • Sin embargo, cuando se trata de volúmenes, la convolución 1 × 1 tiene sentido. En este caso, el filtro correspondiente, \(\boldsymbol{H}\), es un volumen de tamaño \(1\times 1\times d\). Geométricamente, se trata de un “tubo”, con \(h = w = 1\) y \(d\) elementos en profundidad, \(h(1, 1, r), r = 1, 2,\dots,d\). Por lo tanto, el resultado de la convolución de un volumen \(\boldsymbol{I}\) de \(l\times l\times d\) con un \(1\times 1\times d\) volumen \(H\) es la media ponderada,

    \[ O=\boldsymbol{I}\star\boldsymbol{H}=\sum_{r=1}^{d}h(1, 1, r)I_{r} \]

    donde \(I_{r},~r=1,2,\dots,d,\) son las \(d\) matrices, cada una de dimensiones \(l\times l\), que comprende \(\boldsymbol{I}\).

Convolución \(1\times 1\)

  • Ahora bien, cabe preguntarse por qué necesitamos una operación de este tipo en la práctica. La respuesta está relacionada con el tamaño de los volúmenes implicados; mediante el uso de convoluciones \(1\times 1\), se puede controlar y cambiar su tamaño para adaptarlo a las necesidades de la red.

  • Supongamos que en una etapa/capa de una red profunda hemos obtenido un volumen \(\boldsymbol{I}\) de dimensiones \(k\times k\times d\). Para cambiar la profundidad de \(d\) a \(c\), conservando el mismo tamaño \(k\), para la altura y la anchura, empleamos \(c\) volúmenes, \(H_{t}, t = 1, 2,\dots,c\), cada uno de dimensiones \(1\times 1\times d\). Al realizar las \(c\) convoluciones obtenemos

    \[ O_{t}=\boldsymbol{I}\star\boldsymbol{H}_{t}=\sum_{r=1}^{d}h_{t}(1, 1, r)I_{r},~t=1,2,\dots,c. \]

    Apilando \(O_{t}, t = 1, 2,\dots,c\), obtenemos el volumen \(\boldsymbol{O}\) de dimensión \(k\times k\times c\) (ver Fig. 10.36).

  • La información original se sigue conservando en el nuevo volumen, de forma promediada. A menudo, una vez obtenido el nuevo volumen \(\boldsymbol{O}\), sus elementos se “empujan” a través de una no linealidad, por ejemplo, ReLU.

  • La convolución \(1\times 1\) seguida de la no linealidad se denomina operación de red en red y su finalidad es añadir una etapa de no linealidad adicional en el flujo de operaciones a través de la red. Por lo tanto, en este contexto, si \(c<d\), la operación de red en red puede considerarse una técnica de reducción de la dimensionalidad no lineal.

_images/1x1conv_dim_reduction.png

Fig. 10.36 Convolución \(1\times 1\), con \(c\) tubos de dimensión \(1\times 1\times d\), donde \(d\) es la profundidad del volumen de entrada. Fuente [Theodoridis, 2020].#

Ejemplo

  • Consideremos que la entrada de una capa es un volumen \(\boldsymbol{I}\) de 28 × 28 × 192. El objetivo es producir a la salida de la capa un volumen, \(O\), de dimensión 28 × 28 × 32. Para ello emplear 5 × 5 convoluciones iguales y un volumen intermedio \(\boldsymbol{O}'\) de dimensiones \(28\times 28\times 16\). Nótese que en la última capa se usó padding de 2 píxeles en cada borde.

  • Observe que, rellenando todas las matrices apiladas en \(\boldsymbol{I}\) con \(p\) cero columnas y filas, se tiene que el número de multiplicaciones y adiciones (MADS) es \(3.7\times 10^{6}\times 32\approx 120~\) millones MADS. Además, usando convoluciones \(1\times 1\), este número se reduce a \(28^{2}\times 25\times 16\times 32\approx 10\) millones MADS (ver [Theodoridis, 2020]).

  • A menudo, el volumen intermedio, \(\boldsymbol{O}'\), se conoce como capa cuello de botella (bottleneck layer); su función es “reducir” primero el tamaño del volumen de entrada, antes de obtener el volumen de salida final (ver Fig. 10.37).

_images/bottleneck_layer_1x1conv.png

Fig. 10.37 Capa cuello de botella. Fuente [Theodoridis, 2020].#

10.14.8. Arquitectura CNN completa#

  • La forma típica de una red convolucional completa consiste en una secuencia de capas convolucionales, cada una de las cuales que comprende los tres pasos básicos, a saber, convolución (Conv.), no linealidad (ReLU) y agrupación (Pooling), como se describe al principio de esta sección. Dependiendo de la aplicación, se pueden apilar tantas capas como sea necesario, donde la salida de una capa se convierte en la entrada de la siguiente. Las entradas y salidas de cada capa son volúmenes, como se ha descrito anteriormente.

_images/complete_cnnarq.png

Fig. 10.38 Arquitectura CNN completa. Fuente [Theodoridis, 2020].#

Procedimiento de una CNN completa

  • En la primera capa, se aplican filtros para realizar convoluciones seguidas de una transformación no lineal. Luego, la operación de pooling reduce las dimensiones de la salida, que sirve como entrada para la siguiente capa. Este proceso se repite hasta que el volumen final se vectoriza, en un paso conocido como flattening.

  • El resultado es un vector de características obtenido a partir de las transformaciones aplicadas en cada capa convolucional. Este vector se utiliza como entrada para un modelo de aprendizaje, como una red neuronal totalmente conectada o una máquina de vectores de soporte.

Observación

  • La estrategia consiste en reducir dimensiones mientras se incrementa la profundidad, aumentando filtros por etapa para extraer más características. La cantidad de capas convolucionales y totalmente conectadas depende de la aplicación, sin un método formal para su determinación, por lo que se elige mediante evaluación. Se sugiere partir de arquitecturas existentes, y el aprendizaje Bayesiano facilita la selección óptima de nodos o filtros.

10.14.9. Características en una CNN#

  • Las características (features) en una Red Neuronal Convolucional (CNN) son patrones visuales que la red aprende a reconocer en las imágenes. Se extraen automáticamente a través de capas convolucionales aplicadas en diferentes niveles de abstracción. ¿Cuáles son las características en una CNN? Las características son representaciones numéricas que describen el contenido visual de una imagen. Se generan a través de filtros convolucionales que detectan patrones en distintos niveles:

  1. Características de bajo nivel (Primeras capas)

Se detectan patrones simples como:

  • Bordes

  • Texturas

  • Esquinas

  • Gradientes de color

Ejemplo: Un filtro Sobel detecta los bordes en una imagen.

  1. Características de nivel intermedio (Capas medias)

Se combinan los bordes y texturas detectados antes para formar estructuras más complejas, como:

  • Formas básicas (círculos, líneas curvas)

  • Contornos de objetos

  • Partes de objetos (ojos, nariz, ruedas)

Ejemplo: En una CNN que detecta rostros, esta capa podría identificar una nariz o un ojo.

  1. Características de alto nivel (Últimas capas)

Se forman representaciones abstractas y semánticas de la imagen, como:

  • Objetos completos (caras, coches, animales)

  • Relaciones espaciales entre partes

  • Categorización (perro vs. gato)

Ejemplo: En una CNN entrenada para clasificar animales, las capas profundas pueden reconocer la silueta completa de un gato.

Ejemplo Visual: Si la CNN clasifica gatos y perros:

  • Bordes → Detecta líneas y curvas.

  • Formas intermedias → Identifica orejas, ojos y hocico.

  • Características de alto nivel → Distingue un gato de un perro.

10.14.10. Arquitecturas de Redes Neuronales Convolucionales#

Introducción

  • El diseño de una CNN sigue pasos básicos con múltiples variantes arquitectónicas y optimizaciones para mejorar la eficiencia computacional (ver Fig. 10.38). Implementarlas requiere un gran esfuerzo técnico para su aprendizaje y aplicación.

  • A continuación, se presentan redes convolucionales clásicas, cuya comprensión es clave para profundizar en el tema, aunque algunos de sus métodos ya no se utilicen. Los estudios citados ofrecen una base sólida para el aprendizaje de las CNN.

_images/cat_dog_class.png

Fig. 10.39 Clasificación de imágenes. Fuente clarifai#

10.14.11. LeNet-5#

LeNet-5

  • Este es un ejemplo típico de la primera generación de CNNs y fue construido para reconocer dígitos de números (ver [LeCun et al., 1998]). Por razones históricas, comentemos un poco su arquitectura. La entrada de la red consiste en imágenes en escala de grises de tamaño 32 × 32 × 1. La red emplea dos capas de convolución. En la primera capa, el volumen de salida tiene un tamaño de 28 × 28 × 6, que tras la agrupación se convierte en 14 × 14 × 6.

  • Las dimensiones del volumen en la segunda capa eran 10 × 10 × 16 y, tras la agrupación, 5 × 5 × 16. La no linealidad utilizada entonces era de tipo sigmoid. Nótese que la altura y la anchura de los volúmenes disminuyen y la profundidad aumenta, como se ha señalado antes. El número de elementos del último volumen es igual a 400. Estos elementos se apilan en un vector y alimentan los correspondientes nodos de entrada de una red totalmente conectada.

  • Esta última consta de dos capas ocultas con 120 nodos en la primera y 84 nodos en la segunda. Hay 10 nodos de salida, uno por dígito, que utilizan una no linealidad softmax. El número total de parámetros implicados es del orden de 60.000.

_images/letnet5_architecture.jpeg

Fig. 10.40 Arquitectura LeNet-5. Fuente [LeCun et al., 1998].#

10.14.12. AlexNet#

AlexNet

  • Esta red también es histórica ya que, demostró que el punto crucial para hacer grandes redes es la disponibilidad de grandes conjuntos de entrenamiento [Krizhevsky et al., 2012]. El artículo relacionado es el que realmente hizo volver a las CNN y actuó como catalizador para su adopción mucho más allá de la tarea de reconocimiento de dígitos. La Alexnet es un desarrollo de la LeNet-5, pero es mucho más grande e implica aproximadamente 60 millones de parámetros.

  • Las entradas a la red son imágenes RGB de tamaño 227 × 227 × 3. Comprende cinco capas ocultas y el volumen final consta de 9216 elementos que alimentan una red totalmente conectada con dos capas ocultas de 4096 unidades cada una. La salida consta de 1000 nodos softmax (uno por clase) para reconocer imágenes del conjunto de datos ImageNet para el reconocimiento de objetos. ReLU se ha utilizado como no linealidad en las capas ocultas.

_images/alexnet_architecture.png

Fig. 10.41 Arquitectura LeNet-5. Fuente [Krizhevsky et al., 2012].#

10.14.13. VGG-16#

VGG-16

  • Esta red [Simonyan and Zisserman, 2014] es mucho mayor que AlexNet. Implica un total de aproximadamente 140 millones de parámetros. La principal característica de esta red es su regularidad. Involucra filtros 3 × 3 para realizar las mismas convoluciones utilizando padding y stride \(s = 1\) y ventanas 2 × 2 para maxpooling con stride \(s = 2\). Cada vez que se realiza un pooling, la altura y la anchura de los volúmenes se reducen a la mitad y la profundidad se multiplica por dos.

  • Partiendo de 224 × 224 × 3 imagen de entrada y después de 13 capas el volumen final tiene un tamaño de 7 × 7 × 512, un total de 25088 elementos, que tras su vectorización alimenta a una red totalmente conectada con 2 capas ocultas de 4096 nodos cada una. Los 1000 nodos de salida están construidos en torno a la no linealidad softmax y se ha utilizado ReLU para las unidades ocultas en toda la red.

_images/vgg16_architecture.png

Fig. 10.42 Arquitectura VGG-16. Fuente [Simonyan and Zisserman, 2014].#

  • El diseño de VGG-16 se basó en la hipótesis de que aumentar la profundidad de una red convolucional, mientras se usan pequeños filtros, conduciría a un mejor rendimiento en la tarea de clasificación de imágenes. La naturaleza homogénea de la arquitectura con filtros 3x3 repetidos permitió que la red aprendiera características jerárquicas más profundas, conservando al mismo tiempo una buena eficiencia computacional.

  • El uso de filtros pequeños y capas profundas fue una decisión clave porque:

    • Filtros más pequeños capturan características locales, pero apilados sucesivamente tienen un campo receptivo mayor.

    • Mayor profundidad permite extraer características más abstractas y complejas a medida que las capas avanzan.

10.14.14. GoogleNet y la red Inception#

GoogleNet y la red Inception

  • La arquitectura utilizada en esta red se desvía del “arquetípico” que se muestra en Fig. 10.38. En el corazón de esta red se encuentra el llamado módulo de inicio [Szegedy et al., 2015]. Un módulo de inicio consta de filtros de diferentes tamaños y profundidades, así como de una ruta de agrupación diferente.

  • La arquitectura Inception toma la salida de una capa y la divide en varias rutas: una usa convolución 1x1 para reducir la profundidad, otra hace pooling seguido de convolución 5x5, y dos rutas realizan convoluciones separadas con filtros 3x3 y 5x5. Se usa una capa de cuello de botella (convolución 1x1) para reducir la carga computacional antes de las convoluciones.

  • En el módulo de inicio, se concatenan los volúmenes de salida de varias trayectorias, permitiendo que la red, durante el entrenamiento, elija las mejores operaciones para cada capa. A medida que se avanza hacia capas superiores, se incrementa la proporción de convoluciones 3 × 3 y 5 × 5, ya que captan rasgos más abstractos. La red tiene 22 capas y 6 millones de parámetros.

_images/google-net-like.png

Fig. 10.43 Arquitectura GoogleNet. Fuente pubmed#

_images/inception_architecture.png

Fig. 10.44 Arquitectura Inception. Fuente [Szegedy et al., 2015].#

10.14.15. Redes residuales (ResNets)#

Redes residuales (ResNets)

  • Ya hemos hablado de las ventajas de diseñar redes profundas. También abordamos la forma de hacer frente al problema de los gradientes que se desvanecen/explotan mediante una combinación de métodos y trucos que permiten que el algoritmo backpropagation converja con suficiente rapidez. Sin embargo, una vez que empezamos a construir redes muy profundas (del orden de decenas o incluso centenares de capas) nos encontramos con el siguiente comportamiento “poco ortodoxo”.

  • Cabría esperar que, añadiendo más y más capas, el error de entrenamiento mejore o al menos no aumenta. Sin embargo, lo que se observa en la práctica es que a partir de un cierto número de capas, el error de entrenamiento empieza a aumentar. Esto se ilustra gráficamente en la Fig. 10.45. Este fenómeno no tiene nada que ver con el sobreajuste. Después de todo, estamos hablando del error de entrenamiento y no del de generalización. Parece que esto puede deberse a la tarea de optimización que se vuelve más y más difícil a medida que se añaden más y más capas.

_images/residual_net_architecture.png

Fig. 10.45 Aumento en lugar de disminución del error cuando el número de capas supera cierto número, en una red muy profunda. (curva roja) como se esperaba a partir de la teoría.#

  1. Interpretación Matemática de una Capa en la Red Neuronal

  • Cada capa de una red neuronal aplica una función \(H\) que transforma la salida anterior \(\boldsymbol{y}^{r-1}\) en una nueva salida \(\boldsymbol{y}^{r}\):

\[ \boldsymbol{y}^{r} = H(\boldsymbol{y}^{r-1}) \]
  1. ¿Por qué Añadir Más Capas No Debería Aumentar el Error de Entrenamiento?

  • Si una capa ha extraído toda la información útil, una capa adicional, en el peor caso, solo replicaría la salida anterior aplicando la función identidad:

    \[ \boldsymbol{y}^{r} = H(\boldsymbol{y}^{r-1}) = \boldsymbol{y}^{r-1} \]

    Es decir, si no hay nada nuevo que aprender, la capa extra solo copiaría la información.

  1. Problema de redes profundas: saturación y dificultades de optimización

  • En redes profundas, el rendimiento se estanca porque más capas no mejoran la precisión y la optimización enfrenta dificultades para aprender el mapeo de identidad \(H(y^{r-1}) = y^{r-1}\), generando errores por gradientes pequeños o desajustes en los pesos.

  1. Solución propuesta por He et al. (2016): Redes Residuales (ResNets)

  • En lugar de aprender directamente \(H(y^{r-1})\), se propone un mapeo residual \(F(y^{r-1}) = H(y^{r-1}) - y^{r-1}\), donde la capa solo ajusta la diferencia necesaria en vez de replicar la entrada. Así, la salida se obtiene sumando este ajuste residual:

\[ y^r = y^{r-1} + F(y^{r-1}) \]
  • El uso de la representación residual no es nuevo y ya se ha utilizado anteriormente en el contexto de la cuantificación vectorial. La esencia del aprendizaje residual es introducir el llamado bloque de construcción residual, que se muestra en la Fig. 10.46.

_images/residual_architecture_cnn.png

Fig. 10.46 Bloque de construcción residual. Fuente [Theodoridis, 2020].#

  • De esta manera, un número de capas, digamos, dos, como en el caso de la Fig. 10.46, se apilan y dejamos que estas capas se ajusten explícitamente al mapeo residual, a través de las llamadas conexiones de atajo u omisión. Cada capa de pesos realiza una transformación sobre su entrada, por ejemplo, convoluciones. Si \(\boldsymbol{y}^{r}\) e \(\boldsymbol{y}^{r-1}\) son de dimensiones diferentes, el atajo de mapeo de identidad se modifica a \(W\boldsymbol{y}^{r-1}\), donde \(W\) es una matriz de dimensiones apropiadas.

10.14.16. U-Net: Redes Convolucionales para la Segmentación de Imágenes Biomédicas#

_images/segmentation_instance.avif

Fig. 10.47 Image segmentation vs Instance segmentation. Fuente v7labs#

  • En imágenes médicas, la segmentación semántica clasifica cada píxel según una categoría anatómica o patológica, mientras que la segmentación de instancias diferencia objetos individuales dentro de la misma clase. Estas técnicas son esenciales para el diagnóstico automatizado y la planificación clínica.

  • La arquitectura U-Net, diseñada para segmentación biomédica, permite una segmentación precisa mediante un diseño en forma de U que combina información de contexto con detalles espaciales, mejorando la detección de estructuras complejas en imágenes médicas.

_images/unet_segmentation.png

Fig. 10.48 La arquitectura U-Net representa mapas de características con cajas, indicando canales y tamaño, mientras que las flechas señalan las operaciones.#

Arquitectura U-Net

La arquitectura U-Net es una red neuronal convolucional diseñada para la segmentación de imágenes biomédicas. Se basa en una estructura de encoder-decoder con una forma en “U”.

  1. Encoder (contracción): Extrae características mediante convoluciones y capas de pooling para reducir la dimensionalidad.

  2. Bottleneck: Conecta el encoder con el decoder, capturando la información más relevante.

  3. Decoder (expansión): Usa convoluciones transpuestas para recuperar la resolución original de la imagen y refinar la segmentación.

  4. Skip connections: Conectan capas del encoder con el decoder, permitiendo una mejor preservación de detalles.

  • Esta arquitectura es eficiente con pocos datos y logra segmentaciones precisas, lo que la hace ideal para aplicaciones médicas. Considera conv 3x3, max pool 2x2, up-conv 2x2, conv 1x1, padding = 0, stride = 1 fijos. En el encoder se reduce alto y ancho de los mapeos de caracteristicas mientras que se aumenta la profundidad. Lo contrario ocurre en el decoder.

  • Al final se aplican 2 convoluciones de 1x1x64 (tubos) como las vistas en secciones anteriores, para obtener el último kernel de 388x388x2 en el que, cada canal representaria una clase diferente a segmentar, y despues de aplicar funciones de activación, lo que resulta en el mapeo de características es, la probabilidad de que un pixel en una respectiva posición pertenezca a la clase representada por cada canal, por ejemplo a la clase (black) o la clase (blue) (clasificación binaria).

La arquitectura U-Net aprende a segmentar imágenes asignando cada píxel a una clase específica.

  • Entrada (X): Imagen original (ej. un gato en fondo blanco).

  • Salida esperada (Y): Máscara binaria (1 = gato, 0 = fondo).

El modelo ajusta sus parámetros hasta que pueda predecir correctamente la máscara a partir de una nueva imagen.

¿Cómo se maneja la correspondencia de píxeles?

U-Net genera una salida de menor tamaño que la imagen de entrada debido a la reducción espacial provocada por las capas de max pooling en la fase de contracción y el uso de valid convolutions sin padding. Aunque las up-convolutions intentan recuperar la resolución original, la salida sigue siendo más pequeña.

¿Cómo mantener la correspondencia de píxeles?

  1. Same padding: Mantiene el tamaño original sin postprocesamiento, pero puede introducir valores artificiales en los bordes.

  2. Superposición de parches: Divide la imagen en fragmentos solapados para reconstruir la segmentación completa, útil en imágenes médicas de alta resolución.

  3. Interpolación o recorte: Ajusta el tamaño de salida cuando se usan valid convolutions, aunque puede generar imprecisiones.

Recomendación:

  • Para conservar el tamaño original, usar same padding en todas las convoluciones.

  • En imágenes grandes, aplicar superposición de parches para optimizar memoria y evitar artefactos.

  • Si la red ya está entrenada con valid convolutions, usar interpolación como ajuste final.

10.14.17. Aplicación: Reconocimiento facial de emociones#

import warnings
warnings.filterwarnings('ignore')
import tensorflow as tf

devices = tf.config.list_physical_devices()
print("Available devices:")
for device in devices:
    print(device)

gpus = tf.config.list_physical_devices('GPU')
if gpus:
    print("TensorFlow is using GPU.")
    for gpu in gpus:
        gpu_details = tf.config.experimental.get_device_details(gpu)
        print(f"GPU details: {gpu_details}")
else:
    print("TensorFlow is not using GPU.")
Available devices:
PhysicalDevice(name='/physical_device:CPU:0', device_type='CPU')
PhysicalDevice(name='/physical_device:GPU:0', device_type='GPU')
TensorFlow is using GPU.
GPU details: {'compute_capability': (8, 9), 'device_name': 'NVIDIA GeForce RTX 4070'}
I0000 00:00:1743824747.984472    2101 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
import numpy as np
import pandas as pd
import os
from tensorflow.keras.preprocessing.image import ImageDataGenerator
from tensorflow.keras.utils import load_img
from keras.layers import Conv2D, Dense, BatchNormalization, Activation, Dropout, MaxPool2D, Flatten, MaxPooling2D
from tensorflow.keras.metrics import AUC
from tensorflow.keras.optimizers import Adam, RMSprop, SGD
from keras.callbacks import ModelCheckpoint,EarlyStopping
import datetime
from keras import regularizers
import matplotlib.pyplot as plt
from tensorflow.keras.utils import plot_model
train_dir = '/home/lihkir/Data/face_sentiment_detection/train/'
test_dir  = '/home/lihkir/Data/face_sentiment_detection/test/'
def count_exp(path, set_):
    dict_ = {}
    for expression in os.listdir(path):
        dir_ = path + expression
        dict_[expression] = len(os.listdir(dir_))
    df = pd.DataFrame(dict_, index=[set_])
    return df
train_count = count_exp(train_dir, 'train')
test_count = count_exp(test_dir, 'test')
print(train_count)
print(test_count)
       surprise  disgust  happy  fear  angry  neutral   sad
train      3171      436   7215  4097   3995     4965  4830
      surprise  disgust  happy  fear  angry  neutral   sad
test       831      111   1774  1024    958     1233  1247
train_count.transpose().plot(kind='bar');
_images/414f00b7dab5f489f9ded01587bdfb318e0dbb1a7ebfcdcae703f6a3603c4bc6.png
test_count.transpose().plot(kind='bar');
_images/b6881d8e03cd64a1b63fd7ab60440e2b1d8afa620269d4dad4964af3f5d687e7.png
plt.figure(figsize=(14,22))
i = 1
for expression in os.listdir(train_dir):
    img = load_img((train_dir + expression +'/'+ os.listdir(train_dir + expression)[5]))
    plt.subplot(1,7,i)
    plt.imshow(img)
    plt.title(expression)
    plt.axis('off')
    i += 1
_images/a5728fa7db42221980f373d592088eebb275c56ea7b17864fbf1d0d5f0971541.png
  • Creamos los conjuntos de datos de entrenamiento, prueba y validación

train_datagen = ImageDataGenerator(rescale=1.0/255.0,
                                   horizontal_flip=True,
                                   validation_split=0.2)
  • rescale=1./255: Normaliza las imágenes escalando los valores de los píxeles de un rango de [0, 255] a [0, 1]. Esto es una práctica común para mejorar la eficiencia del entrenamiento de redes neuronales.

  • horizontal_flip=True: Permite aplicar una transformación de “flip” horizontal (volteo) de manera aleatoria a las imágenes, una técnica de data augmentation para hacer que el modelo generalice mejor.

  • validation_split=0.2: Define una división de los datos, reservando el 20% de ellos para validación, lo que significa que el 80% de los datos será usado para entrenamiento.

training_set = train_datagen.flow_from_directory(train_dir,
                                                batch_size=64,
                                                target_size=(48,48),
                                                shuffle=True,
                                                color_mode='grayscale',
                                                class_mode='categorical',
                                                subset='training')
Found 22968 images belonging to 7 classes.
  • train_dir: Es la ruta del directorio que contiene las imágenes de entrenamiento organizadas en subdirectorios según la clase.

  • batch_size=64: Carga 64 imágenes a la vez (en lotes) para ser procesadas en cada paso del entrenamiento.

  • target_size=(48,48): Redimensiona todas las imágenes al tamaño de 48x48 píxeles. Esto es útil para mantener un tamaño uniforme de las imágenes que ingresan al modelo.

  • shuffle=True: Aleatoriza las imágenes en cada epoch, lo que evita que el modelo se adapte a un orden particular de los datos.

  • color_mode='grayscale': Convierte las imágenes en escala de grises (1 canal en lugar de 3 para imágenes en color RGB).

  • class_mode='categorical': Las etiquetas se asignan como categóricas, es decir, cada imagen pertenece a una clase específica (por ejemplo, clasificación multiclase). subset='training': Utiliza el 80% de los datos (definidos anteriormente con validation_split=0.2) para entrenamiento.

validation_set = train_datagen.flow_from_directory(train_dir,
                                                batch_size=64,
                                                target_size=(48,48),
                                                shuffle=True,
                                                color_mode='grayscale',
                                                class_mode='categorical',
                                                subset='validation')
Found 5741 images belonging to 7 classes.
  • subset='validation': Selecciona el 20% de los datos reservados para la validación (definidos anteriormente con validation_split=0.2)

test_datagen = ImageDataGenerator(rescale=1.0/255.0,
                                   horizontal_flip=True)
test_set = test_datagen.flow_from_directory(test_dir,
                                                batch_size=64,
                                                target_size=(48,48),
                                                shuffle=True,
                                                color_mode='grayscale',
                                                class_mode='categorical')
Found 7178 images belonging to 7 classes.
training_set.class_indices
{'angry': 0,
 'disgust': 1,
 'fear': 2,
 'happy': 3,
 'neutral': 4,
 'sad': 5,
 'surprise': 6}
  • Arquitectura del modelo. En este caso, padding='same' indica que se realiza padding para que la salida tiene el mismo tamaño que la entrada.

weight_decay = 1e-4
num_classes = 7
model = tf.keras.models.Sequential()

model.add(Conv2D(64, (4,4), padding='same', kernel_regularizer=regularizers.l2(weight_decay), input_shape=(48,48,1)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(64, (4,4), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.2))
 
model.add(Conv2D(128, (4,4), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.3))
 
model.add(Conv2D(128, (4,4), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(Conv2D(128, (4,4), padding='same', kernel_regularizer=regularizers.l2(weight_decay)))
model.add(Activation('elu'))
model.add(BatchNormalization())
model.add(MaxPooling2D(pool_size=(2,2)))
model.add(Dropout(0.4))
model.add(Flatten())
model.add(Dense(128, activation="linear"))
model.add(Activation('elu'))
model.add(Dense(num_classes, activation='softmax'))

model.compile(loss='categorical_crossentropy', optimizer=Adam(0.0001), metrics=[AUC(name='auc')])
model.summary()
Model: "sequential_7"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ conv2d_24 (Conv2D)              │ (None, 48, 48, 64)     │         1,088 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_28 (Activation)      │ (None, 48, 48, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_24          │ (None, 48, 48, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_25 (Conv2D)              │ (None, 48, 48, 64)     │        65,600 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_29 (Activation)      │ (None, 48, 48, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_25          │ (None, 48, 48, 64)     │           256 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_12 (MaxPooling2D) │ (None, 24, 24, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_19 (Dropout)            │ (None, 24, 24, 64)     │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_26 (Conv2D)              │ (None, 24, 24, 128)    │       131,200 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_30 (Activation)      │ (None, 24, 24, 128)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_26          │ (None, 24, 24, 128)    │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_13 (MaxPooling2D) │ (None, 12, 12, 128)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_20 (Dropout)            │ (None, 12, 12, 128)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_27 (Conv2D)              │ (None, 12, 12, 128)    │       262,272 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_31 (Activation)      │ (None, 12, 12, 128)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_27          │ (None, 12, 12, 128)    │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ conv2d_28 (Conv2D)              │ (None, 12, 12, 128)    │       262,272 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_32 (Activation)      │ (None, 12, 12, 128)    │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ batch_normalization_28          │ (None, 12, 12, 128)    │           512 │
│ (BatchNormalization)            │                        │               │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ max_pooling2d_14 (MaxPooling2D) │ (None, 6, 6, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout_21 (Dropout)            │ (None, 6, 6, 128)      │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ flatten_4 (Flatten)             │ (None, 4608)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_36 (Dense)                │ (None, 128)            │       589,952 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ activation_33 (Activation)      │ (None, 128)            │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_37 (Dense)                │ (None, 7)              │           903 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 1,315,335 (5.02 MB)
 Trainable params: 1,314,311 (5.01 MB)
 Non-trainable params: 1,024 (4.00 KB)
plot_model(model, to_file='model_cnn.png', show_shapes=True, show_layer_names=True)
_images/ef92fbc455a8789fc49b10bc3297d539cb7005ee666b25ea1e1526fb591490f1.png
epochs = 60
checkpointer = [
    EarlyStopping(
        monitor='val_auc',  
        verbose=1,
        restore_best_weights=True,
        mode='max',
        patience=10
    ),
    ModelCheckpoint(
        filepath='model.weights.best.keras',
        monitor='val_auc',
        verbose=1,
        save_best_only=True,
        mode='max'
    )
]
import os
from joblib import dump, load
history_cnn = None

if os.path.exists('history_cnn.joblib'):
    history_cnn = load('history_cnn.joblib')
    print("El archivo 'history_cnn.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_cnn = model.fit(x = training_set, 
                            epochs = epochs, callbacks=checkpointer, 
                            validation_data = validation_set,
                            verbose = 2)
    dump(history_cnn.history, 'history_cnn.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_cnn.joblib'.")
Epoch 1/60

Epoch 1: val_auc improved from -inf to 0.67143, saving model to model.weights.best.keras
359/359 - 11s - 29ms/step - auc: 0.6803 - loss: 1.9372 - val_auc: 0.6714 - val_loss: 2.0760
Epoch 2/60

Epoch 2: val_auc improved from 0.67143 to 0.76404, saving model to model.weights.best.keras
359/359 - 5s - 14ms/step - auc: 0.7530 - loss: 1.6691 - val_auc: 0.7640 - val_loss: 1.6854
Epoch 3/60

Epoch 3: val_auc improved from 0.76404 to 0.80314, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.7803 - loss: 1.5911 - val_auc: 0.8031 - val_loss: 1.5313
Epoch 4/60

Epoch 4: val_auc improved from 0.80314 to 0.81094, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8020 - loss: 1.5235 - val_auc: 0.8109 - val_loss: 1.5168
Epoch 5/60

Epoch 5: val_auc improved from 0.81094 to 0.83368, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8205 - loss: 1.4616 - val_auc: 0.8337 - val_loss: 1.4306
Epoch 6/60

Epoch 6: val_auc improved from 0.83368 to 0.84118, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8351 - loss: 1.4098 - val_auc: 0.8412 - val_loss: 1.3964
Epoch 7/60

Epoch 7: val_auc improved from 0.84118 to 0.84737, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8458 - loss: 1.3690 - val_auc: 0.8474 - val_loss: 1.3709
Epoch 8/60

Epoch 8: val_auc improved from 0.84737 to 0.85888, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8547 - loss: 1.3334 - val_auc: 0.8589 - val_loss: 1.3216
Epoch 9/60

Epoch 9: val_auc improved from 0.85888 to 0.86304, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8641 - loss: 1.2943 - val_auc: 0.8630 - val_loss: 1.3112
Epoch 10/60

Epoch 10: val_auc improved from 0.86304 to 0.86898, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8712 - loss: 1.2645 - val_auc: 0.8690 - val_loss: 1.2803
Epoch 11/60

Epoch 11: val_auc improved from 0.86898 to 0.87462, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8779 - loss: 1.2349 - val_auc: 0.8746 - val_loss: 1.2529
Epoch 12/60

Epoch 12: val_auc did not improve from 0.87462
359/359 - 5s - 13ms/step - auc: 0.8843 - loss: 1.2048 - val_auc: 0.8728 - val_loss: 1.2734
Epoch 13/60

Epoch 13: val_auc improved from 0.87462 to 0.88140, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8893 - loss: 1.1819 - val_auc: 0.8814 - val_loss: 1.2270
Epoch 14/60

Epoch 14: val_auc improved from 0.88140 to 0.88157, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8935 - loss: 1.1611 - val_auc: 0.8816 - val_loss: 1.2274
Epoch 15/60

Epoch 15: val_auc improved from 0.88157 to 0.88174, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.8993 - loss: 1.1305 - val_auc: 0.8817 - val_loss: 1.2370
Epoch 16/60

Epoch 16: val_auc improved from 0.88174 to 0.88516, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9041 - loss: 1.1067 - val_auc: 0.8852 - val_loss: 1.2197
Epoch 17/60

Epoch 17: val_auc improved from 0.88516 to 0.89153, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9074 - loss: 1.0887 - val_auc: 0.8915 - val_loss: 1.1922
Epoch 18/60

Epoch 18: val_auc improved from 0.89153 to 0.89536, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9117 - loss: 1.0657 - val_auc: 0.8954 - val_loss: 1.1660
Epoch 19/60

Epoch 19: val_auc did not improve from 0.89536
359/359 - 5s - 13ms/step - auc: 0.9145 - loss: 1.0491 - val_auc: 0.8946 - val_loss: 1.1730
Epoch 20/60

Epoch 20: val_auc improved from 0.89536 to 0.89640, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9188 - loss: 1.0241 - val_auc: 0.8964 - val_loss: 1.1618
Epoch 21/60

Epoch 21: val_auc improved from 0.89640 to 0.89986, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9226 - loss: 1.0025 - val_auc: 0.8999 - val_loss: 1.1463
Epoch 22/60

Epoch 22: val_auc did not improve from 0.89986
359/359 - 5s - 13ms/step - auc: 0.9253 - loss: 0.9858 - val_auc: 0.8986 - val_loss: 1.1517
Epoch 23/60

Epoch 23: val_auc improved from 0.89986 to 0.90138, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9287 - loss: 0.9642 - val_auc: 0.9014 - val_loss: 1.1429
Epoch 24/60

Epoch 24: val_auc did not improve from 0.90138
359/359 - 5s - 13ms/step - auc: 0.9319 - loss: 0.9433 - val_auc: 0.8994 - val_loss: 1.1586
Epoch 25/60

Epoch 25: val_auc did not improve from 0.90138
359/359 - 5s - 13ms/step - auc: 0.9343 - loss: 0.9273 - val_auc: 0.8987 - val_loss: 1.1773
Epoch 26/60

Epoch 26: val_auc improved from 0.90138 to 0.90340, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9376 - loss: 0.9059 - val_auc: 0.9034 - val_loss: 1.1351
Epoch 27/60

Epoch 27: val_auc improved from 0.90340 to 0.90473, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9405 - loss: 0.8848 - val_auc: 0.9047 - val_loss: 1.1375
Epoch 28/60

Epoch 28: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9436 - loss: 0.8633 - val_auc: 0.9007 - val_loss: 1.1595
Epoch 29/60

Epoch 29: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9448 - loss: 0.8552 - val_auc: 0.9046 - val_loss: 1.1446
Epoch 30/60

Epoch 30: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9474 - loss: 0.8350 - val_auc: 0.9017 - val_loss: 1.1689
Epoch 31/60

Epoch 31: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9508 - loss: 0.8097 - val_auc: 0.9023 - val_loss: 1.1735
Epoch 32/60

Epoch 32: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9532 - loss: 0.7912 - val_auc: 0.8993 - val_loss: 1.1985
Epoch 33/60

Epoch 33: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9551 - loss: 0.7748 - val_auc: 0.9043 - val_loss: 1.1604
Epoch 34/60

Epoch 34: val_auc did not improve from 0.90473
359/359 - 5s - 13ms/step - auc: 0.9575 - loss: 0.7554 - val_auc: 0.9046 - val_loss: 1.1811
Epoch 35/60

Epoch 35: val_auc improved from 0.90473 to 0.90527, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9586 - loss: 0.7457 - val_auc: 0.9053 - val_loss: 1.1777
Epoch 36/60

Epoch 36: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9610 - loss: 0.7247 - val_auc: 0.9021 - val_loss: 1.2007
Epoch 37/60

Epoch 37: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9627 - loss: 0.7094 - val_auc: 0.9047 - val_loss: 1.1869
Epoch 38/60

Epoch 38: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9653 - loss: 0.6857 - val_auc: 0.9025 - val_loss: 1.2195
Epoch 39/60

Epoch 39: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9662 - loss: 0.6781 - val_auc: 0.9027 - val_loss: 1.2086
Epoch 40/60

Epoch 40: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9679 - loss: 0.6612 - val_auc: 0.9052 - val_loss: 1.2024
Epoch 41/60

Epoch 41: val_auc did not improve from 0.90527
359/359 - 5s - 13ms/step - auc: 0.9701 - loss: 0.6400 - val_auc: 0.9039 - val_loss: 1.2260
Epoch 42/60

Epoch 42: val_auc improved from 0.90527 to 0.90562, saving model to model.weights.best.keras
359/359 - 5s - 13ms/step - auc: 0.9715 - loss: 0.6244 - val_auc: 0.9056 - val_loss: 1.1978
Epoch 43/60

Epoch 43: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9733 - loss: 0.6056 - val_auc: 0.9028 - val_loss: 1.2416
Epoch 44/60

Epoch 44: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9744 - loss: 0.5929 - val_auc: 0.9024 - val_loss: 1.2413
Epoch 45/60

Epoch 45: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9761 - loss: 0.5754 - val_auc: 0.9032 - val_loss: 1.2518
Epoch 46/60

Epoch 46: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9772 - loss: 0.5634 - val_auc: 0.9013 - val_loss: 1.2659
Epoch 47/60

Epoch 47: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9778 - loss: 0.5560 - val_auc: 0.9024 - val_loss: 1.2741
Epoch 48/60

Epoch 48: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9796 - loss: 0.5339 - val_auc: 0.9046 - val_loss: 1.2549
Epoch 49/60

Epoch 49: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9807 - loss: 0.5215 - val_auc: 0.9052 - val_loss: 1.2472
Epoch 50/60

Epoch 50: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9813 - loss: 0.5111 - val_auc: 0.9037 - val_loss: 1.2770
Epoch 51/60

Epoch 51: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9827 - loss: 0.4967 - val_auc: 0.9030 - val_loss: 1.2908
Epoch 52/60

Epoch 52: val_auc did not improve from 0.90562
359/359 - 5s - 13ms/step - auc: 0.9833 - loss: 0.4875 - val_auc: 0.9029 - val_loss: 1.3039
Epoch 52: early stopping
Restoring model weights from the end of the best epoch: 42.
El entrenamiento se ha completado y el historial ha sido guardado en 'history_cnn.joblib'.
fig, ax = plt.subplots(1, 2)
fig.set_size_inches(12, 4)

ax[0].plot(history_cnn.history['auc'])
ax[0].plot(history_cnn.history['val_auc'])
ax[0].set_title('AUC en Entrenamiento vs Validación')
ax[0].set_ylabel('AUC')
ax[0].set_xlabel('Época')
ax[0].legend(['Entrenamiento', 'Validación'], loc='lower right')

ax[1].plot(history_cnn.history['loss'])
ax[1].plot(history_cnn.history['val_loss'])
ax[1].set_title('Pérdida en Entrenamiento vs Validación')
ax[1].set_ylabel('Pérdida')
ax[1].set_xlabel('Época')
ax[1].legend(['Entrenamiento', 'Validación'], loc='upper right')

plt.tight_layout()
plt.show()
_images/280f723223f273c70c56505c4e5814a5e45045f36b2a64fd70b316ca9285ffd2.png
train_loss, train_auc = model.evaluate(training_set)
test_loss, test_auc   = model.evaluate(validation_set)
print("final train accuracy = {:.2f} , validation accuracy = {:.2f}".format(train_auc*100, test_auc*100))
359/359 ━━━━━━━━━━━━━━━━━━━━ 2s 7ms/step - auc: 0.9908 - loss: 0.4052
90/90 ━━━━━━━━━━━━━━━━━━━━ 1s 6ms/step - auc: 0.9010 - loss: 1.2412
final train accuracy = 99.03 , validation accuracy = 90.65
  • Matriz de confusión del conjunto de entrenamiento

y_pred = model.predict(training_set)
y_pred = np.argmax(y_pred, axis=1)
class_labels = test_set.class_indices
class_labels = {v:k for k,v in class_labels.items()}

from sklearn.metrics import classification_report, confusion_matrix
cm_train = confusion_matrix(training_set.classes, y_pred)
print('Confusion Matrix')
print(cm_train)
print('Classification Report')
target_names = list(class_labels.values())
print(classification_report(training_set.classes, y_pred, target_names=target_names))

plt.figure(figsize=(8,8))
plt.imshow(cm_train, interpolation='nearest')
plt.colorbar()
tick_mark = np.arange(len(target_names))
_ = plt.xticks(tick_mark, target_names, rotation=90)
_ = plt.yticks(tick_mark, target_names)
359/359 ━━━━━━━━━━━━━━━━━━━━ 2s 7ms/step
Confusion Matrix
[[ 458   51  403  820  568  560  336]
 [  43    6   37   79   71   80   33]
 [ 461   45  366  814  635  601  356]
 [ 776   68  699 1536 1113  961  619]
 [ 519   68  495 1030  760  667  433]
 [ 529   54  493  958  729  694  407]
 [ 344   30  337  627  493  443  263]]
Classification Report
              precision    recall  f1-score   support

       angry       0.15      0.14      0.14      3196
     disgust       0.02      0.02      0.02       349
        fear       0.13      0.11      0.12      3278
       happy       0.26      0.27      0.26      5772
     neutral       0.17      0.19      0.18      3972
         sad       0.17      0.18      0.18      3864
    surprise       0.11      0.10      0.11      2537

    accuracy                           0.18     22968
   macro avg       0.14      0.14      0.14     22968
weighted avg       0.18      0.18      0.18     22968
_images/8e3c7a5b7708bb78311b679121d082007e38fd9ccc4c65647898d72b71d56031.png
  • Matriz de confusión en el conjunto de datos de validación

y_pred = model.predict(validation_set)
y_pred = np.argmax(y_pred, axis=1)
cm_val = confusion_matrix(validation_set.classes, y_pred)
print('Confusion Matrix')
print(cm_val)
print('Classification Report')
target_names = list(class_labels.values())
print(classification_report(validation_set.classes, y_pred, target_names=target_names))

plt.figure(figsize=(8,8))
plt.imshow(cm_train, interpolation='nearest')
plt.colorbar()
tick_mark = np.arange(len(target_names))
_ = plt.xticks(tick_mark, target_names, rotation=90)
_ = plt.yticks(tick_mark, target_names)
90/90 ━━━━━━━━━━━━━━━━━━━━ 1s 9ms/step
Confusion Matrix
[[106   4  86 207 165 161  70]
 [ 10   0  12  22  20  14   9]
 [121  11  71 199 170 166  81]
 [183  12 134 381 313 250 170]
 [114   7 114 240 228 206  84]
 [121  13 121 245 202 168  96]
 [ 77  12  73 178 114 108  72]]
Classification Report
              precision    recall  f1-score   support

       angry       0.14      0.13      0.14       799
     disgust       0.00      0.00      0.00        87
        fear       0.12      0.09      0.10       819
       happy       0.26      0.26      0.26      1443
     neutral       0.19      0.23      0.21       993
         sad       0.16      0.17      0.16       966
    surprise       0.12      0.11      0.12       634

    accuracy                           0.18      5741
   macro avg       0.14      0.14      0.14      5741
weighted avg       0.17      0.18      0.18      5741
_images/8e3c7a5b7708bb78311b679121d082007e38fd9ccc4c65647898d72b71d56031.png
  • Matriz de confusión del conjunto de datos de prueba

y_pred = model.predict(test_set)
y_pred = np.argmax(y_pred, axis=1)

cm_test = confusion_matrix(test_set.classes, y_pred)
print('Confusion Matrix')
print(cm_test)
print('Classification Report')
target_names = list(class_labels.values())
print(classification_report(test_set.classes, y_pred, target_names=target_names))

plt.figure(figsize=(8,8))
plt.imshow(cm_test, interpolation='nearest')
plt.colorbar()
tick_mark = np.arange(len(target_names))
_ = plt.xticks(tick_mark, target_names, rotation=90)
_ = plt.yticks(tick_mark, target_names)
113/113 ━━━━━━━━━━━━━━━━━━━━ 1s 7ms/step
Confusion Matrix
[[139  10  96 227 189 194 103]
 [ 17   0  15  25  20  24  10]
 [116   9 109 260 212 204 114]
 [234  21 175 437 383 328 196]
 [176  10 115 326 258 218 130]
 [176  10 133 319 249 225 135]
 [125   8  77 230 169 142  80]]
Classification Report
              precision    recall  f1-score   support

       angry       0.14      0.15      0.14       958
     disgust       0.00      0.00      0.00       111
        fear       0.15      0.11      0.12      1024
       happy       0.24      0.25      0.24      1774
     neutral       0.17      0.21      0.19      1233
         sad       0.17      0.18      0.17      1247
    surprise       0.10      0.10      0.10       831

    accuracy                           0.17      7178
   macro avg       0.14      0.14      0.14      7178
weighted avg       0.17      0.17      0.17      7178
_images/020eb310299efc2fbeb102dda6f64c200a82770caea437d94406fd4f3f6a9b22.png
  • Grafico de predicciones

import numpy as np

x_test, y_test = next(test_set)  
predict = model.predict(x_test)

figure = plt.figure(figsize=(20, 8))
for i, index in enumerate(np.random.choice(x_test.shape[0], size=24, replace=False)):
    ax = figure.add_subplot(4, 6, i + 1, xticks=[], yticks=[])
    ax.imshow(np.squeeze(x_test[index]))
    
    predict_index = class_labels[np.argmax(predict[index])]
    true_index = class_labels[np.argmax(y_test[index])]
    
    ax.set_title(f"{predict_index} ({true_index})",
                 color=("green" if predict_index == true_index else "red"))
2/2 ━━━━━━━━━━━━━━━━━━━━ 0s 56ms/step
_images/fce5b99b6ae18ec194e9c64f805aa9420dd6d8f7a75f5535d2aeb09f82d5d9b8.png

10.14.18. Aplicación: Segmentación de Imágenes Biomédicas con U-Net#

  • El cáncer de mama es una de las principales causas de mortalidad en mujeres, y su detección temprana reduce muertes prematuras. Este conjunto de datos de ultrasonido mamario incluye 780 imágenes de 600 pacientes (25-75 años), recopiladas en 2018, clasificadas en normales, benignas y malignas. Las imágenes, en formato PNG (500×500 píxeles), incluyen referencias (ground truth) para mejorar su análisis mediante aprendizaje automático en clasificación, detección y segmentación (ver Breast Ultrasound Images Dataset).

import tensorflow as tf
from glob import glob
import numpy as np
from sklearn.model_selection import train_test_split
import cv2
import matplotlib.pyplot as plt
from keras.models import Model
from keras.layers import Input, Conv2D, MaxPooling2D, Conv2DTranspose, concatenate
from keras.optimizers import Adam
from tensorflow.keras.metrics import *
2025-03-18 21:14:42.262119: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2025-03-18 21:14:42.534674: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2025-03-18 21:14:42.635125: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2025-03-18 21:14:42.663016: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2025-03-18 21:14:42.834720: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-03-18 21:14:44.051401: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
paths = glob('/home/lihkir/datasets/Dataset_BUSI_with_GT/*/*')
print(f'\033[92m')
print(f"'normal' class has {len([i for i in paths if 'normal' in i and 'mask' not in i])} images and {len([i for i in paths if 'normal' in i and 'mask' in i])} masks.")
print(f"'benign' class has {len([i for i in paths if 'benign' in i and 'mask' not in i])} images and {len([i for i in paths if 'benign' in i and 'mask' in i])} masks.")
print(f"'malignant' class has {len([i for i in paths if 'malignant' in i and 'mask' not in i])} images and {len([i for i in paths if 'malignant' in i and 'mask' in i])} masks.")
print(f"\nThere are total of {len([i for i in paths if 'mask' not in i])} images and {len([i for i in paths if 'mask' in i])} masks.")

'normal' class has 133 images and 133 masks.
'benign' class has 437 images and 454 masks.
'malignant' class has 210 images and 211 masks.

There are total of 780 images and 798 masks.
sorted(glob('/home/lihkir/datasets/Dataset_BUSI_with_GT/benign/*'))[4:7]
['/home/lihkir/datasets/Dataset_BUSI_with_GT/benign/benign (100).png',
 '/home/lihkir/datasets/Dataset_BUSI_with_GT/benign/benign (100)_mask.png',
 '/home/lihkir/datasets/Dataset_BUSI_with_GT/benign/benign (100)_mask_1.png']

Cargue de datos

def load_image(path, size):
    """
    Carga una imagen desde la ruta especificada, la redimensiona y la convierte a escala de grises.

    Parámetros:
    path (str): Ruta de la imagen.
    size (int): Tamaño deseado para la imagen (size x size).

    Retorna:
    np.array: Imagen en escala de grises normalizada.
    """
    image = cv2.imread(path)  # Cargar la imagen
    image = cv2.resize(image, (size, size))  # Redimensionar la imagen
    image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)  # Convertir a escala de grises (shape: (size, size, 3) -> (size, size, 1))
    image = image / 255.  # Normalizar la imagen (valores entre 0 y 1)
    return image
def load_data(root_path, size):
    """
    Carga imágenes y máscaras desde una carpeta, asegurando que múltiples máscaras para una misma imagen se combinen correctamente.

    Parámetros:
    root_path (str): Ruta donde se encuentran las imágenes y máscaras.
    size (int): Tamaño deseado para las imágenes y máscaras.

    Retorna:
    tuple: Dos arrays de NumPy, uno con las imágenes y otro con las máscaras.
    """
    images = []  # Lista para almacenar las imágenes
    masks = []  # Lista para almacenar las máscaras
    
    x = 0  # Variable auxiliar para identificar imágenes con más de una máscara
    
    for path in sorted(glob(root_path)):  # Iterar sobre las imágenes y máscaras en la carpeta
        img = load_image(path, size)  # Cargar la imagen o máscara
        
        if 'mask' in path:  # Verificar si el archivo es una máscara
            if x:  # Si la imagen ya tiene una máscara previa
                masks[-1] += img  # Sumar la nueva máscara a la última almacenada
                
                # Al sumar dos máscaras, los valores pueden variar entre 0 y 2, por lo que se normaliza nuevamente al rango 0-1.
                masks[-1] = np.array(masks[-1] > 0.5, dtype='float64')
            else:
                masks.append(img)  # Agregar la máscara a la lista
                x = 1  # Marcar que esta imagen tiene una máscara
        else:
            images.append(img)  # Agregar la imagen a la lista
            x = 0  # Reiniciar el indicador para la siguiente imagen
            
    return np.array(images), np.array(masks)  # Convertir listas a arrays de NumPy y retornar
X, y = load_data(root_path='/home/lihkir/datasets/Dataset_BUSI_with_GT/*/*', size = 128)  

EDA

fig, ax = plt.subplots(1, 3, figsize=(10, 5))

# Rango de índices para cada categoría de imágenes en el conjunto de datos:
# X[0:437] - Benigno
# X[437:647] - Maligno
# X[647:780] - Normal

# Selecciona un índice aleatorio dentro del rango de imágenes normales
i = np.random.randint(647, 780)

# Muestra la imagen original en escala de grises
ax[0].imshow(X[i], cmap='gray')
ax[0].set_title('Imagen')

# Muestra la máscara correspondiente
ax[1].imshow(y[i], cmap='gray')
ax[1].set_title('Máscara')

# Superpone la máscara sobre la imagen original con transparencia
ax[2].imshow(X[i], cmap='gray')
ax[2].imshow(tf.squeeze(y[i]), alpha=0.5, cmap='jet')
ax[2].set_title('Superposición')

# Título general de la figura
fig.suptitle('Clase Normal', fontsize=16)

# Muestra la figura con las imágenes
plt.show()
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1742351308.174658    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351308.925600    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351308.925692    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351308.934129    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351308.934177    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351308.934196    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351309.072419    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1742351309.072509    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-03-18 21:28:29.072547: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2112] Could not identify NUMA node of platform GPU id 0, defaulting to 0.  Your kernel may not have been built with NUMA support.
I0000 00:00:1742351309.072612    1910 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-03-18 21:28:29.073281: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2021] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 9558 MB memory:  -> device: 0, name: NVIDIA GeForce RTX 4070, pci bus id: 0000:01:00.0, compute capability: 8.9
_images/c14f0285e6dffd5133dfd1fb749a9674786d2cc80938877007088e344bf1c7af.png
fig, ax = plt.subplots(1,3, figsize=(10,5))

i = np.random.randint(437)
ax[0].imshow(X[i], cmap='gray')
ax[0].set_title('Image')
ax[1].imshow(y[i], cmap='gray')
ax[1].set_title('Mask')
ax[2].imshow(X[i], cmap='gray')
ax[2].imshow(tf.squeeze(y[i]), alpha=0.5, cmap='jet')
ax[2].set_title('Union')
fig.suptitle('Benign class', fontsize=16)
plt.show()
_images/7d539561c89921ade9702789875a497d82d92fc2c30c48fe418a6b096f3e1048.png
fig, ax = plt.subplots(1,3, figsize=(10,5))

i = np.random.randint(437,647)
ax[0].imshow(X[i], cmap='gray')
ax[0].set_title('Image')
ax[1].imshow(y[i], cmap='gray')
ax[1].set_title('Mask')
ax[2].imshow(X[i], cmap='gray')
ax[2].imshow(tf.squeeze(y[i]), alpha=0.5, cmap='jet')
ax[2].set_title('Union')
fig.suptitle('Malignant class', fontsize=16)
plt.show()
_images/c578dcb6b302c6d61aeb6e22dad90fb6fd2600b3dcf2fcf5771f29bdc5a0f85c.png

Preprocesamiento

# Eliminar la clase "normal" porque no tiene máscara (usar las primera 647)
X = X[:647]
y = y[:647]

print(f"Forma de X: {X.shape}     |  Forma de y: {y.shape}")

# Preparar los datos para el modelado. Agregar número de canales
X = np.expand_dims(X, -1)
y = np.expand_dims(y, -1)

print(f"\nForma de X: {X.shape}  |  Forma de y: {y.shape}")
Forma de X: (647, 128, 128)     |  Forma de y: (647, 128, 128)

Forma de X: (647, 128, 128, 1)  |  Forma de y: (647, 128, 128, 1)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.1)

print(f'\033[92m')
print('X_train shape:',X_train.shape)
print('y_train shape:',y_train.shape)
print('X_test shape:',X_test.shape)
print('y_test shape:',y_test.shape)

X_train shape: (582, 128, 128, 1)
y_train shape: (582, 128, 128, 1)
X_test shape: (65, 128, 128, 1)
y_test shape: (65, 128, 128, 1)

Construcción de la Arquitectura U-Net

  • Conv block

def conv_block(input, num_filters):
    conv = Conv2D(num_filters, (3, 3), activation="relu", padding="same", kernel_initializer='he_normal')(input)
    conv = Conv2D(num_filters, (3, 3), activation="relu", padding="same", kernel_initializer='he_normal')(conv)
    return conv
  • Encoder block

def encoder_block(input, num_filters):
    conv = conv_block(input, num_filters)
    pool = MaxPooling2D((2, 2))(conv)
    return conv, pool
  • Decoder block

def decoder_block(input, skip_features, num_filters):
    uconv = Conv2DTranspose(num_filters, (2, 2), strides=2, padding="same")(input)
    con = concatenate([uconv, skip_features])
    conv = conv_block(con, num_filters)
    return conv
  • Modelo U-Net

def build_model(input_shape):
    input_layer = Input(input_shape)
    
    s1, p1 = encoder_block(input_layer, 64)
    s2, p2 = encoder_block(p1, 128)
    s3, p3 = encoder_block(p2, 256)
    s4, p4 = encoder_block(p3, 512)

    b1 = conv_block(p4, 1024)

    d1 = decoder_block(b1, s4, 512)
    d2 = decoder_block(d1, s3, 256)
    d3 = decoder_block(d2, s2, 128)
    d4 = decoder_block(d3, s1, 64)
    
    output_layer = Conv2D(1, 1, padding="same", activation="sigmoid")(d4)
    
    model = Model(input_layer, output_layer, name="U-Net")
    return model

model = build_model(input_shape=(size, size, 1))
model.compile(loss="binary_crossentropy", optimizer="Adam", metrics=["accuracy"])
tf.keras.utils.plot_model(model, show_shapes=True)
_images/46cc10ceee216ba4e5a39f79a7c3d3a08be9636cfdce59775d5e65e41ac59960.png
model.summary()
Model: "U-Net"
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━┓
┃ Layer (type)         Output Shape          Param #  Connected to      ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━┩
│ input_layer         │ (None, 128, 128,  │          0 │ -                 │
│ (InputLayer)        │ 1)                │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d (Conv2D)     │ (None, 128, 128,  │        640 │ input_layer[0][0] │
│                     │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_1 (Conv2D)   │ (None, 128, 128,  │     36,928 │ conv2d[0][0]      │
│                     │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ max_pooling2d       │ (None, 64, 64,    │          0 │ conv2d_1[0][0]    │
│ (MaxPooling2D)      │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_2 (Conv2D)   │ (None, 64, 64,    │     73,856 │ max_pooling2d[0]… │
│                     │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_3 (Conv2D)   │ (None, 64, 64,    │    147,584 │ conv2d_2[0][0]    │
│                     │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ max_pooling2d_1     │ (None, 32, 32,    │          0 │ conv2d_3[0][0]    │
│ (MaxPooling2D)      │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_4 (Conv2D)   │ (None, 32, 32,    │    295,168 │ max_pooling2d_1[ │
│                     │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_5 (Conv2D)   │ (None, 32, 32,    │    590,080 │ conv2d_4[0][0]    │
│                     │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ max_pooling2d_2     │ (None, 16, 16,    │          0 │ conv2d_5[0][0]    │
│ (MaxPooling2D)      │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_6 (Conv2D)   │ (None, 16, 16,    │  1,180,160 │ max_pooling2d_2[ │
│                     │ 512)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_7 (Conv2D)   │ (None, 16, 16,    │  2,359,808 │ conv2d_6[0][0]    │
│                     │ 512)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ max_pooling2d_3     │ (None, 8, 8, 512) │          0 │ conv2d_7[0][0]    │
│ (MaxPooling2D)      │                   │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_8 (Conv2D)   │ (None, 8, 8,      │  4,719,616 │ max_pooling2d_3[ │
│                     │ 1024)             │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_9 (Conv2D)   │ (None, 8, 8,      │  9,438,208 │ conv2d_8[0][0]    │
│                     │ 1024)             │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_transpose    │ (None, 16, 16,    │  2,097,664 │ conv2d_9[0][0]    │
│ (Conv2DTranspose)   │ 512)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ concatenate         │ (None, 16, 16,    │          0 │ conv2d_transpose… │
│ (Concatenate)       │ 1024)             │            │ conv2d_7[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_10 (Conv2D)  │ (None, 16, 16,    │  4,719,104 │ concatenate[0][0] │
│                     │ 512)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_11 (Conv2D)  │ (None, 16, 16,    │  2,359,808 │ conv2d_10[0][0]   │
│                     │ 512)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_transpose_1  │ (None, 32, 32,    │    524,544 │ conv2d_11[0][0]   │
│ (Conv2DTranspose)   │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ concatenate_1       │ (None, 32, 32,    │          0 │ conv2d_transpose… │
│ (Concatenate)       │ 512)              │            │ conv2d_5[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_12 (Conv2D)  │ (None, 32, 32,    │  1,179,904 │ concatenate_1[0]… │
│                     │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_13 (Conv2D)  │ (None, 32, 32,    │    590,080 │ conv2d_12[0][0]   │
│                     │ 256)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_transpose_2  │ (None, 64, 64,    │    131,200 │ conv2d_13[0][0]   │
│ (Conv2DTranspose)   │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ concatenate_2       │ (None, 64, 64,    │          0 │ conv2d_transpose… │
│ (Concatenate)       │ 256)              │            │ conv2d_3[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_14 (Conv2D)  │ (None, 64, 64,    │    295,040 │ concatenate_2[0]… │
│                     │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_15 (Conv2D)  │ (None, 64, 64,    │    147,584 │ conv2d_14[0][0]   │
│                     │ 128)              │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_transpose_3  │ (None, 128, 128,  │     32,832 │ conv2d_15[0][0]   │
│ (Conv2DTranspose)   │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ concatenate_3       │ (None, 128, 128,  │          0 │ conv2d_transpose… │
│ (Concatenate)       │ 128)              │            │ conv2d_1[0][0]    │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_16 (Conv2D)  │ (None, 128, 128,  │     73,792 │ concatenate_3[0]… │
│                     │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_17 (Conv2D)  │ (None, 128, 128,  │     36,928 │ conv2d_16[0][0]   │
│                     │ 64)               │            │                   │
├─────────────────────┼───────────────────┼────────────┼───────────────────┤
│ conv2d_18 (Conv2D)  │ (None, 128, 128,  │         65 │ conv2d_17[0][0]   │
│                     │ 1)                │            │                   │
└─────────────────────┴───────────────────┴────────────┴───────────────────┘
 Total params: 31,030,593 (118.37 MB)
 Trainable params: 31,030,593 (118.37 MB)
 Non-trainable params: 0 (0.00 B)
from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint
import os
from joblib import dump, load
checkpointer = [ 
    EarlyStopping(monitor='val_accuracy', verbose=1, restore_best_weights=True, mode='max', patience=10),
    ModelCheckpoint(
        filepath='model.weights.best.keras', 
        monitor='val_accuracy',
        verbose=1,
        save_best_only=True,
        mode='max'
    )
] 
history_unet = None

if os.path.exists('history_unet.joblib'):
    history_unet = load('history_unet.joblib')
    print("El archivo 'history_unet.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_unet = model.fit(
        X_train, y_train, 
        epochs=100, 
        validation_data=(X_test, y_test),
        callbacks=checkpointer,
        verbose=2
    )
    dump(history_unet.history, 'history_unet.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_unet.joblib'.")
Epoch 1/100

Epoch 1: val_accuracy improved from -inf to 0.94092, saving model to model.weights.best.keras
19/19 - 4s - 213ms/step - accuracy: 0.9921 - loss: 0.0152 - val_accuracy: 0.9409 - val_loss: 0.4926
Epoch 2/100

Epoch 2: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9921 - loss: 0.0153 - val_accuracy: 0.9372 - val_loss: 0.4994
Epoch 3/100

Epoch 3: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9925 - loss: 0.0143 - val_accuracy: 0.9393 - val_loss: 0.5038
Epoch 4/100

Epoch 4: val_accuracy did not improve from 0.94092
19/19 - 2s - 119ms/step - accuracy: 0.9929 - loss: 0.0132 - val_accuracy: 0.9393 - val_loss: 0.5313
Epoch 5/100

Epoch 5: val_accuracy did not improve from 0.94092
19/19 - 2s - 119ms/step - accuracy: 0.9929 - loss: 0.0132 - val_accuracy: 0.9401 - val_loss: 0.5307
Epoch 6/100

Epoch 6: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9930 - loss: 0.0134 - val_accuracy: 0.9386 - val_loss: 0.5150
Epoch 7/100

Epoch 7: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9929 - loss: 0.0136 - val_accuracy: 0.9378 - val_loss: 0.4967
Epoch 8/100

Epoch 8: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9928 - loss: 0.0133 - val_accuracy: 0.9387 - val_loss: 0.4966
Epoch 9/100

Epoch 9: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9934 - loss: 0.0122 - val_accuracy: 0.9389 - val_loss: 0.5372
Epoch 10/100

Epoch 10: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9933 - loss: 0.0124 - val_accuracy: 0.9394 - val_loss: 0.5180
Epoch 11/100

Epoch 11: val_accuracy did not improve from 0.94092
19/19 - 2s - 118ms/step - accuracy: 0.9934 - loss: 0.0121 - val_accuracy: 0.9396 - val_loss: 0.5614
Epoch 11: early stopping
Restoring model weights from the end of the best epoch: 1.
El entrenamiento se ha completado y el historial ha sido guardado en 'history_unet.joblib'.
fig, ax = plt.subplots(1, 2, figsize=(10,3))
ax[0].plot(history.epoch, history.history["loss"], label="Train loss")
ax[0].plot(history.epoch, history.history["val_loss"], label="Validation loss")
ax[0].legend()
ax[1].plot(history.epoch, history.history["accuracy"], label="Train accuracy")
ax[1].plot(history.epoch, history.history["val_accuracy"], label="Validation accuracy")
ax[1].legend()
fig.suptitle('Loss and Accuracy', fontsize=16)
plt.show()
_images/cb7db0b9e79e0008855e10f12b687e8c9106a0a50e4fd2de7cd5c6a3a3104876.png
  • Evaluación

fig, ax = plt.subplots(5,3, figsize=(10,18))

j = np.random.randint(0, X_test.shape[0], 5)
for i in range(5):
    ax[i,0].imshow(X_test[j[i]], cmap='gray')
    ax[i,0].set_title('Image')
    ax[i,1].imshow(y_test[j[i]], cmap='gray')
    ax[i,1].set_title('Mask')
    ax[i,2].imshow(model.predict(np.expand_dims(X_test[j[i]],0),verbose=0)[0], cmap='gray')
    ax[i,2].set_title('Prediction')
fig.suptitle('Results', fontsize=16)
plt.show()
_images/c24e72c6c9ee9f35a6750798a0030f945076fa931a3e5ff7b8b42d6f5079e577.png
print(f'\033[93m')
y_pred=model.predict(X_test,verbose=0)
y_pred_thresholded = y_pred > 0.5

# mean Intersection-Over-Union metric
IOU_keras = MeanIoU(num_classes=2)
IOU_keras.update_state(y_pred_thresholded, y_test)
print("Mean IoU =", IOU_keras.result().numpy())

prec_score = Precision()
prec_score.update_state(y_pred_thresholded, y_test)
p = prec_score.result().numpy()
print('Precision Score = %.3f' % p)

recall_score = Recall()
recall_score.update_state(y_pred_thresholded, y_test)
r = recall_score.result().numpy()
print('Recall Score = %.3f' % r)

f1_score = 2*(p*r)/(p+r)
print('F1 Score = %.3f' % f1_score)

Mean IoU = 0.7645979
Precision Score = 0.680
Recall Score = 0.818
F1 Score = 0.743

Evaluación del Algoritmo de Segmentación

  • En un algoritmo de segmentación, estas métricas evalúan la calidad de la segmentación comparando la máscara predicha con la máscara real (ground truth).

1. Mean IoU (Intersection over Union) = 0.7593

  • Mide la superposición entre la segmentación predicha y la real.

  • Se calcula como:

\[ IoU = \frac{\text{Área de intersección}}{\text{Área de unión}} \]

Donde:

Área de intersección: Es la cantidad de píxeles correctamente segmentados en la imagen, es decir, los píxeles que coinciden entre la predicción y la verdad (ground truth). Ejemplo: Si la segmentación real marca 40 píxeles como “gato” y la segmentación predicha marca 50 píxeles, pero 30 de ellos coinciden, entonces la intersección es 30 píxeles.

Área de unión: Es la cantidad total de píxeles marcados como objeto en cualquiera de las dos máscaras (predicha o real). Ejemplo: Siguiendo el caso anterior: 40 píxeles están en la máscara real. 50 píxeles están en la máscara predicha. 30 píxeles coinciden (intersección). La unión es: 40 + 50 − 30 = 60 píxeles

  • Un IoU de 0.7593 indica que, en promedio, el 75.93% del área segmentada coincide con la verdad.

2. Precision Score = 0.695

  • Evalúa cuántos de los píxeles predichos como positivos realmente lo son.

  • Se calcula como:

\[ \text{Precisión} = \frac{\text{TP}}{\text{TP} + \text{FP}} \]
  • Un valor de 0.695 significa que el 69.5% de las predicciones positivas fueron correctas.

3. Recall Score = 0.786

  • Mide cuántos de los píxeles verdaderamente positivos fueron detectados.

  • Se calcula como:

\[ \text{Recall} = \frac{\text{TP}}{\text{TP} + \text{FN}} \]
  • Un recall de 0.786 indica que el 78.6% de los píxeles reales fueron correctamente detectados.

4. F1 Score = 0.738

  • Es la media armónica entre precisión y recall, equilibrando ambos valores.

  • Se calcula como:

\[ F1 = 2 \times \frac{\text{Precisión} \times \text{Recall}}{\text{Precisión} + \text{Recall}} \]
  • Un F1 de 0.738 indica un buen equilibrio entre precisión y recall.

Interpretación Global

  • El modelo segmenta bien las imágenes (IoU alto).

  • El recall es mayor que la precisión, lo que indica que el modelo detecta la mayoría de los píxeles relevantes, pero a costa de generar algunos falsos positivos.

  • El F1-score de 0.738 sugiere que el modelo tiene un rendimiento sólido pero puede mejorarse.

10.15. Redes Neuronales Recurrentes#

Introducción

  • Recordemos de la sección anterior que en el corazón de las redes convolucionales se encuentra el concepto de peso compartido. Es decir, la misma matriz de filtro se desliza sobre una matriz de imágenes en lugar de dedicar un peso específico a cada píxel de la imagen. De este modo, una red neuronal puede escalarse fácilmente a imágenes de diferentes dimensiones.

  • Nuestro interés en esta sección se centra en el caso de los datos secuenciales. Es decir, los vectores de entrada no son independientes, sino que aparecen en secuencia. Además, el orden específico en que se producen encierra información importante. Por ejemplo, este tipo de secuencias se dan en el reconocimiento del habla y en el procesamiento del lenguaje, como la traducción automática, así como también el pronostico de series de tiempo financieras. Sin duda, la secuencia en la que se producen las palabras es de suma importancia.

  • El reparto de pesos mediante convoluciones también podría ser y ha sido utilizado para estos casos [Lang et al., 1990]. Tales redes se conocen como redes neuronales de retardo temporal. Sin embargo, deslizar un filtro a través del tiempo para formar convoluciones es una operación de naturaleza local. La salida es una función de las muestras de entrada dentro de una ventana temporal que abarca la longitud de la respuesta al impulso del filtro, que por razones prácticas no puede ser muy larga.

Redes Neuronales Recurrentes

  • Las variables que intervienen en una RNN son:

    • Vector de estado en el tiempo \(n\), denotado como \(\boldsymbol{h}_{n}\). El símbolo nos recuerda que \(\boldsymbol{h}\) es un vector de variables ocultas (capa oculta en la jerga de las redes neuronales); el vector de estado constituye la memoria del sistema,

    • Vector de entrada en el momento \(n\), denominado \(\boldsymbol{x}_{n}\),

    • Vector de salida en el momento \(n\), \(\hat{\boldsymbol{y}}_{n}\), y el vector de salida objetivo, \(\boldsymbol{y}_{n}\).

  • El modelo se describe mediante un conjunto de matrices y vectores de parámetros desconocidos, a saber, \(U, W, V , \boldsymbol{b}\) y \(\boldsymbol{c}\), que deben aprenderse durante el entrenamiento.

  • Las ecuaciones que describen un modelo RNN son

    (10.14)#\[\begin{split} \begin{align*} \boldsymbol{h}_{n}&=f(U\boldsymbol{x}_{n}+W\boldsymbol{h}_{n-1}+\boldsymbol{b})\\ \hat{\boldsymbol{y}}_{n}&=g(V\boldsymbol{h}_{n}+\boldsymbol{c}). \end{align*} \end{split}\]

    donde las funciones no lineales \(f\) y \(g\)*** actúan elemento a elemento (element-wise)*** y se aplican individualmente a cada elemento de sus argumentos vectoriales.

  • En otras palabras, una vez que se ha observado un nuevo vector de entrada, se actualiza el vector de estado. Su nuevo valor depende de la información más reciente, transmitida por la entrada \(\boldsymbol{x}_{n}\) así como de la historia pasada, ya que ésta se ha acumulado en \(\boldsymbol{h}_{n-1}\). La salida depende del vector de estado actualizado, \(\boldsymbol{h}_{n}\). Es decir, depende de la “historia” hasta el instante actual \(n\), tal y como se expresa en \(\boldsymbol{h}_{n}\).

  • Las opciones típicas para \(f\) son la tangente hiperbólica, tanh, o las no linealidades ReLU. El valor inicial \(\boldsymbol{h}_{0}\) suele ser igual al vector cero. La no linealidad de salida, \(g\), se elige a menudo para ser la función softmax.

_images/recurrent_neural_network_arch.png

Fig. 10.49 Arquitectura de una Red Neuronal Recurrente. Fuente [Theodoridis, 2020].#

  • De las ecuaciones anteriores se deduce que las matrices y vectores de parámetros se comparten en todos los instantes temporales. Durante el entrenamiento, se inicializan mediante números aleatorios. El modelo gráfico asociado con Eq. (10.14) se muestra en la Fig. 10.49A. En la Fig. 10.49B, el gráfico se despliega sobre los distintos instantes de tiempo para los que se dispone de observaciones. Por ejemplo, si la secuencia de interés es una frase de 10 palabras, entonces \(N\) se establece igual a 10, mientras que \(\boldsymbol{x}_{n}\) es el vector que codifica las respectivas palabras de entrada.

10.15.1. Backpropagation en tiempo#

  • El entrenamiento de las RNN sigue una lógica similar a la del algoritmo backpropagation para el entrenamiento de redes neuronales de avance. Después de todo, una RNN puede verse como una red feed-forward con \(N\) capas. La capa superior es la del instante de tiempo \(N\) y la primera capa corresponde al instante de tiempo \(n = 1\). Una diferencia radica en que las capas ocultas en una RNN también producen salidas, es decir, \(\hat{\boldsymbol{y}}_{n}\), y se alimentan directamente con entradas. Sin embargo, en lo que respecta al entrenamiento, estas diferencias no afectan al razonamiento principal.

  • El aprendizaje de las matrices y vectores de parámetros desconocidos se consigue mediante un esquema de gradiente descendente, de acuerdo con Eq. (10.3). Resulta que los gradientes requeridos de la función de coste, con respecto a los parámetros desconocidos, tienen lugar recursivamente, comenzando en el último instante de tiempo, \(N\) , y retrocediendo en el tiempo, \(n = N-1, N-2,\dots,1\). Esta es la razón por la que el algoritmo se conoce como bakpropagation a traves del tiempo (BPTT).

  • La función de coste es la suma a lo largo del tiempo, \(n\), de las correspondientes contribuciones a la función de pérdida, que dependen de los valores respectivos de \(\boldsymbol{h}_{n}, \boldsymbol{x}_{n}\), es decir,

\[ J(U, W, V, \boldsymbol{b}, \boldsymbol{c})=\sum_{n=1}^{N}J_{n}(U, W, V, \boldsymbol{b}, \boldsymbol{c}). \]
  • Por ejemplo, para el caso de la función de pérdida de entropía cruzada,

    \[ J_{n}(U, W, V, \boldsymbol{b}, \boldsymbol{c}):=-\sum_{k}y_{nk}\ln\hat{y}_{nk}, \]

    donde la suma es sobre la dimensionalidad de \(\boldsymbol{y}\), y

    \[ \hat{\boldsymbol{y}}_{n}=g(\boldsymbol{h}_{n}, V, \boldsymbol{c})~\text{y}~\boldsymbol{h}_{n}=f(\boldsymbol{x}_{n}, \boldsymbol{h}_{n-1}, U, W, \boldsymbol{b}). \]
  • En el corazón del cálculo de los gradientes de la función de coste con respecto a las diversas matrices y vectores de parámetros se encuentra el cálculo de los gradientes de \(J\) con respecto a los vectores de estado, \(\boldsymbol{h}_{n}\). Una vez calculados estos últimos, el resto de los gradientes, con respecto a las matrices y vectores de parámetros desconocidos, es una tarea sencilla. Para ello, nótese que cada \(h_{n},~n=1, 2,\dots,N-1\), afecta a \(J\) de dos maneras:

    • Directamente, a traves de \(J_{n}\)

    • Indirectamente, a través de la cadena que impone la estructura RNN, es decir,

    \[ \boldsymbol{h}_{n}\rightarrow\boldsymbol{h}_{n+1}\rightarrow\cdots\rightarrow\boldsymbol{h}_{N}. \]

    Es decir, \(\boldsymbol{h}_{n}\), además de \(J_{n}\), también afecta a todos los valores de coste posteriores, \(J_{n+1},\dots, J_{N}\). Nótese que, a traves de la cadena \(\boldsymbol{h}_{n+1}=f(\boldsymbol{x}_{n+1}, \boldsymbol{h}_{n}, U, W, \boldsymbol{b}).\)

  • Empleando la regla de la cadena para las derivadas, las dependencias anteriores conducen al siguiente cálculo recursivo:

    (10.15)#\[ \frac{\partial J}{\partial\boldsymbol{h}_{n}}=\underbrace{{\left(\frac{\partial\boldsymbol{h}_{n+1}}{\partial\boldsymbol{h}_{n}}\right)^{T}}\frac{\partial J}{\partial\boldsymbol{h}_{n+1}}}_{\text{parte recursiva indirecta}}+\underbrace{\left(\frac{\partial\hat{\boldsymbol{y}}_{n}}{\partial\boldsymbol{h}_{n}}\right)^{T}\frac{\partial J}{\partial\hat{\boldsymbol{y}}_{n}}}_{\text{parte directa}}, \]

    donde, por definición, la derivada de un vector, digamos, \(\boldsymbol{y}\), con respecto a otro vector, digamos, \(\boldsymbol{x}\), se define como la matriz

    \[ \left[\frac{\partial\boldsymbol{y}}{\partial\boldsymbol{x}}\right]_{ij}:=\frac{\partial y_{i}}{\partial x_{j}}. \]
  • Nótese que el gradiente de la función de coste, con respecto a los parámetros ocultos (vector de estado) en la capa “\(n\)”, se da como una función del gradiente respectivo en la capa anterior, es decir, con respecto al vector de estado en el tiempo \(n + 1\). Las dos pasadas requeridas por backpropagation en el tiempo se resumen a continuación.

Pasadas de Backpropagation en Tiempo

  • Paso hacia adelante:

    • Iniciando en \(n=1\) y utilizando las estimaciones actuales de las matrices y vectores de parámetros implicados, calcular en secuencia,

    \[ (\boldsymbol{h}_{1}, \hat{\boldsymbol{y}}_{1})\rightarrow(\boldsymbol{h}_{2}, \hat{\boldsymbol{y}}_{2})\rightarrow\cdots\rightarrow(\boldsymbol{h}_{N}, \hat{\boldsymbol{y}}_{N}). \]
  • Paso hacia atrás:

    • Empezando en \(n = N\), calcular en secuencia,

    \[ \frac{\partial J}{\partial\boldsymbol{h}_{N}}\rightarrow\frac{\partial J}{\partial\boldsymbol{h}_{N-1}}\rightarrow\cdots\rightarrow\frac{\partial J}{\partial\boldsymbol{h}_{1}}. \]
  • Nótese que el cálculo del gradiente \(\partial J/\partial\boldsymbol{h}_{N}\) es sencillo, y solo involucra la parte directa en Eq. (10.15).

  • Para la implementación de la BPTT, se procede a

    1. inicializar aleatoriamente las matrices y vectores desconocidos implicados,

    2. calcular todos los gradientes requeridos, siguiendo los pasos indicados anteriormente, y

    3. realizar las actualizaciones según el esquema de gradiente descendente.

  • Los pasos (2) y (3) se realizan de forma iterativa hasta que se cumple un criterio de convergencia, de forma análoga al algoritmo estándar de backpropagation.

10.15.2. Desvanecimiento y explosión de gradientes#

  • La tarea de desvanecimiento y explosión de gradientes se ha introducido y discutido en secciones anteriores, en el contexto del algoritmo de backpropagation. Los mismos problemas se presentan en el algoritmo BPTT, dado que, este último es una forma específica del concepto de backpropagation y, como se ha dicho, una RNN puede considerarse como una red multicapa, donde cada instante de tiempo corresponde a una capa diferente. De hecho en las RNN, el fenómeno de desvanecimiento/explosión de gradiente aparece de una forma bastante “agresiva”, teniendo en cuenta que \(N\) puede alcanzar valores grandes.

  • La naturaleza multiplicativa de la propagación de gradientes puede verse fácilmente en la Eq. (10.15). Para ayudar a comprender el concepto principal, simplifiquemos el escenario y supongamos que sólo interviene una variante de estado. Entonces los vectores de estado se convierten en escalares, \(h_{n}\) , y la matriz \(W\) en un escalar \(w\). Además, supongamos que las salidas también son escalares. Entonces la recursión en la Eq. (10.15) se simplifica como:

\[ \frac{\partial J}{\partial h_{n}}=\frac{\partial h_{n+1}}{\partial h_{n}}\frac{\partial J}{\partial h_{n+1}}+\frac{\partial\hat{y}_{n}}{\partial h_{n}}\frac{\partial J}{\partial\hat{y}_{n}}. \]
  • Suponiendo en la Eq. (10.14) que \(f\) es la función \(\tanh(\cdot)\) estándar, teniendo en cuenta que, \(\text{sech}^2(\cdot)=1-\tanh^2(\cdot)\) y \(|\tanh(\cdot)|<1\), sobre su dominio, se ve fácilmente que

(10.16)#\[ \frac{\partial h_{n+1}}{\partial h_{n}}=w(1-h_{n+1}^{2}). \]
  • Escribiendo la recursión para dos pasos sucesivos, repitiendo el proceso en Eq. (10.16), por ejemplo, para \(\partial h_{n+2}/\partial h_{n+1}\) obtenemos que,

(10.17)#\[\begin{split} \begin{align*} \frac{\partial J}{\partial h_{n}}&=\frac{\partial h_{n+1}}{\partial h_{n}}\frac{\partial J}{\partial h_{n+1}}+\frac{\partial\hat{y}_{n}}{\partial h_{n}}\frac{\partial J}{\partial\hat{y}_{n}}\\ &=w^{2}(1-h_{n+1}^{2})(1-h_{n+2}^{2})\frac{\partial J}{\partial h_{n+2}}+\text{otro términos} \end{align*} \end{split}\]
  • No es difícil ver que la multiplicación de los términos menores que uno puede llevar a valores de desvanecimiento, sobre todo si tenemos en cuenta que, en la práctica, las secuencias pueden ser bastante grandes, por ejemplo, \(N = 100\). Por lo tanto para instantes de tiempo cercanos a \(n = 1\), la contribución al gradiente del primer término del lado derecho en la Eq. (10.17) implicará un gran número de productos de números menores que uno en magnitud. Por otra parte, el valor de \(w\) estará contribuyendo en \(w^{n}\) potencia. Por lo tanto, si su valor es mayor que uno, puede conducir a valores explosivos de los gradientes respectivos.

  • En varios casos, se puede truncar el algoritmo de backpropagation a unos pocos pasos de tiempo. Otra forma, es sustituir la no linealidad tanh por la ReLU. Para el caso del valor explosivo, se puede introducir una técnica que recorte los valores a un umbral predeterminado, una vez que los valores superen ese umbral. Sin embargo, otra técnica que suele emplearse en la práctica es sustituir la formulación RNN estándar descrita anteriormente por una estructura alternativa, que puede hacer mejor frente a estos fenómenos causados por la dependencia a largo plazo de la RNN.

Observation 10.3

  • RNN profundas: Además de la red RNN básica que comprende una sola capa de estados, se han propuesto extensiones que implican múltiples capas de estados, una encima de otra (ver [Pascanu et al., 2013]).

  • RNN bidireccionales: Como su nombre indica, en las RNN bidireccionales hay dos variables de estado, es decir, una denotada como \(\overset{\rightarrow}{\boldsymbol{h}}\), que se propaga hacia adelante, y otra, \(\overset{\leftarrow}{\boldsymbol{h}}\), que se propaga hacia atrás. De este modo, las salidas dependen tanto del pasado como del futuro (ver [Graves et al., 2013]).

Ejemplo

  • Ejemplo de BPTT con dos secuencias de texto. Este ejemplo utiliza dos secuencias de texto (una positiva y una negativa) para demostrar cómo funciona el proceso de Backpropagation Through Time (BPTT) en una red recurrente. Además, mostramos la actualización de los pesos mediante gradiente descendente.

  • Paso 1: Definición de los datos y parámetros

  • Secuencias de Texto

    • Secuencia 1 (Positiva): "I love this movie"

    • Secuencia 2 (Negativa): "This movie is terrible"

  • Embeddings de las Palabras

    Cada palabra se representa por un vector embedding (Word2Vec) de 3 dimensiones:

    • "I" = \([0.1, 0.2, 0.1]\)

    • "love" = \([0.4, 0.5, 0.6]\)

    • "this" = \([0.3, 0.1, 0.3]\)

    • "movie" = \([0.5, 0.1, -0.3]\)

    • "is" = \([0.2, 0.3, 0.2]\)

    • "terrible" = \([-0.5, -0.6, -0.7]\)

  • Etiquetas de Salida (Clases)

    • Secuencia 1: clase 1 (positiva)

    • Secuencia 2: clase 0 (negativa)

  • Definir los Datos y Parámetros Iniciales (tres filas en las matrices significa que hay tres unidades en la capa oculta)

\[\begin{split}U = \begin{bmatrix} 0.1 & -0.2 & 0.3 \\ 0.4 & 0.1 & -0.3 \\ 0.3 & 0.2 & -0.1 \end{bmatrix}, ~W = \begin{bmatrix} 0.5 & -0.4 & 0.1 \\ -0.1 & 0.2 & 0.4 \\ 0.2 & 0.1 & 0.3 \end{bmatrix}, ~V = \begin{bmatrix} 0.2 & -0.3 \\ 0.4 & 0.1 \\ -0.2 & 0.3 \end{bmatrix}\end{split}\]
  • Sesgos

\[\begin{split}b = \begin{bmatrix} 0.1 \\ 0.1 \\ 0.1 \end{bmatrix}, \quad c = \begin{bmatrix} 0.0 \\ 0.0 \end{bmatrix}\end{split}\]
  • Paso 2: Propagación hacia Adelante

    • Vamos a recalcular las salidas de los estados ocultos \(h_n\) y la salida final \(y_n\), incluyendo la función de activación en la salida (softmax).

    • Tiempo \(n = 1\) ("I")

    • Para la palabra "I", \(x_1 = [0.1, 0.2, 0.1]\):

  1. Calcular la propagación hacia adelante del estado oculto \(h_1\):

    \[\begin{split} U \cdot x_1 = U \cdot [0.1, 0.2, 0.1]^\top = \begin{bmatrix} (0.1 \times 0.1) + (-0.2 \times 0.2) + (0.3 \times 0.1) \\ (0.4 \times 0.1) + (0.1 \times 0.2) + (-0.3 \times 0.1) \\ (0.3 \times 0.1) + (0.2 \times 0.2) + (-0.1 \times 0.1) \end{bmatrix} = \begin{bmatrix} 0.01 + (-0.04) + 0.03 \\ 0.04 + 0.02 + (-0.03) \\ 0.03 + 0.04 + (-0.01) \end{bmatrix} = \begin{bmatrix} 0.02 \\ 0.05 \\ 0.07 \end{bmatrix} \end{split}\]

    Como \(h_0 = [0, 0, 0]\):

    \[ h_1 = \tanh([0.02, 0.05, 0.07] + b) = \tanh([0.12, 0.15, 0.17]) \approx [0.119, 0.149, 0.168] \]
    • Tiempo \(n = 2\) ("love")

    • Para la palabra "love", \(x_2 = [0.4, 0.5, 0.6]\):

  2. Calcular la propagación hacia adelante del estado oculto \(h_2\):

    \[\begin{split} U \cdot x_2 = \begin{bmatrix} 0.22 \\ 0.13 \\ 0.15 \end{bmatrix} \end{split}\]
  3. Calcular el estado recurrente con \(W \cdot h_1\):

    \[\begin{split} W \cdot h_1 = \begin{bmatrix} 0.065 \\ 0.096 \\ 0.074 \end{bmatrix} \end{split}\]
  4. Propagación total del estado oculto \(h_2\):

    \[ h_2 = \tanh([0.22, 0.13, 0.15] + [0.065, 0.096, 0.074] + b) = \tanh([0.385, 0.376, 0.394]) \approx [0.367, 0.359, 0.374] \]
  • Paso 3: Cálculo de la Salida con Activación Softmax

    • La salida se calcula utilizando la matriz \(V\), aplicando después la función de activación softmax para obtener las probabilidades de las dos clases.

  1. Calculamos la salida \(\hat{y}_{2}\) antes de la activación:

    \[\begin{split} \hat{y}_{2} = V \cdot h_2 = \begin{bmatrix} 0.2 & -0.3 \\ 0.4 & 0.1 \\ -0.2 & 0.3 \end{bmatrix} \cdot [0.367, 0.359, 0.374]^\top = \begin{bmatrix} 0.0516 \\ 0.1108 \end{bmatrix} \end{split}\]
  2. Aplicamos la función de activación softmax para convertir la salida en probabilidades:

    \[\begin{split} \text{softmax}(\hat{y}_{2}) = \frac{e^{\hat{y}_{2}}}{\sum e^{\hat{y}_{2}}} = \frac{\begin{bmatrix} e^{0.0516} \\ e^{0.1108} \end{bmatrix}}{e^{0.0516} + e^{0.1108}} = \begin{bmatrix} 0.4852 \\ 0.5148 \end{bmatrix} \end{split}\]
  • Paso 4: Retropropagación del Error

    • Ahora calculamos el error y el gradiente con respecto a los pesos \(V\), \(W\), \(U\), y los sesgos, usando el método de cross-entropy y BPTT

    • Cálculo del Error

    • La pérdida es:

    \[ \text{Loss} = - \left[ y_{\text{real}} \cdot \log(\hat{y}) + (1 - y_{\text{real}}) \cdot \log(1 - \hat{y}) \right] \]
    • Nota: aquí, 0.5148 es la probabilidad asignada a la clase considerada correcta (la etiqueta real), y 0.4852 es la probabilidad para la clase incorrecta”. Si la clase verdadera es 1 (positiva) el error es:

    \[ \text{Loss} = - \log(0.5148) \approx 0.663 \]
  • Gradientes

  1. Gradiente para \(V\):

    \[ \frac{\partial L}{\partial V} = (y_{\text{pred}} - y_{\text{real}}) \cdot h_2 \]
    \[\begin{split} \frac{\partial L}{\partial V} = \left( \begin{bmatrix} 0.4852 \\ 0.5148 \end{bmatrix} - \begin{bmatrix} 0 \\ 1 \end{bmatrix} \right) \cdot \begin{bmatrix} 0.367 \\ 0.359 \\ 0.374 \end{bmatrix}^\top \end{split}\]
    \[\begin{split} \frac{\partial L}{\partial V} = \begin{bmatrix} 0.4852 & 0.4852 \\ -0.4852 & -0.4852 \\ 0.5148 & -0.5148 \end{bmatrix} \end{split}\]
  2. Gradiente para \(W\) y \(U\): Usamos la retropropagación a través del tiempo (BPTT). Los gradientes para \(W\) y \(U\) se calculan usando la regla de la cadena considerando las derivadas del estado oculto.

  • Paso 5: Actualización de Pesos

    • Usamos gradiente descendente para actualizar los pesos. Dado un valor de \(\eta = 0.01\):

    \[ V \leftarrow V - \eta \frac{\partial L}{\partial V} \]
    \[ W \leftarrow W - \eta \frac{\partial L}{\partial W}, \quad U \leftarrow U - \eta \frac{\partial L}{\partial U} \]
    • Actualización de los pesos para \(V\):

    \[\begin{split} V_{\text{nuevo}} = V - 0.01 \times \begin{bmatrix} 0.4852 & 0.4852 \\ -0.4852 & -0.4852 \\ 0.5148 & -0.5148 \end{bmatrix}=\begin{bmatrix} 0.495148 & 0.595148 \\ 0.404852 & 0.304852 \\ -0.005148 & 0.005148 \end{bmatrix} \end{split}\]
    • Realiza las actualizaciones análogas para \(W\) y \(U\).

10.16. Red de memoria a largo plazo (LSTM)#

Observation 10.4

  • La idea clave de la red LSTM, propuesta en el artículo seminal [Hochreiter and Schmidhuber, 1997], es el llamado estado de celda, que ayuda a superar los problemas asociados con los fenómenos de desvanecimiento/explosión que son causados por las dependencias largo plazo dentro de la red.

  • Las redes LSTM tienen la capacidad incorporada de controlar el flujo de información que entra y sale de la memoria del sistema mediante algoritmos no lineales. Estas puertas se implementan mediante la no linealidad sigmoidea y un multiplicador.

  • Desde un punto de vista algorítmico, las puertas equivalen a aplicar una ponderación al flujo de información correspondiente. Los pesos se sitúan en el intervalo \([0,1]\) y dependen de los valores de las variables implicadas que activan la no linealidad sigmoidea.

  • En otras palabras, la ponderación (control) de la información tiene lugar en el contexto. Según este razonamiento, la red tiene la agilidad de olvidar la información que ya ha sido utilizada y ya no es necesaria. La célula/unidad LSTM básica se se muestra en la Fig. 10.50. Se construye en torno a dos conjuntos de variables, apiladas en el vector \(\boldsymbol{s}\), que se conoce como el estado de la célula o unidad, y el vector \(\boldsymbol{h}\), que se conoce como el vector de variables ocultas. Una red LSTM se construye a partir de la concatenación sucesiva de esta unidad básica. La unidad correspondiente al tiempo \(n\), además del vector de entrada, \(\boldsymbol{x}_{n}\), recibe \(\boldsymbol{s}_{n-1}\) y \(\boldsymbol{h}_{n-1}\) de la etapa anterior y pasa \(\boldsymbol{s}_{n}\) y \(\boldsymbol{h}_{n}\) a la siguiente. A continuación se resumen las ecuaciones de actualización asociadas

_images/lstm_arch_rnn.png

Fig. 10.50 Arquitectura de unidad LSTM. Fuente [Theodoridis, 2020].#

\[\begin{split} \begin{align*} \boldsymbol{f}&=\sigma(U^{f}\boldsymbol{x}_{n}+W^{f}\boldsymbol{h}_{n-1}+\boldsymbol{b}^{f}),\\ \boldsymbol{i}&=\sigma(U^{i}\boldsymbol{x}_{n}+W^{i}\boldsymbol{h}_{n-1}+\boldsymbol{b}^{i}),\\ \tilde{\boldsymbol{s}}&=\tanh(U^{s}\boldsymbol{x}_{n}+W^{s}\boldsymbol{h}_{n-1}+\boldsymbol{b}^{s}),\\ \boldsymbol{o}&=\sigma(U^{o}\boldsymbol{x}_{n}+W^{o}\boldsymbol{h}_{n-1}+\boldsymbol{b}^{o}),\\ \boldsymbol{s}_{n}&=\boldsymbol{s}_{n-1}\circ f+\boldsymbol{i}\circ\tilde{\boldsymbol{s}},\\ \boldsymbol{h}_{n}&=\boldsymbol{o}\circ\tanh(\boldsymbol{s}_{n}), \end{align*} \end{split}\]

donde \(\circ\) denota el producto elemento a elemento entre vectores o matrices (producto de Hadamard), es decir, \((s\circ f)_{i} = s_{i}f_{i}\), y \(\sigma\) denota la función sigmoidea logística.

Observation 10.5

  • Nótese que el estado de la célula, \(\boldsymbol{s}\), pasa información directa del instante anterior al siguiente. Esta información es controlada primero por la primera puerta, según los elementos en \(f\), que toman valores en el rango \([0, 1]\), dependiendo de la entrada actual y de las variables ocultas que se reciben de la etapa anterior. Esto es lo que decíamos antes, es decir, que la ponderación se ajusta en “contexto”. A continuación, se añade nueva información, es decir, \(\tilde{\boldsymbol{s}}\), a \(\boldsymbol{s}_{n-1}\), que también está controlada por la segunda red de puertas sigmoidales (es decir, \(\boldsymbol{i}\)). Así se garantiza que la información del pasado se transmita al futuro de forma directa, lo cual, ayuda a la red a memorizar información..

  • Resulta que este tipo de memoria explota mejor las dependencias de largo alcance en los datos, en comparación con la estructura RNN básica. El vector de variables ocultas \(\boldsymbol{h}\) está controlado tanto por el estado de la célula como por los valores actuales de las variables de entrada y de estados anteriores. Todas las matrices y vectores implicados se aprenden en la fase de entrenamiento. Nótese que hay dos líneas asociadas a \(\boldsymbol{h}_{n}\). La de la derecha conduce a la siguiente etapa y la de la parte superior se utiliza para proporcionar la salida, \(\hat{\boldsymbol{y}}_{n}\), en el tiempo \(n\), a través de, digamos, la no linealidad softmax, como en las RNN estándar en Eq. (10.14).

Variantes y Aplicaciones

  • Además de la estructura LSTM ya comentada, se han propuesto diversas variantes. Un extenso estudio comparativo entre diferentes arquitecturas LSTM y RNN se puede encontrar en [Greff et al., 2016, Jozefowicz et al., 2015]. Las RNNs y las LSTMs se han utilizado con éxito en una amplia gama de aplicaciones, tales como:

  • Por ejemplo, en el procesamiento del lenguaje la entrada suele ser una secuencia de palabras, que se codifican como números (son punteros al diccionario disponible). La salida es la secuencia de palabras que hay que predecir. Durante el entrenamiento, se establece \(\boldsymbol{y}_{n} = \boldsymbol{x}_{n+1}\). Es decir, la red se entrena como un predictor no lineal.

10.17. Aplicación: Procesamiento del Lenguaje Natural#

import warnings
warnings.filterwarnings('ignore')
import sys
import tensorflow.keras
import pandas as pd
import sklearn as sk
import tensorflow as tf
  • Iniciamos verificando que Tensorflow está correctamente instalado, y que, además, puede utilizar la GPU disponible en su computadora. En este caso corresponde a una tarjeta de video dedicada GTX 4070 Maxq 12G

print(f"Tensor Flow Version: {tf.__version__}");
print();
print(f"Python {sys.version}");
print(f"Pandas {pd.__version__}");
print(f"Scikit-Learn {sk.__version__}");
gpu = len(tf.config.list_physical_devices('GPU'))>0
print("GPU is", "available" if gpu else "NOT AVAILABLE");
Tensor Flow Version: 2.17.0

Python 3.10.12 (main, Sep 11 2024, 15:47:36) [GCC 11.4.0]
Pandas 2.2.3
Scikit-Learn 1.5.2
GPU is available
  • La librería gensim utilizada en la presente implementación, debe ser instalada en su versión 3.8.3 usando la orden. Además, debe instalar graphviz del siguiente sitio web (ver GraphViz)

pip install gensim==3.8.3
import pandas as pd
import numpy as np
from tqdm import tqdm
from tensorflow.keras.preprocessing.text import Tokenizer
tqdm.pandas(desc="progress-bar")
from gensim.models import Doc2Vec
from sklearn import utils
from sklearn.model_selection import train_test_split
from keras_preprocessing.sequence import pad_sequences
import gensim
from sklearn.linear_model import LogisticRegression
from gensim.models.doc2vec import TaggedDocument
import re
import seaborn as sns
import matplotlib.pyplot as plt
df = pd.read_csv('https://raw.githubusercontent.com/lihkir/Data/main/spam_text_class.csv',delimiter=',',encoding='latin-1')
df = df[['Category','Message']]
df = df[pd.notnull(df['Message'])]
df.rename(columns = {'Message':'Message'}, inplace = True)
df.head()
Category Message
0 ham Go until jurong point, crazy.. Available only ...
1 ham Ok lar... Joking wif u oni...
2 spam Free entry in 2 a wkly comp to win FA Cup fina...
3 ham U dun say so early hor... U c already then say...
4 ham Nah I don't think he goes to usf, he lives aro...
df.shape
(5572, 2)
df.index = range(5572)
df['Message'].apply(lambda x: len(x.split(' '))).sum()
87265
import matplotlib
matplotlib.rc_file_defaults()
cnt_pro = df['Category'].value_counts()
sns.barplot(x=cnt_pro.index, y=cnt_pro.values)
plt.ylabel('Number of Occurrences', fontsize=12)
plt.xlabel('Category', fontsize=12)
plt.xticks(rotation=90)
plt.show();
_images/2bdbac57e597036293ac9088d17c738b25c90c68dad1965d59ca195ccb909ef3.png
def print_message(index):
    example = df[df.index == index][['Message', 'Category']].values[0]
    if len(example) > 0:
        print(example[0])
        print('Message:', example[1])
print_message(12)
URGENT! You have won a 1 week FREE membership in our £100,000 Prize Jackpot! Txt the word: CLAIM to No: 81010 T&C www.dbuk.net LCCLTD POBOX 4403LDNW1A7RW18
Message: spam
print_message(0)
Go until jurong point, crazy.. Available only in bugis n great world la e buffet... Cine there got amore wat...
Message: ham
  • Preprocesamiento de Texto: A continuación, establecemos una función para transformar el texto a minúsculas y eliminar signos de puntuación/símbolos de las palabras.

import lxml
from bs4 import BeautifulSoup
import re
  • BeautifulSoup: Se usa para navegar y manipular la estructura de documentos HTML o XML.

  • re: Permite buscar y manipular texto dentro de esos documentos o cualquier otro texto, usando patrones específicos de expresiones regulares.

def cleanText(text):
    text = BeautifulSoup(text, "html.parser").text
    text = re.sub(r'\|\|\|', r' ', text) 
    text = re.sub(r'http\S+', r'<URL>', text)
    text = text.lower()
    text = text.replace('x', '')
    return text
  • BeautifulSoup(text, "html.parser").text: BeautifulSoup en este caso elimina etiquetas HTML del texto. Convierte text en un objeto BeautifulSoup, y luego el método .text extrae solo el texto visible, eliminando cualquier HTML o XML.

  • text = re.sub(r'\|\|\|', r' ', text): Aquí re.sub reemplaza cualquier secuencia de caracteres "|||" por un espacio " ". Esto es útil para limpiar delimitadores o caracteres específicos del texto.

  • text = re.sub(r'http\S+', r'<URL>', text): Reemplaza cualquier URL (que comience con “http” y continúe sin espacios) con el marcador de posición “. Esto ayuda a evitar que URLs específicas queden en el texto sin ser normalizadas.

  • text = text.replace('x', ''): En tareas de clasificación de texto o en análisis de sentimientos, algunos caracteres pueden no ser útiles para el modelo. Por ejemplo, si “x” se usa como símbolo decorativo o separador, eliminarlo mejora la calidad de las representaciones de texto sin afectar el contenido.

df['Message'] = df['Message'].apply(cleanText)
df['Message'] = df['Message'].apply(cleanText)
train, test = train_test_split(df, test_size=0.20, random_state=42)
import nltk
from nltk.corpus import stopwords
nltk.data.path.append('/home/lihkir/nltk_data')
nltk.download('punkt_tab')
[nltk_data] Downloading package punkt_tab to /home/lihkir/nltk_data...
[nltk_data]   Package punkt_tab is already up-to-date!
True
  • El módulo nltk y, en particular, nltk.corpus.stopwords se usan en Procesamiento de Lenguaje Natural (NLP) para trabajar con “stopwords”, es decir, palabras de relleno o comunes en un idioma que suelen eliminarse en el análisis de texto porque no aportan significado específico. Estas palabras pueden ser artículos, preposiciones, pronombres y otros términos que no añaden valor semántico relevante (por ejemplo, “el”, “la”, “un”, “de”, “y” en español; o “the”, “is”, “in”, “and” en inglés).

def tokenize_text(text):
    tokens = []
    for sent in nltk.sent_tokenize(text):
        for word in nltk.word_tokenize(sent):
            if len(word) <= 0:
                continue
            tokens.append(word.lower())
    return tokens
  • La función tokenize_text toma un texto como entrada y lo divide en una lista de palabras o “tokens” en minúsculas. Esta función usa nltk.sent_tokenize y nltk.word_tokenize para descomponer el texto en oraciones y palabras.

test_message = "Are you unique enough? Find out from 30th August. www.areyouunique.co.uk"
print(tokenize_text(test_message))
['are', 'you', 'unique', 'enough', '?', 'find', 'out', 'from', '30th', 'august', '.', 'www.areyouunique.co.uk']
train_tagged = train.apply(lambda r: TaggedDocument(words=tokenize_text(r['Message']), tags=[r.Category]), axis=1)
test_tagged  = test.apply(lambda r: TaggedDocument(words=tokenize_text(r['Message']), tags=[r.Category]), axis=1)
  • La función de arriba, aplica una transformación a cada fila del DataFrame train, convirtiendo el contenido de cada mensaje en un objeto TaggedDocument, un formato utilizado en NLP para entrenar modelos de aprendizaje de texto como Doc2Vec de la librería Gensim.

  • max_features depende de cuántas palabras únicas hay en todo el corpus, no de la longitud de cada mensaje. Puedes ver cuántas palabras únicas tienes así:

from collections import Counter
import itertools

all_words = list(itertools.chain.from_iterable(df['Message'].apply(lambda x: x.split())))
word_counts = Counter(all_words)
print(f"Total de palabras únicas: {len(word_counts)}")
  • Y luego decidir cuántas palabras mantener. Por ejemplo, si tienes 20.000 palabras únicas, puedes limitar a las 5.000 más frecuentes (max_features = 5000), lo cual es típico. El resto son palabras raras, errores tipográficos, tecnicismos o formas muy específicas que aportan poco al modelo y pueden introducir ruido¨. De esta forma mejoramos la eficiencia, rendimiento, y además, evitamos overfitting.

max_features = 500000
  • Puede usar la siguiente función, para definir MAX_SEQUENCE_LENGTH:

df['Message'].apply(lambda x: len(x.split())).describe()
  • Por ejemplo, si el resultado del .describe() es algo como:

count    10000.000000
mean        12.340000
std          8.567890
min          1.000000
25%          6.000000
50%         10.000000
75%         15.000000
max         78.000000
  • Entonces podrías definir MAX_SEQUENCE_LENGTH = 20 o MAX_SEQUENCE_LENGTH = 25, ya que se cubre más del 75% de tus datos (percentil 75 = 15), y evitas que muchos mensajes se recorten. Otra opción es escoger MAX_SEQUENCE_LENGTH = 50 se cubre más del 99% de los mensajes sin necesidad de truncamiento.

MAX_SEQUENCE_LENGTH = 50
tokenizer = Tokenizer(num_words=max_features, split=' ', filters='!"#$%&()*+,-./:;<=>?@[$$^_`{|}~', lower=True)
tokenizer.fit_on_texts(df['Message'].values)
X = tokenizer.texts_to_sequences(df['Message'].values)
X = pad_sequences(X, maxlen=MAX_SEQUENCE_LENGTH)
print('Found %s unique tokens.' % len(X))
print('Shape of data tensor:', X.shape)
Found 5572 unique tokens.
  • Tokenizer: Es un objeto de Keras que convierte texto en secuencias de números enteros, donde cada número representa un token (una palabra o símbolo) en el vocabulario.

  • num_words=max_features: Establece el número máximo de palabras que se considerarán. Solo se tomarán en cuenta las palabras más frecuentes hasta max_features.

  • filters='!"#$%&()*+,-./:;<=>?@[$$^_{|}~': Especifica los caracteres que se eliminarán del texto, como puntuación y símbolos.

  • pad_sequences: Este método se utiliza para hacer que todas las secuencias en X tengan la misma longitud. Si una secuencia es más corta que MAX_SEQUENCE_LENGTH, se rellenará con ceros (o con un valor especificado); si es más larga, se truncará.

train_tagged.values
array([TaggedDocument(words=['reply', 'to', 'win', 'â£100', 'weekly', '!', 'where', 'will', 'the', '2006', 'fifa', 'world', 'cup', 'be', 'held', '?', 'send', 'stop', 'to', '87239', 'to', 'end', 'service'], tags=['spam']),
       TaggedDocument(words=['hello', '.', 'sort', 'of', 'out', 'in', 'town', 'already', '.', 'that', '.', 'so', 'dont', 'rush', 'home', ',', 'i', 'am', 'eating', 'nachos', '.', 'will', 'let', 'you', 'know', 'eta', '.'], tags=['ham']),
       TaggedDocument(words=['how', 'come', 'guoyang', 'go', 'n', 'tell', 'her', '?', 'then', 'u', 'told', 'her', '?'], tags=['ham']),
       ...,
       TaggedDocument(words=['prabha', '..', 'i', "'m", 'soryda', '..', 'realy', '..', 'frm', 'heart', 'i', "'m", 'sory'], tags=['ham']),
       TaggedDocument(words=['nt', 'joking', 'seriously', 'i', 'told'], tags=['ham']),
       TaggedDocument(words=['did', 'he', 'just', 'say', 'somebody', 'is', 'named', 'tampa'], tags=['ham'])],
      dtype=object)
d2v_model = Doc2Vec(dm=1, dm_mean=1, vector_size=20, window=8, min_count=1, workers=12, alpha=0.065, min_alpha=0.065)
  • El modelo Doc2Vec es útil para convertir documentos en representaciones numéricas (vectores) de tamaño fijo. Esto es esencial porque las LSTM requieren representaciones numéricas para trabajar.

  • dm=1: Especifica que se usará el método Distributed Memory (DM) (en lugar de dm=0, que indica Distributed Bag of Words). Usar DM tiene textos más largos y complejos donde el orden de las palabras es importante y es crucial capturar el contexto para tareas de NLP que requieren una comprensión profunda del documento, como análisis de sentimientos o clasificación temática.

  • dm_mean=1: Utiliza la media de los vectores de contextos de palabras para representar los documentos. Esto suele mejorar la precisión en comparación con otros métodos.

  • vector_size=20: Define el tamaño del vector de salida, lo que significa que cada documento se representará como un vector de 20 dimensiones. Para textos simples o tareas básicas, un tamaño pequeño como 20 puede ser suficiente. Para análisis de texto más detallados, el tamaño del vector puede incrementarse para capturar mejor las relaciones entre palabras y documentos, generalmente entre 100 y 300 dimensiones.

  • window=8: Define el tamaño de la ventana de contexto, es decir, el número máximo de palabras a considerar alrededor de la palabra objetivo en cada documento. En este caso, se consideran hasta 8 palabras a cada lado.

  • min_count=1: Establece el mínimo de veces que una palabra debe aparecer en el corpus para ser incluida en el modelo. Un valor de 1 significa que todas las palabras en el corpus se incluirán.

  • workers=12: Utiliza un solo núcleo de procesamiento para el entrenamiento. Si se incrementa este valor, se pueden usar varios núcleos para acelerar el proceso.

  • alpha=0.065: Establece la tasa de aprendizaje inicial. Doc2Vec reduce gradualmente esta tasa durante el entrenamiento.

  • min_alpha=0.065: Fija el límite inferior de la tasa de aprendizaje para que no disminuya más allá de este valor durante el entrenamiento.

d2v_model.build_vocab([x for x in tqdm(train_tagged.values)])
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 7435963.77it/s]
  • d2v_model.build_vocab([x for x in tqdm(train_tagged.values)]): Crea el vocabulario para el modelo Doc2Vec en función de los documentos de entrada en train_tagged

%%time
for epoch in range(30):
    d2v_model.train(utils.shuffle([x for x in tqdm(train_tagged.values)]), total_examples=len(train_tagged.values), epochs=1)
    d2v_model.alpha -= 0.002
    d2v_model.min_alpha = d2v_model.alpha
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 7251362.66it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8978872.68it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8817930.63it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9375131.86it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8991829.21it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8793044.65it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9204339.21it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9231611.32it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9365737.94it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9127936.00it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9177227.75it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9026563.46it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9455747.56it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9403427.03it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9328349.76it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9119030.70it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9474917.86it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9105705.27it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8780654.26it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 7931274.05it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8202726.16it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9035288.99it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8451181.25it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9268226.54it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9119030.70it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9231611.32it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8872336.46it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 8290027.91it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9087998.51it/s]
100%|███████████████████████████████████████████████████████████████████████████████████████████████████| 4457/4457 [00:00<00:00, 9356362.83it/s]
CPU times: user 2.26 s, sys: 0 ns, total: 2.26 s
Wall time: 2.35 s
print(d2v_model)
Doc2Vec<dm/m,d20,n5,w8,s0.001>
  • dm/m: Indica el tipo de modelo que se está utilizando. dm significa que se está utilizando el enfoque de Distributed Memory (DM). El “m” se refiere a que se está usando la estrategia de “mean” (media) para la representación de los contextos de palabras.

  • d20: Este parámetro indica el tamaño del vector de salida, que es de 20 dimensiones. Es decir, cada documento se representará como un vector de 20 dimensiones.

  • n5: Este número se refiere al número de palabras mínimas requeridas para que una palabra sea incluida en el vocabulario. En este caso, las palabras que aparecen al menos 5 veces serán consideradas.

  • w8: Este valor representa el tamaño de la ventana de contexto, que es de 8 palabras. Esto significa que al entrenar el modelo, se considera un contexto de 8 palabras antes y después de la palabra objetivo.

  • s0.001: Este número indica la tasa de aprendizaje inicial (alpha), que es de 0.001 en este caso. Esta tasa determina cuánto se ajustan los pesos del modelo durante el entrenamiento.

num_words = len(d2v_model.wv.key_to_index)
print(num_words)
8266
words = d2v_model.wv.index_to_key
print(words[-10:])
['phone750', 'weaseling', 'connected', 'ltd.', 'gmw', 'wrongly', 'bless.get', 'gained', 'money.i', '.so']
embedding_matrix = np.zeros((len(d2v_model.wv.key_to_index)+ 1, 20))
  • embedding_matrix = np.zeros((len(d2v_model.wv.key_to_index)+ 1, 20)): Inicializa una matriz de embeddings (representaciones vectoriales) de tamaño (número de palabras en el vocabulario + 1, 20) llena de ceros. Esta matriz se puede utilizar posteriormente para almacenar las representaciones vectoriales de cada palabra del vocabulario aprendidas por el modelo Doc2Vec

for i in range(len(d2v_model.dv)):
    vec = d2v_model.dv[i]
    if vec is not None and len(vec) <= 1000:
        print(i)
        print(d2v_model.dv)
        embedding_matrix[i] = vec
        print(vec)
0
KeyedVectors<vector_size=20, 2 keys>
[ -3.2668898   -3.1738384   -8.505668     4.5485306    2.692948
   3.1493063   -4.462352    -5.3937817   -7.1809254    4.7018075
   2.7759285    6.317104    -4.7875314   -7.075199    -3.3227246
   1.9363822    4.517679     6.162188   -14.190054    -0.26278985]
-3.2668898
1
KeyedVectors<vector_size=20, 2 keys>
[ 1.2465329   1.5963961   0.8784475   2.2543192   2.5438132  -2.474216
 -2.310085   -0.6934201   2.6299498  -3.745987    1.3307427   0.41213423
 -3.2673807  -1.8536966   0.9749198   1.6373183  -1.6213127  -2.8041255
 -0.5990004   0.34091252]
1.5963961
  • Este bucle itera a través de los vectores de documento en el modelo Doc2Vec, verifica que cada vector sea válido y su longitud adecuada, imprime información relevante, y almacena los vectores en una matriz de embeddings.

d2v_model.wv.most_similar(positive=['urgent'], topn=10)
[('resend', 0.7344722747802734),
 ('lower', 0.7169529795646667),
 ('probs', 0.7099040150642395),
 ('08718738034', 0.7082372307777405),
 ('clothes', 0.6989055275917053),
 ('tuition', 0.6945768594741821),
 ('curtsey', 0.6848364472389221),
 ('11mths+', 0.6828041672706604),
 ('having', 0.6789382696151733),
 ('deleted', 0.6787389516830444)]
  • d2v_model.wv.most_similar(positive=['urgent'], topn=10): Se utiliza para encontrar las palabras más similares al término “urgent” en el modelo Doc2Vec

d2v_model.wv.most_similar(positive=['cherish'], topn=10)
[('image', 0.8254433870315552),
 ('intrepid', 0.7505639791488647),
 ('okors', 0.7467709183692932),
 ('count', 0.7441601753234863),
 ('mojibiola', 0.7431001663208008),
 ('ultimatum', 0.7118896245956421),
 ('bâ\x80\x98ham', 0.7115652561187744),
 ('cantdo', 0.7105881571769714),
 ('abiola', 0.7102086544036865),
 ('thank', 0.6918621063232422)]
  • Pasamos a implementar el modelo LSTM

from keras.models import Sequential
from keras.layers import LSTM, Dense, Embedding
from tensorflow.keras.optimizers.legacy import Adam
  • Crear un modelo secuencial, es decir, un modelo LSTM donde las capas se apilan una detrás de otra, en orden

model = Sequential()
  • Vectores de palabras Embedding

model.add(Embedding(len(d2v_model.wv.key_to_index) + 1, 20, input_length=X.shape[1], weights=[embedding_matrix], trainable=True))
  • weights=[embedding_matrix]: Al pasar esto como un argumento de weights, se le indica a Keras que use estos vectores iniciales para la capa de incrustación.

  • LSTM(50) agrega una capa LSTM con 50 unidades (neuronas). return_sequences=False: Esto indica que la capa devuelve solo la última salida de la secuencia, no toda la secuencia de salidas.

model.add(LSTM(50,return_sequences=False))
model.add(Dense(2,activation="softmax"))
  • Usamos summary() para mostrar un resumen de la arquitectura del modelo.

model.summary()
Model: "sequential_4"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ embedding_4 (Embedding)         │ ?                      │       165,340 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_4 (LSTM)                   │ ?                      │   0 (unbuilt) │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense_4 (Dense)                 │ ?                      │   0 (unbuilt) │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 165,340 (645.86 KB)
 Trainable params: 165,340 (645.86 KB)
 Non-trainable params: 0 (0.00 B)
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Embedding, LSTM, Dense
from tensorflow.keras.optimizers import Adam
  • Queda como ejercicio para el estudiante, cambiar la métrica de validación a AUC, tal como se hizo en el ejercicio de reconocimiento facial de emociones usando CNNs

model.compile(optimizer=Adam(), loss="binary_crossentropy", metrics=['accuracy'])
Y = pd.get_dummies(df['Category']).values
X_train, X_test, Y_train, Y_test = train_test_split(X,Y, test_size = 0.15, random_state = 42)
print(X_train.shape,Y_train.shape)
print(X_test.shape,Y_test.shape)
(4736, 50) (4736, 2)
(836, 50) (836, 2)
batch_size = 32
import os
from joblib import dump, load

history_lstm = None
if os.path.exists('history_lstm.joblib'):
    history_lstm = load('history_lstm.joblib')
    print("El archivo 'history_lstm.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_lstm = model.fit(X_train, Y_train, epochs=50, batch_size=batch_size, verbose=2)
    dump(history_lstm.history, 'history_lstm.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_lstm.joblib'.")
Epoch 1/50
148/148 - 2s - 11ms/step - accuracy: 0.9246 - loss: 0.2294
Epoch 2/50
148/148 - 1s - 7ms/step - accuracy: 0.9844 - loss: 0.0612
Epoch 3/50
148/148 - 1s - 7ms/step - accuracy: 0.9939 - loss: 0.0287
Epoch 4/50
148/148 - 1s - 6ms/step - accuracy: 0.9956 - loss: 0.0200
Epoch 5/50
148/148 - 1s - 7ms/step - accuracy: 0.9973 - loss: 0.0117
Epoch 6/50
148/148 - 1s - 7ms/step - accuracy: 0.9975 - loss: 0.0083
Epoch 7/50
148/148 - 1s - 7ms/step - accuracy: 0.9987 - loss: 0.0045
Epoch 8/50
148/148 - 1s - 7ms/step - accuracy: 0.9992 - loss: 0.0019
Epoch 9/50
148/148 - 1s - 7ms/step - accuracy: 0.9996 - loss: 0.0013
Epoch 10/50
148/148 - 1s - 7ms/step - accuracy: 0.9996 - loss: 8.9813e-04
Epoch 11/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 6.4990e-04
Epoch 12/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 5.0027e-04
Epoch 13/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 3.9275e-04
Epoch 14/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 3.0832e-04
Epoch 15/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.5960e-04
Epoch 16/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.1151e-04
Epoch 17/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.7948e-04
Epoch 18/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.5452e-04
Epoch 19/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 1.3186e-04
Epoch 20/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.1505e-04
Epoch 21/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.0080e-04
Epoch 22/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 8.8754e-05
Epoch 23/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 7.9170e-05
Epoch 24/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 6.9984e-05
Epoch 25/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 6.2725e-05
Epoch 26/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 5.6174e-05
Epoch 27/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 5.2045e-05
Epoch 28/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 4.5325e-05
Epoch 29/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 4.1309e-05
Epoch 30/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 3.7269e-05
Epoch 31/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 3.4046e-05
Epoch 32/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 3.0869e-05
Epoch 33/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.8360e-05
Epoch 34/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.5935e-05
Epoch 35/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.3781e-05
Epoch 36/50
148/148 - 1s - 8ms/step - accuracy: 1.0000 - loss: 2.1791e-05
Epoch 37/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 2.0589e-05
Epoch 38/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.8341e-05
Epoch 39/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.7263e-05
Epoch 40/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.5659e-05
Epoch 41/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.4416e-05
Epoch 42/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.3237e-05
Epoch 43/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.2342e-05
Epoch 44/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.1210e-05
Epoch 45/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 1.0379e-05
Epoch 46/50
148/148 - 1s - 6ms/step - accuracy: 1.0000 - loss: 9.6437e-06
Epoch 47/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 8.8003e-06
Epoch 48/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 8.0948e-06
Epoch 49/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 7.7207e-06
Epoch 50/50
148/148 - 1s - 7ms/step - accuracy: 1.0000 - loss: 6.9844e-06
El entrenamiento se ha completado y el historial ha sido guardado en 'history_lstm.joblib'.
plt.plot(history_lstm.history['accuracy'])
plt.title('model accuracy')
plt.ylabel('acc')
plt.xlabel('epochs')
plt.legend(['train', 'test'], loc='upper left');
_images/eacf227bd30eaaf3487edeaa9cd4c33c1db66124d8d081c7082cdcdc286bb593.png
plt.plot(history_lstm.history['loss']);
plt.title('model loss');
plt.ylabel('loss');
plt.xlabel('epochs');
plt.legend(['train', 'test'], loc='upper left');
_images/1d415f105e47f63913c97f0049b135f34ed29c513d6461ec8305ee172066efb9.png
_, train_acc = model.evaluate(X_train, Y_train, verbose=2)
_, test_acc = model.evaluate(X_test, Y_test, verbose=2)
print('Train: %.3f, Test: %.4f' % (train_acc, test_acc))
148/148 - 0s - 3ms/step - accuracy: 1.0000 - loss: 6.5603e-06
27/27 - 0s - 6ms/step - accuracy: 0.9856 - loss: 0.1570
Train: 1.000, Test: 0.9856
  • Las siguientes líneas están utilizando el modelo entrenado para predecir la probabilidad de que cada entrada en X_test pertenezca a cada clase. Luego, se determina la clase más probable para cada entrada y se imprime tanto las probabilidades como las clases predichas. np.argmax(yhat_probs, axis=1) toma la clase con mayor probabilidad en cada fila.

yhat_probs = model.predict(X_test, verbose=0)
print(yhat_probs)
[[1.0000000e+00 1.0334940e-12]
 [1.0000000e+00 1.2381419e-11]
 [1.0000000e+00 1.5744090e-12]
 ...
 [1.0000000e+00 1.0007380e-12]
 [1.0000000e+00 7.4370523e-13]
 [1.0000000e+00 1.3020209e-12]]
yhat_classes = np.argmax(yhat_probs, axis=1)
print(yhat_classes)
[0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 1 0 1 1
 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 1 0 0 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 1
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 0 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0
 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 1 1 0 0 0
 0 1 0 1 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0
 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 1 0 0 0 1 0 0 0
 1 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0
 1 0 0 0 0 0 0 0 1 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 1 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 1 1 1 0 0 0 0 0 1 1 0 0
 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 1 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0
 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 1 0 0 0
 0 0 0 1 0 1 0 1 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0
 0 1 1 1 0 1 0 0 0 0 0 0 0 0 0 0 1 1 0 0 0 0]
  • Cada número en este arreglo representa la clase predicha para cada muestra en el conjunto de prueba. Por ejemplo, si el valor es 0, significa que la muestra pertenece a la clase 0, y si es 1, significa que la muestra pertenece a la clase 1.

yhat_probs = yhat_probs[:, 0]
import numpy as np
rounded_labels=np.argmax(Y_test, axis=1)
rounded_labels
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
       0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0,
       0, 0, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 1, 0,
       0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0,
       0, 1, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1,
       0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
       1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1,
       0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
       0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, 0,
       1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0,
       0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0,
       0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 1,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0,
       0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 0, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0,
       0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
       0, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0])
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(rounded_labels, yhat_classes)
cm
array([[727,   2],
       [ 10,  97]])
from sklearn.metrics import confusion_matrix
import seaborn as sns
lstm_val = confusion_matrix(rounded_labels, yhat_classes)
f, ax = plt.subplots(figsize=(5,5))
sns.heatmap(lstm_val, annot=True, linewidth=0.7, linecolor='cyan', fmt='g', ax=ax, cmap="BuPu")
plt.title('LSTM Classification Confusion Matrix')
plt.xlabel('Y predict')
plt.ylabel('Y test')
plt.show()
_images/acff9545c97ef9ef04aac5cde64f1eea5847c6ae88724e059659b863c114fa11.png
validation_size = 200

X_validate = X_test[-validation_size:]
Y_validate = Y_test[-validation_size:]
X_test = X_test[:-validation_size]
Y_test = Y_test[:-validation_size]
score,acc = model.evaluate(X_test, Y_test, verbose = 1, batch_size = batch_size)

print("score: %.2f" % (score))
print("acc: %.2f" % (acc))
20/20 ━━━━━━━━━━━━━━━━━━━━ 0s 3ms/step - accuracy: 0.9849 - loss: 0.1506   
score: 0.17
acc: 0.98
model.save('Mymodel.keras')
  • Pruebas con conjuntos de datos nuevos y diferentes de los datos para construir el modelo.

message = ['Congratulations! you have won a $1,000 Walmart gift card. Go to http://bit.ly/123456 to claim now.']
seq = tokenizer.texts_to_sequences(message)

padded = pad_sequences(seq, maxlen=X.shape[1], dtype='int32', value=0)

pred = model.predict(padded)

labels = ['ham','spam']
print(pred, labels[np.argmax(pred)])
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 11ms/step
[[5.0022042e-11 1.0000000e+00]] spam
message = ['thanks for accepting my request to connect']
seq = tokenizer.texts_to_sequences(message)

padded = pad_sequences(seq, maxlen=X.shape[1], dtype='int32', value=0)

pred = model.predict(padded)

labels = ['ham','spam']
print(pred, labels[np.argmax(pred)])
1/1 ━━━━━━━━━━━━━━━━━━━━ 0s 10ms/step
[[1.0000000e+00 1.5881167e-12]] ham

10.18. Aplicación: LSTM para la predicción de series de tiempo#

  • Continuaremos usando el conjunto de datos sobre contaminación del aire (pm2.5) usando LSTM. La lectura y el preprocesamiento de los datos se mantienen igual que en los ejemplos de MLPs. El conjunto de datos original se divide en dos conjuntos: entrenamiento y validación, que se usan para el entrenamiento y validación del modelo respectivamente.

  • La función makeXy se utiliza para generar arreglos de regresores y objetivos: X_train, X_val, y_train y y_val. X_train y X_val, generados por la función makeXy, son arreglos 2D de forma (número de muestras, número de pasos de tiempo). Sin embargo, la entrada a las capas RNN debe ser de forma (número de muestras, número de pasos de tiempo, número de características por paso de tiempo).

  • En este caso, estamos tratando solo con pm2.5, por lo tanto, el número de características por paso de tiempo es uno. El número de pasos de tiempo es siete y el número de muestras es el mismo que el número de muestras en X_train y X_val, que se transforman a arreglos 3D

import pandas as pd
from sklearn.preprocessing import MinMaxScaler
import datetime
import matplotlib.pyplot as plt
import seaborn as sns
import numpy as np

import warnings
warnings.filterwarnings('ignore')
plot_fontsize = 18;
df = pd.read_csv('https://raw.githubusercontent.com/lihkir/Data/refs/heads/main/PRSA_data_2010.1.1-2014.12.31.csv', usecols=lambda column: column not in ['DEWP', 'TEMP', 'cbwd', 'Iws',	'Is', 'Ir'])
print('Shape of the dataframe:', df.shape)
Shape of the dataframe: (43824, 7)
df.head()
No year month day hour pm2.5 PRES
0 1 2010 1 1 0 NaN 1021.0
1 2 2010 1 1 1 NaN 1020.0
2 3 2010 1 1 2 NaN 1019.0
3 4 2010 1 1 3 NaN 1019.0
4 5 2010 1 1 4 NaN 1018.0
df.dropna(subset=['pm2.5'], axis=0, inplace=True)
df.reset_index(drop=True, inplace=True)
df['datetime'] = df[['year', 'month', 'day', 'hour']].apply(lambda row: datetime.datetime(year=row['year'], month=row['month'], day=row['day'],
                                                                                          hour=row['hour']), axis=1)
df.sort_values('datetime', ascending=True, inplace=True)
plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1) 
g1 = sns.boxplot(df['pm2.5'], orient="h", palette="Set2")
g1.set_title('Box plot of pm2.5', fontsize=plot_fontsize);
g1.set_xlabel(xlabel='pm2.5', fontsize=plot_fontsize)
g1.set(yticklabels=[])
g1.tick_params(left=False);

plt.subplot(1, 2, 2)
g2 = sns.lineplot(df['pm2.5'])
g2.set_title('Time series of pm2.5', fontsize=plot_fontsize)
g2.set_xlabel('Index', fontsize=plot_fontsize)
g2.set_ylabel('pm2.5 readings');

plt.show()
_images/d492abe47224e8c07be16807d3c2595090f2e3c65ddc7fa41b8e258a7eb813ef.png
plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1)
g1 = sns.lineplot(df['pm2.5'].loc[df['datetime']<=datetime.datetime(year=2010,month=6,day=30)], color='g')
g1.set_title('pm2.5 during 2010', fontsize=plot_fontsize)
g1.set_xlabel('Index', fontsize=plot_fontsize)
g1.set_ylabel('pm2.5 readings', fontsize=plot_fontsize);

plt.subplot(1, 2, 2)
g = sns.lineplot(df['pm2.5'].loc[df['datetime']<=datetime.datetime(year=2010,month=1,day=31)], color='g')
g.set_title('Zoom in on one month: pm2.5 during Jan 2010', fontsize=plot_fontsize)
g.set_xlabel('Index', fontsize=plot_fontsize)
g.set_ylabel('pm2.5 readings', fontsize=plot_fontsize);

plt.show()
_images/35938c40e57736463d7d1458d7b8da872f1822c07f18061b1da90c9b38463f58.png
split_date = datetime.datetime(year=2014, month=1, day=1, hour=0)
df_train = df.loc[df['datetime']<split_date]
df_val = df.loc[df['datetime']>=split_date]
print('Shape of train:', df_train.shape)
print('Shape of test:', df_val.shape)
Shape of train: (33096, 8)
Shape of test: (8661, 8)
df_train.head()
No year month day hour pm2.5 PRES datetime
0 25 2010 1 2 0 129.0 1020.0 2010-01-02 00:00:00
1 26 2010 1 2 1 148.0 1020.0 2010-01-02 01:00:00
2 27 2010 1 2 2 159.0 1021.0 2010-01-02 02:00:00
3 28 2010 1 2 3 181.0 1022.0 2010-01-02 03:00:00
4 29 2010 1 2 4 138.0 1022.0 2010-01-02 04:00:00
df_val.head()
No year month day hour pm2.5 PRES datetime
33096 35065 2014 1 1 0 24.0 1014.0 2014-01-01 00:00:00
33097 35066 2014 1 1 1 53.0 1013.0 2014-01-01 01:00:00
33098 35067 2014 1 1 2 65.0 1013.0 2014-01-01 02:00:00
33099 35068 2014 1 1 3 70.0 1013.0 2014-01-01 03:00:00
33100 35069 2014 1 1 4 79.0 1012.0 2014-01-01 04:00:00
df_val.reset_index(drop=True, inplace=True)
scaler_pm25 = MinMaxScaler(feature_range=(0, 1))
df_train['scaled_pm2.5'] = scaler_pm25.fit_transform(df_train[['pm2.5']])
df_val['scaled_pm2.5'] = scaler_pm25.transform(df_val[['pm2.5']])

print('Shape of train:', df_train.shape)
print('Shape of val:', df_val.shape)
Shape of train: (33096, 9)
Shape of val: (8661, 9)
plt.figure(figsize=(15, 8))

plt.subplot(1, 2, 1)
g1 = sns.lineplot(df_train['scaled_pm2.5'], color='b')
g1.set_title('Time series of scaled pm2.5 in train set', fontsize=plot_fontsize)
g1.set_xlabel('Index', fontsize=plot_fontsize)
g1.set_ylabel('Scaled pm2.5 readings', fontsize=plot_fontsize);

plt.subplot(1, 2, 2)
g2 = sns.lineplot(df_val['scaled_pm2.5'], color='r')
g2.set_title('Time series of scaled pm2.5 in validation set', fontsize=plot_fontsize)
g2.set_xlabel('Index', fontsize=plot_fontsize)
g2.set_ylabel('Scaled pm2.5 readings', fontsize=plot_fontsize);

plt.show()
_images/73112cb48201fbe34418cfd68bbb6269057957ea5aa05119dbd76a421377b504.png
def makeXy(ts, nb_timesteps):
    """
    Input: 
           ts: original time series
           nb_timesteps: number of time steps in the regressors
    Output: 
           X: 2-D array of regressors
           y: 1-D array of target 
    """
    X = []
    y = []
    for i in range(nb_timesteps, ts.shape[0]):
        if i-nb_timesteps <= 4:
            print(i-nb_timesteps, i-1, i)
        X.append(list(ts.loc[i-nb_timesteps:i-1])) #Regressors
        y.append(ts.loc[i]) #Target
    X, y = np.array(X), np.array(y)
    return X, y
X_train, y_train = makeXy(df_train['scaled_pm2.5'], 7)
0 6 7
1 7 8
2 8 9
3 9 10
4 10 11
print('Shape of train arrays:', X_train.shape, y_train.shape)
Shape of train arrays: (33089, 7) (33089,)
X_val, y_val = makeXy(df_val['scaled_pm2.5'], 7)
0 6 7
1 7 8
2 8 9
3 9 10
4 10 11
print('Shape of validation arrays:', X_val.shape, y_val.shape)
Shape of validation arrays: (8654, 7) (8654,)
X_train, X_val = X_train.reshape((X_train.shape[0], X_train.shape[1], 1)), X_val.reshape((X_val.shape[0], X_val.shape[1], 1))
print('Shape of 3D arrays:', X_train.shape, X_val.shape)
Shape of 3D arrays: (33089, 7, 1) (8654, 7, 1)
  • X_train y X_val se han convertido en matrices 3D y sus nuevas formas se ven en la salida de la instrucción de impresión anterior

from keras.layers import Dense, Input, Dropout
from keras.layers import LSTM
from keras.optimizers import SGD
from keras.models import Model
from keras.models import load_model
from keras.callbacks import ModelCheckpoint
2025-04-05 20:36:42.217116: I tensorflow/core/util/port.cc:153] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.
2025-04-05 20:36:42.224213: E external/local_xla/xla/stream_executor/cuda/cuda_fft.cc:485] Unable to register cuFFT factory: Attempting to register factory for plugin cuFFT when one has already been registered
2025-04-05 20:36:42.230760: E external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:8454] Unable to register cuDNN factory: Attempting to register factory for plugin cuDNN when one has already been registered
2025-04-05 20:36:42.232587: E external/local_xla/xla/stream_executor/cuda/cuda_blas.cc:1452] Unable to register cuBLAS factory: Attempting to register factory for plugin cuBLAS when one has already been registered
2025-04-05 20:36:42.241650: I tensorflow/core/platform/cpu_feature_guard.cc:210] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F AVX512_VNNI AVX512_BF16 FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.
2025-04-05 20:36:42.712103: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Could not find TensorRT
  • La red neuronal para desarrollar el modelo de predicción de series temporales tiene una capa de entrada, que alimenta a la capa LSTM. La capa LSTM tiene siete pasos temporales, que es el mismo número de observaciones históricas tomadas para hacer la predicción de la presión atmosférica para el día siguiente. Solo el último paso temporal de la LSTM devuelve una salida.

  • Hay sesenta y cuatro neuronas ocultas en cada paso temporal de la capa LSTM. Por lo tanto, la salida de la LSTM tiene sesenta y cuatro características:

input_layer = Input(shape=(7,1), dtype='float32')
  • Aquí shape=(7,1) forma de la entrada a la LSTM. 7: Número de pasos de tiempo (timesteps). 1: Número de características (features) por cada paso de tiempo. return_sequences=True se usa cuando se necesita pasar la secuencia completa de salidas a la siguiente capa de la red, que también puede ser una capa recurrente como otra LSTM

lstm_layer1 = LSTM(64, input_shape=(7,1), return_sequences=True)(input_layer)
lstm_layer2 = LSTM(32, input_shape=(7,64), return_sequences=False)(lstm_layer1)
WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1743903403.138819   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.168822   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.168868   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.171752   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.171786   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.171805   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.296983   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
I0000 00:00:1743903403.297036   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-04-05 20:36:43.297044: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2112] Could not identify NUMA node of platform GPU id 0, defaulting to 0.  Your kernel may not have been built with NUMA support.
I0000 00:00:1743903403.297078   30821 cuda_executor.cc:1001] could not open file to read NUMA node: /sys/bus/pci/devices/0000:01:00.0/numa_node
Your kernel may have been built without NUMA support.
2025-04-05 20:36:43.297094: I tensorflow/core/common_runtime/gpu/gpu_device.cc:2021] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 9558 MB memory:  -> device: 0, name: NVIDIA GeForce RTX 4070, pci bus id: 0000:01:00.0, compute capability: 8.9
  • A continuación, la salida de LSTM pasa a una capa de exclusión que elimina aleatoriamente el 20% de la entrada antes de pasar a la capa de salida, que tiene una única neurona oculta con una función de activación lineal

dropout_layer = Dropout(0.2)(lstm_layer2)
output_layer = Dense(1, activation='linear')(dropout_layer)
  • Finalmente, todas las capas se envuelven en un keras.models.Model y se entrenan durante veinte epochs para minimizar el MSE utilizando el optimizador Adam

ts_model = Model(inputs=input_layer, outputs=output_layer)
ts_model.compile(loss='mean_absolute_error', optimizer='adam') #SGD(lr=0.001, decay=1e-5))
ts_model.summary()
Model: "functional"
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Layer (type)                     Output Shape                  Param # ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━┩
│ input_layer (InputLayer)        │ (None, 7, 1)           │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm (LSTM)                     │ (None, 7, 64)          │        16,896 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ lstm_1 (LSTM)                   │ (None, 32)             │        12,416 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dropout (Dropout)               │ (None, 32)             │             0 │
├─────────────────────────────────┼────────────────────────┼───────────────┤
│ dense (Dense)                   │ (None, 1)              │            33 │
└─────────────────────────────────┴────────────────────────┴───────────────┘
 Total params: 29,345 (114.63 KB)
 Trainable params: 29,345 (114.63 KB)
 Non-trainable params: 0 (0.00 B)
import os
from joblib import dump, load
from keras.callbacks import EarlyStopping, ModelCheckpoint
save_weights_at = os.path.join('keras_models', 'PRSA_data_PM2.5_LSTM_weights.{epoch:02d}-{val_loss:.4f}.keras')

checkpointer = [
    EarlyStopping(
        monitor='val_loss',  
        verbose=1,
        restore_best_weights=True,
        mode='min',
        patience=10
    ),
    ModelCheckpoint(
        filepath=save_weights_at,
        monitor='val_loss',
        verbose=1,
        save_best_only=True,
        save_weights_only=False,
        mode='min',
        save_freq='epoch'
    )
]
history_pm25_LSTM = None

if os.path.exists('history_pm25_LSTM.joblib'):
    history_pm25_LSTM = load('history_pm25_LSTM.joblib')
    print("El archivo 'history_pm25_LSTM.joblib' ya existe. Se ha cargado el historial del entrenamiento.")
else:
    history_pm25_LSTM = ts_model.fit(
        x=X_train, 
        y=y_train, 
        batch_size=16, 
        epochs=120,
        verbose=2, 
        callbacks=checkpointer,
        validation_data=(X_val, y_val),
        shuffle=True
    )
    dump(history_pm25_LSTM.history, 'history_pm25_LSTM.joblib')
    print("El entrenamiento se ha completado y el historial ha sido guardado en 'history_pm25_LSTM.joblib'.")
Epoch 1/120
2025-04-05 20:36:44.774182: I external/local_xla/xla/stream_executor/cuda/cuda_dnn.cc:531] Loaded cuDNN version 8907
Epoch 1: val_loss improved from inf to 0.01209, saving model to keras_models/PRSA_data_PM2.5_LSTM_weights.01-0.0121.keras
2069/2069 - 16s - 8ms/step - loss: 0.0197 - val_loss: 0.0121
Epoch 2/120

Epoch 2: val_loss did not improve from 0.01209
2069/2069 - 15s - 7ms/step - loss: 0.0153 - val_loss: 0.0122
Epoch 3/120

Epoch 3: val_loss improved from 0.01209 to 0.01193, saving model to keras_models/PRSA_data_PM2.5_LSTM_weights.03-0.0119.keras
2069/2069 - 14s - 7ms/step - loss: 0.0150 - val_loss: 0.0119
Epoch 4/120

Epoch 4: val_loss did not improve from 0.01193
2069/2069 - 15s - 7ms/step - loss: 0.0149 - val_loss: 0.0123
Epoch 5/120

Epoch 5: val_loss improved from 0.01193 to 0.01174, saving model to keras_models/PRSA_data_PM2.5_LSTM_weights.05-0.0117.keras
2069/2069 - 15s - 7ms/step - loss: 0.0149 - val_loss: 0.0117
Epoch 6/120

Epoch 6: val_loss did not improve from 0.01174
2069/2069 - 15s - 7ms/step - loss: 0.0149 - val_loss: 0.0121
Epoch 7/120

Epoch 7: val_loss did not improve from 0.01174
2069/2069 - 16s - 8ms/step - loss: 0.0147 - val_loss: 0.0127
Epoch 8/120

Epoch 8: val_loss did not improve from 0.01174
2069/2069 - 15s - 7ms/step - loss: 0.0148 - val_loss: 0.0120
Epoch 9/120

Epoch 9: val_loss did not improve from 0.01174
2069/2069 - 15s - 7ms/step - loss: 0.0147 - val_loss: 0.0122
Epoch 10/120

Epoch 10: val_loss improved from 0.01174 to 0.01161, saving model to keras_models/PRSA_data_PM2.5_LSTM_weights.10-0.0116.keras
2069/2069 - 15s - 7ms/step - loss: 0.0147 - val_loss: 0.0116
Epoch 11/120

Epoch 11: val_loss did not improve from 0.01161
2069/2069 - 14s - 7ms/step - loss: 0.0148 - val_loss: 0.0128
Epoch 12/120

Epoch 12: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0148 - val_loss: 0.0123
Epoch 13/120

Epoch 13: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0146 - val_loss: 0.0126
Epoch 14/120

Epoch 14: val_loss did not improve from 0.01161
2069/2069 - 16s - 8ms/step - loss: 0.0147 - val_loss: 0.0119
Epoch 15/120

Epoch 15: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0146 - val_loss: 0.0116
Epoch 16/120

Epoch 16: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0146 - val_loss: 0.0117
Epoch 17/120

Epoch 17: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0146 - val_loss: 0.0129
Epoch 18/120

Epoch 18: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0147 - val_loss: 0.0132
Epoch 19/120

Epoch 19: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0147 - val_loss: 0.0121
Epoch 20/120

Epoch 20: val_loss did not improve from 0.01161
2069/2069 - 15s - 7ms/step - loss: 0.0147 - val_loss: 0.0121
Epoch 20: early stopping
Restoring model weights from the end of the best epoch: 10.
El entrenamiento se ha completado y el historial ha sido guardado en 'history_pm25_LSTM.joblib'.
fig, ax = plt.subplots()
fig.set_size_inches(8, 4)

ax.plot(history_pm25_LSTM.history['loss'], label='Entrenamiento')
ax.plot(history_pm25_LSTM.history['val_loss'], label='Validación')
ax.set_title('Pérdida (MAE) en Entrenamiento vs Validación')
ax.set_ylabel('MAE')
ax.set_xlabel('Época')
ax.legend(loc='upper right')

plt.tight_layout()
plt.show()
_images/d9ac891d92596ea9d60a76dbf74bb0febe2b1b336a62b00908abf1c167918ab7.png
import re
model_dir = 'keras_models'
files = os.listdir(model_dir)
pattern = r"PRSA_data_PM2.5_LSTM_weights\.(\d+)-([\d\.]+)\.keras"
    
best_val_loss = float('inf')
best_model_file = None
best_model = None

for file in files:
    match = re.match(pattern, file)
    if match:
        epoch = int(match.group(1))
        val_loss = float(match.group(2))
        if val_loss < best_val_loss:
            best_val_loss = val_loss
            best_model_file = file

if best_model_file:
    best_model_path = os.path.join(model_dir, best_model_file)
    print(f"Cargando el mejor modelo: {best_model_file} con val_loss: {best_val_loss}")
    best_model = load_model(best_model_path)
else:
    print("No se encontraron archivos de modelos que coincidan con el patrón.")
Cargando el mejor modelo: PRSA_data_PM2.5_LSTM_weights.107-0.0114.keras con val_loss: 0.0114
preds = best_model.predict(X_val)
pred_pm25 = scaler_pm25.inverse_transform(preds)
pred_pm25 = np.squeeze(pred_pm25)
271/271 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step
from sklearn.metrics import mean_absolute_error
mae = mean_absolute_error(df_val['pm2.5'].loc[7:], pred_pm25)
print('MAE for the validation set:', round(mae, 4))
MAE for the validation set: 11.3706
  • Hemos utilizado keras.callbacks.ModelCheckpoint como callback para rastrear el MSE del modelo en el conjunto de validación y guardar los pesos de la epoch que da el mínimo MSE. El R-cuadrado del mejor modelo para el conjunto de validación es de 0.9959. Los primeros cincuenta valores reales y predichos se muestran en la siguiente figura

plt.figure(figsize=(5.5, 5.5))
plt.plot(range(50), df_val['pm2.5'].loc[7:56], linestyle='-', marker='*', color='r')
plt.plot(range(50), pred_pm25[:50], linestyle='-', marker='.', color='b')
plt.legend(['Actual','Predicted'], loc=2)
plt.title('Actual vs Predicted pm2.5')
plt.ylabel('pm2.5')
plt.xlabel('Index');
_images/2899e1f4ca2175a17a3086c62438b3d0ea5c6c6df0be84cd37261cebc56b3e43.png

10.19. Atención y Memoria#

Introducción

  • El uso histórico de esquemas de atención en redes neuronales se inspira en el mecanismo de atención humana. En la visión humana, nos centramos en información contextual importante cuando observamos escenas. En el aprendizaje automático, los modelos de atención implementan este concepto asignando pesos a las variables que influyen en la salida.

  • Por ejemplo, en una RNN, la salida \(\hat{\boldsymbol{y}}_{n}\) depende del vector de estado \(\boldsymbol{h}_{n}\), pero no siempre es la información más importante para tareas como la traducción de idiomas (aunque el estado codifica la memoria hasta el tiempo más reciente, \(n\).).

Observation 10.6 (Caso típico)

  • En la traducción del japonés al inglés, la última palabra de una frase japonesa puede influir mucho en la primera palabra de la traducción inglesa. Del mismo modo, en la toma de decisiones, nuestras acciones dependen de las experiencias pasadas acumuladas, y algunas experiencias pasadas específicas ejercen más influencia que las recientes.

  • Para solucionar este problema, los mecanismos de atención asignan pesos a los vectores de estado anteriores, lo que permite que la salida dependa de una combinación ponderada de información previa. Estas ponderaciones se aprenden durante el entrenamiento, lo que permite al sistema dar prioridad a detalles contextuales significativos a la hora de tomar decisiones o generar resultados.

  • Por ejemplo, el vector de salida puede modificarse para que dependa de todos los vectores de estado calculados anteriormente

    \[ \hat{\boldsymbol{y}}_{n}=f\left(\sum_{i=1}^{n}\alpha_{ni}\boldsymbol{h}_{i}+\boldsymbol{c}\right), \]

    donde \(\alpha_{ni}\) son los correspondientes pesos en el tiempo \(n\).

  • La idea anterior de combinar todos los vectores de estado ha sido empleada, en una formulación algo diferente, en el sistema de traducción automática (ver [Bahdanau et al., 2014])

_images/attention_weights_grayscale.png

Fig. 10.51 Valores de los pesos de atención en escala de grises. Example 10.1#

Example 10.1

  • La Fig. 10.51 demuestra el razonamiento que subyace a la utilización de un mecanismo de ponderación. La entrada está representada por las palabras en francés, mientras que las secuencias de salida correspondientes están representadas por las palabras en inglés.

  • La visualización representa los pesos de atención como píxeles, donde los pesos más altos se representan como píxeles más blancos. En particular, el término "produce" en la salida es consecuencia de asignar peso a la información de tres puntos temporales consecutivos, vinculados con las palabras "peut plus produire", mientras que el término "destruction" está asociado con dos palabras, a saber, "La destruction".

Observation 10.7

  • Una ventaja notable de incorporar un mecanismo de atención al modelo es la transparencia que aporta a las acciones del modelo y a la formación de la información de salida. Esto resulta especialmente valioso cuando la interpretabilidad de la red es una preocupación, ya que permite comprender el “por qué” y el “cómo” del proceso de toma de decisiones de la red.

10.20. Entrenamiento adversario#

Introducción

  • Las redes neuronales profundas lideran en la obtención de un rendimiento y precisión sobresalientes, frecuentemente comparables e incluso en algunos casos superiores a las habilidades humanas. Sin embargo, persiste el desafío de afirmar que estos modelos realmente “comprenden” las tareas que han “aprendido” a realizar, a pesar de su habilidad para predecir etiquetas precisas en tareas de clasificación con una confianza considerablemente alta.

  • Es viable generar ejemplos adversos que sistemáticamente engañan a los modelos de aprendizaje automático. Aquí, “adversario” denota la introducción intencional de pequeñas perturbaciones en los patrones dentro del conjunto de datos de entrada, lo que resulta en una alta probabilidad de clasificación incorrecta. Notablemente, estas sutiles perturbaciones de ruido son apenas detectables para los sentidos humanos, ya sea en forma de imágenes o música.

  • La Fig. 10.52 muestra una serie de nueve imágenes. Se ha entrenado una red neuronal (AlexNet) para discernir los contenidos de las imágenes. El trío izquierdo de imágenes, tomadas del respectivo conjunto de pruebas, fueron identificadas con precisión. Las imágenes en el centro son versiones ruidosas añadidas a las originales de la izquierda. Las composiciones resultantes se presentan a la derecha. Mientras los seres humanos anticipan sin dificultad las etiquetas correctas, AlexNet catalogó de forma errónea las tres imágenes como “avestruz, struthio camelus.

_images/wrong_image_classification.png

Fig. 10.52 Imágenes de la izquierda se han clasificado correctamente. Todas las imágenes de la derecha han sido clasificadas como “avestruz, Struthio camelus”. Fuente [Theodoridis, 2020].#

  • La causa de este comportamiento aparentemente “peculiar” parece estar vinculada a la naturaleza altamente dimensional del espacio de entrada. Por lo general, en un contexto de aprendizaje, se cumple el supuesto de suavidad. Esto implica que para un valor positivo suficientemente pequeño \(\epsilon\) y un patrón de entrada \(\boldsymbol{x}\), se espera que el patrón

    \[ \boldsymbol{x}':= \boldsymbol{x} + \boldsymbol{v},~\text{donde}~\boldsymbol{v} : \|\boldsymbol{v}\|\leq\epsilon, \]

    se clasifique en la misma categoría que \(\boldsymbol{x}\) con una alta probabilidad.

  • El impacto de la alta dimensionalidad (número de características igual o superior al número de observaciones) en la condición de suavidad se hace evidente, sobre todo en el contexto de un clasificador lineal. Consideremos un clasificador entrenado descrito por sus parámetros, \(\boldsymbol{\theta}\). Dado un patrón de entrada, \(\boldsymbol{x}\), la etiqueta se determina basándose en el signo del producto interior, \(\boldsymbol{\theta}^{T}\boldsymbol{x}\). Para el escenario en el que interviene \(\boldsymbol{x}'\), el producto interior se expresa como

\[ \boldsymbol{\theta}^{T}\boldsymbol{x}' = \boldsymbol{\theta}^{T}\boldsymbol{x} + \boldsymbol{\theta}^{T}\boldsymbol{v}. \]
  • Si establecemos intencionadamente \(\boldsymbol{v} = \pm\epsilon\cdot\text{sgn}(\boldsymbol{\theta})\), donde la operación signo se aplica elemento a elemento, se obtiene un resultado digno de mención. Entonces

\[ \boldsymbol{\theta}^{T}\boldsymbol{x}'=\boldsymbol{\theta}^{T}\boldsymbol{x}+\boldsymbol{\theta}^{T}\boldsymbol{v}=\boldsymbol{\theta}^{T}\boldsymbol{x}\pm\epsilon\sum_{i=1}^{l}|\theta_{i}|. \]
  • Por lo tanto, cuando la dimensionalidad de la entrada \(l\) es alta, es probable que se produzcan diferencias significativas en los valores del producto interno, lo que podría causar etiquetas de predicción distintas tanto para \(\boldsymbol{x}\) como para \(\boldsymbol{x}'\). En términos sencillos, la interacción de la linealidad y la alta dimensionalidad rompe el supuesto de suavidad.

Observation 10.8

  • A pesar de que los ejemplos adversos son poco frecuentes en los datos de entrada (tanto en los conjuntos de entrenamiento como en los de prueba), su existencia sigue siendo desconcertante. Además, pueden aprovecharse para engañar deliberadamente a las redes. En consecuencia, han surgido varias técnicas para reforzar la resistencia de las redes frente a adversarios.

  • Uno estudio descrito en [Szegedy et al., 2013], consiste en generar ejemplos adversos e incorporarlos de nuevo al conjunto de entrenamiento. Esta estrategia funciona como una forma de regularización a través del aumento de datos. Sin embargo, una perspectiva alternativa presentada en [Kereliuk et al., 2015] argumenta que este método no mejoró notablemente el rendimiento cuando se aplicó a datos musicales.

  • En [Goodfellow et al., 2014], la función de pérdida \(J\) se modifica adecuadamente como

    \[ J'(\boldsymbol{\theta}, \boldsymbol{x}, \boldsymbol{y})=\alpha J(\boldsymbol{\theta}, \boldsymbol{x}, \boldsymbol{y})+(1-\alpha)J(\boldsymbol{\theta}, \boldsymbol{x}+\Delta\boldsymbol{x}, \boldsymbol{y}),~0<\alpha<1, \]

    donde

    \[ \Delta\boldsymbol{x}=\epsilon\cdot\text{sgn}\left(\frac{\partial}{\partial\boldsymbol{x}}J(\boldsymbol{\theta}, \boldsymbol{x}, \boldsymbol{y})\right),~\epsilon>0, \]

    es una dirección para la perturbación adversarial.

  • Es importante señalar que los ejemplos adversarios siguen siendo un foco de investigación en evolución dinámica. Se subrayan los peligros potenciales, como la manipulación de vehículos autónomos utilizando muestras adversarios, entre otros (ver [Kurakin et al., 2018, Papernot et al., 2017, Papernot et al., 2016]).

10.21. Modelos Generativos Profundos#

10.21.1. Máquina de Boltzmann Restringida#

Introducción

  • Una máquina de Boltzmann restringida (RBM) es una red neuronal artificial estocástica generativa que puede aprender una distribución de probabilidad sobre su conjunto de entradas bajo ajuste de parámetros. Las RBM fueron estudiadas inicialmente con el nombre de Harmonium por Paul Smolensky en 1986, y adquirieron prominencia después de que Geoffrey Hinton y sus colaboradores propusieron para estas, algoritmos de aprendizaje rápido a mediados de 2000.

  • Las RBM han encontrado aplicaciones en la reducción de la dimensionalidad, clasificación, filtrado, aprendizaje de funciones, modelado de temas e incluso en muchas mecánicas cuánticas corporales. Pueden ser entrenados en forma supervisada o no supervisada, dependiendo de la tarea.

  • El modelo gráfico de la Fig. 10.53 ilustra una máquina de Boltzmann restringida (RBM). La máquina de Boltzmann utiliza la siguiente distribución de probabilidad conjunta de Boltzmann definida como

    \[ p(x_{1}, \dots, x_{l}):=p(\boldsymbol{x})=\frac{1}{Z}\exp\left(-\sum_{i}\left(\sum_{j>i}\theta_{ij}x_{i}x_{j}+\theta_{i0}x_{i}\right)\right), \]

    donde cada variable aleatoria toma valores binarios en \(\{-1, 1\}\), \(\theta_{ij}=0\) si los nodos respectivos no están conectados y la constante \(Z\) se conoce como función de partición y es la constante normalizadora para garantizar que \(p(x_{1}, \dots, x_{l})\) es una distribución de probabilidad.

  • Es importante destacar que los nodos dentro de la misma capa carecen de interconexiones. Esta arquitectura abarca nodos visibles en la capa inferior que reciben observaciones, mientras que la capa superior contiene nodos vinculados a variables ocultas. Cabe destacar que sólo los nodos de la capa inferior reciben observaciones. Es posible construir RBM profundos apilando múltiples RBM unos sobre otros.

  • De acuerdo con la definición general de una máquina de Boltzmann, la distribución conjunta de las variables aleatorias implicadas puede expresarse como:

    \[ P(v_{1}, \dots, v_{J}, h_{1}, \dots, h_{I})=\frac{1}{Z}\exp(-E(\boldsymbol{v}, \boldsymbol{h})), \]

    donde hemos utilizado símbolos diferentes para las \(J\) variables visibles (\(v_{j},~j = 1, 2,\dots, J\)) y las \(I\) variables ocultas (\(h_{i},~i = 1, 2,\dots, I\)).

  • La función de energía no es la salida de la RBM, por lo contrario, es una métrica para medir la calidad de un modelo. Produce un valor escalar que corresponde básicamente a la configuración del modelo y es un indicador de la probabilidad de que el modelo esté en esa configuración. Si el modelo está configurado para favorecer una energía baja, las configuraciones que conduzcan a una energía baja tendrán una probabilidad más alta.

_images/rboltzmann_model.png

Fig. 10.53 Arquitectura de modelo (RBM). Fuente [Theodoridis, 2020].#

  • La energía se define en función de un conjunto de parámetros desconocidos, es decir,

\[ E(\boldsymbol{v}, \boldsymbol{h})=-\sum_{i=1}^{I}\sum_{j=1}^{J}\theta_{ij}h_{i}v_{j}-\sum_{i=1}^{I}b_{i}h_{i}-\sum_{j=1}^{J}c_{j}v_{j}. \]
  • La constante de normalización se obtiene como

\[ Z=\sum_{v}\sum_{h}\exp(-E(\boldsymbol{v}, \boldsymbol{h})). \]
  • Nuestro enfoque será en variables discretas, lo que implica que las distribuciones relacionadas son de carácter probabilístico. Específicamente, nos centraremos en variables de naturaleza binaria, es decir, aquellas que poseen únicamente dos posibles valores, \(v_{j}, h_{i}\in\{0, 1\},\) \(j=1,\dots,J\), \(i=1,\dots,I\).

  • El objetivo al entrenar una Máquina de Boltzmann Restringida (RBM) consiste en adquirir el conjunto de parámetros desconocidos \(\theta_{ij}, b_{i}, c_{j}\), los cuales serán designados colectivamente como, \(\boldsymbol{\Theta},\boldsymbol{b}\) y \(\boldsymbol{c}\) respectivamente. Un enfoque primordial para lograr esto es maximizar la log-verosimilitud, utilizando \(N\) observaciones de las variables visibles, representadas como \(v_{n},~n=1,\dots, N\), donde

    \[ \boldsymbol{v}_{n}=[v_{1n}, \dots, v_{Jn}]^{T}, \]

    es el vector que contiene las observaciones correspondientes en el momento \(n\).

  • Diremos que los nodos visibles están fijados en las observaciones respectivas. La correspondiente log-verosimilitud (promedio) se expresa como sigue:

    \[\begin{split} \begin{align*} L(\boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c})&=\frac{1}{N}\sum_{n=1}^{N}\ln P(\boldsymbol{v}_{n};\boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c})\\ &=\frac{1}{N}\sum_{n=1}^{N}\ln\left(\frac{1}{Z}\sum_{\boldsymbol{h}}\exp(-E(\boldsymbol{v}_{n}, \boldsymbol{h}; \boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c}))\right)\\ &=\frac{1}{N}\sum_{n=1}^{N}\ln\left(\sum_{\boldsymbol{h}}\exp(-E(\boldsymbol{v}_{n}, \boldsymbol{h}; \boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c}))\right)-\ln\sum_{\boldsymbol{v}}\sum_{\boldsymbol{h}}\exp\left(-E(\boldsymbol{v}, \boldsymbol{h})\right), \end{align*} \end{split}\]

    donde el índice “\(n\)” en la energía se refiere a las observaciones respectivas en las cuales los nodos visibles han sido fijados, y el símbolo “\(\Theta\)” ha sido incluido explícitamente en la notación.

  • Tomando la derivada de \(L(\boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c})\) respecto a \(\theta_{ij}\) (similar es el caso de las derivadas respecto a respecto a \(b_{i}\) y \(c_{j}\)) y aplicando las propiedades estándar de las derivadas, se tiene que (verifíquelo)

    (10.18)#\[ \frac{\partial L(\boldsymbol{\Theta}, \boldsymbol{b}, \boldsymbol{c})}{\partial\theta_{ij}}=\frac{1}{N}\sum_{n=1}^{N}\left(\sum_{\boldsymbol{h}}P(\boldsymbol{h}|\boldsymbol{v}_{n})h_{i}v_{jn}\right)-\sum_{\boldsymbol{v}}\sum_{\boldsymbol{h}}P(\boldsymbol{v}, \boldsymbol{h})h_{i}v_{j}, \]

    donde

    \[ P(\boldsymbol{h}|\boldsymbol{v})=\frac{P(\boldsymbol{v}|\boldsymbol{h})}{\sum_{\boldsymbol{h}'}P(\boldsymbol{v}, \boldsymbol{h}')}. \]
  • El gradiente en la Eq. (10.18) consta de dos términos. El primero puede ser calculado una vez que \(P(\boldsymbol{h}|\boldsymbol{v})\) está disponible. Básicamente, este término es la tasa media de disparo o correlación cuando la Máquina de Boltzmann Restringida (RBM) está operando en su fase fijada; a menudo, nos referimos a esto como la fase positiva (regla de aprendizaje de Hebb [Hebb, 2005]), y el término se denota como \(\langle h{i}v_{j}\rangle^{+}\). El segundo término es la correlación correspondiente cuando la RBM está funcionando en su llamada fase libre o negativa, y se denota como \(\langle h_{i}v_{j}\rangle^{-}\). Por lo tanto, un esquema de aumento de gradiente para maximizar la log-verosimilitud tendrá la forma:

\[ \theta_{ij}(\text{new})=\theta_{ij}(old)+\mu(\langle h{i}v_{j}\rangle^{+}-\langle h_{i}v_{j}\rangle^{-}). \]

10.22. Autoencoders#

_images/autoencoder-architecture.png
  • Los autoencoders han sido propuestos como métodos para la reducción de dimensionalidad. Un autoencoder consta de dos partes, el codificador y el decodificador. La salida del codificador es la representación reducida del patrón de entrada, y se define en términos de una función vectorial

    \[ \boldsymbol{g}_{\phi}:~\boldsymbol{x}\in\mathbb{R}^{l}\longmapsto\boldsymbol{z}\in\mathbb{R}^{m}, \]

    donde

    \[ z_{i}:=g_{\phi, i}(\boldsymbol{x})=\phi_{e}(\boldsymbol{\theta}_{i}^{T}\boldsymbol{x}+b_{i}),\quad i=1,2,\dots,m, \]

    con \(\phi_{e}\) función de activación; esta última suele tomarse como la función sigmoidea logística \(\phi_{e}(\cdot)=\sigma(\cdot)\). En otras palabras, el codificador es una red neuronal feed-forward de una sola capa oculta

  • El decodificador es otra función \(\boldsymbol{f}_{\theta}\).

    \[ \boldsymbol{f}_{\theta}:~\boldsymbol{z}\in\mathbb{R}^{m}\longmapsto\boldsymbol{x}'\in\mathbb{R}^{l}, \]

    donde

    \[ \hat{x}_{j}=f_{\theta, j}(\boldsymbol{z})=\phi_{d}(\boldsymbol{\theta}'^{T}\boldsymbol{z}+b_{j}'),\quad j=1,\dots,l. \]

    La función de activación \(\phi_{d}\) suele ser la identidad (reconstrucción lineal) o la sigmoidea logística.

  • La tarea de entrenamiento consiste en estimar los parámetros

\[ \Theta:=[\boldsymbol{\theta}_{1},\dots,\boldsymbol{\theta}_{m}],~\boldsymbol{b},~\Theta':=[\boldsymbol{\theta}_{1}',\dots,\boldsymbol{\theta}_{l}'],~\boldsymbol{b}'. \]
  • Es habitual suponer que \(\Theta'=\Theta^{T}\). Los parámetros se estiman para que el error de reconstrucción \(\boldsymbol{e}=\boldsymbol{x}-\boldsymbol{x}'\), sobre las muestras de entrada disponibles sea mínimo. Normalmente, se utiliza el coste por mínimos cuadrados pero también son posibles otras opciones.

  • Las versiones regularizadas, que implican una norma de los parámetros, también es una posibilidad. Si se elige que la activación \(\phi_{e}\) sea la identidad (representación lineal) y \(m < l\) (para evitar la trivialidad), el autoencoder es equivalente a la técnica PCA.

10.23. Aplicación: Autoencoders para MNIST data#

import numpy as np
import matplotlib.pyplot as plt
from tensorflow.keras.datasets import mnist
from tensorflow.keras.models import Model
from tensorflow.keras.layers import Input, Dense
import warnings
warnings.filterwarnings('ignore')
_images/incode_decode_784to32to784.png
  • A continuación

    • Se cargan los datos del conjunto MNIST, que contiene imágenes de dígitos escritos a mano.

    • Cada muestra es una imagen en escala de grises de 28×28 píxeles.

    • Se ignoran las etiquetas de clase (_) porque el objetivo es reconstruir las imágenes (no clasificarlas), tal como se requiere en tareas de autoencoders.

(x_train, _), (x_test, _) = mnist.load_data()
print("Forma original de x_train:", x_train.shape)
print("Forma original de x_test:", x_test.shape)
Forma original de x_train: (60000, 28, 28)
Forma original de x_test: (10000, 28, 28)
  • Se normalizan los valores de los píxeles de las imágenes, originalmente en el rango [0, 255], para que estén en el intervalo [0, 1]. La normalización facilita el entrenamiento del modelo al mejorar la estabilidad numérica y acelerar la convergencia del optimizador. Se convierte el tipo de dato a float32 para asegurar compatibilidad con las operaciones en TensorFlow/Keras.

x_train = x_train.astype('float32') / 255.
x_test = x_test.astype('float32') / 255.
  • Cada imagen del conjunto de datos MNIST tiene dimensiones 28×28, es decir, 784 píxeles. Estas imágenes se transforman en vectores unidimensionales de 784 elementos para que puedan ser procesadas por capas densas (Dense) del autoencoder. Esta transformación no altera la información de la imagen, solo su forma (reshape) para adaptarse a la entrada del modelo.

x_train = x_train.reshape((len(x_train), 784))
x_test = x_test.reshape((len(x_test), 784))
print("Forma después del reshape de x_train:", x_train.shape)
print("Forma después del reshape de x_test:", x_test.shape)
Forma después del reshape de x_train: (60000, 784)
Forma después del reshape de x_test: (10000, 784)
  • input_img = Input(shape=(784,)):
    Se define la capa de entrada del modelo, correspondiente a una imagen aplanada de 784 píxeles.

  • encoded = Dense(32, activation='relu')(input_img):
    Esta capa es el codificador, que transforma la imagen de entrada en una representación latente de 32 dimensiones utilizando una función de activación ReLU.

  • decoded = Dense(784, activation='sigmoid')(encoded):
    Esta capa actúa como decodificador, intentando reconstruir la imagen original a partir de la representación latente.

  • sigmoid se adapta bien cuando se usa la función de pérdida binary_crossentropy, ya que esa pérdida espera que la salida esté entre 0 y 1. Asegura que los valores de salida no sean negativos ni mayores a 1, lo que tendría poco sentido en términos de imágenes normalizadas.

  • autoencoder = Model(input_img, decoded):
    Se define el modelo completo del autoencoder, que aprende a mapear una imagen a su reconstrucción.

input_img = Input(shape=(784,))
encoded = Dense(32, activation='relu')(input_img)
decoded = Dense(784, activation='sigmoid')(encoded)
autoencoder = Model(input_img, decoded)
  • autoencoder.compile(...):
    Se compila el modelo autoencoder, especificando el optimizador y la función de pérdida.

  • optimizer='adam':
    Se utiliza el optimizador Adam, ampliamente usado en redes neuronales por su eficiencia y capacidad de adaptación del aprendizaje.

  • loss='binary_crossentropy':
    Se emplea la entropía cruzada binaria como función de pérdida, apropiada en tareas de reconstrucción donde los valores de salida están en el rango [0, 1].

autoencoder.compile(optimizer='adam', loss='binary_crossentropy')
  • autoencoder.fit(...):
    Entrena el modelo autoencoder utilizando como entrada y objetivo los mismos datos, dado que la tarea es de reconstrucción.

  • x_train, x_train:
    Se entrena el modelo para que aprenda a reproducir su entrada como salida.

  • epochs=50:
    El entrenamiento se realiza durante 50 épocas completas sobre el conjunto de datos.

  • batch_size=256:
    El tamaño del mini-batch para cada actualización de parámetros es de 256 muestras.

  • shuffle=True:
    Se mezclan aleatoriamente las muestras en cada época para mejorar la generalización.

  • validation_data=(x_test, x_test):
    Se utiliza el conjunto de prueba para evaluar la pérdida de validación y así monitorear el sobreajuste.

autoencoder.fit(x_train, x_train,
                epochs=50,
                batch_size=256,
                shuffle=True,
                validation_data=(x_test, x_test))
Epoch 1/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 3s 8ms/step - loss: 0.3830 - val_loss: 0.1906
Epoch 2/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 1s 2ms/step - loss: 0.1813 - val_loss: 0.1540
Epoch 3/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.1492 - val_loss: 0.1339
Epoch 4/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1317 - val_loss: 0.1216
Epoch 5/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1203 - val_loss: 0.1134
Epoch 6/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1132 - val_loss: 0.1079
Epoch 7/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1080 - val_loss: 0.1037
Epoch 8/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1041 - val_loss: 0.1005
Epoch 9/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.1009 - val_loss: 0.0978
Epoch 10/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0984 - val_loss: 0.0960
Epoch 11/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0968 - val_loss: 0.0948
Epoch 12/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0957 - val_loss: 0.0940
Epoch 13/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0952 - val_loss: 0.0935
Epoch 14/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0949 - val_loss: 0.0932
Epoch 15/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0944 - val_loss: 0.0929
Epoch 16/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0943 - val_loss: 0.0927
Epoch 17/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.0938 - val_loss: 0.0926
Epoch 18/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0937 - val_loss: 0.0924
Epoch 19/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0937 - val_loss: 0.0923
Epoch 20/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.0935 - val_loss: 0.0923
Epoch 21/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0935 - val_loss: 0.0921
Epoch 22/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.0933 - val_loss: 0.0921
Epoch 23/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.0933 - val_loss: 0.0920
Epoch 24/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0932 - val_loss: 0.0920
Epoch 25/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0931 - val_loss: 0.0919
Epoch 26/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0932 - val_loss: 0.0919
Epoch 27/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0931 - val_loss: 0.0919
Epoch 28/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0919
Epoch 29/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 2ms/step - loss: 0.0930 - val_loss: 0.0919
Epoch 30/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0931 - val_loss: 0.0918
Epoch 31/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0918
Epoch 32/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0930 - val_loss: 0.0917
Epoch 33/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0931 - val_loss: 0.0917
Epoch 34/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0929 - val_loss: 0.0917
Epoch 35/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0917
Epoch 36/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0917
Epoch 37/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0929 - val_loss: 0.0917
Epoch 38/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0930 - val_loss: 0.0917
Epoch 39/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0917
Epoch 40/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0916
Epoch 41/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0926 - val_loss: 0.0917
Epoch 42/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0929 - val_loss: 0.0916
Epoch 43/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0916
Epoch 44/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0929 - val_loss: 0.0916
Epoch 45/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0917
Epoch 46/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0916
Epoch 47/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0916
Epoch 48/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0927 - val_loss: 0.0916
Epoch 49/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0926 - val_loss: 0.0916
Epoch 50/50
235/235 ━━━━━━━━━━━━━━━━━━━━ 0s 1ms/step - loss: 0.0928 - val_loss: 0.0916
<keras.src.callbacks.history.History at 0x7fb01ba215a0>
  • Define el modelo encoder que transforma una imagen de entrada (784 dimensiones) en su representación latente de 32 dimensiones. Este modelo extrae la parte de codificación del autoencoder original.

  • Crea un nuevo modelo decoder tomando como entrada una representación latente de 32 dimensiones y aplicando la última capa del autoencoder (la capa de decodificación). Esto permite reconstruir imágenes directamente desde sus representaciones codificadas, de forma independiente.

  • autoencoder.layers[-1] accede a la última capa del modelo autoencoder (el Dense(784, activation='sigmoid'))

encoder = Model(input_img, encoded)

encoded_input = Input(shape=(32,))
decoder_layer = autoencoder.layers[-1]

decoder = Model(encoded_input, decoder_layer(encoded_input))
  • Codifica las imágenes de prueba transformándolas desde el espacio original (784 dimensiones) al espacio latente (64 dimensiones) mediante el modelo encoder.

  • Reconstruye las imágenes a partir de sus representaciones latentes usando el modelo decoder.

  • Este paso permite evaluar qué tan bien el autoencoder logra aproximar las imágenes originales.

encoded_imgs = encoder.predict(x_test)
decoded_imgs = decoder.predict(encoded_imgs)
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 632us/step
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 607us/step
  • Selecciona 10 imágenes del conjunto de prueba y configura el tamaño de la figura para mostrar una comparación clara entre las imágenes originales y sus reconstrucciones.

  • Primera fila: imágenes originales. Se toma la \(i\)-ésima imagen original, se remodela a su forma 2D (28×28) y se muestra en escala de grises sin ejes para mayor claridad.

  • Segunda fila: imágenes reconstruidas. Se visualiza la imagen generada por el autoencoder a partir de la codificación latente.

n = 10
plt.figure(figsize=(20, 4))
for i in range(n):
    ax = plt.subplot(2, n, i + 1)
    plt.imshow(x_test[i].reshape(28, 28), cmap="gray")
    plt.axis('off')

    ax = plt.subplot(2, n, i + 1 + n)
    plt.imshow(decoded_imgs[i].reshape(28, 28), cmap="gray")
    plt.axis('off')
plt.show()
_images/65a8a30923976ca24ee93ce357afa99872d5ee04c71a0a39c04fad51a94f089f.png
  1. Reconstrucción de imágenes
    Se utiliza el modelo entrenado para generar reconstrucciones del conjunto de prueba, es decir, aproximaciones de las imágenes originales.

  2. Cálculo del Error Cuadrático Medio (MSE)
    Se calcula el error cuadrático medio entre las imágenes originales y sus reconstrucciones. Este valor indica qué tan lejos están, en promedio, las imágenes reconstruidas de las originales.

  3. Estimación de la varianza total
    Se computa la varianza de los datos originales del conjunto de prueba. Representa la cantidad total de información (variabilidad) presente en los datos sin comprimir.

  4. Cálculo de la variabilidad explicada
    Se emplea la fórmula:

    \[ \text{Variabilidad explicada} = 1 - \frac{\text{MSE}}{\text{Varianza total}} \]

    Esta expresión indica qué fracción de la variación total en los datos originales ha sido retenida por el modelo de autoencoder. Es una métrica análoga al coeficiente de determinación (\(R^2\)) utilizado en modelos de regresión.

  5. Interpretación del resultado
    Un valor cercano a 1 sugiere que el autoencoder captura gran parte de la estructura de los datos. Un valor bajo indica que el modelo no ha logrado representar eficazmente la información original.

import numpy as np

decoded_imgs = autoencoder.predict(x_test)
mse = np.mean(np.square(x_test - decoded_imgs))
var_total = np.var(x_test)
var_explicada = 1 - (mse / var_total)

print("Variabilidad explicada por el autoencoder:", var_explicada)
313/313 ━━━━━━━━━━━━━━━━━━━━ 0s 705us/step
Variabilidad explicada por el autoencoder: 0.8997409120202065
  • En un autoencoder, el número de neuronas en la capa latente (bottleneck layer) representa la dimensión comprimida o el número de “componentes” aprendidos. Aunque no son componentes lineales como en PCA, son equivalentes en el sentido de que capturan una representación reducida de los datos. Por ejemplo, en el modelo:

    encoded = Dense(32, activation='relu')(input_img)
    

    Aquí el autoencoder utiliza 32 dimensiones latentes, lo que se puede comparar directamente con las 32 primeras componentes principales en PCA.

¿Cómo hacer una comparación justa con PCA?

  1. Entrena un PCA con el mismo número de componentes:

    from sklearn.decomposition import PCA
    pca = PCA(n_components=32)
    X_pca = pca.fit_transform(x_test)
    X_reconstructed = pca.inverse_transform(X_pca)
    
  2. Calcula el MSE y la variabilidad explicada del PCA, de la misma forma que hiciste con el autoencoder:

    mse_pca = np.mean(np.square(x_test - X_reconstructed))
    var_total = np.var(x_test)
    var_explicada_pca = 1 - (mse_pca / var_total)
    
  3. Compara valores:

    • Variabilidad explicada por el Autoencoder

    • Variabilidad explicada por el PCA

Esto te dirá cuál técnica preserva mejor la estructura de los datos bajo la misma cantidad de compresión (32 dimensiones, por ejemplo).

from sklearn.decomposition import PCA
pca = PCA(n_components=32)
X_pca = pca.fit_transform(x_test)
X_reconstructed = pca.inverse_transform(X_pca)
mse_pca = np.mean(np.square(x_test - X_reconstructed))
var_total = np.var(x_test)
var_explicada_pca = 1 - (mse_pca / var_total)
print("Variabilidad explicada por el PCA:", var_explicada_pca)
Variabilidad explicada por el PCA: 0.8270216286182404