Maqueta de red neuronal convolucional#

import numpy as np
from tqdm import tqdm 
import pickle
import logging
from scipy import signal as sg

Configuración del proceso

entrenamiento=False

Redes convolucionales#

_images/Densa_vs_ConvNet.png

Izquierda: Una red neuronal normal de 3 capas. Derecha: Un ConvNet organiza sus neuronas en tres dimensiones (ancho, alto, profundidad), como se visualiza en una de las capas. Cada capa de un ConvNet transforma el volumen de entrada 3D en un volumen de salida 3D de activaciones neuronales. En este ejemplo, la capa de entrada roja contiene la imagen, por lo que su ancho y alto serían las dimensiones de la imagen, y la profundidad sería de 3 (canales rojo, verde, azul).

Una Arquitectura Convolucional tipo estará compuesta por las capas: [INPUT - CONV - RELU - POOL - FC]. Cuyo detalle es:

  • INPUT [bxhx3] contendrá los valores de píxeles raw de la imagen, en este caso una imagen de ancho b, altura h, y con tres canales de color R, G, B.

  • La capa CONV calculará la salida de las neuronas que están conectadas a las regiones locales en la entrada, cada una calculando un producto de punto entre sus pesos y una pequeña región a la que están conectadas en el volumen de entrada. Esto puede resultar en un volumen como [BxHxd] si decidimos usar d filtros. Los pesos que conectan los pixels localmente son compartidos y son aprendibles.

  • La capa RELU aplicará una función de activación por elementos, como el umbral \((max(0,x))\) en cero. Esto deja el tamaño del volumen sin cambios ([BxHxd]).

  • La capa POOL realizará una operación de downsampling a lo largo de las dimensiones espaciales (ancho, alto), lo que dará como resultado un volumen como [bxhxd], siendo (b,h) inferiores a (B,H). Esta capa no utiliza pesos aprendibles.

  • La capa FC o densa (es decir, totalmente conectada) calculará los puntajes de clase, lo que resultará en un volumen de tamaño [1x1xK], donde cada uno de los K números corresponde a un puntaje de clase, entre las K categorías del conjunto. Al igual que con las redes neuronales ordinarias y como su nombre indica, cada neurona en esta capa estará conectada a todos los números en el volumen anterior.

Fundamento teórico#

Una red neuronal se puede considerar (LeCun et al.; 1998) un sistema construido como una cascada de módulos, cada uno de los cuales implementa una función

\[X_l = F_l(W_l, X_{l-1})\]

Se toma \(l\) como el índicador de número de capa (layer en inglés).

Dónde \(X_l\) es un vector que representa la salida del módulo, \(W_l\) es el vector de los parámetros entrenables del módulo (y que forma parte del conjunto total \(W\)) y \(X_{l-1}\) es el vector de entrada al módulo (así como la salida del módulo previo).

Se ha implementado una maqueta naíf que implementa 4 tipos de capas: densa o perceptrón, convolucional, agrupación máxima (max-pooling) y aplanado. La capa densa con una programación matricial en numpy se implementa de acuerdo a la documentación previa. La capa convolucional se restringe a convoluciones 2D sin relleno a ceros y con salto 1. La agrupación máxima usa un paso único en ambas direcciones de la imagen. El aplanado es un simple cambio de formato para pasar de las capas convolucionales a las densas.

Al final de la última capa se aplica una función softmax para obtener las probabilidades de cada clase y la pérdida se calcula con una función de croos-entropy.

También se incorpora la optimación Adam que provee un ratio de aprendizaje modulado ([Kingma and Ba, 2014]).

El vector de datos de entrada \(X_0\) va a ser un tensor o matriz de 4 dimensiones (n, c, h, b), donde n representará el número de observaciones en el lote tratado, c el número de canales, h es la altura de la imagen y b es el ancho de la imagen.

Esta estructura de los vectores \(X_l\) se mantiene durante los procesos convolucionales y de agrupación, mientras que las capas densas manejan tensores de la forma (n, m), siendo n el número de observaciones tratadas y m las características de cada observación. Lógicamente, la capa de aplanado efectua reestructuración de las dimensiones con m = c * h * b.

En una capa convolucional los parámetros entrenables están formados por un filtro \(W_l\) cuyas dimensiones son (co, ci, f, f), donde co es el número de canales de salida (los canales del tensor \(X_l\)), ci es el número de canales de entrada (los canales del tensor \(X_{l-1}\)) y f es el tamaño del filtro. Además se incluye un parámetro de bias \(B_l\), que es un vector de dimensiones (co).

Las capas de agrupación y aplanado no tienen parámetros entrenables.

Las capas densas tienen una matriz entrenable \(W_l\) de dos dimensiones (m,p), siendo m el nº de carácteristicas de la salida de la capa (el vector \(X_l\)) y p el número de características de la entrada a la capa (el vector \(X_{l-1}\))

El proceso de retropropagación#

En cuadernos anteriores se ha justificado el proceso de retropropagación de las capas densas que se incluye en la maqueta. Aquí se va a justificar las fórmulas usadas a partir de un ejemplo teórico de dimensión reducida en las condiciones de la convolución expuestas. A continuación se muestra un gráfico del proceso de entrenamiento de un lote de 32 imágenes con la arquitectura LetNet5. En el se indican los pasos forward de cada una de las capas, el final de proceso donde se obtiene las probabilidades de clase con la función softmax y el cálculo de la función de pérdida a partir de la entropía cruzada (\(C\)).

En este proceso final se obtiene el grandiente del coste con respecto a la combinación lineal de la última capa (\(\frac{\partial C}{\partial Z ^L}\)), este gradiente que en el gráfico y en las rutinas programadas utiliza la nomeclatura dA comparte la dimensión con los tensores Z y A de cada capa. El primer proceso de retropropagación (BAC1 en el esquema) arranca con dA8 que es el gradiente de coste de la capa 8. Hay que entender Softmax como una activación específica. Por eso la entrada a esta función es Z8 la combinación lineal de la última capa. La salida de la función softmax para una observación x es un vector probabilidad p con la probabilidad de cada etiqueta. Para todo el lote es una matriz P donde cada fila es la probabilidad por observación. Más adelante comprobaremos que este gradiente de la capa final cuando se usa softmax unido a la función de coste de entropia cruzada resulta algo tan sintético como:

\[\frac{\partial C}{\partial Z ^L} = P - Y\]

Siendo \(P = softmax(Z^L)\) y \(Y\) una matriz one-hot de las etiquetas reales.

Por tanto los procesos de back-propagación siempre tienen la misma estructura:

  • Partiendo del gradiente de coste con respecto a la ponderación lineal de su capa (\(\frac{\partial C}{\partial Z ^l}\)), obtener o propagar el de la capa anterior (\(\frac{\partial C}{\partial Z ^{l-1}}\)).

\[\frac{\partial C}{\partial Z ^l} \rightarrow \frac{\partial C}{\partial Z ^{l-1}}\]
  • Obtiener el gradiente del coste con respecto a los pesos y bias de su capa, siempre y cuando sean capas entrenables:

\[\frac{\partial C}{\partial W ^l}; \frac{\partial C}{\partial B ^l}\]
_images/Esquema_FWDBAC.png

Justificación de la retropropagación en convolución#

Para realizar esta justificación suponemos una única observación y un único canal (n = c = co = ci = 1).

Además para simplificar las fórmulas al máximo se toma en \(X_{n-1}\) h=b=4 en la entrada a la convolución y debido al filtro aplicado la salida se reduce en \(X_n\) a h=b=2 (por no usar relleno a ceros). Además el tamaño del filtro es f=3.

Dada una capa convolucional \(l\) cuya entrada suponemos definida por una matriz

\[\begin{split}\begin{bmatrix} x_{11} & x_{12} & x_{13} & x_{14} \\ x_{21} & x_{22} & x_{23} & x_{24} \\ x_{31} & x_{32} & x_{33} & x_{34} \\ x_{41} & x_{42} & x_{43} & x_{44} \\ \end{bmatrix} \end{split}\]

Esta matriz puede estar representando los datos de una observación de la capa de entrada a la red neuronal o la salida de la activación de la capa \(l-1\), que en su caso vendrá notada por \(A^{l-1}\).

En la capa \(l\) tenemos definida una convolución simple (suponemos un canal de entrada y uno de salida) que requiere una matriz de pesos o filtro \(W^l\) y una matriz de bias \(B^l\), suponemos un filtro 3x3 y dado que suponemos un sólo canal el bias es una matriz unidimensional:

\[\begin{split}W^l = \begin{bmatrix} w_{11} & w_{12} & w_{13} \\ w_{21} & w_{22} & w_{23} \\ w_{31} & w_{32} & w_{33} \\ \end{bmatrix} \end{split}\]
\[B^l = \begin{bmatrix} b \end{bmatrix} \]

Por comodidad sólo se usa superindice con el orden de la capa en las matrices, pero en los elementos de las matrices no se utiliza superíndice, si no sólo el subindice correspondiente. Así se nota la matriz \(W^l\) y sus elementos \(w_{ij}\) en lugar de \(w^l_{ij}\) por no complicar más la notación.

La matriz \(Z^l = W^l \otimes A^{l-1} \oplus B^l\), resultado de aplicar el filtro convolucional y sumar el bias, viene dada por:

