Abrir en Google Colab
|
Descargar notebook
|
Modelo de clasificación de imágenes multiple con transferencia de aprendizaje
PRECAUCIÓN 😱: El tema presentado en esta sección está clasificado como avanzado. El entendimiento de este contenido es totalmente opcional.
Preparación del ambiente
Intalamos las librerias necesarias
[ ]:
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/docs/vision/tasks/classification/code/cnn_mclass_transfer_learning.txt \
--quiet --no-clobber
!pip install -r cnn_mclass_transfer_learning.txt --quiet
Descargamos el conjunto de datos
Nota: Note que estamos descargando el conjunto de datos completo de imágenes en este ejemplo. Si quiere utilizar un conjunto de datos más chico, puede cambiar donde dice
imdb-movie-gull.zipporimdb-movie.zip.
[ ]:
!wget https://santiagxf.blob.core.windows.net/public/imdb-movie-genre-full.zip \
--quiet --no-clobber --directory-prefix ./Datasets/
!unzip -o -qq ./Datasets/imdb-movie-genre-full.zip -d Datasets
Verifiquemos el tamaño del conjunto de datos
[ ]:
!ls Datasets/SampleMoviePosters | wc -l
36337
En caso de que quiera descargar todo el conjunto de datos de imágenes puede utilizar el siguiente codigo para descargar las imagenes dadas sus URLs (aproximadamente 40K imagenes). Su uso es:
donwload_all(data)
[ ]:
import urllib
def download_file(download_url, filename):
try:
response = urllib.request.urlopen(download_url)
except urllib.error.HTTPError:
return False
file = open(filename, 'wb')
file.write(response.read())
file.close()
return True
def donwload_all(data):
data['Downloaded'] = False
from tqdm import tqdm
for index, row in tqdm(data.iterrows()):
data['Downloaded'][index] = download_file(row['Poster'], row['Image'])
Sobre el conjunto de datos de este ejemplo
Este conjunto de datos proviene de Kaggle y contiene carteles (posters) de películas del sitio web de IMDB. Este conjunto de datos contiene la siguiente información para cada película:
ID de IMDB
enlace de IMDB
título
puntuación de IMDB
género
un enlace para descargar el póster de la película
En este conjunto de datos, cada póster de película puede pertenecer al menos a un género y puede tener como máximo 3 etiquetas asignadas. El número total de carteles ronda los 40K.
[ ]:
import pandas as pd
data = pd.read_csv("Datasets/MovieGenre.csv", encoding = "ISO-8859-1").dropna()
data['Image'] = "Datasets/SampleMoviePosters/" + data["imdbId"].astype(str) + ".jpg"
data['Genre'] = data['Genre'].apply(lambda x: x.split("|"))
[ ]:
data.head(5)
| Unnamed: 0 | imdbId | Imdb Link | Title | IMDB Score | Genre | Poster | Downloaded | Image | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 0 | 114709 | http://www.imdb.com/title/tt114709 | Toy Story (1995) | 8.3 | [Animation, Adventure, Comedy] | https://images-na.ssl-images-amazon.com/images... | True | Datasets/SampleMoviePosters/114709.jpg |
| 1 | 1 | 113497 | http://www.imdb.com/title/tt113497 | Jumanji (1995) | 6.9 | [Action, Adventure, Family] | https://images-na.ssl-images-amazon.com/images... | True | Datasets/SampleMoviePosters/113497.jpg |
| 2 | 2 | 113228 | http://www.imdb.com/title/tt113228 | Grumpier Old Men (1995) | 6.6 | [Comedy, Romance] | https://images-na.ssl-images-amazon.com/images... | True | Datasets/SampleMoviePosters/113228.jpg |
| 3 | 3 | 114885 | http://www.imdb.com/title/tt114885 | Waiting to Exhale (1995) | 5.7 | [Comedy, Drama, Romance] | https://images-na.ssl-images-amazon.com/images... | True | Datasets/SampleMoviePosters/114885.jpg |
| 4 | 4 | 113041 | http://www.imdb.com/title/tt113041 | Father of the Bride Part II (1995) | 5.9 | [Comedy, Family, Romance] | https://images-na.ssl-images-amazon.com/images... | True | Datasets/SampleMoviePosters/113041.jpg |
Filtraremos el conjunto de datos solo por aquellas imágenes que están disponible. La columna «Downloaded» indica esta información.
[ ]:
images = data[data['Downloaded'] == True]["Image"].to_numpy()
labels = data[data['Downloaded'] == True]["Genre"].to_numpy()
Preparación de los datos
Primero debemos generar las etiquetas que se utilizarán para el modelo de clasificación. Dado que este es un problema de clasificación multiple, utilizaremos MultiLabelBinarizer:
[ ]:
from sklearn.preprocessing import MultiLabelBinarizer
label_encoder = MultiLabelBinarizer()
labels = label_encoder.fit_transform(labels)
[ ]:
N_LABELS = len(label_encoder.classes_)
Dividimos el conjunto de datos en entrenamiento y testing:
[ ]:
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(images, labels, test_size=0.3)
Configuramos algunos parámetros de las imágenes
[ ]:
IMG_SIZE = 224 # Tamaño en pixeles de las imágenes
CHANNELS = 3 # Numero de canales. RGB = 3
BATCH_SIZE = 128 # Tamaño del lote para entrenamiento
SHUFFLE_BUFFER_SIZE = 1024 # Mezclaremos el conjunto de datos en lotes de 1024 imágenes
Definimos una función para leer las imágenes de los directorios:
[ ]:
import tensorflow as tf
from tensorflow.data.experimental import AUTOTUNE
def parse_image(filename, labels, CHANNELS:int=3, IMG_SIZE:int=224):
image_string = tf.io.read_file(filename)
image_decoded = tf.image.decode_jpeg(image_string, channels=CHANNELS)
image_resized = tf.image.resize(image_decoded, [IMG_SIZE, IMG_SIZE])
image_normalized = image_resized / 255.0
return image_normalized, labels
Definimos una función para generar el conjunto de datos:
[ ]:
def create_dataset(filenames, labels, is_training=True, cache=False):
dataset = tf.data.Dataset.from_tensor_slices((filenames, labels))
dataset = dataset.map(parse_image, num_parallel_calls=AUTOTUNE)
if is_training == True:
if cache:
# Utilice caching solo si el dataset es pequeño y cabe en memoria
dataset = dataset.cache()
dataset = dataset.shuffle(buffer_size=SHUFFLE_BUFFER_SIZE)
dataset = dataset.batch(BATCH_SIZE)
dataset = dataset.prefetch(buffer_size=AUTOTUNE)
return dataset
Instanciamos nuestros conjuntos de datos de entrenamiento y testing:
[ ]:
train_ds = create_dataset(X_train, y_train, is_training=True)
test_ds = create_dataset(X_test, y_test, is_training=False)
[ ]:
dataset = train_ds.take(1)
for x, y in dataset.as_numpy_iterator():
print(x.shape)
(128, 224, 224, 3)
Utilizando transferencia de aprendizaje en nuestro modelo
¿Qué es TensorFlow Hub?
Un concepto que es esencial en el desarrollo de software es la idea de reutilizar el código que está disponible a través de librerías. Las librerías hacen que el desarrollo sea más rápido y generan más eficiencia. Para los cientificos de datos que trabajan en visión artificial, entrenar arquitecturas de redes neuronales complejas desde cero resulta altamente complejo. TensorFlow Hub es una biblioteca que permite publicar y reutilizar componentes de aprendizaje automático prediseñados. Con TF.Hub, resulta sencillo volver a entrenar la capa superior de un modelo previamente entrenado para reconocer las clases en un nuevo conjunto de datos. TensorFlow Hub también distribuye modelos sin la capa de clasificación. Estos se pueden utilizar para realizar fácilmente el aprendizaje por transferencia. Puede descargar cualquier modelo para imágenes compatible con Tensorflow 2 de tfhub.dev. La única condición es asegurarse de que la forma de las características de la imagen en nuestro conjunto de datos preparado coincida con la forma de entrada esperada del modelo que desea reutilizar.
Utilizando un modelo para transferencia de aprendizaje
Usaremos una instancia previamente entrenada de MobileNetV2 y un tamaño de entrada de 224x224. MobileNetV2 es en realidad una gran familia de arquitecturas de redes neuronales que se diseñaron principalmente para acelerar la inferencia en el dispositivo celular. Vienen en diferentes tamaños según el multiplicador de profundidad (número de características en capas convolucionales ocultas) y el tamaño de las imágenes de entrada.
[ ]:
import tensorflow_hub as hub
feature_extractor_url = "https://tfhub.dev/google/imagenet/mobilenet_v2_100_224/feature_vector/4"
feature_extractor_layer = hub.KerasLayer(feature_extractor_url,
input_shape=(IMG_SIZE,IMG_SIZE,CHANNELS))
El extractor que estamos usando aquí acepta imágenes de forma (224, 224, 3) y devuelve un vector de 1280 de longitud para cada imagen. Para evitar que TensorFlow intente ajustar los pesos de esta parte del modelo, debemos congelar los pesos en la capa del extractor, de modo que el entrenamiento solo modifique las capas de clasificación. Por lo general, esto es una buena práctica cuando se trabaja con conjuntos de datos que son muy pequeños en comparación con el conjunto de datos original en el que se entrenó el extractor de predictor. Solo se recomienda entrenarla si el conjunto de datos de entrenamiento es grande y muy similar al conjunto de datos de ImageNet original.
[ ]:
feature_extractor_layer.trainable = False
Generamos el modelo agregando un clasificador:
[ ]:
import tensorflow.keras as keras
import tensorflow_addons as tfa
model = keras.Sequential([
feature_extractor_layer,
keras.layers.Dense(1024, activation='relu', name='hidden_layer'),
keras.layers.Dense(N_LABELS, activation='sigmoid', name='output')
])
[ ]:
model.compile(
optimizer=tf.keras.optimizers.Adam(learning_rate=1e-5),
loss='binary_crossentropy',
metrics=[tf.keras.metrics.categorical_accuracy])
Entrenamos el modelo:
[ ]:
EPOCHS = 10
[ ]:
history = model.fit(train_ds, epochs=EPOCHS, validation_data=test_ds)
Epoch 1/10
202/202 [==============================] - 85s 406ms/step - loss: 0.3871 - categorical_accuracy: 0.1850 - val_loss: 0.2406 - val_categorical_accuracy: 0.2630
Epoch 2/10
202/202 [==============================] - 83s 402ms/step - loss: 0.2259 - categorical_accuracy: 0.2778 - val_loss: 0.2206 - val_categorical_accuracy: 0.2837
Epoch 3/10
202/202 [==============================] - 83s 403ms/step - loss: 0.2142 - categorical_accuracy: 0.3003 - val_loss: 0.2133 - val_categorical_accuracy: 0.3045
Epoch 4/10
202/202 [==============================] - 83s 400ms/step - loss: 0.2080 - categorical_accuracy: 0.3169 - val_loss: 0.2085 - val_categorical_accuracy: 0.3197
Epoch 5/10
202/202 [==============================] - 82s 399ms/step - loss: 0.2035 - categorical_accuracy: 0.3300 - val_loss: 0.2050 - val_categorical_accuracy: 0.3290
Epoch 6/10
202/202 [==============================] - 82s 397ms/step - loss: 0.2002 - categorical_accuracy: 0.3394 - val_loss: 0.2025 - val_categorical_accuracy: 0.3365
Epoch 7/10
202/202 [==============================] - 82s 399ms/step - loss: 0.1977 - categorical_accuracy: 0.3457 - val_loss: 0.2006 - val_categorical_accuracy: 0.3439
Epoch 8/10
202/202 [==============================] - 82s 397ms/step - loss: 0.1956 - categorical_accuracy: 0.3505 - val_loss: 0.1991 - val_categorical_accuracy: 0.3429
Epoch 9/10
202/202 [==============================] - 83s 399ms/step - loss: 0.1940 - categorical_accuracy: 0.3553 - val_loss: 0.1979 - val_categorical_accuracy: 0.3459
Epoch 10/10
202/202 [==============================] - 83s 401ms/step - loss: 0.1925 - categorical_accuracy: 0.3581 - val_loss: 0.1970 - val_categorical_accuracy: 0.3526
Generamos las predicciones para evaluar el modelo
[ ]:
import numpy as np
y_prob = model.predict(test_ds)
y_pred = np.round(y_prob)
Generamos una matriz de confusión para todas las etiquetas
[ ]:
from sklearn.metrics import multilabel_confusion_matrix
cm = multilabel_confusion_matrix(y_test, y_pred)
Verificando los resultados
Dado que se trata de un problema de clasificación multiple, utilizaremos una matriz de confusión multiple para verificar la performance de nuestro modelo. La siguiente función intentará graficar esta información para poder verificarla más facilmente:
[ ]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
def print_confusion_matrix(confusion_matrix, axes, class_label, class_names, fontsize=14):
df_cm = pd.DataFrame(
confusion_matrix, index=class_names, columns=class_names,
)
try:
heatmap = sns.heatmap(df_cm, annot=True, fmt="d", cbar=False, ax=axes)
except ValueError:
raise ValueError("Confusion matrix values must be integers.")
heatmap.yaxis.set_ticklabels(heatmap.yaxis.get_ticklabels(), rotation=0, ha='right', fontsize=fontsize)
heatmap.xaxis.set_ticklabels(heatmap.xaxis.get_ticklabels(), rotation=45, ha='right', fontsize=fontsize)
axes.set_ylabel('True label')
axes.set_xlabel('Predicted label')
axes.set_title(class_label)
fig, ax = plt.subplots(4, 4, figsize=(12, 7))
for axes, cfs_matrix, label in zip(ax.flatten(), cm, label_encoder.classes_):
print_confusion_matrix(cfs_matrix, axes, label, ["N", "Y"])
fig.tight_layout()
plt.show()
Persisitendo el modelo
Guardamos el modelo en caso de que lo necesitemos más tarde:
[ ]:
from datetime import datetime
t = datetime.now().strftime("%Y%m%d_%H%M%S")
export_path = "./Models/imbd-mclass_{}.h5".format(t)
model.save(export_path)
print("Modelo almacenado en: '{}'".format(export_path))
Modelo almacenado en: './Models/imbd-mclass_20211011_051505.h5'
Luego podemos cargarlo nuevamente como sigue:
[ ]:
reloaded = tf.keras.models.load_model(export_path, custom_objects={'KerasLayer':hub.KerasLayer})
Abrir en Google Colab
Descargar notebook