Ir al contenido principal

Clasificación multiclase con deep learning y keras


Dentro de los diferentes tipos de problemas que trata de abordar el aprendizaje automático, uno de los más interesantes es la clasificación. Básicamente consiste en, dado un conjunto de elementos, asignarle a una etiqueta, donde cada etiqueta corresponde a una clase diferente. Por ejemplo, una aplicación interesante y bastante actual es clasificar a los visitantes de una web en dos categorías, los que, con alta probabilidad, pulsarán sobre un anuncio concreto y los que no. Este es un ejemplo de clasificación binaria, porque sólo hay dos categorías. Otro ejemplo puede ser el que ya vimos en el artículo sobre reconocimiento de caracteres manuscritos usando K-NN. En este caso hablábamos de un problema de clasificación multiclase en el que asignábamos a cada imagen una sola etiqueta de un conjunto de diez posibles (los dígitos del 0 al 9). Vimos que K-NN es un enfoque sencillo y efectivo para este tipo de problemas, pero hay otras alternativas. Vamos a enfrentarnos a un problema similar pero con un dataset y una técnica diferente.
Vamos a usar una red neuronal para resolver un problema de clasificación multiclase similar a MNIST (si no tienes muy claro qué es eso de una red neuronal puedes leer un artículo que escribí en mi antiguo blog sobre el perceptrón). Se trata del conjunto fashion MNIST, que creó la empresa Zalando con imágenes de prendas de vestir. Este dataset contiene 60.000 imágenes de prendas (28x28 en escala de grises), y 10.000 imágenes para test. Las categorías con su etiqueta son las siguientes:

0 T-shirt/top
1 Trouser
2 Pullover
3 Dress
4 Coat
5 Sandal
6 Shirt
7 Sneaker
8 Bag
9 Ankle boot

Se trata de, dada una imagen, asignarle su etiqueta numérica correspondiente. Para crear y entrenar la red neuronal vamos a recurrir a la librería keras, que nos ofrece una capa de abstracción sobre TensorFlow. Keras, además de una interfaz de alto nivel para el diseño de modelos de aprendizaje basado en redes de neuronas, nos permite utilizar una serie de datasets que están ya integrados. Las siguiente líneas nos permiten descargar el dataset fashion MNIST y ponerlo en memoria.

from keras.datasets import fashion_mnist
(x_train, y_train), (x_test, y_test) = fashion_mnist.load_data()

Si quiere visualizar una de las imágenes para ver qué pinta tienen puede hacerlo con matplotlib.

import matplotlib.pyplot as plt

plt.imshow(x_test[0], cmap="gray")
plt.show()



Son imágenes de 28x28 píxeles en escala de grises (con 256 niveles de gris). No son muy vistosas, pero tienen la ventaja de que son muy manejables a la hora de alimentar nuestra red neuronal. Como las imágenes son de 28x28 necesitamos 784 entradas (28x28=784) ya que a cada pixel le corresponde una entrada en la red. Así que lo primero que tenemos que hacer es adaptar las imágenes a un formato más manejable.
Primero transformamos cada imagen de una matriz de 28x28 a un vector de tamaño 784. Por otra parte, cada pixel es un entero en el rango 0-255. Lo vamos a normalizar para que cada pixel sea un número real que esté en el rango 0-1.
También vamos a transformar las etiquetas de números enteros a un vector binario (one-hot), que es más conveniente a la hora de entrenar la red. Esto lo hacemos con la función to_categorical() de keras, que nos devuelve una matriz en la que cada elemento es un vector de diez posiciones donde todos sus valores están a 0 menos uno de ellos. Por ejemplo, la etiqueta con el valor 2 quedaría como 0010000000 (no se trata del valor binario de la etiqueta, sino de asignar una posición concreta al 1 de cada etiqueta).

from keras.utils import to_categorical

x_train = x_train.reshape((60000, 784))
x_train = x_train.astype('float32') / 255
y_train = to_categorical(y_train)

x_test = x_test.reshape((10000, 784))
x_test = x_test.astype('float32') / 255
y_test = to_categorical(y_test)

