
简介面向机器学习和深度学习方向的人体动作识别完整项目适合用于毕业设计、课程设计或期末大作业参考。项目涵盖数据收集与标注、特征工程、模型选择及训练测试等完整流程并有多种算法对比实验支撑可帮助研究者与开发者快速理解动作识别系统的构建思路。压缩包共7个文件约2.28MB主要包括Jupyter Notebook主程序、Matlab环境下的模型参数寻优脚本、数据集文件与说明文档。主程序完整记录了从数据探索到模型评估的开发链路Matlab脚本分别给出随机森林、AdaBoost、逻辑回归与SVM的最优参数配置便于复现和迁移到其他识别任务配套README对项目背景、实现方法与实验环境做了细致说明。目前已有41人学习适合需要系统掌握机器学习建模流程并完成课题交付的学生及开发者参考使用。1. 人体动作识别机器学习项目zip 包里为什么值得先做数据体检拿到「基于机器学习的人体动作识别.zip」这类压缩包时很多人第一反应是解压、打开 Notebook、直接 run all看到准确率 99% 就以为自己已经掌握了这个方向。但我做过的动作识别项目里真正决定成败的往往不是模型而是数据形态和数据处理方式。这个 zip 背后是一个典型的机器学习分类任务让算法从传感器时序或视频帧里识别出走路、跑步、上下楼、静坐等动作。它不像计算机视觉那样必须上深度模型随机森林、SVM 在干净的特征上就能跑出不错效果。适合想跑通机器学习应用流程的新手也适合做可穿戴设备、康复监测和安防分析的工程师快速建立 baseline。但动手前先想清楚你手里这份数据到底是哪种形态。2. 先分清传感器数据还是视频数据动作识别路线在一开始就分叉人体动作识别在业界其实有两条非常成熟的路线压缩包里的数据格式决定了你后面所有选型。如果这一步判断错了后面调参再久也是白费力气。2.1 两种数据形态对应两条技术栈第一种是惯性传感器数据常见来源是手机加速度计、手环或 IMU 模块。每条样本一般是时间序列字段包括 acc_x、acc_y、acc_z、gyro_x、gyro_y、gyro_z采样率通常在 20Hz 到 100Hz 之间。标签是动作类别比如 walk、run、sit、stand、upstairs、downstairs。这类数据的建模思路是切窗口、提特征、上传统机器学习模型或者用 LSTM 做端到端分类。第二种是视频数据来源是摄像头或公开动作识别数据集。这种数据是图片帧序列建模思路又分两类一类是用 OpenCV 或 MediaPipe 先提取人体骨骼关键点坐标把每一帧的关节位置作为特征另一类是直接拿裁剪好的人体区域图去训练 CNN。前者轻量、可解释性强在边缘设备上非常常用后者需要大量标注数据和 GPU不是这个 zip 项目通常会选择的路。判断方法很简单——解压后看一级目录。如果看到 accelerometer、sensor、csv 这类文件走传感器路线如果看到 frames、video、jpg 这类目录走视觉路线。我见过不少项目里两种数据都有那就分别建模再做结果融合千万别把两张图喂进同一个分类器。2.2 解压后先做数据体检采样率、缺失值与类别均衡开始写特征工程之前先对数据做一遍全面体检。动作识别项目里最常见的翻车现场不是模型收敛不了而是数据文件里藏着缺失值、采样率不一致和标签错位。我一般会写一个独立脚本把训练数据的基本情况全部打印出来import pandas as pd import numpy as np df pd.read_csv(dataset/train.csv) print(shape:, df.shape) print(columns:, df.columns.tolist()) print(missing values:, df.isnull().sum().sum()) print(dtypes:\n, df.dtypes) print(label distribution:\n, df[activity].value_counts())这段代码的核心目的有三个。第一确认特征列里有没有 NaN传感器在采集过程中掉线是常态缺失值如果直接交给随机森林虽然树模型能处理但分裂点会偏移。第二确认标签是不是字符串多数 SKlearn API 能自动编码但保存模型时需要保留标签映射表。第三确认类别分布是否均衡。如果 walk 有 5 万条、jump 只有 800 条那么模型会把跳这个动作彻底忽略准确率依然很高因为类别本身就不平衡。参数说明isnull().sum().sum()是两层叠加第一个sum()按列统计空值数第二个sum()把所有列加成一个整数便于一眼看出总量。value_counts()后面最好加上normalizeTrue这样能直接看到占比而不只是绝对数量。2.3 用 pandas 做 EDA一眼看出动作样本的分布问题体检只解决了数据能不能读的问题接下来要解决数据值不值得信的问题。我会针对每个动作类别采样一段连续时间序列画图观察波形的周期性和幅值范围。比如走路时加速度计 z 轴会有明显的正弦波动而静坐时三条轴基本是平直线。这一步能快速发现两类问题标签贴反了或者传感器方向装反了。代码层面最简单的 EDA 是对每个类别随机抽一条记录打印它的时间长度和统计特征import pandas as pd df pd.read_csv(dataset/train.csv) activity_groups df.groupby(activity) for name, group in activity_groups: sample group.sample(n1, random_state42) print(f{name}: count{len(group)}, duration{sample[timestamp].max() - sample[timestamp].min():.2f}s) # 如果有 participant 字段按人看样本量 if participant in df.columns: print(df.groupby(participant)[activity].value_counts())这段代码的核心价值在于发现两个隐藏问题。第一个是长度问题如果某个动作的平均持续时长明显短于其他动作比如其他动作都是 10 秒jump 只有 2 秒说明采集协议不一致后续切窗口时这个动作的样本量会严重不足。第二个是受试者问题如果 participant A 只采集了 walkparticipant B 只采集了 run那么任何模型都能轻易通过识别被试者的习惯来猜测动作这种情况叫隐式数据泄漏。我在实际项目中会把这个 EDA 脚本输出成一份 HTML 报告包括每类样本数、平均时长、加速度三轴的箱线图。这样做一方面方便组会讨论另一方面也为后面划分训练集、测试集提供了依据。千万别跳过这一步直接建模否则你会浪费大量时间在一个坏数据集上反复调参最后只能懊恼地说一句这数据有毒。3. 把原始信号变成特征向量滑动窗口与特征工程的必调参数动作识别和图像分类最大的不同在于原始时间序列不能直接输入随机森林。你不可能把一整个 CSV 文件的 600 行数据拉平成一个 1800 维向量——维度太高、噪声太大、泛化差。正确做法是把连续信号切成小段再从每一段里提炼固定长度的统计特征。3.1 滑动窗口怎么设窗口长度、重叠率与边缘效应切窗口是动作识别里最核心的预处理步骤直接影响模型效果。窗口太长特征里混入多个动作的过渡段窗口太短无法捕捉动作周期。以 50Hz 采样率为例走路一个完整步态周期大约 1 到 1.2 秒所以窗口长度我一般取 2 秒也就是 100 个采样点。窗口重叠率常见取 50%这样可以增加样本数量同时避免动作起始相位被硬切掉。下面是一个通用的窗口切分函数直接用 pandas 实现不引入额外依赖import pandas as pd import numpy as np def sliding_window(data, window_size, step_size): windows [] labels [] max_start len(data) - window_size for start in range(0, max_start 1, step_size): end start window_size window data.iloc[start:end] # 取该窗口中出现次数最多的动作作为标签 label window[activity].mode()[0] windows.append(window[[acc_x, acc_y, acc_z]].values) labels.append(label) return np.array(windows), np.array(labels) # 参数窗口 100 点步长 50 点对应 50Hz 下的 2 秒窗口、50% 重叠 windows, labels sliding_window(df, window_size100, step_size50) print(windows shape:, windows.shape)参数说明里最容易忽略的是标签定义。窗口内部可能存在动作切换比如从走路到跑步的过渡段这时取众数最合理但过渡段本身仍然容易错标。另一个细节是range的右边界max_start 1它保证最后一个完整窗口也被包含否则数据尾部会被白白舍弃。窗口大小和步长应该作为全局超参保存下来推理时需要完全一致否则离线训练和在线识别的特征空间对不上。3.2 时域和频域特征清单16 个常用特征与计算代码切完窗口后每个窗口是一个window_size × 3的矩阵。不能把这个矩阵直接当特征展开我们要为每个轴的每个窗口计算统计量。常用的特征包括均值、方差、标准差、均方根、最大值、最小值、峰峰值、过零率、四分位距、偏度、峭度以及频域里的 FFT 频谱能量和主频位置。这些特征组合起来每个窗口就变成一个几十维的向量足够喂给分类器。我常用下面的脚本对三维加速度数据批量提取特征import numpy as np def extract_features(window): features [] for axis in range(window.shape[1]): # 三轴分别计算 signal window[:, axis] features.append(np.mean(signal)) features.append(np.std(signal)) features.append(np.sqrt(np.mean(signal ** 2))) # RMS features.append(np.max(signal) - np.min(signal)) # 峰峰值 zero_crossings np.sum(np.diff(np.sign(signal)) ! 0) features.append(zero_crossings) # 频域取 FFT 能量和主频 fft_vals np.fft.rfft(signal) power np.abs(fft_vals) ** 2 features.append(np.sum(power)) features.append(np.argmax(power)) # 主频 bin 索引 return features list_of_features [] list_of_labels [] for w, l in zip(windows, labels): list_of_features.append(extract_features(w)) list_of_labels.append(l) X np.array(list_of_features) y np.array(list_of_labels) print(X shape:, X.shape)这里每个窗口提取 7 个特征 × 3 轴 21 维。np.diff(np.sign(signal)) ! 0是计算过零率的标准写法先取符号再做差分非零就说明发生了一次正负切换。主频np.argmax(power)返回的是 FFT 分箱索引需要除以窗口时长才是实际频率但索引本身可以作为特征输入。需要注意FFT 的计算假设信号是平稳的而人体动作在 2 秒窗口内基本可以认为是平稳的这个假设成立。3.3 基准模型三选一随机森林、SVM、逻辑回归的取舍特征向量准备好后先用经典机器学习模型跑 baseline不要直接上 LSTM。原因很简单经典模型训练快、可解释、参数少而且在小数据集上效果往往不差。随机森林适合处理高维特征对特征缩放不敏感SVM 在特征维度适中时边界更精细但需要做标准化逻辑回归最简单适合做后面上线时的对照。我会把三个模型放进同一个Pipeline里对比from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.ensemble import RandomForestClassifier from sklearn.linear_model import LogisticRegression def make_model(name): if name rf: return Pipeline([ (scaler, StandardScaler()), (clf, RandomForestClassifier(n_estimators200, max_depth12, random_state42)) ]) if name svm: return Pipeline([ (scaler, StandardScaler()), (clf, SVC(kernelrbf, gammascale, C1.0, random_state42)) ]) if name lr: return Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(max_iter1000, random_state42)) ]) raise ValueError(unknown model) # 示例训练随机森林 model make_model(rf) model.fit(X_train, y_train) print(train acc:, model.score(X_train, y_train)) print(test acc:, model.score(X_test, y_test))随机森林里的max_depth12是常见设置限制深度能防止树在特征噪声上过拟合。SVM 的C1.0是正则化强度的初始值C 越大越容易过拟合。逻辑回归这里必须开max_iter1000否则特征量较大时不收敛。三个模型里随机森林通常表现最稳因为动作识别特征中存在大量离群点树模型对离群点免疫而 SVM 会被个别异常样本带着跑偏。3.4 视频数据怎么建模用骨骼关键点替代原始像素如果 zip 里是视频帧数据我强烈建议不要直接训练 3D CNN那需要大量数据和 GPU。更实用的做法是先用 MediaPipe 或 OpenCV 的 Pose 模块提取每帧的骨骼关键点坐标比如左肩、右肘、左髋、右膝的 x、y 坐标然后对这些坐标时间序列做和传感器数据一样的窗口切分、特征提取最终送入同样的分类器。下面是用 MediaPipe 提取单帧关键点的示意代码import cv2 import mediapipe as mp mp_pose mp.solutions.pose pose mp_pose.Pose(static_image_modeTrue, min_detection_confidence0.5) def extract_keypoints(frame_path): frame cv2.imread(frame_path) frame_rgb cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) results pose.process(frame_rgb) if not results.pose_landmarks: return None points [] for lm in results.pose_landmarks.landmark: points.extend([lm.x, lm.y, lm.z, lm.visibility]) return points # 返回 33 个关键点每个 4 维共 132 个数值 kp extract_keypoints(frame_0001.jpg) print(keypoints dim:, len(kp))这里 33 个关键点是 MediaPipe 的默认人体姿态模型输出每个关键点包含 x、y、z 和 visibility 可见度。在实际项目里我会丢弃 visibility 为 0 的关键点或者直接在特征工程里给低可见度点补一个特殊值否则缺失值会让模型混乱。视频路线的核心优势是特征维度高但语义清晰比如跑步时手肘和膝盖的摆动频率会直接体现在关键点坐标的变化里随机森林能很快抓到这些规律。需要注意的是如果 zip 里的数据集是已经裁剪好的人体框你依然要先保证所有帧都经过了同一个预处理否则骨架坐标的参考系不一致。4. 训练与评估动作识别模型按人划分测试集才能防泄漏许多动作识别项目在开发时准确率很高一部署到真实环境就崩问题通常不在算法而在数据划分方式上。传感器数据天然带时间相关性和个体差异如果随手train_test_split同一个人的前后几段数据会同时出现在训练集和测试集里模型等于提前看到了答案。4.1 数据泄漏为什么是动作识别项目的头号事故假设你的数据集来自 10 个受试者每个人连续记录了 10 分钟动作。如果按行随机划分训练集和测试集里都会包含同一个人的连续片段。由于人的步态习惯相对固定模型会去记忆这个人走路是什么样的而不是走路普遍是什么样的。一旦换一个没见过的人效果立刻暴跌。这也是为什么我在第 2 章强调要有participant字段。如果 zip 提供的数据里没有这个字段也要看有没有 subject_id 或者文件名前缀可以区分受试者。没有受试者信息的动作识别数据集在工程上价值要打折扣因为训练出的模型很难证明具备跨人泛化能力。4.2 用 GroupShuffleSplit 按受试者划分数据正确的做法是按受试者分组保证同一个人的所有数据只能出现在训练集或测试集其中一个。GroupShuffleSplit就是干这件事的from sklearn.model_selection import GroupShuffleSplit # 假设 y 是标签数组groups 是每个样本对应的受试者编号 gss GroupShuffleSplit(n_splits1, test_size0.2, random_state42) train_idx, test_idx next(gss.split(X, y, groupsgroups)) X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] print(train groups:, set(groups[train_idx])) print(test groups:, set(groups[test_idx]))这里的groups数组长度必须与 X 的行数一致。n_splits1表示只划分一次test_size0.2取 20% 受试者作为测试组。划分后务必打印训练和测试的受试者集合确认没有交集。另一个常见做法是LeaveOneGroupOut也就是每次拿一个受试者做测试其余训练适合样本量小、需要充分评估跨人效果的情况。不过那会训练很多次模型时间成本高做基线时用 GroupShuffleSplit 就够了。4.3 调参看两个指标交叉验证 F1 和混淆矩阵模型训练完成后不能只看准确率。动作识别数据经常类别不平衡比如静坐远多于跳跃准确率会被多数类拉高。我习惯把每个类别的 Precision、Recall 和 F1-score 全部打出来并观察混淆矩阵里的常见错误from sklearn.model_selection import cross_validate from sklearn.metrics import classification_report, confusion_matrix model make_model(rf) # 交叉验证返回每一折的测试分数 scores cross_validate(model, X, y, groupsgroups, cv5, scoring[f1_macro, accuracy], return_estimatorTrue) print(F1 macro:, scores[test_f1_macro]) print(Accuracy:, scores[test_accuracy]) # 在测试集上输出逐类别指标 model.fit(X_train, y_train) y_pred model.predict(X_test) print(classification_report(y_test, y_pred, zero_division0)) cm confusion_matrix(y_test, y_pred) print(cm)f1_macro是所有类别的 F1 取平均不考虑类别样本量。在类别不平衡时它比 accuracy 更真实。return_estimatorTrue会保存每折训练的模型你可以取其中表现最好的一折作为候选模型。classification_report里要关注的是少数类的 recall 是否接近 0比如 jump 的 recall 太低说明特征没有抓住跳跃的瞬时冲击特性这时应该回到特征工程增加峰值和能量特征而不是继续调模型参数。混淆矩阵的输出是一个方阵行是真实标签列是预测标签。我喜欢把矩阵可视化保存下来因为误判模式非常有规律比如上楼和下楼常被互相混淆因为腿部运动模式相似。这类误判不能靠调参解决要在特征层面增加方向信息比如 z 轴加速度的均值符号。5. 动作识别项目的五个常见坑从路径乱码到换人失效这个章节全是血泪经验。动作识别项目看起来简单但坑都藏在数据细节里。我把过去踩过的五个典型问题整理成现象、原因、解决三段式方便你直接对照。5.1 解压后路径乱码与相对路径失效现象zip 解压后脚本报FileNotFoundError但文件明明就在数据集目录里。原因Windows 下用系统自带解压工具解压中文路径 zip可能出现编码错乱另外很多人把 Notebook 的当前工作目录换到了项目根目录之外导致相对路径失效。解决项目最顶层放一个config.py里面用Path(__file__).parent动态拼接绝对路径而不是依赖os.getcwd()。代码里所有数据路径都从这里导入。如果文件名乱码用 Python 的zipfile模块重新解压并显式指定编码为gbk或utf-8遇到异常打印文件名来排查。5.2 传感器数据时间轴没对齐模型效果稀碎现象训练集和测试集来自不同设备模型在训练集上效果还行验证集上准确率断崖式下跌。原因训练数据在采集时传感器佩戴在右手手腕测试数据佩戴在腰部加速度计的手腕摆动幅度和腰部完全不同。或者训练数据采样率是 100Hz测试数据是 50Hz窗口长度没做重采样导致特征对不上。解决在数据预处理阶段强制统一采样率。用scipy.signal.resample或插值把所有信号重采样到固定频率然后在日志里打印每个文件的长度和采样率确保数据进入特征工程前一致。习惯上我会在数据加载后立即做一次采样率检查若不一致就报警。5.3 测试集准确率 98%换一个人直接打回原形现象在自有数据集上跑出 98% 的准确率拿给同事或客户去录一段新数据识别结果基本靠猜。原因按行随机划分导致数据泄漏模型记住了受试者的个体特征比如身高、步频习惯而不是动作的通用模式。前面第 4 章说的 GroupShuffleSplit 就是为这个问题准备的。解决重新划分数据集强制按受试者分组训练和测试。如果原数据没有受试者信息就只能放弃这个数据集去公开数据集里找带 subject 标识的。另一个补救办法是增加数据增强比如给加速度数据加随机噪声、随机缩放但效果有限最根本的还是分组划分。5.4 视频帧率不一致导致预测结果高频抖动现象视频路线的动作识别在实时推理时动作标签在几个类别之间来回跳比如站着时偶尔被识别成走路。原因视频帧率不稳定比如检测到关键点的帧率是 25fps但特征窗口按 30fps 计算导致窗口时间和动作相位错位。另外骨骼关键点本身存在抖动特别是低光照条件下x、y 坐标微小的跳动被特征放大。解决固定推理时的帧率用一个时间戳队列控制帧消费。同时给分类器的输出加一个时序平滑常见做法是滑动投票对最近 5 次的预测结果取众数作为最终输出。这个技巧在传感器路线和视频路线都有效能在不增加模型复杂度的情况下明显降低抖动。5.5 训练时内存爆炸特征矩阵太大怎么办现象代码在特征提取后直接报MemoryError或者训练随机森林时速度慢到无法接受。原因滑动窗口步长设置过小样本量暴增。比如 10 万行数据2 秒窗口加 10% 重叠会生成几万条样本每条样本还要展开成特征向量内存自然撑爆。解决先减少重叠率从 50% 降到 25%样本量直接减半。然后分批量提取特征每处理 1000 个窗口就保存一次中间结果到 HDF5 或 Parquet 文件不要一次性把所有特征保存在内存。另外可以把特征的类型从 float64 转成 float32内存直接减半。对随机森林把n_jobs-1打开吃满 CPU 核数速度会有显著提升。6. 把模型导出并跑实时推理验证落地的两个小技巧模型效果达标后下一步是把它导出、封装然后做实时推理验证。这一步做得草率前面所有工作都会卡在论文能跑工程不能用这个尴尬阶段。6.1 用 joblib 一次性导出模型与特征工程流水线特征提取和模型训练是两个独立环节最容易出问题的是推理时忘了做同样的标准化。解决方法是把特征提取函数和分类器全部封装进一个Pipeline用 joblib 整个序列化import joblib from sklearn.pipeline import Pipeline pipeline Pipeline([ (feature_engineer, FeatureExtractor()), # 自定义特征提取转换器 (scaler, StandardScaler()), (clf, RandomForestClassifier(n_estimators200, max_depth12)) ]) pipeline.fit(X_train, y_train) joblib.dump(pipeline, action_recognition_model.pkl) print(model saved)这里的FeatureExtractor需要实现fit、transform两个方法内部调用第 3 章的extract_features函数。这样保存后推理脚本只需要加载一个文件不需要分别加载 scaler 和分类器。加载时要注意版本问题joblib.dump会把 sklearn 版本信息写进对象换环境时最好是pip freeze固定版本。我吃过这个亏在本机训练好模型服务器上加载直接报错重新装环境花了两个小时。6.2 实时滑动窗口预测的最小实现实时推理的本质还是滑动窗口只是窗口变成了最近 N 个采样点。我习惯维护一个环形缓冲区每来一个新采样点就追加一次当缓冲区满后提取特征并预测然后移动窗口。import numpy as np from collections import deque BUFFER_SIZE 100 # 对应 2 秒 50Hz slide deque(maxlenBUFFER_SIZE) def predict_live(sample, model): slide.append(sample) if len(slide) BUFFER_SIZE: return None, 0.0 # 转成 numpy 二维数组形状为 (1, BUFFER_SIZE) window np.array(slide).reshape(1, -1) window_feat extract_features(window[0]) # 提特征 proba model.predict_proba([window_feat])[0] pred_idx np.argmax(proba) confidence proba[pred_idx] return model.classes_[pred_idx], confidence # 调用示例假设 data_stream 是逐采样点的生成器 # for sample in data_stream: # action, conf predict_live(sample, pipeline) # if action is not None and conf 0.7: # print(action, conf)这段代码里deque(maxlenBUFFER_SIZE)是最关键的结构它自动丢弃最旧的数据维护一个固定长度的窗口。预测时predict_proba返回每个类别的概率而不是只返回硬标签。如果概率低于 0.7我会输出不确定这比强行给出一个错误标签更安全特别是做医疗康复监测时误报一个跌倒可能引发严重后果。6.3 置信度阈值与未知动作兜底动作识别系统的边界在于训练集里只有 6 个动作真实世界里还有无数非目标动作。如果模型没见过打喷嚏它也会勉强把这个样本分类到最接近的动作里。所以我在所有落地项目里都会设置一个置信度阈值低于阈值就归为unknown类。这个阈值没有通用值通常在 0.5 到 0.8 之间需要根据自己的业务容忍度调整。它的调法很简单找几段完全不相关的数据比如讲话时的传感器数据或做家务的视频帧输入模型看模型给出最大概率的分布取一个能覆盖这些干扰样本的上限。我自己的习惯是无论 zip 里的项目代码写得多么完整拿到手也要先做一次小样本重放测试确认模型能跑通再用独立数据验证最后才考虑部署。很多项目代码能跑通是因为作者在特定的数据切片上做过微调搬到自己的场景里马上露馅。动作识别这类任务环境依赖极强数据分布一变模型就得重新校准这不是玄学而是工程常态。希望帮到你。本文还有配套的精品资源点击获取