Reeuso de Redes Preentrenadas

Transferencia del conocimiento (Knowledge Transfering)

Basado en el ejemplo 5.3 de F Chollet, Deep Learning with Python, Ed Manning, 2018

Mariano Rivera
version 1.0
marzo 2019

import os
os.environ["CUDA_DEVICE_ORDER"]="PCI_BUS_ID";
os.environ["CUDA_VISIBLE_DEVICES"]="1";  
import keras
keras.__version__
Using TensorFlow backend.
'2.2.4'

VGG16: Una Red Convolucional Preentrenada

Una estrategia para lidiar en redes neuronales profundas es usar redes previamente preentrenadas con bases de datos grandes y adaptarlas al problema de nuestro interés.

Para este propósito es necesario que la red preentrenada haya sido entrenada para resolver un problema de carácter mas general, del que nuestro problema se pueda consider un caso particular. Por ejemplo, para el caso de clasificar perros y gatos podemos usar una red entrenada para clasificar mas clases como la llamada VGG16 (Simonyan and Zisserman, 2015). Las razones que por la que usamos VGG16 son las siguientes

(Simonyan and Zisserman, 2015) K. Simonyan and A. Zisserman, Very Deep Convolutional Networks for Large-Scale Image Recognition, 3rd ICLR 2015.

Las redes diponibles en keras que fueron entrenadas en la BD ImageNet son

La red VGG19 es una variante con mas capas de cálculo que la VGG16, por lo tanto mas pesada de almacenar en memoria y en requerimientos de cómputo.

Como vemos, dado que VGG16 fue entrenada para resolver el problema de clasificación de 1000 clases en ImageNet, debe en sus pesos codificar información para extraer rasgos de muy distintas classes de representradas en las mas de 1.4 millones de fotografías de ImageNet. Entre esas clases hay muy distintas variedades de animales, en muy distintos entornos. Por ello, VGG16 es una muy buena candidata para ser particularizada al problema de clasificación binario de perros y gatos.

Notamos que hay aquitecturas mas modernas, con mejores desempeños, pero VGG16 servirá muy bien para nuestro propósito. Como vimos en los ejemplos anteriores, las redes convolucionales para clasificación siguen una estructura de dos bloques:

  1. Etapa de capas convolucionales para extracción de rasgos

  2. Etapa de decisión basada en capas densas.

VGG16 sigue fielmente esta arquitectura. Ver la siginete figura

preentrenada0

El usar la VGG16 entrenada previamente con la BD ImageNet nos permite asumir que su etapa de extración de rasgos codifica, efectivamente, las relaciones epaciales que hacen a los objetos distinguibles y por la bastedad de ImageNet, nos hace asumir que dichas relaciones espaciales son los suficientemente genéricas para poder codificar rasgos distintivos de perros y gatos.

Iremos paso a paso, primero cargamos la red VGG16 dentro del paquete keras.applications.

Acceso a los Componentes de una Red Convolucional Preentrenada

Veamos como cargar un modelo preentrenado y como podemos tener acceso a sus componentes. A manera de ilustración definimos nuestra versión de la función summary de los modelos de keras. Con ello mostramos como acceder a los nombres de las capas, sus número de parametros, etc.

def resumen(model=None):
    '''
    '''
    header = '{:4} {:16} {:24} {:24} {:10}'.format('#', 'Layer Name','Layer Input Shape','Layer Output Shape','Parameters'
    )
    print('='*(len(header)))
    print(header)
    print('='*(len(header)))
    count=0
    count_trainable=0
    for i, layer in enumerate(model.layers):
        count_trainable += layer.count_params() if layer.trainable else 0
        input_shape = '{}'.format(layer.input_shape)
        output_shape = '{}'.format(layer.output_shape)
        str = '{:<4d} {:16} {:24} {:24} {:10}'.format(i,layer.name, input_shape, output_shape, layer.count_params())
        print(str)
        count += layer.count_params()
    print('_'*(len(header)))
    print('Total Parameters : ', count)
    print('Total Trainable Parameters : ', count_trainable)
    print('Total No-Trainable Parameters : ', count-count_trainable)
    
  
vgg16=None

Luego cargamos el modelo VGG16 con los siguientes parámetros:

from keras.applications import VGG16