Del conjunto de entrenamiento vamos a separar una porción de ellos como conjunto de validación. Este subconjunto lo vamos a utilizar para, durante el entrenamiento, ir viendo si el modelo sigue funcionando bien con datos que no están en el conjunto de entrenamiento. Esto lo hacemos para evitar el sobreentrenamiento u overfitting, que ocurre cuando un modelo termina "aprendiéndose de memoria" los datos de entrenamiento, con lo cual funciona muy bien con estos datos, pero no con otros datos de fuera del conjunto de entrenamiento. Es decir, el modelo no es capaz de generalizar.

import numpy as np

np.random.seed(42)

# barajamos el dataset
permutation = np.random.permutation(x_train.shape[0])
x_train = x_train[permutation]
y_train = y_train[permutation]

# obtenemos el conjunto de validación
num_val = 10000
x_val = x_train[:num_val]
x_train = x_train[num_val:]
y_val = y_train[:num_val]
y_train = y_train[num_val:]

Ahora llega el momento de diseñar una arquitectura para nuestra red neuronal. No hay reglas de oro a la hora de diseñar una arquitectura, hay mucho de arte, de intuición y de experiencia. Vamos a partir de una arquitectura con una capa de entrada, dos capas ocultas y una de salida.

from keras import models, layers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_shape=(784,))) # 28x28=784
model.add(layers.Dense(128, activation='relu'))
model.add(layers.Dense(10, activation='softmax'))

Esquemáticamente nuestra red tiene la siguiente arquitectura.



Sin entrar en mucho detalle, nuestra red tiene 784 entradas (28x28 píxeles), que se conectan a una primera capa oculta de 256 neuronas conectadas densamente (todas las entradas se conectan a todas las neuronas de esta capa). Como función de activación se usa la función rectificador o relu, cuya función es

\[f(x) = \max(0, x)\]


Hay diversas funciones de activación posibles, aunque en los últimos años la función relu ha ido ganando popularidad frente a la más clásica función de activación sigmoide.
La segunda capa oculta, también densamente conectada y usando la función de activación relu, tiene 128 neuronas. Finalmente, la capa de salida tiene 10 neuronas, una por cada clase que queremos clasificar. En esta capa usamos la función de activación softmax, que nos devuelve una distribución de probabilidad entre las salidas, de forma que cada una de estas salidas nos indica la probabilidad de que la imagen de entrada pertenezca a dicha clase. Es decir, que la suma de todas las salidas sumarán 1.

Las siguientes líneas compilan el modelo y lo parametrizan. En este caso, para optimizar la función de costes, que es la que queremos minimizar, usamos el optimizador rmsprop, que es un tipo de optimización basada en el descenso de gradiente que ya os presenté en otro artículo anterior (con algunas diferencias). como función de coste (loss function) usaremos categorical_crossentropy, que funciona especialmente bien en problemas de clasificación donde cada ejemplo pertenece exclusivamente a una sola categoría.

model.compile(optimizer = 'rmsprop',
             loss = 'categorical_crossentropy',
             metrics = ['accuracy'])

Ya sólo nos resta entrenar el modelo. Vamos a entrenarlo usando lotes (batch) de 128 ejemplos durante 30 iteraciones (epochs). Además le indicamos cuál es nuestro conjunto de validación.

train_log = model.fit(x_train, y_train, 
                      epochs=30, batch_size=128,
                     validation_data=(x_val, y_val))

Finalizado el entrenamiento, podemos probar el modelo contra el conjunto de test y obtener la exactitud de nuestro modelo resultante.

test_loss, test_accuracy = model.evaluate(x_test, y_test)
print(test_accuracy)
10000/10000 [==============================] - 0s 12us/step
0.8788999915122986


Es decir, hemos conseguido una exactitud del 87% asignando las imágenes del conjunto de test a su clase correspondiente. No está mal. Sin embargo, para ir ajustando el modelo necesitamos alguna herramienta que nos permita medir qué tan bien o mal se comporta el modelo. Durante el entrenamiento se genera un histórico con los valores que ha ido tomando la función de coste y la precisión tanto del conjunto de entrenamiento como del de validación. Podemos obtener una representación gráfica.

# grafica con la función de coste

loss = train_log.history['loss']
val_loss = train_log.history['val_loss']
epochs = range(1, len(loss) + 1)
plt.plot(epochs, loss, 'b', label='Training loss')
plt.plot(epochs, val_loss, 'r', label='Validation loss')
plt.title('Training and validation loss')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()



También nos puede ser útil ver cómo ha ido evolucionando la precisión durante el entrenamiento.

# grafica de la exactitud