\[\begin{split}\begin{bmatrix} x_{11} \cdot w_{11} + x_{12} \cdot w_{12}+x_{13} \cdot w_{13}+x_{21} \cdot w_{21}+x_{22} \cdot w_{22}+x_{23} \cdot w_{23}+x_{31} \cdot w_{31}+x_{32} \cdot w_{32}+x_{33} \cdot w_{33}+b & x_{12} \cdot w_{11} + x_{13} \cdot w_{12} + x_{14} \cdot w_{13} + x_{22} \cdot w_{21} + x_{23} \cdot w_{22} + x_{24} \cdot w_{23} + x_{32} \cdot w_{31} + x_{33} \cdot w_{32} + x_{34} \cdot w_{33} + b \\ x_{21} \cdot w_{11} + x_{22} \cdot w_{12} + x_{23} \cdot w_{13} + x_{31} \cdot w_{21} + x_{32} \cdot w_{22} + x_{33} \cdot w_{23} + x_{41} \cdot w_{31} + x_{42} \cdot w_{32} + x_{43} \cdot w_{33} + b & x_{22} \cdot w_{11} + x_{23} \cdot w_{12} + x_{24} \cdot w_{13} + x_{32} \cdot w_{21} + x_{33} \cdot w_{22} + x_{34} \cdot w_{23} + x_{42} \cdot w_{31} + x_{43} \cdot w_{32} + x_{44} \cdot w_{33} + b \end{bmatrix} \end{split}\]

Si se aplica la función de activación resulta \(A^l = f(Z^l) = \begin{bmatrix} a_{11} & a_{12} \\ a_{21} & a_{22} \end{bmatrix} \)

En el proceso de retropropagación, al procesar la capa \(l+1\) se habrá retropropagado una matriz con el gradiente del coste con relación a la ponderación lineal \(Z^l\), con igual dimensión que \(Z^l\) y \(A^l\) y que se nota:

\[\begin{split}\frac{\partial C}{\partial Z^l} = \begin{bmatrix} \delta_{11} & \delta_{12} \\ \delta_{21} & \delta_{22} \end{bmatrix} = \begin{bmatrix} \frac{\partial C}{\partial z_{11}} & \frac{\partial C}{\partial z_{12}} \\ \frac{\partial C}{\partial z_{21}} & \frac{\partial C}{\partial z_{22}} \end{bmatrix} \end{split}\]

El objetivo del proceso de retropropagación de la capa \(l\) es obtener el gradiente del coste en relación a los pesos \(W^l\) y del bias \(B^l\), estos gradientes tiene igual dimensión que \(W^l\) y \(B^l\). Se observa en la notación que es una matriz jacobiana con las derivadas primeras del coste con respecto a cada peso:

\[\begin{split}\frac{\partial C}{\partial W^l} = \begin{bmatrix} \frac{\partial C}{\partial w_{11}} & \frac{\partial C}{\partial w_{12}} & \frac{\partial C}{\partial w_{13}} \\ \frac{\partial C}{\partial w_{21}} & \frac{\partial C}{\partial w_{22}} & \frac{\partial C}{\partial w_{23}} \\ \frac{\partial C}{\partial w_{31}} & \frac{\partial C}{\partial w_{32}} & \frac{\partial C}{\partial w_{33}} \\ \end{bmatrix} \end{split}\]
\[\frac{\partial C}{\partial B^l} = \begin{bmatrix} \frac{\partial C}{\partial b} \end{bmatrix} \]

Por la regla de la cadena, el término \( \frac{\partial C}{\partial w_{11}}\) se obtiene:

\[ \frac{\partial C}{\partial w_{11}} = \frac{\partial C}{\partial a_{11}} \frac{\partial a_{11}}{\partial w_{11}} + \frac{\partial C}{\partial a_{12}} \frac{\partial a_{12}}{\partial w_{11}} + \frac{\partial C}{\partial a_{21}} \frac{\partial a_{21}}{\partial w_{11}} + \frac{\partial C}{\partial a_{22}} \frac{\partial a_{22}}{\partial w_{11}} \]

Como \(\frac{\partial C}{\partial a_{ij}} = \delta_{ij}\), la matriz \(\frac{\partial C}{\partial W^l}\), dónde el resultado anterior aparece en el término 1,1 es:

\[\begin{split}\begin{bmatrix} \delta_{11} \cdot x_{11} + \delta_{12} \cdot x_{12} + \delta_{21} \cdot x_{21} + \delta_{22} \cdot x_{22} & \delta_{11} \cdot x_{12} + \delta_{12} \cdot x_{13} + \delta_{21} \cdot x_{22} + \delta_{22} \cdot x_{23} & \delta_{11} \cdot x_{13} + \delta_{12} \cdot x_{14} + \delta_{21} \cdot x_{23} + \delta_{22} \cdot x_{24} \\ \delta_{11} \cdot x_{21} + \delta_{12} \cdot x_{22} + \delta_{21} \cdot x_{31} + \delta_{22} \cdot x_{32} & \delta_{11} \cdot x_{22} + \delta_{12} \cdot x_{23} + \delta_{21} \cdot x_{32} + \delta_{22} \cdot x_{33} & \delta_{11} \cdot x_{23} + \delta_{12} \cdot x_{24} + \delta_{21} \cdot x_{33} + \delta_{22} \cdot x_{34} \\ \delta_{11} \cdot x_{31} + \delta_{12} \cdot x_{32} + \delta_{21} \cdot x_{41} + \delta_{22} \cdot x_{42} & \delta_{11} \cdot x_{32} + \delta_{12} \cdot x_{33} + \delta_{21} \cdot x_{42} + \delta_{22} \cdot x_{43} & \delta_{11} \cdot x_{33} + \delta_{12} \cdot x_{34} + \delta_{21} \cdot x_{43} + \delta_{22} \cdot x_{44} \end{bmatrix} \end{split}\]

El gradiente del coste con respecto a \(B^l\), usando la regla de la cadena resulta:

\[ \frac{\partial C}{\partial b} = \frac{\partial C}{\partial z_{11}} \frac{\partial z_{11}}{\partial b} + \frac{\partial C}{\partial z_{12}} \frac{\partial z_{12}}{\partial b} + \frac{\partial C}{\partial z_{21}} \frac{\partial z_{21}}{\partial b} + \frac{\partial C}{\partial z_{22}} \frac{\partial z_{22}}{\partial b} \]

Y como \(\frac{\partial a_{ij}}{\partial b} = 1\) se tiene:

\[ \frac{\partial C}{\partial B^l} = \delta_{11} + \delta_{12} + \delta_{21} + \delta_{22} \]

Se puede observar que la matriz \(\frac{\partial C}{\partial W^l}\) se construye recorriendo uno a uno los elementos de \(\frac{\partial C}{\partial Z^l}\) a la vez que nos movemos usando las dimensiones del filtro \(W^l\) sobre la matriz \(A^{l-1}\) y los subproductos escalares entre \(\delta_{ij}\) y las submatrices (3x3) se van acumulando en \(\frac{\partial C}{\partial W^l}\). El primer subproducto se ve en la siguiente imagen:

_images/conv_gradW.png

Y el siguiente subproducto sería:

_images/conv_gradW2.png

El código programado aparece en la función backward de la clase conv2DLayer (ajustando la notación):

             dW[co, :] += Aprev[i, :, _h:_h+f, _b:_b+f] * Delta[i, co, _h,_b]

La última operación del proceso de retropropagación de la capa \(l\) consiste en obtener la matriz jacobiana

\[ \frac{\partial C}{\partial Z^{l-1}} \]

Que es de la misma dimensión que \(A^{l-1}\), en este ejemplo (4x4), siendo el elemento \((i,j)\) el resultado de obtener

\[ \frac{\partial C}{\partial x_{ij}} \]

Por la regla de la cadena, el término \( \frac{\partial C}{\partial x_{11}}\) se obtiene:

\[ \frac{\partial C}{\partial x_{11}} = \frac{\partial C}{\partial z_{11}} \frac{\partial z_{11}}{\partial x_{11}} + \frac{\partial C}{\partial z_{12}} \frac{\partial z_{12}}{\partial x_{11}} + \frac{\partial C}{\partial z_{21}} \frac{\partial z_{21}}{\partial x_{11}} + \frac{\partial C}{\partial z_{22}} \frac{\partial z_{22}}{\partial x_{11}} = \delta_{11} \cdot w_{11} \]

La matriz jacobiana completa

\[\begin{split}\frac{\partial C}{\partial Z^{l-1}} = \begin{bmatrix} \frac{\partial C}{\partial x_{11}} & \frac{\partial C}{\partial x_{12}} & \frac{\partial C}{\partial x_{13}} & \frac{\partial C}{\partial x_{14}}\\ \frac{\partial C}{\partial x_{21}} & \frac{\partial C}{\partial x_{22}} & \frac{\partial C}{\partial x_{23}} & \frac{\partial C}{\partial x_{24}} \\ \frac{\partial C}{\partial x_{31}} & \frac{\partial C}{\partial x_{32}} & \frac{\partial C}{\partial x_{33}} & \frac{\partial C}{\partial x_{34}}\\ \frac{\partial C}{\partial x_{41}} & \frac{\partial C}{\partial x_{42}} & \frac{\partial C}{\partial x_{43}} & \frac{\partial C}{\partial x_{44}}\\ \end{bmatrix} \end{split}\]