vgg16 = VGG16(weights='imagenet',
                  include_top=True,
                  input_shape=(224, 224, 3))

resumen(vgg16)
==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    input_5          (None, 224, 224, 3)      (None, 224, 224, 3)               0
1    block1_conv1     (None, 224, 224, 3)      (None, 224, 224, 64)           1792
2    block1_conv2     (None, 224, 224, 64)     (None, 224, 224, 64)          36928
3    block1_pool      (None, 224, 224, 64)     (None, 112, 112, 64)              0
4    block2_conv1     (None, 112, 112, 64)     (None, 112, 112, 128)         73856
5    block2_conv2     (None, 112, 112, 128)    (None, 112, 112, 128)        147584
6    block2_pool      (None, 112, 112, 128)    (None, 56, 56, 128)               0
7    block3_conv1     (None, 56, 56, 128)      (None, 56, 56, 256)          295168
8    block3_conv2     (None, 56, 56, 256)      (None, 56, 56, 256)          590080
9    block3_conv3     (None, 56, 56, 256)      (None, 56, 56, 256)          590080
10   block3_pool      (None, 56, 56, 256)      (None, 28, 28, 256)               0
11   block4_conv1     (None, 28, 28, 256)      (None, 28, 28, 512)         1180160
12   block4_conv2     (None, 28, 28, 512)      (None, 28, 28, 512)         2359808
13   block4_conv3     (None, 28, 28, 512)      (None, 28, 28, 512)         2359808
14   block4_pool      (None, 28, 28, 512)      (None, 14, 14, 512)               0
15   block5_conv1     (None, 14, 14, 512)      (None, 14, 14, 512)         2359808
16   block5_conv2     (None, 14, 14, 512)      (None, 14, 14, 512)         2359808
17   block5_conv3     (None, 14, 14, 512)      (None, 14, 14, 512)         2359808
18   block5_pool      (None, 14, 14, 512)      (None, 7, 7, 512)                 0
19   flatten          (None, 7, 7, 512)        (None, 25088)                     0
20   fc1              (None, 25088)            (None, 4096)              102764544
21   fc2              (None, 4096)             (None, 4096)               16781312
22   predictions      (None, 4096)             (None, 1000)                4097000
__________________________________________________________________________________
Total Parameters :  138357544
Total Trainable Parameters :  138357544
Total No-Trainable Parameters :  0

vgg16_full

