视频理解之动作识别:从原理到落地的完整实战指南
做动作识别也有几年了,从最早的课程大作业一路做到工业项目落地,这个方向算是视频理解里最基础也最常被问起的一块。很多人一听“视频理解”就觉得高大上,其实说白了,动作识别就是让计算机看一段视频,判断画面里的人“在做什么”——是跑步、挥手、打篮球,还是摔倒。听起来简单,真的上手做一遍,坑比想象中多得多。
这篇文章我会从任务定义讲起,把主流的方法路线、选型思路、完整可复现的实操流程,还有我踩过的那些坑一次性说清楚。无论你是正在做计算机视觉大作业的学生,还是刚入门视频理解领域的研究生,又或者是想评估动作识别方案能不能落到业务里的工程师,这篇文章应该都能给你省下不少试错的时间。
1. 动作识别到底在解决什么问题
1.1 任务定义与输入输出
动作识别(Action Recognition)的标准定义是:给定一段未分割的视频片段,判断这段视频中人物执行的动作类别。这里的“动作”概念很宽泛,可以是原子动作如“挥手”“站立”,也可以是复合活动如“打篮球”“做菜”。
从数学角度看,模型要做的事是学习一个映射函数:输入是视频帧序列 (X = {x_1, x_2, ..., x_T}),其中 (x_t \in \mathbb{R}^{H \times W \times 3}),输出是动作类别的概率分布 (P(y|X))。我们通常取概率最大的类别作为预测结果。
这个任务和图像分类最大的区别就在“时间”上。图像分类只需要处理空间信息,而动作识别必须同时处理空间维度和时间维度。一个人“挥手”这个动作,单看某一帧,模型很难区分是在挥手还是在招手;只有把多帧连贯起来看,才能捕捉到动作的动态模式。
1.2 和图像分类的本质区别
我在带新人做项目时经常发现,大家误以为动作识别就是把图像分类模型逐帧跑一遍然后取平均。这个思路听起来合理,实际效果却很一般。原因很简单:逐帧分类完全没有利用帧与帧之间的时序依赖关系。
举个例子,“倒水”和“喝水”这两个动作,如果只看静态帧,画面里可能都是一个人手拿杯子,视觉特征非常相似。但放到时间轴上,倒水时手是从水瓶移向杯子,喝水时手是从杯子移向嘴边,这两种运动轨迹完全不同。这就是时序信息(temporal dynamics)的价值。
另一个区别是数据量。图像分类数据集动辄上百万张图片,而视频数据集UCF101只有1.3万个视频,Kinetics-400也只有约30万个视频片段。视频数据标注成本极高,一个视频往往需要多个标注者确认,还要处理标签歧义(比如“跑步”和“慢跑”的边界到底在哪),所以数据规模和标注质量都远不如图像领域。
1.3 核心难点与挑战
动作识别难在哪?我总结成几个方面,新手踩坑基本都逃不出这几条:
类内差异大,类间差异小 。同样是“打篮球”,职业比赛和小区篮球场的动作幅度、背景环境差异巨大;而“跑步”和“快走”在视觉上又极其相似。模型既要学出足够的类内泛化能力,又要捕捉类间的细微差别。
背景和相机运动的干扰 。一段“跑步”的视频如果镜头跟着人平移,光流特征里全是背景的运动信息,很容易干扰模型对“人体动作”的建模。这也是为什么很多工业项目要加人体检测器,先框出人再识别动作。
多尺度问题 。人的尺度在视频中变化很大,远景里人只占几十个像素,近景里人充满整个画面。这对模型的尺度鲁棒性要求很高。
时长不定 。一个“打哈欠”动作持续1秒,“包饺子”可能持续几分钟。模型输入通常固定帧数(比如8帧或16帧),如何从长视频中采样出有代表性的片段,是个容易被忽视但影响很大的细节。
1.4 值得落地的应用场景
动作识别不是只能在论文里跑实验的技术,它在实际业务里有很多刚需场景:
- 智慧安防 :识别打架、摔倒、异常聚集等行为,在监控场景中实时预警。
- 体育分析 :统计运动员的投篮、挥拍、跑动等动作,辅助训练和比赛复盘。
- 人机交互 :通过手势或身体动作控制设备,比如体感游戏、智能家居。
- 医疗康复 :识别患者的康复训练动作是否标准,辅助医生评估恢复进度。
- 视频内容理解 :视频网站自动打标签、审核违规内容、提取精彩片段。
我见过不少团队直接用动作识别模型去做“摔倒检测”,效果也还行。但要提醒一句:动作识别解决的是“这个动作是什么”的问题,如果想做“这个动作是否发生”的异常检测,那属于另一个任务范畴(时序动作检测),实现思路差别很大。
2. 方法选型:从手工特征到 Transformer,主流路线怎么选
2.1 早期思路与双流网络
2014年之前,动作识别领域还是手工特征(如iDT、HOG/HOF)的天下。这类方法先提取视频中的密集轨迹,再在轨迹周围计算梯度直方图等特征,最后用SVM分类。效果尚可,但计算量大,且严重依赖光流场(Optical Flow)的质量。
双流网络(Two-Stream Network)是深度学习在动作识别上的里程碑之作。它的核心想法很直接:人类识别动作时,既看外观(人长什么样、拿着什么),也看运动模式(肢体怎么动)。因此双流网络用两个独立的卷积网络,一个吃RGB帧,一个吃连续几帧的光流图,最后把两个分支的预测分数融合。
这个方案在当时的精度提升非常大,关键原因是光流输入显式地编码了运动信息,大大减轻了模型学习时序动态的负担。但它也有硬伤:光流计算非常耗时(OpenCV的Farneback算法跑一帧要几十毫秒),训练和推理都需要两步,工程部署很不方便。
2.2 3D卷积:从C3D到I3D
既然2D卷积是“空间特征提取器”,那能不能直接用3D卷积同时处理空间和时间?C3D(Convolutional 3D)就是这么做的。它在每个卷积层同时滑动空间维度和时间维度,理论上可以端到端地学习时空特征。
C3D在UCF101上取得了不错的成绩,但有个致命问题:模型参数量巨大,训练速度慢,而且由于当时数据集规模太小,3D卷积的优势并没有完全发挥出来。
真正让3D CNN成为主流的是I3D(Inflated 3D ConvNet)。它的做法很聪明:把在ImageNet上预训练好的2D卷积网络“膨胀”成3D,也就是把卷积核 (K \times K) 变成 (K \times K \times K),并沿着时间轴重复初始化权重。这样既借力了图像预训练模型的特征表达,又能处理视频的时序信息。
I3D在Kinetics-400上训练后,迁移到UCF101和HMDB51精度提升非常明显,一举成为领域标杆。直到今天,很多顶会论文依然用I3D作为特征提取器。
2.3 工程常用方案:TSN与TSM的取舍
3D CNN效果好,但计算量大,训练慢。在计算资源有限的场景下(比如课程作业、创业团队),工程上更常用的是TSN(Temporal Segment Networks)和TSM(Temporal Shift Module)。
TSN的思路是把视频均匀分成K段,每段随机采样一帧(或一小段),然后让2D CNN独立处理每一帧,最后对K个帧的特征做融合。它本质上是一个“分段采样+特征聚合”的框架,计算量和纯2D CNN一样小,却能覆盖整个视频的内容。这个方案很适合做baseline,速度极快,调参方便。
TSM则在TSN的基础上更进一步,它通过一种叫“时间移位”(Temporal Shift)的零参数操作,把一部分通道沿着时间轴平移一个位置,让2D卷积在推理时也能“看到”前后帧的信息。换句话说,它用“作弊”的方式模拟了3D卷积的时序建模能力,但计算量几乎没有增加。
实际项目中,TSM是性价比极高的选择。我在一个安防项目里用TSM替换I3D做摔倒识别,推理速度提升了将近4倍,精度只掉了不到两个点,非常划算。
2.4 Transformer路线:TimeSformer与VideoMAE
近两年,Transformer架构也进入了视频理解领域。TimeSformer将视频帧划分成时空patch(类似ViT的做法),然后分开做时间注意力和空间注意力,在多个数据集上超过了I3D。
VideoMAE则把MAE(Masked Autoencoder)的思路扩展到视频上,随机遮挡大部分帧和空间patch,只让模型从剩余部分重建被遮挡的内容,用自监督方式在大量无标注视频上预训练。它学到的特征非常强,下游任务只要做轻量微调就能达到不错的精度,是目前学术研究的一个主流方向。
不过说实话,Transformer路线对算力和显存的要求更高,而且训练技巧比CNN复杂。如果只是想快速完成一个课程项目或者跑通业务demo,我的建议是先把CNN路线搞扎实,再考虑升级到Transformer。
2.5 选型建议汇总
我做了个对比表,方便你根据自己的场景快速选型:
| 方案 | 计算量 | 推理速度 | 精度 | 工程难度 | 适合场景 |
|---|---|---|---|---|---|
| 2D CNN + TSN | 低 | 快 | 中 | 低 | 课程作业、快速baseline |
| 双流网络 | 中 | 慢(光流耗时) | 中高 | 中 | 学术对比实验 |
| C3D / 3D ResNet | 高 | 慢 | 中高 | 中 | 有GPU资源的研究项目 |
| I3D | 很高 | 慢 | 高 | 中 | 学术基准、数据充足的场景 |
| TSM | 低 | 快 | 高 | 低 | 工业落地、实时推理 |
| TimeSformer / VideoMAE | 很高 | 慢 | 很高 | 高 | 前沿研究、有预训练模型可用 |
这个表不是绝对的,具体选型还要考虑你的数据量、标签噪声、场景复杂度和硬件限制。后面我会详细讲实操,就拿TSN作为主线,因为它最容易跑通,也最容易理解。
3. 实操:从零训练一个人体动作识别模型
3.1 数据集选择与预处理
动作识别最常用的公开数据集是UCF101(101类,约1.3万视频)和HMDB51(51类,约7000视频)。UCF101类别覆盖比较广,包括人机交互、人体动作、运动等,而且视频时长较短,训练门槛低,非常适合作为学习用的“练手集”。
如果你有条件访问Kinetics-400或Something-Something,效果会更好,但体积很大(Kinetics-400约300GB),对学生党不友好。我建议初期先用UCF101的子集或者完整UCF101,跑通整个流程再说。
数据预处理的流程一般是这样:
- 把每个视频解码成帧序列。可以全量保存为jpg图片,也可以在线解码(用OpenCV或PyAV实时读取)。
- 统一所有视频的短边尺寸(比如256像素),保持宽高比不变。
- 训练时做随机裁剪224x224,随机水平翻转,做颜色扰动;测试时用中心裁剪。
全量保存为图片的好处是训练时不用反复解码视频,速度更快,但会占用较多磁盘空间。UCF101全量抽帧大约要几十GB,如果你的磁盘紧张,直接用OpenCV在线解码也行,就是训练时会慢一些。
3.2 视频帧采样策略踩坑记录
采样策略是整个流程里最容易被忽视、却对最终精度影响最大的部分。这里我想重点说清楚。
假设我们的模型输入是8帧,每帧224×224。如果从一个10秒的视频里随机取8帧,可能取不到动作发生的关键时刻;如果全取连续帧,又会让模型关注的时序视野太窄。TSN的做法是:把视频均匀分成8段,每段里随机采样一帧,这样8帧能覆盖整个视频的内容,采样到的帧也具备多样性。
如果采样时发现视频帧数少于所需帧数,我习惯先做时间维度的线性插值补齐,而不是简单重复最后一帧。重复帧会让模型误以为画面长时间静止,学到错误的时序特征。
测试和推理阶段,我会把视频均匀分成8段,每段取中间帧(或做三次随机采样取平均),得到一个更稳定的预测结果。这个技巧能让Top-1精度提升1-2个百分点,几乎零成本。
3.3 模型搭建:用PyTorch实现一个TSN基线
下面我给出一个实用且完整的TSN基线代码。我用
torchvision
里的ResNet18作为2D主干网络,在特征层之后做时序平均池化,再接线性分类器。这个模型在网络结构上足够简单,但在UCF101上能做到约80%以上的Top-1准确率(需要预训练权重)。
import torch
import torch.nn as nn
import torchvision.models as models
class TSN(nn.Module):
def __init__(self, num_classes, num_segments=8, base_model='resnet18'):
super().__init__()
# 加载ImageNet预训练模型
if base_model == 'resnet18':
backbone = models.resnet18(weights=models.ResNet18_Weights.IMAGENET1K_V1)
elif base_model == 'resnet50':
backbone = models.resnet50(weights=models.ResNet50_Weights.IMAGENET1K_V1)
else:
raise ValueError('Unsupported backbone')
# 去掉最后的全局池化和分类层
self.features = nn.Sequential(*list(backbone.children())[:-2])
self.global_pool = nn.AdaptiveAvgPool2d((1, 1))
# 输出分类
self.fc = nn.Linear(backbone.fc.in_features, num_classes)
self.num_segments = num_segments
def forward(self, x):
# 输入x的形状: (B, num_segments, C, H, W)
batch_size, num_segments, C, H, W = x.shape
# 把片段维度合并到batch维度一起过主干网络
x = x.view(batch_size * num_segments, C, H, W)
feat = self.features(x)
feat = self.global_pool(feat)
feat = feat.view(batch_size, num_segments, -1)
# 时序平均池化
feat = feat.mean(dim=1)
out = self.fc(feat)
return out
这段代码里有个细节值得说明:为什么把8帧合到batch维度一起过主干网络?因为2D CNN本身不知道时间维度的概念,把8帧当成8张独立图片处理,计算效率最高,而且ResNet的预训练权重可以直接复用,不需要重新学习特征提取器。时序信息则完全靠后面的平均池化层来聚合。
如果你想进一步引入TSM的时间移位思想,可以在每个残差块前对输入特征做通道维度的shift。不过这里我先不展开TSM的完整实现,避免篇幅过长,后面有问题可以在评论区交流。
3.4 完整训练流程与关键参数
训练流程里,数据加载部分我单独提一下,因为动作识别的数据loader比图像分类复杂。关键逻辑是:读视频 -> 解码 -> 均匀分段 -> 每段取帧 -> 空间变换 -> 组装成张量。
我用一个简化的伪代码来展示核心逻辑:
class VideoDataset(Dataset):
def __init__(self, video_paths, labels, num_segments=8, is_train=True):
self.video_paths = video_paths
self.labels = labels
self.num_segments = num_segments
self.is_train = is_train
def __len__(self):
return len(self.video_paths)
def __getitem__(self, idx):
frames = self.load_frames(self.video_paths[idx]) # 返回一列帧
total_frames = len(frames)
# 把帧序号均匀分成num_segments组
segment_size = total_frames / self.num_segments
selected_idx = []
for i in range(self.num_segments):
start = int(i * segment_size)
end = int((i + 1) * segment_size)
if self.is_train:
# 训练时随机取一帧
pick = random.randint(start, max(start, end - 1))
else:
# 测试时取中间帧
pick = (start + end) // 2
selected_idx.append(pick)
# 读取对应帧并做空间变换
...
return clip_tensor, self.labels[idx]
训练参数方面,我的常用配置如下:
- 优化器:SGD,momentum=0.9,weight_decay=1e-4
- 初始学习率:0.01(batch size为32时),用余弦退火调度(cosine annealing)逐步降至0
- Batch size:32(ResNet18 + 8帧 + 224尺寸,大约需要8-10GB显存)
- 训练轮数:UCF101上30-50轮足够收敛,官方TSN通常训练到80轮(但那是更大模型)
- 损失函数:交叉熵损失
训练过程每隔一个epoch评估一次测试集精度,保存精度最高的权重。实际训练中我见过很多人不保存最优权重,最后模型过拟合了还在用最后一个epoch的权重去测试,效果差很多。这个坑一定要避免。
3.5 评估指标与结果分析
动作识别领域最常用的评估指标是Top-1准确率(预测概率最高的类别是否匹配)和Top-5准确率(预测概率前五的类别是否包含真实类别)。学术论文里一般报告这两个指标。
除了准确率,我还建议你画一下混淆矩阵。通过混淆矩阵可以直观看到哪些类别容易互相混淆,比如UCF101里的“PlayingGuitar”和“PlayingSitar”、“HorseRiding”和“BikeDriving”,经常被模型搞混。分析混淆矩阵能帮你定位模型到底在视觉上忽略了什么信息。
我曾用上述TSN基线在UCF101上训练,最终Top-1准确率在80%左右(ResNet18主干)。如果换成ResNet50主干,能到85%左右。这个成绩在学术实验里不算高,但作为baseline和课程作业已经完全够用。
推理速度方面,ResNet18 + 8帧 + 224尺寸,在单张V100 GPU上可以达到每秒处理约300个采样片段。如果用TensorRT加速,还能再快一截。
4. 常见问题与排查技巧实录
4.1 训练不收敛怎么办
这是新手问得最多的问题。现象是loss不降,或者准确率一直卡在随机水平。我的排查顺序如下:
先检查数据。把batch里的帧可视化出来,确认采样后的图片确实包含人体动作,而不是一堆黑屏或静止画面。UCF101有很多视频是镜头快速切换的,采样策略不合理时,8帧可能来自完全不同的镜头,模型根本学不到连贯的动作信息。
然后检查预处理。归一化是否正确?我见过有人把像素值除以255之后再减均值,结果均值设置用的是ImageNet统计量,值域对不上,导致模型一直震荡。确认你的归一化方案和预训练权重一致。
最后检查学习率和batch size的匹配关系。学习率0.01配上batch size 16,经常会出现loss爆炸。一般batch size减小,学习率也要跟着降。我惯用经验是batch size翻倍,学习率翻倍;batch size减半,学习率减半。
4.2 过拟合严重怎么办
视频模型参数量大,数据量又少,过拟合几乎是必然的。训练集准确率90%多、测试集只有60%多,这种情况很常见。
缓解手段按优先级排序:
- 使用更强的数据增强。随机裁剪、水平翻转之外,还可以加入颜色抖动、随机擦除、时间维度上的随机快慢扰动(SlowFast里用到的技巧)。
- 增加Dropout。在主干网络输出之后、分类层之前加一个dropout层,p=0.5。这个改动虽然微小,但往往能提升1-2个点。
- 减小模型容量。如果ResNet50过拟合严重,可以换ResNet18试试。
- 使用预训练模型。在ImageNet上预训练的权重能显著改善视频模型的过拟合问题,因为主干网络已经学到了足够的视觉特征。
4.3 类别不均衡怎么处理
动作识别数据集往往天然不均衡。UCF101里有些类别的视频数量是其他类别的两倍,这种偏差虽然不算剧烈,但也会让模型偏向高频类别。
最简单的做法是在采样时做类别均衡,也就是每个epoch里从样本量最少的那个类别的数量为基准,对高频类别做下采样。这个方法实现简单,效果立竿见影。
如果不想损失训练数据量,也可以用class-weighted loss,根据每个类别样本数量的倒数给损失加权。这个方法不用改动数据loader,但调参时要注意学习率可能要相应调小。
4.4 推理速度慢怎么优化
如果你部署到边缘设备或者RTSP实时视频流,推理速度就是硬指标。优化的方向有几个:
第一个方向是减少输入帧数。从8帧降到4帧,推理时间几乎减半,精度通常只降1-2个点。这个trade-off在很多场景下非常划算。
第二个方向是换更轻量级的主干网络。比如用MobileNetV3替代ResNet,或者换TSM这类零参数时序建模模块,保持精度同时大幅压缩计算量。
第三个方向是模型量化。用TensorRT做FP16或INT8量化,在V100或T4上可以再快2-3倍。不过量化对精度的影响需要实测验证,有些动作类别边界模糊,量化后性能下降会比较明显。
4.5 动作边界模糊如何解决
标准的动作识别假设输入视频是“已经切割好的动作片段”,但真实场景里动作发生的起止时间点什么都不好判断。比如监控视频里一个人走路走了一半开始打电话,到底在哪里算“开 始打电话”?这就是时序动作检测(Temporal Action Detection)的范畴,比纯动作识别复杂得多。
如果你被迫用一个定长窗口做滑动窗口推理,我建议把窗口设置的比目标动作平均时长略短一些,stride设成窗口长度的一半,这样能把动作的关键部分切进来。我在摔倒检测项目里就是用这个策略,配合后处理做时序平滑(抑制单帧抖动),可以把误检率降下来不少。
这里有个通用技巧:不要直接用单窗口的预测结果去报警,而是取相邻3-5个窗口预测分数的平均,再做阈值判断。这个“时序平滑”的trick在几乎所有视频任务里都通用,能显著提升鲁棒性。
5. 一些个人经验与后续方向
我的个人体会是,动作识别入门容易、精通难。入门只需要掌握2D CNN + 时序聚合这一条技术路线,跑通一个baseline,理解采样、训练、评估的基本流程,就能应付大部分课程作业和应用级需求。但要做到业务可用,就必须花大量时间在数据清洗、采样策略优化、精度和速度之间找平衡这些“脏活累活”上。
如果这篇文章的读者正在犹豫要不要做这个方向,我的建议是从TSN开始。首先在自己熟悉的数据集上跑通baseline,然后尝试把模型换成TSM,观察精度和训练速度的变化,再去理解3D CNN和Transformer方案的改进动机。这个路径能让你对视频理解领域的演进脉络建立起直观的认知,而不是停留在Paper的抽象描述里。
最后分享一个我在多个项目中验证过的小技巧:如果你只有少量标注数据(比如几百个视频),不要直接随机初始化训练深度模型,更不要直接加载ImageNet预训练后冻结主干——试着在大型视频数据集(如Kinetics-400)上的预训练模型权重上微调,哪怕只是冻结前几层,效果远超从头训练。现在有社区发布的预训练模型可以直接下载,这比自己训练省出好几天时间。
动作识别只是视频理解的第一课。理解了时序信息如何与空间信息融合之后,接下来无论你做时序动作检测、视频描述、还是视频检索,都会觉得顺理成章很多。希望这篇文章能给你的项目开个好头。
更多推荐

所有评论(0)