Entonces \(\frac{\partial C}{\partial Z^{l-1}}\) resulta:

\[\begin{split}\begin{bmatrix} w_{11} \cdot \delta_{11} & w_{12} \cdot \delta_{11} + w_{11} \cdot \delta_{12} & w_{13} \cdot \delta_{11} + w_{12} \cdot \delta_{12} & w_{13} \cdot \delta_{12} \\ w_{21} \cdot \delta_{11} + w_{11} \cdot \delta_{21} & w_{22} \cdot \delta_{11} + w_{21} \cdot \delta_{12} + w_{12} \cdot \delta_{21} + w_{11} \cdot \delta_{22} & w_{23} \cdot \delta_{11} + w_{22} \cdot \delta_{12} + w_{13} \cdot \delta_{21} + w_{12} \cdot \delta_{22} & w_{23} \cdot \delta_{12} + w_{13} \cdot \delta_{22} \\ w_{31} \cdot \delta_{11} + w_{21} \cdot \delta_{22} & w_{32} \cdot \delta_{11} + w_{31} \cdot \delta_{12} + w_{22} \cdot \delta_{21} + w_{21} \cdot \delta_{22} & w_{33} \cdot \delta_{11} + w_{32} \cdot \delta_{12} + w_{23} \cdot \delta_{21} + w_{22} \cdot \delta_{22} & w_{33} \cdot \delta_{12} + w_{23} \cdot \delta_{22} \\ w_{31} \cdot \delta_{21} & w_{32} \cdot \delta_{21} + w_{31} \cdot \delta_{22} & w_{33} \cdot \delta_{21} + w_{32} \cdot \delta_{22} & w_{33} \cdot \delta_{22} \end{bmatrix} \end{split}\]

La forma de obtener esta matriz es acumulando el producto escalar

\[ \delta_{ij} \cdot W^l \]

En la matriz \(\frac{\partial C}{\partial Z^{l-1}}\) cuya dimensión coindicen con \(A^{l-1}\), en este ejemplo 4x4, acumulando el resultado matricial, en este caso 3x3, en la submatriz de acuerdo al movimiento del filtro. El primer subproducto escalar se ve en la siguiente imagen:

_images/conv_gradA.png

Y el siguiente subproducto en:

_images/conv_gradA2.png

El código programado aparece en la función backward de la clase conv2DLayer (ajustando la notación):

             DeltaPrev[i, :, _h:_h+f, _b:_b+f] += self.W[co, :] * Delta[i, co, _h,_b]

El código anterior finaliza concatenado el producto de Hadamard de la derivada primera de la función de activación en \(Z^{l-1}\)

Softmax y cross-entropy#

A la salida de la última capa se aplica la función softmax. Si el vector \(z\) tiene la transformación lineal de la última capa (antes de aplicar la función de activación) la probabilidad de la neurona \(j\) de salida es de acuerdo a la función softmax:

\[p_j = \frac{e^{z_j}}{\sum_j{e^{z_j}}}\]

Finalmente la pérdida se obtiene con la función entropia cruzada que es:

\[- \sum_j {y_j \cdot ln(p_j)}\]

Dónde \(y_j\) es el valor de la etiqueta en formato one-hot (vector con tantos dígitos como posibles etiquetas hay y que lleva todo ceros y un 1 en la posición que corresponde a la etiqueta).

El uso de softmax y cross-entropy a la salida de la última capa (\(L\)) como se indica hace que el gradiente del coste con respecto a la transformación lineal de esa última capa:

\[ \frac{\partial C}{\partial z^l_i} = p_i - y_i\]

Para comprobarlo en un caso sencillo, se supone que una observación que tiene una transformación lineal:

\[z=(z_1, z_2, z_3)\]

Sabiendo que las etiquetas reales de esa observación en formato one-hot viene dado por

\[y=(y_1, y_2, y_3)\]

La probabilidad usando la función softmax es:

\[p=(p_1, p_2, p_3)\]
\[p_1 = \frac{e^{z_1}}{e^{z_1} + e^{z_2} + e^{z_3}}\]
\[p_2 = \frac{e^{z_2}}{e^{z_1} + e^{z_2} + e^{z_3}}\]
\[p_3 = \frac{e^{z_3}}{e^{z_1} + e^{z_2} + e^{z_3}}\]

La función de coste por entropia cruzada es:

\[ C = -y1 \cdot ln(p_1) -y2 \cdot ln(p_2) -y3 \cdot ln(p_3) \]

El gradiente del coste con respecto a \(z\) será el vector

\[\begin{pmatrix} \frac{\partial C}{\partial z_1}, \frac{\partial C}{\partial z_2}, \frac{\partial C}{\partial z_3} \end{pmatrix}\]

Desarrollando la primera componente:

\[\frac{\partial C}{\partial z_1} = -y1 \frac{1}{p_1}\frac{\partial p_1}{\partial z_1} -y2 \frac{1}{p_2}\frac{\partial p_2}{\partial z_1} -y3 \frac{1}{p_3}\frac{\partial p_3}{\partial z_1}\]

Quedando:

\[\frac{\partial C}{\partial z_1} = \frac{-y_1e^{z_2}-y_1e^{z_3}+y_2e^{z_1}+y_3e^{z_1}}{e^{z_1} + e^{z_2} + e^{z_3}}\]

Sumando y restando el término \(y_1e^{z_1}\) la fracción se puede poner

\[\frac{\partial C}{\partial z_1} = \frac{-y_1 (e^{z_1} + e^{z_2} + e^{z_3}) + e^{z_1}(y_1+y_2+y_3)}{e^{z_1} + e^{z_2} + e^{z_3}}\]

Pero como el vector \(y\) es un formato one-hot, se cumple que \(y_1+y_2+y_3=1\) quedado la expresión:

\[\frac{\partial C}{\partial z_1} = -y_1 + \frac{e^{z_1}}{e^{z_1} + e^{z_2} + e^{z_3}} = p_1 - y_1\]

Haciendo lo mismo con las otras 2 derivadas parciales se ve que el vector gradiente se obtiene así:

\[\begin{pmatrix} \frac{\partial C}{\partial z_1}, \frac{\partial C}{\partial z_2}, \frac{\partial C}{\partial z_3} \end{pmatrix} = \begin{pmatrix}p_1 - y_1, & p_2 - y_2, & p_3 - y_3 \end{pmatrix} = p - y\]

Este gradiente es el que aparece en el esquema anterior de LetNet5 identificado como dA8, una matriz de 32 filas y 10 columnas para cada una de las 10 posibles etiquetas. Esta matriz jacobiana es la que arranca el proceso de retro-propagación de los gradientes del coste.

Optimización ADAM#

Al actualizar los pesos y bias con sus gradientes de coste multiplicados por la tasa de entrenamiento (\(\eta\)) en arquitecturas que mezclan capas de diferentes naturaleza surge el inconveniente de que las velocidades de crecimiento de los gradientes no son homogéneas y sería preciso utilizar valores de \(\eta\) diferentes en cada capa.

Como sería muy difícil estimar estos valores en cada capa, hay algoritmos, por ejemplo Root Mean Square Propagation (RMSprop) que utilizan una media móvil que promedia más los valores actuales que los antiguos. A la vez se utiliza un momento de segundo orden para normalizar los elementos del gradiente (pues la raiz cuadrada de de la estimación del momento del segundo orden equivale a la desviación estándar).

ADAM (o estimación adaptativa del momento) añade a RMSprop el cálculo de un momento y es el método más utilizado. Su fórmula es:

\[m_{t+1}=\beta \cdot m_t + (1 - \beta) \bigtriangledown f_i(w_t)\]
\[v_{t+1}=\alpha \cdot v_t + (1 - \alpha) \bigtriangledown f_i(w_t) ^2\]
\[w_{t+1} = w_t - \eta \frac{m_t}{\sqrt{v_t + 1} + \epsilon}\]

Donde \(\epsilon\) es un valor muy próximo a cero (\(10^{-7}\) por ejemplo) para evitar divisiones por cero

Funciones generales#

