Vicente Rodríguez

Nov. 27, 2018

Cómo crear una red neuronal convolucional con keras

En este tutorial crearemos un modelo CNN para identificar los distintos modelos de coches Tesla.

La explicación sobre redes neuronales convolucionales esta en mi tutorial anterior.

Importando librerías

Primero importaremos las librerías necesarias para crear una red neuronal convolucional con keras, dejo el link a la libreta donde esta todo el código, es recomendable verla.


import numpy as np

import os

import shutil

from keras.utils import to_categorical



from keras.models import Sequential

from keras.layers import Dense, Flatten, Activation, regularizers

from keras.layers import Conv2D, MaxPooling2D

from keras import optimizers



from keras.preprocessing.image import ImageDataGenerator, array_to_img, img_to_array, load_img

from keras.callbacks import ModelCheckpoint



from matplotlib import pyplot as plt

import os

Obtener imagenes

Este es el mismo código que usamos la vez pasada para obtener las imagenes de los coches, primero descargamos el dataset de github y con python cargamos las imagenes a la memoria ram, usaré comandos de la terminal de linux para descargar el código y crear carpetas aunque estos pasos se pueden realizar con el explorador de archivos de cada sistema operativo.

Comandos:


wget https://github.com/vincent1bt/tesla-cars-dataset/archive/master.zip # descargar imagenes



unzip -qq master.zip # descomprimir zip



mkdir -p validation_images/tesla_model_3 && mkdir validation_images/tesla_model_s && mkdir validation_images/tesla_model_x # crear carpetas de validacion



Python:

Estas líneas moveran 30 imagenes de cada clase a las carpetas de validación:


validation_set_size = 30



def move_images(from_path, to_path):

  files = os.listdir(from_path)

  folder_size = len(files)

  first_index = folder_size - validation_set_size

  files_to_move = files[first_index:]



  for file_name in files_to_move:

    source_file_name = from_path + file_name

    destination_file_name = to_path + file_name

    shutil.move(source_file_name, destination_file_name)



move_images("./tesla-cars-dataset-master/tesla-model-3/", "./validation_images/tesla_model_3/")

move_images("./tesla-cars-dataset-master/tesla-model-s/", "./validation_images/tesla_model_s/")

move_images("./tesla-cars-dataset-master/tesla-model-x/", "./validation_images/tesla_model_x/")



Comandos:


mv tesla-cars-dataset-master training_images #cambiar el nombre de la carpeta de entrenamiento



mv training_images/tesla-model-3 training_images/tesla_model_3

mv training_images/tesla-model-s training_images/tesla_model_s

mv training_images/tesla-model-x training_images/tesla_model_x



Python:

Cargamos las imagenes a memoria ram, en X estarán las imagenes y en y las clases.


img_height = 256

img_width = 256



def load_images(paths):

  X = []

  y = []



  for path in paths:

    images_paths = os.listdir(path)



    for image_path in images_paths:

      complete_path = path + image_path

      image = load_img(complete_path, target_size=(img_height, img_width))

      image_array = img_to_array(image)

      X.append(image_array)

      label = paths.index(path)

      y.append(label)



  return X, y

Ejecutamos la función indicando las rutas de las imagenes:


training_paths = ["training_images/tesla_model_3/", "training_images/tesla_model_s/", "training_images/tesla_model_x/"]

validation_paths = ["validation_images/tesla_model_3/", "validation_images/tesla_model_s/", "validation_images/tesla_model_x/"]



X_train, y_train = load_images(training_paths)

X_val, y_val = load_images(validation_paths)

Transformamos las imagenes a arrays de numpy:


X_train = np.array(X_train)

X_val = np.array(X_val)



y_train = np.array(y_train)

y_val = np.array(y_val)

y modificamos el array de clases y para que keras lo pueda leer:


y_train = to_categorical(y_train)

y_val = to_categorical(y_val)

Data Augmentation

El primer metodo que usaremos es Data Augmentation, este metodo sirve para crear más imagenes a partir de modificaciones de una imagen, si tenemos una imagen como:

imagen original

Aplicando Data Augmentation obtendremos varias imagenes con modificaciones:

Data Augmentation

Algunas tienen pequeñas rotaciones, otras tienen un efecto espejo para cambiar la orientación, también aplicamos un acercamiento o zoom.

