☰
UCF101视频分类实战:3D CNN与CNN+RNN双模型源码解析
2026/10/1 15:27:04 网站建设 项目流程

简介:这份源码面向计算机视觉与深度学习方向的研究人员和开发者,围绕UCF101数据集提供3D CNN与CNN+RNN两条技术路线的视频动作识别实现,适合具备一定Python与深度学习基础、希望快速上手视频分类实验的读者。压缩包共82个文件、约19.35MB,包含12个py脚本与12个ipynb笔记本用于模型定义、训练与测试,16个npy和14个pkl文件保存训练损失、评分曲线与预测结果,另有11张png图表、1个gif演示动画及readme说明文档。目录按Conv3D、CRNN、ResNetCRNN三个模块组织,各自配有独立的检查预测脚本与输出结果,便于横向对比不同架构的表现。目前已有360人学习。读者可据此复现从数据加载、时空特征提取到长序列建模的完整流程,并借助损失曲线与错误预测样本分析模型改进方向。

1. 从一段“跑不动”的 UCF101 训练说起:这套源码到底能帮你省掉什么

如果你做过视频动作识别,大概率经历过这个场景:数据集下好了,UCF101 的 101 个类别、13320 段视频躺在硬盘里,但真要把它们喂进网络,光是抽帧、对齐、切分训练集就够折腾一整天。更别提 3D CNN 的输入张量是(batch, frames, H, W, C)这种五维结构,和图像分类完全不是一个套路,很多人卡在数据管道上就放弃了。这套基于 3D CNN 和 CNN+RNN 的 UCF101 视频分类源码,解决的正是从“原始 avi 文件”到“可训练模型”再到“推理输出类别”的完整链路。它适合两类人:一是想快速跑通视频分类 baseline 的学生和转行者,二是需要拿一个能改、能对比双模型结构的从业者。源码把两条技术路线放在同一个工程里,3D CNN 直接对时空立方体做卷积,CNN+RNN 则用 CNN 抽帧特征再交给 RNN 建模时序,两条路各有取舍,后面会拆开讲。你不需要从零写 Dataset,也不用纠结 UCF101 的官方 split 怎么读,这些在源码里都有对应实现。

2. 两条技术路线怎么选:3D CNN 与 CNN+RNN 的结构差异和输入约定

2.1 3D CNN 的时空卷积到底卷了什么

3D CNN 的核心思路是把视频当成一个三维体数据,卷积核在时间、高度、宽度三个维度上同时滑动。以常见的 C3D 结构为例,输入是(batch, 16, 112, 112, 3),表示一次取 16 帧、每帧缩放到 112×112、3 通道。卷积核尺寸是(3, 3, 3),也就是在时间轴上一次看 3 帧,空间上 3×3。这样第一层就能捕捉到短时运动,比如挥手、跳跃这类动作在连续几帧里的变化。源码里 3D CNN 分支通常堆叠 4 到 5 个卷积块,每个块后接池化,时间维度逐步压缩,最后用全局平均池化或全连接输出 101 维分类向量。

为什么选 3D CNN?因为它对短时动作的建模是“原生”的,不需要额外设计时序模块。但代价也很直接:参数量大、显存吃紧。16 帧 112×112 的输入,单样本就是 16×112×112×3≈60 万个体素值,batch size 稍微大一点就爆显存。所以源码里一般会把帧数控制在 16 或 32,分辨率压到 112 或 128,这是工程上的折中。

2.2 CNN+RNN 的“抽帧 + 时序”组合逻辑

CNN+RNN 走的是另一条路:先用一个 2D CNN(比如 ResNet 或自己搭的卷积网络)对每一帧单独抽特征,得到一串特征向量序列,再送进 LSTM 或 GRU 做时序建模。输入形状变成(batch, T, H, W, C),其中 T 是帧数,CNN 对每个时间步独立处理,输出(batch, T, feature_dim),RNN 再在这个序列上跑。

这种结构的好处是 CNN 部分可以复用图像分类的预训练权重,收敛更快,显存占用也比 3D CNN 低——因为同一时刻只处理一帧或少量帧。但它的短板在于:CNN 抽的是单帧空间特征,帧与帧之间的运动信息完全靠 RNN 去“猜”,对于需要精细时空建模的动作(比如区分“打开盒子”和“关上盒子”),效果可能不如 3D CNN。