Hide code cell content
class netFuntion(object):
    def __init__(self, seed=1):
        
        self.random = np.random.RandomState(seed)

        return 
    
    def sigmoid(self, x):
        #return 1.0/(1.0 + np.exp(-x))  ## versión básica con problemas de desbordamiento en valores x<<<0
        #return np.where(x < 0, np.exp(x)/(1.0 + np.exp(x)), 1.0/(1.0 + np.exp(-x)))
        #return 1. / (1. + np.exp(-np.clip(x, -250, 250)))
        from scipy.special import expit
        try:
            return expit(x)  ##Función sigmoidea de scipy; algo más lenta
        except:
            print('Error sigmoid en X=', x)
            return x

    def sigmoid_derivada(self, x):
        return self.sigmoid(x)*(1.0-self.sigmoid(x))

    def tanh(self, x):
        return np.tanh(x)

    def tanh_derivada(self, x):
        return 1.0 - np.tanh(x)**2

    def ReLU(self, x):
        return np.maximum(0, x)

    def ReLU_derivada(self, x):
        return np.where(x <= 0, 0, 1)
    
    def softmax(self, raw_preds):
        '''
        pass raw predictions through softmax activation function
        '''
        try:
            out = np.exp(raw_preds)
            soft = out/np.sum(out) # divide the exponentiated vector by its sum. All values in the output sum to 1.
        except:
            print('ejecutada excepcion en softmax')
            soft = self.stable_softmax(raw_preds)
 
        return soft

    def stable_softmax(self, x):
        z = x - np.max(x, axis=-1, keepdims=True)
        numerator = np.exp(z)
        denominator = np.sum(numerator, axis=-1, keepdims=True)
        softmax = numerator / denominator
        return softmax

    def categoricalCrossEntropy(self, probs, label):
        '''
        Calcula la pérdida de entropía cruzada categórica de las predicciones
        Multiplica la etiqueta de salida deseada por el registro de la predicción, y se suman todos los valores en el vector        
        Se evita calcular el logaritmo de cero sumando un epsilon = 1e-7
        '''
        
        entr = 0
        for v_pro, v_lab in zip(probs, label):
            for _pro, _lab in zip(v_pro, v_lab):
                entr -= _lab * np.log(_pro + 1e-7)
        
        return entr
        #return  -np.sum(label * np.log(probs + 1e-7))

    def listToString(self, list):
        cadena = ''
        for ele in list:
            cadena = cadena + ele + '; '
        return cadena
    
    def validarOpciones(self, valOpcion, tipOpcion):
        if tipOpcion == 'ACTIVACION':
            lstOpciones = ['RELU', 'SIGMOID', 'TANH', 'IDENTITY', 'SOFTMAX']
            literal = 'Tipo de activación'
        elif tipOpcion == 'OPTIMIZADOR':
            lstOpciones = ['ADAM', 'GRAD']
            literal = 'Optimizador'
        else: ## 'PERDIDA'
            lstOpciones = ['CROSSENTROPY', 'ERRORCUADRATICO']
            literal = 'Función de pérdida'
        
        valOpcion = valOpcion.upper()
        
        assert valOpcion in lstOpciones, literal + ' <' + valOpcion + '> no contemplada. Usar: ' + self.listToString(lstOpciones)
        
        return valOpcion
        
    
    def activacionPrima(self, tipActiva, A, B):
        if (tipActiva == 'RELU'):
            A[B<0] = 0
            return A
        elif (tipActiva == 'SIGMOID'):
            return A * self.sigmoid_derivada(B)
        elif (tipActiva == 'TANH'):
            return A * self.tanh_derivada(B)
        else:  ## No cambia si es Identity y Softmax (porque sólo se usa en la última capa)
            return A

    def activacion(self, tipActiva, X):
        if (tipActiva == 'RELU'):
            return self.ReLU(X)
        elif (tipActiva == 'SIGMOID'):
            return self.sigmoid(X)
        elif (tipActiva == 'TANH'):
            return self.tanh(X)
        elif (tipActiva == 'SOFTMAX'):
            return np.asarray([self.softmax(sp) for sp in X])
        else:
            return X

    def onehot(self, y, n_classes):
        """Convierte las etiquetas en una representación de vectores de base canónica R^K siendo K el total de etiquetas
        Parameters
        ------------
        y : array, shape = [n_samples]
           Valores objetivo.
        Returns
        -----------
        onehot : array, shape = (n_samples, n_labels)
        """
        if n_classes == 2:
            onehot = np.zeros((1, y.shape[0]))
            for idx, val in enumerate(y.astype(int)):
                ilabel = self.Clases_y.tolist().index(val)
                onehot[0, idx] = int(ilabel)
        else:
            onehot = np.zeros((n_classes, y.shape[0]))
            for idx, val in enumerate(y.astype(int)):
                ilabel = self.Clases_y.tolist().index(val)
                onehot[ilabel, idx] = 1.
        return onehot.T

Clase para una capa densa#

Hide code cell content
class linealLayer(netFuntion):
    def __init__(self, neurInp, neurOut, tipActiva):
        
        super().__init__()
        
        self.neurInp = neurInp
        self.neurOut = neurOut
        self.tipActiva = self.validarOpciones(tipActiva, 'ACTIVACION')
        self.W = self.random.standard_normal(size=(neurOut, neurInp)) * 0.01
        self.B = np.zeros((neurOut))
        self.pesos=True
        
        return 
    
    def forward(self,  X): 
        '''
        Calcula la transformación lineal por la matriz de pesos W
        Hay c_i canales de entrada. La dimensión de X ha de ser c_i mapas 

        Argumentos:
        p -- Tamaño de Pool
        X -- Salida de la activación de la capa anterior, numpy array de dimension (n, c_i, h, b) asumiendo c_i canales de entrada

        Retornos:
        H -- Salida agrupada con dimensión (c_i, h/p, b/p)
        '''
        assert len(X.shape)==2 or len(X.shape)==4, "La matriz de entrada X deben ser listas de 2 o 4 dimensiones"

        # Si la entrada es un mapa que ha devuelto una convolución se aplanan las dimensiones canal, alto, ancho en una
        if len(X.shape)==4:
            X = np.reshape(X,(X.shape[0], int(X.shape[1]*X.shape[2]*X.shape[3])))

        #print('forwardLineal - X.shape=', X.shape)
        # Se recogen las dimensiones de los mapas de entrada
        (nf_x, nc_x) = X.shape
        (nf_w, nc_w) = self.W.shape
        (nc_b,) = self.B.shape

        assert nc_x == nc_w, 'El numero de columnas de las matrices X y W deben ser iguales'
        assert nc_b == nf_w, 'La dimensión del Bias y el nº de filas de W debe ser iguales nc_b='+ str(nc_b) + '_nf_w=' + str(nf_w)

        Z = np.dot(X, np.transpose(self.W)) + np.transpose(self.B)
        A = self.activacion(self.tipActiva, Z)
        return Z, A
    
    def backward(self, dA, Aprev, l):
        '''
        El cálculo hacia atrás para una capa Densa

         Argumentos:
         dA: gradiente del costo con respecto a la salida de la capa densa, 
             matriz numpy de forma (n_H, n_W) asumiendo canales = 1
         Apre -- Salida de la activación de la capa previa
         W -- Matriz de pesos de la capa actual
         l: identifica el nº de capa. Si l==0 es la primera capa oculta

         Retornos:
         dAprev: gradiente del costo con respecto a la entrada de la capa anterior. Gradiente que se propaga
         dW: gradiente de la matriz de peso de la capa actual
         dB: gradiente de la matriz de bias de la capa actual
        '''

        if len(Aprev.shape)==4:
            Aprev = np.reshape(Aprev,(Aprev.shape[0], int(Aprev.shape[1]*Aprev.shape[2]*Aprev.shape[3])))

        dW = np.dot(dA.T, Aprev)
        dB = np.sum(dA, axis=0)

        ## Propagación del delta dA sobre la capa previa. En la primera capa no tiene sentido la propagación
        if l==0:
            dAprev = None
        else:
            dAprev = np.dot(dA, self.W)

        return dAprev, dW, dB

Clase para una capa convolucional#

