LeNet-5, primera red convolucional

La Máquina Oráculo

Rubén Rodríguez Abril – 13 de abril de 2022

-LeNet5

Primera red convolucional

El objeto de este artículo es construir y ejecutar el modelo de red neuronal LeNet5, especializada en el reconocimiento de dígitos manuscritos del 0 al 9, utilizando para ello las bibliotecas de Keras y TensorFlow de Python.

LeNet5 es una de las redes convolucionales más fáciles de construir, y por ello, la programación de su modelo es considerado como el «Hola mundo» del Aprendizaje Profundo.

Para construir la red tomaremos como referencia el artículo original de Yann LeCun del año 1998, cuyo contenido ya fue analizado en nuestro artículo dedicado a las redes convolucionales.

Programas

Elaboraremos dos programas en Python: el primero de ellos construirá, entrenará y evaluará el modelo y tras ello lo guardará en disco. El segundo programa cargará el modelo ya guardado y realizará predicciones con el mismo.

El programa se articula internamente en siete secciones diferentes:

  • Importación de las bibliotecas de Python necesarias para la tarea.
  • Carga del banco de imágenes.
  • Construcción de la arquitectura de red.
  • Determinación de la función de pérdida, y el modo de optimización (modificación de parámetros entrenables) del modelo.
  • Entrenamiento del modelo.
  • Comprobación de la eficiencia del modelo ya entrenado.
  • Almacenamiento en disco.
Esquema de la red convolucional LeNet5

[1] -Importación de bibliotecas

Para nuestro primer programa, nos bastará con importar las bibliotecas de TensorFlow y Keras, así como los objetos correspondientes al «modelo secuencial» (más abajo se expondrá este concepto) y las diferentes capas que componen LeNet-5 (convolucional, densa, etc…). Ello se hace mediante las líneas indicadas a la derecha

import tensorflow as tf
from tensorflow import keras
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten

[2] -Carga del banco de imágenes

La segunda sección del programa tiene por misión cargar el banco de imágenes usado para entrenar y testar la red.

Estructura de los datos

La base de datos a emplear es MNIST, que está compuesta de miles de gráficos en cada uno de los cuales se traza un dígito manuscrito del 0 al 9. De dichos gráficos, 60.000 son imágenes de entrenamiento y otras 10.000 imágenes de evaluación. Cada imagen es un cuadro con dimensiones de 28×28 píxeles. Los píxeles representan gamas de grises que oscilan entre 0 (negro) y 255 (blanco). Estas imágenes vienen acompañadas de una etiqueta (un entero del 0 al 9) que señala de qué dígito se trata.

En Keras y TensorFlow las bases de datos se descargan de la red o se extraen de un archivo local. Y tras ello, se organizan en matrices n-dimensionales denominadas tensores. En nuestro programa, crearemos tres tensores que almacenarán las imágenes de entrenamiento (entrena_x), validación (val_x), y comprobación (comp_x). Los tensores tienen tres dimensiones: ancho, alto e índice de la imagen. El rango de las dos primeras se extiende de 0 a 27. El de la tercera, de 0 a 59.999 (datos de entrenamiento) o de 0 a 4.999 (datos de validación y comprobación). Los píxeles representan gamas de grises. Oscilan entre 0 (negro) y 255 (blanco).

Las etiquetas se almacenarán a su vez en tres tensores de una dimensión, llamados entrena_yval_y y comp_y. La única dimensión señala el índice de la imagen de que se trate. El contenido de cada elemento es un entero del 0 al 9, como ya hemos señalado.

Carga de la base de datos

La instrucción load_data() descarga por entero la base de datos MNIST desde el repositorio remoto de Keras y la almacena, bajo la forma de cuatro tensores, en la memoria de la GPU.

Como el lector puede fácilmente comprobar, dos de los cuatro tensores generados por la instrucción son de entrenamiento (entrena_x y entrena_y) y los otros dos de comprobación (test_x y test_y).

Reestructuración de los datos

Los datos de comprobación serán a su vez divididos en dos: con las primeras 5.000 imágenes y etiquetas se construirán los dos tensores de validación (val_xval_y), usados para comprobar cómo evoluciona el modelo durante el entrenamiento. Con las últimas 5.000 imágenes y etiquetas construiremos los dos tensores de comprobación (comp_xcomp_y) o evaluación del modelo al final, una vez este haya sido entrenado por completo.