2.3 两条路线的参数对比与选型建议

对比项3D CNNCNN+RNN
输入张量(B, 16, 112, 112, 3)(B, T, 224, 224, 3)
时序建模方式3D 卷积核直接建模RNN 在特征序列上建模
显存占用高,batch 通常 8~16中,batch 可到 32
预训练权重较少,常从头训可复用 2D CNN 权重
适合动作类型短时、局部运动长时、依赖帧间关系

我一般会建议:如果你手头显存有限、又想快速看到准确率上升,先跑 CNN+RNN;如果你追求更高的时空建模上限、且能接受更长的训练时间,再上 3D CNN。源码把两条路放在一起,正好方便你做 A/B 对比。

3. 把 UCF101 喂进网络:数据管道、抽帧脚本与训练入口

3.1 数据目录结构与官方 split 的读取

UCF101 官方提供三个 split,每个 split 里训练集和测试集按类别分文件夹存放。源码通常要求你把数据整理成这样的结构:

UCF101/ ├── train/ │ ├── ApplyEyeMakeup/ │ │ ├── v_ApplyEyeMakeup_g01_c01.avi │ │ └── ... │ └── ... └── test/ ├── ApplyEyeMakeup/ │ └── ... └── ...

读取时用torchvision.datasets.DatasetFolder或自定义 Dataset 都可以。关键是类别到索引的映射要固定,否则训练和推理的标签会对不上。源码里一般会保存一个class_to_idx.json,训练完顺手存下来,推理时直接加载。

3.2 抽帧与预处理:从 avi 到张量

视频不能直接送进网络,必须先抽帧。常见做法是用 OpenCV 按固定间隔取帧,比如每段视频取 16 帧或 32 帧。下面是一个可抄的抽帧函数:

import cv2 import numpy as np def extract_frames(video_path, num_frames=16, size=(112, 112)): cap = cv2.VideoCapture(video_path) total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) # 均匀采样,避免开头结尾冗余帧 indices = np.linspace(0, total - 1, num_frames, dtype=int) frames = [] for i in range(total): ret, frame = cap.read() if not ret: break if i in indices: frame = cv2.resize(frame, size) frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) frames.append(frame) cap.release() # 不足帧数时循环补齐 while len(frames) < num_frames: frames.append(frames[-1]) return np.stack(frames) # (num_frames, H, W, 3)

逻辑说明:np.linspace保证在整段视频上均匀取帧,而不是只取前 16 帧。cv2.cvtColor把 BGR 转成 RGB,因为 PyTorch 预训练模型期望 RGB。不足帧数时用最后一帧补齐,避免张量形状不一致。参数num_frames和size要和模型输入对齐,3D CNN 一般用 16 帧 + 112×112,CNN+RNN 可以用 32 帧 + 224×224。

3.3 训练入口与关键超参

源码的训练脚本通常长这样:

python train.py \ --model 3dcnn \ --data_root ./UCF101 \ --split 1 \ --frames 16 \ --batch_size 8 \ --lr 1e-3 \ --epochs 50 \ --gpu 0

参数说明:--model切换 3dcnn 或 cnn_rnn;--split选官方三个 split 之一;--frames是抽帧数;--batch_size在 3D CNN 下建议 8 或 16,CNN+RNN 可以开到 32;--lr初始学习率,3D CNN 常用 1e-3 配合 StepLR,CNN+RNN 可以更小。训练过程中重点看验证集准确率,UCF101 上 3D CNN 从头训一般能到 50%~60%,CNN+RNN 用预训练 CNN 能到 70% 以上,具体取决于帧数和分辨率。

4. 避坑与排查:UCF101 训练里最容易翻车的五个地方

4.1 显存爆炸:现象是 CUDA out of memory

现象:刚启动训练就报RuntimeError: CUDA out of memory,或者跑几个 batch 后崩掉。原因通常是 3D CNN 的输入张量太大,或者 batch size 设高了。解决:先把 batch size 降到 4 或 8,再把帧数从 32 降到 16,分辨率从 224 降到 112。如果还不够,用torch.cuda.amp做混合精度训练,显存能省 30% 左右。