Hide code cell content
class conv2DLayer(netFuntion):
    def __init__(self, canalInp, canalOut, fSize, tipActiva):
        
        super().__init__()
        
        self.canalInp = canalInp
        self.canalOut = canalOut
        self.fSize = fSize
        self.tipActiva = self.validarOpciones(tipActiva, 'ACTIVACION')
        stddev = 1.0/np.sqrt(np.prod((canalOut, canalInp, fSize, fSize)))
        self.W = self.random.normal(loc = 0, scale = stddev, size = (canalOut, canalInp, fSize, fSize))
        self.B = np.zeros((canalOut))
        self.pesos=True
        
        return 
    
    def forward(self, X): 
        '''
        Calcula una convolución suponiendo que no hay padding (relleno a ceros) y strike 1 (sin salto)
        El filtro es dimension f x f
        Por lo tanto el mapa de salida tiene una dimensión que se reduce en f-1 con respecto al de entrada
        Hay c_i canales de entrada y c_o canales de salida. La dimensión de X ha de ser c_i mapas 
        Y los pesos han de ser c_o lista de c_i matrices de dimension f

        Argumentos:
        W -- Pesos, numpy array con dimension (c_o, c_i, f, f), siendo un filtro individual (f, f) 
        X -- Salida de la activación de la capa anterior, numpy array de dimension (n, c_i, h, b) asumiendo c_i canales de entrada

        Retornos:
        A -- Salida convolucionada, numpy array de tamaño (n, c_o, h, b), se asume salto 1 y siempre relleno a ceros
        '''

        #W = np.asarray(W)
        assert len(X.shape)==4, "La matriz de entrada X deben ser listas de 4 dimensiones. X.shape=" + str(X.shape)
        assert len(self.W.shape)==4, "La matriz de pesos W deben ser listas de 4 dimensiones. W.shape=" + str(W.shape)
        # Se recogen las dimensiones de los mapas de entrada
        (n, c_i, h, b) = X.shape
        # Se recogen las dimensiones de los pesos 
        (c_o, _c_i, f, f) = self.W.shape
        # Se recoge las dimensiones de los Bias
        assert len(self.B.shape)==1 and self.B.shape[0]==c_o, "Bias debe ser de dimensión y con nº igual a canal salida="  + str(self.B.shape)

        assert _c_i == c_i, "El nº de canales de entrada en X debe ser igual que al nº canales de entrada en W"

        # Inicializa la salida H con ceros
        Z = np.zeros((n, c_o, h-(f-1), b-(f-1)))

        # Bucle sobre los ejes vertical(h) y la horizontal(b) del mapa de salida
        #for i in range(n):
        #    for co in range(c_o):
        #        for ci in range(c_i):
        #            for _h in range(h-(f-1)):
        #                for _b in range(b-(f-1)):
        #                    x_slice = X[i, ci, _h:_h+f, _b:_b+f]
        #                    Z[i, co, _h,_b] += np.sum(x_slice * self.W[co, ci]) + self.B[co]

        # Bucle sobre los ejes vertical(h) y la horizontal(b) del mapa de salida
        for co in range(c_o):
            for ci in range(c_i):
                for _h in range(h-(f-1)):
                    for _b in range(b-(f-1)):
                        x_slice = X[:, ci, _h:_h+f, _b:_b+f]
                        Z[:, co, _h,_b] += np.tensordot(x_slice, self.W[co, ci]) + self.B[co]
        
        A = self.activacion(self.tipActiva, Z)
        return Z, A

    def backward(self, dA, Aprev, l):
        '''
        El cálculo hacia atrás para una función de convolución

         Argumentos:
         dA: gradiente del costo con respecto a la salida de la capa de conv (A), 
             matriz numpy de forma (n_H, n_W) asumiendo canales = 1
         Aprev: Activación de la capa previa
         W, B: matrices de peso y bias de la capa actual
         shape_A : Tamaño de la activación en la capa actual

         Retornos:
         dAprev: gradiente del costo con respecto a la entrada de la capa anterior
         dW: gradiente del costo con respecto a los pesos del filtro de la capa actual 
         dB: gradiente dle costo con respecto a los bias del filgro de la capa actual
         l: identifica el nº de capa. Si l==0 es la primera capa oculta
        '''
        
        #W = np.asarray(W)
        assert len(dA.shape)==4, "La matriz de entrada dA deben ser listas de 4 dimensiones. dA.shape=" + str(dA.shape)
        assert len(Aprev.shape)==4, "La matriz de entrada Aprev deben ser listas de 4 dimensiones. Aprev.shape=" + str(Aprev.shape)
        assert len(self.W.shape)==4, "La matriz de pesos W deben ser listas de 4 dimensiones="  + str(self.W.shape)
        
        # Se recogen las dimensiones de los mapas de entrada
        (n, co, h, b) = dA.shape
        (n2, c_i, h2, b2) = Aprev.shape
        # Se recogen las dimensiones de los pesos 
        (c_o, _c_i, f, f) = self.W.shape
        assert _c_i == c_i, "El nº de canales de entrada en Aprev debe ser igual que al nº canales de entrada en W"

        assert n==n2, "El nº de observaciones en Aprev y dA debe ser igual n=" + str(n) + " n2=" + str(n2)
        assert h2==h+(f-1), "La altura del mapa previo y actual deben diferir en el filtro. h=" + str(h) + " h2=" + str(h2)
        assert b2==b+(f-1), "La anchua del mapa previo y actual deben diferir en el filtro. b=" + str(h) + " b2=" + str(h2)

        # Initializing dAprev, dW with the correct shapes
        dAprev = np.zeros_like(Aprev)
        dW = np.zeros_like(self.W)
        dB = np.zeros_like(self.B)

        # Inicializamos dAprev con la dimensión de Aprev
        dAprev = np.zeros_like(Aprev)

        # Bucle sobre los ejes vertical(h) y la horizontal(b) de dA
        for i in range(n):
            for _h in range(h):
                for _b in range(b):
                    for co in range(c_o):
                            ##Full Convolution 180º rotate Filter
                            dAprev[i, :, _h:_h+f, _b:_b+f] += self.W[co, :] * dA[i, co, _h,_b]
                            ##Convolution
                            dW[co, :] += Aprev[i, :, _h:_h+f, _b:_b+f] * dA[i, co, _h,_b]
                            dB[co] += dA[i, co, _h, _b]
        
        return dAprev, dW, dB

Clase para una capa de agrupación por máximo (max-pooling)#

Hide code cell content
class maxPooling(netFuntion):
    def __init__(self, pSize, tipActiva):
        
        super().__init__()
        
        self.pSize = pSize
        self.tipActiva = self.validarOpciones(tipActiva, 'ACTIVACION')
        self.pesos=False
        
        return 

    def forward(self, X): 
        '''
        Calcula la agrupación por pool, suponiendo un tamaño de pool p
        Hay c_i canales de entrada. La dimensión de X ha de ser c_i mapas 

        Argumentos:
        p -- Tamaño de Pool
        X -- Salida de la activación de la capa anterior, numpy array de dimension (n, c_i, h, b) asumiendo c_i canales de entrada

        Retornos:
        H -- Salida agrupada con dimensión (c_i, h/p, b/p)
        '''
        assert len(X.shape)==4, "La matriz de entrada X deben ser listas de 4 dimensiones"
        # Se recogen las dimensiones de los mapas de entrada
        (n, c_i, h, b) = X.shape

        ## Se calcula la nueva dimensión una vez aplicado el pool
        n_h = int(h/self.pSize)
        n_b = int(b/self.pSize)
        # Inicializa la salida H con ceros
        H = np.zeros((n, c_i, n_h, n_b))

        # Bucle sobre los ejes vertical(h) y la horizontal(b) del mapa de salida
        for i in range(n):
            for ci in range(c_i):
                for _h in range(n_h):
                    for _b in range(n_b):
                        x_slice = X[i, ci, self.pSize*_h:self.pSize*(_h+1), self.pSize*_b:self.pSize*(_b+1)]
                        H[i, ci, _h,_b] = np.max(x_slice)

        return H, self.activacion(self.tipActiva, H)
    
    def backward(self, dA, Aprev, l):
        '''
        El cálculo hacia atrás para una función de convolución

         Argumentos:
         dA: gradiente del costo con respecto a la salida de la capa de pool (A), 
             matriz numpy de forma (n_H, n_W) asumiendo canales = 1
         Aprev, A: Activación de la capa previa y de la capa actual. Sus dimensiones permiten calcular el pooling

         Retornos:
         dAprev: gradiente del costo con respecto a la entrada de la capa de conv (Aprev), 
             matriz numpy de forma (n_H_prev, n_W_prev) asumiendo canales = 1
         l: identifica el nº de capa. Si l==0 es la primera capa oculta
        '''
        ##print("backwardPool - dA.shape=" + str(dA.shape))
        ##print("backwardPool - Aprev.shape=" + str(Aprev.shape))
        assert len(Aprev.shape)==4, "La matriz de entrada Aprev deben ser listas de 4 dimensiones. Aprev.shape=" + str(Aprev.shape)

        assert len(dA.shape)==4, "La matriz de entrada dA deben ser listas de 4 dimensiones. dA.shape=" + str(dA.shape)

        # Se recoge las dimensiones del gradiente del costo con respecto a la salida, dA
        (n, c_o, h, b) = dA.shape

        (n2, c_o2, h2, b2) = Aprev.shape
        assert n==n2 and c_o==c_o2, "El nº de observaciones y canales en dA y Aprev deben ser iguales. n=" + str(n) + "; n2=" 
                                     #str(n2) + ";c_o=" + str(c_o) + ";c_o2=" + str(c_o2)
        p = int(h2/h)
        p2 = int(b2/b)
        assert p==p2 and p>1 and p==self.pSize, "Padding incorrecto p=" + str(p) + ";p2=" + str(p2) + ";self.pSize=" + str(self.pSize)

        # Initializing dX, dW with the correct shapes
        dAprev = np.zeros_like(Aprev)

        # Bucle sobre los ejes vertical(h) y la horizontal(b) del mapa de salida
        for i in range(n):
            for co in range(c_o):
                for _h in range(h):
                    for _b in range(b):
                        mapa = Aprev[i, co, _h*self.pSize:(_h+1)*self.pSize, _b*self.pSize:(_b+1)*self.pSize]
                        idx = np.nanargmax(mapa)
                        (hm, bm) = np.unravel_index(idx, mapa.shape)
                        dAprev[i, co, _h*self.pSize + hm, _b*self.pSize + bm] = dA[i, co, _h, _b]
                        ## Forma alternativa de hacerlo
                        ###dAprev[i, co, _h*p:(_h+1)*p, _b*p:(_b+1)*p] = (1/p**2)*dA[i, co, _h, _b]


        return dAprev, None, None

Clase de aplanado#

Hide code cell content
class flatten(netFuntion):
    def __init__(self, shapeInp, sizeOut):
        
        super().__init__()
        
        assert len(shapeInp) == 3, 'shapeInp debe ser de longitud 3. len(shapeInp)=' + str(len(shapeInp))
        self.shapeInp = shapeInp
        self.sizeOut = sizeOut
        self.tipActiva = 'IDENTITY'
        self.pesos=False
        
        return 

    def forward(self, X): 
        
        H = np.reshape(X,(X.shape[0], self.sizeOut))
        
        return H, H
    
    def backward(self, dA, Aprev, l):
        
        dAprev = np.reshape(dA,(dA.shape[0], self.shapeInp[0], self.shapeInp[1], self.shapeInp[2]))
        
        return dAprev, None, None