(entrena_x, entrena_y), (test_x, test_y) = keras.datasets.mnist.load_data()
val_x = test_x[:5000]
val_y = test_y[:5000]
comp_x = test_x[5000:]
comp_y = test_y[5000:]

[3] -Construcción del modelo

Para Keras, una red neuronal es un modelo. Y un perceptrón multicapas orientado hacia adelante es un modelo secuencial, que está compuesto de capas que se van agregando sucesivamente, desde la entrada hasta la salida. Tanto el modelo como las capas son considerados como objetos en el lenguaje Python, que se crean a partir de clases.

Procedemos a crear el modelo, que denominamos lenet5, mediante la ejecución del método Sequential().

Tras ello, vamos agregando una por una las diferentes capas de la arquitectura, tal y como están descritas en el célebre artículo de Yann LeCun y sus compañeros del año 1998. Las capas son objetos pertenecientes a las clases Conv2D, MaxPooling2, Flatten y Dense, dependiendo del tipo de capa de que se trate. Cada vez que se crea una de ellas, se debe agregar al modelo mediante el método add:

Capas convolucionales

Como vemos, dos capas convolucionales con kernel 5×5 se intercalan con otras dos de max pooling, de dimensiones 2×2. La primera capa convolucional, consta de 6 mapas (aquí llamados filters o filtros) y está dotada de padding same (no se produce, pues, disminución de resolución). La segunda capa convolucional consta de 16 filtros, y está está dotada de padding valid, que provoca una disminución de resolución en la capa siguiente.

Capas densas y pooling

La transición de la sección convolucional a las capas densamente conectadas se realiza por una capa Flatten, que transforma las matrices de la capa de pooling en una array unidimensional, destinado a alimentar a la siguiente capa densa. Las tres últimas capas, densamente conectadas con la anterior, están dotadas de 120, 84 y 10 neuronas respectivamente.

La función de activación de todas las neuronas es la tangente hiperbólica, salvo las de la última capa de salida, que tienen una función softmax, tal y como es usual en las redes clasificadoras.

lenet5 = Sequential()
lenet5.add(Conv2D(input_shape=(28,28,1), filters=6, kernel_size=(5,5), strides=1, padding='same', activation='tanh'))
lenet5.add(MaxPooling2D(pool_size=(2,2)))
lenet5.add(Conv2D(input_shape=(14,14,1), filters=16 , kernel_size=(5,5), strides=1, padding='same', activation='tanh'))
lenet5.add(MaxPooling2D(pool_size=(2,2)))
lenet5.add(Flatten())
lenet5.add(Dense(120, activation='tanh'))
lenet5.add(Dense(84, activation='tanh'))
lenet5.add(Dense(10, activation='softmax'))

[4] -Entrenamiento de la red

Tras la construcción de la arquitectura de red, pasamos a la compilación del modelo. Tenemos que determinar cuál es la función de pérdida, y qué sistema de optimización (es decir, de modificación de pesos sinápticos y de sesgos) va a ser utilizado durante los entrenamientos.

Parámetros de aprendizaje

Como es usual en las redes clasificadoras, la función de pérdida es la entropía cruzada, que compara el resultado ofrecido por la capa de salida de la red con un vector de verdad subyacente (ground truth) de tipo 1-de-N (o one-hot), en el que todos los valores se ponen a cero salvo el correspondiente a la solución correcta (es decir, una expresión de tipo [0,0,0,1,0,0,0,0,0,0]).

Dado que las etiquetas de MNIST no consisten en vectores one-hot sino en un número entero del cero al nueve, es necesario utilizar una variante de la entropía categórica denominada entropía categoría dispersa (sparse categorical crossentropy), específica para etiquetas numéricas de clases, y que transforma números enteros del 0 al 9 en vectores one-hot.

Entrenamiento de la red

Todo está listo, pues, para entrenar al modelo. Ello se realiza mediante una simple línea de código. El método fit() recibe cuatro argumentos: El tensor de imágenes de entrenamiento (entrena_x), el tensor de etiquetas de entrenamiento (entrena_y), el número de épocas (epochs), en este caso cinco, y los datos de validación (val_x y val_y). Soi se tiene habilitada un GPU, todos los datos de entrenamiento, así como el código a ejecutar, son enviados a dicha GPU, que es donde tienen lugar los procesos de aprendizaje. LeNet-5 tiene unos 60.000 parámetros, que tras ser calculados por la tarjeta gráfica serán enviados al chipset y almacenados en la memoria RAM.

Tal y como se puede leer en la última línea, la precisión final (accuracy) de nuestro modelo supera el 98% con los datos de entrenamiento, y casi un 97% con los datos de validación.

