☰
基于LSTM的五类动作识别源码包:从视频特征提取到可视化全流程
2026/10/13 1:59:16 网站建设 项目流程

简介:这份资源面向深度学习入门者、课程设计与毕业设计学生,以及需要快速验证行为识别方案的技术人员,提供一套可直接运行的五类动作识别项目源码。项目覆盖jump、walk、pickup、salute、wave五种日常动作,并实现LSTM、TA-LSTM、SA-LSTM、STA-LSTM四种网络结构,每类模型均配有训练好的权重文件与准确率、损失曲线图,便于对比不同结构的训练表现。压缩包共41个文件,约27.99MB,包含10个Python脚本、5个h5权重、6个mp4动作样本、14张png可视化图表,以及数据集目录、模型定义目录和说明文档,结构清晰,覆盖数据预处理、模型训练、验证与单视频预测全流程。已有44人学习关注。读者可借助现成代码与权重快速复现实验,理解时序建模与注意力机制在视频分析中的差异,并在此基础上替换数据集或调整网络完成自己的课题验证。

1. 含5类动作识别的LSTM系列模型源码包:从视频到可视化图表的完整落地路径

手里有一批标注好的动作视频,想用 LSTM 做五分类动作识别,却卡在“视频怎么变成 LSTM 吃得下的张量”这一步——这是很多做行为识别方向的开发者都会遇到的场景。含5类动作识别的LSTM系列模型源码包,本质上解决的就是这条链路:从原始视频抽帧、关键点或特征序列提取,到 LSTM/LSTM+Attention 等系列模型训练,再到混淆矩阵、准确率曲线、时序预测对比图等可视化图表输出。它适合两类人:一类是刚接触时序动作识别、想跑通一个完整 pipeline 的新手;另一类是已有 CNN 特征提取能力、想把时序建模部分补齐的熟手。这篇笔记不讲空泛概念,只讲这条链路怎么搭、参数怎么设、哪里容易翻车。

2. 五类动作识别为什么选 LSTM:时序建模的选型逻辑与数据准备

2.1 LSTM 在动作识别里的定位:它到底吃的是什么数据

动作识别按输入形态大致分三条路:一是直接把视频帧堆叠送进 3D CNN;二是先用 CNN 逐帧提特征,再把特征序列交给时序模型;三是基于骨架关键点序列做时序分类。LSTM 系列模型主要吃的是后两种——定长的特征序列。也就是说,LSTM 本身不处理原始像素,它处理的是形状为(batch, timesteps, feature_dim)的三维张量。

理解这一点很关键,因为它决定了整个数据准备的方向。假设你有 5 类动作,每类动作视频长度不一,那么你需要先确定一个统一的timesteps,比如 30 帧或 60 帧。短于这个长度的序列做填充,长于这个长度的做采样或截断。feature_dim则取决于你用什么特征:如果用 CNN 提特征,可能是 512 或 2048;如果用骨架关键点,可能是 2×17×3 展平后的维度。

常见做法是:先用一个轻量 CNN(如 MobileNet 或 ResNet 的某一层)对每帧提特征,缓存成.npy文件,训练 LSTM 时直接读缓存。这样避免每个 epoch 都重新过一遍 CNN,训练速度能差出好几倍。我一般会先把所有视频抽帧、提特征、存成(N, T, D)的大数组,再按类别分目录,训练时用Dataset按索引取。

2.2 五类动作数据集的目录结构与抽帧脚本

源码包类项目最怕目录乱。我习惯按下面的结构组织,后面写 DataLoader 时直接按这个约定走:

dataset/ train/ action_01/ video_001.mp4 video_002.mp4 action_02/ ... action_05/ val/ action_01/ ... features/ train/ action_01/ video_001.npy ...

抽帧和提特征用一个脚本搞定,核心是控制采样密度。下面是一个可复现的抽帧+提特征骨架:

import cv2 import numpy as np import os from torchvision import models, transforms import torch # 统一采样帧数,短补长截 TARGET_FRAMES = 30 # 特征维度取决于所用CNN,这里以ResNet18的512维为例 FEAT_DIM = 512 def sample_frames(video_path, target=TARGET_FRAMES): cap = cv2.VideoCapture(video_path) frames = [] while True: ret, frame = cap.read() if not ret: break frames.append(frame) cap.release() total = len(frames) if total == 0: return None # 均匀采样索引,保证时序覆盖均匀 idxs = np.linspace(0, total - 1, target).astype(int) return [frames[i] for i in idxs] def extract_features(frames, model, device): preprocess = transforms.Compose([ transforms.ToPILImage(), transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) batch = torch.stack([preprocess(f) for f in frames]).to(device) with torch.no_grad(): # 取全局平均池化后的特征,形状 (T, 512) feats = model(batch) return feats.cpu().numpy() # 初始化去掉分类头的ResNet18 backbone = models.resnet18(pretrained=True) backbone.fc = torch.nn.Identity() backbone.eval().to("cuda" if torch.cuda.is_available() else "cpu")

这段代码的逻辑说明:sample_frames用np.linspace做均匀采样,而不是取前 30 帧或随机取,这样能保证动作的完整时序被覆盖。extract_features里把fc层替换成Identity,输出就是 512 维特征。参数上,TARGET_FRAMES设 30 是五类动作识别的常见起点,动作周期短可以降到 16,长动作可以升到 60,但要注意 LSTM 的序列长度和显存占用是线性关系。

注意:抽帧时如果视频本身帧率很低(比如 15fps),均匀采样 30 帧可能覆盖不到 2 秒,这时要结合动作的实际时长调整TARGET_FRAMES,不要盲目套用。

2.3 特征缓存与标签映射:避免训练时重复读视频

提完特征后,每个视频对应一个(30, 512)的.npy文件。训练时用Dataset读这些文件,同时把五类动作映射成 0 到 4 的整数标签。这里有个容易忽略的点:类别顺序必须固定,训练集和验证集用同一套映射,否则混淆矩阵的轴会对不上。

import numpy as np from torch.utils.data import Dataset CLASS_MAP = {"action_01": 0, "action_02": 1, "action_03": 2, "action_04": 3, "action_05": 4} class ActionDataset(Dataset): def __init__(self, feature_root): self.samples = [] for cls_name, label in CLASS_MAP.items(): cls_dir = os.path.join(feature_root, cls_name) for fname in os.listdir(cls_dir): if fname.endswith(".npy"): self.samples.append( (os.path.join(cls_dir, fname), label)) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] feat = np.load(path) # 形状 (30, 512) return torch.tensor(feat, dtype=torch.float32), label

逻辑上,CLASS_MAP是全局唯一的标签定义,ActionDataset只负责按路径读特征和返回标签。参数方面,feature_root指向features/train或features/val。如果特征维度不是 512,比如用了 ResNet50 就是 2048,那 LSTM 的input_size要同步改,这个后面模型定义时会再提。

3. LSTM系列模型源码包的核心实现:从单层LSTM到Attention变体

3.1 单层LSTM基线:最小可跑通的模型定义

源码包里通常不会只给一个模型,而是给一组对比模型,方便你看出不同结构的差异。最基础的基线是单层 LSTM 接全连接分类头。它的作用是先跑通链路,确认数据没问题,再往上加结构。

import torch.nn as nn class LSTMClassifier(nn.Module): def __init__(self, input_size=512, hidden_size=256, num_layers=1, num_classes=5, dropout=0.3): super().__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0 ) self.classifier = nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden_size, num_classes) ) def forward(self, x): # x: (batch, timesteps, input_size) out, (h_n, c_n) = self.lstm(x) # 取最后一个时间步的隐状态做分类 last = out[:, -1, :] return self.classifier(last)

逻辑说明:batch_first=True让输入形状是(batch, T, D),和前面 Dataset 返回的形状一致。out[:, -1, :]取最后一个时间步,这是最常用的池化方式。参数上,hidden_size=256是五分类任务的常见起点,数据量小可以降到 128,数据量大可以升到 512。num_layers=1时dropout不生效,这是 PyTorch 的行为,不要误以为设了没用。

3.2 双向LSTM与Attention变体:什么场景下值得加

单层 LSTM 跑通后,常见的两个升级方向是双向 LSTM 和加 Attention。双向 LSTM 适合动作的上下文信息前后都重要的场景,比如“拿起-放下”这类动作,后面的帧能帮助理解前面的帧。Attention 则适合序列里有少数关键帧决定分类结果的场景,比如某个动作只在中间几帧有明显特征。