Tal vez para nosotros las diferencias no sean muchas y simplemente veamos imagenes repetidas, pero estas variaciones son muy importantes para la red neuronal, permite aprovechar cada parte de la imagen para encontrar patrones y aumenta la cantidad de imagenes que tenemos para entrenar, algo importante ya que el numero de imagenes que tenemos es muy pequeño, son aproximadamente 120 imagenes por clase.

Si queremos aplicar este metodo a una imagen tenemos que hacer lo siguiente:


datagen = ImageDataGenerator(

        rotation_range = 20,

        width_shift_range = 0.2,

        height_shift_range = 0.2,

        rescale = 1. / 255,

        shear_range = 0.2,

        zoom_range = 0.2,

        horizontal_flip = True,

        fill_mode = 'nearest')

Primero usamos el metodo ImageDataGenerator de keras, en los parametros de este metodo especificamos las modificaciones que queremos que se le apliquen a la imagen:

Necesitamos crear una carpeta con el nombre previews:


mkdir preview

Ahora cargamos la imagen que queramos usar y le aplicamos las transformaciones:


img = load_img("training_images/tesla_model_3/1-2.jpg", target_size=(256, 256))

img = img_to_array(img)

img = img.reshape((1,) + img.shape)



i = 0

for batch in datagen.flow(img, batch_size=1,

  save_to_dir='preview', save_prefix='car', save_format='jpeg'):

    i += 1

    if i > 20:

        break

Creamos 20 imagenes con las transformaciones de ImageDataGenerator y las guardamos en la carpeta previews

Si queremos visualizarlas usaremos dos funciones más:


def load_preview_images():

  path = "./preview/"

  X = []



  images_paths = os.listdir(path)



  for image_path in images_paths:

    complete_path = path + image_path

    image = load_img(complete_path)

    X.append(image)



  return X



X_preview = load_preview_images()



def plot_images(images):    

  fig, axes = plt.subplots(4, 5)

  plt.rcParams["figure.figsize"] = (20, 15)



  for i, ax in enumerate(axes.flat):

      ax.imshow(images[i])



      ax.set_xticks([])

      ax.set_yticks([])



  plt.show()



plot_images(X_preview)

Para nuestra red neuronal necesitaremos dos generadores:


train_generator = ImageDataGenerator(

        rotation_range = 20,

        width_shift_range = 0.2,

        height_shift_range = 0.2,

        rescale = 1. / 255,

        shear_range = 0.2,

        zoom_range = 0.2,

        horizontal_flip = True,

        fill_mode = 'nearest')



valid_generator = ImageDataGenerator(rescale = 1. / 255)

train_generator aplicará transformaciones a las imagenes de entrenamiento, valid_generator solo reescalará las imagenes de validación.

Modelo

Crearemos un modelo CNN, como solo tenemos 3 clases y las imagenes son sencillas no necesitamos un modelo tan complejo, con pocos kernels y pocas capas será suficiente.

Primero indicamos las dimensiones de cada imagen, usaremos imagenes un poco pequeñas (256 x 256 x 3), el tamaño del kernel para todas las capas sera de 5 x 5.


input_shape=(256, 256, 3)

kernel_size = 5

Creamos una función para imprimir graficas del rendimiento de la red neuronal en cada epoch:


def plot_loss_and_accuracy(model_trained):

  accuracy = model_trained.history['acc']

  val_accuracy = model_trained.history['val_acc']

  loss = model_trained.history['loss']

  val_loss = model_trained.history['val_loss']

  epochs = range(len(accuracy))

  plt.plot(epochs, accuracy, 'b', label='Training accuracy')

  plt.plot(epochs, val_accuracy, 'r', label='Validation accuracy')

  plt.ylim(ymin=0)

  plt.ylim(ymax=1)

  plt.xlabel('Epochs ', fontsize=16)

  plt.ylabel('Accuracity', fontsize=16)

  plt.title('Training and validation accuracy', fontsize = 20)

  plt.legend()

  plt.figure()

  plt.plot(epochs, loss, 'b', label='Training loss')

  plt.plot(epochs, val_loss, 'r', label='Validation loss')

  plt.xlabel('Epochs ', fontsize=16)

  plt.ylabel('Loss', fontsize=16)

  plt.title('Training and validation loss', fontsize= 20)

  plt.legend()

  plt.show()

Esta función crea un nuevo modelo con los parametros indicados:


def create_model(X_train, X_val, y_train, y_val, learning_rate, epochs, batch_size, callbacks):

  model = Sequential()



  #Primera capa

  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid",

        strides=1, input_shape=input_shape))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  #Segunda capa

  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid", strides=1))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  #Tercera capa

  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid",

        strides=1,))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  #Convertir los datos

  model.add(Flatten())



  #Cuarta capa

  model.add(Dense(500))

  model.add(Activation('relu'))



  #Clasificación 

  model.add(Dense(3))

  model.add(Activation('softmax'))



  AdamOptimizer = optimizers.Adam(lr=learning_rate)



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



  model_trained = model.fit_generator(train_generator.flow(X_train, y_train, batch_size=batch_size, shuffle = True), steps_per_epoch=len(X_train) // batch_size, epochs=epochs, verbose=1, callbacks=callbacks, validation_data=valid_generator.flow(X_val, y_val, shuffle = True), validation_steps=len(X_val) // batch_size)



  return model_trained, model

La función regresa los valores del modelo entrenado(model_trained) y la arquitectura del modelo(model).

Este modelo tiene cuatro capas, 3 capas convolucionales con 64 kernels de tamaño 5 x 5 un stride de 1, no aplicamos padding, un pooling con un kernel de tamaño 2 x 2 y un stride de 1 que son los valores por defecto de MaxPooling2D, la cuarta capa es una capa normal Dense con 500 neuronas, la capa de salida tiene 3 neuronas, una para cada clase, en todas las capas ocultas usamos la función de activación relu y en la capa de salida la función de activación softmax. Usamos Adam como optimizador.

El metodo model.fit_generator es nuevo, este metodo permite usar el generador ImageDataGenerator para crear las imagenes con transformaciones en tiempo de ejecución, esto quiere decir que no necesitamos saturar la memoria ram con estas nuevas imagenes ya que se crean cuando se necesitan y despues se eliminan.

train_generator.flow(X_train, y_train, batch_size=batch_size, shuffle = True) necesitamos indicar que generador usaremos para crear las imagenes de entrenamiento

steps_per_epoch=len(X_train) // batch_size, este parametro es importante, un generador no sabe cuando tiene que parar y su ejecución es infinita, para evitar esto indicamos cuandos pasos tiene que dar cada epoch, el valor tiene que ser igual al numero de imagenes que tengamos entre el valor del batch_size, si tenemos 90 imagenes y el batch_size tiene un valor de 32 entonces el resultado de 90 // 32 es 2, daremos dos pasos cada epoch, aunque hay que tener cuidado con esto ya que como solo estamos dando dos pasos 32 * 2, usaremos 64 imagenes cada epoch y no las 90 que tenemos disponibles, al final como las imagenes que se usan son aleatorias terminaremos usando todas las imagenes a lo largo de los epochs pero lo recomendable sería tener un numero de imagenes como 96 para que la división de 96 // 32 sea 3 y de esta manera usar todas las imagenes cada epoch.

Para las imagenes de validación tenemos que indicar parametros parecidos y la lógica es la misma:

validation_data=valid_generator.flow(X_val, y_val, shuffle = True) validation_steps=len(X_val) // batch_size

El ultimo parametro importante del metodo model.fit_generator es callbacks, a este parametro le podemos pasar funciones que se ejecutaran cada vez que se realice un epoch.

Ahora necesitamos indicar los valores para los hiperparametros del modelo:


epochs = 250

batch_size = 32

learning_rate = 0.0003



callbacks = [ModelCheckpoint(filepath='weights.{epoch:02d}-val_acc:{val_acc:.2f}.h5', monitor='val_acc', save_best_only=True, verbose=1)]

Entrenaremos el modelo 250 epochs, con un batch_size de 32 ya que tenemos pocas imagenes y un learning_rate de 0.0003, el learning rate es uno de los hiperparametros más importantes de una red neuronal, un buen valor para este hiperparametro puede aumentar un 30% de exactitud del modelo, siempre es bueno empezar buscando valores para este parametro y despues cambiar otros hiperparametros como el numero de capas o kernels del modelo.

Usaremos una función de keras (ModelCheckpoint), esta función se pasa como parametro (callbacks) y lo que hace es guardar el valor de los pesos cada vez que la exactitud en los datos de validación del modelo mejora, con esto podemos entrenar un modelo en muchos epochs y automaticamente ir guardando los mejores pesos y sin preocuparnos por el estado final del modelo.

Para empezar el entrenamiento de la red neuronal y al final graficar los resultados usaremos estas líneas de código:


model_trained, model = create_model(X_train, X_val, y_train, y_val, learning_rate, epochs, batch_size, callbacks)



plot_loss_and_accuracy(model_trained)

validation_acc = model_trained.history['val_acc'][-1] * 100

training_acc = model_trained.history['acc'][-1] * 100

print("Validation accuracy: {}%\nTraining Accuracy: {}%".format(validation_acc, training_acc))

Con este modelo y los hiperparametros anteriores podemos obtener una exactitud del 88% en el set de entrenamiento y del 84% en el set de validación, este es un porcentaje muy bueno teniendo en cuenta que tenemos muy pocas imagenes para cada clase, este porcentaje lo obtuve en el epoch 184 y la función ModelCheckpoint guardo los pesos en los archivos.

Cargando los pesos guardados

Tenemos la opción de cargar un archivo de pesos al modelo, podemos crear un modelo nuevo sin entrenar para cargar los pesos o usar el modelo entrenado y sustituir los pesos, lo que haré es usar un nuevo modelo sin entrenar:


def create_empty_model(learning_rate):

  model = Sequential()



  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid",

        strides=1, input_shape=input_shape))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid", strides=1))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  model.add(Conv2D(64, 

        kernel_size=(kernel_size, kernel_size), padding="valid",

        strides=1,))

  model.add(Activation('relu'))

  model.add(MaxPooling2D())



  model.add(Flatten())



  model.add(Dense(500))

  model.add(Activation('relu'))



  model.add(Dense(3))

  model.add(Activation('softmax'))



  AdamOptimizer = optimizers.Adam(lr=learning_rate)



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



  return model

