简介:这是一份面向机器学习与行为识别入门者的人类活动建模资源包,聚焦如何将智能手机采集的GPS、加速度计、陀螺仪等多源传感数据转换为可分析特征,并构建识别行走、跑步、驾驶等活动的模型。资源共34个文件,约54.63MB,以txt说明文档、Python脚本及数据集配套文件为主,其中Python代码覆盖数据加载、清洗预处理、特征工程与模型训练等关键环节,txt文件则提供数据集字段说明、活动标签与使用指引,便于边读边练。已有127人学习,适合有Python基础、希望结合真实传感数据掌握人类活动识别流程的开发者参考。通过该资源可系统了解从原始传感器数据到活动分类模型的完整链路,并可直接复用其中脚本开展实验,为后续健康管理、智能家居等领域应用打下基础。
1. 人类活动建模的真实痛点:信号并不认识动作
加速度计输出的三轴数值,叠加了重力、颠簸和走路节奏,直接丢给分类器并不会得到可用的动作标签。人类活动建模要解决的是把这串连续信号切成短窗口,再把每个窗口映射成站、走、跑、上楼、下楼中的一个动作。补齐这条链路不需要特别深的算法,却非常依赖数据处理顺序:传感器采样率如何对齐、窗口滑动多少、训练数据按人还是按随机行切分,每一步都直接影响最终分数。更重要的是,这套“传感器数据+特征工程+建模代码”的组合可以复用到跌倒检测、老人看护、运动能耗估算等场景里。下文把从原始数据到可部署模型的全过程逐步拆开。
2. 攒数据:智能手机数据从哪读、怎么洗成可用样本
先解决一个基础问题:拿到的智能手机数据长什么样,又能从中读到什么。业界处理人类活动建模(HAR,Human Activity Recognition)时,常用数据源是手机内置的运动传感器,其中加速度计与陀螺仪是识别站、坐、走、跑的核心。
2.1 智能手机数据里有什么:传感器的职责与采样率
手机里的传感器各管一段运动模态,读到的信号形态完全不同。加速度计感知的是线加速度与重力加速度的合量,静态放置时读到的数值就是重力投影,所以走路时波形会叠在一个接近 1g 的直流分量上;陀螺仪输出角速度,对转身、旋腕这类旋转运动非常敏感;磁力计看方向的长期漂移;气压计则能感知微小高度变化,对楼层识别帮助最大。
| 传感器 | 输出 | 典型采样率 | 从它身上能读出的活动特征 |
|---|---|---|---|
| 加速度计 | 三轴加速度(m/s²) | 50Hz | 走路节奏、冲击强度、站起的瞬时加速度 |
| 陀螺仪 | 三轴角速度(rad/s) | 50Hz | 转身、弯腰、手臂摆动幅度 |
| 磁力计 | 三轴磁场强度 | 10~30Hz | 朝向辅助、室内定位修正 |
| 气压计 | 气压(hPa) | 5~25Hz | 楼层变化,能直接区分上楼和下楼 |
| GPS | 经纬度、速度 | 1Hz | 室外移动速度辅助判断 |
采样率不是越高越好。50Hz 已经能覆盖人正常活动的主要能量频段,再高只会增加功耗和存储。处理多传感器数据时,第一步是把它们按时间戳对齐到同一个采样网格,否则后面切窗后每个窗口内各轴长度不一致,特征矩阵根本拼不起来。
2.2 公开数据集怎么选:从 UCI HAR 到自采数据
不想从零标数据,一般从 UCI HAR 数据集起步:30 名受试者,6 类活动(走、上楼、下楼、坐、站、躺),使用三星手机内置加速度计和陀螺仪采集并做了滤波。数据已经切成 128 个采样点的窗口,附带 561 维预提取特征,适合快速验证模型思路。
WISDM 数据集则更“原始”:它直接给出 CSV 格式的原始加速度记录,没有预切窗口,适合想亲手走完整个数据处理链路的人。公开数据集可以帮你把流程跑通,但真实业务里往往需要自己采:把手机放裤袋、拿手上、放包里,采集完成后让用户自报标签。自采数据的优势是传感器位置可控、标签可靠,代价是需要写一段带界面的采集应用。
| 维度 | UCI HAR | WISDM |
|---|---|---|
| 采样率 | 50Hz | 20Hz |
| 窗口 | 已切好,128 点 | 未切,需要自己处理 |
| 活动类别 | 6 类 | 6 类 |
| 传感器 | 加速度+陀螺仪 | 仅加速度计 |
| 适合阶段 | 模型快速验证 | 练完整数据链路 |
2.3 数据预处理的 Python 基线:重采样、去均值与文件对齐
UCI HAR 的文件是空格分隔的文本,读取时常犯的错是直接用默认分隔符导致列错位,正确做法是用正则分隔符读取。
import pandas as pd # UCI HAR 的特征表是空格分隔的固定宽度文本 X_train = pd.read_csv("UCI_HAR/train/X_train.txt", sep=r"\s+", header=None) y_train = pd.read_csv("UCI_HAR/train/y_train.txt", sep=r"\s+", header=None, names=["activity"]) subject_train = pd.read_csv("UCI_HAR/train/subject_train.txt", sep=r"\s+", header=None, names=["subject"]) train = pd.concat([X_train, y_train, subject_train], axis=1) print(train.shape)sep=r"\s+"会把连续多个空格当作一个分隔符处理,避免列偏移;names=["activity"]给标签列命名,方便后续 groupby;concat(..., axis=1)按列拼接时,三个文件的每一行必须严格对应同一条记录。UCI HAR 官方文件行数相同,可以安全拼接,但自采数据不一定满足这一点,拼接前要注意先按时间戳排序。
拿到原始加速度后,需要做去重力和降采样。去均值是一个快速替代方案,严格做法是用低通滤波器估计重力方向,再从原始信号中减掉。
from scipy import signal import numpy as np def remove_gravity_and_resample(raw_accel, timestamps, target_fs=50): # raw_accel: (N, 3) 原始三轴加速度 fs_old = 1.0 / np.mean(np.diff(timestamps)) # 静态时重力在窗口内接近直流分量,去均值可大致剥离重力 accel_detrend = raw_accel - raw_accel.mean(axis=0) # 重采样到统一频率,便于多传感器拼接 resampled = signal.resample(accel_detrend, int(len(accel_detrend) * target_fs / fs_old), axis=0) return resampledraw_accel.mean(axis=0)计算每个轴在整个样本段上的平均值,这个值近似重力分量;对快速切换动作的窗口并不精确,但作为基线够用。signal.resample是按采样点数做傅里叶方法的插值,target_fs / fs_old算出目标长度与源长度之比,注意它假设信号全程等间隔,自采数据如果不是等间隔,需要先做三次样条插值。
提示:严格去重力应使用截止频率 0.5Hz 的低通滤波器估计重力,再用原始信号减去滤波结果。去均值只适合整段信号姿态变化较小的场景。
3. 特征工程:滑动窗口、时域频域与按用户切分的坑
原始信号不能直接进分类器,一是维度太高,二是孤立一个时间点的数值没有意义。人类活动建模通常把信号切成长度为数秒的短窗口,再从每个窗口里提取统计特征。
3.1 滑动窗口切多大、重叠取多少:两个必调参数
窗口长度决定了特征的时间分辨率,也直接决定模型能感知的动作粒度。走路一步约 0.5 秒,一次迈步周期约 1 秒,窗口短于 1 秒装不下完整步态周期,窗口超过 5 秒又容易把“走完改站住”这种状态变化混在一个样本里。常见做法是 2 秒窗口,重叠 50%。
| 窗口长度 | 重叠率 | 单样本延迟 | 特征稳定性 | 适用动作 |
|---|---|---|---|---|
| 1s | 50% | 低 | 偏低,易抖动 | 手势、点按 |
| 2s | 50% | 中等 | 稳定 | 走、跑、上楼、下楼 |
| 5s | 0% | 高 | 很高 | 长期姿态监测,睡姿判断 |
滑动窗口切分逻辑简单,但边界条件容易写错。窗口起点步进是stride,不是 1,否则样本量爆炸且相邻样本几乎完全重复。
import numpy as np def sliding_window(data, window_length, stride): # data: (N, C),返回 (num_windows, window_length, C) num_windows = 1 + (len(data) - window_length) // stride windows = np.stack([ data[i * stride : i * stride + window_length] for i in range(num_windows) ]) return windowswindow_length是采样点数,不是秒数,2 秒 @50Hz 就是 100;stride是 1 秒 @50Hz 就是 50。num_windows的公式里+1保证最后一段尾部能覆盖到;data[i * stride : i * stride + window_length]的切片步进帮你去掉末尾不足窗口长度的残缺段,避免模型学到不完整的动作信号。
3.2 特征工程的 Python 实现:时域与频域特征
时域特征描述信号的幅度和波动速度,频域特征描述周期性。走路时的重心起伏有一个稳定主频,跑步时这个主频升高;而“坐”这种静态动作,三个轴的能量都集中在极低频,两者特征空间差异很大。
def extract_features(window, sample_rate=50): feats = {} n = len(window) for axis in range(window.shape[1]): x = window[:, axis] feats[f"rms_{axis}"] = np.sqrt(np.mean(x**2)) feats[f"zero_crossing_{axis}"] = np.diff(np.signbit(x)).sum() # FFT 前加汉宁窗,降低频谱泄漏 win = x * np.hanning(n) fft_vals = np.abs(np.fft.rfft(win)) * 2.0 / n freqs = np.fft.rfftfreq(n, d=1.0 / sample_rate) # 去掉直流分量,找主频 main_idx = np.argmax(fft_vals[1:]) + 1 feats[f"main_freq_{axis}"] = freqs[main_idx] feats[f"peak_power_{axis}"] = fft_vals[main_idx] return featsnp.diff(np.signbit(x)).sum()统计正负符号跳变次数,过零率体现波形抖动的频率,坐着时信号平稳,过零率低;走路时每个步态周期都会引起几次方向变化。np.hanning(n)给原始信号加窗,防止 FFT 时截断处产生频谱泄漏,泄漏会拖出大量不存在的低频分量。fft_vals[1:]从索引 1 开始取是因为第 0 个频率是直流,幅度通常远大于运动频率,不剔除会永远当选主频。
3.3 训练集怎么切不能偷懒:按受试者留出而非随机切
特征矩阵拼好后,最隐蔽的坑出现在划分训练集和验证集时。直接train_test_split(X, y, test_size=0.2)会把同一受试者的不同窗口同时分到两边,这些窗口有重叠、高度相关,验证分数会虚高百分之五到百分之十,真实环境换新人识别时精度大跌。
from sklearn.model_selection import GroupShuffleSplit # X 是(样本数, 特征维度)的特征矩阵,subjects 是每个样本对应的受试者编号 gss = GroupShuffleSplit(n_splits=1, test_size=0.2, random_state=42) train_idx, val_idx = next(gss.split(X, y, groups=subjects)) X_train, X_val = X[train_idx], X[val_idx] y_train, y_val = y[train_idx], y[val_idx]GroupShuffleSplit与普通划分的核心区别在第三个参数groups:它要求同一group内的样本要么全进训练集,要么全进验证集。n_splits=1只切一次,random_state固定后结果可复现。人类活动建模里,一个受试者就是一组,这样验证的才是“没见过的人”,而不是“同一人的另一个瞬间”。
4. 建模与参数:从随机森林到 LSTM 的取舍与训练验证
特征矩阵拿到手后,建模的起点不是最复杂的模型,而是最快能验证特征质量的那个。人类活动数据通常样本量不大,特征维度不低,树模型往往比深度学习先出效果。
4.1 结构化特征先上树模型:随机森林的参数与代码
特征提取完成的本质是结构化数据建模,每一行是一个窗口的统计量。随机森林对这种输入有天然优势:不需要做特征归一化,能处理部分无关特征,训练时还能输出特征重要性供排查。参数不贪多,默认设置加上深度限制就能跑出好结果。
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report rf = RandomForestClassifier( n_estimators=200, max_depth=12, class_weight="balanced", n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_val) print(classification_report(y_val, y_pred, digits=3))n_estimators=200棵树对几千样本足够,再大收益递减且训练变慢;max_depth=12限制单棵树深度,防止在少量样本上过拟合,树深太深时训练分数 1.0、验证分数明显更低;class_weight="balanced"对坐、站这类时长长但标签重复率高的类别自动加权;n_jobs=-1让所有 CPU 核并行训练。特征重要性可以直接从rf.feature_importances_看,我之前发现频域主频通常排最前,说明周期性和动作类别的关联比幅度更强。
4.2 序列模型什么时候值得上:LSTM/1D-CNN 的建模切入点
当样本量突破数万甚至数十万,并且原始窗口时序性更重要时,再考虑深度学习。LSTM 能建模窗口内的时间依赖,比如走路时相邻几十毫秒的加速度变化是连续的,统计特征无法保留这种先后关系,LSTM 可以。代价是训练时间长、需要在训练前做标准化、且推理时资源占用更高。
import torch import torch.nn as nn class HARLSTM(nn.Module): def __init__(self, n_channels, hidden_size=32, num_classes=6): super().__init__() self.lstm = nn.LSTM( input_size=n_channels, hidden_size=hidden_size, batch_first=True, bidirectional=True ) self.head = nn.Linear(hidden_size * 2, num_classes) def forward(self, x): # x: (batch, seq_len, n_channels) out, _ = self.lstm(x) # out: (batch, seq_len, hidden_size * 2) return self.head(out[:, -1, :]) # 取最后一个时间步bidirectional=True让模型从前后两个方向读时序,因为走路这类循环动作,前向和后向信息都能帮助判断动作类型;out[:, -1, :]取最后时间步的输出作为整段窗口表征,这是 LSTM 常用的做法。输入x的形状对应上一章切好的原始窗口(batch, seq_len, channels),不需要压平成特征向量。损失函数用nn.CrossEntropyLoss,训练前对每个通道做均值方差标准化,否则 LSTM 收敛会很慢。
4.3 验证指标怎么读:混淆矩阵与宏平均 F1
分类报告里的 accuracy 在活动识别里不够用,因为各动作时长不均衡,坐和站占了大量样本,走和跑样本少,整体准确率高可能是数据倾斜带来的假象。更稳的是macro F1,它先按类算 F1 再取平均,给少数类同样权重。
from sklearn.metrics import confusion_matrix, f1_score cm = confusion_matrix(y_val, y_pred) print("confusion matrix:\n", cm) print("macro F1:", f1_score(y_val, y_pred, average="macro"))confusion_matrix输出的每行是真实类别,每列是预测类别,对角线越亮越好。活动识别里最常见的混淆有两个方向:走路与跑步在快速行走时界限模糊,站与坐在静止时区别极小。若发现上下楼互相错分,常规解法是加上气压计数据,因为高度变化是区分这两种动作的最直接证据;如果只依赖加速度和三轴陀螺仪,这两个类别本来就高度相似。
5. 部署到手机:降采样、int8 量化与活动平滑投票
模型在电脑上跑通只是第一步,真实智能手机上部署要考虑电池和发热。业内把训练好的模型转换到手机端时,通常会同步做三件事:把采样率从 50Hz 降到 25Hz,压缩输入数据量;对小模型做 int8 量化,降低体积和计算量;加一层输出平滑,过滤瞬时误判。
5.1 端侧推理的三个现实约束:采样率、量化、内存
人正常活动的能量几乎都低于 25Hz,所以 50Hz 数据直接降一半采样,有效信息丢失很小,但传感器功耗和计算量都显著下降。落地时可以用 TensorFlow Lite 转换模型,先做量化再部署。
import tensorflow as tf converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = generate_rep_data # 校准数据生成器 tflite_model = converter.convert()optimizations=[tf.lite.Optimize.DEFAULT]启用默认量化策略,通常作用于权重和激活值,模型体积可压缩到原来的四分之一;representative_dataset是量化校准的关键,它必须是训练数据的一小部分子集,约几百个片段即可,作用是让量化器看到真实数据分布来调整量化范围。校准数据取太少会导致量化后精度骤降,取多一点又会让转换变慢。量化后的 int8 模型在传感器数据上准确率掉点通常在 1 个百分点以内,因为运动信号本身有噪声,对量化并不敏感。
5.2 活动平滑投票:把单秒判断变成连续状态
单窗口预测天然会有抖动,即使是很准的模型,也经常在两个相邻动作的边界处来回横跳。一个不依赖额外训练的实用技巧是滑动投票:取最近几个窗口的预测结果投票,得票最多的作为最终输出。
from collections import Counter def vote_smooth(predictions, vote_window=5): smoothed = [] for i in range(len(predictions)): start = max(0, i - vote_window + 1) votes = predictions[start : i + 1] smoothed.append(Counter(votes).most_common(1)[0][0]) return smoothedvote_window=5意味着连续五个窗口的预测都被纳入计票,只有刚落进窗口的多数动作才能翻转当前判断;Counter(votes).most_common(1)取出现次数最多的类别,平票时返回最先出现的那个,满足稳定优先的要求。投票窗越长,识别越稳,但延迟也越大:一个 2 秒窗口加 5 次投票,从动作切换到最终确认约需 3 到 4 秒。这个延迟对记步、坐姿检测完全够用,对跌倒检测则不可接受,只适合部署后再做一次低延迟通道单独处理紧急事件。
本文还有配套的精品资源,点击获取