4.2 准确率不涨:现象是 loss 震荡或卡在随机水平

现象:训练 loss 一直在 4.6 附近(101 类的随机水平是 ln(101)≈4.6),准确率 1% 左右。原因可能是学习率太大、数据标签没对齐、或者抽帧函数返回的帧顺序乱了。解决:先用一个极小数据集(比如 2 个类别各 5 段视频)过拟合,如果能降到接近 0 loss,说明模型和管道没问题,再排查全量数据。另外检查class_to_idx是否在训练和验证时一致。

4.3 抽帧不均匀:现象是模型对某些类别特别差

现象:训练集准确率还行,但测试集上“太极”“跳绳”这类长时动作准确率明显低。原因是均匀抽帧可能漏掉关键动作阶段。解决:改成按动作周期抽帧,或者增加帧数到 32,让时间覆盖更密。源码里如果有--frames参数,直接调大即可,但要注意显存。

4.4 数据加载成为瓶颈:现象是 GPU 利用率低

现象:nvidia-smi显示 GPU 利用率只有 20%~30%,训练一个 epoch 要很久。原因是 DataLoader 的num_workers设成了 0,或者抽帧在训练时实时做,CPU 扛不住。解决:把num_workers设成 4 或 8,并考虑预先把抽好的帧存成 npy 文件,训练时直接读,用空间换时间。

4.5 类别不平衡:现象是某些类别召回率为 0

现象:UCF101 虽然大致均衡,但某些类别视频长度差异大,抽帧后有效样本数可能偏少。解决:在 Dataset 里做重采样,或者用WeightedRandomSampler给样本少的类别更高权重。源码里如果没带这个,可以自己加几行。

5. 进阶技巧:用双模型集成和帧级预测把 UCF101 准确率再抬一截

跑通单模型之后,真正能拉开差距的是后处理。我一般会做两件事:一是把 3D CNN 和 CNN+RNN 的预测概率做加权平均,二是对长视频做多段抽帧预测再投票。

先看集成。假设你已经训好了两个模型,保存了各自的state_dict,推理时可以这样写:

import torch import torch.nn.functional as F def ensemble_predict(model_3d, model_rnn, video_tensor, alpha=0.6): model_3d.eval() model_rnn.eval() with torch.no_grad(): # 3D CNN 输入 (1, 16, 112, 112, 3) logit_3d = model_3d(video_tensor) prob_3d = F.softmax(logit_3d, dim=1) # CNN+RNN 输入 (1, 32, 224, 224, 3) logit_rnn = model_rnn(video_tensor_rnn) prob_rnn = F.softmax(logit_rnn, dim=1) # alpha 控制 3D CNN 权重,0.6 是我在 UCF101 上试出来的经验值 prob = alpha * prob_3d + (1 - alpha) * prob_rnn return prob.argmax(dim=1)

逻辑说明:两个模型输出的 logits 先过 softmax 变成概率,再按权重相加。alpha需要根据验证集调,我一般从 0.5 开始,每次加 0.1 看准确率变化。注意两个模型的输入张量形状不同,要分别准备。

再来看多段抽帧投票。一段视频只抽 16 帧可能漏掉关键动作,可以把它切成 3 段,每段抽 16 帧,分别预测后取多数票。这样对长视频更稳,代价是推理时间变成 3 倍。如果对延迟不敏感,这个技巧通常能再涨 2~3 个百分点。

还有一个容易被忽略的点:测试时增强(TTA)。对同一段视频做水平翻转、轻微裁剪,各预测一次再平均,也能小幅提升。源码里如果没带 TTA,自己加十几行就能实现。

最后说个血泪经验:每次改完抽帧逻辑或模型结构,一定先用 2 个类别的小数据集跑一遍过拟合,确认 loss 能降到接近 0,再上全量数据。我早期有次直接在全量 UCF101 上训了 8 小时,结果发现标签映射错了,准确率一直 1%,那种后悔药没地方买。从那以后我每次动数据管道,都强制走一遍小数据过拟合验证。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询