Con esta función creamos un nuevo modelo, esta vez solo nos importa la arquitectura y por esta razon no ejecutamos el entrenamiento model.fit_generator


best_model = create_empty_model(learning_rate)



best_model.load_weights("./weights.184-val_acc_0.84.h5")

con load_weights cargamos los pesos al modelo, tenemos que indicar la ruta donde se guardaron los pesos, en mi caso el archivo se llama weights.184-val_acc_0.84.h5, puede variar el nombre dependiendo de la exactitud y de el numero de epoch.

Probando nuevas imagenes

Para probar la red neuronal, usaremos tres nuevas imagenes una para cada clase:


wget https://static.urbantecno.com/2018/08/Tesla-Model-3-4-720x550.jpg



wget https://www.autonavigator.hu/wp-content/uploads/2014/01/109102_source-2.jpg



wget https://upload.wikimedia.org/wikipedia/commons/9/92/2017_Tesla_Model_X_100D_Front.jpg

Ahora tenemos que cargarlas:


X_test = []



image = load_img("./Tesla-Model-3-4-720x550.jpg", target_size=(img_height, img_width))

image_array = img_to_array(image)

X_test.append(image_array)



image = load_img("./109102_source-2.jpg", target_size=(img_height, img_width))

image_array = img_to_array(image)

X_test.append(image_array)



image = load_img("./2017_Tesla_Model_X_100D_Front.jpg", target_size=(img_height, img_width))

image_array = img_to_array(image)

X_test.append(image_array)



X_test = np.array(X_test)

Las reescalamos


X_test = X_test.astype('float32') / 255

Y las pasamos a la red neuronal:


y_pred = best_model.predict(X_test, batch_size=None, verbose=1, steps=None)

Sabemos que la primer imagen pertenece a la primera clase, la segunda imagen a la segunda clase y la tercera imagen a la tercera clase:


y_true = [[1, 0, 0], [0, 1, 0], [0, 0, 1]]

los valores de y_true y de y_pred tienen que ser iguales:


np.argmax(y_true, axis=1), np.argmax(y_pred, axis=1)


(array([0, 1, 2]), array([1, 1, 2]))

como podemos notar la red neuronal clasificó correctamente dos imagenes y solo se equivoco en la primera, es un resultado mucho mejor que el que obtuvimos en la primera red neuronal, hay que tener en mente que las imagenes disponibles no son las mejores y son muy pocas, también que los modelos de coches se parecen mucho entre si, la red neuronal confundió el modelo 3 con el modelo s, ambos modelos son muy parecidos.

