
简介覆盖视频行为识别场景的LSTM系列模型源码包面向课程设计、毕业设计及AI行为识别入门实践的开发者与在校学生。实现jump、walk、pickup、salute、wave五类日常动作识别提供LSTM、TA-LSTM、SA-LSTM、STA-LSTM四种网络结构附训练好的.h5权重与准确率、损失曲线图可在TensorFlow/Keras环境直接完成训练、验证与单视频预测全流程。压缩包共41个文件约27.99MB以Python源码、h5权重、mp4动作视频和png可视化图表为主目录包括原始视频、预处理数据集、模型定义与中间结果。所有代码实测可直接运行无需额外调试适合快速验证行为识别技术或作为课程、毕业设计代码底稿目前已有42人学习下载。1. 项目定位这套5类动作识别源码包到底能解决什么问题前几天整理代码库时翻出去年做的一个动作识别项目当时为了验证LSTM在人体动作分类上的效果前后折腾了将近两周。现在把代码、训练视频、可视化脚本全部整理成了一份源码包命名就叫“含5类动作识别的LSTM系列模型源码包”。它做的事情很聚焦输入一段包含人物的视频或实时摄像头画面模型输出当前动作属于“站立、走路、跑步、坐下、举手”中的哪一类。虽然只有5类但已经能覆盖很多安防、健身、人机交互场景里的基础动作判别需求而且整套流程从数据预处理到模型训练再到结果可视化都是完整的不是那种只丢几个残缺Demo的仓库。这份源码包适合谁我个人认为正在做动作识别相关课程设计、毕业设计的同学以及刚入门时序模型、想在真实视频数据上跑通一个完整项目的开发者都会用得比较顺。代码主体是PyTorch写的所以你需要有一点点Python和深度学习基础至少知道张量是什么、训练一个模型大概有哪些步骤。如果你只是想抄一段能跑的动作分类代码但又不想去啃姿态估计、序列建模这些细节这份包里的脚本可以直接改路径后使用省掉一两个星期搭框架的时间。1.1 源码包的目录结构与关键文件先看整体文件布局源码包解压后大致是这个样子action_lstm_package/ ├── data/ │ ├── raw_videos/ # 训练和测试用短视频 │ │ ├── stand/ │ │ ├── walk/ │ │ ├── run/ │ │ ├── sit/ │ │ └── wave/ │ └── keypoints/ # 提取后的关键点序列 ├── models/ │ └── lstm_model.py # LSTM模型定义 ├── scripts/ │ ├── extract_keypoints.py # 视频转关键点序列 │ ├── train.py # 模型训练脚本 │ ├── inference.py # 单段视频/摄像头推理 │ └── visualize_results.py # 绘制训练曲线和混淆矩阵 ├── requirements.txt └── README.md每个文件的职责比较清晰。raw_videos目录下按动作名称分子文件夹每个类别放了10段左右的短视频每段2到5秒基本是30fps。keypoints目录里保存的是从这些视频中提取后的关键点序列文件格式是npy或csv。models目录下的lstm_model.py结构非常简单适合阅读和在此基础上修改。scripts目录里的脚本是我日常用的没有包成复杂的类直接运行即可。1.2 为什么固定选这5类动作当初定这5类不是随便拍的。站立、走路、跑步、坐下、举手这几个动作在日常生活中出现频率高而且互相之间存在容易混淆的对子走路和跑步很相似只是腿部摆动幅度和频率不同坐下和站立身体形态接近关键区别在时序上有没有“下降”过程举手则是一个比较明显的瞬态动作适合检验模型对短时动作变化的捕捉能力。选这些动作去验证LSTM模型要比选“游泳、打篮球”这种复杂动作更有利于你理解模型本身的规律因为干扰因素相对少如果模型连这5类都分不清换到更复杂的动作集上只会更惨。2. 模型选型为什么是LSTM而不是CNN或Transformer很多朋友拿到视频数据的第一反应是上CNN比如用ResNet逐帧提取特征再对帧级结果做平均或者投票。这种方式不是不行但它默认了每一帧是独立的动作与动作之间的衔接关系被切断了。动作识别本质上是一个时间序列分类问题走路和跑步从某单帧上看可能只有腿部角度的细微差别真正区分它们的是帧与帧之间的动态变化。所以我把输入端从原始像素换成了人体关键点序列再把序列喂给LSTM让模型自己去学习时间维度上的依赖关系。2.1 LSTM处理时序动作的基本原理LSTM是长短期记忆网络的缩写它最核心的贡献是通过“门”结构控制信息保留和遗忘输入门决定给当前状态写入多少新信息遗忘门决定之前的记忆要丢弃多少输出门决定最终输出什么。用人话说它在读取一段序列时会“跟着剧情走”一边更新自己的记忆一边根据当前输入和历史记忆给出结果。对动作识别来说前一秒这是站立还是弯腰会影响下一秒判断是坐下还是下蹲LSTM正好能把这些历史信息保留下来。这也解释了为什么它和普通RNN相比更不容易梯度消失。普通RNN在时间步很长时前面的信息经常传到后面就衰减没了LSTM由于有细胞状态这条“传送带”可以相对稳定地把上下文传递几十步甚至更多。我们做5类动作识别单条样本序列一般只需要30帧左右其实RNN也能勉强跑但考虑到以后你可能扩展到60帧、90帧我最终选用了LSTM这个更稳妥的系列模型。2.2 输入到底是什么关键点序列而不是原始像素训练数据不是直接把视频帧的RGB像素喂给模型而是先从视频中提取人体姿态关键点。我这里用了MediaPipe Pose它每帧可输出33个人体关键点包括鼻子、肩膀、手肘、手腕、髋、膝盖、脚踝等。每个关键点用(x, y, z)表示其中z是深度信息但不同相机下的z差异很大实际使用中我通常只取(x, y)两个坐标这样输入维度相对可控而且不会因为相机标定差异产生额外噪声。于是一个样本的形状可以理解成(序列长度, 关键点数 * 坐标数)。如果用33个关键点每点取x和y坐标那么最后一维就是66。我让每个视频片段长度为30帧也就是大约1秒的画面这样每个样本的shape就是(30, 66)。LSTM接受这个三维输入经过若干层循环计算后把最后一个时间步的隐藏状态送到全连接层最终输出5个类别的得分。选30帧而不是更长是因为在30fps的视频里1秒钟已经足以看清站、坐、举手这类动作也可以让模型在实时推理时延迟更低。2.3 模型结构定义与参数选择模型文件里核心代码就三四十行去掉注释之后是这样的import torch import torch.nn as nn class ActionLSTM(nn.Module): def __init__(self, input_size66, hidden_size128, num_layers2, num_classes5): super(ActionLSTM, self).__init__() self.lstm nn.LSTM(input_size, hidden_size, num_layers, batch_firstTrue) self.dropout nn.Dropout(0.3) self.fc nn.Linear(hidden_size, num_classes) def forward(self, x): out, _ self.lstm(x) # out: (batch, seq_len, hidden_size) out self.dropout(out[:, -1, :]) # 取最后一个时间步 return self.fc(out)这里的关键是最后输出取最后一个时间步。为什么不是把所有时间步的隐藏状态取平均因为动作分类任务的答案往往在整个动作接近完成时最清晰。一个“跑”的动作前几帧可能和“走”非常接近让最后一个隐藏状态去汇总前面所有信息相当于让模型在动作快结束的时候做判定效果更稳。当然取平均的鲁棒性也不错我在早期版本里试过二者准确率差距不大但最后一个时间步的收敛速度更快。hidden_size取128两层LSTM这个配置在视频动作分类上属于比较经典的中小规模配置既能捕捉时间依赖又不会因为参数太多导致过拟合。我见过有人一上来就堆4层LSTM、hidden_size512结果在小数据集上训练几十轮都上不了90%准确率反而出现过拟合。其实动作关键点序列的信息量比原始视频小很多网络规模不需要太大128维的隐藏层已经够用。再加上一个0.3的Dropout可以有效降低过拟合风险。3. 从视频到训练数据关键点提取和归一化的完整链路拿到原始视频后最烦的不是训练而是把视频变成LSTM能吃的数据。这个环节直接决定了模型上限如果提取的关键点位置乱跳后面模型再复杂也白搭。3.1 用MediaPipe批量提取关键点我写了scripts/extract_keypoints.py脚本逻辑并不复杂用OpenCV逐帧读取视频每帧调用MediaPipe Pose推理把得到的关键点坐标写入一个npy文件。核心代码可以浓缩成下面这个循环import cv2 import mediapipe as mp import numpy as np mp_pose mp.solutions.pose pose mp_pose.Pose( static_image_modeFalse, model_complexity1, min_detection_confidence0.5, min_tracking_confidence0.5) cap cv2.VideoCapture(video_path) keypoint_list [] while cap.isOpened(): ret, frame cap.read() if not ret: break frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) if results.pose_landmarks: points [] for lm in results.pose_landmarks.landmark: points.extend([lm.x, lm.y]) # 只取归一化的x y keypoint_list.append(points) else: keypoint_list.append(None) # 这一帧没有检测到人体 cap.release()两个小坑需要提醒。第一MediaPipe输入要用RGB格式OpenCV默认是BGR不转换的话结果会差很多。第二min_detection_confidence和min_tracking_confidence不要调到0.9以上否则很多动作幅度大的帧会被判定为“无人”造成关键点序列断裂。0.5是平衡值。3.2 坐标系归一化和滑窗采样关键点坐标本身是MediaPipe做过的归一化坐标值范围大概在0到1之间但这是针对整张图片归一化的不同身高、不同画面位置的同一个人关键点绝对位置差异很大。为了让模型关注“姿态”而不是“人在画面里的位置”我把所有关键点做了相对坐标系处理以左右髋连线的中点作为原点所有关键点坐标去减这个原点再除以左右肩连线的中点到左右髋连线中点的距离也就是躯干长度。经过这一步不管人的身高是160还是190也不管人站在画面左边还是右边关键点的值都能大致统一在类似的范围里。滑窗采样同样在提取阶段完成。如果一段视频有90帧我直接用30帧窗口去切每5帧滑动一次能生成(90-30)/5 1 ≈ 13个样本。这样做最大的好处是数据量变多原来只能训练一次的样本膨胀了十几倍。而且相邻窗口之间高度重叠模型相邻样本非常相似所以训练时我会把同一个人同一动作的视频放在同一批里避免过拟合的假象。3.3 训练集/测试集的划分原则划分数据集时最容易犯的错误是把同一个视频滑窗出来的样本随机分配到训练集和测试集。这样测试集里有很多和训练集几乎一样的内容模型评估的准确率高得离谱但一换成新视频立刻拉胯。正确做法是先按视频文件划分比如每类动作10段视频6段进训练、2段进验证、2段进测试划分完成后才允许从这些视频里滑窗采样。源码包里的脚本已经按这个逻辑处理好了你不用手动去调整。4. 训练脚本与可视化图表让模型曲线代替眼睛判断网络结构确定之后训练过程本身没什么玄学但训练日志和可视化图表能帮你快速判断模型到底有没有学好。源码包里包含两个重要图表训练过程中的loss和accuracy曲线以及最终模型在测试集上的混淆矩阵。4.1 训练主循环与损失函数配置训练脚本里用的是交叉熵损失函数优化器选择Adam初始学习率1e-3batch size 32训练最大轮数50。每轮训练结束后在验证集上计算准确率连续10轮验证准确率不提升就早停并把验证集上表现最好的模型参数保存下来。交叉熵损失配合softmax本质上就是让模型输出的5个数字变成概率分布再和目标标签做KL距离约束这是多分类任务的标准做法。下面这段是简化后的训练循环骨架import torch import torch.nn as nn from torch.utils.data import DataLoader model ActionLSTM() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) scheduler torch.optim.lr_scheduler.StepLR(optimizer, step_size20, gamma0.1) for epoch in range(50): model.train() for x, y in train_loader: optimizer.zero_grad() out model(x) loss criterion(out, y) loss.backward() optimizer.step() scheduler.step() # 计算验证集准确率、保存最优模型等StepLR每20个epoch把学习率降低到原来的0.1目的是让模型在训练后期步子小一点更精细地收敛到局部最优。如果你发现loss下降很慢可以先跑30个epoch做快速实验确认能收敛后再把epoch改大。4.2 训练过程曲线和混淆矩阵怎么读训练脚本会把每个epoch的loss和准确率记录到history.jsonvisualize_results.py读取之后用matplotlib画图。画出来的loss曲线通常会经历一个快速的下降阶段然后趋于平缓。这里有个常见误判训练loss降到0.01验证loss却在0.5附近波动这就是明显的过拟合。看到这个情况第一步是调大Dropout比如从0.3调到0.5第二步是检查训练集是不是太小考虑要不要加数据增强。如果验证loss和训练loss同步下降但验证准确率始终在80%附近上不去问题往往出在数据预处理比如关键点坐标没有做相对坐标系归一化。混淆矩阵是我最依赖的诊断工具。如果“走路”和“跑步”两类互相混淆严重说明输入序列的时序细节还不够可以尝试把滑窗长度从30加到45。如果“站立”和“坐下”混淆多半是切窗口时把坐下前的“站立姿态”也算成了“坐下”标签解决办法是删除动作过渡帧只保留动作已经稳定的中间段。4.3 只画曲线还不够特征为什么不多看一层除了loss、准确率曲线和混淆矩阵源码包里还放了一个额外的可视化模块用t-SNE把LSTM最后一个隐藏层输出的128维特征压缩到二维平面然后按类别着色。你会在图上看到同一类动作的特征点应该聚成一团不同类别之间距离拉开。如果t-SNE图上五类样本完全混在一起说明模型没有学到有效特征问题大概率在输入数据而不是网络结构。这个图表在写报告或答辩时特别有用可以直观展示“模型确实区分出了不同动作”。5. 源码包自带的训练视频数据来源、标注规范和扩容技巧源码包没有只给代码data/raw_videos目录下准备了可以直接训练的短视频。很多人会问这些视频是从哪来的我用手机和普通摄像头拍的每段2到5秒动作单一一位自愿者完成全部动作。每类动作10段视频内容覆盖了正对镜头、侧身、不同距离等场景。5.1 视频采集有哪些该注意的细节采集时建议让拍摄背景尽量干净一点不要一群人走来走去把人物关键点检测带偏。视频分辨率720p已经够用码率不必太高因为最终用的是人体关键点不是像素细节。拍摄帧率最好统一设成30fps这样后续滑窗时每帧代表的时间间隔一致。如果你在网上下载视频一定要注意帧率标签很多视频虽然写的是30fps实际打包时可能掉帧这会导致同一个动作在不同视频里速度不同模型学起来会非常困惑。5.2 标注规范和自动标注实现标注规则很简单按文件夹名给视频打标签。raw_videos/stand下的所有视频标签统一为0walk是1run是2sit是3wave是4。我在extract_keypoints.py脚本里会自动读取上一级文件夹名生成对应的标签数组省去手动标注csv的体力活。这个规则对单动作视频有效如果一段视频里连续做了多个动作比如先坐再站那就要先人工把视频切开否则一个样本里包含两个标签模型学不清楚。如果你想扩充自己的数据集方法也是一样的新建一个文件夹比如“jump”把动作视频放进去然后修改脚本里的class_list把“jump”加进去重新跑提取和训练流程即可。整个框架不限制必须是5类改成10类、20类都能跑只要样本量跟得上。6. 复现全流程中的踩坑记录与排查思路最后这部分是我最想分享的因为训练视频、可视化图表都是“好看”的结果但真正让项目卡住的是那些不起眼的小问题。下面几个坑我前前后后踩了不止一次。6.1 关键点抖动导致训练集噪声大MediaPipe在动作幅度大时会出现关键点抖动尤其是跑步动作里脚踝点偶尔会跳到大腿附近。这种噪声如果不处理模型会认为“腿在高速摆动”是一件很随机的事。我的解决办法分两步第一在提取时记录每个关键点的置信度低于0.5就把这一帧丢掉不写入序列第二对连续时间序列做平滑用savgol_filter对x、y坐标分别做窗口长度为5的滤波。实测下来训练集噪声明显降低测试准确率大约提升了3到5个百分点。6.2 视频帧率不一致让同样的动作看起来“忽快忽慢”模型不关心视频的绝对时间只关心帧与帧之间的关键点位置变化。同一段“举手”动作如果A视频是15fpsB视频是30fps模型看到的B视频关键点轨迹会更平滑速度感不同容易把明明是同一类动作分成两类。解决方案是在提取关键点前先统一视频帧率用OpenCV或ffmpeg重采样到30fps如果源视频帧率低于30fps就多做插帧或者干脆把LSTM的序列长度改成按时间窗口计算比如“2秒内固定取30帧”而不是固定取30个连续帧。6.3 类别不均衡和过渡帧带来的假准确率刚开始我的数据集里“站立”视频有20段其他类别只有10段模型最后把所有样本都预测成“站立”也能有接近25%的准确率且多数误分类都发生在少数类上。后来我用每个类别采样相同数量视频的方式做平衡。另一个隐蔽问题是动作过渡帧比如“坐下”这个过程从开始到结束可能只要1秒我如果直接从视频开头切片前面几帧其实就是站立姿态但标签写的是坐下模型就会被“没坐下去但标签是坐”的样本搞糊涂。简单做法是切掉每个视频前0.5秒和后0.5秒只保留动作最稳定的中间部分。6.4 训练集和测试集划分不当会让结果虚高这一点前面提过但值得再强调一次。如果你把所有滑窗样本混合后随机划分测试集里会出现大量训练集样本的“近亲”因为相邻滑窗之间只差5帧内容几乎一样。这样的模型在测试集上准确率可能高达98%但你在摄像头前录一段新动作视频准确率立刻掉到70%。正确划分方式是按视频文件划分确保同一个视频的所有窗口只出现在一个集合中。源码包里的train.py已经这样处理但如果你改了自己的数据很容易踩回这个坑。我实际操作中最后一个体会是LSTM动作识别的项目模型结构真的不是瓶颈数据和预处理才是。一个60行不到的LSTM模型配合规范化处理后的关键点序列在CPU上跑推理都能做到实时。如果你想继续扩展可以考虑在TinyML设备上部署或者把LSTM替换成Transformer增加更长时序的建模能力但前提一定是先把当前这条数据链路完全跑通。源码包里所有脚本都是开箱即用的遇到问题可以先看日志输出再对照我上面写的这几个调试方向去排查。本文还有配套的精品资源点击获取