视频行为识别是计算机视觉中一个应用面很广的方向,比如安防场景的异常行为检测、体育动作分析、工厂流水线上的操作规范识别等。I3D(Inflated 3D ConvNet)是目前最经典的视频识别骨干网络之一,它把 2D 的 Inception-V1 网络逐层膨胀成 3D 结构,卷积核在时间和空间三个维度上同时滑动,天然适合捕捉动作的时序信息。官方提供了在 Kinetics-400 上预训练好的权重,我们要做的就是把这份知识迁移到自己的数据集上。这篇文章完整梳理一遍微调流程,包括权重加载、分类头替换、数据管道搭建和训练细节,照着做基本能一次跑通。

一、I3D 模型结构与预训练权重的处理
I3D 的核心思想是参数膨胀:把一个 2D 卷积核尺寸的权重直接复制堆叠 N 份得到 3D 卷积核,这样既继承了 ImageNet 预训练的视觉特征,又能处理视频输入。常用的开源实现有 dpc(piergiaq)和 videoclassification 两个仓库,前者代码简洁、适合二次开发,社区用得最多。模型输出层是一个 VideoClassifier 模块,最后一层全连接输出 400 维的 Kinetics 类别分数。
微调的第一步是替换分类头。假设我们的自定义数据集有 10 个类别,就需要把最后的全连接层换成输出 10 维的新层。由于新层是随机初始化的,学习率也应该和其他层区分开。权重加载时要注意 PyTorch 的 load_state_dict 默认 strict 模式会因为新旧结构不一致而报错,处理办法是先加载骨干部分的权重,再手动替换分类层:
import torch
import torch.nn as nn
from pytorch_i3d import InceptionI3d
model = InceptionI3d(num_classes=400, in_channels=3)
# 加载 Kinetics 预训练权重,忽略缺失和多余的键
state_dict = torch.load('rgb_imagenet.pt', map_location='cpu')
model.load_state_dict(state_dict, strict=False)
# 替换分类头为自定义类别数
NUM_CLASSES = 10
model.logits = nn.Linear(768, NUM_CLASSES)
这样骨干网络的 768 维特征被保留下来,新分类头负责映射到 10 个类别。有一点容易被忽略:如果使用光流双流版本,光流分支的输入通道数要设为 2,且预训练权重要选 flow 版本而不是 rgb 版本,两者不能混用,否则收敛会非常慢甚至不收敛。
二、视频数据管道:采样、增强与批处理
视频数据不能像图片那样整段读进显存,必须做帧采样。I3D 标准输入是 16 帧连续片段,分辨率 224x224。实践中常用的策略有两种:训练时在视频时间轴上随机取一个起点,连续截取 16 帧,这样每次看到的片段不同,相当于一种天然的数据增强;验证和测试时则均匀采样多个片段,分别推理后取分数平均,结果更稳定。
数据增强方面,空间维度的随机裁剪、水平翻转都适用,但要注意竖直翻转通常没有意义(人倒立做动作的场景很少见)。时序维度可以做轻微的抖动,比如起点加一个随机偏移。下面给出一个简化版的 Dataset 实现:
import cv2
import numpy as np
from torch.utils.data import Dataset
class VideoDataset(Dataset):
def __init__(self, samples, num_frames=16, size=224, training=True):
# samples 是 (视频路径, 标签) 的列表
self.samples = samples
self.num_frames = num_frames
self.size = size
self.training = training
def __getitem__(self, idx):
path, label = self.samples[idx]
cap = cv2.VideoCapture(path)
frames = []
while True:
ret, frame = cap.read()
if not ret:
break
frames.append(frame)
cap.release()
total = len(frames)
if self.training:
start = np.random.randint(0, max(1, total - self.num_frames))
else:
start = (total - self.num_frames) // 2
clip = frames[start:start + self.num_frames]
# 统一到固定帧数,短视频循环填充
while len(clip) < self.num_frames:
clip.append(clip[-1])
processed = []
for f in clip:
f = cv2.cvtColor(f, cv2.COLOR_BGR2RGB)
if self.training:
# 随机裁剪到 224
h, w = f.shape[:2]
top = np.random.randint(0, h - self.size)
left = np.random.randint(0, w - self.size)
f = f[top:top + self.size, left:left + self.size]
if np.random.rand() > 0.5:
f = np.flip(f, axis=1) # 水平翻转
else:
f = cv2.resize(f, (self.size, self.size))
processed.append(f)
# 转成 (C, T, H, W),归一化到 [-1, 1]
data = np.stack(processed).astype(np.float32) / 255.0
data = (data - 0.5) * 2.0
data = data.transpose(3, 0, 1, 2)
return torch.from_numpy(data.copy()), label
特别要注意 I3D 的输入维度顺序是 (N, C, T, H, W),时间维在通道后面,这一点和不少其他视频模型不同,transpose 错了维度模型不报错但精度会大幅下降。归一化建议用 [-1, 1] 区间,与预训练时保持一致。另外用 OpenCV 逐帧解码非常慢,数据量大时建议提前把视频抽帧存成 jpg 或者转成解码更快的格式,否则训练瓶颈会卡在数据加载上,GPU 利用率常常不到三成。
三、训练配置与常见问题处理
训练配置上有几个经验值值得参考。优化器用 SGD 配合动量 0.9,骨干网络学习率设为 0.001,新分类头可以放大 10 倍用 0.01,因为新头是随机初始化的,需要更快的学习速度。学习率衰减用余弦退火或者每 20 个 epoch 衰减一半都可以。batch size 受显存限制,16 帧的 I3D 比较吃显存,一张 8G 卡大概只能放 4 到 8 个样本,配梯度累积可以等效扩大 batch:
import torch.optim as optim
from torch.optim.lr_scheduler import CosineAnnealingLR
model = model.cuda()
backbone_params = [p for n, p in model.named_parameters() if 'logits' not in n]
head_params = model.logits.parameters()
optimizer = optim.SGD([
{'params': backbone_params, 'lr': 1e-3},
{'params': head_params, 'lr': 1e-2},
], momentum=0.9, weight_decay=1e-7)
scheduler = CosineAnnealingLR(optimizer, T_max=50)
ACCUM_STEPS = 4
for epoch in range(50):
model.train()
optimizer.zero_grad()
for i, (inputs, labels) in enumerate(train_loader):
inputs = inputs.cuda(non_blocking=True)
labels = labels.cuda(non_blocking=True)
out = model(inputs) # 输出 (N, num_classes, 1, 1)
out = out.mean(dim=[2, 3]) # 抹掉空间维
loss = nn.functional.cross_entropy(out, labels)
(loss / ACCUM_STEPS).backward()
if (i + 1) % ACCUM_STEPS == 0:
optimizer.step()
optimizer.zero_grad()
scheduler.step()
print(f'epoch {epoch} loss {loss.item():.4f}')
常见报错里最典型的是显存溢出(CUDA out of memory),解决办法按优先级排:减小 batch size、把 num_frames 从 16 降到 8(会牺牲一点精度)、开启混合精度训练(amp)。其次是验证集精度剧烈波动,多半是学习率太大或者数据集类别不均衡,后者可以按类别加权计算交叉熵损失。还有一点,I3D 的输出形状是 (N, num_classes, 1, 1),直接喂给交叉熵会维度不匹配,记得先做上面的 mean 或者 squeeze 处理。
最后谈谈数据量的问题。经验上每个类别至少要有 50 段以上的视频,微调才能稳定收敛;数据更少时建议冻结前面的 Mixed 层,只训练后面几层加分类头,虽然上限低一些但不容易过拟合。评估时用 top-1 加混淆矩阵一起看,能快速发现哪几类动作容易混淆,再针对性补充训练样本,往往比盲目加 epoch 更有效。整个流程跑通后,把训练和推理的采样策略、归一化方式固化成同一套代码,部署阶段的精度就能和验证结果保持一致。