一、方案核心原理
视频由空间维度(单帧图像) + 时间维度(帧间时序变化) 两部分信息构成:
- CNN :负责单帧空间特征提取,提取画面里物体、纹理、轮廓、局部语义(静态视觉信息);
- LSTM :负责时序建模,接收 CNN 输出的逐帧特征序列,学习帧与帧之间的运动、动作、时序依赖(动态时序信息);
- 拼接逻辑:逐帧过 CNN 得到固定长度特征向量,按帧顺序堆叠成时序序列送入 LSTM,最后 LSTM 输出融合时空特征做分类 / 回归。
二、方案优势
- 特征解耦,分工明确
CNN 专攻空间视觉,LSTM 专攻时序运动,相比 3D 卷积(C3D/R3D)更轻量化:
-
3D 卷积同时学习时空,参数量、计算量极大;
-
CNN+LSTM 复用成熟 2D 卷积骨干(ResNet/MobileNet),预训练权重丰富,训练成本更低。
- 灵活适配可变长度视频
LSTM 天然支持不定长序列输入,长短不一的短视频无需强制统一帧数,可通过 padding/truncation 处理,适配真实场景碎片化视频。
- 迁移学习能力强
CNN 骨干可直接使用 ImageNet 预训练权重,大幅减少视频数据集不足带来的过拟合;LSTM 仅负责时序微调,小样本视频任务效果提升明显。
- 可解释性强
空间特征与时序特征分离,可单独可视化单帧 CNN 特征、LSTM 时序注意力,方便调试视频动作、时序异常问题。
- 部署友好
CNN 和 LSTM 可分开优化,CNN 可用 TensorRT/ONNX 加速图像推理,LSTM 时序推理计算量远低于 3D 卷积,适合端侧、嵌入式视频设备。
三、适用任务场景
适合场景
- 视频动作识别:人体行为分类(走路、跑步、摔倒、挥手);
- 时序异常检测:监控视频异常行为、工业设备故障时序识别;
- 短视频分类:影视片段、短视频内容标签分类;
- 视频情感识别:人脸时序表情、人体动作情绪判断;
- 交通监控:车辆行驶轨迹、行人违规时序识别;
- 文本 + 视频时序任务:简易视频时序特征提取作为多模态输入。
不适合场景
- 高速密集运动、短时剧烈动作(优先 3D 卷积 SlowFast);
- 超长视频(1000 帧以上,推荐 Transformer Video);
- 高精度动作关键点检测(专用 Pose3D 网络)。
四、CNN+LSTM PyTorch 简单样例代码
整体网络结构流程
视频帧序列 [B, T, C, H, W] → 逐帧共享 CNN 骨干提取空间特征 [B, T, feat_dim] → 置换维度适配 LSTM 输入 [T, B, feat_dim] → LSTM 时序编码 [T, B, hidden_dim] → 取最后时序输出 / 全局平均池化融合时序特征 → 全连接层输出分类结果
1. 环境依赖
import torch
import torch.nn as nn
import torchvision.models as models
import torchvision.transforms as transforms
2. CNN 特征提取模块(冻结预训练骨干)
使用 ResNet18 作为空间特征提取器,移除最后分类层,输出 256 维空间特征:
class CNNBackbone(nn.Module):
def __init__(self, feat_dim=256, pretrained=True):
super().__init__()
# 加载预训练ResNet18
resnet = models.resnet18(pretrained=pretrained)
# 移除最后两层:avgpool + fc
self.feature_extractor = nn.Sequential(*list(resnet.children())[:-2])
# 自适应池化统一特征尺寸,映射到指定维度
self.avg_pool = nn.AdaptiveAvgPool2d((1, 1))
self.fc_spatial = nn.Linear(512, feat_dim)
# 可选:冻结CNN骨干权重,只训练LSTM
for param in self.feature_extractor.parameters():
param.requires_grad = False
def forward(self, frame_img):
"""
frame_img: 单帧图像 [B, 3, H, W]
return: 单帧空间特征 [B, feat_dim]
"""
feat_map = self.feature_extractor(frame_img) # [B,512,7,7]
feat_pool = self.avg_pool(feat_map) # [B,512,1,1]
feat_flat = torch.flatten(feat_pool, 1) # [B,512]
feat_out = self.fc_spatial(feat_flat) # [B,256]
return feat_out
代码讲解
- 共享权重:所有视频帧共用同一个 CNN,保证空间特征分布一致;
- 冻结骨干:小数据集场景下冻结 CNN,仅训练 LSTM 防止过拟合;大数据集可放开微调;
- 维度压缩:将 ResNet 原始 512 维特征映射为 256 维,降低 LSTM 计算开销。
3. CNN+LSTM 融合主网络
class CNNLSTMVideoModel(nn.Module):
def __init__(
self,
feat_dim=256, # CNN输出单帧特征维度
lstm_hidden=128, # LSTM隐藏层维度
lstm_layers=2, # LSTM层数
num_classes=10, # 视频分类类别数
bidirectional=False # 双向LSTM可选
):
super().__init__()
self.cnn = CNNBackbone(feat_dim=feat_dim)
# LSTM时序模块
self.lstm = nn.LSTM(
input_size=feat_dim,
hidden_size=lstm_hidden,
num_layers=lstm_layers,
batch_first=False, # 输入格式 [T,B,C]
bidirectional=bidirectional,
dropout=0.2
)
# 双向LSTM维度翻倍
lstm_out_dim = lstm_hidden * 2 if bidirectional else lstm_hidden
# 分类头
self.classifier = nn.Sequential(
nn.Dropout(0.3),
nn.Linear(lstm_out_dim, 64),
nn.ReLU(),
nn.Linear(64, num_classes)
)
def forward(self, video_frames):
"""
video_frames: 输入视频张量 [B, T, C, H, W]
B: batch_size 批次
T: 单段视频帧数
C: 3通道RGB
H,W: 图像分辨率
return: 视频分类logits [B, num_classes]
"""
B, T, C, H, W = video_frames.shape
# 步骤1:逐帧提取CNN空间特征
frame_feats = []
for t in range(T):
single_frame = video_frames[:, t, :, :, :] # 取出第t帧 [B,3,H,W]
feat = self.cnn(single_frame) # [B,256]
frame_feats.append(feat)
# 堆叠时序特征 [T, B, feat_dim]
seq_feats = torch.stack(frame_feats, dim=0)
# 步骤2:LSTM时序前向传播
lstm_out, (h_n, c_n) = self.lstm(seq_feats)
# h_n: [num_layers*direction, B, hidden_dim]
# 取最后一层时序隐藏状态作为全局时序特征
if self.lstm.bidirectional:
# 双向:拼接前向最后一层、反向最后一层
last_h = torch.cat([h_n[-2, :, :], h_n[-1, :, :]], dim=-1)
else:
last_h = h_n[-1, :, :] # [B, lstm_hidden]
# 步骤3:分类输出
logits = self.classifier(last_h)
return logits
核心流程拆解
- 输入维度拆解:
[B,T,C,H,W]批量视频,循环取出每一帧送入 CNN; - 时序序列构造:把 T 帧特征按时间维度堆叠,适配 LSTM 默认
[T,B,Dim]输入; - LSTM 输出处理:取最后时刻隐藏态代表整条视频的时空融合特征;也可替换为对 lstm_out 全局平均池化适配长视频;
- 双向 LSTM:同时利用过去、未来帧信息,动作识别精度更高,计算量翻倍。
4. 测试推理代码(模拟视频输入)
# 超参配置
BATCH_SIZE = 4
FRAME_NUM = 16 # 每个视频采样16帧
IMG_H, IMG_W = 128, 128
NUM_CLASS = 8
# 初始化模型
model = CNNLSTMVideoModel(
feat_dim=256,
lstm_hidden=128,
lstm_layers=2,
num_classes=NUM_CLASS,
bidirectional=True
)
# 模拟一批视频数据 [B, T, 3, H, W]
dummy_video = torch.randn(BATCH_SIZE, FRAME_NUM, 3, IMG_H, IMG_W)
# 前向推理
output = model(dummy_video)
print("输出分类张量 shape:", output.shape) # [4, 8] 4个视频,8个类别
输出:输出分类张量 shape: torch.Size([4, 8])
5. 训练配套损失、优化器示例
# 分类任务损失
criterion = nn.CrossEntropyLoss()
# 优化器:只更新LSTM和分类头,CNN冻结不更新
optimizer = torch.optim.AdamW(
filter(lambda p: p.requires_grad, model.parameters()),
lr=1e-4, weight_decay=1e-5
)
# 单步训练伪代码
def train_step(video_batch, label_batch):
optimizer.zero_grad()
pred = model(video_batch)
loss = criterion(pred, label_batch)
loss.backward()
optimizer.step()
return loss.item()
五、关键调优技巧
-
帧采样策略 不要全帧输入,均匀采样 8/16/32 帧,降低时序冗余;超长视频分段送入 LSTM。
-
CNN 微调策略 小数据集:冻结 CNN,仅训练 LSTM;大数据集:解冻 CNN 骨干,设置更小学习率(1e-5)。
-
LSTM 优化 时序过长易梯度消失,可搭配梯度裁剪
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5); 时序特征可加入 Dropout 缓解过拟合。 -
特征聚合改进 除取最后一步隐藏态,可对全部时序输出做平均池化:
# lstm_out shape [T,B,hidden_dim*2] global_feat = torch.mean(lstm_out, dim=0) # [B, hidden_dim*2] -
轻量化改造 CNN 替换 MobileNetv2,大幅降低单帧推理耗时,适合监控、嵌入式设备。
要注意的是,虽然CNN+LSTM支持任意长度视频序列,灵活采样,但是长时序(>64 帧)下 LSTM 梯度消失问题会变严重,所有CNN+LSTM的方案更适合目标数据是短时序或者一般时序。