Creación de una red convolucional con Tensorflow

Dicen que una imagen vale más que mil palabras, pero ser capaz de interpretarlas y extraer información relevante de ellas vale mucho más. Para llevar a cabo este propósito, la inteligencia artificial nos brinda herramientas muy interesantes. 

En artículos anteriores del blog pudimos ver como crear una red neuronal, un perceptrón, desde cero. Sin embargo, para problemáticas más complejas como detección de enfermedades en imágenes, quizás una red neuronal simple no sea la mejor solución. Es por ello que hoy introducimos las redes neuronales convolucionales, un tipo de red neuronal artificial orientada a la utilización de imágenes. 

El objetivo de este artículo es llevar a cabo una demo sencilla de como crear una red neuronal mediante la librería de machine learning Tensorflow. No obstante, antes de entrar en el código es conveniente que se definan correctamente un conjunto de conceptos relacionados con esta implementación.

Conceptos básicos

Las redes neuronales convolucionales son un tipo de redes neuronales orientadas al uso de imágenes como entrada o tipos de datos que sean representables en formato matricial. Antes de adentrarte en profundidad en este artículo es conveniente conocer el concepto de una red neuronal y del perceptrón simple. Para ello aconsejo leer los artículos de nuestro blog de Damavis:

Tras repasar los conceptos básicos del perceptrón podemos definir el vocabulario que necesitaremos en este caso. En primer lugar, ya conocemos lo que es una red neuronal artificial, que podríamos definir como un conjunto de neuronas donde cada una de ellas representan funciones simples y que al conectarse entre sí crean un entramado de conexiones más complejo que permite extraer patrones de un conjunto de datos.

A diferencia del artículo mencionado anteriormente, en este caso no crearemos la red convolucional desde cero sino que utilizaremos la librería de machine learning de Tensorflow. Esta librería nos permite definir los hiperparámetros y las capas de la red de una forma más sencilla, así como el entrenamiento y la evaluación de resultados, entre muchas otras funcionalidades. Es decir, una visión completa del pipeline típico en la creación de estos modelos de inteligencia artificial.

Los potenciómetros de nuestra radio

Así pues, ya han aparecido los otros dos conceptos que debemos definir antes de entrar de lleno en el código, los hiperparámetros y las capas. Como hemos visto en los artículos anteriores las neuronas de la red poseen diferentes tipos de parámetros que definen la salida de cada neurona. Distinguimos dos tipos: parámetros e hiperparámetros. En realidad, los parámetros, conocidos como pesos, no son más que los distintos factores que definen propiamente la función que representa cada neurona. Es decir, si la neurona representa la función de una recta definida por la siguiente fórmula:

Damavis - Fórmula

Podemos observar que la variable dependiente ŷ vendrá determinada por dos factores o pesos,  ω0  y  ω1  sobre la variable independiente x . El valor real de estos pesos se deriva del entrenamiento siendo así conocidos como parámetros entrenables.

Por otro lado, encontramos los hiperparámetros que, a diferencia de los otros parámetros, son definidos antes de comenzar el entrenamiento. Además, determinan el proceso de aprendizaje y son los valores que mediante un proceso iterativo durante la búsqueda del mejor modelo serán modificados para encontrar el conjunto de hiperparámetros que desemboquen en un mayor rendimiento.

Un ejemplo de hiperparámetro en cualquier red neuronal puede ser el ratio de aprendizaje o learning rate que define cuál debe ser la variación de los pesos en cada iteración del proceso de aprendizaje para acercarse al punto óptimo. Es decir, define cómo de grande debe ser cada paso que dé nuestro modelo durante el entrenamiento. En el caso de las redes convolucionales otro hiperparámetro habitual podría ser la dimensión de las imágenes de entrada o las medidas de los filtros convolucionales en cada capa.

No solo los héroes llevan capa

De esta forma introducimos el último concepto básico: las capas y sus propiedades. Tal y como hemos podido entender, las redes neuronales de cualquier tipo se definen por neuronas conectadas entre sí. Las conexiones de estas neuronas no son anárquicas sino que definen una cierta jerarquía entre ellas. Esta organización permite agrupar neuronas que poseen finalidades similares. Así pues, a raíz de la arquitectura de una red neuronal artificial se ejecutarán un conjunto de neuronas en un determinado orden. Comúnmente, en las redes neuronales más sofisticadas, estas agrupaciones son conocidas como capas. Siendo así la sucesión y conexión entre distintas capas la que define finalmente la arquitectura de un modelo en concreto.

