Capítulo 5 Overfitting y Datos

Datos y Overfitting

 

Contenido abierto del Capítulo 5 del libro DEEP LEARNING Introducción práctica con Keras

Este post es una versión preliminar del capítulo 5 del libro “Deep Learning – Introducción práctica con Keras (SEGUNDA PARTE)” que se publicará este otoño en Kindle Direct Publishing con ISBN 978-1-687-47399-8 en la colección WATCH THIS SPACE – Barcelona (Book 6). Como autor agradeceré que cualquier error o comentario que el lector o lectora encuentre en este texto me lo comunique por email a libro.keras@gmail.com para mejorar su contenido. Muchas gracias de antemano.

En la primera parte del libro, donde aprendimos a usar Keras para implementar desde una red neuronal simple hasta una red neuronal convolucional básica, ya comprendimos que disponer de datos resulta esencial para poder entrenar redes neuronales. En este capítulo descubriremos dónde podemos encontrar datos para entrenar redes neuronales.

Además, como veremos, en general los datos no se encuentran tan preparados y “limpios” como el conjunto de datos MNIST que hemos visto en la PRIMERA PARTE del libro. Esto resulta a menudo un problema y en este capítulo veremos algunas soluciones para descargar y preparar datos para poder ser usados por las redes neuronales.

Finalmente, otro tema que introduciremos aquí, desde un punto de vista absolutamente práctico, es elOverfittingo sobreajuste de un modelo, que aparece cuando  el modelo entrenado se ajusta demasiado a los datos de entrada y por ello no realiza buenas predicciones.

 

5.1 ¿Dónde encontrar datos para entrenar redes neuronales?

Cualquier desarrollador que se haya aventurado a trabajar en Machine Learningsabe que los datos son uno de los ingredientes principales para entrenar los algoritmos que después emplearemos como modelos para incluir en nuestras aplicaciones informáticas.

Pero, ¿dónde podemos obtenerlos? Pues bien, una gran cantidad de trabajos de investigación utilizan y ofrecen públicamente conjuntos de datos para su uso: un ejemplo es el conjunto de datos MNIST, ya utilizado en el capítulo 4 y del cual hemos podido comprobar lo fácil que resulta usar datos ya preparados y precargados en Keras.

Conjuntos de datos públicos

Como decíamos, hay muchos grupos de investigación, empresas e instituciones que han liberado sus datos para que la comunidad investigadora pueda usarlos para avanzar en este campo. De los muchos que hay en estos momentos, indico algunos para que el lector pueda hacerse una idea: COCO[1], ImageNet[2], Open Images[3], Visual Question Answering[4], SVHN[5], CIFAR-10/100[6], Fashion-MNIST[7], IMDB Reviews[8], Twenty Newsgroups[9], Reuters-21578[10], WordNet[11], Yelp Reviews[12], Wikipedia Corpus[13], Blog Authorship Corpus[14], Machine Translation of European Languages[15], Free Spoken Digit Dataset[16], Free Music Archive[17], Ballroom[18], The Million Song[19], LibriSpeech[20], VoxCeleb[21], The Boston Housing[22], Pascal[23], CVPPP Plant Leaf Segmentation[24], Cityscapes[25].

Conjuntos de datos precargados en Keras

Al igual que con el conjunto de datos MNIST, Keras ha hecho un esfuerzo para crear un grupo de datasetspara facilitarnos el trabajo de aprendizaje a los que nos iniciamos en Deep Learning: básicamente ha preprocesado algunos de los datasetsmencionados anteriormente, que  a menudo requieren bastante esfuerzo de preparación para poder ser consumidos directamente por redes neuronales. Algunos de los más usuales para empezar que ofrece el paquete Keras son:

  • CIFAR 10:conjunto de datos de 50000 imágenes de entrenamiento en color de 32×32, etiquetadas en 10 categorías y 10000 imágenes de prueba.
  • CIFAR100: conjunto de datos de 50000 imágenes de entrenamiento en color de 32×32, etiquetadas en 100 categorías y 10000 imágenes de prueba.
  • IMDB: conjunto de datos formado por 25000 reseñas de películas de IMDB, etiquetadas por valoraciones (positivas/negativas). Las revisiones han sido preprocesadas y cada una de ellas se codifica como una secuencia de índices de palabras (enteros).
  • Reuters newswire topics classification: conjunto de datos de 11228 noticias de Reuters, con más de 46 temas.
  • Fashion-MNIST database of fashion articles: conjunto de datos de 60000 imágenes de moda en escala de grises 28×28, con 10 categorías de moda, junto con un conjunto de prueba de 10000 imágenes.
  • Boston housing price regression dataset:conjunto de datos de la Universidad Carnegie Mellon que contienen 13 atributos de casas en diferentes ubicaciones alrededor de los suburbios de Boston a fines de los años setenta.

Todos estos son accesibles de la misma manera que hicimos con MNIST cuando publicamos la PRIMERA PARTE del libro. Pero con la aparición de TensorFlow 2.0 se han incorporado una amplia colección de datasetspúblicos preparados para ser utilizados con TensorFlow, por lo que invito al lector que visite el GitHub[26]de TensorFlow para ver la gran cantidad que hay disponibles en imagen, audio, video, texto, traducción, estructurados, etc.[27]

Conjuntos de datos de Kaggle

