Alberto Beiz

Mis pruebas y experimentos.

Contacta sin miedo:

Finetuning VGG16 con Keras (97% de precisión)

¿VGG16? Vgg16 es una red neuronal entrenada para clasificar imágenes en 1.000 clases. Entrenada por gente con más conocimientos, tiempo y dinero que nosotros.

¿ImageNet? Es una competición de clasificación de imágenes. Vgg fue creada para participar en esta competición.

¿Finetuning? Es adaptar un modelo ya entrenado para resolver una tarea parecida.

Pues con esta información vamos al lio. Vamos a adaptar VGG para nuestro problema de diferenciar perretes y gatetes.

import utils

from keras.preprocessing import image
from keras.applications import VGG16
from keras.models import Model, Sequential
from keras.layers import Dense, Dropout, Flatten, BatchNormalization
from keras.optimizers import Adam
from keras.utils.np_utils import to_categorical

import numpy as np
import os

working_dir = os.getcwd()
LESSON_HOME_DIR = working_dir + '/catsdogs'
DATA_HOME_DIR = LESSON_HOME_DIR + '/data'
# Tamaño del minibatch
batch_size = 64

# Tamaño de las imágenes
image_size = 224
# Generamos los batches, usamos un iterador que
# va devolviendo imágenes según las necesitemos

# NO NORMALIZAMOS porque VGG no esta entrenada
# en imágenes normalizadas, MUY IMPORTANTE
gen = image.ImageDataGenerator()
train_data = gen.flow_from_directory(
                DATA_HOME_DIR+'/train/', 
                target_size=(image_size,image_size),
                class_mode='categorical', 
                shuffle=True, 
                batch_size=batch_size)

valid_data = gen.flow_from_directory(
                DATA_HOME_DIR+'/valid/', 
                target_size=(image_size,image_size),
                class_mode='categorical', 
                shuffle=True, 
                batch_size=batch_size)
Found 23000 images belonging to 2 classes.
Found 2000 images belonging to 2 classes.
# Podemos ver algunas imágenes y sus clases
images = next(train_data)
utils.plot_images(images[0][:8], images[1][:8], denormalize=False)

png

# Creamos un modelo VGG que nos proporciona Keras
vgg = VGG16(include_top=True, weights='imagenet', input_shape=(224,224,3))

# Bloqueamos las capas
# Entrenar las capas de un modelo convolucional ya entrenado
# no suele aportar mucho y alarga el entrenamiento
for layer in vgg.layers: layer.trainable=False

    
# Conectamos la penúltima capa de VGG
# a una capa densa que clasifique en
# nuestras dos clases, perro o gato.

# Usamos la penúltima porque la última
# es la que usa VGG para clasificar en 
# 1000 clases
x = vgg.layers[-2].output
output_layer = Dense(2, activation='softmax', name='predictions')(x)

# Combinamos los modelos
model = Model(inputs=vgg.input, outputs=output_layer)

model.compile(optimizer=Adam(lr=0.001),
                          loss='categorical_crossentropy', 
                          metrics=['accuracy'])

# Haz vgg.summary() y model.summary() y fíjate en
# la última capa
# Entrenamos
model.fit_generator(train_data,
                   steps_per_epoch = train_data.samples // batch_size,
                   validation_data = valid_data, 
                   validation_steps = valid_data.samples // batch_size,
                   epochs = 1)
Epoch 1/1
359/359 [==============================] - 345s 962ms/step - loss: 0.1280 - acc: 0.9595 - val_loss: 0.1088 - val_acc: 0.9677

No esta mal, casi un 97% en 6 minutos de GPU.

Precalculando las predicciones de VGG

Podemos mejorar el tiempo de entrenamiento y la facilidad para realizar pruebas si en vez de extender el modelo VGG calculamos su salida, que nunca va a cambiar porque no es entrenable, y luego usamos esa salida como entrada de nuestro modelo.

# No incluimos el top del modelo (las capas densas)
# Solo nos interesan las capas convolucionales
vgg_pred = VGG16(include_top=False, weights='imagenet', input_shape=(224,224,3))
# Los generadores ahora no deben mezclar las imágenes
# Queremos una predicción por cada imágen
gen = image.ImageDataGenerator()
train_data = gen.flow_from_directory(
                DATA_HOME_DIR+'/train/', 
                target_size=(image_size,image_size),
                class_mode='categorical', 
                shuffle=False, 
                batch_size=batch_size)

valid_data = gen.flow_from_directory(
                DATA_HOME_DIR+'/valid/', 
                target_size=(image_size,image_size),
                class_mode='categorical', 
                shuffle=False, 
                batch_size=batch_size)
Found 23000 images belonging to 2 classes.
Found 2000 images belonging to 2 classes.
# Calculamos la predicción de cada imagen
train_pred = vgg_pred.predict_generator(train_data, train_data.samples/batch_size, verbose=1)
valid_pred = vgg_pred.predict_generator(valid_data, valid_data.samples/batch_size, verbose=1)
360/359 [==============================] - 382s 1s/step
32/31 [==============================] - 34s 1s/step
utils.save_array('train_data.bc', train_pred)
utils.save_array('valid_data.bc', valid_pred)
train_pred = utils.load_array('train_data.bc')
valid_pred = utils.load_array('valid_data.bc')
# Pasamos las clases a one-hot encoding
to_categorical(valid_data.classes)
array([[1., 0.],
       [1., 0.],
       [1., 0.],
       ...,
       [0., 1.],
       [0., 1.],
       [0., 1.]])
# Estos arrays también deberíamos guardarlos
# y asi no hay que usar más los generadores
train_pred_labels = to_categorical(train_data.classes)
valid_pred_labels = to_categorical(valid_data.classes)
# Creamos un modelo muy sencillo para
# clasificar la salida de las capas convolucionales
# de VGG en nuestras dos clases
top_model = Sequential()

top_model.add(BatchNormalization(input_shape=train_pred[0].shape))

top_model.add(Flatten())
top_model.add(Dense(1024, activation='relu'))
top_model.add(BatchNormalization())
top_model.add(Dropout(0.2))

top_model.add(Dense(1024, activation='relu'))
top_model.add(BatchNormalization())
top_model.add(Dropout(0.2))

top_model.add(Dense(2, activation='softmax'))

top_model.compile(optimizer=Adam(lr=0.001),
                    loss='categorical_crossentropy', 
                    metrics=['accuracy'])
# Entrenamos el modelo
top_model.fit(train_pred, train_pred_labels,
                batch_size=batch_size,
                epochs=1,
                validation_data=(valid_pred, valid_pred_labels))
Train on 23000 samples, validate on 2000 samples
Epoch 1/1
23000/23000 [==============================] - 24s 1ms/step - loss: 0.1531 - acc: 0.9493 - val_loss: 0.0735 - val_acc: 0.9700

Ahora podríamos cambiar el top_model y añadir más capas o parámetros y volver a entrenar por 20 segundos cada epoch en vez de 6 minutos. ¡Brutal!

Conclusión

¡Vamos mejorando! Un 97% de precisión resolviendo un problema nada trivial con pocas líneas de código. Y aprovechando los modelos que sabemos que funcionan.

Fuentes