Son cerca de 139 millones de parámetros. Realmente consumió tiempo decargar el modelo completo (se realiza solo para primera vez que se invoca la función VGG16. Notamos que, afortunadamente, muchos de los parámetros (como el 90%) corresponden a la etapa de decisión. La etapa de extracción de rasgos (ilustrada en la siguiente figura) tiene menos parámetros.

vgg16_model

(Imagen tomada de la red, usada en varios blogs, como en la ilustración de vgg16 )

Para evitar cargar capas que no usaremos podemos invocar el método con el parámetro include_top=False y para el tamañno específico que hemos usado (150×150150 \times 150 pixeles)

if vgg16 != None:
    del vgg16
    
from keras.applications import VGG16

conv_base = VGG16(weights='imagenet',
                  include_top=False,
                  input_shape=(150, 150, 3))
    
resumen(conv_base)    
==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    input_6          (None, 150, 150, 3)      (None, 150, 150, 3)               0
1    block1_conv1     (None, 150, 150, 3)      (None, 150, 150, 64)           1792
2    block1_conv2     (None, 150, 150, 64)     (None, 150, 150, 64)          36928
3    block1_pool      (None, 150, 150, 64)     (None, 75, 75, 64)                0
4    block2_conv1     (None, 75, 75, 64)       (None, 75, 75, 128)           73856
5    block2_conv2     (None, 75, 75, 128)      (None, 75, 75, 128)          147584
6    block2_pool      (None, 75, 75, 128)      (None, 37, 37, 128)               0
7    block3_conv1     (None, 37, 37, 128)      (None, 37, 37, 256)          295168
8    block3_conv2     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
9    block3_conv3     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
10   block3_pool      (None, 37, 37, 256)      (None, 18, 18, 256)               0
11   block4_conv1     (None, 18, 18, 256)      (None, 18, 18, 512)         1180160
12   block4_conv2     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
13   block4_conv3     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
14   block4_pool      (None, 18, 18, 512)      (None, 9, 9, 512)                 0
15   block5_conv1     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
16   block5_conv2     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
17   block5_conv3     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
18   block5_pool      (None, 9, 9, 512)        (None, 4, 4, 512)                 0
__________________________________________________________________________________
Total Parameters :  14714688
Total Trainable Parameters :  14714688
Total No-Trainable Parameters :  0

Son “solamente” poco menos de 15 millones de parámetros, una reducción sustancial respecto a la red VGG16 completa.

Veamos el resumen de la red de Keras

#conv_base.summary()

La salida final del modelo base cargado (conv_base) tienen la forma (4, 4, 512).

Red Preentrenada como Extractor Rasgos Fuera de Línea

La primera estrategia que usaremos para reusar el conocimiento almacenado (adquirido) por una red clasificadora y particularizarlo a nuestro será en el considerar la extracción de rasgos independiente de la clasificación. Es un enfoque del tipo somero (en contraposición con lo profundo). Esto es, pasaremos las imágenes a la red convolucional base (conv_base) y almacenaremos en memoria o disco las características (lo que es computacionalmente eficiente) para luego alimentar con dichos rasgos un clasificador. Esto se ilustra en la siguiente figura.

preentrenada_somero

Para generar la codificación (encaje o embedding) de las imágenes de perros y gatos usaremos un generador

Es importante si usamos VGG16 preprocesar los datos (imágenes) para normalizarlas con el mismo procedimiento que se uso para entrenar la red original. En este caso, no es reescalarlas al intervalo [0,1][0,1], sino restar la media de cada canal de color

import os
import numpy as np
from tqdm import tqdm
from keras.applications.imagenet_utils import preprocess_input
#from tqdm import tqdm_notebook as tqdm

from keras.preprocessing.image import ImageDataGenerator

base_dir = '/home/mariano/Data/dogs_vs_cats_small'
#base_dir = '/home/mariano/Documents/deep/kaggle/dogs_vs_cats_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')

datagen = ImageDataGenerator(#rescale=1./255)
                             preprocessing_function=preprocess_input)
batch_size = 20

def extract_features(directory, sample_count):
    '''
    Codificador de imagenes mediante conv_base en rasgos para
    posteriormente usarlos como datos para una red clasificadora densa
    
    parámetros
    directory      directorio con con los subdirectorios que definen clases
    sample_count   número de muestras a generar
    
    resultados
    conjunto de características y  etiquetas  
    '''
    # memoria para tensores con datos y etiquetas
    features  = np.zeros(shape=(sample_count, 4, 4, 512))
    labels    = np.zeros(shape=(sample_count))
    # instanciación del generador a partir del directorio donde estan las clases
    generator = datagen.flow_from_directory(directory,
                                            target_size = (150, 150),
                                            batch_size  = batch_size,
                                            class_mode  = 'binary')
    
    rango = list(range(int(sample_count/batch_size)))
    i = 0
    with tqdm(total=len(rango)) as pbar:
        for inputs_batch, labels_batch in tqdm(generator):
            # características predichas (codificadas) por la subred base
            # para las imágenes generadas (aumentadas) en lote
            features_batch = conv_base.predict(inputs_batch)
            # datos y etiquetas
            features[i * batch_size : (i + 1) * batch_size] = features_batch
            labels  [i * batch_size : (i + 1) * batch_size] = labels_batch
            i += 1 
            if i * batch_size >= sample_count:
                # La ejecucion del generador debe terminarse explícitamente después 
                # usar todas la imágenes
                break
            pbar.update(1)
    
    return features, labels

Conjunto de datos-rasgos para entrenamiento, validación y prueba

train_features, train_labels           = extract_features(train_dir,      2000)
validation_features, validation_labels = extract_features(validation_dir, 1000)
test_features, test_labels             = extract_features(test_dir,       1000)
  0%|          | 0/100 [00:00<?, ?it/s]
  0%|          | 0/100 [00:00<?, ?it/s]

Found 2000 images belonging to 2 classes.


  1%|          | 1/100 [00:01<02:46,  1.68s/it]
  3%|▎         | 3/100 [00:01<01:56,  1.20s/it]
  ...
  99%|█████████▉| 99/100 [00:08<00:00, 13.13it/s]
  0%|          | 0/50 [00:00<?, ?it/s]
  2%|▏         | 1/50 [00:00<00:05,  8.29it/s]
  ...
  98%|█████████▊| 49/50 [00:03<00:00, 13.63it/s]
  0%|          | 0/50 [00:00<?, ?it/s]
  2%|▏         | 1/50 [00:00<00:19,  2.49it/s]
  ...
  98%|█████████▊| 49/50 [00:03<00:00, 13.49it/s]
generatorInfo = datagen.flow_from_directory(train_dir,
                                            target_size = (150, 150),
                                            batch_size  = 32,
                                            class_mode  = 'binary')

print('Generador            : ',generatorInfo.image_data_generator)
print('Directorio con datos : ',generatorInfo.directory)
print('Clases               : ',generatorInfo.class_indices)
print('Forma de cada imagen : ',generatorInfo.image_shape)
Found 2000 images belonging to 2 classes.
Generador            :  <keras.preprocessing.image.ImageDataGenerator object at 0x7fee4dec2fd0>
Directorio con datos :  /home/mariano/Data/dogs_vs_cats_small/train
Clases               :  {'cats': 0, 'dogs': 1}
Forma de cada imagen :  (150, 150, 3)

La salida de conv_base tienen la forma (samples, 4, 4, 512).

Que será la entrada a la red con capas densas, por lo que debe ser aplanada (Flattened) a (samples, 8192).

train_features      = np.reshape(train_features,      (2000, 4 * 4 * 512))
validation_features = np.reshape(validation_features, (1000, 4 * 4 * 512))
test_features       = np.reshape(test_features,       (1000, 4 * 4 * 512))

model = None

Red Clasificadora Densa

Clasifica las imágenes usando sus características extraidas conv_base

if model != None:
    model.reset_states()     
from keras import models
from keras import layers
from keras import optimizers

model = models.Sequential()
model.add(layers.Dense(256, activation='relu', input_dim=4 * 4 * 512))
model.add(layers.Dropout(0.5))
model.add(layers.Dense(1, activation='sigmoid'))

model.compile(optimizer=optimizers.RMSprop(lr=2e-5),
              loss='binary_crossentropy',
              metrics=['acc'])

import time
tstart = time.time()

history = model.fit(train_features, train_labels,
                    epochs          = 30,
                    batch_size      = 20,
                    validation_data = (validation_features, validation_labels),
                    verbose         = 2)

print('seconds=', time.time()-tstart)
Train on 2000 samples, validate on 1000 samples
Epoch 1/30
 - 1s - loss: 3.3298 - acc: 0.7205 - val_loss: 1.0112 - val_acc: 0.8950
...
Epoch 30/30
 - 0s - loss: 0.0800 - acc: 0.9905 - val_loss: 0.3714 - val_acc: 0.9630
seconds= 14.222734451293945

El entrenamiento es muy rápido dado que la red clasificadora consta únicamente de dos capas Dense.

las curvas del valor función objetivo y la exactitud (accuracy) se muestran a continuación.

import matplotlib.pyplot as plt

acc      = history.history['acc']
val_acc  = history.history['val_acc']
loss     = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Entrenamiento acc')
plt.plot(epochs, val_acc, 'b', label='Validación acc')
plt.title('Accuracy - exactitud de entrenamiento y validación')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Entrenamiento loss')
plt.plot(epochs, val_loss, 'b', label='Validación loss')
plt.title('Loss - función objetivo en entrenamiento y prueba')
plt.legend()

plt.show()

png

png

La valicación alcanza los 90% de exactitud, mucho mejor que el modelo entrenado con aumentación de datos y la BD paqueña que alcanzó un 86%.

La curva de la exactitud ilustra que muy pronto se empieza a tener un sobreajuste (overfitting). Se debe a que no usamos aumentación.

Para implementar aumentación debemos hacer que la extracción de rasgos se haga a la vez que clasificamos, como un modelo completo. Pues en memoria o disco sería impráctico almacenar los datos aumentados para su posterior clasificación. Esto lo explicamos a continuación.

Transferencia de Conocimiento de Redes Preentrenadas a Nuevos Problemas

El modelo conv_base como capa de extracción de resgos

Si asumimos que etapa Convolucional de la VGG16 extrae caractéristicas generales, será esa la parte que podamos reusar. Las características extraidas deberán ser pasadas a un nuevo clasificador binario que debemos implementar y entrenar ex-profeso. Este proceso se mustra en la siguiente figura

preentrenada

Como la figura ilustra,

  1. Reusamos solo la etapa convolucional de la red.

  2. Definimos una nueva etapa de clasificación acorde a nuestro problema.

  3. Dado que tenemos, relativamente, pocos datos de entrenamiento, fijamos la subred de extracción de rasgos (congelamos sus pesos) para evitar que sean modificados en el entrenamiento.

  4. Entrenamos los pesos “entrenables”, los de la etapa de clasificación haciendolos pasar nuestro datos por toda la red.

Usaremos los generadores de datos para hacer un entrenamiento con generadores (fit_generator), como en el ejemplo de la sección anterior.

En Keras todo son capas (layers), por lo que usaremos el modelo conv_base como la primera capa de la red, una capa que calcula rasgos.

Luego añadimos al nuevo modelo secuencial capas clasificadoras densas.

Esta técnica aunque simple de implementar hara que cada imagen aumentada sea pasada por la red conv_base y luego por la clasificadora. Esto hace que los calculos sean ahora mas costosos y no es posible realizarlos en CPU, requerimos de una GPU.

model.reset_states()
from keras import models
from keras import layers

model = models.Sequential()
model.add(conv_base)        # modelo base agradado como una capa!
model.add(layers.Flatten())
model.add(layers.Dense(256, activation='relu'))
#model.add(layers.Dropout(0.3))  # a ver
model.add(layers.Dense(1, activation='sigmoid'))

This is what our model looks like now:

#model.summary()

resumen(model)
==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    vgg16            (None, 150, 150, 3)      (None, 4, 4, 512)          14714688
1    flatten_1        (None, 4, 4, 512)        (None, 8192)                      0
2    dense_3          (None, 8192)             (None, 256)                 2097408
3    dense_4          (None, 256)              (None, 1)                       257
__________________________________________________________________________________
Total Parameters :  16812353
Total Trainable Parameters :  16812353
Total No-Trainable Parameters :  0

La red convolucional base VGG16 htienen casi 15 millones de parámetros, lo que es muy grande. El clasificador en el tope añade otros 2 milliones de parámetetros.

Antes de entrenar “congelaremos” la red extracción de rasgos dado que contamos con pocos datos y queremos aprovechar “el conocimiento” almacenado en la subred base de VGG16 que fue entrenada con muchas clases y debe ser un extractor muy general de características.

Esto lo logramos marcando los pesos de “la capa” conv_base como no-entrenable:

print('Número de pesos (matrices) entrenables antes de congelar conv_base : ', len(model.trainable_weights))

Número de pesos (matrices) entrenables antes de congelar conv_base :  30
conv_base.trainable = False
print('Número de pesos (matrices) entrenables después de congelar conv_base : ', len(model.trainable_weights))

Número de pesos (matrices) entrenables después de congelar conv_base :  4
resumen(model)
==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    vgg16            (None, 150, 150, 3)      (None, 4, 4, 512)          14714688
1    flatten_1        (None, 4, 4, 512)        (None, 8192)                      0
2    dense_3          (None, 8192)             (None, 256)                 2097408
3    dense_4          (None, 256)              (None, 1)                       257
__________________________________________________________________________________
Total Parameters :  16812353
Total Trainable Parameters :  2097665
Total No-Trainable Parameters :  14714688

El número de matrices sobrepasa las capas porque cada capa puede tener una matriz de pesos ww y un vector de bias bb.

Para hacer que se congelen los datos necesitamos compilar el modelo y definimos luego los parametros para el generador.

from keras.preprocessing.image import ImageDataGenerator
from keras import models
from keras import layers
from keras import optimizers

train_datagen = ImageDataGenerator(#rescale           = 1./255,
                                   preprocessing_function=preprocess_input,
                                   rotation_range    = 40,
                                   width_shift_range = 0.2,
                                   height_shift_range= 0.2,
                                   shear_range       = 0.2,
                                   zoom_range        = 0.2,
                                   horizontal_flip   = True,
                                   fill_mode         = 'constant',  #'nearest')
                                   cval              = 0)

# La validación no se aumenta!
test_datagen = ImageDataGenerator(#rescale=1./255)
                                    preprocessing_function=preprocess_input)

train_generator = train_datagen.flow_from_directory(
                        train_dir,                # directorio con datos de entrenamiento
                        target_size= (150, 150),  # tamaño de la imágenes 
                        batch_size = 20,   
                        shuffle    = True,
                        class_mode = 'binary')    # para clasificación binaria

validation_generator = test_datagen.flow_from_directory(
                        validation_dir,
                        target_size=(150, 150),
                        batch_size=20,
                        class_mode='binary')

model.compile(loss='binary_crossentropy',
              optimizer=keras.optimizers.Nadam(lr=2e-5), #optimizers.RMSprop(lr=2e-5),
              metrics=['acc'])

#model.reset_states()

import time
tstart = time.time()

history = model.fit_generator(train_generator,
                              steps_per_epoch = 100, 
                              epochs          = 30,
                              validation_data = validation_generator,
                              validation_steps= 50,
                              verbose         = 2)

print('seconds=', time.time()-tstart)
Found 2000 images belonging to 2 classes.
Found 1000 images belonging to 2 classes.
Epoch 1/30
 - 10s - loss: 1.9860 - acc: 0.7660 - val_loss: 0.7680 - val_acc: 0.9040
Epoch 2/30
 - 9s - loss: 0.9115 - acc: 0.8815 - val_loss: 0.5760 - val_acc: 0.9210

...
Epoch 30/30
 - 9s - loss: 0.1956 - acc: 0.9640 - val_loss: 0.3213 - val_acc: 0.9660
seconds= 281.5276675224304
model.save('cats_and_dogs_small_3.h5')

Graficamos de nuevo el comportamiento de las métricas registradas durante el entrenamiento

import matplotlib.pyplot as plt

acc      = history.history['acc']
val_acc  = history.history['val_acc']
loss     = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Entrenamiento acc')
plt.plot(epochs, val_acc, 'b', label='Validación acc')
plt.title('Accuracy - exactitud de entrenamiento y validación')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Entrenamiento loss')
plt.plot(epochs, val_loss, 'b', label='Validación loss')
plt.title('Loss - función objetivo en entrenamiento y prueba')
plt.legend()

plt.show()

png

png

De acuerdo al autor del libro (Chollet), se alcanza una exactitud de cercana al 96.5%.

Ajuste fino

vgg16_fino

Otra estrategia ampliamente usada en reuso de conocimeinto es el Ajuste Fino (fine-tuning), el cual consite en que, una vez entrenada una capa clasificadora con base prenetrenada, descongelar las últimas capas de la base convolucional para permitir que sus pesos se adapten mejor a la capa clasificadora. Logrando una mejor integración entre la base (entrenada para un problema diferente) y la clasificación.

Los pasos del entrenamiento fino son:

  1. Añadir a un base convolucional general una etapa final de clasificación.
  2. Congelar la base convolucional.
  3. Entrenar la etapa clasificadora.
  4. Descongelar las últimas capas de la base convolucional.
  5. Entrenar etapa clasificadora y últimas capas de la convolucional juntas.

Partiedo del modelo anterio entrenado (pasos 1,2,y 3) vemos que las capas a descongelar son las correspondientes al bloque5.

Como recordatorio el resumen de la convolucional es

resumen(conv_base)
==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    input_6          (None, 150, 150, 3)      (None, 150, 150, 3)               0
1    block1_conv1     (None, 150, 150, 3)      (None, 150, 150, 64)           1792
2    block1_conv2     (None, 150, 150, 64)     (None, 150, 150, 64)          36928
3    block1_pool      (None, 150, 150, 64)     (None, 75, 75, 64)                0
4    block2_conv1     (None, 75, 75, 64)       (None, 75, 75, 128)           73856
5    block2_conv2     (None, 75, 75, 128)      (None, 75, 75, 128)          147584
6    block2_pool      (None, 75, 75, 128)      (None, 37, 37, 128)               0
7    block3_conv1     (None, 37, 37, 128)      (None, 37, 37, 256)          295168
8    block3_conv2     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
9    block3_conv3     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
10   block3_pool      (None, 37, 37, 256)      (None, 18, 18, 256)               0
11   block4_conv1     (None, 18, 18, 256)      (None, 18, 18, 512)         1180160
12   block4_conv2     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
13   block4_conv3     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
14   block4_pool      (None, 18, 18, 512)      (None, 9, 9, 512)                 0
15   block5_conv1     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
16   block5_conv2     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
17   block5_conv3     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
18   block5_pool      (None, 9, 9, 512)        (None, 4, 4, 512)                 0
__________________________________________________________________________________
Total Parameters :  14714688
Total Trainable Parameters :  14714688
Total No-Trainable Parameters :  0

Haremos el ajuste fino sobre las últimas tres capas convolucionales: block5_conv1, block5_conv2 y block5_conv3.

Es importante notar que las capas mas tempranas codifican información mas general por lo que no es conveniente renetrenarlas si el propósito es reusar conocimiento. Terminaríamos sobreentrenando el modelo a nustras BD pequeña.

# Orden de las capas 
for layer in conv_base.layers:
    print(layer.name)
input_6
block1_conv1
block1_conv2
block1_pool
block2_conv1
block2_conv2
block2_pool
block3_conv1
block3_conv2
block3_conv3
block3_pool
block4_conv1
block4_conv2
block4_conv3
block4_pool
block5_conv1
block5_conv2
block5_conv3
block5_pool
conv_base.trainable = True

set_trainable = False
for layer in conv_base.layers:
    if layer.name in ['block5_conv1','block5_conv2','block5_conv3']:
        layer.trainable = True
    else:
        layer.trainable = False
        
resumen(conv_base)   

==================================================================================
#    Layer Name       Layer Input Shape        Layer Output Shape       Parameters
==================================================================================
0    input_6          (None, 150, 150, 3)      (None, 150, 150, 3)               0
1    block1_conv1     (None, 150, 150, 3)      (None, 150, 150, 64)           1792
2    block1_conv2     (None, 150, 150, 64)     (None, 150, 150, 64)          36928
3    block1_pool      (None, 150, 150, 64)     (None, 75, 75, 64)                0
4    block2_conv1     (None, 75, 75, 64)       (None, 75, 75, 128)           73856
5    block2_conv2     (None, 75, 75, 128)      (None, 75, 75, 128)          147584
6    block2_pool      (None, 75, 75, 128)      (None, 37, 37, 128)               0
7    block3_conv1     (None, 37, 37, 128)      (None, 37, 37, 256)          295168
8    block3_conv2     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
9    block3_conv3     (None, 37, 37, 256)      (None, 37, 37, 256)          590080
10   block3_pool      (None, 37, 37, 256)      (None, 18, 18, 256)               0
11   block4_conv1     (None, 18, 18, 256)      (None, 18, 18, 512)         1180160
12   block4_conv2     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
13   block4_conv3     (None, 18, 18, 512)      (None, 18, 18, 512)         2359808
14   block4_pool      (None, 18, 18, 512)      (None, 9, 9, 512)                 0
15   block5_conv1     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
16   block5_conv2     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
17   block5_conv3     (None, 9, 9, 512)        (None, 9, 9, 512)           2359808
18   block5_pool      (None, 9, 9, 512)        (None, 4, 4, 512)                 0
__________________________________________________________________________________
Total Parameters :  14714688
Total Trainable Parameters :  7079424
Total No-Trainable Parameters :  7635264

Note ahora que la conv_base tienen paramteros entrenables y no entrenables.

Para el entrenamiento fino procedemos a compilar el modelo con un tamaño de paso pequeño para evitar cambios grandes en los pesos, ya que asumimos estra cerca del óptimo y el ajuste no debe ser muy grande.

model.compile(loss='binary_crossentropy',
              optimizer=optimizers.RMSprop(lr=1e-5),
              metrics=['acc'])

history = model.fit_generator(
      train_generator,
      steps_per_epoch=100,
      epochs=100,
      validation_data=validation_generator,
      validation_steps=50)
Epoch 1/100
100/100 [==============================] - 10s 104ms/step - loss: 0.1938 - acc: 0.9645 - val_loss: 0.2769 - val_acc: 0.9580
Epoch 2/100
100/100 [==============================] - 9s 94ms/step - loss: 0.2239 - acc: 0.9635 - val_loss: 0.2606 - val_acc: 0.9650
...    
Epoch 100/100
100/100 [==============================] - 10s 97ms/step - loss: 0.0269 - acc: 0.9960 - val_loss: 0.3666 - val_acc: 0.9620
model.save('cats_and_dogs_small_4.h5')

Gráficas de las métricas registradas durante el entrenamiento

import matplotlib.pyplot as plt

acc      = history.history['acc']
val_acc  = history.history['val_acc']
loss     = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))