class BiLSTMAttention(nn.Module): def __init__(self, input_size=512, hidden_size=256, num_classes=5, dropout=0.3): super().__init__() self.bilstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=2, batch_first=True, bidirectional=True, dropout=dropout ) # 双向输出维度是 hidden_size * 2 self.attn = nn.Linear(hidden_size * 2, 1) self.classifier = nn.Sequential( nn.Dropout(dropout), nn.Linear(hidden_size * 2, num_classes) ) def forward(self, x): out, _ = self.bilstm(x) # (batch, T, hidden*2) # 计算每个时间步的注意力权重 score = self.attn(out) # (batch, T, 1) weight = torch.softmax(score, dim=1) # 加权求和得到序列表示 context = torch.sum(out * weight, dim=1) return self.classifier(context)

逻辑说明:bidirectional=True让输出维度翻倍,所以attn和classifier的输入维度都是hidden_size * 2。torch.softmax(score, dim=1)在时间维度上归一化,保证权重和为 1。参数上,双向 LSTM 的num_layers=2时dropout才生效,这里设 0.3 是防止过拟合的常见值。Attention 的引入会让模型多出少量参数,但训练时间增加不明显,适合作为默认升级项。

3.3 训练循环与关键超参:学习率、批次大小、早停

模型定义好后,训练循环里有几个参数直接决定能不能收敛。我一般用 Adam 优化器,学习率从 1e-3 开始,配合ReduceLROnPlateau在验证损失不降时减半。批次大小设 16 或 32,取决于显存。早停的耐心值设 10 个 epoch,避免过拟合。

from torch.optim import Adam from torch.optim.lr_scheduler import ReduceLROnPlateau import torch.nn as nn device = "cuda" if torch.cuda.is_available() else "cpu" model = BiLSTMAttention().to(device) optimizer = Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = ReduceLROnPlateau(optimizer, mode="min", factor=0.5, patience=5) criterion = nn.CrossEntropyLoss() best_val_loss = float("inf") patience_counter = 0 EARLY_STOP_PATIENCE = 10 for epoch in range(100): model.train() for feat, label in train_loader: feat, label = feat.to(device), label.to(device) optimizer.zero_grad() logits = model(feat) loss = criterion(logits, label) loss.backward() # 梯度裁剪防止LSTM梯度爆炸 nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() # 验证阶段 model.eval() val_loss = 0.0 with torch.no_grad(): for feat, label in val_loader: feat, label = feat.to(device), label.to(device) logits = model(feat) val_loss += criterion(logits, label).item() val_loss /= len(val_loader) scheduler.step(val_loss) if val_loss < best_val_loss: best_val_loss = val_loss torch.save(model.state_dict(), "best_model.pth") patience_counter = 0 else: patience_counter += 1 if patience_counter >= EARLY_STOP_PATIENCE: break

逻辑说明:clip_grad_norm_的max_norm=5.0是 LSTM 训练的常用值,能有效防止梯度爆炸导致的 loss 变 NaN。weight_decay=1e-4是轻量正则化。ReduceLROnPlateau的patience=5表示验证损失连续 5 个 epoch 不降就减半学习率。早停的EARLY_STOP_PATIENCE=10是经验值,数据量小可以降到 5,数据量大可以升到 15。

注意:如果训练 loss 一直不降,先检查特征是否归一化、标签是否对错位,再调学习率。LSTM 对输入尺度敏感,特征没归一化时学习率要设得更小。

4. 可视化图表怎么出:混淆矩阵、准确率曲线与时序预测对比

4.1 混淆矩阵与分类报告:看出哪两类容易混

五分类任务里,最容易出问题的是两个动作在时序上相似,比如“挥手”和“举手”。混淆矩阵能直接暴露这个问题。源码包里一般会提供sklearn的confusion_matrix和classification_report封装。

import matplotlib.pyplot as plt import seaborn as sns from sklearn.metrics import confusion_matrix, classification_report import numpy as np def plot_confusion(y_true, y_pred, class_names): cm = confusion_matrix(y_true, y_pred) plt.figure(figsize=(8, 6)) sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=class_names, yticklabels=class_names) plt.xlabel("Predicted") plt.ylabel("True") plt.title("Confusion Matrix") plt.tight_layout() plt.savefig("confusion_matrix.png", dpi=150) plt.close() # 同时打印分类报告 print(classification_report(y_true, y_pred, target_names=class_names))

