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:
Aplicando Data Augmentation obtendremos varias imagenes con modificaciones:
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:
-
rotation_range: Indica el numero maximo de grados que la imagen se puede inclinar.
-
width shift range, height shift range: cambia de orientación los pixeles de algunas partes de la imagen.
-
rescale: Sirve para reescalar la imagen de 0-255 a 0-1
-
shear_range: Modifica algunas partes de la imagen modificando la orientación.
-
zoom_range: Aplica un acercamiento a la imagen.
-
horizontal_flip: Cambia la orientación de la imagen.
-
fill_mode: Cuando a la imagen se le aplica una rotación cambia su aspecto, para mantener el mismo aspecto se tienen que rellenar los pixeles faltantes, con la opción nearest los pixeles cercanos se repiten para rellenar las areas faltantes.
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:
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)
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:
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.