视频理解业务在近两年越来越常见,从安防摄像头里的异常行为识别,到体育视频中的动作拆解,再到短视频平台的内容理解,底层都离不开“怎么建模时间维度”这个问题。过去做图像分类,我们关心的是空间语义;到了视频里,语义变化的节奏通常很慢,而运动细节变化得非常快。一套网络如果只用一个固定帧率去“看”视频,要么为了捕捉运动导致语义特征不够深,要么为了吃透语义而丢失运动线索。SlowFast 正是针对这个矛盾提出的一套双分支视频时序建模方案,核心思路可以概括成八个字:慢看语义,快看运动。

这篇文章会围绕 SlowFast 的原理、结构、PyTorch 实现、实验设计和工业落地展开。适合刚接触视频理解的算法工程师,也适合需要在业务中接入动作识别能力的后端或平台开发者。阅读完你可以理解 SlowFast 为什么用两条路径、侧向连接到底在融合什么,也能拿到一份可以运行的结构代码和部署优化思路。

1. 背景与核心概念

1.1 为什么视频理解比图像分类更难

图像分类的核心是空间特征:一张图片里的物体形状、纹理、颜色构成了分类依据。视频多了一个时间轴,但我们不能简单地把视频看成一组图片再加个平均池化。视频里的动作信息是跨帧的,比如“挥手”和“招手”在单帧里几乎无法区分,区别只体现在时间维度上;而“跑步”和“快走”的空间表现可能非常接近,区分它们需要看身体姿态随时间的变化速率。

这就带来两个矛盾:

  • 时间分辨率 vs 空间语义。想要捕捉快速运动,需要高帧率输入,但高帧率带来的计算量很大;想要保留丰富语义,需要足够深的网络和大通道特征,但深网络往往通过降采样牺牲时间分辨率。
  • 局部运动 vs 全局上下文。像手部晃动、嘴唇开合这些小尺度运动,需要在时间上做较细的采样;而场景是体育馆还是街道、人物在做什么大动作,这种语义变化相对缓慢,不需要把每一帧都精细建模。

过去常见的解决思路是使用三维卷积网络,比如 C3D、I3D,直接对连续的时空体做卷积。这类方法能学到时间特征,但通常采用单一帧率,网络内部不会刻意区分“快变化”和“慢变化”。另一种思路是双流网络,用 RGB 分支建模外观、用光流分支建模运动。双流方法效果不错,但光流计算本身很重,而且在训练时依赖预计算的光流数据,工程链路过长。

SlowFast 的切入点不一样。它不依赖外部光流,而是通过网络结构设计,让模型自己从原始视频中分别挖掘“慢语义”和“快运动”。

1.2 SlowFast 是什么

SlowFast 是 Facebook AI Research 在 2019 年提出的动作识别模型,全称是 SlowFast Networks for Video Recognition。它设计了两条并行的三维卷积路径:

  • Slow 路径:帧率低,通道数多,负责捕捉空间语义和缓慢变化的上下文信息。
  • Fast 路径:帧率高,通道数少,负责捕捉快速变化的运动信息。

两条路径通过侧向连接(Lateral Connection)进行信息交换,把 Fast 路径学到的运动信息融合到 Slow 路径中,最终用融合后的特征去分类。这个设计在 Kinetics 系列数据集上刷新了当时的精度,同时计算量也可以做到比单分支高帧率网络更低,因为它把“高帧率”和“大通道”这两个开销最大的属性拆到了不同分支上。

在工程角度看,SlowFast 的优势还在于它是纯卷积结构,不依赖预计算光流,所以训练和推理链路比传统双流方法简单很多。这也是它后来被大量集成到开源视频理解工具箱里的原因之一。

2. 核心思想与网络结构精讲

2.1 “慢看语义,快看运动”如何理解

用一个生活化的例子来说明。假设你要判断一段视频里的人在做什么,观察过程可以分成两类信息:

  • 第一类是上下文语义。比如人物衣着、周围环境、房间里有没有球桌,这些信息变化很慢,你看几帧甚至一帧就能建立基本判断。
  • 第二类是运动细节。比如球拍的挥动方向、手腕的翻转、脚步的移动,这些细节在相邻几帧之间就会发生明显变化,必须用较高的时间分辨率去观察。

