简介:这份资源面向生物医学工程、康复医学与人机交互方向的学习者和研究者,围绕表面肌电信号(sEMG)分类任务,提供数据集与可运行代码的完整实践材料,帮助读者从特征抽取到分类评估走通一遍机器学习流程。压缩包共7个文件,以6个Python脚本和1个数据压缩包为主,脚本分别对应主成分分析、SVM递归消除、相关性热力图三种特征处理方法,以及KNN、SVM、随机森林三种分类器的训练与评估,整体约14.8MB,结构清晰便于按模块对照学习。目前已有539人学习下载。读者可借助其中的肌电数据集复现实验,比较不同降维与特征选择策略对分类效果的影响,理解各算法的适用条件与调参思路,并在此基础上迁移到假肢控制、运动分析等自己的研究或项目中。
1. 肌电信号分类数据集:一份能直接跑通 sEMG 机器学习分类的落地包
手上有一批表面肌电(sEMG)采集数据,想验证某个分类算法到底行不行,最耗时间的往往不是调模型,而是找一份标注清楚、通道对齐、能直接喂进 sklearn 或 PyTorch 的数据。这份肌电信号分类数据集及相关代码,解决的就是这个前置问题:它把 sEMG 的原始信号、预处理脚本和分类训练代码打包在一起,省掉从零搭数据管线的功夫。适合两类人——做手势识别、康复评估方向、需要快速验证特征工程方案的研究生;以及想把 sEMG 分类流程完整走一遍、搞清楚每个环节参数怎么设的工程师。下面按「数据长什么样 → 怎么预处理 → 怎么训分类器 → 坑在哪」的顺序拆开讲。
2. 先看清数据形态:sEMG 的通道、采样率与标签结构
拿到一份肌电数据集,第一件事不是急着写模型,而是把数据拆开看清楚。sEMG 和图像、文本不一样,它的信息密度低、噪声大、个体差异极强,同一动作不同人做出来的波形可能差出一大截。所以数据形态决定了后面所有预处理和分类策略的选择,这一步看漏了,后面调参全是玄学。
2.1 通道数与采样率的选型逻辑
常见 sEMG 采集设备有 4 通道、8 通道、16 通道几种。通道越多,空间分辨率越高,能区分的动作类别也越多,但代价是特征维度膨胀、训练样本需求上升。这份数据集对应的采集配置,从代码里的数组形状能反推出来——通常是「样本数 × 通道数 × 时间点」的三维结构,或者展平成「样本数 × (通道数 × 特征数)」的二维结构。
采样率方面,sEMG 的有效信号频段集中在 20–500 Hz,按奈奎斯特准则采样率至少 1000 Hz,工程上常用 1000 Hz 或 2000 Hz。采样率设低了,高频成分丢失,动作起始段的陡峭变化就抓不住;设高了,数据量翻倍但信息增量有限。我一般会先确认代码里有没有重采样步骤,如果有,目标采样率是多少,这直接决定了后面滑窗长度怎么取。
import numpy as np # 假设数据以 .npy 存储,形状为 (n_samples, n_channels, n_timesteps) data = np.load("emg_data.npy") labels = np.load("emg_labels.npy") print("数据形状:", data.shape) # 例如 (1200, 8, 200) print("标签形状:", labels.shape) # 例如 (1200,) print("类别分布:", np.unique(labels, return_counts=True)) print("单通道幅值范围:", data[:, 0, :].min(), data[:, 0, :].max())这段代码做的是数据体检。data.shape的三个维度分别对应样本数、通道数、时间点数;np.unique带return_counts=True能一眼看出类别是否均衡——如果某个动作的样本数只有其他的三分之一,后面训练就得考虑加权或过采样。幅值范围用来判断数据是否已经做过归一化,如果原始值在毫伏量级(比如 -5 到 5),说明还没标准化,后面必须补一步。
提示:如果
data.shape是二维的,说明数据已经被展平或已经提取过特征,这时候要去看代码里有没有对应的特征提取函数,别把特征当原始信号再处理一遍。
2.2 标签编码与滑窗切分
sEMG 是连续时间序列,但分类任务需要「一个窗口对应一个标签」的样本。所以原始采集数据通常要经过滑窗切分:设定窗长(window size)和步长(step),把长序列切成一段段固定长度的片段,每段继承该时间段内的动作标签。
窗长的选择有讲究。太短(比如 50 ms),一个窗口内的信号还没覆盖完整的动作模式,特征不稳定;太长(比如 500 ms),动作切换的边界被模糊掉,实时性也差。常见做法是 200–300 ms 窗长配 50–100 ms 步长,重叠率 50%–75%。这份代码里如果已经做好了切分,重点看两个参数:窗长对应多少个采样点、步长是多少,这决定了最终样本量。
def sliding_window(signal, label, window_size, step): """ signal: (n_channels, n_timesteps) label: (n_timesteps,) 逐点标签 返回: (n_windows, n_channels, window_size), (n_windows,) """ n_channels, n_timesteps = signal.shape windows, win_labels = [], [] for start in range(0, n_timesteps - window_size + 1, step): end = start + window_size seg = signal[:, start:end] # 取窗口内出现次数最多的标签作为该窗口标签 seg_label = np.bincount(label[start:end]).argmax() windows.append(seg) win_labels.append(seg_label) return np.array(windows), np.array(win_labels) # 窗长 200 点(1000Hz 下约 200ms),步长 50 点(50ms) X, y = sliding_window(data[0], labels_pointwise, window_size=200, step=50) print("切分后样本数:", X.shape[0])这里用众数投票给窗口打标签,是为了处理窗口跨越动作边界的情况。window_size=200在 1000 Hz 采样率下对应 200 ms,step=50对应 50 ms 步长,重叠率 75%。如果切分后样本数远少于预期,先检查n_timesteps是不是被误当成样本数了——这是新手最容易翻车的地方,把时间维度和样本维度搞混,后面所有形状都对不上。
3. 特征工程与分类器:从时域特征到模型训练
sEMG 分类的精度,七成靠特征,三成靠模型。原始波形直接丢进分类器效果通常很差,因为肌电信号是非平稳的随机过程,必须提取能反映动作模式的统计特征。这一章把特征提取和分类器训练串起来,给出能直接抄的参数配置。
3.1 时域与频域特征提取
常用的 sEMG 特征分三类:时域特征(MAV、RMS、WL、ZC、SSC)、频域特征(MF、MNF、PSD)、时频特征(小波系数)。时域特征计算快、实时性好,是嵌入式场景的首选;频域特征对肌肉疲劳更敏感;时频特征精度高但计算量大。
这份代码里大概率封装了时域特征提取函数。我一般会先跑 MAV(平均绝对值)和 RMS(均方根)这两个最稳的,再叠加 WL(波形长度)和 ZC(过零率)补充动态信息。特征维度 = 通道数 × 每通道特征数,8 通道 × 4 特征 = 32 维,这个维度对 SVM 和随机森林都很友好。
def extract_features(window): """ window: (n_channels, window_size) 返回: (n_channels * 4,) 特征向量 """ feats = [] for ch in window: mav = np.mean(np.abs(ch)) # 平均绝对值 rms = np.sqrt(np.mean(ch ** 2)) # 均方根 wl = np.sum(np.abs(np.diff(ch))) # 波形长度 zc = np.sum(np.diff(np.sign(ch)) != 0) # 过零率 feats.extend([mav, rms, wl, zc]) return np.array(feats) # 对所有窗口批量提特征 X_feat = np.array([extract_features(w) for w in X]) print("特征矩阵形状:", X_feat.shape) # (n_windows, n_channels*4)np.diff(ch)求一阶差分,np.abs后求和就是波形长度,反映信号的变化剧烈程度;np.sign配合np.diff统计符号变化次数即过零率。这两个特征对动作起始和持续状态区分度不错。注意过零率要加阈值死区,否则噪声引起的微小抖动会被误计——代码里如果没做死区处理,建议手动加一个threshold=0.01的判断,这是血泪经验,不加的话 ZC 特征基本是噪声。
3.2 分类器选型与训练配置
特征提完之后,分类器选择要看样本量和实时性要求。样本量几百到几千、特征维度几十维,SVM(RBF 核)和随机森林是性价比最高的两个选择;样本量上万、追求端到端,可以上 1D-CNN 直接吃原始波形,但对数据量要求高,小数据集容易过拟合。
这份代码如果用的是 sklearn 路线,典型配置是 SVM + 标准化 + 网格搜索。标准化这一步不能省,MAV 和 WL 的量纲差好几个数量级,不标准化的话 RBF 核的距离计算会被大数值特征主导。
from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.pipeline import Pipeline from sklearn.metrics import classification_report X_train, X_test, y_train, y_test = train_test_split( X_feat, y, test_size=0.2, stratify=y, random_state=42 ) pipe = Pipeline([ ("scaler", StandardScaler()), ("svm", SVC(kernel="rbf", probability=True)) ]) param_grid = { "svm__C": [0.1, 1, 10, 100], "svm__gamma": ["scale", 0.01, 0.001] } grid = GridSearchCV(pipe, param_grid, cv=5, scoring="f1_macro", n_jobs=-1) grid.fit(X_train, y_train) print("最优参数:", grid.best_params_) y_pred = grid.predict(X_test) print(classification_report(y_test, y_pred))stratify=y保证训练集和测试集的类别比例一致,小数据集上不做分层的话,某个类别可能全被分到一边。scoring="f1_macro"而不是默认的 accuracy,是因为肌电数据类别不均衡时 accuracy 会虚高,macro-F1 对每个类别一视同仁。C控制正则强度,gamma控制 RBF 核的作用范围,这两个是 SVM 在 sEMG 上最敏感的超参,网格范围建议先粗后细。
注意:如果
classification_report里某个类别的 recall 明显偏低,先别急着换模型,回去看这个类别的窗口切分是不是跨了动作边界,标签投票投错了。数据问题比模型问题更常见。
4. 避坑与排查:sEMG 分类里最容易翻车的五个地方
这套流程跑通不难,难的是跑出来的结果可信。下面五条是我在实际项目里反复踩过的坑,每条按「现象 → 原因 → 解决」写,照着排查能省不少时间。
4.1 准确率虚高到 99%
现象:训练完一看测试集准确率 99% 以上,换一批数据立刻掉到 60%。原因:滑窗切分时训练集和测试集共享了同一段连续信号的重叠窗口,信息泄漏了。解决:切分必须按「采集试次」或「受试者」为单位划分,不能先切窗口再随机分。代码里如果用的是train_test_split直接分窗口,一定要改成先按试次分组再分。
4.2 换个人就认不出来
现象:同一受试者内交叉验证 90%,跨受试者测试掉到 50%。原因:sEMG 的个体差异极大,电极位置、皮下脂肪厚度、肌肉发力习惯都不同,模型学到了受试者特定的信号模式而非动作本质。解决:做跨受试者评估时,训练集和测试集必须来自不同人;工程上要么做受试者自适应校准,要么用对抗训练剥离个体特征。数据集如果只含单人或少数几人,结论别外推。
4.3 特征量纲不一致导致 SVM 失效
现象:SVM 训练不收敛或结果随机。原因:MAV 在 0.01 量级,WL 在几十量级,没标准化直接进 RBF 核,距离计算被大特征主导。解决:StandardScaler放进 Pipeline,且 fit 只在训练集上做,别在全集上 fit 再分——那又是泄漏。
4.4 窗长设错导致实时性崩掉
现象:离线精度很好,部署到实时系统延迟超标。原因:窗长 500 ms 加步长 100 ms,单次推理要等半个秒的缓冲。解决:实时场景窗长压到 150–200 ms,配合重叠步长 25–50 ms,用滑动缓冲增量推理;精度掉几个点换实时性是值得的。
4.5 标签对齐错位
现象:模型学出来的混淆矩阵完全没规律,像随机猜。原因:逐点标签和信号的时间轴没对齐,采集时打了标记但导出时偏移了几个采样点。解决:拿一段已知动作的起始点,人工核对信号突变位置和标签跳变位置是否重合;代码里加一个可视化检查,画信号和标签的时序对照图,错位一眼就能看出来。
5. 进阶验证:用混淆矩阵和跨受试者协议给结果上保险
模型跑出高分不等于方案可用,验证协议的设计决定了结论能不能信。我习惯在交付前强制走两遍验证:先看混淆矩阵找系统性混淆对,再做跨受试者留一验证看泛化边界。
混淆矩阵比单一准确率信息量大得多。sEMG 分类里常见的混淆对是「握拳 vs 屈腕」「伸指 vs 张手」,因为这两组动作激活的肌肉群高度重叠。如果混淆矩阵显示某两类互相误判严重,说明特征维度不够区分它们,要么加通道,要么引入频域特征补充。
from sklearn.metrics import confusion_matrix import numpy as np cm = confusion_matrix(y_test, y_pred) # 按行归一化,看每个真实类别的召回分布 cm_norm = cm.astype("float") / cm.sum(axis=1, keepdims=True) print(np.round(cm_norm, 2))按行归一化后,对角线是各类召回率,非对角线是误判去向。如果某一行有两个 0.4 左右的值,说明这个类别和另一个类别纠缠不清,回去查这两类的动作定义和电极位置。
跨受试者验证用留一法:每次留一个受试者的全部数据做测试,其余人训练。这个协议下精度通常比受试者内低 20–30 个百分点,这是 sEMG 的常态,不是模型不行。如果留一法精度还在 70% 以上,说明特征有一定泛化性;如果掉到 50% 附近(接近随机),就得考虑加校准环节或换更鲁棒的特征。
| 验证协议 | 典型精度区间 | 适用结论 |
|---|---|---|
| 受试者内随机划分 | 85%–95% | 仅证明流程可跑通 |
| 受试者内按试次划分 | 80%–90% | 同人不同次采集的稳定性 |
| 跨受试者留一 | 55%–75% | 泛化能力的真实边界 |
最后说个习惯:每次拿到新的 sEMG 数据集,我都会先画三张图——单通道原始波形、特征分布直方图、混淆矩阵。前两张确认数据没坏,第三张确认模型没骗我。这套动作走完再谈调参,比上来就网格搜索靠谱得多。希望帮到你。
本文还有配套的精品资源,点击获取