Una de las fuentes más populares de datos públicos es la plataforma Kaggle que alberga competiciones de análisis de datos y modelado predictivo donde compañías e investigadores aportan sus datos mientras que científicos de datos de todo el mundo compiten por crear los mejores modelos de predicción o clasificación. También es una buena fuente,  ya que todos los datasetsde las competiciones quedan disponibles públicamente.  El conjunto de datos de “Dogs vs. Cats” que usaremos en este capítulo no está precargado en Keras y lo descargaremos de Kaggle[28]. Este datasetes muy usado en cursos y libros introductorios, y nos será muy útil para poder mostrar al lector cómo se pueden descargar y tratar datos reales.

 

5.2 ¿Cómo descargar y usar datos reales?

En la PRIMERA PARTE del libro ya vimos cómo usar una red neuronal convolucional (ConvNet) para el reconocimiento de los dígitos de escritura a mano. En esta sección, realizaremos un paso más y pasaremos a reconocer imágenes reales de gatos y perros para clasificarlas como una u otra.

En particular, para el reconocimiento de la escritura a mano, nos facilitaba mucho el trabajo el hecho de que todas las imágenes tuvieran el mismo tamaño y forma, y que todas fueran de color monocromo. Pero las imágenes del mundo real no son así: tienen diferentes tamaños, relaciones de aspecto, etc., ¡y generalmente son en color! Y si tenemos en cuenta que los datos para poder ser consumidos por una ConvNet deben estar almacenados en tensores de números enteros, debemos ineludiblemente realizar un preprocesado a los datos: decodificar el contenido en formato RGB[29], convertirlo en tensores, reescalar los valores de los píxeles, etc.

Puede resultar un poco desalentador para el lector tanto preproceso, pero afortunadamente Keras tiene utilidades para encargarse de estos pasos automáticamente; en concreto Keras tiene un importante módulo con herramientas auxiliares de procesamiento de imágenes (keras.preprocessing.image[30]). En particular, contiene la clase ImageDataGenerator, que nos facilitará enormemente convertir automáticamente los archivos de imagen que tenemos almacenados en lotes de tensores preprocesados.

Además TensorFlow dispone de la API Dataset[31]que permite manejar todo tipo de carga de datos en un modelo. Se trata de una API de alto nivel para leer y transformar datos en la manera que sea más conveniente para su uso en entrenamiento. En el capítulo 8 presentamos un caso práctico donde se usará y explicará el funcionamiento de esta API.

Caso de estudio: “Dogs vs. Cats”

El conjunto de datos de “Dogs vs. Cats” hemos de descargarlo, como comentaba anteriormente, de Kaggle[32]. En concreto, este conjunto de datos fue puesto a disposición de todo el mundo como parte de una competencia de visión artificial a finales del 2013, cuando las ConvNet no eran tan habituales aún. Se puede descargar el conjunto de datos original desde esta página[33]después de crearse una cuenta gratuita.

keras tensorflow overfitting

En concreto este archivo contiene 25000 imágenes de perros y gatos (12500de cada clase), a color (JPG[34]) y con varios tamaños. Ahora bien, en este libro de introducción al tema usaremos un subconjunto para disminuir los tiempos de computación requerido para entrenar los modelo y a la vez poder mostrar al lector técnicas muy importantes para tratar problemas habituales que aparecen cuando queremos entrenar redes neuronales con conjuntos de datos pequeños.

Comencemos descargando nuestros datos de ejemplo, un archivo comprimido de 4000 imágenes JPG. Concretamente se han preparado tres carpetas (train, test y validation) con 2000 imágenes para el conjunto de datos de Training, 1000 para el conjunto de datos de testy 1000 para el conjunto de datos de validation, siempre balanceadas, es decir, mismo numero de imágenes de perros que de gatos. El código para descargar los datos es el siguiente:

!wget --no-check-certificate \
   https://www.dropbox.com/s/sshnskxxolkrq9h/cats_and_dogs_small.zip?dl=0\
   -O /tmp/cats_and_dogs_small.zip

Una vez descargados los datos, podemos usar librerías de sistema operativo para tener acceso al sistema de archivos y poder descomprimir los datos:

import os
import zipfile
local_zip = '/tmp/cats_and_dogs_small.zip'
zip_ref = zipfile.ZipFile(local_zip, 'r')
zip_ref.extractall('/tmp')
zip_ref.close()

Pero antes de avanzar, hagamos un repaso de cómo debemos organizar y repartir los datos para poder entrenar correctamente los modelos de redes neuronales.

Training, Validation y Testdataset

Generalmente en el mundo del Machine Learningse organizan los datos disponibles de manera que una parte sirve para entrenar y otra para hacer una prueba final (Test dataset) usándolos una sola vez como evaluación del modelo final. Pero de los datos que hemos destinado para entrenar reservamos una parte como datos de validación (validation dataset).

El conjunto de datos que nos quedan para el entrenamiento (Training dataset) son datos que se utilizan para entrenar diciéndole al modelo de red neuronal que «así es como se ve un gato» y «así es como se ve un perro». En cambio el conjunto de datos de validación son imágenes de gatos y perros que la red neuronal no verá como parte del entrenamiento pero serán usadas para comprobar qué tan bien o qué tan mal se ha entrenado el modelo en cada epoch. Con las métricas, como la Accuracy, que se pueden obtener de este conjunto de datos de validación, nos guiamos para decidir como sintonizar los hiperparámetros del algoritmo antes de repetir el proceso de entrenamiento para otra epoch.

Es importante notar que cuando nos esforzamos por mejorar el algoritmo sintonizando los hiperparámetros gracias al comportamiento del modelo con los datos de validación, estamos inadvertidamente incidiendo en el modelo, que puede sesgar los resultados a favor del conjunto de validación. De aquí la importancia de disponer de unos datos de prueba (Test dataset) reservados para una prueba final, siendo datos que el modelo no ha visto nunca anteriormente durante la etapa de entrenamiento (ni como datos de entrenamiento ni como datos de validación), permitiendo obtener una medida de comportamiento del algoritmo más objetiva y evaluar si nuestro modelo generaliza correctamente.

