Unidad 3: Redes Neuronales Convolucionales (CNN)
Las CNNs son una familia de modelos que fueron originalmente inspirados por cómo funciona el cerebro humano al reconocer objetos.
Esquema del sistema óptico humano
El descubrimiento original de cómo la corteza visual de nuestro cerebro funciona fue hecho por Hubel y Wiesel (1959), a través de la inserción de un microelectrodo en la corteza visual de un gato anestesiado.
Observaron que las neuronas cerebrales responden de forma diferente después de proyectar diferentes patrones de luz en frente del gato.
Esto eventualmente llevó al descubrimiento de las diferentes capas de la corteza visual.
Mientras que la capa primaria detecta principalmente bordes y líneas rectas, las capas superiores se enfocan más en extraer patrones y formas complejas.
El desarrollo de las CNNs se remonta a los años 90, cuando Yann LeCun y sus colegas propusieron una nueva arquitectura de red neuronal para clasificar dígitos escritos a mano desde imágenes Handwritten Digit Recognition with a Back-Propagation Network (LeCun et al. 1989), publicado en la conferencia de Sistemas de Procesamiento de Información Neural (NeurIPS).
Debido al rendimiento sobresaliente de las CNNs para tareas de clasificación de imágenes, este tipo particular de red neuronal feedforward ganó mucha atención y condujo a mejoras tremendas en los sistemas de aprendizaje de máquinas en evolución.
Varios años más tarde, en 2019, Yann LeCun recibió el premio Turing (el premio más prestigioso en la ciencia de computadoras) por sus contribuciones al campo de la inteligencia artificial (IA), junto con otros investigadores, Yoshua Bengio y Geoffrey Hinton.
Las Convolutional Neural Networks o CNNs, son un tipo especial de redes neuronales para procesar datos que tienen una topología en forma de cuadrícula conocida. Como por ejemplo, series de tiempo, que pueden ser pensados como una malla 1-dimensional que toman datos a intervalos regulares, datos de imagen, que pueden ser pensados como una malla 2-dimensional.
Este tipo de redes neuronales ha sido muy exitoso en la industria y práctica.
El nombre red neuronal convolucional indica que la red utiliza una operación matemática especifica: la convolución, esta es un tipo especial de operación lineal.
Las redes neuronales convolucionales son simplemente redes neuronales que usan convolución en lugar de una multiplicación matricial en al menos una de sus capas
La exitosa extracción de características relevantes es clave para el rendimiento de cualquier algoritmo de aprendizaje automático y los modelos de aprendizaje automático tradicionales dependen de las características que pueden venir de un experto en el dominio o que se basan en técnicas computacionales de extracción de características.
Las CNNs son capaces de aprender automáticamente las características de los datos en bruto que son más útiles para una tarea específica. Por esta razón, es común considerar las capas de las CNNs como extractores de características:
las capas iniciales (justo después de la capa de entrada) extraen características de bajo nivel de los datos en bruto,
y las capas posteriores, a menudo completamente conectadas (fully connected) como en un perceptrón multicapa (MLP) utilizan estas características para predecir un valor objetivo continuo o una etiqueta de clase.
Ciertos tipos de NNs multicapas, y en particular, las redes neuronales convolucionales profundas (deep CNNs), construyen lo que se llama una jerarquía de características combinando las características de bajo nivel en una secuencia de capas para formar características de alto nivel.
Si se quiere preservar la información espacial de una imagen u otra forma de datos, entonces es conveniente representar cada imagen con una matriz de píxeles.
Una forma simple de codificar la estructura local es conectar una submatriz de neuronas de entrada adyacentes en una única neurona oculta que pertenece a la siguiente capa. Esa única neurona oculta representa un campo receptivo local.
Esta operación se denomina convolución, y es de donde se deriva el nombre para este tipo de red.
Una forma intuitiva de pensar en la convolución es como el tratamiento de una matriz por otra matriz, a la que se le llama kernel.
Ejemplo
Por ejemplo, si estamos tratando con imágenes, entonces las características de bajo nivel, como bordes y manchas, se extraen de las capas anteriores, las cuales se combinan para formar características de alto nivel. Estas características de alto nivel pueden formar formas más complejas, como los contornos generales de objetos como edificios, gatos o perros.
Una CNN calcula mapas de características de una imagen de entrada, donde cada elemento proviene de un parche local de píxeles en la imagen de entrada:
Supongamos que el tamaño de cada submatriz es de \(5 \times 5\) y que esas submatrices se utilizan con imágenes MNIST de \(28 \times 28\) píxeles. Entonces seremos capaces de generar \(24 \times 24\) neuronas de campo receptivo local en la capa oculta. De hecho, es posible deslizar las submatrices solo 23 posiciones antes de tocar los bordes de las imágenes.
En TensorFlow
, el número de píxeles a lo largo de un borde del kernel o submatriz, es el tamaño del kernel,
y la longitud del paso es el número de píxeles por los cuales el kernel se mueve en cada paso de la convolución.
Definamos el mapa de características de una capa a otra. Por supuesto, podemos tener múltiples mapas de características que aprenden independientemente de cada capa oculta. Por ejemplo, podemos empezar con \(28 \times 28\) neuronas de entrada para procesar imágenes MNIST
, y luego definir \(k\) mapas de características de tamaño \(24 \times 24\) neuronas cada uno (nuevamente con forma de \(5 \times 5\)) en la siguiente capa oculta.
Típicamente, las CNNs están compuestas por varias capas convolucionales y de submuestreo (pooling) que son seguidas por una o más capas fully connected al final. Las capas fully connected son esencialmente un MLP, donde cada unidad de entrada, \(i\), está conectada a cada unidad de salida, \(j\), con un peso \(w_{ij}\).
Las capas de submuestreo (pooling), comúnmente conocidas como capas de agrupamiento (pooling layers), no tienen parámetros que se puedan aprender; por ejemplo, no hay unidades de peso o sesgo en las capas de pooling. Sin embargo, tanto las capas convolucionales como las fully connected tienen pesos y sesgos que son optimizados durante el entrenamiento.
Esquema de una CNN
En su forma general, la convolución es una operación sobre dos funciones con argumentos reales.
Supongamos que estamos rastreando la ubicación de una nave espacial con un sensor láser. Nuestro sensor láser nos entrega una sola salida \(x(t)\), la posición de la nave espacial en el tiempo \(t\), en donde \(x\) y \(t\) son valores reales.
Ahora supongamos que nuestro sensor laser es algo ruidoso. Para obtener una estimación menos ruidosa de la posición de la nave, podriamos promediar muchas mediciones, siendo las mediciones más recientes más relevantes, por lo que sería un promedio ponderado que otorga más peso a las observaciones más recientes.
Podemos hacer esto con una función \(w(a)\), donde \(a\) es la edad de la medición. Si deseamos aplicar la operación de ponderación en cada momento, debemos obtener una nueva función \(s\) que entregue una estimación suavizada de la posición de la nave:
\[s(t)=\int x(a)w(t-a)da\] Esta operación es llamada convolución. La operaciónde convolución se denota típicamente como:
\[s(t)=(x * w)(t)\]
Algunas consideaciones:
\(w\) necesita ser una función de densidad de probabilidad válida, sino la salida no sería una ponderación.
\(w\) necesita ser 0 para todos los argumentos negativos, o esta función mirará en el futuro. Estas limitaciones son particulares del ejemplo.
En general, la convolución está definida para cualquier función para que la integral anterior está definida, y puede ser ocupada con otros fines.
En este contexto, el primer argumento (\(x\)) se le llama input y el segundo argumento (\(w\)) se le llama kernel, y a la salida se le llama feature map
En el caso del ejemplo, la idea de que el sensor láser entregue medidas en cada instante de tiempo no es realista, pues trabajamos con una discretización del tiempo, usualmente a tiempos regulares. Así, tendremos:
\[s(t)=(x*w)(t)=\sum_{a=-\infty}^{\infty}x(a)w(t-a)\]
Frecuentemente se usan convoluciones sobre más de un eje en un tiempo especifico.
Por ejemplo, si se usa una imagen 2-dimensional \(I\) como input, probablemente se necesite usar un kernel \(K\) 2-dimensional:
\[S(i,j)=(I*K)(i,j)=\sum_m \sum_n I(m,n)K(i-m,j-n)\]
\[S(i,j)=(K*I)(i,j)=\sum_m \sum_n I(i-m,j-n)K(m,n)\]
El parche local de elementos que participan de la convolución, se conoce como el campo receptivo local. Las CNNs generalmente se desempeñan muy bien en tareas relacionadas con imágenes, y eso se debe en gran medida a tres ideas importantes:
Conectividad dispersa (sparse connectivity): Un único elemento en el mapa de características está conectado solo a un pequeño parche de píxeles. (Esto es muy diferente de conectar a toda la imagen de entrada como en el caso de los perceptrones.
Compartir parámetros (parameter sharing): Los mismos pesos se utilizan para diferentes parches de la imagen de entrada.
Representación equivariante (equivariant representation): Desplazar la señal de entrada resulta en una señal de salida igualmente desplazada. La mayoría de nosotros podemos reconocer rostros específicos bajo una variedad de condiciones porque aprendemos abstracción. Estas abstracciones son, por lo tanto, invariantes al tamaño, contraste, rotación y orientación.
Como consecuencia directa de estas ideas, reemplazar una red fully connected (MLP) convencional por una capa de convolución reduce sustancialmente el número de pesos (parámetros) en la red y veremos una mejora en la capacidad de capturar características inherentes.
En el contexto de los datos de imagen, tiene sentido suponer que los píxeles cercanos son típicamente más relevantes entre sí que los píxeles que están lejos unos de otros.
Además de permitir trabajar con entradas de tamaño variable.
Interacciones escasas o sparse interactions, que también se le refiere como sparse connectivity o sparse weights, viene desde la siguiente idea:
Las capas de una red neuronal tradicional usan multiplicación de matrices por una matriz de parámetros con un parámetro separado que describe la interacción entre cada unidad de entrada y cada unidad de salida.
Esto significa que cada unidad de salida interactúa con cada unidad de entrada. Las redes convolucionales, en cambio, no necesariamente. Este es logrado utilizando kernels más pequeños que la entrada.
Por ejemplo, cuando se procesa una imagen, la entrada podría tener millones de pixeles, pero es posible detectar unas pequeñas pero relevantes características, que al interactuar con el kernel ocupan sólo cientos de pixeles. Esto implica guardar mucho menos parámetros, que reduce la memoria requerida del modelo y mejora su eficiencia estadística.
Sparse connectivity
Esta característica hace referencia a usar los mismos parámetros para más de una función en un modelo. Reduciendo así, el número de parámetros a optimizar y mejorando la eficiencia estadística.
Configurando particularmente los parámetros, podemos obtener la propiedad de representación de equivalencia, que refiere a que si las entradas cambian, las salidas cambian en la misma manera.
Parameter sharing
Importante
En las siguientes diapositivas, estudiaremos las capas convolucionales y de pooling con más detalle y veremos cómo funcionan.
Para entender cómo funcionan las operaciones de convolución, empezaremos con una convolución en una dimensión, que a veces se utiliza para trabajar con ciertos tipos de datos secuenciales, como el texto.
Después, trabajaremos a través de las convoluciones bidimensionales, que se aplican comúnmente a imágenes.
Notación
En esta parte, usaremos subíndices para denotar el tamaño de un arreglo multidimensional (tensor); por ejemplo, \(A_{n_1 \times n_2}\) es un arreglo bidimensional de tamaño \(n_1 \times n_2\).
Usamos corchetes, \(\left[ \; \right]\), para denotar la indexación de un arreglo multidimensional. Por ejemplo, \(A[i, j]\) se refiere al elemento en el índice i, j de la matriz A.
Además, notar que usamos un símbolo especial, \(*\), para denotar la operación de convolución entre dos vectores o matrices, lo cual no debe confundirse con el operador de multiplicación, *
, que típicamente utilizamos en R
o Python
.
Una convolución discreta1 para dos vectores, \(x\) y \(w\), se denota por \(y = x * w\), donde el vector \(x\) es nuestra entrada (a veces llamado señal) y \(w\) se llama el filtro o kernel. Una convolución se define matemáticamente de la siguiente manera:
\[ y = x * w \rightarrow y[i] = \sum_{k=-\infty}^{+\infty} x[i - k] \cdot w[k] \]
Como se mencionó anteriormente, los corchetes, [], se usan para denotar la indexación para los elementos del vector. El índice, \(i\), recorre cada elemento del vector de salida, \(y\).
Hay dos puntos a mencionar a partir de la fórmula anterior que deben ser destacados:
índices de \(-\infty\) a \(+\infty\)
indexación negativa para \(x\).
El hecho de que la suma recorra índices de \(-\infty\) a \(+\infty\) parece raro, principalmente porque en aplicaciones de CNN siempre tratamos con vectores de características finitas.
Por ejemplo, si \(x\) tiene 10 características con índices \(0, 1, 2,\ldots, 9\), entonces los índices \(-\infty = -1\) y \(10 : +\infty\) estarían fuera de los límites para \(x\).
Por lo tanto, para calcular correctamente la sumatoria mostrada en la fórmula anterior, se asume que \(x\) y \(w\) están rellenados con ceros. Esto resultará en un vector de salida, \(y\), que también tiene un tamaño infinito, con muchos ceros también. Dado que esto no es útil en situaciones prácticas, a \(x\) se le añaden solo un número finito de ceros.
Este proceso se llama relleno de ceros o simplemente relleno (padding). Aquí, el número de ceros añadidos a cada lado se denota por \(p\). Un ejemplo de relleno para un vector unidimensional, \(x\), se muestra a continuación:
Supongamos que la entrada original, \(x\), y el filtro, \(w\), tienen \(n\) y \(m\) elementos, donde \(m \leq n\). Por lo tanto, el vector rellenado, \(x^{'p}\), tiene tamaño \(n + 2p\). La fórmula práctica para calcular la convolución cambiará a la siguiente:
\[ y = x * w \rightarrow y[i] = \sum_{k=0}^{m-1} x^p[i + m - k] \cdot w[k] \]
Ahora que hemos resuelto el problema del índice infinito, el segundo problema es indexar \(x\) con \(i + m - k\).
El punto importante a notar aquí es que \(x\) y \(w\) están indexados en diferentes direcciones en esta sumatoria.
Calcular la suma con un índice yendo en la dirección inversa es equivalente a calcular la suma con ambos índices en la dirección hacia adelante después de voltear uno de esos vectores, \(x\) o \(w\), después de que son rellenados.
Entonces, simplemente podemos calcular su producto punto. Supongamos que invertimos (rotamos) el filtro, \(w\), para obtener el filtro rotado, \(w^r\).
Entonces, el producto punto, \(x[i : i + m] \cdot w^r\), se calcula para obtener un elemento, \(y[i]\), donde \(x[i : i + m]\) es un segmento de \(x\) con tamaño \(m\).
Esta operación se repite como en un enfoque de ventana deslizante para obtener todos los elementos de salida.
En el ejemplo anterior se pueden notar algunos detalles:
El tamaño del padding es cero (\(p = 0\)).
El filtro rotado, \(w^r\), se desplaza dos celdas cada vez que se desliza. Este desplazamiento es otro hiperparámetro de una convolución, el paso o stride, \(s\). En este ejemplo, el stride es dos, \(s = 2\).
El stride debe ser un número positivo menor que el tamaño del vector de entrada.
Hasta ahora, solo hemos utilizado el zero-padding en las convoluciones para calcular vectores de salida de tamaño finito. Técnicamente, el padding se puede aplicar con cualquier \(p \geq 0\). Dependiendo de la elección de \(p\), las celdas en los bordes pueden ser tratadas de manera diferente a las celdas situadas en el medio de \(x\).
Existen tres modos de padding que se utilizan comúnmente en la práctica: completo (full), mismo (same) y válido (valid):
En modo full, el parámetro de relleno, \(p\), se establece en \(p = m - 1\). El full padding aumenta las dimensiones de la salida; por lo tanto, rara vez se usa en arquitecturas de CNN.
El same padding generalmente se utiliza para asegurar que el vector de salida tenga el mismo tamaño que el vector de entrada, \(x\). En este caso, el parámetro de padding, \(p\), se calcula de acuerdo al tamaño del filtro, junto con el requisito de que el tamaño de entrada y salida sean los mismos.
Finalmente, calcular una convolución usando valid padding se refiere al caso donde \(p = 0\) (sin padding).
El tamaño de salida de una convolución está determinado por el número total de veces que se puede desplazar el filtro, \(w\), a lo largo del vector de entrada. Supongamos que el vector de entrada es de tamaño \(n\) y el filtro es de tamaño \(m\). Entonces, el tamaño de la salida resultante de \(y = x * w\), con padding, \(p\), y stride, \(s\), se determinaría de la siguiente manera:
\[o = \left\lfloor \frac{n + 2p - m}{s} \right\rfloor + 1\]
Aquí, \(\lfloor \cdot \rfloor\) denota la operación de redondeo hacia abajo (floor). Esta operación devuelve el entero más grande que es igual o menor que la entrada, por ejemplo: \(\text{floor}(1.77) = \lfloor 1.77 \rfloor = 1 \]\)
conv1d <- function(x, w, p = 0, s = 1) {
# Invertir el kernel para la convolución
w_rot <- rev(w)
# Preparar el vector de entrada con el relleno inicial
x_padded <- x
if (p > 0) {
# Añadir ceros para el relleno antes y después del vector original
x_padded <- c(rep(0, p), x_padded, rep(0, p))
}
# Inicializar el vector de resultados
res <- numeric()
# Aplicar la convolución con el paso (stride) especificado
for (i in seq(1, length(x_padded) - length(w_rot) + 1, by = s)) {
res <- c(res, sum(x_padded[i:(i + length(w_rot) - 1)] * w_rot))
}
return(res)
}
# Prueba:
x <- c(1, 3, 2, 4, 5, 6, 1, 3)
w <- c(1, 0, 3, 1, 2)
cat("Implementación Conv1d: ", conv1d(x, w, p = 2, s = 1), "\n")
Implementación Conv1d: 5 14 16 26 24 34 19 22
import numpy as np
def conv1d(x, w, p=0, s=1):
w_rot = np.array(w[::-1])
x_padded = np.array(x)
if p > 0:
zero_pad = np.zeros(shape=p)
x_padded = np.concatenate([zero_pad, x_padded, zero_pad])
res = []
for i in range(0, int(len(x)/s),s):
res.append(np.sum(x_padded[i:i+w_rot.shape[0]] * w_rot))
return np.array(res)
## Prueba:
x = [1, 3, 2, 4, 5, 6, 1, 3]
w = [1, 0, 3, 1, 2]
print('Implementación de Conv1d: ',
conv1d(x, w, p=2, s=1))
Implementación de Conv1d: [ 5. 14. 16. 26. 24. 34. 19. 22.]
Resultados de Numpy: [ 5 14 16 26 24 34 19 22]
Los conceptos que hemos visto hasta ahora son fácilmente extensibles a 2D. Cuando tratamos con entradas en 2D, como una matriz, \(X_{n1 \times n2}\), y la matriz del filtro, \(W_{m1 \times m2}\), donde \(m1 \leq n1\) y \(m2 \leq n2\), entonces la matriz \(Y = X * W\) es el resultado de una convolución en 2D entre \(X\) y \(W\). Esto se define matemáticamente de la siguiente manera:
\[ Y = X * W \rightarrow Y[i,j] = \sum_{k1=-\infty}^{+\infty} \sum_{k2=-\infty}^{+\infty} X[i - k1,j - k2] \cdot W[k1,k2] \]
Notar que, todas las técnicas mencionadas anteriormente, como el padding, rotar la matriz del filtro, y el uso de strides, también son aplicables a convoluciones en 2D, siempre y cuando se extiendan a ambas dimensiones de manera independiente. La figura siguiente demuestra la convolución en 2D de una matriz de entrada de tamaño \(6 \times 6\), utilizando un kernel de tamaño \(2 \times 2\). La matriz se rellena con ceros con \(p = 1\). Como resultado, la salida de la convolución en 2D tendrá un tamaño de \(4 \times 4\):
Acá se muestra una convolución 2D entre una matriz de entrada, \(X_{3 \times 3}\), y una matriz de núcleo, \(W_{3 \times 3}\), utilizando un padding \(p = (1, 1)\) y un stride \(s = (2, 2)\). De acuerdo al padding especificado, se añade una capa de ceros en cada lado de la matriz de entrada, lo que resulta en la matriz con padding \(X_{padded}\) de \(5 \times 5\), de la siguiente manera:
Con el filtro anterior, el filtro rotado será:
\[ W' = \begin{bmatrix} 0.5 & 1 & 0.5 \\ 0.1 & 0.4 & 0.3 \\ 0.4 & 0.7 & 0.5 \end{bmatrix} \]
Notar que esta rotación no es lo mismo que la matriz transpuesta. Para obtener el filtro rotado en NumPy
, podemos escribir W_rot = W[::-1, ::-1]
. En R
, podemos invertir las filas y las columnas utilizando el siguiente código:
El código anterior crea primero la matriz \(W\) y luego utiliza la indexación para invertir tanto las filas como las columnas. A continuación, podemos desplazar la matriz de filtro rotado a lo largo de la matriz de entrada con padding, \(X_{\text{padded}}\), como una ventana deslizante y calcular la suma del producto elemento a elemento, que es denotado por el operador \(\odot\) en la figura siguiente:
# Función de convolución 2D
conv2d <- function(X, W, p=c(0,0), s=c(1,1)) {
# Rota y da vuelta a la matriz de pesos W
W_rot <- W[nrow(W):1, ncol(W):1]
# Añade el padding a la matriz X
X_padded <- matrix(0, nrow = nrow(X) + 2*p[1], ncol = ncol(X) + 2*p[2])
X_padded[(p[1]+1):(p[1]+nrow(X)), (p[2]+1):(p[2]+ncol(X))] <- X
# Lista para guardar los resultados
res <- list()
# Recorrer la imagen con el filtro
for (i in seq(1, nrow(X_padded) - nrow(W_rot) + 1, by = s[1])) {
temp <- c()
for (j in seq(1, ncol(X_padded) - ncol(W_rot) + 1, by = s[2])) {
X_sub <- X_padded[i:(i+nrow(W_rot)-1), j:(j+ncol(W_rot)-1)]
# Realizar la convolución (producto punto)
temp <- c(temp, sum(X_sub * W_rot))
}
res[[length(res) + 1]] <- temp
}
# Convertir la lista a matriz
do.call(rbind, res)
}
# Datos de ejemplo
X <- matrix(c(1, 3, 2, 4, 5, 6, 1, 3, 1, 2, 0, 2, 3, 4, 3, 2),
nrow = 4, byrow = TRUE)
W <- matrix(c(1, 0, 3, 1, 2, 1, 0, 1, 1),
nrow = 3, byrow = TRUE)
# Aplicar la función de convolución definida
result <- conv2d(X, W, p=c(1,1), s=c(1,1))
print(result)
[,1] [,2] [,3] [,4]
[1,] 11 25 32 13
[2,] 19 25 24 13
[3,] 13 28 25 17
[4,] 11 17 14 9
import numpy as np
def conv2d(X, W, p=(0,0), s=(1,1)):
W_rot = np.array(W)[::-1,::-1]
X_orig = np.array(X)
n1 = X_orig.shape[0] + 2*p[0]
n2 = X_orig.shape[1] + 2*p[1]
X_padded = np.zeros(shape=(n1,n2))
X_padded[p[0]:p[0] + X_orig.shape[0],
p[1]:p[1] + X_orig.shape[1]] = X_orig
res = []
for i in range(0, int((X_padded.shape[0] -
W_rot.shape[0])/s[0])+1, s[0]):
res.append([])
for j in range(0, int((X_padded.shape[1] -
W_rot.shape[1])/s[1])+1, s[1]):
X_sub = X_padded[i:i+W_rot.shape[0], j:j+W_rot.shape[1]]
res[-1].append(np.sum(X_sub * W_rot))
return(np.array(res))
El submuestreo se aplica típicamente en dos formas de operaciones de pooling en las CNNs: max-pooling y mean-pooling (también conocido como average-pooling).
La capa de pooling se denota generalmente por \(P_{n1 \times n2}\). Aquí, el subíndice determina el tamaño del vecindario (el número de píxeles adyacentes en cada dimensión) donde se realiza la operación de máximo o promedio. Nos referimos a tal vecindario como el tamaño de pooling.
La operación se describe en la siguiente figura. Aquí, max-pooling toma el valor máximo de un vecindario de píxeles, y mean-pooling calcula su promedio:
La ventaja del pooling es doble:
El pooling (max-pooling) introduce una invariancia local. Esto significa que pequeños cambios en un vecindario local no cambian el resultado del max-pooling. Por lo tanto, ayuda a generar características que son más robustas al ruido en los datos de entrada.
Pooling disminuye el tamaño de las características, lo cual resulta en una mayor eficiencia computacional. Además, la reducción en el número de características puede también reducir el riesgo de sobreajuste.
Podemos decir que la operación más importante en una NN tradicional es la multiplicación de matrices.
Por ejemplo, usamos multiplicaciones de matrices para computar las pre-activaciones (o entradas netas) como en \(z = Wx + b\).
Aquí, \(x\) es un vector columna (matriz \(R^{N \times 1}\)) que representa píxeles, y \(W\) es la matriz de pesos que conecta las entradas de píxeles a cada unidad oculta.
En una CNN, esta operación es reemplazada por una operación de convolución, como en \(Z = W * X + b\), donde \(X\) es una matriz que representa los píxeles en un arreglo de altura por ancho.
En ambos casos, las pre-activaciones se pasan a una función de activación para obtener la activación de una unidad oculta, \(A = \phi(Z)\), donde \(\phi\) es la función de activación.
El submuestreo es otro bloque de construcción de una CNN, que puede aparecer en la forma de pooling, como se describió en la sección anterior.
Una entrada a una capa convolucional puede contener uno o más arreglos o matrices 2D con dimensiones \(N_1 \times N_2\) (por ejemplo, la altura y ancho de la imagen en píxeles).
Estas matrices \(N_1 \times N_2\) se llaman canales. Las implementaciones convencionales de capas convolucionales esperan una representación tensorial de rango 3 como entrada, por ejemplo, un arreglo tridimensional, \(X_{N_1 \times N_2 \times C_{in}}\), donde \(C_{in}\) es el número de canales de entrada.
Por ejemplo, consideremos imágenes como entrada a la primera capa de una CNN.
Si la imagen es a color y usa el modo de color RGB, entonces \(C_{in} = 3\) (para los canales de color rojo, verde y azul en RGB).
Si la imagen es en escala de grises, entonces tenemos \(C_{in} = 1\), porque solo hay un canal con los valores de intensidad de píxeles en escala de grises.
¿cómo podemos incorporar múltiples canales de entrada en la operación de convolución que discutimos en las secciones anteriores?
Se realiza la operación de convolución por cada canal por separado y luego se suman los resultados utilizando la suma de matrices. La convolución asociada con cada canal (\(c\)) se lleva a cabo con nuestra matriz de kernel como \(W[:, :, c]\). El resultado total de pre-activación se calcula en la siguiente fórmula:
\[ \begin{matrix} \text{Dado un ejemplo } \mathbf{X}_{n_1 \times n_2 \times c_{in'}} \\ \text{una matriz de kernel } \mathbf{W}_{m_1 \times m_2 \times c_{in'}} \\ \text{y un valor de sesgo } b \end{matrix} \Rightarrow \left\{\begin{matrix} Z_{\text{conv}} = \sum_{c=1}^{C_{\text{in}}} W \left[ :, :, c \right] * X \left[ :, :, c \right] \\ \text{Pre-activación: } Z = Z_{\text{conv}} + b_c \\ \text{Mapa de características: } A= \phi(Z) \end{matrix}\right. \]
El resultado final, \(A\), es un mapa de características. Usualmente, una capa convolucional de una CNN tiene más de un mapa de características. Si usamos múltiples mapas de características, el tensor del kernel se vuelve cuatridimensional: anchura \(\times\) altura \(\times C_{\text{in}} \times C_{\text{out}}\). Aquí, \(m_1 \times m_2\) es el tamaño del kernel, \(C_{\text{in}}\) es el número de canales de entrada, y \(C_{\text{out}}\) es el número de mapas de características de salida.
Ahora, al incluir el número de mapas de características de salida en la fórmula precedente, podemos actualizarla de la siguiente manera:
\[\begin{matrix} \text{Dado un ejemplo } \mathbf{X}_{n_1 \times n_2 \times c_{in'}} \\ \text{una matriz de kernel } \mathbf{W}_{m_1 \times m_2 \times c_{in} \times c_{out'}} \\ \text{y un valor de sesgo } b_{C_{out}} \end{matrix} \Rightarrow \left\{\begin{matrix} Z_{\text{conv}}[:, :, k] =& \sum_{c=1}^{C_{\text{in}}} W[:, :, c, k] * X[:, :, c] \\ Z[:, :, k] =& Z_{\text{conv}}[:, :, k] + b[k] \\ A[:, :, k] =& \phi(Z[:, :, k]) \end{matrix}\right.\]
En este ejemplo, hay tres canales de entrada.
El tensor del kernel es cuatridimensional. Cada matriz de kernel está denotada como \(m_1 \times m_2\) y hay tres de ellas, una para cada canal de entrada.
Además, hay cinco de esos kernels, contabilizando cinco mapas de características de salida.
Finalmente, hay una capa de pooling para submuestrear los mapas de características.
Una matriz de confusión es una herramienta útil utilizada en aprendizaje supervisado, especialmente en clasificación, para visualizar el desempeño de un algoritmo. Esencialmente, la matriz compara las etiquetas predichas por el modelo contra las etiquetas verdaderas conocidas del conjunto de datos de prueba. Esta comparación ayuda a evaluar la precisión de un modelo y a identificar de qué manera sus predicciones son correctas o incorrectas.
La matriz de confusión es una tabla cuadrada que se organiza en filas y columnas, donde:
Las filas representan las clases verdaderas (etiquetas reales de los datos).
Las columnas representan las clases predichas por el modelo.
Cada entrada en esta matriz indica el número de predicciones realizadas por el modelo para una clase verdadera en comparación con una clase predicha. Para una matriz de confusión de un problema de clasificación binaria, tendrías una estructura como la siguiente:
Esquema de una matriz de confusión para clasificación binaria
La matriz de confusión permite varios niveles de interpretación:
Diagonal Principal: Los valores en la diagonal principal (de arriba a la izquierda a abajo a la derecha) indican el número de predicciones correctas que hizo el modelo para cada clase.
Fuera de la Diagonal: Los valores fuera de la diagonal principal muestran los errores cometidos por el modelo, donde se clasificó incorrectamente una clase como otra.
A partir de la matriz de confusión, se pueden calcular diversas métricas que proporcionan más detalles sobre el desempeño del modelo:
Precisión: La exactitud de las predicciones positivas. \(\text{Precisión} = \frac{VP}{VP + FP}\)
Recuperación (Sensibilidad o Tasa de Verdaderos Positivos): La proporción de positivos reales que se identificaron correctamente. \(\text{Recuperación} = \frac{VP}{VP + FN}\)
Especificidad (Tasa de Verdaderos Negativos): La proporción de negativos reales que se identificaron correctamente. \(\text{Especificidad} = \frac{VN}{VN + FP}\)
Puntuación F1: El promedio ponderado de la precisión y la recuperación. \(F1 = 2 \times \frac{\text{Precisión} \times \text{Recuperación}}{\text{Precisión} + \text{Recuperación}}\)
Estas métricas ayudan a entender no solo la “exactitud global” del modelo, sino también cómo se comporta en términos de diferentes tipos de errores y aciertos, lo cual es crucial para aplicaciones donde ciertos tipos de errores pueden tener consecuencias más graves que otros.
Más detalle para una matriz de confusión multiclase puede ser revisado acá
Línea de tiempo (hasta 2019) de las CNNs y su desarrollo
LeNet-5 es una de las primeras arquitecturas de redes neuronales convolucionales, desarrollada por Yann LeCun en 1998. Fue diseñada para reconocer caracteres escritos a mano y se compone de dos capas convolucionales seguidas de capas de agrupamiento y capas completamente conectadas.
library(keras)
model <- keras_model_sequential() %>%
layer_conv_2d(filters = 6, kernel_size = c(5, 5), activation = 'tanh', input_shape = c(32, 32, 1)) %>%
layer_average_pooling_2d(pool_size = c(2, 2)) %>%
layer_conv_2d(filters = 16, kernel_size = c(5, 5), activation = 'tanh') %>%
layer_average_pooling_2d(pool_size = c(2, 2)) %>%
layer_conv_2d(filters = 120, kernel_size = c(5, 5), activation = 'tanh') %>%
layer_flatten() %>%
layer_dense(units = 84, activation = 'tanh') %>%
layer_dense(units = 10, activation = 'softmax')
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_sgd(),
metrics = 'accuracy'
)
Model: "sequential"
________________________________________________________________________________
Layer (type) Output Shape Param #
================================================================================
conv2d_2 (Conv2D) (None, 28, 28, 6) 156
average_pooling2d_1 (AveragePooli (None, 14, 14, 6) 0
ng2D)
conv2d_1 (Conv2D) (None, 10, 10, 16) 2416
average_pooling2d (AveragePooling (None, 5, 5, 16) 0
2D)
conv2d (Conv2D) (None, 1, 1, 120) 48120
flatten (Flatten) (None, 120) 0
dense_1 (Dense) (None, 84) 10164
dense (Dense) (None, 10) 850
================================================================================
Total params: 61706 (241.04 KB)
Trainable params: 61706 (241.04 KB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________
AlexNet, desarrollada por Alex Krizhevsky y su equipo en 2012, es una red profunda que ganó la competencia ImageNet. Introdujo el uso de ReLU como función de activación y popularizó el uso de GPU para entrenamiento.
library(keras)
model <- keras_model_sequential() %>%
layer_conv_2d(filters = 96, kernel_size = c(11, 11), strides = c(4, 4), activation = 'relu', input_shape = c(227, 227, 3)) %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2)) %>%
layer_conv_2d(filters = 256, kernel_size = c(5, 5), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2)) %>%
layer_conv_2d(filters = 384, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 384, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 256, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2)) %>%
layer_flatten() %>%
layer_dense(units = 4096, activation = 'relu') %>%
layer_dropout(rate = 0.5) %>%
layer_dense(units = 4096, activation = 'relu') %>%
layer_dropout(rate = 0.5) %>%
layer_dense(units = 1000, activation = 'softmax')
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_sgd(),
metrics = 'accuracy'
)
summary(model)
Model: "sequential_1"
________________________________________________________________________________
Layer (type) Output Shape Param #
================================================================================
conv2d_7 (Conv2D) (None, 55, 55, 96) 34944
max_pooling2d_2 (MaxPooling2D) (None, 27, 27, 96) 0
conv2d_6 (Conv2D) (None, 27, 27, 256) 614656
max_pooling2d_1 (MaxPooling2D) (None, 13, 13, 256) 0
conv2d_5 (Conv2D) (None, 13, 13, 384) 885120
conv2d_4 (Conv2D) (None, 13, 13, 384) 1327488
conv2d_3 (Conv2D) (None, 13, 13, 256) 884992
max_pooling2d (MaxPooling2D) (None, 6, 6, 256) 0
flatten_1 (Flatten) (None, 9216) 0
dense_4 (Dense) (None, 4096) 37752832
dropout_1 (Dropout) (None, 4096) 0
dense_3 (Dense) (None, 4096) 16781312
dropout (Dropout) (None, 4096) 0
dense_2 (Dense) (None, 1000) 4097000
================================================================================
Total params: 62378344 (237.95 MB)
Trainable params: 62378344 (237.95 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________
GoogleNet, también conocida como Inception v1, fue desarrollada por Google y ganó la competencia ImageNet en 2014. Introduce los módulos de Inception que permiten diferentes tamaños de convoluciones en la misma capa.
library(keras)
input <- layer_input(shape = c(224, 224, 3))
conv1 <- layer_conv_2d(input, filters = 64, kernel_size = c(7, 7), strides = c(2, 2), activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2), padding = 'same')
conv2 <- layer_conv_2d(conv1, filters = 64, kernel_size = c(1, 1), activation = 'relu') %>%
layer_conv_2d(filters = 192, kernel_size = c(3, 3), padding = 'same', activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2), padding = 'same')
# Inception Module 1
inception_3a <- conv2 %>%
layer_conv_2d(filters = 64, kernel_size = c(1, 1), activation = 'relu') %>%
layer_conv_2d(filters = 128, kernel_size = c(3, 3), padding = 'same', activation = 'relu') %>%
layer_conv_2d(filters = 32, kernel_size = c(1, 1), activation = 'relu') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(1, 1), padding = 'same')
# Flatten and Dense Layers
flatten <- layer_flatten(inception_3a) %>%
layer_dense(units = 1000, activation = 'softmax')
model <- keras_model(inputs = input, outputs = flatten)
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_sgd(),
metrics = 'accuracy'
)
summary(model)
Model: "model"
________________________________________________________________________________
Layer (type) Output Shape Param #
================================================================================
input_1 (InputLayer) [(None, 224, 224, 3)] 0
conv2d_8 (Conv2D) (None, 109, 109, 64) 9472
max_pooling2d_3 (MaxPooling2D) (None, 55, 55, 64) 0
conv2d_10 (Conv2D) (None, 55, 55, 64) 4160
conv2d_9 (Conv2D) (None, 55, 55, 192) 110784
max_pooling2d_4 (MaxPooling2D) (None, 28, 28, 192) 0
conv2d_13 (Conv2D) (None, 28, 28, 64) 12352
conv2d_12 (Conv2D) (None, 28, 28, 128) 73856
conv2d_11 (Conv2D) (None, 28, 28, 32) 4128
max_pooling2d_5 (MaxPooling2D) (None, 28, 28, 32) 0
flatten_2 (Flatten) (None, 25088) 0
dense_5 (Dense) (None, 1000) 25089000
================================================================================
Total params: 25303752 (96.53 MB)
Trainable params: 25303752 (96.53 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________
VGG16, creada por el Visual Geometry Group de la Universidad de Oxford en 2014, es conocida por su simplicidad y profundidad, usando solo convoluciones 3x3 y max-pooling. Esta arquitectura es efectiva y fácil de implementar.
library(keras)
model <- keras_model_sequential() %>%
layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = 'relu', padding = 'same', input_shape = c(224, 224, 3)) %>%
layer_conv_2d(filters = 64, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2, 2), strides = c(2, 2)) %>%
layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 128, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2, 2), strides = c(2, 2)) %>%
layer_conv_2d(filters = 256, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 256, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 256, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2, 2), strides = c(2, 2)) %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2, 2), strides = c(2, 2)) %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_conv_2d(filters = 512, kernel_size = c(3, 3), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(2, 2), strides = c(2, 2)) %>%
layer_flatten() %>%
layer_dense(units = 4096, activation = 'relu') %>%
layer_dense(units = 4096, activation = 'relu') %>%
layer_dense(units = 1000, activation = 'softmax')
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_sgd(),
metrics = 'accuracy'
)
summary(model)
Model: "sequential_2"
________________________________________________________________________________
Layer (type) Output Shape Param #
================================================================================
conv2d_26 (Conv2D) (None, 224, 224, 64) 1792
conv2d_25 (Conv2D) (None, 224, 224, 64) 36928
max_pooling2d_10 (MaxPooling2D) (None, 112, 112, 64) 0
conv2d_24 (Conv2D) (None, 112, 112, 128) 73856
conv2d_23 (Conv2D) (None, 112, 112, 128) 147584
max_pooling2d_9 (MaxPooling2D) (None, 56, 56, 128) 0
conv2d_22 (Conv2D) (None, 56, 56, 256) 295168
conv2d_21 (Conv2D) (None, 56, 56, 256) 590080
conv2d_20 (Conv2D) (None, 56, 56, 256) 590080
max_pooling2d_8 (MaxPooling2D) (None, 28, 28, 256) 0
conv2d_19 (Conv2D) (None, 28, 28, 512) 1180160
conv2d_18 (Conv2D) (None, 28, 28, 512) 2359808
conv2d_17 (Conv2D) (None, 28, 28, 512) 2359808
max_pooling2d_7 (MaxPooling2D) (None, 14, 14, 512) 0
conv2d_16 (Conv2D) (None, 14, 14, 512) 2359808
conv2d_15 (Conv2D) (None, 14, 14, 512) 2359808
conv2d_14 (Conv2D) (None, 14, 14, 512) 2359808
max_pooling2d_6 (MaxPooling2D) (None, 7, 7, 512) 0
flatten_3 (Flatten) (None, 25088) 0
dense_8 (Dense) (None, 4096) 102764544
dense_7 (Dense) (None, 4096) 16781312
dense_6 (Dense) (None, 1000) 4097000
================================================================================
Total params: 138357544 (527.79 MB)
Trainable params: 138357544 (527.79 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________
ResNet (Redes Residuales), introducida por Microsoft en 2015, es una arquitectura profunda que utiliza “bloques residuales” para permitir la construcción de redes muy profundas sin el problema de la desaparición del gradiente.
library(keras)
input <- layer_input(shape = c(224, 224, 3))
conv1 <- layer_conv_2d(input, filters = 64, kernel_size = c(7, 7), strides = c(2, 2), activation = 'relu', padding = 'same') %>%
layer_max_pooling_2d(pool_size = c(3, 3), strides = c(2, 2), padding = 'same')
# Residual Block
residual_block <- function(input, filters, strides = 1) {
x <- layer_conv_2d(input, filters = filters, kernel_size = c(3, 3), strides = strides, padding = 'same', activation = 'relu')
x <- layer_conv_2d(x, filters = filters, kernel_size = c(3, 3), padding = 'same')
if (strides != 1) {
input <- layer_conv_2d(input, filters = filters, kernel_size = c(1, 1), strides = strides, padding = 'same')
}
layer_add(list(x, input)) %>% layer_activation('relu')
}
resnet <- residual_block(conv1, 64) %>%
residual_block(filters = 128, strides = 2) %>%
residual_block(filters = 256, strides = 2) %>%
residual_block(filters = 512, strides = 2)
flatten <- layer_flatten(resnet) %>%
layer_dense(units = 1000, activation = 'softmax')
model <- keras_model(inputs = input, outputs = flatten)
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_sgd(),
metrics = 'accuracy'
)
summary(model)
Model: "model_1"
________________________________________________________________________________
Layer (type) Output Shape Param Connected to
#
================================================================================
input_2 (InputLayer) [(None, 224, 224, 3)] 0 []
conv2d_27 (Conv2D) (None, 112, 112, 64) 9472 ['input_2[0][0]']
max_pooling2d_11 (Max (None, 56, 56, 64) 0 ['conv2d_27[0][0]']
Pooling2D)
conv2d_31 (Conv2D) (None, 56, 56, 64) 36928 ['max_pooling2d_11[0][0]
']
conv2d_32 (Conv2D) (None, 56, 56, 64) 36928 ['conv2d_31[0][0]']
add (Add) (None, 56, 56, 64) 0 ['conv2d_32[0][0]',
'max_pooling2d_11[0][0]
']
activation (Activatio (None, 56, 56, 64) 0 ['add[0][0]']
n)
conv2d_30 (Conv2D) (None, 28, 28, 128) 73856 ['activation[0][0]']
conv2d_33 (Conv2D) (None, 28, 28, 128) 14758 ['conv2d_30[0][0]']
4
conv2d_34 (Conv2D) (None, 28, 28, 128) 8320 ['activation[0][0]']
add_1 (Add) (None, 28, 28, 128) 0 ['conv2d_33[0][0]',
'conv2d_34[0][0]']
activation_1 (Activat (None, 28, 28, 128) 0 ['add_1[0][0]']
ion)
conv2d_29 (Conv2D) (None, 14, 14, 256) 29516 ['activation_1[0][0]']
8
conv2d_35 (Conv2D) (None, 14, 14, 256) 59008 ['conv2d_29[0][0]']
0
conv2d_36 (Conv2D) (None, 14, 14, 256) 33024 ['activation_1[0][0]']
add_2 (Add) (None, 14, 14, 256) 0 ['conv2d_35[0][0]',
'conv2d_36[0][0]']
activation_2 (Activat (None, 14, 14, 256) 0 ['add_2[0][0]']
ion)
conv2d_28 (Conv2D) (None, 7, 7, 512) 11801 ['activation_2[0][0]']
60
conv2d_37 (Conv2D) (None, 7, 7, 512) 23598 ['conv2d_28[0][0]']
08
conv2d_38 (Conv2D) (None, 7, 7, 512) 13158 ['activation_2[0][0]']
4
add_3 (Add) (None, 7, 7, 512) 0 ['conv2d_37[0][0]',
'conv2d_38[0][0]']
activation_3 (Activat (None, 7, 7, 512) 0 ['add_3[0][0]']
ion)
flatten_4 (Flatten) (None, 25088) 0 ['activation_3[0][0]']
dense_9 (Dense) (None, 1000) 25089 ['flatten_4[0][0]']
000
================================================================================
Total params: 29991912 (114.41 MB)
Trainable params: 29991912 (114.41 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________
MobileNet, desarrollada por Google, está optimizada para dispositivos móviles y embebidos. Utiliza separable depthwise convolutions para reducir la cantidad de parámetros y cálculos.
library(keras)
model <- keras_model_sequential() %>%
layer_conv_2d(filters = 32, kernel_size = c(3, 3), strides = c(2, 2), activation = 'relu', input_shape = c(224, 224, 3)) %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), activation = 'relu') %>%
layer_conv_2d(filters = 64, kernel_size = c(1, 1), activation = 'relu') %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), strides = c(2, 2), activation = 'relu') %>%
layer_conv_2d(filters = 128, kernel_size = c(1, 1), activation = 'relu') %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), activation = 'relu') %>%
layer_conv_2d(filters = 128, kernel_size = c(1, 1), activation = 'relu') %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), strides = c(2, 2), activation = 'relu') %>%
layer_conv_2d(filters = 256, kernel_size = c(1, 1), activation = 'relu') %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), activation = 'relu') %>%
layer_conv_2d(filters = 256, kernel_size = c(1, 1), activation = 'relu') %>%
layer_depthwise_conv_2d(kernel_size = c(3, 3), strides = c(2, 2), activation = 'relu') %>%
layer_conv_2d(filters = 512, kernel_size = c(1, 1), activation = 'relu') %>%
layer_global_average_pooling_2d() %>%
layer_dense(units = 1000, activation = 'softmax')
model %>% compile(
loss = 'categorical_crossentropy',
optimizer = optimizer_adam(),
metrics = 'accuracy'
)
summary(model)
Model: "sequential_3"
________________________________________________________________________________
Layer (type) Output Shape Param #
================================================================================
conv2d_45 (Conv2D) (None, 111, 111, 32) 896
depthwise_conv2d_5 (DepthwiseConv (None, 109, 109, 32) 320
2D)
conv2d_44 (Conv2D) (None, 109, 109, 64) 2112
depthwise_conv2d_4 (DepthwiseConv (None, 54, 54, 64) 640
2D)
conv2d_43 (Conv2D) (None, 54, 54, 128) 8320
depthwise_conv2d_3 (DepthwiseConv (None, 52, 52, 128) 1280
2D)
conv2d_42 (Conv2D) (None, 52, 52, 128) 16512
depthwise_conv2d_2 (DepthwiseConv (None, 25, 25, 128) 1280
2D)
conv2d_41 (Conv2D) (None, 25, 25, 256) 33024
depthwise_conv2d_1 (DepthwiseConv (None, 23, 23, 256) 2560
2D)
conv2d_40 (Conv2D) (None, 23, 23, 256) 65792
depthwise_conv2d (DepthwiseConv2D (None, 11, 11, 256) 2560
)
conv2d_39 (Conv2D) (None, 11, 11, 512) 131584
global_average_pooling2d (GlobalA (None, 512) 0
veragePooling2D)
dense_10 (Dense) (None, 1000) 513000
================================================================================
Total params: 779880 (2.98 MB)
Trainable params: 779880 (2.98 MB)
Non-trainable params: 0 (0.00 Byte)
________________________________________________________________________________