简介:一份面向可穿戴传感器时间序列数据预处理与建模的数据增强示例代码包,适合从事人体活动识别、帕金森病监测等研究的开发者与学习者。代码基于Python与Jupyter Notebook实现,通过对原始时间序列施加窗口滑动、噪声注入、幅值扰动等多种失真操作,有效扩充训练样本,提升CNN等模型的泛化能力。资源共5个文件,包含可运行的.ipynb与.py脚本、示例数据X_sample.npy、结果示意图.png以及README.md说明文档,压缩包大小892KB,结构紧凑便于快速上手。已有475人学习下载。读者可对照论文实验逻辑,理解数据增强在可穿戴传感数据上的具体落地方式,并直接替换自有传感器数据进行测试,也可借助示例文件观察增强前后数据形态变化,将方法迁移到其他时序任务中。
1. 可穿戴传感器数据增强:为什么时间序列比图像更需要这套 python 示例
可穿戴传感器数据(加速计、陀螺仪采集的时间序列数据)在帕金森病监测、跌倒检测、行为识别里越来越常见,标签却要靠专家逐段标注,成本高、样本少,训练 1D CNN 时经常捉襟见肘。图像领域的数据增强早就成了标配,时间序列却一直被当成黑匣子,很少有人系统地做。这套来自 ICMI 2017 论文的 python 示例代码,配套 Jupyter notebook 和一段示例数据 X_sample.npy,展示了对原始信号做幅值弯曲、时间弯曲、排列、旋转等失真变换来生成新样本的完整方法。
它解决的是"训练数据不够、类别不平衡、模型过拟合"这三个传感器项目最常见的痛点。适合做健康监测、动作识别的工程师,也适合所有在 python 里训练时间序列分类模型但手里样本不够的人。下载解压后,按 README.md 的说明跑通 Jupyter notebook,就能直接看到数据增强给训练带来的差别。
2. 从 ICMI 2017 论文到代码包:时间序列增强的失真原理
2.1 为什么是"失真"而不是"加噪":标签保真的先验逻辑
图像增强里旋转、缩放、平移不改变"一只猫还是猫"这个结论,因为人类知道这些变换保持语义。时间序列同理:一段"走路"的加速计信号,走快一点、幅度大一点,它仍然是走路。数据增强在识别问题里承担的第二个角色,就是把这种关于标签不变的先验知识注入训练过程,等效于对模型做正则化。
很多人一上手就只加高斯噪声,但噪声只是最基础的一种扰动。Um 等人在 ICMI 2017 论文里做帕金森病监测时,用的是多种失真的组合:对幅值轴做平滑弯曲、对时间轴做非线性拉伸、把窗口切成几段打乱、对三轴数据做旋转。这些变换模拟的都是真实采集时必定出现的情况——传感器佩戴松紧造成幅度波动,步速差异造成时间轴伸缩,手腕姿态变化造成坐标轴漂移。
那为什么"失真"比"加噪"效果好?因为加噪只增加高频扰动,模型很容易学会忽略微小波动,而幅值弯曲、时间弯曲改变的是信号的形状结构,逼迫模型学到更鲁棒的模式。这份代码的价值在于它不是拼几个噪声函数应付事,而是把增强当作一组保持标签的变换族来设计。
2.2 解压后拿到什么:文件清单与运行主线
压缩包解开以后结构很干净。README.md 是使用说明,DA_examples.png 是增强前后的效果预览图,主代码有两个入口:Example_DataAugmentation_TimeseriesData.ipynb,以及对应的 .py 脚本,再加一个 X_sample.npy 作为可直接加载的示例传感器数据。
我一般会用 Jupyter notebook 打开 .ipynb,因为 notebook 里每一步输出都可见,适合先理解再动手;等要把自己的数据接进来时,再改用 .py 脚本放进训练管线。这里提醒一句:notebook 的默认保存路径经常让人找不着文件,解压后最好把整个文件夹放到纯英文路径下,否则后面 np.load 的相对路径会出一堆问题。
运行主线不复杂:加载 X_sample.npy → 对信号施加若干失真变换 → 用 matplotlib 把原始信号和增强结果画在同一张图 → 以 DA_examples.png 为参照确认输出形状一致。整个过程不依赖 GPU,普通笔记本就能跑。
2.3 四类失真的适用场景:该用在哪类传感器上
按我对这套代码的理解,最关键的几个变换拆开看是这样的。
幅值弯曲(magnitude warping)生成一条平滑的随机曲线乘到原始信号上,模拟佩戴松紧引起的幅度波动,适合加速度计、陀螺仪这类幅值敏感的信号,关键参数是样条节点数和弯曲幅度。时间弯曲(time warping)对时间轴做非线性重采样,一部分片段被拉长、一部分被压缩,模拟动作快慢变化,对步态这类周期性数据很有用,但参数设大会让波形变得很不自然。排列(permutation)把整个窗口切成若干段后打乱顺序,模拟复合动作中局部顺序的抖动,适合坐-站-走这类长片段,但切得太碎会把动作语义直接打散。旋转(rotation)对三轴数据施加三维旋转矩阵,模拟设备姿态变化,天然适合加速度计加陀螺仪的 IMU 组合,一维单通道信号用不上。
| 变换 | 作用维度 | 适用传感器 | 关键参数 |
|---|---|---|---|
| 幅值弯曲 | 幅值轴 | 加速度计、陀螺仪 | 样条节点、幅度范围 |
| 时间弯曲 | 时间轴 | 步态、周期性动作 | 弯曲幅值、节点数 |
| 排列 | 片段顺序 | 复合动作 | 切割段数 |
| 旋转 | 三维坐标轴 | 三轴加速度计、IMU | 旋转角范围 |
| 加噪 | 幅值高频 | 所有信号 | 噪声标准差 |
这张表可以作为你调参时的第一版参照,后面第 4 章会给更细的参数区间。
3. 在 Jupyter 里跑通增强流程:从环境准备到可视化检查
3.1 环境安装与依赖版本
这套代码依赖很轻,python 加 numpy、matplotlib、jupyter notebook 就够。我建议先建一个虚拟环境,别把系统 python 搞乱。
python -m venv sensor_aug_env source sensor_aug_env/bin/activate # Windows 下用 sensor_aug_env\Scripts\activate pip install numpy matplotlib jupyter jupyter notebook第一行创建独立的 python 环境,第二行激活它,第三行安装三个核心包,最后启动 notebook。如果你 pip 装包慢,可以加镜像参数,比如pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy matplotlib jupyter,这只是换下载源,不影响任何代码逻辑。
提示:命令行启动 jupyter 后,终端会输出一串 URL,里面带 token 和端口号。浏览器没自动打开时,把这串 URL 完整复制到地址栏就能进。很多人卡在"password or token"这一步,其实就是复制终端里那串带 token 的地址,而不是自己瞎猜密码。
启动后进入解压文件夹,打开 Example_DataAugmentation_TimeseriesData.ipynb。这里有个新手常见误区:双击 .ipynb 文件会用系统记事本打开,看到的是一堆 JSON 字符串,以为文件损坏。其实应该在 jupyter 网页界面里找到这个文件再点开,记事本打开的只是 notebook 的底层存储格式。
3.2 加载示例数据并拆解核心增强调用
打开 notebook 后第一件事是加载数据并确认形状。
import numpy as np X = np.load("X_sample.npy") print(X.shape, X.dtype) # 常见输出可能是 (几百, 3),也可能带一个 batch 维,取决于保存布局这一步的意义在于确认维度布局。因为后面所有增强函数都依赖正确的 shape,如果是 (L, C) 就直接按二维处理,如果多了一个 batch 维,取信号时要先去掉它。很多运行时报错都源于这里布局没搞对。
接下来对信号施加变换。代码包里函数原始命名在不同版本里略有差异,我按最常见的等价骨架示意,你解压后对照真实函数名替换即可:
# 取第一通道作为示例,兼容两种常见布局 sig = X[:, 0] if X.ndim == 2 else X[0, :, 0] # 幅值弯曲:knot 是样条节点数,sigma 控制弯曲幅度 sig_warped = magnitude_warping(sig, sigma=0.2, knot=4) # 时间弯曲:对时间轴做平滑重映射,模拟动作快慢 sig_tw = time_warping(sig, sigma=0.2, knot=5) # 排列:把信号切成 4 段后打乱顺序 sig_perm = permutation(sig, n_segments=4)参数说明:sigma 控制失真强度,0.2 表示相对信号幅值约 20% 的随机波动,对归一化后的传感器数据是偏保守的取值,不会把波形搞得面目全非;knot 是样条曲线控制节点数,节点越多弯曲越剧烈,4 到 8 是合理区间;n_segments 是排列的切割段数,段数越多破坏性越强,复合动作用 3 到 5 比较合适。这些函数的输入输出约定是一致的:输入原始信号数组,输出同样长度的新数组。
3.3 可视化增强结果:怎么判断波形变形是否合理
增强跑完一定要画图,这一步别偷懒。数据增强本身是个黑匣子,不画图你根本不知道自己的参数把信号变成了什么。
import matplotlib.pyplot as plt %matplotlib inline plt.figure(figsize=(14, 8)) plt.subplot(4, 1, 1) plt.plot(sig, label="original", linewidth=1.2) plt.legend() plt.subplot(4, 1, 2) plt.plot(sig_warped, label="magnitude warped", linewidth=1.2) plt.legend() plt.subplot(4, 1, 3) plt.plot(sig_tw, label="time warped", linewidth=1.2) plt.legend() plt.subplot(4, 1, 4) plt.plot(sig_perm, label="permuted", linewidth=1.2) plt.legend() plt.tight_layout() plt.show()画图本身没什么玄学,就是把原始信号和三种增强结果叠成四个子图对比。判断尺度看三点:幅值弯曲后的信号不能出现突然的毛刺,因为样条是平滑的,输出也该平滑;时间弯曲后的波峰波谷数量大致不变,只是位置被拉伸或压缩;排列后的信号在接缝处会有跳变,但每个片段内部保持原始波形。如果四条曲线几乎完全重叠,说明参数设小了,如果完全看不出和原始信号的关系,说明设大了。
4. 参数怎么调:把示例增强应用到自己的真实数据
4.1 先统一数据维度:从 DataFrame 到定长窗口
几乎所有把这份代码用到自己数据上的人,第一个坑都是维度。X_sample.npy 是一段示例片段,形状是 (L, C),而你自己从设备导出的数据往往是 (N, L, C),N 是样本数,L 是窗口长度,C 是通道数。如果数据是 CSV 或 pandas DataFrame,统一成定长窗口是第一优先级的任务。
我一般会先写一个滑动窗口函数,把连续时间序列切成固定长度片段:
def sliding_windows(df, window_size, step): data = df.values.astype(np.float32) X = [] for start in range(0, len(data) - window_size + 1, step): X.append(data[start:start + window_size]) return np.stack(X) X_windows = sliding_windows(df, window_size=128, step=64) print(X_windows.shape) # 期望输出 (N, 128, 3)window_size 是窗口长度,128 对应多少个采样点,要根据实际采样率换算成秒数;step 是滑动步长,64 表示相邻窗口有 50% 重叠,这是动作识别里很常见的配置。切成 (N, L, C) 之后,增强函数要逐段处理。
这个环节最常被忽略的是采样率。如果你换了一台设备,采样率从 100Hz 变成 50Hz,原来 128 点的窗口只覆盖 1.28 秒,现在覆盖 2.56 秒,模型看到的语义完全不同。换设备后第一件事就是重新计算窗口覆盖的时间范围。
4.2 参数选择边界:sigma、knot 和段数怎么定
增强参数没有银弹,但归一化后的传感器数据(均值 0、标准差 1)上,几条实用边界是可以直接抄的。
| 变换 | 推荐区间 | 配套参数 | 注意点 |
|---|---|---|---|
| 加噪 | sigma 0.05 ~ 0.1 | 无 | 超过 0.3 信号基本淹没 |
| 幅值弯曲 | sigma 0.1 ~ 0.3 | knot 4 ~ 6 | 波形应保持原始轮廓 |
| 时间弯曲 | sigma ≤ 0.2 | knot 3 ~ 4 | 周期信号必须保守 |
| 排列 | 无 | 段数 2 ~ 5 | 超过 8 段等同随机噪声 |
这些区间不是拍脑袋定的,因为传感器信号的信噪比本身比图像低。图像增强稍微过头一点,人眼还能认出轮廓,传感器信号一过头,波形就变成了另一个动作类别。每次改完参数重复一遍 3.3 的可视化流程,再决定要不要进入训练。
还有一个容易被忽略的点:参数尺度要跟着原始信号幅值走。如果你的数据没有归一化,加速度计量程是 ±16g,那 sigma=0.2 的幅值弯曲就是 ±3.2g 的波动,对一段静止信号完全不可接受。所以我在增强前会先对所有通道做 z-score 归一化,让量纲统一。
4.3 与 sklearn 和 PyTorch 管线的集成方式
增强应该放在训练时即时做,而不是离线把数据集放大十倍存盘。离线增强的坏处有两个:一是内存和硬盘扛不住,二是同一个变形样本会被模型反复看到,正则化收益有限。
用 scikit-learn 做快速实验时,可以封装一个简单加载器测试增强收益:
def make_augmented_loader(X, y, aug_fn, factor=4): X_aug, y_aug = [], [] for _ in range(factor): for xi, yi in zip(X, y): X_aug.append(aug_fn(xi)) y_aug.append(yi) return np.stack(X_aug), np.stack(y_aug)如果项目本身在 PyTorch 里训练 1D CNN,更合理的做法是把增强放到 Dataset 的__getitem__里,每个 epoch 看到的变形不完全相同,模型不容易记住固定噪声:
class AugmentedSensorDataset(torch.utils.data.Dataset): def __init__(self, X, y, aug_fn, p=0.5): self.X, self.y = X, y self.aug_fn, self.p = aug_fn, p def __getitem__(self, idx): x = self.X[idx] if np.random.rand() < self.p: x = self.aug_fn(x) return torch.tensor(x, dtype=torch.float32), self.y[idx] def __len__(self): return len(self.X)p 表示每个样本被增强的概率,0.5 意味着静态样本和增强样本各占一半,训练时既有多样性又有原始分布兜底,比 1.0 全量增强更稳。
5. 避坑与排查:可穿戴传感器数据增强的六个翻车现场
这一章列我用这套代码踩过的坑,每条按现象、原因、解决的顺序写,方便对号入座。
5.1 增强以后验证集准确率反而下降
现象:训练集用增强翻了三倍,loss 下降很顺,验证集准确率掉了三个百分点。
原因:大多数情况是把增强误用到了验证集或测试集上。验证集的作用是模拟真实采集分布,真实采集不会有这种人为变形,所以验证集和测试集只做归一化,不做任何失真变换。
解决:检查数据划分代码,确认增强只作用在训练集分支。一个很有效的自检方法是打印训练集和验证集的第一个样本对比形状——如果验证集样本也被变了形,就是划分逻辑错了。
5.2 时间弯曲把步态周期拉成了乱码
现象:对一段步行数据做时间弯曲后,波形极度扭曲,步频完全看不出原来的节奏。
原因:步态是周期性信号,一个周期里包含 heel strike、toe off 等多个相位。时间弯曲节点太多或幅度太大时,周期内部相位关系被破坏,模型会把这种变形当成新的类别学进去,反而干扰分类。
解决:对周期信号,knot 设在 3 到 4,sigma 控制在 0.1 以内,每次变换后目测一个周期的长度是否稳定。如果只是想对步速鲁棒,更稳妥的做法是直接对信号重采样,把步频整体拉快或拉慢 10% 到 20%,而不是逐点弯曲时间轴。
5.3 加了随机种子还是不能复现实验结果
现象:同一个 notebook 连续跑两次,训练准确率完全相同,但增强样本图每次都不一样。
原因:numpy 的随机数生成器是全局状态,而 notebook 的 kernel 会保留上一次运行的随机状态。只在一开始调用一次np.random.seed(42),第二次运行时随机流已经从上次末尾继续了,等于没固定。
解决:把种子设置放在每次增强调用之前,或者运行前强制重启 kernel 再 Run All。如果同时用了 sklearn 和 PyTorch,还需要固定 random_state 和 torch.manual_seed,三者都固定才能完整复现。
5.4 对统计特征做增强,效果反而变差
现象:把原始窗口算成均值、方差、峰度等特征后,对特征向量做加噪或缩放,分类精度不升反降。
原因:特征已经是统计聚合的结果,时序结构早就没了。对特征扰动等于直接篡改模型输入的真实分布,而数据增强的正则化作用来自"同一标签下多样的原始波形",这两件事完全不同。
解决:增强永远发生在原始时间序列层面,做完增强之后再重新计算统计特征。如果你觉得特征层面的多样性不够,正确做法是增加原始信号的变形种类,而不是在特征向量上做文章。这个顺序搞反了,后面的调参基本白做。
5.5 对少数类增强后,类别不平衡问题更严重
现象:正常样本一万条、异常事件两百条,统一增强后正常类四万、异常类八百,模型照样偏向多数类。
原因:增强的本质是复制标签分布,它不会自动缓解不平衡。统一增强让多数类继续占绝对优势,而且少数类的增强副本彼此高度相关,模型学到的只是重复模式而不是泛化特征。
解决:只对少数类做增强,把少数类数量抬到与多数类同一量级,或者配合加权采样。增强之前先花两分钟看一眼类别分布,这个习惯能省掉后面一整个调参周。
5.6 替换自己的数据后报维度不匹配
现象:把 X_sample.npy 换成自己的数据,notebook 里的增强函数直接抛 IndexError 或 ValueError,提示索引越界。
原因:示例数据是 (L, C),自己的数据可能是 (N, L, C) 或 (L, C, 1),通道轴位置也可能不同。增强函数内部默认输入是二维,取列时自然越界。
解决:加载数据后第一件事永远是打印 shape,并按 4.1 的 sliding_windows 统一成 (L, C) 或 (N, L, C) 布局后再进增强函数。这一步多花十秒,能省掉后面所有函数层面的报错排查。
6. 一个验证技巧:上线前先跑增强消融对比
很多读者把这份代码包下载下来、跑通了 notebook 之后,会直接把手里的数据全部增强、模型重训、看测试集准确率。我要说的是:先别急着上全量增强,先做一个半小时能跑完的消融对比,用结论决定要不要上。
做法是固定同一份训练集、同一份验证集、同一个模型和同一个随机种子,只变化"训练集是否做了增强"这一个变量。基线准确率先跑出来,再对训练集做增强,重新训练,比较验证集准确率的变化。
from sklearn.ensemble import RandomForestClassifier # 假设 X_train, y_train, X_val, y_val 已切好且做过 z-score 归一化 clf = RandomForestClassifier(n_estimators=200, random_state=42, n_jobs=-1) # 基线:不增强 clf.fit(X_train, y_train) base_acc = clf.score(X_val, y_val) # 增强:每个训练样本生成 3 个变形副本(aug_fn 换成你解压出的增强函数名) X_train_aug, y_train_aug = make_augmented_loader( X_train, y_train, aug_fn=magnitude_warping, factor=3 ) clf.fit(X_train_aug, y_train_aug) aug_acc = clf.score(X_val, y_val) print(f"baseline: {base_acc:.4f} | augmented: {aug_acc:.4f}")这段代码的思路是用随机森林当快速信号测试器,它不需要精细调参,十几秒就能判断增强带来的收益方向。如果增强后验证集准确率提升一个百分点以上,值得把增强正式加进 1D CNN 的训练管线;如果持平甚至下降,说明数据集规模本身够用,或者增强参数和任务不匹配,回头调 sigma、knot,而不是硬着头皮加大增强倍数。
等增强方向和参数稳定后,再在 PyTorch 里把增强做成 on-the-fly 的 Dataset 变换,最后用测试集做一次性的最终评估。测试集在消融阶段绝对不能碰,否则调参过程就变成了对测试集的隐性过拟合。
做一次完整消融,会清楚看到数据增强代码在不同场景下的适用边界。我以前贪省事,跳过这步直接把全部训练数据增强五倍丢给 CNN,结果验证集一路掉点,排查两天才发现是时间弯曲参数把步态拉成了乱码。从那以后我每次做传感器数据的实验,都强制自己先跑一遍这段消融脚本、画一遍增强波形图,再决定要不要把增强上线。希望帮到你。
本文还有配套的精品资源,点击获取