如果只使用低帧率慢速看,能获得很好的语义特征,但快速运动信息会被时间降采样丢掉;如果只使用高帧率快速看,运动是保住了,但因为通道数和网络深度受限,语义能力不够。

SlowFast 的做法是把这两种观察方式放在同一个网络里并行执行。Slow 分支把输入视频的时间维度压缩,比如每 4 帧或 8 帧才看一帧,但给这条分支分配更多的通道,让它有足够的表达能力去建模场景语义;Fast 分支保持原始高帧率输入,但通道数较少,因为运动信息本质上是相对局部的,不需要像语义分支那么强大的通道容量。两条分支的输出再通过侧向连接整合,最终模型既拥有强语义,又不会丢失快速运动。

2.2 Slow 路径与 Fast 路径设计

先看两个关键的超参数。论文中通常用 α 表示帧率比例,用 β 表示通道比例。

慢路径的帧率是基础帧率。如果 Fast 路径的时间分辨率是 Slow 路径的 α 倍,那么输入视频在时间上采样时,Fast 路径使用原始帧序列,Slow 路径则每隔 α 帧取一帧。常用的 α 是 4 或 8。时间分辨率相差这么多,意味着 Fast 路径能看到更多中间帧的瞬时变化,Slow 路径则更关注整体趋势。

通道比例 β 通常是 1/8,意思是 Fast 路径的通道数是 Slow 路径的 1/8。这里需要注意,通道少并不代表 Fast 路径不重要,而是因为快速运动特征的空间相关性相对简单,不需要用太多通道去表达。如果两个分支使用相同的通道数,计算量会成倍上涨,融合收益却不会线性提升。

两个分支在网络内部的结构很相似,都用 3D ResNet 作为主干,但有几个差别:

  • Slow 路径的 stem 卷积在时间维上 kernel 通常为 1,因为它输入的时间长度本来就短,不需要再做时间卷积。
  • Fast 路径的 stem 卷积在时间维上 kernel 通常为 5,这样从一开始就能感知短时间内的帧间变化。
  • 两个分支使用各自的 BatchNorm,不共享参数。

用一个简单的表格来总结差异:

设计项 Slow 路径 Fast 路径
时间分辨率 低,输入帧率约为 Fast 的 1/α 高,使用原始帧率
通道容量 大,默认相对宽 小,默认约为 Slow 的 1/β
负责信息 空间语义、场景上下文 快速运动、时序变化
stem 时间卷积核 一般为 1 一般为 5
BatchNorm 独立 独立

2.3 侧向连接如何融合快慢信息

如果两个分支只是各自计算然后最后拼在一起,那么 Fast 路径的运动信息对 Slow 路径的中间层没有任何帮助。侧向连接解决的就是这个问题:它把 Fast 路径的特征在时间维度上下采样,再通过简单卷积变换通道数,送入 Slow 路径对应层。

侧向连接很像三维卷积版的“残差融合”,它的输入是 Fast 分支的特征,输出通道数对齐到 Slow 分支。时间维度上通过一个 stride 等于 α 的卷积或池化来降低分辨率,使 Fast 分支的时间长度和 Slow 分支对齐。这样 Slow 路径在每一层都能看到 Fast 路径提炼出来的运动信息,而不是只能在最后一层做一次性融合。

Fast 路径则不需要反向的侧向连接。原因是 Fast 路径的输入信息已经足够细致,它缺少的是语义,而语义最终主要靠 Slow 路径承载,在末端分类头融合已经足够;给 Fast 路径也加反向连接会显著增加计算量,收益有限。

2.4 与其他视频模型的区别

这里容易混淆的是:SlowFast 看起来像双流网络,但它并不依赖光流。经典双流网络用一个分支输入 RGB 图像,另一个分支输入预计算光流图,光流本身需要额外算法提取;SlowFast 的两个分支输入都是原始视频,只是帧率和通道数不同。它的“运动信息”是 Fast 路径在高帧率输入下自己学出来的,不需要额外监督。

SlowFast 也不同于普通 3D CNN。普通 3D CNN 用一组固定帧率的卷积核同时处理语义和运动,而 SlowFast 主动把这两种信息分流,再用侧向连接汇总。这种设计让 Fast 路径可以用很低的通道成本获得高时间分辨率,整体计算效率更高。

3. 环境准备与工程目录