Clase red neuronal convolucional#

Hide code cell content
class CNN(netFuntion):
    """MultiLayer LInear NEuron classifier.
    Parametros
    ------------
    eta : float Ratio de aprendizaje (entre 0.0 y 1.0)
    n_iter : int Pasos sobre el conjunto de datos de entrenamiento.
    capas: capas[0] las neuronas de entrada, capas[1] las neuronas de salida
    random_state : int Generador de semillas de números aleatorios para inicializar los pesos.
    shuffle : boolean. Hace una mezcla o barajado aleatorio del conjunto de entrenamiento en los minibatch
    minibatch_size: tamaño del minibatch o subconjuntos en que se divide el conjunto de entrada para el entrenamiento
    hiddenLayers: Neuronas en cada capa oculta. Aquí se excluye la capa de entrada y la de salida que la obtiene
                  automáticamente el proceso fit() de X e y
    Atributos
    -----------
    w_ : Array de dimensión 1 con los pesos después del ajuste.
    cost_ : lista de Suma-de-cuadrados de los valores de la función coste en cada Paso del algoritmo.
    """
    def __init__(self, eta=0.01, epocas=50, shuffle=False, minibatch_size=0, seed=1, optimizador='ADAM', loss='CrossEntropy'):
        
        super().__init__(seed=seed)
        
        self.activarTrazaTensor=False ## Activa o inhibe las trazas de la función trazaTensor
        self.eta = eta
        self.epocas = epocas
        self.shuffle = shuffle
        self.minibatch_size = minibatch_size
        self.optimizador = self.validarOpciones(optimizador, 'OPTIMIZADOR')
        self.loss  = self.validarOpciones(loss, 'PERDIDA')
        self.capas = []
        self.L = 0
        
        if self.activarTrazaTensor == True:
            self.epocas = 1 #Se fuerzan condiciones de mínima actividad
            self.minibatch_size = min(1, self.minibatch_size) #Se fuerzan condiciones de mínima actividad
            f = open('trazaCNN.txt', 'w')
            f.write('Inicio de la traza\n')
            f.close()

    def addLayer(self, layer):
        assert type(layer) in [linealLayer, conv2DLayer, maxPooling, flatten], "Objeto layer no permitido"
        
        self.capas.append(layer)
        self.L += 1
        
        return
    
    def validaLayers(self):
        assert self.L>0, "Debe tener alguna capa definida en la red neuronal"
        if self.capas[-1].tipActiva == 'SOFTMAX':
            assert self.optimizador == 'ADAM' and self.loss == 'CROSSENTROPY', "Optimizidor Adam debe ir con perdida CrossEntropy y en la última capa softmax"
        else:
            assert self.optimizador == 'GRAD' and self.loss == 'ERRORCUADRATICO', "Optimizidor GRAD (gradiente descenso) debe ir con perdida errorCuadratico y en la última capa distinto de softmax"
        
        return


    def fit(self, X, y_inicial):
        """ Ajuste con los datos de entrenamiento.
        Parametros
        ----------
        X : {Tipo array}, shape = [n_ejemplo, n_caracteristicas]
        Vectores de entrenamiento, donde n_ejemplo es el numero de ejemplos y 
        n_caracteristicas es el número de características.
        y : tipo array, shape = [n_ejemplo] Valores Objetivo.
        Retorno
        -------
        self : objecto
        """
        self.minibatch_size = len(X) if (self.minibatch_size == 0) else self.minibatch_size
        self.Clases_y = np.unique(y_inicial)
        y = self.onehot(y_inicial, len(self.Clases_y))
        self.coste = []
        self.validaLayers()
        if self.optimizador == 'ADAM': self.initAdam()
                
        for t in range(self.epocas):
            # iterate over minibatches
            cost=0
            indices = np.arange(X.shape[0])
            if self.shuffle:
                self.random.shuffle(indices)
            lstIndices = tqdm([i for i in range(0, indices.shape[0] - self.minibatch_size + 1, self.minibatch_size)])
            for start_idx in lstIndices:
                batch_idx = indices[start_idx:start_idx + self.minibatch_size]
                self.Z = []  ## lista donde guarda las salidas del sumatorio en cada capa
                self.A = [X[batch_idx]] ## lista para guardar la activación de cada capa. En la capa 1 es la X, en resto f(W(X))
                ### Primero se hace el avance hacia adelante
                self.trazaTensor("IMAGEN INICIAL", self.A[-1])
                for l in range(self.L):
                    sumatorioPesos, activacion = self.capas[l].forward(self.A[l])
                    self.Z.append(sumatorioPesos)  ## Se guarda la lista de sumatorios en A
                    self.A.append(activacion)  ## La activación de esta capa es la entrada de la próxima
                    self.trazaTensor("ACTIVACION[" + str(l) + "]", self.A[-1])
                ## Se obtiene la matriz Delta en la capa L con la tasa de cambio del error
                
                #probs = np.asarray([self.softmax(sp) for sp in sumatorioPesos]) # predict class probabilities with the softmax activation function
                probs = activacion
                errors = probs - y[batch_idx]   ## Los errores y - la activacion de la ultima capa
                if self.loss == 'CROSSENTROPY':
                    loss = self.categoricalCrossEntropy(probs, y[batch_idx]) # categorical cross-entropy loss
                    delta= errors  ## Delta de la función CrossEntropy + SoftMax
                else: ## 'errorCuadratico'
                    loss = (errors**2).sum() / (2.0 * X[batch_idx].shape[0])
                    delta = self.activacionPrima(self.capas[-1].tipActiva, errors, self.Z[-1])
                
                self.trazaTensor("PROBS", probs)
                self.trazaTensor("LOSS", loss)
                
                self.trazaTensor("DELTA ULTIMA", delta)
                self.deltas = [delta] ## delta de la capa última, la capa L

                ### Retropropagación. Calcula desde la capa L-1 hacia atrás la matriz Delta 
                ## propagando la tasa de cambio del error obtenido en la capa L
                for l in reversed(range(self.L)):
                    self.backwardProceso(l, t+1)
                    
                cost = loss / X[batch_idx].shape[0]
                lstIndices.set_description("Época: %d Coste: %.4f" % (t, cost))
                self.coste.append(cost)
                if self.activarTrazaTensor == True:
                    return ## Se fuerzan condiciones de mínima actividad
                
        return

    def backwardProceso(self, l, t):
        dA, dW, dB = self.capas[l].backward(dA=self.deltas[-1], Aprev=self.A[l], l=l)
        self.trazaTensor("dA[" + str(l) + "]", dA, nDecimal = 4)
        self.trazaTensor("dW[" + str(l) + "]", dW, nDecimal = 4)
        self.trazaTensor("dB[" + str(l) + "]", dB, nDecimal = 4)
        if l>0:  ## Se añade la activación derivada de la capa anterior para completar la retro-progración de dA
            dA = self.capas[l-1].activacionPrima(self.capas[l-1].tipActiva, dA, self.Z[l-1]) ## self.Z[l-1] es Zprev
        self.deltas.append(dA)

        if self.capas[l].pesos==True:  ## La capa tiene pesos (W y B) que actualizar
            if self.optimizador == 'ADAM': ## Optimización por el algorimo de Adam
                beta1=0.95
                beta2=0.99
                epsilon=1e-7
                self.capas[l].W_moment = beta1*self.capas[l].W_moment + (1-beta1)*dW/self.minibatch_size
                self.capas[l].W_RMS = beta2*self.capas[l].W_RMS + (1-beta2)*(dW/self.minibatch_size)**2
                self.capas[l].B_moment = beta1*self.capas[l].B_moment + (1-beta1)*dB/self.minibatch_size
                self.capas[l].B_RMS = beta2*self.capas[l].B_RMS + (1-beta2)*(dB/self.minibatch_size)**2
                self.capas[l].W -= self.eta * self.capas[l].W_moment/(np.sqrt(self.capas[l].W_RMS)+epsilon) 
                self.capas[l].B -= self.eta * self.capas[l].B_moment/(np.sqrt(self.capas[l].B_RMS)+epsilon)
            else:  ## Gradiente Descenso estándar
                self.capas[l].W -= self.eta * dW
                self.capas[l].B -= self.eta * dB
            self.trazaTensor("UPDATED W[" + str(l) + "]", self.capas[l].W, nDecimal = 4)
            self.trazaTensor("UPDATED B[" + str(l) + "]", self.capas[l].B, nDecimal = 4)

    def initAdam(self):
        for l in range(self.L):
            if self.capas[l].pesos==True:  ## La capa tiene pesos (W y B) que actualizar
                self.capas[l].W_moment = np.zeros_like(self.capas[l].W) 
                self.capas[l].W_RMS = np.zeros_like(self.capas[l].W) 
                self.capas[l].B_moment = np.zeros_like(self.capas[l].B) 
                self.capas[l].B_RMS = np.zeros_like(self.capas[l].B) 
        
        return
    
    def predict(self, X):
        """Return class label after unit step"""
        # Se aplica un forward a través de todas las capas
        A_=X
        for l in range(self.L):
            Z, A_ = self.capas[l].forward(A_)

        i_labels = np.argmax(A_, axis=1)
        y_pred = [self.Clases_y[i] for i in i_labels] 
        
        return np.array(y_pred)
    
    def predict_proba(self, X):
        """Return la probabilidad de cada clase"""
        # Se aplica un forward a través de todas las capas
        A_=X
        for l in range(self.L):
            Z, A_ = self.capas[l].forward(A_)
        
        probs = np.asarray([self.softmax(sp) for sp in Z])
        
        return probs
    def accuracy(self, ypred, yok):
        
        assert len(ypred)==len(yok) and len(ypred.shape)==1 and len(yok.shape)==1, "ypre, yok no son array 1D de igual longitud. ypred=" + str(ypred.shape) + "yok=" + str(yok.shape) 

        return np.sum(ypred==yok)/len(ypred)   
    
    def writeModel(self, fileName):
        W = []
        B = []
        for l in range(self.L):
            if self.capas[l].pesos==True:  ## La capa tiene pesos (W y B) que actualizar
                W.append(self.capas[l].W)
                B.append(self.capas[l].B)
        
        with open(fileName, 'wb') as file:
            pickle.dump([W,B], file)
        
        return

    def readModel(self, fileName):
        with open(fileName, 'rb') as file:
            [W,B] = pickle.load(file)

        i=0
        for l in range(self.L):
            if self.capas[l].pesos==True:  ## La capa tiene pesos (W y B) que actualizar
                self.capas[l].W = W[i]
                self.capas[l].B = B[i]
                i+=1
        
        return

    def trazaTensor(self, literal, T, nDecimal = 2):
        if self.activarTrazaTensor == False:
            return
        
        f = open('trazaCNN.txt', 'a')
        
        f.write(literal + '\n')
        
        if not type(T)  in [np.ndarray, np.float64, np.float32]:
            f.write(str(T))
            f.close()
            return
        
        n = len(T.shape)
        if (n==4):
            for i in range(T.shape[0]):
                for j in range(T.shape[1]):
                    for k in range(T.shape[2]):
                        f.write("[")
                        for m in range(T.shape[3]):
                            f.write(str(round(T[i][j][k][m], nDecimal)) + " ")
                        f.write("] \n")
                    f.write("-----------------------------------------\n")
        elif (n==3):
            for i in range(T.shape[0]):
                for j in range(T.shape[1]):
                    f.write("[")
                    for k in range(T.shape[2]):
                        f.write(str(round(T[i][j][k], nDecimal)) + " ")
                    f.write("] \n")
                f.write("-----------------------------------------\n")
        elif (n==2):
            for i in range(T.shape[0]):
                f.write("[")
                for j in range(T.shape[1]):
                    f.write(str(round(T[i][j], nDecimal)) + " ")
                f.write("] \n")
        elif (n==1):
            f.write("[")
            for i in range(T.shape[0]):
                f.write(str(round(T[i], nDecimal)) + " ")
            f.write("] \n")
        else:
            f.write(str(T))
        
        f.write("==========================================\n")
        f.close()
        return

