Abrir en Google Colab
|
Descargar notebook
|
Modelo de clasificación de videos con redes recurrentes
PRECAUCIÓN 😱: El tema presentado en esta sección está clasificado como avanzado. El entendimiento de este contenido es totalmente opcional.
Introducción
La clasificación de videos es la tarea por la cual un modelo de aprendizaje automático asigna una o varias etiquetas a todo un video dependiendo del contenido del mismo. Esta tarea nos permite reconocer acciones o estados que son transmitidos en un video. Para ejemplificar esta tarea, construiremos una red recurrente basada en LSTM donde los valores de entrada de la misma corresponderán a vectores que se aprendieron con una red de convolución.
Importante: Es posible que no pueda ejecutar este notebook en Google Colab. No tendrá suficiente memoria en las tarjetas de GPU que suelen disponibilizarce. Si dispone de un equipo con GPU compatible con CUDA, ejecutelo allí.
Preparación del ambiente
Intalamos las librerias necesarias
[2]:
!wget https://raw.githubusercontent.com/santiagxf/M72109/master/docs/vision/tasks/sequences/code/lstm_cnn_class.txt \
--quiet --no-clobber
!pip install -r lstm_cnn_class.txt --quiet
ERROR: responsibleai 0.10.0 has requirement dice-ml<0.8,>=0.7.1, but you'll have dice-ml 0.6.1 which is incompatible.
ERROR: raiwidgets 0.10.0 has requirement jinja2==2.11.3, but you'll have jinja2 2.11.2 which is incompatible.
ERROR: azureml-responsibleai 1.34.0 has requirement responsibleai==0.9.4, but you'll have responsibleai 0.10.0 which is incompatible.
ERROR: autokeras 1.0.16 has requirement tensorflow<=2.5.0,>=2.3.0, but you'll have tensorflow 2.1.0 which is incompatible.
ERROR: tensorflow-metadata 1.2.0 has requirement absl-py<0.13,>=0.9, but you'll have absl-py 0.13.0 which is incompatible.
Para ejemplificar esta técnica utilizaremos un conjunto de datos muy popular llamado UCF-101. UCF-101 es un conjunto de datos con videos reales extraidos de YouTube clasificados en 101 categorías dendiendo de la acción que muestran. Este conjunto de datos tiene originalmente 13320 videos de las 101 categorias disponibles. Estos videos, adicionalmente estan agrupados en subgrupos donde los videos muestran propiedades similares como por ejemplo fondos similares, angulo de la cámara, etc.
Puede obtener más información sobre este conjunto de datos y sus derechos de autor en: UCF101 - Action Recognition Data Set
Para simplificar el uso de este conjunto de datos, utilizaremos una versión reducida del mismo con solo 3 categorias:
Tocando la guitarra
Tocando el violin
Tocando el chelo
Dataset/
├─ PlayingGuitar/
│ ├─ video1.avi
│ ├─ video2.avi
│ └─ ...
└─ PlayingCello/
│ ├─ video1.avi
│ ├─ video2.avi
│ └─ ...
└─ PlayingViolin/
├─ video1.avi
├─ video2.avi
└─ ...
Descargamos el conjunto de datos:
[3]:
!wget https://santiagxf.blob.core.windows.net/public/datasets/UCF3.zip \
--quiet --no-clobber
!mkdir -p /tmp/videos
!unzip -qq UCF3.zip -d /tmp/videos
Antes de comenzar necesitaremos verificar que tenemos el runtime correcto en nuestro ambiente. Esta tarea se beneficiará mucho de una GPU.
[1]:
import tensorflow as tf
print("GPUs disponibles: ", len(tf.config.experimental.list_physical_devices('GPU')))
GPUs disponibles: 4
Trabajando con video
Generando frames de un video
Como se mencionó, un video puede ser defragmentado en una secuencia de cuadros que se transicionan en el tiempo. Sin embargo, descomponer un video de tal forma puede dar lugar a estructuras de datos másivas. Considere un video de 30 segundos, a 24 cuadros tendriamos secuencias de 720 pasos. En general deberemos utilizar alguna estrategia de sampling para seleccionar los cuadros.
El siguiente ejemplo toma como entrada un directorio donde se encuentran videos, para generar otro directorio donde cada video es una carpeta. En tal carpeta se encuentran todos los cuadros de tal video. Los cuadros son seleccionado equitativamente en el tiempo para obtener la cantidad de secuencias necesarias.
Dataset/
├─ vide1/
│ ├─ video1_frame_00.jpg
│ ├─ video1_frame_01.jpg
│ ├─ video1_frame_02.jpg
│ ├─ video1_frame_03.jpg
│ └─ ...
└─ video2/
├─ video2_frame_00.jpg
├─ video2_frame_01.jpg
├─ video2_frame_02.jpg
├─ video2_frame_03.jpg
└─ ...
[2]:
import cv2
import os
from typing import Optional
from tqdm import tqdm
import pathlib
def extract_frames(videos_dir: str, out_dir: str, sample_each_seconds: Optional[int], max_sequence_lenght: Optional[int]):
for file_ in tqdm(pathlib.PosixPath(videos_dir).glob("*/*.avi")):
count = 0
basename = file_.name.split('.')[0]
label = file_.parent.name
targetpath = os.path.join(out_dir, label, basename)
if os.path.isdir(targetpath):
continue
os.makedirs(targetpath, exist_ok=True)
vidcap = cv2.VideoCapture(str(file_))
if (sample_each_seconds):
sample_every_frame = 1000 * sample_each_seconds
else:
num_frames = int(vidcap.get(cv2.CAP_PROP_FRAME_COUNT))
sample_every_frame = max(1, num_frames // max_sequence_lenght)
success, image = vidcap.read()
while success:
cv2.imwrite(os.path.join(targetpath, "%s_frame_%d.jpg" % (basename, count)), image)
count += 1
vidcap.set(cv2.CAP_PROP_POS_MSEC,(count*sample_every_frame))
success,image = vidcap.read()
vidcap.release()
Ejecutamos este procedimiento en el directorio donde se encuentra nuestro conjnto de datos:
[3]:
SEQUENCE_LENGTH=10
IMG_SIZE = 224
CHANNELS = 3
[4]:
VIDEOS_PATH = '/tmp/videos'
FRAMES_PATH = '/tmp/frames'
[8]:
extract_frames(VIDEOS_PATH, FRAMES_PATH, sample_each_seconds=1, max_sequence_lenght=SEQUENCE_LENGTH)
424it [00:35, 11.90it/s]
Labels:
[5]:
labels = [folder.name for folder in pathlib.PosixPath(FRAMES_PATH).glob('*/')]
[6]:
NUM_LABELS = len(labels)
print(NUM_LABELS)
3
Construyendo un modelo basado en CNN y LSTM
Utilizaremos TensorFlow para construir un modelo que pueda extraer los predictores desde las imágenes de forma independiente para luego unir todos estos predictores en una secuencia que sera utilizada como entrada para una red recurrente.
Nuestra red CNN estará basada en una CNNs típica. En este caso la misma constará de:
2 capas de CNN
1 capas de Pooling
1 capa de regularización
Esta unidad básica la repetiremos 4 veces sobre cada imagen de la secuencia. Para realizar esta operación utilizaremos la capa TimeDistributed que permite realizar una misma operación de forma distribuida sobre todos los elementos de una secuencia.
[7]:
import tensorflow as tf
import tensorflow.keras as keras
[11]:
def build_cnn():
model = keras.models.Sequential([
keras.layers.InputLayer(input_shape=(IMG_SIZE, IMG_SIZE, CHANNELS)),
keras.layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
keras.layers.Conv2D(32, (3, 3), padding='same', activation='relu'),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
keras.layers.Conv2D(64, (3, 3), padding='same', activation='relu'),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Conv2D(128, (4, 4), padding='same', activation='relu'),
keras.layers.Conv2D(128, (3, 3), padding='same', activation='relu'),
keras.layers.MaxPooling2D((2, 2)),
keras.layers.Flatten(),
])
return model
def build_lstm(feature_extractor):
model = keras.models.Sequential([
keras.layers.InputLayer(input_shape=(SEQUENCE_LENGTH, IMG_SIZE, IMG_SIZE, CHANNELS)),
keras.layers.TimeDistributed(feature_extractor),
keras.layers.Masking(mask_value=0.),
keras.layers.LSTM(128, dropout=0.5, recurrent_dropout=0.5),
keras.layers.Dense(32, activation='relu'),
keras.layers.Dropout(0.3),
keras.layers.Dense(NUM_LABELS, activation='softmax')
])
model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'])
return model
Contruimos el modelo:
[12]:
cnn_model = build_cnn()
cnn_model.summary()
Model: "sequential_2"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
conv2d_8 (Conv2D) (None, 224, 224, 32) 896
_________________________________________________________________
conv2d_9 (Conv2D) (None, 224, 224, 32) 9248
_________________________________________________________________
max_pooling2d_4 (MaxPooling2 (None, 112, 112, 32) 0
_________________________________________________________________
conv2d_10 (Conv2D) (None, 112, 112, 64) 18496
_________________________________________________________________
conv2d_11 (Conv2D) (None, 112, 112, 64) 36928
_________________________________________________________________
max_pooling2d_5 (MaxPooling2 (None, 56, 56, 64) 0
_________________________________________________________________
conv2d_12 (Conv2D) (None, 56, 56, 128) 73856
_________________________________________________________________
conv2d_13 (Conv2D) (None, 56, 56, 128) 147584
_________________________________________________________________
max_pooling2d_6 (MaxPooling2 (None, 28, 28, 128) 0
_________________________________________________________________
conv2d_14 (Conv2D) (None, 28, 28, 128) 262272
_________________________________________________________________
conv2d_15 (Conv2D) (None, 28, 28, 128) 147584
_________________________________________________________________
max_pooling2d_7 (MaxPooling2 (None, 14, 14, 128) 0
_________________________________________________________________
flatten_1 (Flatten) (None, 25088) 0
=================================================================
Total params: 696,864
Trainable params: 696,864
Non-trainable params: 0
_________________________________________________________________
[13]:
model = build_lstm(cnn_model)
model.summary()
WARNING:tensorflow:Layer lstm_1 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.
Model: "sequential_3"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
time_distributed_1 (TimeDist (None, 10, 25088) 696864
_________________________________________________________________
masking_1 (Masking) (None, 10, 25088) 0
_________________________________________________________________
lstm_1 (LSTM) (None, 128) 12911104
_________________________________________________________________
dense_2 (Dense) (None, 32) 4128
_________________________________________________________________
dropout_1 (Dropout) (None, 32) 0
_________________________________________________________________
dense_3 (Dense) (None, 3) 99
=================================================================
Total params: 13,612,195
Trainable params: 13,612,195
Non-trainable params: 0
_________________________________________________________________
Generando un conjunto de datos que funcione con el modelo
Generaremos una función que tome un directorio donde se encuentran las imágenes y retorne tensores con los valores de los pixeles junto con su respectiva etiqueta. Recuerde como estan almacenados nuestros datos:
Dataset/
├─ clase1/
│ ├─ vide1/
│ │ ├─ video1_frame_00.jpg
│ │ ├─ video1_frame_01.jpg
│ │ ├─ video1_frame_02.jpg
│ │ ├─ video1_frame_03.jpg
│ │ └─ ...
│ └─ video2/
│ ├─ video2_frame_00.jpg
│ ├─ video2_frame_01.jpg
│ ├─ video2_frame_02.jpg
│ ├─ video2_frame_03.jpg
│ └─ ...
├─ clase2/
│ ├─ vide1/
│ │ ├─ video1_frame_00.jpg
│ │ ├─ video1_frame_01.jpg
│ │ ├─ video1_frame_02.jpg
│ │ ├─ video1_frame_03.jpg
│ │ └─ ...
Etiquetas
Veamos cuales son las etiquetas disponibles:
[14]:
import pathlib
from sklearn import preprocessing
label_encoder = preprocessing.LabelEncoder()
label_encoder.fit(labels)
[14]:
LabelEncoder()
Consultemos sus valores:
[15]:
label_encoder.classes_
[15]:
array(['PlayingCello', 'PlayingGuitar', 'PlayingViolin'], dtype='<U13')
Generator
[16]:
import numpy as np
import pathlib
def parse_image(filename, channels:int, img_size:int):
image_string = tf.io.read_file(str(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
def parse_video(video_folder, sequence_length: int, channels: int, img_size: int):
images_path = video_folder.glob("*.jpg")
padded_sequence = np.zeros((sequence_length, img_size, img_size, channels))
for idx, img in enumerate(images_path):
if idx >= sequence_length:
break
padded_sequence[idx] = parse_image(img, channels, img_size)
return padded_sequence
def generate_sequences():
for video_folder in pathlib.PosixPath(FRAMES_PATH).glob('*/*/'):
label = video_folder.parent.name
yield (parse_video(video_folder, SEQUENCE_LENGTH, CHANNELS, IMG_SIZE), label_encoder.transform([str(label)])[0])
Construimos un objeto tf.data.Dataset:
[17]:
import tensorflow as tf
dataset = tf.data.Dataset.from_generator(generate_sequences,
output_signature=(
tf.TensorSpec(shape=(SEQUENCE_LENGTH, IMG_SIZE, IMG_SIZE, CHANNELS), dtype=tf.float32),
tf.TensorSpec(shape=(), dtype=tf.int16))
).shuffle(400).batch(16)
Probemos el generador para revisar si funciona correctamente:
[18]:
list(dataset.take(1))[0][0].shape
[18]:
TensorShape([16, 10, 224, 224, 3])
Entrenando el modelo
[19]:
history = model.fit(dataset, epochs=10)
Epoch 1/10
27/27 [==============================] - 75s 2s/step - loss: 1.1001 - accuracy: 0.3561
Epoch 2/10
27/27 [==============================] - 48s 1s/step - loss: 1.0946 - accuracy: 0.3514
Epoch 3/10
27/27 [==============================] - 48s 1s/step - loss: 0.8938 - accuracy: 0.5448
Epoch 4/10
27/27 [==============================] - 48s 1s/step - loss: 0.7407 - accuracy: 0.6651
Epoch 5/10
27/27 [==============================] - 48s 1s/step - loss: 0.7253 - accuracy: 0.6533
Epoch 6/10
27/27 [==============================] - 48s 1s/step - loss: 0.9794 - accuracy: 0.4717
Epoch 7/10
27/27 [==============================] - 48s 1s/step - loss: 0.6776 - accuracy: 0.6698
Epoch 8/10
27/27 [==============================] - 48s 1s/step - loss: 0.5862 - accuracy: 0.7052
Epoch 9/10
27/27 [==============================] - 48s 1s/step - loss: 0.5810 - accuracy: 0.7264
Epoch 10/10
27/27 [==============================] - 48s 1s/step - loss: 0.6770 - accuracy: 0.7217
Preguntas:
¿Que le parecen estos resulados?
¿Como podría aplicar transferencia de aprendizaje en este ejemplo?
Utilizando una capa más potente como extractor de predictores
Podemos combinar esta técnica con transferencia de aprendizaje. Para realizar esto podemos utilizar TensorFlow Hub.
Nota: esta forma de implementación resulta ineficiente computacionalmente, aunque sencilla de interpretar.
[25]:
EXTRACTOR_SIZE = 1280
[26]:
import tensorflow_hub as tfhub
def build_cnn_tfhub():
extractor = tfhub.KerasLayer("https://tfhub.dev/google/imagenet/mobilenet_v2_100_224/feature_vector/4",
input_shape=(IMG_SIZE, IMG_SIZE, CHANNELS),
output_shape=(EXTRACTOR_SIZE),
trainable=False)
return keras.layers.Lambda(lambda x: extractor(x))
Instanciamos el extractor de predictores:
[27]:
feature_extractor = build_cnn_tfhub()
Lo insertamos en nuestra red:
[28]:
def build_lstm(feature_extractor):
model = keras.models.Sequential([
keras.layers.InputLayer(input_shape=(SEQUENCE_LENGTH, IMG_SIZE, IMG_SIZE, CHANNELS)),
keras.layers.TimeDistributed(feature_extractor),
keras.layers.Masking(mask_value=0.),
keras.layers.LSTM(512, dropout=0.5, recurrent_dropout=0.5),
keras.layers.Dense(128, activation='relu'),
keras.layers.Dropout(0.3),
keras.layers.Dense(3, activation='softmax')
])
model.compile(optimizer='adam', loss=tf.keras.losses.SparseCategoricalCrossentropy(), metrics=['accuracy'])
return model
Nota: Si TimeDistributed no funciona para usted, puede cambiarlo por el siguiente codigo:
keras.layers.Lambda(
lambda x: tf.reshape(feature_extractor(tf.reshape(x, [-1, IMG_SIZE, IMG_SIZE,CHANNELS])),
[-1, SEQUENCE_LENGTH, EXTRACTOR_SIZE]),
),
Contruimos el modelo
[29]:
model = build_lstm(cnn_model)
model.summary()
WARNING:tensorflow:Layer lstm_3 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.
WARNING:tensorflow:Layer lstm_3 will not use cuDNN kernels since it doesn't meet the criteria. It will use a generic GPU kernel as fallback when running on GPU.
Model: "sequential_5"
_________________________________________________________________
Layer (type) Output Shape Param #
=================================================================
time_distributed_3 (TimeDist (None, 10, 25088) 696864
_________________________________________________________________
masking_3 (Masking) (None, 10, 25088) 0
_________________________________________________________________
lstm_3 (LSTM) (None, 512) 52430848
_________________________________________________________________
dense_6 (Dense) (None, 128) 65664
_________________________________________________________________
dropout_3 (Dropout) (None, 128) 0
_________________________________________________________________
dense_7 (Dense) (None, 3) 387
=================================================================
Total params: 53,193,763
Trainable params: 53,193,763
Non-trainable params: 0
_________________________________________________________________
Entrenamos el modelo
[30]:
history = model.fit(dataset, epochs=10)
Epoch 1/10
27/27 [==============================] - 56s 1s/step - loss: 0.8045 - accuracy: 0.5802
Epoch 2/10
27/27 [==============================] - 53s 2s/step - loss: 0.5732 - accuracy: 0.7146
Epoch 3/10
27/27 [==============================] - 53s 2s/step - loss: 0.4512 - accuracy: 0.7948
Epoch 4/10
27/27 [==============================] - 53s 2s/step - loss: 0.3997 - accuracy: 0.8396
Epoch 5/10
27/27 [==============================] - 53s 2s/step - loss: 0.3705 - accuracy: 0.8349
Epoch 6/10
27/27 [==============================] - 53s 2s/step - loss: 0.2939 - accuracy: 0.8939
Epoch 7/10
27/27 [==============================] - 53s 2s/step - loss: 0.2220 - accuracy: 0.9175
Epoch 8/10
27/27 [==============================] - 53s 2s/step - loss: 0.1632 - accuracy: 0.9575
Epoch 9/10
27/27 [==============================] - 53s 2s/step - loss: 0.2006 - accuracy: 0.9387
Epoch 10/10
27/27 [==============================] - 53s 2s/step - loss: 0.5872 - accuracy: 0.7783
Abrir en Google Colab
Descargar notebook