简介:面向生物医学工程、康复医学与人机交互方向的学习者,这份资源围绕表面肌电信号(sEMG)分类任务,提供数据集与配套实现代码,适合具备一定机器学习基础、希望动手复现特征抽取与分类流程的读者。压缩包共7个文件,以6个Python脚本和1个数据压缩包为主,脚本分别对应主成分分析、SVM递归消除法、相关性热力图三种特征处理方式,以及KNN、支持向量机、随机森林三种分类器的训练与评估,整体约14.8MB,结构清晰便于按模块查阅。目前已有537人学习下载。读者可借助完整数据与可运行代码,理解PCA降维去噪、SVM-RFE特征筛选与热力图去冗余的思路,对比不同分类算法在小样本高维肌电数据上的表现,并在此基础上迁移到假肢控制、运动分析等实际研究场景。
1. 从一段贴片电极说起:这套 sEMG 分类资源到底能干什么
手腕上贴两片电极,让受试者做握拳、伸掌、捏取几个动作,采集卡吐出一串毫伏级电压——这就是表面肌电(sEMG)最朴素的形态。麻烦在于,这串信号既非平稳又强噪声,同一动作换个人、换次贴片位置,波形就能差出一截。所以做手势识别、康复评估、假肢控制的团队,绕不开一个核心问题:怎么把 sEMG 稳定地分到几个动作类别里。这份「肌电信号分类数据集及相关代码」就是冲这个问题来的,它把一段可复现的采集数据和多套机器学习分类流程打包在一起,覆盖从原始信号预处理、特征提取到分类器训练评估的完整链路。适合两类人:一是刚接触 sEMG 机器学习分类算法、想跑通一条 baseline 的学生和新入行工程师;二是手里有采集设备、需要一套参照流程来验证自己数据质量的老手。下面按「数据长什么样、代码怎么跑、参数怎么调、哪里会翻车」的顺序拆。
2. 数据与代码结构:先看清 sEMG 的采样率和通道布局
拿到一个肌电资源包,别急着 import,先搞清楚数据是怎么组织的。sEMG 的很多坑在读取阶段就埋下了:采样率对不上、通道顺序错位、标签和窗口没对齐,后面调参调到怀疑人生也救不回来。这一章先把数据形态和代码骨架摸清楚,再谈算法。
2.1 采样率、通道数与标签编码的核对
sEMG 的有效频带通常在 20–500 Hz,按奈奎斯特准则采样率一般取 1000 Hz 以上,常见采集设备给到 1000 Hz 或 2000 Hz。资源包里的数据文件一般分两类:原始时序(每行一个采样点,列为通道)和已切好的窗口特征(每行一个样本,列为特征,末列或单独文件存标签)。动手前先做三件事:读文件头确认采样率、数一下通道列数、把标签的唯一值打印出来看编码方式。
import numpy as np import pandas as pd # 读取原始 sEMG 时序,假设首行是列名,无时间戳列 raw = pd.read_csv("data/raw_emg.csv") print("shape:", raw.shape) # (采样点数, 通道数) print("columns:", list(raw.columns)) # 采样率从配套的 meta 文件或文件名里取,别猜 fs = 1000 # Hz,按实际采集设置改 duration = len(raw) / fs print(f"时长约 {duration:.2f} s") # 标签单独存放时,检查编码是否连续 labels = pd.read_csv("data/labels.csv") print("unique labels:", sorted(labels["label"].unique())) print("counts:\n", labels["label"].value_counts())这段代码的重点不在读取本身,而在三个校验点。shape的第二维就是通道数,单通道、双通道、八通道阵列的处理方式完全不同,特征提取时要按通道分别算再拼接。duration用来反推数据是否被截断——如果标称采集 60 秒却只算出 12 秒,多半是文件被切过或采样率记错了。标签的value_counts能立刻暴露类别不平衡,某个动作样本数只有别人的三分之一,后面训练就得考虑加权或重采样。常见做法是把这些校验写成一个inspect_data()函数,每换一份数据先跑一遍,比事后 debug 省事得多。
2.2 预处理链路:带通滤波、陷波与归一化
原始 sEMG 里混着工频干扰、电极接触噪声和运动伪迹,直接送进分类器,模型学到的可能是噪声的分布。标准预处理链路是:带通滤波保留有效频带、陷波去掉 50 Hz 工频、按通道做幅度归一化。滤波器的阶数和截止频率是有讲究的,截止频率压得太低会削掉动作起始的高频成分,阶数太高则引入相位失真和边缘振铃。
from scipy.signal import butter, filtfilt, iirnotch def bandpass(sig, fs, low=20, high=450, order=4): # 归一化截止频率必须落在 (0, 1) 内,high 要小于 fs/2 nyq = fs / 2 b, a = butter(order, [low / nyq, high / nyq], btype="band") return filtfilt(b, a, sig, axis=0) # 零相位滤波,避免时序偏移 def notch(sig, fs, f0=50, q=30): b, a = iirnotch(f0 / (fs / 2), q) return filtfilt(b, a, sig, axis=0) def normalize(sig): # 按通道做 z-score,消除个体间幅度差异 return (sig - sig.mean(axis=0)) / (sig.std(axis=0) + 1e-8) sig = raw.values.astype(float) sig = notch(sig, fs) sig = bandpass(sig, fs) sig = normalize(sig)filtfilt而不是lfilter,是因为前者正反两次滤波抵消了相位延迟,对后续做窗口切分和特征对齐很关键。high参数设成 450 而不是 500,是给 1000 Hz 采样率留余量,避免逼近奈奎斯特频率时滤波器响应畸变。陷波的q值控制阻带宽度,工频漂移明显时把q调小一点让阻带更宽。归一化放在滤波之后,顺序反了会把滤波器瞬态也放大。这套链路我一般会先在一段静息信号上跑,看输出是否接近零均值小方差,确认滤波器没把信号滤没了再上真实数据。
2.3 窗口切分与特征提取的代码骨架
sEMG 分类很少直接拿原始采样点喂模型,常规做法是滑动窗口切分,每个窗口算一组时域和频域特征。时域常用均方根(RMS)、过零率、波形长度(WL),频域常用平均功率频率(MPF)和中值频率(MDF)。窗口长度和重叠率直接影响样本量和实时性:窗口太短特征不稳,太长则动作过渡段混进来。
def sliding_window(sig, labels, win=200, step=100): # win/step 单位是采样点,200 点 @1000Hz = 200ms X, y = [], [] for start in range(0, len(sig) - win, step): seg = sig[start:start + win] feat = [ np.sqrt(np.mean(seg ** 2, axis=0)), # RMS,逐通道 np.mean(np.abs(np.diff(np.sign(seg), axis=0)), axis=0), # 过零率 np.sum(np.abs(np.diff(seg, axis=0)), axis=0), # 波形长度 ] X.append(np.concatenate(feat)) y.append(labels[start + win // 2]) # 取窗口中点对应标签 return np.array(X), np.array(y) X, y = sliding_window(sig, labels["label"].values) print("feature matrix:", X.shape)这里win=200、step=100对应 50% 重叠,是实时手势识别里比较常见的折中。标签取窗口中点而不是起点,是为了让窗口中心和标注时刻对齐,减少过渡段污染。三个特征函数都按axis=0逐通道计算再concatenate,通道数变了特征维度自动跟着变,不用改代码。如果资源包里已经提供了特征文件,可以直接跳过这一步,但建议还是自己从原始信号重算一遍,确认特征数值量级合理——RMS 应该在毫伏量级,过零率是 0 到 1 之间的小数,波形长度随窗口长度线性增长,任何一个数量级离谱都说明前面预处理出了问题。
3. 分类器选型与训练:从 SVM 到轻量网络的取舍
特征矩阵有了,接下来是选模型。sEMG 分类这个场景有个特点:样本量通常不大(几百到几千窗口),特征维度中等(十几到几十维),所以不是越复杂的模型越好。这一章把常见分类器的适用边界讲清楚,再给一套能直接跑的训练评估代码。
3.1 SVM、随机森林与 LDA 的适用边界
线性判别分析(LDA)是最老的 baseline,假设各类特征服从同方差高斯分布,计算快、可解释,在特征区分度好的小数据集上经常不输复杂模型。SVM 配 RBF 核在中等样本量下表现稳,但对特征缩放敏感,前面做了 z-score 正好满足它的前提。随机森林对特征量纲不敏感、能给出特征重要性,适合做特征筛选的前置探索,缺点是模型体积大、推理延迟高,嵌入式部署要掂量。
| 分类器 | 适合样本量 | 训练速度 | 推理延迟 | 主要短板 |
|---|---|---|---|---|
| LDA | 小(<500) | 极快 | 极低 | 非线性边界拟合弱 |
| SVM-RBF | 中(500–5000) | 中 | 中 | 参数敏感、需缩放 |
| 随机森林 | 中到大 | 快 | 较高 | 模型体积大 |
| 轻量 CNN | 大(>5000) | 慢 | 低(可量化) | 需大量数据防过拟合 |
选型时先问自己部署在哪:如果是离线分析,随机森林随便用;如果要跑在单片机或移动端,LDA 和量化后的小网络更现实。我一般会先用 LDA 跑一个下限,再用 SVM 看能提升多少,两者差距很小就说明特征本身已经够用,没必要上深度模型。
3.2 交叉验证与混淆矩阵的正确打开方式
sEMG 数据有个隐蔽的陷阱:同一段连续采集切出来的窗口高度相关,如果随机划分训练测试集,相邻窗口会同时出现在两边,评估结果虚高。正确做法是按采集试次(trial)或按受试者做分组划分。资源包里如果带了试次编号,务必用它做GroupKFold。
from sklearn.model_selection import GroupKFold, cross_val_predict from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.metrics import confusion_matrix, classification_report # groups 为每个窗口所属的试次编号,来自数据配套文件 groups = trial_ids # 长度与 X 一致 clf = make_pipeline(StandardScaler(), SVC(kernel="rbf", C=1.0, gamma="scale")) y_pred = cross_val_predict(clf, X, y, cv=GroupKFold(n_splits=5), groups=groups) print(confusion_matrix(y, y_pred)) print(classification_report(y, y_pred, digits=3))make_pipeline把标准化和分类器绑在一起,保证标准化只在训练折上拟合,避免信息泄漏——这是很多人手写循环时容易漏的一步。GroupKFold保证同一试次的窗口不会跨折,评估结果才接近真实泛化能力。看混淆矩阵时重点盯对角线两侧的错分对:如果两个动作互相错得多,多半是它们在特征空间里本来就接近(比如握拳和捏取的 RMS 差异小),这时候要么加区分度更高的特征,要么在应用层做时序平滑。classification_report里的 macro-F1 比 accuracy 更能反映类别不平衡下的真实水平。
3.3 超参数搜索的实用范围
SVM 的C和gamma是最常调的两个。C大偏向拟合训练集,小则正则强;gamma大让决策边界更弯曲,容易过拟合。经验范围是C在 0.1–100 之间按对数取,gamma在 1e-3 到 1 之间。别一上来就网格搜几百组,先用GridSearchCV粗扫,再在最优附近细调。
from sklearn.model_selection import GridSearchCV param_grid = { "svc__C": [0.1, 1, 10, 100], "svc__gamma": [0.001, 0.01, 0.1, 1], } gs = GridSearchCV(clf, param_grid, cv=GroupKFold(n_splits=5), scoring="f1_macro", n_jobs=-1) gs.fit(X, y, groups=groups) print("best:", gs.best_params_, "score:", round(gs.best_score_, 3))注意 pipeline 里分类器那一步的名字是svc,所以参数键要写成svc__C这种双下划线形式,写错了会直接报ValueError。scoring选f1_macro而不是默认 accuracy,是为了在类别不平衡时不被多数类带偏。n_jobs=-1用满核,但数据量大时内存会吃紧,可以降到 2 或 4。搜出来的最优参数如果落在网格边界上(比如C=100最好),说明范围还得往外扩,别直接拿来用。
4. 避坑与排查:sEMG 分类里最容易翻车的五件事
这套流程跑通不难,难的是结果不稳定却找不到原因。下面五条是我在类似项目里反复踩过的,按「现象 → 原因 → 解决」记下来,遇到问题先对照排查。
4.1 准确率虚高到 99% 却没法用
现象:交叉验证准确率漂亮得不像话,换一批数据或换个人就崩。原因:随机划分导致相邻窗口泄漏,或者同一试次的数据同时进了训练和测试。解决:改用GroupKFold按试次或受试者分组,并检查特征提取时窗口是否跨了试次边界。评估指标突然高得离谱,第一反应就该是泄漏,而不是模型太强。
4.2 换受试者后性能断崖式下跌
现象:在受试者 A 上训练测试都正常,换到 B 上准确率掉到随机水平附近。原因:sEMG 的幅度和频谱分布个体差异大,按通道 z-score 只消除了整体幅度,没消除频谱形态差异。解决:做跨受试者评估时,在特征层加归一化(比如按受试者做特征标准化),或引入域适应思路;至少要在报告里如实给出跨受试者结果,别只报同受试者内的数字。
4.3 滤波器把动作起始段滤没了
现象:分类器对静态段识别正常,但动作刚起始的那几十毫秒总是错。原因:带通下限设得太高(比如 50 Hz),把动作起始的低频成分削掉了;或者filtfilt的边缘效应影响了窗口首尾。解决:下限降到 20 Hz 左右,窗口切分时丢弃每个试次首尾各一个窗口长度,避开滤波瞬态。
4.4 标签与窗口错位一格
现象:整体准确率还行,但混淆矩阵里出现系统性的相邻类别混淆。原因:标签数组和信号数组长度不一致,或者取标签时用了窗口起点而标注是按终点记的。解决:切窗前先断言len(sig) == len(labels),标签取窗口中点,并在小样本上手工核对几个窗口的波形和标签是否对得上。
4.5 训练集和测试集特征量纲不一致
现象:离线评估正常,部署到实时管线后输出全是同一类。原因:训练时用整个数据集拟合了标准化参数,实时推理时却用了另一套统计量,或者干脆没做标准化。解决:把StandardScaler的均值方差存下来,推理时复用同一组参数;pipeline 方式训练能天然避免这个问题,手写流程就要自己管好这个「黑匣子」。
5. 把 baseline 变成可信结论:留一受试者验证与特征消融
前面跑通的是一条能出数的链路,但「能出数」和「结论可信」是两回事。真正决定这套资源值不值得深挖的,是你能不能用它做出经得起追问的评估。我习惯在 baseline 之后补两步:留一受试者交叉验证(LOSO)和特征消融。
LOSO 的做法是每次留一个受试者的全部数据做测试,其余受试者训练,轮完取平均。它比 GroupKFold 更严格,直接回答「换个人还能不能用」这个部署时最关心的问题。代码上把groups换成受试者编号即可,但要注意每个受试者的类别覆盖是否完整,缺类的受试者不能单独做测试折。
from sklearn.model_selection import LeaveOneGroupOut logo = LeaveOneGroupOut() scores = [] for train_idx, test_idx in logo.split(X, y, groups=subject_ids): clf.fit(X[train_idx], y[train_idx]) scores.append(clf.score(X[test_idx], y[test_idx])) print("LOSO mean acc:", round(np.mean(scores), 3), "std:", round(np.std(scores), 3))std比mean更值得看:均值 0.85、标准差 0.15,说明模型对某些受试者几乎失效,这种方差不解决,均值再高也不敢上线。特征消融则是逐组去掉 RMS、过零率、波形长度、频域特征,看哪组掉得最多,掉得少的就是冗余特征,可以砍掉换推理速度。我一般会做一张「去掉某组特征后的 LOSO 均值」表,比单看全特征准确率有信息量得多。
还有个容易忽略的点:报告结果时把窗口长度、重叠率、滤波参数一起写清楚。sEMG 的分类准确率对这些参数极其敏感,别人复现不出来往往不是代码错,而是这些「默认值」没交代。从那以后我每次交评估报告,都强制把预处理参数和划分方式列成一张表附在后面,少一项就退回重跑。希望这套流程帮到你,少走几段弯路。
本文还有配套的精品资源,点击获取