lenet5.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
lenet5.fit(entrena_x, entrena_y, epochs=5, validation_data=(val_x, val_y))

Epoch 1/5
1875/1875 [==============================] – 54s 29ms/step – loss: 0.1732 – accuracy: 0.9461 – val_loss: 0.1029 – val_accuracy: 0.9656

Epoch 2/5
1875/1875 [==============================] – 54s 29ms/step – loss: 0.0780 – accuracy: 0.9759 – val_loss: 0.1053 – val_accuracy: 0.9648

Epoch 3/5
1875/1875 [==============================] – 54s 29ms/step – loss: 0.0573 – accuracy: 0.9812 – val_loss: 0.0998 – val_accuracy: 0.9672

Epoch 4/5
1875/1875 [==============================] – 54s 29ms/step – loss: 0.0468 – accuracy: 0.9852 – val_loss: 0.0870 – val_accuracy: 0.9702

Epoch 5/5
1875/1875 [==============================] – 54s 29ms/step – loss: 0.0465 – accuracy: 0.9854 – val_loss: 0.1045 – val_accuracy: 0.9666

[5] -Evaluación de la red

La evaluación del modelo se realiza invocando el método evaluate(), que recibe como argumento el tensor de imágenes de comprobación y el tensor de las etiquetas de dichas imágenes.

El sistema retorna entonces el valor de la función de pérdida y el porcentaje de acierto (accuracy) de la red.

La precisión es de más del 98%.

lenet5.evaluate(comp_x, comp_y)
157/157 [==============================] – 2s 11ms/step – loss: 0.0401 – accuracy: 0.9860 [0.04012678563594818, 0.9860000014305115]

[6] -Almacenamiento del modelo

Si cerramos definitivamente tanto la sesión en Keras como el compilador de Python, los datos relativos a la arquitectura de la red y los parámetros entrenables de esta última se perderán. Para prevenirlo, hemos de guardar y exportar estos datos por medio del método save() donde ruta* es la ruta del directorio donde se guardará el modelo (recordemos que en Python es necesario hacer uso de la doble barra \\ para marcar la ruta).

lenet5.save('C:/Users/Modelos/lenet5')

-Segundo programa: recarga y uso del modelo

El segundo programa se encargará de cargar el modelo que previamente hemos guardado en disco y realizar predicciones con él a partir de imágenes aleatorias extraídas de MNIST.

Se compone de las siguientes secciones:

-Importación de las bibliotecas necesarias.

-Carga de la base de datos

-Carga del modelo.

-Selección de una muestra de la base de datos, e impresión de su imagen.

-Realización de la predicción respecto de dicha imagen.

[1] -Importación de bibliotecas

En este nuevo programa programa, junto con las bibliotecas de TensorFlow y Keras importaremos también numpy (para crear arrays), random (generación de números aleatorios) y parte de matplotlib (con el objetivo de dibujar las imágenes de entrada). Para cargar el modelo, es necesario importar también previamente los objetos Sequential, así como los correspondientes a los diferentes tipos de capas (Conv2D, Dense, etc…).
import tensorflow as tf
from tensorflow import keras
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten
import numpy as np
import random
import matplotlib.pyplot as plt

[2] -Carga de la base de datos y del modelo

Bajamos de nuevo la base de datos MNIST, utilizando las mismas líneas de código que en el primer programa, y cargamos el modelo desde el disco duro mediante el método load_model():

(entrena_x, entrena_y), (test_x, test_y) = keras.datasets.mnist.load_data()
val_x = test_x[:5000]
val_y = test_y[:5000]
comp_x = test_x[5000:]
comp_y = test_y[5000:]
 
lenet5 = keras.models.load_model('C:/Users/Modelos/lenet5')

[3] -Impresión de una imagen de muestra

Extraemos un número aleatorio del 0 al 9.999 (10.000 es el tamaño del tensor de imágenes de comprobación). Tras ello, cargamos la imagen correspondiente a dicho número. Por último, la imprimimos en la pantalla con la instrucción plt.imshow():
muestra = random.randrange(0,(len(comp_x)-1))
plt.imshow(comp_x[muestra], cmap=plt.cm.binary)
print('/n' + 'Imprimimos la imagen de la muestra ' + str(muestra) + ':')

[4] - Realización de una predicción

