Phase 04 - Lesson 12
Video Understanding — Modelagem Temporal
Um vídeo é uma sequência de imagens mais a física que as conecta. Cada modelo de vídeo trata o tempo como um eixo extra (conv 3D), uma sequência sobre a qual aplicar atenção (transformer) ou uma feature para extrair uma vez e realizar o pooling (2D+pool).
Tipo: Aprender + Construir Linguagens: Python Pré-requisitos: Fase 4 Lição 03 (CNNs), Fase 4 Lição 04 (Classificação de Imagens) Tempo: ~45 minutos
Objetivos de aprendizagem
- Distinguir as três principais abordagens de modelagem de vídeo (2D+pool, conv 3D, transformer espaço-temporal) e prever seus trade-offs de custo e acurácia
- Implementar amostragem de frames, pooling temporal e um classificador baseline 2D+pool em PyTorch
- Explicar por que os kernels 3D "inflados" do I3D se transferem bem dos pesos do ImageNet e o que uma conv factorizada (2+1)D faz de diferente
- Ler os datasets e métricas padrão de reconhecimento de ação: Kinetics-400/600, UCF101, Something-Something V2; acurácia top-1 nos níveis de clipe e vídeo
O Problema
Um vídeo de 30 segundos a 30 fps possui 900 imagens. Ingenuamente, a classificação de vídeo é a classificação de imagens executada 900 vezes, seguida por algum tipo de agregação. Isso funciona quando a ação é visível em quase todos os frames (esportes, culinária, vídeos de exercícios) e falha drasticamente quando a ação é definida pelo próprio movimento: "empurrar algo da esquerda para a direita" parece com dois objetos estáticos em cada frame individual.
A questão central para toda arquitetura de vídeo é: quando a estrutura temporal é modelada e como? A resposta direciona todo o resto — custo de computação, estratégia de pré-treinamento, se você pode reutilizar pesos do ImageNet, em quais datasets o modelo treina.
Esta lição é deliberadamente mais curta do que as lições de imagem estática. O maquinário básico de imagem já está pronto, e a compreensão de vídeo é principalmente sobre a história temporal: amostragem, modelagem e agregação.
O Conceito
As três famílias arquiteturais
flowchart LR
V["Clipe de vídeo<br/>(T frames)"] --> A1["2D + pool<br/>rodar CNN 2D por frame,<br/>média ao longo do tempo"]
V --> A2["Conv 3D<br/>convolucionar sobre<br/>T x H x W"]
V --> A3["Transformer<br/>espaço-temporal<br/>atenção 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
Pegue uma CNN 2D (ResNet, EfficientNet, ViT). Rode-a independentemente em cada frame amostrado. Tire a média (ou max-pool, ou attention-pool) dos embeddings de cada frame. Alimente o vetor resultante do pooling em um classificador.
Prós:
- O pré-treinamento no ImageNet se transfere diretamente.
- Mais simples de implementar.
- Barato: T frames * custo de inferência de uma única imagem.
Contras:
- Não consegue modelar o movimento. Ação = agregado de aparências.
- O pooling temporal é invariante à ordem; "abrir porta" e "fechar porta" parecem iguais.
Quando usar: tarefas focadas em aparência, transfer learning em datasets de vídeo pequenos, baselines iniciais.
Convoluções 3D
Substitua os kernels 2D (H, W) por kernels 3D (T, H, W). A rede convoluciona tanto no espaço quanto no tempo. Famílias iniciais: C3D, I3D, SlowFast.
Truque do I3D: pegue um modelo ImageNet 2D pré-treinado, "infle" cada kernel 2D copiando-o ao longo de um novo eixo temporal. Uma conv 2D de 3x3 torna-se uma conv 3D de 3x3x3. Isso fornece ao modelo 3D pesos pré-treinados fortes, em vez de treinar do zero.
Prós:
- Modela o movimento diretamente.
- A inflação do I3D oferece transfer learning de graça.
Contras:
- T/8 mais FLOPs que a contraparte 2D (para um kernel temporal de 3 empilhado 3 vezes).
- Os kernels temporais são pequenos; movimentos de longo alcance exigem uma abordagem de pirâmide ou de fluxo duplo (dual-stream).
Quando usar: reconhecimento de ação onde o movimento é o sinal principal (Something-Something V2, Kinetics com classes focadas em movimento).
Transformers espaço-temporais
Tokenize o vídeo em uma grade de patches de espaço-tempo e aplique atenção sobre todos eles. TimeSformer, ViViT, Video Swin, VideoMAE.
Padrões de atenção importantes:
- Joint — uma grande atenção sobre (t, h, w). Quadrática em
T*H*W; cara. - Divided — duas atenções por bloco: uma no tempo, uma no espaço. Escalonamento quase linear.
- Factorised — a atenção temporal se alterna com a atenção espacial ao longo dos blocos.
Prós:
- Acurácia SOTA em todos os principais benchmarks.
- Transfere de transformers de imagem (ViT) via inflação de patches.
- Suporta vídeos de contexto longo via atenção esparsa.
Contras:
- Consome muita computação.
- Exige uma escolha cuidadosa do padrão de atenção, caso contrário o tempo de execução explode.
Quando usar: datasets grandes, compreensão de vídeo de alta fidelidade, tarefas multimodais de vídeo+texto.
Amostragem de frames
Um clipe de 10 segundos a 30 fps possui 300 frames; alimentar todos os 300 em qualquer modelo é um desperdício. Estratégias padrão:
- Amostragem uniforme (uniform sampling) — escolhe T frames uniformemente ao longo do clipe. Padrão para 2D+pool.
- Amostragem densa (dense sampling) — janela contígua aleatória de T frames. Comum para convs 3D porque o movimento exige frames vizinhos.
- Multi-clipe (multi-clip) — amostra múltiplas janelas de T frames do mesmo vídeo, classifica cada uma e tira a média das previsões no momento do teste.
T geralmente é 8, 16, 32 ou 64. T maior = mais sinal temporal com mais computação.
Avaliação
Dois níveis:
- Acurácia a nível de clipe (clip-level accuracy) — o modelo vê um clipe de T frames e reporta top-k.
- Acurácia a nível de vídeo (video-level accuracy) — média das previsões a nível de clipe em múltiplos clipes por vídeo; mais alta e mais estável.
Sempre reporte ambas. Um modelo com pontuação de 78% clipe / 82% vídeo depende muito da média no momento do teste; um que pontua 80% / 81% é mais robusto por clipe.
Datasets que você encontrará
- Kinetics-400 / 600 / 700 — o dataset de ações de uso geral. 400 mil clipes; URLs do YouTube (muitas agora fora do ar).
- Something-Something V2 — ações definidas pelo movimento ("mover X da esquerda para a direita"). Não pode ser resolvido por 2D+pool.
- UCF-101, HMDB-51 — mais antigos, menores, ainda reportados.
- AVA — localização de ações no espaço e no tempo; mais difícil que classificação.
Construa
Passo 1: Amostrador de frames
Amostradores uniformes e densos que funcionam em uma lista de frames (ou em um tensor de vídeo).
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 retornam T índices que você usa para fatiar o tensor de vídeo.
Passo 2: Um baseline 2D+pool
Rode uma ResNet-18 2D sobre cada frame, faça o average-pooling das features e classifique.
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()):,}")
Onze milhões de parâmetros, pré-treinado no ImageNet, roda por frame, tira a média e classifica. Este baseline frequentemente fica a 5-10 pontos de distância de modelos 3D dedicados em tarefas focadas em aparência — às vezes até melhor, porque reutiliza um backbone ImageNet mais forte.
Passo 3: Uma conv 3D inflada no estilo I3D
Transforme uma única conv 2D em uma conv 3D repetindo os pesos ao longo de um novo eixo 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)}")
A divisão por time_kernel mantém as magnitudes de ativação aproximadamente constantes — o que é importante para não quebrar as estatísticas do batch-norm logo na primeira passada.
Passo 4: Conv factorizada (2+1)D
Divida uma conv 3D em uma conv 2D (espacial) e uma conv 1D (temporal). Mesmo campo receptivo, menos parâmetros, melhor acurácia em alguns 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)}")
Uma rede R(2+1)D completa é idêntica a uma ResNet-18, mas com cada conv 3x3 substituída por Conv2Plus1D.
Use
Duas bibliotecas cobrem o trabalho de vídeo em produção:
torchvision.models.video— R(2+1)D, MViT, Swin3D com pesos Kinetics pré-treinados. Mesma API dos modelos de imagem.pytorchvideo(Meta) — zoo de modelos, data loaders para Kinetics / SSv2 / AVA, transforms padrão.
Para modelos de vídeo Vision-Language (legenda de vídeo, vídeo QA), use transformers (VideoMAE, VideoLLaMA, InternVideo).
Entregue
Esta lição produz:
outputs/prompt-video-architecture-picker.md— um prompt que escolhe entre 2D+pool / I3D / (2+1)D / transformer com base em aparência versus movimento, tamanho do dataset e orçamento de computação.outputs/skill-frame-sampler-auditor.md— uma habilidade que inspeciona o amostrador de um pipeline de vídeo e sinaliza bugs comuns: índice off-by-one, amostragem desigual quandonum_frames < T, falta de crop que preserve a proporção (aspect ratio), etc.
Exercícios
- (Fácil) Calcule os FLOPs (aproximados) para FramePool com T=8 vs uma ResNet 3D no estilo I3D com T=8. Justifique por que o 2D+pool é de 3 a 5 vezes mais barato.
- (Médio) Gere um dataset de vídeo sintético: bolas aleatórias se movendo em direções aleatórias, rotuladas pela direção do movimento ("left-to-right", "right-to-left", "diagonal-up"). Treine o FramePool nele. Mostre que ele alcança uma acurácia próxima à do acaso (chance accuracy), provando que apenas a aparência é insuficiente para tarefas de movimento.
- (Difícil) Construa uma R(2+1)D-18 substituindo cada Conv2d em uma ResNet-18 por
Conv2Plus1D. Infle os pesos da primeira conv a partir de uma ResNet-18 pré-treinada no ImageNet. Treine no dataset de movimento do exercício 2 e supere o FramePool.
Termos-chave
| Termo | O que as pessoas dizem | O que realmente significa |
|---|---|---|
| 2D + pool | "Classificador por frame" | Roda uma CNN 2D em cada frame amostrado, faz o average-pooling das features ao longo do tempo e classifica |
| Convolução 3D | "Kernel espaço-temporal" | Kernel que realiza convolução sobre (T, H, W); pode modelar movimento nativamente |
| Inflação | "Elevar pesos 2D para 3D" | Inicializa os pesos de uma conv 3D repetindo os pesos de uma conv 2D ao longo do novo eixo temporal, depois divide por kernel_T para preservar a escala de ativação |
| (2+1)D | "Conv factorizada" | Divide a conv 3D em 2D espacial + 1D temporal; menos parâmetros, não-linearidade extra entre elas |
| Atenção dividida | "Tempo, depois espaço" | Bloco transformer com duas atenções por camada: uma sobre tokens no mesmo frame, uma sobre tokens na mesma posição |
| Clipe | "Janela de T frames" | Uma subsequência amostrada de T frames; a unidade que um modelo de vídeo consome |
| Acurácia de clipe vs vídeo | "Duas configurações de avaliação" | Clipe = uma amostra por vídeo; vídeo = média sobre múltiplos clipes amostrados |
| Kinetics | "O ImageNet dos vídeos" | 400 a 700 classes de ação, mais de 300 mil clipes do YouTube, o corpus padrão para pré-treinamento de vídeo |
Leitura adicional
- I3D: Quo Vadis, Action Recognition (Carreira & Zisserman, 2017) — apresenta a inflação e o dataset Kinetics
- R(2+1)D: A Closer Look at Spatiotemporal Convolutions (Tran et al., 2018) — conv factorizada, ainda um forte baseline
- TimeSformer: Is Space-Time Attention All You Need? (Bertasius et al., 2021) — o primeiro transformer de vídeo forte
- VideoMAE (Tong et al., 2022) — pré-treinamento com autoencoder mascarado para vídeo; a receita de pré-treinamento dominante atual