Ver resultados

Si cargamos las imagenes para comparar las clases que predijo la red neuronal con las clases reales:


img_height = 256

img_width = 256



def load_complete_images(paths):

  X = []

  y = []



  for path in paths:

    images_paths = os.listdir(path)



    for image_path in images_paths:

      complete_path = path + image_path

      image = load_img(complete_path, target_size=(img_height, img_width))

      X.append(image)

      label = paths.index(path)

      y.append(label)



  return X, y

Primero necesitamos una función para cargar las imagenes, esta vez no las convertimos a array ya que necesitamos que las imagenes se vean con su color real.


X_complete, y_complete = load_complete_images(training_paths)

X_val_complete, y_val_complete = load_complete_images(validation_paths)

Como no usaremos los generadores tenemos que reescalar las imagenes:


X_train = X_train.astype('float32') / 255

X_val = X_val.astype('float32') / 255

Guardamos los valores que el modelo predijo:


y_train_pred = best_model.predict(X_train, batch_size=None, verbose=1, steps=None)

y_val_pred = best_model.predict(X_val, batch_size=None, verbose=1, steps=None)

estos arrays tienen 3 valores, la probabilidad que le asignó el modelo a cada clase, solo queremos la clase que tiene la probabilidad más alta:


y_train_pred_max = np.argmax(y_train_pred, axis=1)

y_val_pred_max = np.argmax(y_val_pred, axis=1)

las clases reales (y_train) las convertimos a un formato compatible con keras:


[[0, 0 , 1], [0, 0 , 1], [0, 0 , 1], [1, 0 , 0], [1, 0 , 0]]

ahora necesitamos el formato normal:


[2, 2, 2, 0, 0]


y_train_true = np.argmax(y_train, axis=1)

y_val_true = np.argmax(y_val, axis=1)

Con la siguiente función imprimimos las imagenes y un texto indicando su clase real y la que predijo el modelo:


classes_array = ['3', 's', 'x']



def plot_images(images, cls_true, cls_pred):    

    fig, axes = plt.subplots(8, 8)

    fig.subplots_adjust(hspace=0.5, wspace=0.5)

    plt.rcParams["figure.figsize"] = (20, 20)



    for i, ax in enumerate(axes.flat):

        ax.imshow(images[i])



        true_class = classes_array[int(cls_true[i])]

        pred_class = classes_array[int(cls_pred[i])]



        xlabel = "True: {0}, Pred: {1}".format(true_class, pred_class)



        ax.set_xlabel(xlabel)



        ax.set_xticks([])

        ax.set_yticks([])



    plt.show()

Obtenemos las primeras 64 imagenes que pertenecen a la primera clase (tesla model 3)


images = X_complete[0:64]

cls_true = y_train_true[0:64]

cls_pred = y_train_pred_max[0:64]

plot_images(images, cls_true, cls_pred)

y el resultado es el siguiente:

tesla model 3 pred

En algunas imagenes el modelo se equivocó y asigno la clase 1 y 2 (model s, model x), parece que en las imagenes donde el color del coche es negro el modelo piensa que es un model x, tal vez el problema se pueda resolver con batch normalization.

Si queremos imprimir las imagenes de la segunda clase:


images = X_complete[150:214]

cls_true = y_train_true[150:214]

cls_pred = y_train_pred_max[150:214]

plot_images(images, cls_true, cls_pred)

tesla model s pred

En esta clase el modelo asignó correctamente la mayoría de las clases, aunque podemos notar que hay una que asignó mal, penso que era un model 3 en lugar de un model s.

En la libreta se encuentra la imagen de la tercera clase.

Por ultimo veamos cómo imprimir los kernels de las capas convolucionales:


total_kernel = 64

conv1_kernels = best_model.layers[0].get_weights()[0] # 0 para obtener weights, 1 para obtener bias

plt.rcParams["figure.figsize"] = (15, 15)



for i in range(total_kernel):

  plt.subplot(8, 8, i + 1)

  plt.imshow(conv1_kernels[:, :, 0, i], cmap='BrBG')

  plt.axis('off')

Estos son los 64 kernels de la primera capa convolucional:

kernels

cada kerel tiene un tamaño 5 x 5.

Es interesante ver cuales son los patrones que la red neuronal guarda, para nosotros no tienen mucho sentido pero es la parte más importante de las redes neuronales.