El último paso es la realización de la predicción, que se realiza con el método lenet5.predict(), que toma por argumento un tensor de entrada. Cabe señalar que el modelo, la red, no toma por entrada una imagen, sino un tensor. Por ese motivo, hemos de construir previamente un tensor compuesto de una sola imagen. Ello se realiza mediante el método np.expand_dims(). La imagen, que hasta entonces tiene dimensiones (28,28,1), se transforma en un tensor de (28,28,1,1), donde la nueva dimensión señala el número de imágenes del nuevo tensor. El resultado de la predicción es un array, prediccion, cuyos 10 elementos señalan la puntuación atribuida por la red a cada dígito. Por último, la instrucción np.argmax(prediccion) señala la neurona de salida (el elemento del array) que ha obtenido mayor puntuación.
entrada = np.expand_dims(comp_x[muestra], axis=0)
prediccion = lenet5.predict(entrada)
print('La imagen corresponde al ' + str(np.argmax(prediccion)))

- Conclusión

En este tutorial hemos construido una red neuronal relativamente simple, LeNet-5, utilizando para ello, y por razones pedagógicas, la menor cantidad de código posible. Animamos a nuestros lectores a estudiar en profundidad el lenguaje Python, y particularmente sus bibliotecas Keras y TensorFlow, con ayuda de las cuales podrá implementar la mayor parte de las ideas expuestas en esta serie.

Nuestro siguiente tutorial mostrará cómo podemos cargar otras bases de datos e incluso hacer uso de imágenes propias para entrenar nuestras redes.

Primer Programa

¡Copia y pega el código de abajo en un cuaderno online de Colab y observa cómo entrena la red!

#Importamos las bibliotecas necesarias
import tensorflow as tf
from tensorflow import keras
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten
 
#Cargamos la base de datos y creamos los tres tensores
(entrena_x, entrena_y), (test_x, test_y) = keras.datasets.mnist.load_data()
val_x = test_x[:5000]
val_y = test_y[:5000]
comp_x = test_x[5000:]
comp_y = test_y[5000:]
 
#Construcción del modelo LeNet5
lenet5 = Sequential()
lenet5.add(Conv2D(input_shape=(28,28,1), filters=6, kernel_size=(5,5), strides=1, padding='same', activation='tanh'))
lenet5.add(MaxPooling2D(pool_size=(2,2)))
lenet5.add(Conv2D(input_shape=(14,14,1), filters=16 , kernel_size=(5,5), strides=1, padding='same', activation='tanh'))
lenet5.add(MaxPooling2D(pool_size=(2,2)))
lenet5.add(Flatten())
lenet5.add(Dense(120, activation='tanh'))
lenet5.add(Dense(84, activation='tanh'))
lenet5.add(Dense(10, activation='softmax'))
 
#Establecemos los parámetros de aprendizaje
lenet5.compile(optimizer='adam', loss='sparse_categorical_crossentropy', metrics=['accuracy'])
 
#Entrenamos el modelo
lenet5.fit(entrena_x, entrena_y, epochs=10, validation_data=(val_x, val_y))
 
#Evaluación de la red
lenet5.evaluate(comp_x, comp_y)
 
#Guardamos el modelo
lenet5.save('C:/Users/Modelos/lenet5')

Segundo Programa

Copia y pega el código de abajo en un cuaderno online de Colab y observa cómo entrena la red.

#Importamos las bibliotecas necesarias
import tensorflow as tf
from tensorflow import keras
from keras.models import Sequential
from keras.layers import Conv2D, MaxPooling2D, Dense, Flatten
import numpy as np
import random
import matplotlib.pyplot as plt
 
#Cargamos la base de datos y creamos los tres tensores
(entrena_x, entrena_y), (test_x, test_y) = keras.datasets.mnist.load_data()
val_x = test_x[:5000]
val_y = test_y[:5000]
comp_x = test_x[5000:]
comp_y = test_y[5000:]
 
#Cargamos el modelo
lenet5 = keras.models.load_model('C:/Users/Modelos/lenet5')
 
#Imprimimos una imagen de muestra
muestra = random.randrange(0,(len(comp_x)-1))
plt.imshow(comp_x[muestra], cmap=plt.cm.binary)
print('/n' + 'Imprimimos la imagen de la muestra ' + str(muestra) + ':')
 
#Realizamos la predicción
entrada = np.expand_dims(comp_x[muestra], axis=0)
prediccion = lenet5.predict(entrada)
print('La imagen corresponde al ' + str(np.argmax(prediccion)))

Copyright de La Máquina de Oráculo 2021

info@lamaquinaoraculo.com

Síguenos en Redes Sociales