3.1 依赖环境

本文的简化实现基于 PyTorch,建议使用 Python 3.8 及以上版本,并安装好 CUDA 版的 PyTorch。下面命令可以创建一个专用环境:

conda create -n slowfast python=3.8
conda activate slowfast
pip install torch torchvision
pip install numpy opencv-python
pip install thop   # 用于统计 FLOPs 和参数量,非必需

版本需要根据你的机器和 CUDA 环境调整,本文示例重点演示网络结构和训练思路,不依赖某个特定版本。如果你使用的是较新的 PyTorch 2.x,下面的代码同样可以运行。

3.2 项目目录

为了便于管理,建议把工程拆成下面这样:

slowfast_demo/
├── models/
│   ├── __init__.py
│   └── simple_slowfast.py
├── train.py
├── inference.py
└── config.py

简化实现的核心文件是 models/simple_slowfast.py ,训练脚本是 train.py ,推理脚本是 inference.py 。这样拆分的好处是结构清晰,后面接入真实数据集或替换成官方实现都很方便。

4. 完整实战案例:从零实现一个可运行的 SlowFast

4.1 数据准备思路

在动手写网络前,先明确数据怎么进模型。SlowFast 的输入通常是一段固定长度的视频片段,空间分辨率统一到 224×224。训练时先把视频抽帧,再按每段 32 帧或 64 帧采样。采样时需要考虑时间维对齐:如果 α=4,那么 Slow 路径实际看到的是 8 帧,Fast 路径看到完整的 32 帧。

这里先给出一个最简单的数据形式:把视频预处理成形状为 [C, T, H, W] 的张量,其中 C 是 RGB 通道,T 是帧数,H 和 W 是空间尺寸。实际项目中,你可以用 OpenCV 或 PyTorch 的 torchvision.io.read_video 读取视频并抽帧,本节先用随机张量演示完整训练流程,说明网络能够正常前向反向。

4.2 简化版网络实现

下面这个实现不是官方 SlowFast 的逐层复刻,而是保留了最核心的 Slow/Fast 双分支、帧率差异、通道差异、侧向连接四个要素。它可以直接用随机数据跑通,帮助理解结构。

# 文件路径:slowfast_demo/models/simple_slowfast.py
import torch
import torch.nn as nn
import torch.nn.functional as F


