做动作识别也有几年了,从最早的课程大作业一路做到工业项目落地,这个方向算是视频理解里最基础也最常被问起的一块。很多人一听“视频理解”就觉得高大上,其实说白了,动作识别就是让计算机看一段视频,判断画面里的人“在做什么”——是跑步、挥手、打篮球,还是摔倒。听起来简单,真的上手做一遍,坑比想象中多得多。

这篇文章我会从任务定义讲起,把主流的方法路线、选型思路、完整可复现的实操流程,还有我踩过的那些坑一次性说清楚。无论你是正在做计算机视觉大作业的学生,还是刚入门视频理解领域的研究生,又或者是想评估动作识别方案能不能落到业务里的工程师,这篇文章应该都能给你省下不少试错的时间。

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,跑通整个流程再说。

数据预处理的流程一般是这样:

  1. 把每个视频解码成帧序列。可以全量保存为jpg图片,也可以在线解码(用OpenCV或PyAV实时读取)。
  2. 统一所有视频的短边尺寸(比如256像素),保持宽高比不变。
  3. 训练时做随机裁剪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%多,这种情况很常见。

缓解手段按优先级排序:

  1. 使用更强的数据增强。随机裁剪、水平翻转之外,还可以加入颜色抖动、随机擦除、时间维度上的随机快慢扰动(SlowFast里用到的技巧)。
  2. 增加Dropout。在主干网络输出之后、分类层之前加一个dropout层,p=0.5。这个改动虽然微小,但往往能提升1-2个点。
  3. 减小模型容量。如果ResNet50过拟合严重,可以换ResNet18试试。
  4. 使用预训练模型。在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)上的预训练模型权重上微调,哪怕只是冻结前几层,效果远超从头训练。现在有社区发布的预训练模型可以直接下载,这比自己训练省出好几天时间。

动作识别只是视频理解的第一课。理解了时序信息如何与空间信息融合之后,接下来无论你做时序动作检测、视频描述、还是视频检索,都会觉得顺理成章很多。希望这篇文章能给你的项目开个好头。

Logo

邀请您加入社区

更多推荐