Phase 04 - Lesson 12
Video Understanding — Modelado Temporal
Un video es una secuencia de imágenes más la física que las conecta. Cada modelo de video trata el tiempo como un eje extra (conv 3D), una secuencia sobre la cual aplicar atención (transformer) o una característica a extraer una vez y realizar pooling (2D+pool).
Tipo: Aprender + Construir Lenguajes: Python Prerrequisitos: Fase 4 Lección 03 (CNNs), Fase 4 Lección 04 (Clasificación de Imágenes) Tiempo: ~45 minutos
Objetivos de aprendizaje
- Distinguir los tres enfoques principales de modelado de video (2D+pool, conv 3D, transformer espacio-temporal) y predecir sus compromisos de costo y precisión
- Implementar muestreo de frames, pooling temporal y un clasificador baseline 2D+pool en PyTorch
- Explicar por qué los kernels 3D "inflados" de I3D se transfieren bien desde los pesos de ImageNet y qué hace de manera diferente una conv factorizada (2+1)D
- Leer los datasets y métricas estándar de reconocimiento de acciones: Kinetics-400/600, UCF101, Something-Something V2; precisión top-1 a nivel de clip y de video
El Problema
Un video de 30 segundos a 30 fps consta de 900 imágenes. Ingenuamente, la clasificación de video es la clasificación de imágenes ejecutada 900 veces seguida de algún tipo de agregación. Eso funciona cuando la acción es visible en casi todos los frames (videos de deportes, cocina, ejercicio) y falla rotundamente cuando la acción se define por el movimiento en sí: "empujar algo de izquierda a derecha" se ve como dos objetos estáticos en cada frame individual.
La pregunta central para toda arquitectura de video es: ¿cuándo se modela la estructura temporal y cómo? La respuesta define todo lo demás — costo de cómputo, estrategia de preentrenamiento, si se pueden reutilizar los pesos de ImageNet, en qué datasets se entrena el modelo.
Esta lección es deliberadamente más corta que las lecciones de imágenes estáticas. El maquinario básico de imágenes ya está en su lugar, y la comprensión de video se centra principalmente en la historia temporal: muestreo, modelado y agregación.
El Concepto
Las tres familias arquitectónicas
flowchart LR
V["Clip de video<br/>(T frames)"] --> A1["2D + pool<br/>ejecutar CNN 2D por frame,<br/>promedio a lo largo del tiempo"]
V --> A2["Conv 3D<br/>convolucionar sobre<br/>T x H x W"]
V --> A3["Transformer<br/>espacio-temporal<br/>atención sobre<br/>tokens (t, h, w)"]
A1 --> C["Logits"]
A2 --> C
A3 --> C
style A1 fill:#dbeafe,stroke:#2563eb
style A2 fill:#fef3c7,stroke:#d97706
style A3 fill:#dcfce7,stroke:#16a34a
2D + pool
Toma una CNN 2D (ResNet, EfficientNet, ViT). Ejecútala de forma independiente en cada frame muestreado. Promedia (o realiza max-pool, o attention-pool) los embeddings de cada frame. Alimenta el vector resultante a un clasificador.
Pros:
- El preentrenamiento en ImageNet se transfiere directamente.
- Es el más simple de implementar.
- Económico: T frames * costo de inferencia de una sola imagen.
Contras:
- No puede modelar el movimiento. Acción = agregado de apariencias.
- El pooling temporal es invariante al orden; "abrir puerta" y "cerrar puerta" se ven iguales.
Cuándo usarlo: tareas donde predomina la apariencia, transfer learning en datasets de video pequeños, baselines iniciales.
Convoluciones 3D
Reemplaza los kernels 2D (H, W) por kernels 3D (T, H, W). La red realiza la convolución tanto en el espacio como en el tiempo. Familias iniciales: C3D, I3D, SlowFast.
Truco de I3D: toma un modelo 2D preentrenado de ImageNet, "infla" cada kernel 2D copiándolo a lo largo de un nuevo eje temporal. Una conv 2D de 3x3 se convierte en una conv 3D de 3x3x3. Esto le da al modelo 3D pesos preentrenados fuertes en lugar de tener que entrenar desde cero.
Pros:
- Modela el movimiento directamente.
- La inflación de I3D ofrece transfer learning gratuito.
Contras:
- T/8 más FLOPs que la contraparte 2D (para un kernel temporal de 3 apilado 3 veces).
- Los kernels temporales son pequeños; el movimiento de largo alcance requiere un enfoque de pirámide o de flujo doble (dual-stream).
Cuándo usarlo: reconocimiento de acciones donde el movimiento es la señal clave (Something-Something V2, Kinetics con clases donde predomina el movimiento).
Transformers espacio-temporales
Tokeniza el video en una cuadrícula de parches (patches) espacio-temporales y aplica atención sobre todos ellos. TimeSformer, ViViT, Video Swin, VideoMAE.
Patrones de atención importantes:
- Joint — una gran atención sobre (t, h, w). Cuadrática en
T*H*W; costosa. - Divided — dos atenciones por bloque: una en el tiempo y otra en el espacio. Escalamiento casi lineal.
- Factorised — la atención temporal se alterna con la atención espacial a lo largo de los bloques.
Pros:
- Precisión SOTA en todos los benchmarks importantes.
- Se transfiere desde transformers de imagen (ViT) mediante la inflación de parches.
- Admite videos de contexto largo mediante atención dispersa (sparse).
Contras:
- Requiere mucho cómputo.
- Exige una elección cuidadosa del patrón de atención para evitar que el tiempo de ejecución se dispare.
Cuándo usarlo: datasets grandes, comprensión de video de alta fidelidad, tareas multimodales de video+texto.
Muestreo de frames
Un clip de 10 segundos a 30 fps consta de 300 frames; alimentar los 300 a cualquier modelo es un desperdicio. Estrategias estándar:
- Muestreo uniforme (uniform sampling) — elige T frames distribuidos uniformemente a lo largo del clip. Por defecto para 2D+pool.
- Muestreo denso (dense sampling) — ventana contigua aleatoria de T frames. Común para convs 3D porque el movimiento requiere frames vecinos.
- Multi-clip — toma múltiples ventanas de T frames del mismo video, clasifica cada una y promedia las predicciones durante la inferencia.
T suele ser 8, 16, 32 o 64. Un valor mayor de T = mayor señal temporal con mayor costo de cómputo.
Evaluación
Dos niveles:
- Precisión a nivel de clip (clip-level accuracy) — el modelo procesa un clip de T frames y reporta top-k.
- Precisión a nivel de video (video-level accuracy) — promedio de las predicciones a nivel de clip a lo largo de múltiples clips del mismo video; más alta y estable.
Siempre reporta ambas. Un modelo que obtiene 78% en clip / 82% en video depende en gran medida del promedio durante la inferencia; uno que obtiene 80% / 81% es más robusto a nivel de clip individual.
Datasets que encontrarás
- Kinetics-400 / 600 / 700 — el dataset de acciones de propósito general. 400k clips; URLs de YouTube (muchas ya no están disponibles).
- Something-Something V2 — acciones definidas por el movimiento ("mover X de izquierda a derecha"). No se puede resolver con 2D+pool.
- UCF-101, HMDB-51 — más antiguos y pequeños, pero todavía reportados.
- AVA — localización de acciones en el espacio y en el tiempo; más difícil que la clasificación.
Constrúyelo
Paso 1: Muestreador de frames
Muestreadores uniformes y densos que funcionan sobre una lista de frames (o un tensor de video).
import numpy as np
def sample_uniform(num_frames_total, T):
if num_frames_total <= T:
return list(range(num_frames_total)) + [num_frames_total - 1] * (T - num_frames_total)
step = num_frames_total / T
return [int(i * step) for i in range(T)]
def sample_dense(num_frames_total, T, rng=None):
rng = rng or np.random.default_rng()
if num_frames_total <= T:
return list(range(num_frames_total)) + [num_frames_total - 1] * (T - num_frames_total)
start = int(rng.integers(0, num_frames_total - T + 1))
return list(range(start, start + T))
Ambos devuelven T índices que se utilizan para segmentar el tensor de video.
Paso 2: Un baseline 2D+pool
Ejecuta una ResNet-18 2D sobre cada frame, promedia las características mediante pooling y clasifica.
import torch
import torch.nn as nn
from torchvision.models import resnet18, ResNet18_Weights
class FramePool(nn.Module):
def __init__(self, num_classes=400, pretrained=True):
super().__init__()
weights = ResNet18_Weights.IMAGENET1K_V1 if pretrained else None
backbone = resnet18(weights=weights)
self.features = nn.Sequential(*(list(backbone.children())[:-1])) # global avg pool kept
self.head = nn.Linear(512, num_classes)
def forward(self, x):
# x: (N, T, 3, H, W)
N, T = x.shape[:2]
x = x.view(N * T, *x.shape[2:])
feats = self.features(x).view(N, T, -1)
pooled = feats.mean(dim=1)
return self.head(pooled)
model = FramePool(num_classes=10)
x = torch.randn(2, 8, 3, 224, 224)
print(f"output: {model(x).shape}")
print(f"params: {sum(p.numel() for p in model.parameters()):,}")
Once millones de parámetros, preentrenado en ImageNet, se ejecuta frame por frame, promedia y clasifica. Este baseline suele estar a una distancia de 5-10 puntos de los modelos 3D dedicados en tareas donde predomina la apariencia — y a veces es mejor, ya que reutiliza un backbone de ImageNet más robusto.
Paso 3: Una conv 3D inflada al estilo I3D
Convierte una única conv 2D en una conv 3D repitiendo los pesos a lo largo de un nuevo eje temporal.
def inflate_2d_to_3d(conv2d, time_kernel=3):
out_c, in_c, kh, kw = conv2d.weight.shape
weight_3d = conv2d.weight.data.unsqueeze(2) # (out, in, 1, kh, kw)
weight_3d = weight_3d.repeat(1, 1, time_kernel, 1, 1) / time_kernel
conv3d = nn.Conv3d(in_c, out_c, kernel_size=(time_kernel, kh, kw),
padding=(time_kernel // 2, conv2d.padding[0], conv2d.padding[1]),
stride=(1, conv2d.stride[0], conv2d.stride[1]),
bias=False)
conv3d.weight.data = weight_3d
return conv3d
conv2d = nn.Conv2d(3, 64, kernel_size=3, padding=1, bias=False)
conv3d = inflate_2d_to_3d(conv2d, time_kernel=3)
print(f"2D weight shape: {tuple(conv2d.weight.shape)}")
print(f"3D weight shape: {tuple(conv3d.weight.shape)}")
x = torch.randn(1, 3, 8, 56, 56)
print(f"3D output shape: {tuple(conv3d(x).shape)}")
La división por time_kernel mantiene las magnitudes de activación aproximadamente constantes — lo cual es importante para no alterar las estadísticas de batch-norm en la primera pasada.
Paso 4: Conv factorizada (2+1)D
Divide una conv 3D en una conv 2D (espacial) y una conv 1D (temporal). Mismo campo receptivo, menos parámetros y mejor precisión en algunos benchmarks.
class Conv2Plus1D(nn.Module):
def __init__(self, in_c, out_c, kernel_size=3):
super().__init__()
mid_c = (in_c * out_c * kernel_size * kernel_size * kernel_size) \
// (in_c * kernel_size * kernel_size + out_c * kernel_size)
self.spatial = nn.Conv3d(in_c, mid_c, kernel_size=(1, kernel_size, kernel_size),
padding=(0, kernel_size // 2, kernel_size // 2), bias=False)
self.bn = nn.BatchNorm3d(mid_c)
self.act = nn.ReLU(inplace=True)
self.temporal = nn.Conv3d(mid_c, out_c, kernel_size=(kernel_size, 1, 1),
padding=(kernel_size // 2, 0, 0), bias=False)
def forward(self, x):
return self.temporal(self.act(self.bn(self.spatial(x))))
c = Conv2Plus1D(3, 64)
x = torch.randn(1, 3, 8, 56, 56)
print(f"(2+1)D output: {tuple(c(x).shape)}")
Una red R(2+1)D completa es idéntica a una ResNet-18 con cada conv de 3x3 reemplazada por Conv2Plus1D.
Úsalo
Dos librerías cubren el trabajo de video en producción:
torchvision.models.video— R(2+1)D, MViT, Swin3D con pesos Kinetics preentrenados. Misma API que los modelos de imagen.pytorchvideo(Meta) — zoo de modelos, cargadores de datos para Kinetics / SSv2 / AVA, transformaciones estándar.
Para modelos de video de lenguaje visual (video captioning, video QA), utiliza transformers (VideoMAE, VideoLLaMA, InternVideo).
Entrégalo
Esta lección produce:
outputs/prompt-video-architecture-picker.md— un prompt que elige 2D+pool / I3D / (2+1)D / transformer basándose en apariencia vs. movimiento, tamaño del dataset y presupuesto de cómputo.outputs/skill-frame-sampler-auditor.md— una skill que inspecciona el muestreador de un pipeline de video y señala fallas comunes: error de índice por uno (off-by-one), muestreo desigual cuandonum_frames < T, falta de recorte que conserve la relación de aspecto, etc.
Ejercicios
- (Fácil) Calcula los FLOPs (aproximados) para FramePool con T=8 frente a una ResNet 3D al estilo I3D con T=8. Justifica por que 2D+pool es de 3 a 5 veces más económico.
- (Medio) Genera un dataset sintético de video: bolas aleatorias moviéndose en direcciones aleatorias, etiquetadas por la dirección del movimiento ("left-to-right", "right-to-left", "diagonal-up"). Entrena FramePool con él. Demuestra que alcanza una precisión cercana al azar, lo que prueba que la apariencia por sí sola es insuficiente para tareas de movimiento.
- (Difícil) Construye una R(2+1)D-18 reemplazando cada Conv2d en una ResNet-18 con
Conv2Plus1D. Infla los pesos de la primera conv a partir de una ResNet-18 preentrenada en ImageNet. Entrena en el dataset de movimiento del ejercicio 2 y supera a FramePool.
Términos clave
| Término | Lo que dice la gente | Lo que realmente significa |
|---|---|---|
| 2D + pool | "Clasificador por frame" | Ejecutar una CNN 2D en cada frame muestreado, realizar pooling de características a lo largo del tiempo y clasificar |
| Convolución 3D | "Kernel espacio-temporal" | Kernel que realiza la convolución sobre (T, H, W); puede modelar el movimiento de forma nativa |
| Inflación | "Elevar pesos 2D a 3D" | Inicializar los pesos de una conv 3D repitiendo los pesos de una conv 2D a lo largo del nuevo eje temporal, luego dividir por kernel_T para conservar la escala de activación |
| (2+1)D | "Conv factorizada" | Dividir la conv 3D en una conv 2D espacial + una conv 1D temporal; menos parámetros y una no-linealidad adicional entre ambas |
| Atención dividida | "Tiempo, luego espacio" | Bloque de transformer con dos módulos de atención por capa: uno sobre los tokens del mismo frame, y otro sobre los tokens en la misma posición espacial |
| Clip | "Ventana de T frames" | Una subsecuencia muestreada de T frames; la unidad básica que consume un modelo de video |
| Precisión de clip vs video | "Dos configuraciones de evaluación" | Clip = una muestra por video; video = promedio a lo largo de múltiples clips muestreados |
| Kinetics | "El ImageNet de video" | De 400 a 700 clases de acciones, más de 300k clips de YouTube; el corpus de preentrenamiento estándar para video |
Lectura adicional
- I3D: Quo Vadis, Action Recognition (Carreira & Zisserman, 2017) — presenta la inflación y el dataset Kinetics
- R(2+1)D: A Closer Look at Spatiotemporal Convolutions (Tran et al., 2018) — conv factorizada, sigue siendo un baseline robusto
- TimeSformer: Is Space-Time Attention All You Need? (Bertasius et al., 2021) — el primer transformer de video de alto rendimiento
- VideoMAE (Tong et al., 2022) — preentrenamiento con autoencoder enmascarado para video; la receta de preentrenamiento dominante en la actualidad