plt.plot(epochs, acc, 'bo', label='Entrenamiento acc')
plt.plot(epochs, val_acc, 'b', label='Validación acc')
plt.title('Accuracy - exactitud de entrenamiento y validación')
plt.legend()

plt.figure()

plt.plot(epochs, loss, 'bo', label='Entrenamiento loss')
plt.plot(epochs, val_loss, 'b', label='Validación loss')
plt.title('Loss - función objetivo en entrenamiento y prueba')
plt.legend()

plt.show()

png

png

Dado que las curvas son muy ruidosas, es posible filtrarlas para apreciar mejor la tendencia mediante un filtro pasa baja, uno podria intentar

s~i=k=0nwksik \tilde s_i = \sum_{k=0}^n w_k s_{i-k}
donde ww son un pesos con kwk=1\sum_k w_k =1; pero requeririamos usar un filtro bastante ancho (nn grande) para eliminar ruido apropiadamente, en vez de ello tratemos la versión recursiva (como en el libro en que se basa estas notas)

s~i=αs~i1+(1α)si \tilde s_i = \alpha \tilde s_{i-1} + (1-\alpha) s_i

Que usa el dato recien actualizado con un procedimiento similare. Estos filtros recursivos pueden expresarse como no recursivos (sustituyendo recursivamente el dato actualizado por su fórmula). La ventaja de la forma recursiva es lo compacto de se expresión de actualización

