视频理解:时序建模


文档摘要

视频理解:时序建模 本节摘要:视频是一串图像加上把它们连起来的物理。每个视频模型要么把时间当成一个额外轴(3D 卷积)、要么当成一段要去 attend 的序列(Transformer)、要么当成提取一次再池化的特征(2D+pool)。本节把这三大家族的成本与精度权衡摆清楚,实现帧采样、时间池化和 2D+pool 基线分类器,讲透 I3D「膨胀」3D 核为何能直接迁移 ImageNet 权重、(2+1)D 分解卷积做了什么不同,并认识 Kinetics、Something-Something V2 等动作识别数据集的评估方式。读完本节,你能为给定任务在三类架构间做出合理选择。 对应原课程:Phase 4 · Lesson 12 · (原英文 )。

视频理解:时序建模

本节摘要:视频是一串图像加上把它们连起来的物理。每个视频模型要么把时间当成一个额外轴(3D 卷积)、要么当成一段要去 attend 的序列(Transformer)、要么当成提取一次再池化的特征(2D+pool)。本节把这三大家族的成本与精度权衡摆清楚,实现帧采样、时间池化和 2D+pool 基线分类器,讲透 I3D「膨胀」3D 核为何能直接迁移 ImageNet 权重、(2+1)D 分解卷积做了什么不同,并认识 Kinetics、Something-Something V2 等动作识别数据集的评估方式。读完本节,你能为给定任务在三类架构间做出合理选择。

对应原课程:Phase 4 · Lesson 12 · video-understanding(原英文 phases/04-computer-vision/12-video-understanding/docs/en.md)。

学习目标

阅读完本节,你应当能够:

  1. 区分三大视频建模范式(2D+pool、3D 卷积、时空 Transformer),预测它们的算力与精度权衡
  2. 用 PyTorch 实现帧采样、时间池化和 2D+pool 基线分类器。
  3. 解释 I3D「膨胀」3D 核为何能从 ImageNet 权重良好迁移,以及分解式 (2+1)D 卷积有何不同。
  4. 读懂标准动作识别数据集与指标:Kinetics-400/600、UCF101、Something-Something V2;片段级与视频级 top-1 准确率。

一、问题与直觉

30 秒、30 fps 的视频是 900 张图。朴素地说,视频分类就是把图像分类跑 900 次再做某种聚合。当动作几乎在每一帧都看得见(体育、烹饪、健身)时它有效;但当动作本身由运动定义时它彻底失败:「从左推到右」在每一帧里都是两个静止物体。

每个视频架构的核心问题是:时序结构何时被建模、怎么建模? 答案决定了一切——算力成本、预训练策略、能否复用 ImageNet 权重、要训什么数据集。

本节比静态图像章节短是有意的。核心图像机制已经就位,视频理解主要是讲时间的故事:采样、建模、聚合。

三大架构家族

2D + pool

拿一个 2D CNN(ResNet、EfficientNet、ViT),对每个采样帧独立跑一遍,把每帧嵌入平均(或最大池化、或注意力池化),送进分类器。

优点:ImageNet 预训练直接迁移;实现最简单;便宜——T 帧 × 单图推理成本。
缺点:无法建模运动,动作只是外观的聚合;时间池化对顺序不变,「开门」和「关门」看起来一样。
适用:外观为主的任务、小视频数据集上的迁移学习、初始基线。

3D 卷积

把 2D (H, W) 核换成 3D (T, H, W) 核,网络同时在空间和时间上卷积。早期家族:C3D、I3D、SlowFast。

I3D 的招:拿一个预训练的 2D ImageNet 模型,沿新时间轴复制每个 2D 核把它「膨胀」成 3D。3×3 的 2D 卷积变成 3×3×3 的 3D 卷积,于是 3D 模型直接拿到了强力预训练权重,而非从头训。

优点:直接建模运动;I3D 膨胀白送迁移学习。
缺点:比 2D 对应物多约 T/8 的 FLOPs(时间核为 3、堆叠 3 次时);时间核小,长程运动要金字塔或双流。
适用:运动是信号的动作识别(Something-Something V2、运动密集的 Kinetics 类别)。

时空 Transformer

把视频切成时空 patch 网格,在所有 patch 间做注意力。TimeSformer、ViViT、Video Swin、VideoMAE。

要紧的注意力模式:

  • 联合(Joint)——对 (t, h, w) 做一次大注意力,对 T·H·W 平方复杂度,贵。
  • 分离(Divided)——每个块两次注意力:一次时间、一次空间,近似线性扩展。
  • 分解(Factorised)——块间时间注意力和空间注意力交替。

优点:每个主流基准上的 SOTA 精度;可经 patch 膨胀从图像 Transformer(ViT)迁移;支持稀疏注意力的长上下文视频。
缺点:算力大;注意力模式选不好运行时就炸。
适用:大数据集、高保真视频理解、多模态视频+文本任务。

帧采样

10 秒 30 fps 的片段是 300 帧;全喂给任何模型都是浪费。标准策略:

  • 均匀采样——沿片段均匀挑 T 帧,2D+pool 默认。
  • 密集采样——随机取连续 T 帧窗口,3D 卷积常用,因为运动需要相邻帧。
  • 多片段——同一视频采样多个 T 帧窗口,各分类一次,测试时平均预测。