Volviendo al código de nuestro caso de estudio, antes de continuar debemos crear los directorios donde almacenaremos los datos ante de repartir los datos en los tres grupos Training, Validation y Test. El siguiente código que encontrara el lector en el GitHub del libro realiza esta tarea usando comandos del sistema operativo:

base_dir = '/tmp/cats_and_dogs_small'
train_dir =      os.path.join(base_dir, 'train')
validation_dir = os.path.join(base_dir, 'validation')
test_dir =       os.path.join(base_dir, 'test')
# Directorio con las imagenes de training
train_cats_dir = os.path.join(train_dir, 'cats')
train_dogs_dir = os.path.join(train_dir, 'dogs')
# Directorio con las imagenes de validation
validation_cats_dir = os.path.join(validation_dir, 'cats')
validation_dogs_dir = os.path.join(validation_dir, 'dogs')
# Directorio con las imagenes de test
test_cats_dir = os.path.join(test_dir, 'cats')
test_dogs_dir = os.path.join(test_dir, 'dogs')

Algo a lo que debe prestar atención el lector en este caso de estudio es que no se etiquetarán explícitamente las imágenes como “gato” o “perro”. Si recuerda con el ejemplo de MNIST, habíamos etiquetado las imágenes de tal manera que se indicaba ‘este es un 6’, ‘este es un 9’, etc. Esto es debido a que una de las utilidades interesantes de Keras (y TensorFlow) es que si organizamos las imágenes en subdirectorios podemos generar las etiquetará automáticamente.

En nuestro caso de estudio podemos usar esta utilidad y por tanto solo nos hace falta subdividir los datos (imágenes) en tres subdirectorios. Más adelante veremos que podemos usar ImageGeneratorpara poder leer imágenes de subdirectorios y etiquetarlas automáticamente con el nombre de ese subdirectorio.

Resumiendo, y para facilitar el seguimiento del ejemplo al lector, mostramos esta figura donde se esquematiza los nombres de los directorios y cómo quedan organizados los datos que tenemos descargados en nuestro sistema de ficheros:

keras tensorflow overfitting

Para facilitar el seguimiento del código del caso de estudio hemos decidido repartir los datos en “números redondos” sin priorizar la mejor proporción de repartición para obtener los mejores resultados posibles del modelo, dado que lo que buscamos en este libro es ayudar a entender al lector las diferentes técnicas. Ahora bien, este es un tema muy extenso en Machine Learningy nos gustaría adentrarnos un poco más dada su importancia, pero queda fuera del alcance previsto de este libro dado su propósito introductorio.

En resumen: tenemos tres conjuntos de imágenes, 2000 para entrenar, 1000 para validar y 1000 de prueba final respectivamente (50% directorio train, 25% directorio validationy 25% directorio test). Los tres conjuntos de datos están exactamente balanceados, que aunque no entremos en detalle en este libro, es un factor importante para garantizar que la Accuracydel modelo pueda ser una métrica apropiada para evaluar su validez.

Les proponemos que comprueben que el contenido de los directorios es el correcto antes de avanzar con el siguiente código que nos da el nombre de fichero de las 5 primeras imágenes de cadas uno de los directorios:

train_cat_fnames = os.listdir( train_cats_dir )
print(train_cat_fnames[:5])
train_dog_fnames = os.listdir( train_dogs_dir )
print(train_dog_fnames[:5])
validation_cat_fnames = os.listdir( validation_cats_dir )
print(validation_cat_fnames[:5])
validation_dog_fnames = os.listdir( validation_dogs_dir )
print(validation_dog_fnames[:5])
test_cat_fnames = os.listdir( test_cats_dir )
print(test_cat_fnames[:5])
test_dog_fnames = os.listdir( test_dogs_dir )
print(test_dog_fnames[:5])
['cat.986.jpg', 'cat.308.jpg', 'cat.452.jpg', 'cat.102.jpg', 'cat.541.jpg']['dog.680.jpg', 'dog.732.jpg', 'dog.672.jpg', 'dog.23.jpg', 'dog.834.jpg']['cat.1341.jpg', 'cat.1435.jpg', 'cat.1256.jpg', 'cat.1342.jpg', 'cat.1361.jpg']['dog.1048.jpg', 'dog.1456.jpg', 'dog.1258.jpg', 'dog.1012.jpg', 'dog.1341.jpg']['cat.1519.jpg', 'cat.1842.jpg', 'cat.1798.jpg', 'cat.1963.jpg', 'cat.1670.jpg']['dog.1562.jpg', 'dog.1512.jpg', 'dog.1847.jpg', 'dog.1598.jpg', 'dog.1709.jpg']

Si el lector se crea su propio juego de pruebas y quiere comprobar que el número de imágenes de cada directorio es el correcto, puede hacerlo con el siguiente código:

print('total training cat images :', len(os.listdir(train_cats_dir ) ))
print('total training dog images :', len(os.listdir(train_dogs_dir ) ))
print('total validation cat images :', len(os.listdir( validation_cats_dir ) ))
print('total validation dog images :', len(os.listdir( validation_dogs_dir ) ))
print('total test cat images :', len(os.listdir( test_cats_dir ) ))
print('total test dog images :', len(os.listdir( test_dogs_dir ) ))
total training cat images : 1000
total training dog images : 1000
total validation cat images : 500
total validation dog images : 500
total test cat images : 500
total test dog images : 500