plt.clf()
acc = train_log.history['accuracy']
val_acc = train_log.history['val_accuracy']
epochs = range(1, len(loss) + 1)
plt.plot(epochs, acc, 'b', label='Training acc')
plt.plot(epochs, val_acc, 'r', label='Validation acc')
plt.title('Training and validation accuracy')
plt.xlabel('Epochs')
plt.ylabel('Accuracy')
plt.legend()
plt.show()



En la primera gráfica, la de la función de costes, vemos que a partir de la octava iteración (epoch), la función de coste sobre el conjunto de validación comienza a crecer (empeora), mientras que en el caso del conjunto de entrenamiento sigue decreciendo (mejorando). Eso es señal inequívoca de que nuestro modelo esta sobreentrenado (overfitting). Es decir, que predice muy bien en el conjunto de entrenamiento, pero mal en datos externos a este. Y eso es malo. Podemos hacer varias cosas para evitarlo, pero las dos más sencillas son, quitar neuronas para simplificar el modelo y/o reducir el número de iteraciones. Vamos a probar con la segunda opción (hay otras un poco más complejas y efectivas, pero ya os las cuento otro día). Vamos a ver qué pasa si lo dejamos en 8 iteraciones (que es donde empieza a empeorar la función de coste del conjunto de validación).


10000/10000 [==============================] - 0s 13us/step
0.8809999823570251

Tenemos un 88% de exactitud, lo que de por sí ya es una pequeña mejora, pero además nos hemos ahorrado 22 iteraciones, lo que es una buena cantidad de tiempo de cómputo. Si no tienes una buena GPU pueden llegar a ser algunos minutos en un ordenador no muy potente. La gráfica de la evolución de la función de coste ahora es esta.



y la de exactitud es esta.



Ahora que tenemos un modelo entrenado, podemos hacer predicciones. Por ejemplo sobre el conjunto de test.

predictions = model.predict(x_test)
np.argmax(predictions[0]) # clase más probable

Esto nos devuelve el valor 9 (Ankle boot), que es precisamente la categoría a la que pertenece la primera imagen del conjunto de test, que ya os mostré al principio del artículo.

Se puede seguir jugando con el modelo añadiendo o quitando neuronas, o capas completas. También cambiando el tipo de función de activación, etc. Aquí os dejo el notebook de jupyter por si queréis trastear un poco con la red neuronal que acabamos de crear.

Comentarios

Entradas populares de este blog

Criptografía en Python con PyCrypto

A la hora de cifrar información con Python, tenemos algunas opciones, pero una de las más fiables es la librería criptográfica PyCrypto, que soporta funciones para cifrado por bloques, cifrado por flujo y cálculo de hash. Además incorpora sus propios generadores de números aleatorios. Seguidamente os presento algunas de sus características y también como se usa.


Regresión lineal y descenso de gradiente con Python

En machine learning, el objetivo principal es encontrar un modelo que explique el comportamiento de un sistema (en el amplio sentido de la palabra). A partir de unos datos de entrenamiento, un sistema de aprendizaje automático ha de ser capaz de inferir un modelo capaz de explicar, al menos en su mayoría, los efectos observados. Pero también aplicar ese aprendizaje. Por ejemplo, un sistema de machine learning muy lucrativo para las empresas anunciantes es aquél que dado un perfil de usuario (datos de entrada A), sea capaz de predecir si pinchará o no (salida B) sobre un anuncio publicitario de, por ejemplo, comida para gatos. No es sencillo crear un modelo capaz de predecir el comportamiento del usuario (o sí), pero en todo caso, existen diferentes técnicas que nos permiten abordar el problema. En el caso del ejemplo que acabamos de ver, el modelo debería ser capaz de clasificar a los usuarios en dos clases diferentes, los que pulsarán y los que no pulsarán el anuncio de comida de ga…

Desbordamiento de enteros (Integer Overflow)

Ya os he hablado en este blog de posibles problemas potenciales que se pueden dar en los programas y que son susceptibles de ser explotados para hacer que dichos programas se comporten de forma diferente a la que deberían. Uno de estos problemas es el del desbordamiento de la pila. Sin embargo, hay otros posibles errores de programación que, aunque menos obvios, son igual de peligrosos. Uno de ellos es el desbordamiento de enteros o integer overflow. Para entender cómo funciona os presento un ejemplo muy sencillo pero didáctico.