Las capas suelen tener una finalidad concreta donde podemos encontrar multitud de tipos. Entre los casos más habituales encontramos: capas de reducción de dimensionalidad, como las de Average Pooling o las de Max Pooling; capas de desconexión de neuronas para evitar el overfitting, como las capas de Dropout; o capas convolucionales, entre muchas otras.

En cuanto a las capas convolucionales cabe hacer una mención especial ya que no es casual que aporten el nombre propio a las redes que implementaremos en este cuaderno. Una convolución puede definirse matemáticamente con la siguiente fórmula:

Damavis - Fórmula

Si atendemos a ella podemos observar que esta definición implica dos funciones:  f  y  g  a lo largo de un dominio  t . La convolución se puede entender como el producto de dos funciones cuyo resultado define la magnitud de ambas cuando se solapan. Es decir, se desliza una función sobre otra integrando el producto de ambas y el resultado define una nueva función.

El entendimiento de su uso en las redes neuronales que implican imágenes es inmediato si se entiende la matriz de una imagen como una de las funciones. Por otro lado, la segunda función que se deslizará a lo largo de la imagen será un filtro. Los filtros no son más que matrices, con una dimensión menor a la de la imagen original, que representan un patrón en concreto. En las capas más tempranas de la red estos filtros definirán patrones simples como líneas rectas o bordes dentro de la imagen. Mientras que en las capas más profundas estos filtros serán más complejos definiendo así patrones sobre la imagen mucho más elaborados.

Así pues, el deslizamiento del filtro sobre la imagen como una convolución de estas dos funciones tendrá como resultado una nueva imagen, una nueva

función, donde se resaltarán los píxeles que encajen con el patrón que representa el filtro.

Una vez conocidos los aspectos básicos de las redes neuronales convolucionales podemos pasar a su implementación en código mediante Tensorflow.

Implementación

A continuación se describen los pasos más relevantes del desarrollo en Python para la creación de la red convolucional. La implementación completa se encuentra en el siguiente notebook de Jupyter: damavis/blog-posts-code/notebooks/cnn_demo.ipynb

Datos: Malaria Dataset

El dataset utilizado pertenece al catálogo de datos de Tensorflow Datasets. Se ha utilizado este conjunto de datos dado que el objetivo de este cuaderno no reside en llevar a cabo el preprocesamiento de ellos. De esta manera obtenemos un dataset de imágenes que cumple con los requisitos que deseamos, limpio y enfocado para la problemática de clasificación de imágenes.

En el dataset de Malaria se representan imágenes de de diapositivas de frotis de sangre fina de células segmentadas. Tendremos dos clases: células parasitadas y células no infectadas.

Un vistazo a los datos

Si observamos los datos que vamos a utilizar podemos ver en la siguiente imagen un ejemplo de las distintas clases de nuestro dataset. Por un lado disponemos de las imágenes de células parasitadas y por el otro de células no infectadas. El dataset se compone en nuestro caso de 19.291 imágenes de entrenamiento y 8.267 de testeo.

(train_ds, test_ds), info = tfds.load("malaria", split=['train[:70%]', 'train[70%:]'], shuffle_files=True, with_info=True, as_supervised=True)

Mediante la función tfds.load() conseguimos cargar el dataset de Malaria del catálogo de Tensorflow.

Imágenes dataset - Damavis

Figura 1. Ejemplos de las imágenes del dataset.

Preprocesamiento del dataset

A pesar de que los datos ya se encuentren preprocesados y limpios debemos realizar un leve preprocesamiento para poder cargar los datos en nuestro modelo correctamente. En este caso la transformación más relevante es redimensionar la imagen a un tamaño común para todo el conjunto y que no resulte demasiado grande. Aunque pueda parecer contraintuitivo, no siempre una mayor cantidad de píxeles desemboca en un mayor rendimiento del modelo. Además, cuanto mayores son las dimensiones de las imágenes más conjuntos de píxeles deberá procesar la red y por tanto consumirá más tiempo.

Para la realización de esta demo un tamaño de 28×28 píxeles es suficiente.

def preprocessing(img, label):
  # Transformamos el tipo de la imagen
  image_converted = tf.image.convert_image_dtype(img, tf.float32)
  # Agregamos padding a la imagen
  image_resized = tf.image.resize(image_converted, (IMG_HEIGHT, IMG_WIDTH))
 
  return image_resized, label
 
processed_train_ds = train_ds.cache().map(preprocessing).batch(BATCH_SIZE)
processed_test_ds = test_ds.cache().map(preprocessing).batch(BATCH_SIZE)