Seguramente lo que al lector le gustaría es hacer una comprobación visual de las imágenes. Proponemos el siguiente código para comprobar las imágenes de gatos que usaremos para entrenar el modelo (directorio train_cats_dir):

%matplotlib inline
import matplotlib.image as mpimg
import matplotlib.pyplot as plt
nrows = 4
ncols = 4
pic_index = 0 # Índice para iterar sobre las imagenes
fig = plt.gcf()
fig.set_size_inches(ncols*4, nrows*4)
pic_index+=8
next_cat_pix = [os.path.join(train_cats_dir, fname)
                for fname in train_cat_fnames[ pic_index-8:pic_index]
]
for i, img_path in enumerate(next_cat_pix):
   sp = plt.subplot(nrows, ncols, i + 1)
   sp.axis('Off') # no imprimir ejes
   img = mpimg.imread(img_path)
   plt.imshow(img)
plt.show()

keras tensorflow overfitting

El lector puede reutilizar este código para visualizar el contenido de cualquier directorio de datos: solo hace falta sustituir en el código anterior el nombre del directoriotrain_cats_dirpor el nombre del directorio que quiera consultar.

Bien, ahora que tenemos una idea de cómo se ven nuestros datos, el siguiente paso es definir en Keras el modelo que entrenaremos posteriormente para reconocer si una imagen corresponde a un gato o a un perro.

Modelo de reconocimiento de imágenes reales

Antes de definir el modelo, prestemos atención a un detalle importante sobre las imágenes de nuestro caso de estudio: si se fija el lector en las imágenes en la cuadrícula anterior, un detalle importante aquí, y una diferencia significativa con respecto al ejemplo de reconocimiento de dígitos, es que estas imágenes vienen en todas las formas (y tamaños, aunque no se aprecie en la cuadrícula). Recordemos que para entrenar una red neuronal se requiere que los datos de entrada tengan un tamaño uniforme. En nuestro caso de estudio elegimos 150×150 para definir el modelo y ya veremos después el código que convierte previamente las imágenes a este tamaño.

Ya construimos una pequeña red convolucional para MNIST en la PRIMERA PARTE del libro, por lo que podemos considerar que el lector está familiarizado y podemos reutilizar la misma estructura general como punto de partida para este caso de estudio donde la red neuronal será una pila de capas alternadas de Conv2D(con función de activación ReLu)y MaxPooling2D.

Pero debido a que estamos tratando imágenes más grandes y un problema más complejo, propondremos una red en consecuencia: tendrán más etapas de Conv2D+ MaxPooling2D. Esto sirve tanto para aumentar la capacidad de la red como para reducir aún más el tamaño de los mapas de características para que no sean demasiado grandes cuando llegue a la capa final. En concreto en este caso hemos decidido que comienza con entradas de tamaño 150×150 y terminar con mapas de características de tamaño 7×7 justo antes de la capa Flatten.

Finalmente, debido a que requerimos resolver un problema de clasificación binaria, nuestra red propuesta terminará con una capa densa con una sola neurona y una activación sigmoidea. Esta última neurona codificará la probabilidad de que la red esté mirando una clase u otra. Veamos el modelo propuesto para nuestro caso de estudio escrito en Keras:

import tensorflow as tf
model = tf.keras.models.Sequential([
    tf.keras.layers.Conv2D(32, (3,3), activation='relu', input_shape=(150, 150, 3)),
    tf.keras.layers.MaxPooling2D(2, 2),
    tf.keras.layers.Conv2D(64, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(128, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Conv2D(128, (3,3), activation='relu'),
    tf.keras.layers.MaxPooling2D(2,2),
    tf.keras.layers.Flatten(),
    tf.keras.layers.Dense(512, activation='relu'),
    tf.keras.layers.Dense(1, activation='sigmoid')
])

Un detalle a fijarse es el parámetro de forma de input_shape. En el ejemplo de MNIST en el capítulo 4, era 28×28×1, porque la imagen era 28×28 en escala de grises (8 bits, 1 byte para profundidad de color). En este caso de estudio es de 150×150 para el tamaño y 3 para la profundidad de color en RGB.

Recordemos que en general las capas convoluciones operan sobre tensores de 3D, llamados mapas de características (feature maps), con dos ejes espaciales de altura y anchura (height y width), además de un eje de canal (channels) también llamado profundidad (depth). Para una imagen de color RGB, la dimensión del eje depthes 3, pues la imagen tiene tres canales: rojo, verde y azul (Red, Greeny Blue).

Podemos observar que el número de mapas de características (profundidad) aumenta progresivamente en la red (de 32 a 128), mientras que el tamaño de los mapas de características disminuye (de 148×148 a 7×7). Este es un patrón que se usa a menudo en redes neuronales convolucionales.

Con el método model.summary( )podemos ver con detalle cómo cambian las dimensiones de los mapas de características en cada capa sucesiva. Recordemos que la columna «Output Shape» muestra el tamaño de los mapas de características en cada una de las capa. Fijémonos que las capas convolucionales reducen el tamaño del mapa de características debido al paddingy cada capa poolingreduce a la mitad sus dimensiones. Veamos la salida del método summary( ):

model.summary()
Model: "sequential"
_________________________________________________________________
Layer (type)                 Output Shape              Param #   
=================================================================
conv2d (Conv2D)              (None, 148, 148, 32)      896       
_________________________________________________________________
max_pooling2d (MaxPooling2D) (None, 74, 74, 32)        0         
_________________________________________________________________
conv2d_1 (Conv2D)            (None, 72, 72, 64)        18496     
_________________________________________________________________
max_pooling2d_1 (MaxPooling2 (None, 36, 36, 64)        0         
_________________________________________________________________
conv2d_2 (Conv2D)            (None, 34, 34, 128)       73856     
_________________________________________________________________
max_pooling2d_2 (MaxPooling2 (None, 17, 17, 128)       0         
_________________________________________________________________
conv2d_3 (Conv2D)            (None, 15, 15, 128)       147584    
_________________________________________________________________
max_pooling2d_3 (MaxPooling2 (None, 7, 7, 128)         0         
_________________________________________________________________
flatten (Flatten)            (None, 6272)              0         
_________________________________________________________________
dense (Dense)                (None, 512)               3211776   
_________________________________________________________________
dense_1 (Dense)              (None, 1)                 513       
=================================================================
Total params: 3,453,121
Trainable params: 3,453,121
Non-trainable params: 0
_________________________________________________________________

Como aprendimos en la primera parte del libro, a continuación debemos configurar los hiperparámetros que hacen referencia a cómo será el proceso de aprendizaje de nuestro modelo con el método compile( ). Dado que se trata de un problema de clasificación binaria y nuestra función de activación final es sigmoidevamos a entrenar nuestro modelo con una función de coste binary_crossentropy[35].

Como optimizador vamos a usar rmspropcon un learning ratede 0.001. Finalmente especificamos con el argumento metricque durante el entrenamiento querremos monitorearla precisión (Accuracy) de la clasificación:

from tensorflow.keras.optimizers import RMSprop
model.compile(optimizer=RMSprop(lr=1e-4),
   loss='binary_crossentropy',
   metrics = ['acc']
   )

Preprocesado de datos reales con ImageDataGenerator

Una vez tenemos el modelo definido y configurado, pasemos a ver cómo podemos cargar las imágenes que están en los directorios indicados, convirtiéndolas en tensores float32 y pasarlas a nuestra red neuronal, junto con sus respectivas etiquetas. Para ello usaremos un objeto generator (que actúa de iterador en Python y se puede usar con el operador for .. in)[36]  tanto para las imágenes de entrenamiento, las imágenes de validación o para las imágenes de prueba. Nuestros generadores deberán generar lotes de 20 imágenes de tamaño 150×150 y sus respectivas etiquetas (binarias).

Pero además, como recordamos de capítulos previos, en general los datos deben ser normalizados antes de pasarlos a la red neuronal. En nuestro caso, preprocesaremos nuestras imágenes normalizando los valores de píxeles para que estén en el rango [0,1](originalmente todos los valores están en el rango [0,255]). En Keras esto se consigue con el parámetro rescalede la clase ImageDataGeneratordel paquete preprocessingde Keras:

from tensorflow.keras.preprocessing.image import ImageDataGenerator
train_datagen = ImageDataGenerator( rescale = 1.0/255. )
validation_datagen  = ImageDataGenerator( rescale = 1.0/255. )
test_datagen  = ImageDataGenerator( rescale = 1.0/255. )

Esta clase ImageDataGeneratorpermite instanciargeneradores de lotes de imágenes (y sus etiquetas) a través de los métodos flow(data, labels)o flow_from_directory(directory) cuando se trata de un directorio, como es nuestro caso. Estos generadores se pueden usar con los métodos del modelo de Keras que aceptan instancias de generadores de datos como argumento como son fit_generator( ), evaluate_generator( )y predict_generator( ).

El código de nuestro caso de estudio que crea tres objeto que instanciana cada uno de los generadores respectivamente es:

train_generator = train_datagen.flow_from_directory(train_dir,
                  batch_size=20,
                  class_mode='binary',
                  target_size=(150, 150))
validation_generator = validation_datagen.flow_from_directory(validation_dir,
                  batch_size=20,
                  class_mode = 'binary',
                  target_size = (150, 150))
test_generator = test_datagen.flow_from_directory(validation_dir,
                  batch_size=20,
                  class_mode = 'binary',
                  target_size = (150, 150))
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.

 

5.3 Overfitting

Tener que entrenar un modelo de clasificación de imágenes con muy pocos datos es una situación común, que seguramente se encuentre el lector en la práctica y provoca el problema de sobreaprendizaje o Overfitting. Vamos a aprovechar el caso de estudio que estamos tratando para mostrar este problema y esbozar algunas de las técnicas más habituales que pueden solucionarlo.

Modelos a partir de conjuntos de datos pequeños

Se ha dicho que el Deep Learningsolo funciona cuando hay muchos datos disponibles, pero lo que constituye “muchos datos” es relativo, en relación con el tamaño y la profundidad de la red neuronal que estemos tratando de entrenar. Es cierto que no es posible entrenar una red neuronal convolucional para resolver un problema complejo con solo unas pocas decenas de datos, pero unos cientos podrían llegar a ser suficientes si el modelo es pequeño y la tarea es simple, pudiendo llegar a arrojar resultados razonables a pesar de la relativa falta de datos.

Analicemos el caso de estudio que nos ocupa y del que antes hemos ya definido el modelo: ahora es el momento de entrenar la red que ya hemos definido. En nuestro caso podemos usar el método fit_generator( )para pasar los datos a nuestro modelo, que es el equivalente al método fit()que usamos en el ejemplo de MNIST del capítulo 4. Como primer argumento se especifica el generador de Python que produce lotes de entradas y etiquetas indefinidamente. Debido a que los datos se generan de manera indefinida, el modelo de Keras necesita saber cuántas muestras extraer del generador antes de decidir que ha finalizado una época (epoch): este es el papel del argumento steps_per_epoch.En nuestro ejemplo, los lotes son de 20 muestras, por lo que requeriremos 100 lotes hasta que el modelo vea las 2000 imágenes de entrenamiento.

Cuando usamos el métodofit_generator( )también podemos pasar el argumento validation_datacon el generador de las imágenes que usaremos para validar. En este caso se requiere indicar con el argumento validation_stepscuántas muestras deben extraerse del generador en cada epoch, que serán 50 lotes hasta que se vean las 1000 imágenes.

Podemos calcular estos valores a mano o mediante el siguiente código:

batch_size = 20
steps_per_epoch = train_generator.n // batch_size
validation_steps = validation_generator.n // batch_size
print (steps_per_epoch)
print (validation_steps)
100
50

Ahora solo nos falta definir el argumento epoch; propongo que el lector empiece por un valor pequeño para probar (dado que cada epochpuede tardar varios segundos). Nosotros hemos entrenado con 100 epochpara obtener datos que permitan visualizar más fácilmente al lector el Overfitting que se produce en este ejemplo.

history = model.fit_generator(
             train_generator,
             steps_per_epoch= steps_per_epoch,
             epochs=100,
             validation_data=validation_generator,
             validation_steps= validation_steps,
             verbose=2)
Epoch 1/100
100/100 - 11s - loss: 0.6727 - acc: 0.5905 - val_loss: 0.6501 - val_acc: 0.6340
Epoch 2/100
100/100 - 10s - loss: 0.6258 - acc: 0.6575 - val_loss: 0.6201 - val_acc: 0.6560
Epoch 3/100
100/100 - 10s - loss: 0.5797 - acc: 0.6790 - val_loss: 0.6254 - val_acc: 0.6480
Epoch 4/100
100/100 - 10s - loss: 0.5515 - acc: 0.7200 - val_loss: 0.5903 - val_acc: 0.6840
Epoch 5/100
100/100 - 10s - loss: 0.5263 - acc: 0.7405 - val_loss: 0.6444 - val_acc: 0.6620
Epoch 6/100
100/100 - 10s - loss: 0.5024 - acc: 0.7570 - val_loss: 0.5821 - val_acc: 0.6930
Epoch 7/100
100/100 - 10s - loss: 0.4839 - acc: 0.7580 - val_loss: 0.6088 - val_acc: 0.6850
Epoch 8/100
100/100 - 10s - loss: 0.4573 - acc: 0.7755 - val_loss: 0.5810 - val_acc: 0.7020
Epoch 9/100
100/100 - 10s - loss: 0.4399 - acc: 0.7950 - val_loss: 0.5645 - val_acc: 0.7120
Epoch 10/100
100/100 - 10s - loss: 0.4032 - acc: 0.8080 - val_loss: 0.6303 - val_acc: 0.7050
...
...
Epoch 90/100
100/100 - 10s - loss: 0.0115 - acc: 0.9980 - val_loss: 2.4556 - val_acc: 0.7220
Epoch 91/100
100/100 - 10s - loss: 0.0041 - acc: 0.9985 - val_loss: 2.6333 - val_acc: 0.7260
Epoch 92/100
100/100 - 10s - loss: 0.0036 - acc: 0.9980 - val_loss: 2.6447 - val_acc: 0.7360
Epoch 93/100
100/100 - 10s - loss: 0.0066 - acc: 1.0000 - val_loss: 2.6211 - val_acc: 0.7340
Epoch 94/100
100/100 - 10s - loss: 0.0045 - acc: 0.9975 - val_loss: 2.6362 - val_acc: 0.7280
Epoch 95/100
100/100 - 10s - loss: 0.0080 - acc: 0.9990 - val_loss: 2.6694 - val_acc: 0.7310
Epoch 96/100
100/100 - 10s - loss: 0.0068 - acc: 1.0000 - val_loss: 2.7026 - val_acc: 0.7330
Epoch 97/100
100/100 - 10s - loss: 0.0030 - acc: 0.9995 - val_loss: 2.8117 - val_acc: 0.7290
Epoch 98/100
100/100 - 10s - loss: 0.0086 - acc: 0.9995 - val_loss: 2.9679 - val_acc: 0.7160
Epoch 99/100
100/100 - 10s - loss: 0.0031 - acc: 0.9985 - val_loss: 2.7538 - val_acc: 0.7260
Epoch 100/100
100/100 - 10s - loss: 0.0142 - acc: 1.0000 - val_loss: 2.8386 - val_acc: 0.7290

Hemos indicado con el argumento verboseque se informe por pantalla de la evolución del entrenamiento. Podemos ver que nos va indicando para cada epochcuánto tiempo ha tardado (información muy útil para estimar cuanto va a tardar aproximadamente realizar todas las epochs) y los valores de 4 métricas por cada epoch: loss, accuracy,validation_lossy validation_accuracy.

Las métricas Loss, Accuracyson buenos indicadores del progreso del entrenamiento. Durante este proceso se trata de adivinar la clasificación de los datos de entrenamiento y luego medirlos con la etiqueta conocida (si es gato o perro) y calcular el resultado. El valor de accuracynos indica la parte de las conjeturas correctas: podemos ver que a medida que vamos realizando epochsva aumentando su valor hasta llegar al máximo. En cambio la validation_accuracy, que se calcula con los datos de validación que no se han utilizado en el entrenamiento, son más bajos llegando a un umbral que no puede sobrepasar. Esto es justamente lo que nos indica que se está produciendo el efecto del sobreentrenamiento (Overfitting) a partir de un número de epochs.

Visualización del comportamiento del entrenamiento

Pero este análisis que hemos realizado en el anterior apartado, a partir de los datos que se iban imprimiendo por pantalla durante el proceso de entrenamiento después de cada epoch, lo podemos hacer de una manera más fácil. Para ello Keras proporciona la capacidad de registrar todos estos datos y retornarlos como resultado del método fit()(History callback[37]). Concretamente al final de cada epochel modelo almacena en un diccionario la Lossy Accuracyde los datos de entrenamiento y validación. Este objeto que retorna el método contiene un elemento history, que es un diccinario que a su vez contiene los datos que hemos descrito:

history_dict = history.history
print(history_dict.keys())
dict_keys(['val_loss', 'val_acc', 'loss', 'acc'])

A partir de aquí, los datos de este diccionario podemos expresarlos fácilmente en una gráfica que nos ayudará a ver más fácilmente la evolución de estas variables. El siguiente código prepara y dibuja un gráfico con toda esta información:

acc      = history.history[     'acc' ]
val_acc  = history.history[ 'val_acc' ]
loss     = history.history[    'loss' ]
val_loss = history.history['val_loss' ]
epochs   = range(1,len(acc)+1,1)
plt.plot ( epochs,     acc, 'r--', label='Training acc'  )
plt.plot ( epochs, val_acc,  'b', label='Validation acc')
plt.title ('Training and validation accuracy')
plt.ylabel('acc')
plt.xlabel('epochs')
plt.legend()
plt.figure()
plt.plot ( epochs,     loss, 'r--' )
plt.plot ( epochs, val_loss ,  'b' )
plt.title ('Training and validation loss'   )
plt.ylabel('acc')
plt.xlabel('epochs')
plt.legend()
plt.figure()

El resultado son las dos siguientes gráficas:

keras tensorflow overfitting grafica

keras tensorflow overfitting grafica

En el primer gráfico se presenta la Accuracyobtenida en cada epoch, tanto para los datos de entrenamiento (Training) como los de validación (validation). En el segundo gráfico vemos la evolución en cada epochde la Losspara los dos conjuntos de datos.

Si miramos el comportamiento general de estas gráficas vemos que son el característico cuando un modelo presenta Overfitting. Por un lado la Accuracyde los datos de entrenamiento aumenta linealmente con las epochs, hasta alcanzar casi el 100%, mientras que la Accuracyde los datos de validación se detiene alrededor del 80% y a partir de aquí se mantiene constante a lo largo de las epochs. La Lossde los datos de validación alcanza su mínimo después de pocos epochsy luego empieza a subir, mientras que la Lossde los datos de entrenamiento disminuye linealmente hasta llegar a casi 0 donde se mantiene.

A pesar de todo, los resultados no son tan malos, si consideramos que tenemos muy pocas imágenes. Si evaluamos el modelo con los datos de Test, nos lo confirma, obtenemos que este modelo acierta alrededor del 73.9 %:

test_lost, test_acc= model.evaluate_generator(test_generator)
print ("Test Accuracy:", test_acc)
Test Accuracy: 0.739

Eso muestra que nuestro modelo no es del todo malo (un modelo aleatorio rondaría sobre el 50%), pero realmente no ganamos nada con el entrenamiento tan largo, y con muy pocas epochstendríamos suficiente. Se puede confirmar en la gráfica de la Loss, donde podemos ver que después de 3epochsaproximadamente, la Lossde entrenamiento disminuye poco a poco, es decir el modelo se va ajustando a los datos de entrenamiento, pero que la Lossde validación va aumentando. Así que nuestro modelo realmente no necesita entrenar tanto. Como decíamos, este es un ejemplo de Overfittingque se puede detectar rápidamente viendo las gráficas anteriores: cuando las dos líneas que representan los datos de entrenamiento y validación divergen consistentemente.

Recordemos que los motivos que nos mueve construir un modelo es ponerlo en producción usando el método predict(), en este caso, para determinar si una imagen es un gato o un perro. Veamos como se comporta este que hemos construido con una fotografía de Wiliams, la mascota de nuestros amigos Mónica y Paco:

 

keras tensorflow overfitting foto gato williams

¿Un gato o un perro? Podemos preguntar al modelo usando este código que el lector puede encontrar en el GitHub:

import numpy as np
from google.colab import files
uploaded=files.upload()
for fn in uploaded.keys():
path='/content/' + fn
img=tf.keras.preprocessing.image.load_img(path, target_size=(150, 150))
x=image.img_to_array(img)
x=np.expand_dims(x, axis=0)
images = np.vstack([x])
classes = model.predict(images, batch_size=10)
print(classes[0])
plt.imshow(img)
plt.show()
if classes[0]>0:
   print( fn + " IS A DOG")
else:
   print( fn + " IS A CAT")

keras tensorflow overfitting foto

Como vemos el modelo no ha acertado con la foto de Wiliams (que es un gato encantador). Está claro que debemos mejorar el modelo si no queremos que nuestros amigos se enfaden. En el siguiente capítulo presentaremos propuestas para ello. El lector puede probar con fotografías propias, el código está preparado para cargar fotografías en formato JPG desde el sistema de almacenamiento del ordenador en el que se esté ejecutándose el navegador con el que accede a Colab.

Concepto de Overfitting

En resumen, con Overfittingo sobreajuste, nos referimos a lo que le sucede a un modelo cuando este modela los datos de entrenamiento demasiado bien, aprendiendo detalles de estos que no son generales. Esto es debido a que sobreentrenamos nuestro modelo y este estará considerando como válidos solo los datos idénticos a los de nuestro conjunto de entrenamiento, incluidos sus defectos (también llamado ruidoen nuestro contexto).

Es decir, nos encontramos en la situación que el modelo puede tener una baja tasa de error de clasificación para los datos de entrenamiento, pero no se generaliza bien a la población general de datos en los que estamos interesados en realidad. Visualmente este ejemplo simple de puntos en 2D quiere expresar la idea visualmente:

 

keras tensorflow overfitting

Un buen modelo para los puntos representados en la figura de la izquierda podría ser la línea de la figura del medio, pero cuando se sobreentrena un modelo el resultado es una línea como la mostrada en la figura de la derecha.

Es evidente que, en general, esta situación presenta un impacto negativo en la eficiencia del modelo cuando este se usa para inferencia con datos nuevos. Por ello es muy importante evitar estar en esta situación y de aquí la utilidad de reservar una parte de datos de entrenamiento como datos de validación, como indicábamos en la sección anterior, para poder detectar esta situación. Los datos de validación del modelo se usan para probar y evaluar diferentes opciones de hiperparámetros para minimizar la situación de Overfitting, como el número de epochscon las que entrenar el modelo, el learning rateo la mejor arquitectura de red, por poner algunos ejemplos.

Como resumen de esta sección, resaltar la importancia de recolectar y revisar las métricas durante el proceso de entrenamiento de nuestra red neuronal para detectar si estamos incurriendo en Overfitting. Si este es el caso, existe una variedad de técnicas que podemos usar para mitigarlo y que cubriremos algunas de ellas en el siguiente capítulo.

 

Referencias del capítulo

[1]Véase http://ccodataset.org [Accedido: 18/08/2019]

[2]Véase http://www.image-net.org [Accedido: 18/08/2019]

[3]Véase http://github.com/openimages/dataset [Accedido: 18/08/2019]

[4]Véase http://www.visualqa.org [Accedido: 18/08/2019]

[5]Véase http://ufldl.stanford.edu/housenumbers [Accedido: 18/08/2019]

[6]Véase http://www.cs.toronto.edu/~kriz/cifar.htmt [Accedido: 18/08/2019]

[7]Véase https://github.com/zalandoresearch/fashion-mnist [Accedido: 18/08/2019]

[8]Véase http://ai.stanford.edu/~amaas/data/sentiment [Accedido: 18/08/2019]

[9]Véase https://archive.ics.uci.edu/ml/datasets/Twenty+Newsgroups [Accedido: 18/08/2019]

[10]Véase https://archive.ics.uci.edu/ml/datasets/reuters-21578+text+categorization+collection

[11]Véase https://wordnet.princeton.edu [Accedido: 18/08/2019]

[12]Véase https://www.yelp.com/dataset [Accedido: 18/08/2019]

[13]Véase https://corpus.byu.edu/wiki[Accedido: 18/08/2019]

[14]Véase http://u.cs.biu.ac.il/~koppel/BlogCorpus.htm[Accedido: 18/08/2019]

[15]Véase http://statmt.org/wmt11/translation-task.html[Accedido: 18/08/2019]

[16]Véase https://github.com/Jakobovski/free-spoken-digit-dataset[Accedido: 18/08/2019]

[17]Véase https://github.com/mdeff/fma [Accedido: 18/08/2019]

[18]Véase http://mtg.upf.edu/ismir2004/contest/tempoContest/node5.html [Accedido: 18/08/2019]

[19]Véase https://labrosa.ee.columbia.edu/millionsong[Accedido: 18/08/2019]

[20]Véase http://www.openslr.org/12 [Accedido: 18/08/2019]

[21]Véase http://www.robots.ox.ac.uk/~vgg/data/voxceleb [Accedido: 18/08/2019]

[22]Véase https://archive.ics.uci.edu/ml/machine-learning-databases/housing/housing.names

[23]Véase http://host.robots.ox.ac.uk/pascal/VOC/[Accedido: 18/08/2019]

[24]Véase https://www.plant-phenotyping.org/CVPPP2017[Accedido: 18/08/2019]

[25]Véase https://www.cityscapes-dataset.com[Accedido: 18/08/2019]

[26]Véase https://github.com/tensorflow/datasets [Accedido: 18/08/2019]

[27]Véase https://github.com/tensorflow/datasets/blob/master/docs/datasets.md [Accedido: 18/08/2019]

[28]Véase https://www.kaggle.com  [Accedido: 18/08/2019]

[29]Véase https://es.wikipedia.org/wiki/RGB [Accedido: 18/08/2019]

[30]Véase https://www.tensorflow.org/versions/r2.0/api_docs/python/tf/keras/preprocessing /image/ImageDataGenerator [Accedido: 18/08/2019]

[31]Véase https://www.tensorflow.org/guide/datasets [Accedido: 18/08/2019]

[32]Véase https://www.kaggle.com [Accedido: 18/08/2019]

[33]Véase https://www.kaggle.com/c/dogs-vs-cats/data [Accedido: 18/08/2019]

[34]Véase https://es.wikipedia.org/wiki/Joint_Photographic_Experts_Group [Accedido: 18/08/2019]

[35]Para más detalle de como elegir una función de coste remito al lector a la sección 4.5 del libro Deep Learning with Python de François Chollet.

[36]Véase https://wiki.python.org/moin/Generators [Accedido: 18/08/2019]

[37]Véase https://keras.io/callbacks/ [Accedido: 18/08/2019]