def smooth_curve(points, factor=0.8):
  smoothed_points = []
  for point in points:
    if smoothed_points:
      previous = smoothed_points[-1]
      smoothed_points.append(previous * factor + point * (1 - factor))
    else:
      smoothed_points.append(point)
  return smoothed_points


import matplotlib.pyplot as plt

acc      = history.history['acc']
val_acc  = history.history['val_acc']
loss     = history.history['loss']
val_loss = history.history['val_loss']

epochs = range(len(acc))
alpha = 0.9

plt.plot(epochs, smooth_curve(acc,     alpha), 'bo', label='Entrenamiento acc')
plt.plot(epochs, smooth_curve(val_acc, alpha), 'b', label='Validación acc')
plt.title('Accuracy - exactitud de entrenamiento y validación')
plt.legend()

plt.figure()

plt.plot(epochs, smooth_curve(loss,    alpha), 'bo', label='Entrenamiento loss')
plt.plot(epochs, smooth_curve(val_loss,alpha), 'b', label='Validación loss')
plt.title('Loss - función objetivo en entrenamiento y prueba')
plt.legend()

plt.show()

png

png

En estas curvas se puede apreciar mejor la tendencia, podemos notar que se tuvo una mejora de alrededor del 1%.

En (Chollet, 2018) se hace la observación de que la exactitud esta mejorando aun cuando la pérdida no lo hace. Esto se debe a que la pérdida se evalúa mediante un proceso de suma de errores en el vector de salida, algo como:

iepochyiy^iM \sum_{i \in epoch} \| y_i - \hat y_i \|_M

donde asumimos que yy esta en una codificación one-hot y M indica alguna métrica. En cambio, la exactitud se calcula mediante una suma errores de clasificación:

iepoch1δ(argmaxiyiargmaxiy^i) \sum_{i \in epoch} 1-\delta (\arg \max_i y_i - \arg \max_i {\hat y_i})

Ahora evaluemos, finalmente y por única vez, el conjunto de prueba:

test_generator = test_datagen.flow_from_directory(
        test_dir,
        target_size=(150, 150),
        batch_size=20,
        class_mode='binary')

test_loss, test_acc = model.evaluate_generator(test_generator, steps=50)
print('test acc:', test_acc)
Found 1000 images belonging to 2 classes.
test acc: 0.9669999933242798

*Muy cercano al 97%

En la competencia original de Kaggle, estaría entre los mejores (top) resultados, y eso que sólo usamos 2000 datos de entrenamiento vs. 20,000 en la competencia de Kaggle.

Resumen