T 通常取 8、16、32 或 64。T 越大,时序信号越多但算力越大。

评估

两个层级:

  • 片段级准确率——模型看一个 T 帧片段,报 top-k。
  • 视频级准确率——每视频多个片段的片段级预测取平均,更高也更稳。

两者都要报。一个 78% 片段 / 82% 视频的模型严重依赖测试时平均;80% / 81% 的则每片段更鲁棒。

常见数据集

  • Kinetics-400 / 600 / 700——通用动作数据集,40 万片段,YouTube 链接(很多已失效)。
  • Something-Something V2——运动定义的动作(「把 X 从左移到右」),2D+pool 解不了。
  • UCF-101、HMDB-51——更老更小,仍被报告。
  • AVA——空间和时间上的动作定位,比分类难。

二、从零实现

步骤 1:帧采样器

均匀采样器和密集采样器,作用在帧列表(或视频张量)上。

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))

两者都返回 T 个下标,用来切片视频张量。

步骤 2:2D+pool 基线

对每帧跑 2D ResNet-18,平均池化特征,分类。

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])) # 保留全局平均池化 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"输出: {model(x).shape}") print(f"参数量: {sum(p.numel() for p in model.parameters()):,}")

1100 万参数,ImageNet 预训练,逐帧跑、平均、分类。这个基线在外观为主的任务上常常只比正经 3D 模型低 5~10 个点——有时还更高,因为它复用了更强的 ImageNet 主干。

步骤 3:I3D 风格的膨胀 3D 卷积

把单个 2D 卷积沿新时间轴重复权重,变成 3D 卷积。

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 权重形状: {tuple(conv2d.weight.shape)}") print(f"3D 权重形状: {tuple(conv3d.weight.shape)}") x = torch.randn(1, 3, 8, 56, 56) print(f"3D 输出形状: {tuple(conv3d(x).shape)}")

除以 time_kernel 是为了保持激活幅度大致不变——这对第一遍不破坏批归一化统计至关重要。

步骤 4:分解式 (2+1)D 卷积

把 3D 卷积拆成一个 2D(空间)和一个 1D(时间)卷积。感受野相同,参数更少,在某些基准上精度更高。

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 输出: {tuple(c(x).shape)}")

一个完整的 R(2+1)D 网络就是把 ResNet-18 里每个 3×3 卷积换成 Conv2Plus1D

三、框架对比

两个库覆盖生产级视频工作:

  • torchvision.models.video——R(2+1)D、MViT、Swin3D,带 Kinetics 预训练权重,API 与图像模型一致。
  • pytorchvideo(Meta)——模型库、Kinetics / SSv2 / AVA 的数据加载器、标准变换。

视觉-语言视频模型(视频字幕、视频问答)用 transformers(VideoMAEVideoLLaMAInternVideo)。

四、可复用产物

本节产出两个可复用文件(位于原课程 outputs/):

  • prompt-video-architecture-picker.md:一个提示词——按「外观 vs 运动」、数据集大小、算力预算,在 2D+pool / I3D / (2+1)D / Transformer 之间挑。
  • skill-frame-sampler-auditor.md:一个技能——审查视频流水线的采样器,标记常见 bug:off-by-one 下标、num_frames < T 时采样不均、缺少保宽高比裁剪等。

五、练习

  1. (简单) 估算 T=8 的 FramePool 与 T=8 的 I3D 风格 3D ResNet 的 FLOPs(近似)。论证为什么 2D+pool 便宜 3~5 倍。
  2. (中等) 合成一个视频数据集:随机方向运动的随机球,按运动方向标注(「左到右」「右到左」「斜向上」)。在其上训 FramePool,证明它接近随机猜测——证明单靠外观解不了运动任务。
  3. (困难) 把 ResNet-18 里每个 Conv2d 换成 Conv2Plus1D,搭一个 R(2+1)D-18。第一个卷积的权重从 ImageNet 预训练的 ResNet-18 膨胀而来。在练习 2 的运动数据集上训练并打败 FramePool。

本节要点回顾

  1. 视频 = 图像序列 + 连接它们的物理——何时建模时间是每个架构的核心抉择。
  2. 三大家族:2D+pool(逐帧跑+池化,便宜但不能建模运动)、3D 卷积(原生建模运动,I3D 膨胀白送迁移)、时空 Transformer(SOTA,但算力大)。
  3. 2D+pool 不能分辨开门和关门——时间池化对顺序不变;运动定义的任务(Something-Something V2)非 3D 不可。
  4. I3D 膨胀——沿时间轴复制 2D 权重再除以 time_kernel 保激活尺度,3D 模型直接拿到 ImageNet 权重。
  5. (2+1)D 分解卷积——3D 拆成 2D 空间 + 1D 时间,感受野不变、参数更少、中间多一个非线性。
  6. 注意力模式要紧——联合(贵)、分离(线性)、分解(交替);选错运行时就炸。
  7. 帧采样三策略——均匀(2D+pool 默认)、密集(3D 要相邻帧)、多片段(测试时平均)。
  8. 报两级准确率——片段级和视频级;两者差距大说明严重依赖测试时平均。

下一节进入3D 视觉与 NeRF——从二维图像升到三维场景,用神经辐射场表示并渲染新视角。


发布者: 作者: Rohit Gupta 转发
评论区 (0)
U