Gracias a la función de preprocessing() podemos mapear las transformaciones necesarias para cada una de las imágenes de nuestro dataset. De esta manera, conseguimos dos nuevas particiones de datasets donde todas las imágenes poseen las mismas propiedades.

Creación del modelo

Llegados a este punto podemos llevar a cabo la construcción de nuestra red convolucional. Mediante Tensorflow definir la organización secuencial de capas es muy sencillo. En este caso utilizaremos las capas mencionadas en el apartado de conceptos básicos de este artículo.

def build_model():
    model = tf.keras.Sequential([
        tf.keras.Input(shape=(IMG_HEIGHT, IMG_WIDTH, CHANNELS)),
        tf.keras.layers.Conv2D(filters=16, 
                               kernel_size=3, 
                               activation='relu', 
                               padding='same'),
 
        tf.keras.layers.MaxPool2D(),
 
        tf.keras.layers.Conv2D(filters=32, 
                               kernel_size=3, 
                               activation='relu', 
                               padding='same'),
 
        tf.keras.layers.MaxPool2D(),
        tf.keras.layers.Dropout(0.2),
        
        tf.keras.layers.Flatten(),
 
        tf.keras.layers.Dense(8, activation='relu'),
        tf.keras.layers.Dropout(0.2),
        
        tf.keras.layers.Dense(1, activation='sigmoid')
    ])
    
    return model

La arquitectura de la red se compone de una primera capa de entrada, seguido de capas de convolución, max pooling, dropout y capas densas. Las capas de convolución extraerán los patrones característicos de cada imagen. Las capas de max pooling reducirán las dimensiones de la imagen mediante un proceso de selección del valor más alto en un conjunto de píxeles. Por último las capas densas no son más que conjuntos de neuronas interconectadas todas ellas entre sí, también conocidas como capas fully connected. Las capas de dropout nos mitigarán el overfitting del modelo desconectando aleatoriamente algunas conexiones entre neuronas.

CNN_model.compile(
    optimizer=tf.keras.optimizers.Adam(learning_rate=LR),
    loss=tf.keras.losses.binary_crossentropy,
    metrics=[tf.keras.metrics.AUC(name='auc')]
)

En Tensorflow una vez creado el modelo este debe ser compilado debido a su gestión interna de los tensores. Es en esta función compile() donde definiremos algunos de los hiperparámetros como el learning rate, la función de pérdida o la métrica para evaluar el modelo como en este caso el AUC.

Entrenamiento

En este apartado se ha considerado llevar a cabo un entrenamiento corto de tan solo 5 iteraciones o épocas. Tan solo con 5 iteraciones ya podemos observar que la pérdida del modelo disminuye tras cada una de ellas y que nuestra métrica de evaluación aumenta sutilmente. La métrica utilizada ha sido el AUC, area under the curve, que nos define un tipo de curva ROC que relaciona la tasa de verdaderos positivos con la de falsos positivos. Cuanto mayor es su valor, mayor rendimiento tendrá nuestro modelo. Es una métrica comúnmente utilizada en la detección de enfermedades.

history = CNN_model.fit(
    processed_train_ds, 
    epochs=EPOCHS
)
Salida de la ejecución de la función - Damavis

Figura 2. Salida de la ejecución de la función fit().

Evaluación

CNN_model.evaluate(processed_test_ds)
Salida de la ejecución de la función evaluate() - Damavis

Figura 3. Salida de la ejecución de la función evaluate().

En la evaluación del modelo se observa que su rendimiento en el subconjunto de test es óptimo alcanzando un 98,47% de AUC.

That’ s all folks!

Esto es todo… o casi todo. En esta publicación hemos podido ver lo sencillo que resulta crear un modelo de inteligencia artificial que sea capaz de clasificar imágenes. Sin embargo, hemos partido de unas condiciones muy concretas ya que no hemos tenido que realizar muchas de las fases del pipeline habitual.

No ha sido necesario hacer un preprocesamiento demasiado exhaustivo de los datos, ni llevar a cabo ningún proceso iterativo para conocer la arquitectura que nos condujera a un modelo con un alto rendimiento y tampoco hemos necesitado serializar y desplegar el modelo en producción para que fuese consumido por un cliente. Todo ello son pasos importantes dentro del pipeline de un proyecto de machine learning y que al igual que en esta publicación requieren tiempo.

En las próximas publicaciones abordaremos cada una de estas fases en profundidad. Y así podremos obtener una visión completa de todo el proceso.

Te animamos a compartir este artículo en redes. No olvides mencionarnos para poder conocer tu opinión. ¡Hasta pronto!
Nadal Comparini
Nadal Comparini
Artículos: 10