逻辑说明:sns.heatmap的annot=True会在格子里显示数值,fmt="d"保证显示整数。class_names要和CLASS_MAP的顺序一致。参数上,dpi=150是论文插图常用的分辨率,再高文件会很大。分类报告里的f1-score比准确率更能反映类别不均衡时的真实表现。

4.2 训练曲线与学习率变化:判断过拟合和欠拟合

准确率曲线和损失曲线是判断模型状态最直接的工具。我习惯把训练和验证的 loss、accuracy 画在同一张图里,同时把学习率变化也画出来,方便对应。

def plot_curves(train_losses, val_losses, train_accs, val_accs, lrs): fig, axes = plt.subplots(1, 3, figsize=(18, 5)) epochs = range(1, len(train_losses) + 1) axes[0].plot(epochs, train_losses, label="Train Loss") axes[0].plot(epochs, val_losses, label="Val Loss") axes[0].set_xlabel("Epoch") axes[0].set_ylabel("Loss") axes[0].legend() axes[1].plot(epochs, train_accs, label="Train Acc") axes[1].plot(epochs, val_accs, label="Val Acc") axes[1].set_xlabel("Epoch") axes[1].set_ylabel("Accuracy") axes[1].legend() axes[2].plot(epochs, lrs, label="LR", color="green") axes[2].set_xlabel("Epoch") axes[2].set_ylabel("Learning Rate") axes[2].legend() plt.tight_layout() plt.savefig("training_curves.png", dpi=150) plt.close()

逻辑说明:三张子图分别对应损失、准确率、学习率。train_losses和val_losses在每个 epoch 结束后追加。参数上,如果验证损失先降后升,说明过拟合,可以加 dropout 或减模型容量;如果两者都居高不下,说明欠拟合,可以加层或加特征维度。

4.3 时序预测对比图:单样本的逐帧预测可视化

这个图在源码包里容易被忽略,但对调试很有用。它展示的是某个验证样本在每个时间步上的预测概率变化,能看出模型是在哪几帧“犹豫”的。

def plot_temporal_prediction(model, sample_feat, true_label, class_names, device): model.eval() with torch.no_grad(): feat = sample_feat.unsqueeze(0).to(device) # 取LSTM每个时间步的输出做逐帧分类 out, _ = model.bilstm(feat) score = model.attn(out) weight = torch.softmax(score, dim=1) context = torch.sum(out * weight, dim=1) probs = torch.softmax(model.classifier(context), dim=1) probs = probs.squeeze(0).cpu().numpy() plt.figure(figsize=(10, 4)) plt.bar(class_names, probs) plt.axhline(y=1/len(class_names), color="r", linestyle="--", label="Random") plt.title(f"True: {class_names[true_label]}") plt.ylabel("Probability") plt.legend() plt.tight_layout() plt.savefig("temporal_prediction.png", dpi=150) plt.close()

逻辑说明:这里展示的是整个序列加权后的分类概率分布,axhline画的是随机猜测的基线(五分类是 0.2)。参数上,sample_feat是单个(30, 512)的特征。如果要做逐帧预测曲线,可以把classifier作用到每个时间步的out上,画出(T, num_classes)的曲线,但要注意双向 LSTM 的每个时间步已经包含了前后文信息。

5. 避坑与排查:五类动作识别训练中最容易翻车的五个点

5.1 现象:训练准确率很高,验证准确率只有随机水平

原因:最常见的是特征和标签错位。比如ActionDataset里os.listdir的顺序在不同系统上不一致,导致特征文件和标签对不上。另一个原因是抽帧时视频读取失败返回了空列表,特征文件是空的或全零。

解决:在__getitem__里加断言检查特征形状,assert feat.shape == (30, 512)。同时打印前几个样本的标签和文件名,人工核对。如果用的是缓存特征,重新生成一遍并检查文件大小是否一致。

5.2 现象:loss 变成 NaN,训练直接中断

原因:LSTM 的梯度爆炸,或者学习率设得太大。特征没有归一化时,输入尺度过大也会导致这个问题。