class Bottleneck3D(nn.Module):
    def __init__(self, in_channels, out_channels, temporal_stride=1, spatial_stride=1):
        super(Bottleneck3D, self).__init__()
        self.conv1 = nn.Conv3d(in_channels, out_channels, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm3d(out_channels)
        self.conv2 = nn.Conv3d(
            out_channels,
            out_channels,
            kernel_size=(3, 3, 3),
            stride=(temporal_stride, spatial_stride, spatial_stride),
            padding=(1, 1, 1),
            bias=False,
        )
        self.bn2 = nn.BatchNorm3d(out_channels)
        self.conv3 = nn.Conv3d(out_channels, out_channels, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm3d(out_channels)
        self.relu = nn.ReLU(inplace=True)

        self.downsample = None
        if in_channels != out_channels or temporal_stride != 1 or spatial_stride != 1:
            self.downsample = nn.Sequential(
                nn.Conv3d(
                    in_channels,
                    out_channels,
                    kernel_size=1,
                    stride=(temporal_stride, spatial_stride, spatial_stride),
                    bias=False,
                ),
                nn.BatchNorm3d(out_channels),
            )

    def forward(self, x):
        identity = x
        out = self.relu(self.bn1(self.conv1(x)))
        out = self.relu(self.bn2(self.conv2(out)))
        out = self.bn3(self.conv3(out))
        if self.downsample is not None:
            identity = self.downsample(identity)
        out = out + identity
        return self.relu(out)


class LateralConnection(nn.Module):
    def __init__(self, fast_channels, slow_channels, out_channels, alpha=4):
        super(LateralConnection, self).__init__()
        self.conv = nn.Conv3d(
            fast_channels,
            out_channels,
            kernel_size=(5, 1, 1),
            stride=(alpha, 1, 1),
            padding=(2, 0, 0),
            bias=False,
        )
        self.bn = nn.BatchNorm3d(out_channels)

    def forward(self, fast_feat):
        return self.bn(self.conv(fast_feat))


class SimpleSlowFast(nn.Module):
    def __init__(self, num_classes=10, alpha=4, beta=0.125, slow_channels=64):
        super(SimpleSlowFast, self).__init__()
        fast_channels = int(slow_channels * beta)
        self.alpha = alpha

        # Slow 路径先做时间维降采样,模拟低帧率输入
        self.slow_pool = nn.MaxPool3d(
            kernel_size=(alpha, 1, 1),
            stride=(alpha, 1, 1),
        )
        self.slow_stem = nn.Sequential(
            nn.Conv3d(3, slow_channels, kernel_size=(1, 7, 7), stride=(1, 2, 2), padding=(0, 3, 3), bias=False),
            nn.BatchNorm3d(slow_channels),
            nn.ReLU(inplace=True),
        )

        # Fast 路径保持高时间分辨率
        self.fast_stem = nn.Sequential(
            nn.Conv3d(3, fast_channels, kernel_size=(5, 7, 7), stride=(1, 2, 2), padding=(2, 3, 3), bias=False),
            nn.BatchNorm3d(fast_channels),
            nn.ReLU(inplace=True),
        )

        self.slow_block = Bottleneck3D(slow_channels, slow_channels)
        self.fast_block = Bottleneck3D(fast_channels, fast_channels)
        self.lateral = LateralConnection(fast_channels, slow_channels, slow_channels, alpha=alpha)

        self.head = nn.Sequential(
            nn.AdaptiveAvgPool3d(1),
            nn.Flatten(),
            nn.Linear(slow_channels + slow_channels, num_classes),
        )

    def forward(self, x):
        # x 形状: [B, 3, T, H, W],T 需要是 alpha 的整数倍
        x_slow = self.slow_pool(x)
        x_fast = x

        feat_slow = self.slow_block(self.slow_stem(x_slow))
        feat_fast = self.fast_block(self.fast_stem(x_fast))
        feat_lateral = self.lateral(feat_fast)

        # 检查时间维是否对齐
        assert feat_slow.shape[2] == feat_lateral.shape[2], "temporal dimension mismatch"

        out = torch.cat([feat_slow, feat_lateral], dim=1)
        return self.head(out)

这段代码你可以直接保存为 models/simple_slowfast.py 。几个设计细节值得注意:

  • alpha=4 表示 Slow 路径时间分辨率是 Fast 路径的 1/4。
  • beta=0.125 表示 Fast 路径通道数是 Slow 路径的 1/8。
  • Slow 路径的 stem 时间卷积核是 1,因为输入帧数少,不需要再做时间卷积;Fast 路径的 stem 时间卷积核是 5,可以在第一层就捕捉局部运动。
  • 侧向连接用时间维 stride=4、kernel=5 的卷积把 Fast 特征压缩到与 Slow 特征相同的时间长度,再做通道变换。

用随机张量验证结构是否正常:

model = SimpleSlowFast(num_classes=10)
x = torch.randn(2, 3, 32, 224, 224)
y = model(x)
print(y.shape)  # 期望输出 torch.Size([2, 10])

如果显存不够,可以把空间尺寸改成 112 或使用更小的 batch。

4.3 训练骨架

下面这个训练脚本使用随机视频数据,目的是把完整的训练流程跑通。替换成真实数据时,只需要把 DummyVideoDataset 换成你的视频数据集。

# 文件路径:slowfast_demo/train.py
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

from models.simple_slowfast import SimpleSlowFast


class DummyVideoDataset(Dataset):
    def __init__(self, num_samples=128, T=32, H=112, W=112, num_classes=10):
        self.num_samples = num_samples
        self.T = T
        self.H = H
        self.W = W
        self.num_classes = num_classes

    def __len__(self):
        return self.num_samples

    def __getitem__(self, idx):
        video = torch.randn(3, self.T, self.H, self.W)
        label = torch.randint(0, self.num_classes, (1,)).item()
        return video, label


def main():
    device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
    model = SimpleSlowFast(num_classes=10).to(device)

    dataset = DummyVideoDataset(num_samples=128, T=32, H=112, W=112)
    loader = DataLoader(dataset, batch_size=4, shuffle=True, num_workers=0)

    optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9, weight_decay=1e-4)
    criterion = nn.CrossEntropyLoss()

    model.train()
    for epoch in range(3):
        total_loss = 0.0
        for videos, labels in loader:
            videos = videos.to(device)
            labels = torch.tensor(labels).to(device)

            outputs = model(videos)
            loss = criterion(outputs, labels)

            optimizer.zero_grad()
            loss.backward()
            optimizer.step()

            total_loss += loss.item()

        avg_loss = total_loss / len(loader)
        print(f"epoch {epoch + 1}, loss: {avg_loss:.4f}")


if __name__ == "__main__":
    main()

这个脚本里的 DummyVideoDataset 返回随机视频,所以 loss 不会降到很低,重点是验证前向、反向、优化器更新这一整套流程没有问题。换成真实视频时, __getitem__ 中应该返回一段经过抽帧、裁剪、归一化的视频张量,以及对应的动作类别标签。

4.4 推理与输出结果

模型训练完成后,推理过程由三部分组成:读取视频片段、预处理、前向得到概率。下面这段代码假设你已经把视频帧读取成 [N, H, W, C] 的 numpy 数组, N 是总帧数。

# 文件路径:slowfast_demo/inference.py
import numpy as np
import torch
import torchvision.transforms as T

from models.simple_slowfast import SimpleSlowFast


def preprocess_frames(frames, T=32, spatial_size=224):
    """
    frames: np.ndarray, 形状 [N, H, W, C],取值范围 0-255
    """
    transform = T.Compose([
        T.ToTensor(),
        T.Resize((spatial_size, spatial_size)),
        T.Normalize(mean=[0.45, 0.45, 0.45], std=[0.225, 0.225, 0.225]),
    ])

    indices = np.linspace(0, len(frames) - 1, T).astype(int)
    clip_frames = [transform(frames[i]) for i in indices]
    clip = torch.stack(clip_frames, dim=1)  # [C, T, H, W]
    return clip.unsqueeze(0)               # [1, C, T, H, W]


def predict(model, clip, class_names=None):
    model.eval()
    with torch.no_grad():
        logits = model(clip)
        probs = torch.softmax(logits, dim=1)
        top2 = torch.topk(probs, k=2, dim=1)

    for i in range(top2.indices.size(1)):
        cls_id = top2.indices[0, i].item()
        score = top2.values[0, i].item()
        name = class_names[cls_id] if class_names else str(cls_id)
        print(f"top-{i + 1}: {name} ({score:.4f})")


if __name__ == "__main__":
    model = SimpleSlowFast(num_classes=10)
    # 实际使用时应加载训练好的权重
    # model.load_state_dict(torch.load("slowfast.pt"))

    # 这里用随机数据模拟读取到的视频帧
    dummy_frames = np.random.randint(0, 255, size=(64, 256, 256, 3), dtype=np.uint8)
    clip = preprocess_frames(dummy_frames, T=32, spatial_size=224)
    predict(model, clip, class_names=[f"class_{i}" for i in range(10)])

preprocess_frames 中使用的均值和标准差是视频领域比较常见的配置,真实项目中应该替换成你自己训练数据集的统计值。运行这段代码后,会打印出概率最高的两个类别。因为是随机权重,预测结果没有实际意义,但流程是完整的。

4.5 使用 MMAction2 快速跑官方模型

如果只是想快速用官方 SlowFast 在公开数据集上训练,可以直接使用 OpenMMLab 的 MMAction2。安装时要注意 mmcv 版本与当前 PyTorch、CUDA 的匹配关系,具体以官方文档为准。

pip install openmim
mim install mmcv
pip install mmaction2

安装完成后,找到你本地 mmaction2 仓库或安装包中的配置目录,训练官方 SlowFast:

python tools/train.py \
    ${MMACTION2_HOME}/configs/recognition/slowfast/slowfast_r50_8x8x1_256e_kinetics400_rgb.py \
    --work-dir work_dirs/slowfast_r50

这个配置文件名在不同版本中可能会有调整,建议先查看本地 configs 目录下的实际文件名。官方配置文件里包含完整的模型结构、数据增强、优化器、学习率调度等参数,适合用于对比实验和生产基线。

5. 实验设计与效果评估

5.1 评估指标

动作识别最常用的指标是 Top-1 和 Top-5 准确率。Top-1 表示预测概率最高的类别是否正确,Top-5 表示真实类别是否出现在概率前五中。对于类别数量较多、类别间相似度较高的数据集,Top-5 通常更有参考意义。

还有一个容易忽略的指标是类别平均准确率。如果数据集中有的类别样本很多、有的类别样本很少,整体准确率会被多数类主导,此时类别平均准确率更能反映模型在小类上的表现。类别不均衡问题在自采数据里非常常见,训练时建议按类别分布去统计 confusion matrix,而不是只看一个总准确率。

统计模型计算量和参数量的简单方法:

from thop import profile

model = SimpleSlowFast(num_classes=10)
input_tensor = torch.randn(1, 3, 32, 224, 224)
flops, params = profile(model, inputs=(input_tensor,))
print(f"FLOPs: {flops / 1e9:.2f} G")
print(f"Params: {params / 1e6:.2f} M")

FLOPs 和参数量是模型复杂度的重要参考,部署时需要结合硬件算力评估吞吐量,而不是只看参数量。

5.2 对比实验与可视化

如果要在论文或技术方案中验证 SlowFast 的有效性,建议设计三组对比:

  • 单分支低速网络:把 Fast 路径去掉,只用 Slow 路径,观察运动信息丢失后精度下降多少。
  • 单分支高速网络:把 Slow 路径去掉,只用 Fast 路径,观察语义能力不足时的表现。
  • 完整 SlowFast 网络:双分支加侧向连接,保留强语义和运动信息。

三组实验使用完全相同的数据集、采样方式、优化器和训练轮数,这样差异才能归因于网络结构。另外一个常见做法是把侧向连接去掉,只在最后一层融合,用来验证侧向连接的价值。

结果分析时,可以结合 Grad-CAM 可视化辅助判断。视频版本的 Grad-CAM 比图像复杂一些,通常做法是先对时间维求平均,得到空间注意力热力图,再叠加到关键帧上。观察模型在预测“投篮”时是否关注到篮球和手臂区域,能帮你定位模型是学到运动线索还是仅仅根据静态背景做了记忆。

6. 工业落地与部署优化

6.1 ONNX 导出

工业落地很少直接把 PyTorch 模型部署到生产环境,通常先导出为 ONNX,再转换成 TensorRT 或 OpenVINO 等推理引擎。导出代码如下:

model = SimpleSlowFast(num_classes=10)
model.load_state_dict(torch.load("slowfast.pt"))
model.eval()

dummy_input = torch.randn(1, 3, 32, 224, 224)
torch.onnx.export(
    model,
    dummy_input,
    "slowfast.onnx",
    input_names=["input"],
    output_names=["logits"],
    opset_version=11,
    dynamic_axes={"input": {0: "batch", 2: "frames"}},
)
print("export onnx done")

导出时需要注意,如果后续要在 TensorRT 中使用固定尺寸优化,建议不要开过多 dynamic axis。 dynamic_axes 中把 frames 设置为动态后,推理时帧数可以变化,但 TensorRT 会为多个 shape 分别优化,可能增加显存占用。

6.2 TensorRT 加速

ONNX 模型导出后,可以用 TensorRT 的命令行工具转成 engine 文件。下面命令是一个常见示例:

trtexec \
    --onnx=slowfast.onnx \
    --saveEngine=slowfast.engine \
    --fp16

使用 FP16 后,推理速度通常会有明显提升,但精度可能会有小幅度下降。更保守的做法是先在验证集上对比 FP16 和 FP32 的 Top-1 准确率,误差在可接受范围内再上线。

如果视频长度比较长,还需要考虑切段策略。比如业务视频是 5 分钟,模型每次只处理 32 帧,那么可以按 1 秒一个窗口切段,相邻窗口保留 0.5 秒重叠,避免动作恰好被切分到两段中。多段预测结果可以用平均或投票融合成整条视频的类别。

6.3 生产环境注意事项

生产环境部署前需要确认几个问题:数据授权是否完整、业务类别是否与训练类别一致、输入视频编码格式是否统一。尤其是 H.264、H.265、VP9 等不同编码的视频,解码后的帧内容没有差异,但解码耗时不同,会影响整体吞吐量。建议上游统一转码或使用硬件解码。

模型上线时不要直接全量替换旧版本,建议采用灰度发布,先让 5% 到 10% 的流量打到新模型上,观察准确率和延迟指标,确认稳定后再逐步放量。同时在推理服务里记录输入视频 ID、模型版本、预测结果、置信度和响应时间,方便事后追溯和分析误报。

7. 常见问题与排查思路

实际开发和部署中,以下问题出现频率较高,整理成表格方便查阅:

问题现象 常见原因 解决思路
训练时显存溢出 输入帧数太多、空间分辨率太大、batch 过大 减小 T 或 H/W,使用梯度累积,开启混合精度
Slow 和 Fast 分支时间维不对齐 T 不是 alpha 的整数倍,或侧向连接 padding 设置不合理 保证 T 是 alpha 的倍数,打印 shape 检查
分布式训练时精度不稳定 每张卡 BatchNorm 统计量不一致 使用 SyncBatchNorm,或把 batch 调大
视频加载慢,GPU 利用率低 解码在 CPU 上串行完成 提前抽帧存帧目录,使用多进程预处理
模型在验证集上精度高,上线后误报多 训练数据与线上视频分布不一致 增加线上数据回流,持续迭代训练集
训练 loss 不下降 学习率设置不合理、标签错乱、输入预处理异常 先用少量样本过拟合,确认前向反向正常
FP16 推理精度下降明显 模型敏感、动态范围不足 回退 FP32,或使用混合精度校准

遇到问题不要急着改模型结构,先用小规模数据验证流程。比如固定 10 个视频样本,训练几个 epoch,如果 loss 能正常下降并过拟合,说明数据管线、模型和优化器都没问题,再逐步扩大数据规模。

8. 最佳实践与工程建议

从项目实战角度,我建议在开始训练前就把以下几个问题确定下来。

首先是数据采样规范。视频理解模型的输入是固定帧数的片段,但不同视频的长度差异很大。采样时建议先统一帧率,比如把视频重采样到 25fps,再按固定间隔取帧。不要直接按视频总帧数等分,因为不同视频时长差异会导致同一动作在不同样本里的时间跨度不一致。

其次是数据增强策略。SlowFast 对空间裁剪敏感,通常使用随机短边缩放和随机裁剪,时间维上可以做随机片段采样。水平翻转对于动作识别通常是有效的,但如果你的业务动作存在左右区分,比如“向左挥手”和“向右挥手”,就不要加水平翻转。

第三是模型加载预训练权重。Kinetics 预训练权重对工业小数据集帮助很大。训练时可以先冻结 Backbone 只训练分类头,跑通后解除冻结进行微调。这里的“冻结”不是指参数完全固定,而是先用较小的学习率适应新数据,避免预训练特征被破坏。

第四是实验管理。视频模型的训练周期长,建议为每次实验记录数据集版本、参数配置、seed、训练 loss、验证准确率和模型权重路径。自己搭建一个简单的实验记录脚本,或者在团队里使用 wandb 等工具,可以避免“这个模型之前效果不错但忘了用什么参数训出来的”这种问题。

关于代码组织,网络结构、数据管线、训练配置尽量解耦。入口脚本只负责组合,不负责具体实现。模型相关代码保持无业务逻辑,方便在不同项目间复用。数据增强和预处理放到独立模块,后续调整抖动策略时不需要动训练脚本。

9. 总结与学习路线

这篇内容完整梳理了 SlowFast 视频时序建模的核心思想、双分支设计和工程落地路径。理解它只需要抓住几个关键点:慢路径用低帧率大通道建模语义,快路径用高帧率小通道建模运动,侧向连接负责把运动信息逐层融入语义特征。掌握了这三个点,再去看官方代码就不会迷路。

推荐的下一步路线:先把简化版网络用随机数据跑通,再替换成 UCF101 或你业务中的小规模视频数据集,训练一个能收敛的 baseline;然后用 MMAction2 加载 Kinetics 预训练权重微调;最后再考虑 ONNX 导出和 TensorRT 加速。每一步都建立在前面已经验证的基础上,不要一上来就追大模型和大数据集。

视频理解领域后续还出现了 VideoMAE、Video Swin Transformer 等新方法,但 SlowFast 的“快慢双分支”思想在工业界依然有很强的参考价值,尤其是在算力受限、需要快速工程落地的场景下,它仍然是一个非常可靠的动作识别基线方案。

Logo

邀请您加入社区

更多推荐