Prueba Integral con la arquitectura LeNet-5 y el conjunto MNIST#

Se implementa la arquitectura LeNet-5 (LeCun et al.; 1998)

_images/LeNet5.png
import torch
from torchvision import datasets, transforms
import torchvision.transforms.functional as fn

# Download and load the test data
w_dataset = datasets.MNIST('data', download=True, train=False, transform=transforms.ToTensor())
dataResized = fn.resize(w_dataset.data, 32,antialias=True)
dataCifar = dataResized.data.numpy()
dataCifar.shape
(10000, 32, 32)
## Se añade una dimensión para el canal
nTotal = 2000
X = np.empty((nTotal, dataCifar.shape[1], dataCifar.shape[2]), dtype=np.float32)
#X = np.empty(dataCifar.shape)


## Se crea la matriz de labels
y = []
#for i in range(w_dataset.data.size()[0]):
for i in range(nTotal):
    _x, t = w_dataset.__getitem__(i)
    y.append(t)
    X[i] = dataCifar[i]
y = np.asarray(y)
X = np.asarray([ [_x] for _x in X ], dtype=np.float32)
print('X.shape=', X.shape)
np.unique(y)
X.shape= (2000, 1, 32, 32)
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])

Se define el modelo#

model = CNN(minibatch_size=32, epocas=1, eta=0.01, optimizador='Adam', loss='crossEntropy')
model.addLayer(conv2DLayer(canalInp=1, canalOut=6, fSize=5, tipActiva='ReLU'))
model.addLayer(maxPooling(pSize=2, tipActiva='ReLU'))
model.addLayer(conv2DLayer(canalInp=6, canalOut=16, fSize=5, tipActiva='ReLU'))
model.addLayer(maxPooling(pSize=2, tipActiva='ReLU'))
model.addLayer(flatten(shapeInp=(16,5,5), sizeOut=400))
model.addLayer(linealLayer(neurInp=400, neurOut=120, tipActiva='ReLU'))
model.addLayer(linealLayer(neurInp=120, neurOut=84, tipActiva='ReLU'))
model.addLayer(linealLayer(neurInp=84, neurOut=10, tipActiva='softmax'))

Se realiza el entrenamiento#

if entrenamiento:
    model.fit(X, y)
else:
    print('modo de entrenamiento no activo')
modo de entrenamiento no activo

Serialización del modelo (input/output según estado)#

if entrenamiento:
    model.writeModel('data/modeloLetNet5.pt')
    print('modelo guardado en fichero')
else:
    model.readModel('data/modeloLetNet5.pt')
    print('modelo recuperado del fichero')
modelo recuperado del fichero

Cálculo del ratio de precisión en el conjunto de entrenamiento#

minibatch_size = 20
indices = np.arange(X.shape[0])
first=True
lstIndices = tqdm([i for i in range(0, indices.shape[0] - minibatch_size + 1, minibatch_size)])
model.Clases_y = np.unique(y)
for start_idx in lstIndices:
    batch_idx = indices[start_idx:start_idx + minibatch_size]
    y_pred2 = model.predict(X[batch_idx])
    y_pred = y_pred2 if first else np.concatenate((y_pred,y_pred2),axis=0)
    first=False
