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

Creando firmas de virus para ClamAV

ClamAv es un antivirus opensource y multiplataforma creado por Tomasz Kojm muy utilizado en los servidores de correo Linux. Este antivirus es desarrollado por la comunidad, y su utilidad práctica depende de que su base de datos de firmas sea lo suficientemente grande y actualizado. Para ello es necesario que voluntarios contribuyan activamente aportando firmas. El presente artículo pretende describir de manera sencilla cómo crear firmas de virus para ClamAV y contribuir con ellas a la comunidad.

Manejo de grafos con NetworkX en Python

El aprendizaje computacional es un área de investigación que en los últimos años ha tenido un auge importante, sobre todo gracias al aprendizaje profundo (Deep Learning). Pero no todo son redes neuronales. Paralelamente a estas técnicas, más bien basadas en el aprendizaje de patrones, también hay un auge de otras técnicas, digamos, más basadas en el aprendizaje simbólico. Si echamos la vista algunos años atrás, podemos considerar que quizá, la promesa de la web semántica como gran base de conocimiento ha fracasado, pero no es tan así. Ha ido transmutándose y evolucionando hacia bases de conocimiento basadas en ontologías a partir de las cuales es posible obtener nuevo conocimiento. Es lo que llamamos razonamiento automático y empresas como Google ya lo utilizan para ofrecerte información adicional sobre tus búsquedas. Ellos lo llaman Grafos de Conocimiento o Knowledge Graphs . Gracias a estos grafos de conocimiento, Google puede ofrecerte información adicional sobre tu búsqueda, ad

Scripts en NMAP

Cuando pensamos en NMAP, pensamos en el escaneo de puertos de un host objetivo al que estamos relizando una prueba de intrusión, pero gracias a las posibilidades que nos ofrecen su Scripting Engine , NMAP es mucho más que eso. Antes de continuar, un aviso: algunas de posibilidades que nos ofrecen los scripts de NMAP son bastante intrusivas, por lo que recomiendo hacerlas contra hosts propios, máquinas virtuales como las de Metasploitable, o contrato de pentesting mediante. Para este artículo voy a usar las máquinas de Metasploitable3 . No voy a entrar en los detalles sobre el uso básico de NMAP, ya que hay miles de tutoriales en Internet que hablan sobre ello. Lo cierto es que NMAP tiene algunas opciones que permiten obtener información extra, además de qué puertos están abiertos y cuales no. Por ejemplo, la opción -sV trata de obtener el servicio concreto, e incluso la versión del servicio que está corriendo en cada puerto. Otro ejemplo es la opción -O, que intenta averiguar el