解决:加clip_grad_norm_(model.parameters(), max_norm=5.0),学习率从 1e-3 降到 1e-4 试。检查特征是否做了标准化,np.load出来的特征如果均值不在 0 附近,先做(feat - mean) / std。

5.3 现象:混淆矩阵里某两类几乎完全混在一起

原因:这两类动作在时序特征上区分度不够,或者采样帧数太少,关键动作段没被覆盖到。

解决:先看这两类的视频,确认动作本身是否真的相似。如果是,增加TARGET_FRAMES到 60,或者换用更强的 CNN 特征(如 ResNet50)。也可以在 Attention 层加可视化,看模型关注的是哪些帧。

5.4 现象:训练速度极慢,一个 epoch 要几十分钟

原因:没有缓存特征,每个 epoch 都在重新读视频和过 CNN。或者num_workers设成了 0,数据加载是单线程。

解决:先把特征提完存成.npy,训练时只读特征。DataLoader的num_workers设成 4 或 8,pin_memory=True。如果特征文件很多,可以用np.memmap或 HDF5 合并成一个大文件,减少小文件读取开销。

5.5 现象:可视化图表保存后是空白或乱码

原因:matplotlib在无图形界面的服务器上默认后端不支持显示,或者中文字体缺失导致标签变成方块。

解决:在脚本开头加import matplotlib; matplotlib.use("Agg"),强制用非交互后端。中文字体问题可以设plt.rcParams["font.sans-serif"] = ["DejaVu Sans"],或者把类别名改成英文。保存时用plt.savefig而不是plt.show。

6. 从跑通到用好:LSTM动作识别的进阶调优与验证习惯

跑通五类动作识别的 LSTM 源码包只是起点,真正决定效果的是后面这些调优动作。我自己的习惯是:先固定数据 pipeline,再调模型结构,最后调超参。顺序反了,很容易在错误的数据上调出一个“看起来不错”的模型,换一批数据就崩。

第一个进阶方向是特征层面的替换。源码包里默认用 ResNet18 提特征,但你可以换成时序更敏感的 backbone,比如在 ImageNet 上预训练的 SlowFast 或 TSM。替换后input_size要同步改,LSTM 的hidden_size也可以适当加大。验证方法是:固定 LSTM 结构不变,只换特征,看验证集准确率的变化。如果换了更强的特征但准确率没涨,说明瓶颈在时序建模,不在特征。

第二个方向是序列长度的自适应。固定TARGET_FRAMES=30对短动作可能冗余,对长动作可能不够。可以按动作类别分别设不同的采样帧数,或者用可变长度序列配合pack_padded_sequence。后者需要 Dataset 返回真实长度,训练时用nn.utils.rnn.pack_padded_sequence打包,LSTM 会自动忽略填充部分。这个改动对准确率的提升在动作时长差异大的数据集上比较明显。

第三个方向是交叉验证。五类动作如果每类样本数不多,单次划分验证集的结果波动会很大。我一般用 5 折交叉验证,每折都保存混淆矩阵和准确率,最后看均值和方差。方差大的类别说明样本代表性不够,需要补数据或做数据增强。数据增强在时序特征上可以做时间抖动、随机裁剪、加高斯噪声,但要注意不要破坏动作的时序逻辑。

验证习惯上,我坚持每跑完一个模型就存三样东西:best_model.pth、confusion_matrix.png、training_curves.png。这三个文件放在以时间戳命名的目录里,后面对比不同模型时直接翻图,比翻日志快得多。另外,验证集和测试集要严格分开,测试集只在最后跑一次,不要用它来调参,否则那个准确率就是自欺欺人。

最后一个具体技巧:如果五类动作里有明显的不均衡,比如某一类样本数是其他的三倍,CrossEntropyLoss要加weight参数,权重按类别频率的倒数设置。同时看分类报告时重点看macro avg的 f1,而不是accuracy。这个细节在源码包里经常被忽略,但实际项目里几乎每次都会遇到。

我自己踩过最深的一个坑是:早期为了省事,把验证集也拿去提特征时用了和训练集一样的归一化参数,结果验证集的分布和训练集不一致,准确率虚高。后来改成归一化参数只在训练集上统计,再应用到验证集和测试集,结果才真实。这个习惯希望你能一开始就养成。希望帮到你。

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

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

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

立即咨询