Hide code cell output
  0%|                                                                                                                                                    | 0/100 [00:00<?, ?it/s]
  1%|█▍                                                                                                                                          | 1/100 [00:01<02:25,  1.47s/it]
  2%|██▊                                                                                                                                         | 2/100 [00:03<02:34,  1.57s/it]
  3%|████▏                                                                                                                                       | 3/100 [00:04<02:40,  1.66s/it]
  4%|█████▌                                                                                                                                      | 4/100 [00:06<02:30,  1.57s/it]
  5%|███████                                                                                                                                     | 5/100 [00:07<02:29,  1.57s/it]
  6%|████████▍                                                                                                                                   | 6/100 [00:09<02:28,  1.58s/it]
  7%|█████████▊                                                                                                                                  | 7/100 [00:11<02:47,  1.80s/it]
  8%|███████████▏                                                                                                                                | 8/100 [00:13<02:49,  1.84s/it]
  9%|████████████▌                                                                                                                               | 9/100 [00:18<04:00,  2.64s/it]
 10%|█████████████▉                                                                                                                             | 10/100 [00:21<04:16,  2.84s/it]
 11%|███████████████▎                                                                                                                           | 11/100 [00:25<04:37,  3.12s/it]
 12%|████████████████▋                                                                                                                          | 12/100 [00:29<05:06,  3.49s/it]
 13%|██████████████████                                                                                                                         | 13/100 [00:32<04:49,  3.33s/it]
 14%|███████████████████▍                                                                                                                       | 14/100 [00:36<05:00,  3.50s/it]
 15%|████████████████████▊                                                                                                                      | 15/100 [00:41<05:30,  3.89s/it]
 16%|██████████████████████▏                                                                                                                    | 16/100 [00:44<05:11,  3.71s/it]
 17%|███████████████████████▋                                                                                                                   | 17/100 [00:46<04:29,  3.24s/it]
 18%|█████████████████████████                                                                                                                  | 18/100 [00:48<03:56,  2.89s/it]
 19%|██████████████████████████▍                                                                                                                | 19/100 [00:50<03:33,  2.64s/it]
 20%|███████████████████████████▊                                                                                                               | 20/100 [00:52<03:21,  2.51s/it]
 21%|█████████████████████████████▏                                                                                                             | 21/100 [00:55<03:21,  2.55s/it]
 22%|██████████████████████████████▌                                                                                                            | 22/100 [00:57<03:08,  2.41s/it]
 23%|███████████████████████████████▉                                                                                                           | 23/100 [00:59<02:56,  2.29s/it]
 24%|█████████████████████████████████▎                                                                                                         | 24/100 [01:01<02:45,  2.17s/it]
 25%|██████████████████████████████████▊                                                                                                        | 25/100 [01:03<02:46,  2.22s/it]
 26%|████████████████████████████████████▏                                                                                                      | 26/100 [01:06<02:43,  2.21s/it]
 27%|█████████████████████████████████████▌                                                                                                     | 27/100 [01:08<02:40,  2.20s/it]
 28%|██████████████████████████████████████▉                                                                                                    | 28/100 [01:10<02:41,  2.24s/it]
 29%|████████████████████████████████████████▎                                                                                                  | 29/100 [01:13<02:59,  2.53s/it]
 30%|█████████████████████████████████████████▋                                                                                                 | 30/100 [01:15<02:38,  2.26s/it]
 31%|███████████████████████████████████████████                                                                                                | 31/100 [01:16<02:17,  1.99s/it]
 32%|████████████████████████████████████████████▍                                                                                              | 32/100 [01:18<02:03,  1.81s/it]
 33%|█████████████████████████████████████████████▊                                                                                             | 33/100 [01:19<01:53,  1.70s/it]
 34%|███████████████████████████████████████████████▎                                                                                           | 34/100 [01:20<01:46,  1.62s/it]
 35%|████████████████████████████████████████████████▋                                                                                          | 35/100 [01:22<01:43,  1.60s/it]
 36%|██████████████████████████████████████████████████                                                                                         | 36/100 [01:25<02:01,  1.89s/it]
 37%|███████████████████████████████████████████████████▍                                                                                       | 37/100 [01:27<02:17,  2.18s/it]
 38%|████████████████████████████████████████████████████▊                                                                                      | 38/100 [01:30<02:20,  2.26s/it]
 39%|██████████████████████████████████████████████████████▏                                                                                    | 39/100 [01:32<02:14,  2.21s/it]
 40%|███████████████████████████████████████████████████████▌                                                                                   | 40/100 [01:34<02:15,  2.26s/it]
 41%|████████████████████████████████████████████████████████▉                                                                                  | 41/100 [01:37<02:16,  2.31s/it]
 42%|██████████████████████████████████████████████████████████▍                                                                                | 42/100 [01:39<02:16,  2.36s/it]
 43%|███████████████████████████████████████████████████████████▊                                                                               | 43/100 [01:42<02:13,  2.34s/it]
 44%|█████████████████████████████████████████████████████████████▏                                                                             | 44/100 [01:43<01:56,  2.08s/it]
 45%|██████████████████████████████████████████████████████████████▌                                                                            | 45/100 [01:45<01:46,  1.93s/it]
 46%|███████████████████████████████████████████████████████████████▉                                                                           | 46/100 [01:46<01:38,  1.82s/it]
 47%|█████████████████████████████████████████████████████████████████▎                                                                         | 47/100 [01:48<01:31,  1.72s/it]
 48%|██████████████████████████████████████████████████████████████████▋                                                                        | 48/100 [01:49<01:23,  1.62s/it]
 49%|████████████████████████████████████████████████████████████████████                                                                       | 49/100 [01:50<01:17,  1.51s/it]
 50%|█████████████████████████████████████████████████████████████████████▌                                                                     | 50/100 [01:52<01:12,  1.46s/it]
 51%|██████████████████████████████████████████████████████████████████████▉                                                                    | 51/100 [01:53<01:09,  1.42s/it]
 52%|████████████████████████████████████████████████████████████████████████▎                                                                  | 52/100 [01:54<01:06,  1.38s/it]
 53%|█████████████████████████████████████████████████████████████████████████▋                                                                 | 53/100 [01:56<01:03,  1.36s/it]
 54%|███████████████████████████████████████████████████████████████████████████                                                                | 54/100 [01:57<01:02,  1.35s/it]
 55%|████████████████████████████████████████████████████████████████████████████▍                                                              | 55/100 [01:58<01:00,  1.35s/it]
 56%|█████████████████████████████████████████████████████████████████████████████▊                                                             | 56/100 [02:00<01:00,  1.38s/it]
 57%|███████████████████████████████████████████████████████████████████████████████▏                                                           | 57/100 [02:02<01:05,  1.51s/it]
 58%|████████████████████████████████████████████████████████████████████████████████▌                                                          | 58/100 [02:03<01:03,  1.52s/it]
 59%|██████████████████████████████████████████████████████████████████████████████████                                                         | 59/100 [02:05<01:09,  1.70s/it]
 60%|███████████████████████████████████████████████████████████████████████████████████▍                                                       | 60/100 [02:08<01:16,  1.91s/it]
 61%|████████████████████████████████████████████████████████████████████████████████████▊                                                      | 61/100 [02:10<01:15,  1.92s/it]
 62%|██████████████████████████████████████████████████████████████████████████████████████▏                                                    | 62/100 [02:12<01:13,  1.94s/it]
 63%|███████████████████████████████████████████████████████████████████████████████████████▌                                                   | 63/100 [02:15<01:24,  2.28s/it]
 64%|████████████████████████████████████████████████████████████████████████████████████████▉                                                  | 64/100 [02:18<01:29,  2.48s/it]
 65%|██████████████████████████████████████████████████████████████████████████████████████████▎                                                | 65/100 [02:21<01:34,  2.71s/it]
 66%|███████████████████████████████████████████████████████████████████████████████████████████▋                                               | 66/100 [02:25<01:45,  3.10s/it]
 67%|█████████████████████████████████████████████████████████████████████████████████████████████▏                                             | 67/100 [02:27<01:30,  2.74s/it]
 68%|██████████████████████████████████████████████████████████████████████████████████████████████▌                                            | 68/100 [02:29<01:18,  2.45s/it]
 69%|███████████████████████████████████████████████████████████████████████████████████████████████▉                                           | 69/100 [02:30<01:10,  2.27s/it]
 70%|█████████████████████████████████████████████████████████████████████████████████████████████████▎                                         | 70/100 [02:32<01:04,  2.17s/it]
 71%|██████████████████████████████████████████████████████████████████████████████████████████████████▋                                        | 71/100 [02:34<01:02,  2.17s/it]
 72%|████████████████████████████████████████████████████████████████████████████████████████████████████                                       | 72/100 [02:39<01:19,  2.84s/it]
 73%|█████████████████████████████████████████████████████████████████████████████████████████████████████▍                                     | 73/100 [02:41<01:09,  2.59s/it]
 74%|██████████████████████████████████████████████████████████████████████████████████████████████████████▊                                    | 74/100 [02:43<01:03,  2.46s/it]
 75%|████████████████████████████████████████████████████████████████████████████████████████████████████████▎                                  | 75/100 [02:45<00:58,  2.33s/it]
 76%|█████████████████████████████████████████████████████████████████████████████████████████████████████████▋                                 | 76/100 [02:47<00:54,  2.28s/it]
 77%|███████████████████████████████████████████████████████████████████████████████████████████████████████████                                | 77/100 [02:49<00:51,  2.24s/it]
 78%|████████████████████████████████████████████████████████████████████████████████████████████████████████████▍                              | 78/100 [02:51<00:47,  2.18s/it]
 79%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████▊                             | 79/100 [02:54<00:45,  2.19s/it]
 80%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████▏                           | 80/100 [02:56<00:44,  2.23s/it]
 81%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                          | 81/100 [02:58<00:42,  2.26s/it]
 82%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉                         | 82/100 [03:00<00:40,  2.24s/it]
 83%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎                       | 83/100 [03:05<00:48,  2.85s/it]
 84%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊                      | 84/100 [03:09<00:52,  3.28s/it]
 85%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏                    | 85/100 [03:14<00:55,  3.68s/it]
 86%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌                   | 86/100 [03:18<00:55,  3.95s/it]
 87%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉                  | 87/100 [03:24<00:57,  4.40s/it]
 88%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎                | 88/100 [03:28<00:53,  4.44s/it]
 89%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋               | 89/100 [03:32<00:45,  4.16s/it]
 90%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████              | 90/100 [03:39<00:50,  5.03s/it]
 91%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍            | 91/100 [03:43<00:42,  4.74s/it]
 92%|███████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▉           | 92/100 [03:46<00:33,  4.20s/it]
 93%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▎         | 93/100 [03:49<00:26,  3.81s/it]
 94%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▋        | 94/100 [03:51<00:20,  3.38s/it]
 95%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████       | 95/100 [03:54<00:16,  3.38s/it]
 96%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▍     | 96/100 [03:57<00:12,  3.06s/it]
 97%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▊    | 97/100 [03:59<00:08,  2.74s/it]
 98%|████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▏  | 98/100 [04:01<00:04,  2.48s/it]
 99%|█████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████▌ | 99/100 [04:02<00:02,  2.25s/it]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 100/100 [04:04<00:00,  2.07s/it]
100%|██████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████████| 100/100 [04:04<00:00,  2.44s/it]

"Accuracy en entrenamiento=", 100 * model.accuracy(y_pred,y)
('Accuracy en entrenamiento=', 85.95)

Ejecución en modo Perceptron con sólo capas densas#

X2 = np.reshape(X,(X.shape[0], 32*32))
X.shape, X2.shape
((2000, 1, 32, 32), (2000, 1024))
model_cd = CNN(minibatch_size=32, epocas=30, eta=0.01, optimizador='grad', loss='errorCuadratico')
model_cd.addLayer(linealLayer(neurInp=1024, neurOut=256, tipActiva='sigmoid'))
model_cd.addLayer(linealLayer(neurInp=256, neurOut=64, tipActiva='sigmoid'))
model_cd.addLayer(linealLayer(neurInp=64, neurOut=10, tipActiva='sigmoid'))
if entrenamiento:
    model_cd.fit(X, y)
else:
    print('modo de entrenamiento no activo')
modo de entrenamiento no activo