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.
  • AVAlocalizaçã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 quando num_frames < T, falta de crop que preserve a proporção (aspect ratio), etc.

Exercícios

  1. (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.
  2. (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.
  3. (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

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).