ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

肌电信号分类数据集与代码:从预处理到SVM/CNN的完整流水线

肌电信号分类数据集与代码:从预处理到SVM/CNN的完整流水线 简介这份资源面向生物医学工程、康复医学与人机交互方向的学习者和研究者围绕表面肌电信号sEMG分类任务提供数据集与配套代码帮助读者理解肌肉运动状态分析在医疗诊断、假肢控制与运动分析中的应用。压缩包共7个文件以6个Python脚本和1个数据压缩包为主整体约14.8MB脚本分别对应特征抽取与分类算法的实现数据包则用于训练和测试。特征抽取部分涵盖主成分分析降维、支持向量机递归消除法特征选择以及相关性热力图可视化分类部分包含K近邻、支持向量机与随机森林三种常用机器学习方法便于横向对比不同策略的效果。已有537人学习下载读者可借助完整代码与数据集复现实验流程掌握从特征处理到模型评估的关键环节并迁移到自身研究或项目中。1. 肌电信号分类数据集与代码从一段手臂信号到可复现的分类流水线肌电信号分类数据集及相关代码这个标题背后其实是一条很典型的落地链路采集表面肌电sEMG信号做预处理和特征提取再训练分类器把手势或动作区分开。2022 年前后公开的一批肌电数据集大多配套了 Python 读取脚本和基线模型代码让后来者不用从零搭采集硬件就能跑通全流程。它适合两类人一类是做康复辅具、人机交互、手势识别的工程同学需要一份能直接跑的数据和代码另一类是刚接触生理信号分类的新手想找一个比 CWRU 轴承数据集更贴近可穿戴场景的练手项目。这篇笔记就按「数据长什么样、代码怎么跑、参数怎么调、哪里容易翻车」的顺序把这条流水线讲透。2. 肌电信号分类数据集结构、标签与读取方式2.1 表面肌电信号的物理含义与采样参数表面肌电信号是肌肉收缩时运动单元动作电位在皮肤表面的叠加本质是一段非平稳、非高斯的随机信号。它的有效频带通常在 20–500 Hz主能量集中在 50–150 Hz幅度在微伏到毫伏量级。采集时最关键的三个参数是采样率、通道数和电极位置采样率一般取 1000 Hz 或 2000 Hz按奈奎斯特准则要覆盖 500 Hz 上限通道数常见 8 通道前臂环形电极或双通道肱二头肌、肱三头肌电极位置决定你采到的是哪块肌肉的激活位置偏一点类间可分性就明显下降。公开数据集通常已经把这些参数固定好了比如常见的 NinaPro、EMG-EPN 系列以及一些高校放出的手势数据集。你拿到手的第一件事不是急着训练而是确认采样率、通道顺序、标签映射这三样东西。很多「代码跑不通」的玄学问题根源就是通道顺序和标签对不上。2.2 数据集目录结构与标签文件解析一个典型的肌电分类数据集目录大致长这样emg_dataset/ ├── subject_01/ │ ├── session_1/ │ │ ├── gesture_01.csv # 单次试次列为 [time, ch1..ch8] │ │ ├── gesture_02.csv │ │ └── ... │ └── session_2/ ├── subject_02/ ├── labels.csv # 试次到类别标签的映射 └── README.txt # 采样率、通道、单位说明labels.csv一般两列trial_id, class_id。有的数据集把标签直接写进文件名有的单独放一个labels.mat。读取时用 pandas 或 scipy 都行关键是别把试次顺序和标签顺序搞错。下面是一段通用的读取代码import os import numpy as np import pandas as pd DATA_ROOT emg_dataset FS 1000 # 采样率按数据集 README 改 N_CHANNELS 8 # 通道数 def load_subject(subject_id): 读取单个受试者的所有试次和标签 subj_dir os.path.join(DATA_ROOT, fsubject_{subject_id:02d}) signals, labels [], [] label_map pd.read_csv(os.path.join(DATA_ROOT, labels.csv)) for _, row in label_map.iterrows(): trial_path os.path.join(subj_dir, fgesture_{row[trial_id]:02d}.csv) if not os.path.exists(trial_path): continue df pd.read_csv(trial_path) # 取通道列去掉时间列 sig df.iloc[:, 1:1 N_CHANNELS].values # shape: (T, 8) signals.append(sig) labels.append(row[class_id]) return signals, np.array(labels)逻辑说明load_subject按标签文件逐条找试次文件读进来后只保留通道列。参数上FS和N_CHANNELS必须和数据集说明一致iloc[:, 1:1N_CHANNELS]里的起始 1 是因为第一列通常是时间戳如果你的数据没有时间列就改成iloc[:, :N_CHANNELS]。这一步看着简单但通道列偏移是新手最常见的翻车点读出来的信号形状不对后面全错。2.3 标签分布与类别不平衡的初步检查拿到数据后先做一次标签分布统计别急着进模型from collections import Counter signals, labels load_subject(1) print(试次数:, len(signals)) print(类别分布:, Counter(labels)) print(单试次长度范围:, min(len(s) for s in signals), -, max(len(s) for s in signals))如果某个类别样本数远少于其他类训练时就要考虑加权或重采样。肌电数据集里静态手势通常每类试次数量均衡但动态动作或疲劳状态数据往往不平衡。另外注意单试次长度是否一致长度差异大说明采集协议里试次时长不统一切窗时要单独处理。3. 从原始信号到特征预处理与特征提取的可复现步骤3.1 滤波、去噪与工频干扰处理原始 sEMG 里混着基线漂移、工频干扰和高频噪声。标准处理链是先 20 Hz 高通去运动伪迹再 450 Hz 低通抗混叠然后用 50 Hz 陷波去工频。用 scipy 实现from scipy.signal import butter, filtfilt, iirnotch def bandpass_filter(sig, fs, low20, high450, order4): 零相位带通滤波避免相位失真 nyq fs / 2 b, a butter(order, [low / nyq, high / nyq], btypeband) return filtfilt(b, a, sig, axis0) def notch_filter(sig, fs, freq50, q30): 工频陷波 b, a iirnotch(freq / (fs / 2), q) return filtfilt(b, a, sig, axis0) def preprocess(sig, fs): sig bandpass_filter(sig, fs) sig notch_filter(sig, fs) return sig参数说明order4是巴特沃斯阶数阶数越高过渡带越陡但相位非线性越强配合filtfilt零相位滤波可以抵消。q30是陷波品质因数太大陷波带宽太窄去不干净太小会把 50 Hz 附近有用信号一起削掉。注意filtfilt要求信号长度大于滤波器阶数的 3 倍短试次要检查。3.2 时域与频域特征MAV、RMS、WL、ZC 与功率谱肌电分类很少直接把原始波形喂给分类器通常先提特征。经典时域特征四个平均绝对值 MAV、均方根 RMS、波形长度 WL、过零率 ZC。频域常用中值频率 MDF 和平均功率频率 MPF。实现如下def extract_features(window): window: (T, C)返回每通道特征拼接向量 feats [] for c in range(window.shape[1]): x window[:, c] mav np.mean(np.abs(x)) rms np.sqrt(np.mean(x ** 2)) wl np.sum(np.abs(np.diff(x))) zc np.sum(np.diff(np.sign(x)) ! 0) # 频域 freqs np.fft.rfftfreq(len(x), d1.0 / FS) psd np.abs(np.fft.rfft(x)) ** 2 mdf freqs[np.searchsorted(np.cumsum(psd), psd.sum() / 2)] feats.extend([mav, rms, wl, zc, mdf]) return np.array(feats)逻辑说明每个通道提 5 个特征8 通道就是 40 维。zc用符号变化次数近似实际工程里会加一个阈值避免噪声引起的虚假过零。mdf用累积功率谱找中值频率是频域疲劳相关特征。参数上窗口长度一般取 150–250 ms重叠 50%这个后面切窗章节细说。3.3 滑动窗口切分与特征矩阵构建肌电是连续流分类要切成固定长度窗口。常见做法是窗长 200 ms、步长 100 ms50% 重叠def sliding_window(sig, win_len, step): sig: (T, C) - list of (win_len, C) windows [] for start in range(0, len(sig) - win_len 1, step): windows.append(sig[start:start win_len]) return windows WIN 200 # 200 ms STEP 100 # 50% 重叠 X, y [], [] for sig, label in zip(signals, labels): sig preprocess(sig, FS) for w in sliding_window(sig, WIN, STEP): X.append(extract_features(w)) y.append(label) X, y np.array(X), np.array(y)参数说明WIN太短特征估计不稳太长则丢失动作切换的时间分辨率200 ms 是手势识别的常用折中。STEP决定样本量和重叠度步长越小样本越多但相邻样本高度相关划分训练测试集时要按试次划分不能随机打散窗口否则同一试次的窗口同时进训练和测试准确率虚高——这是肌电分类里最经典的踩坑。4. 分类模型训练从 SVM 基线到 CNN 的选型与调参4.1 用 SVM 跑通第一个基线特征提完后先用 SVM 建立基线确认整条链路通了再上深度模型from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline from sklearn.model_selection import GroupKFold, cross_val_score # groups 标记每个窗口来自哪个试次防止泄漏 groups np.array([...]) # 与 X 等长 clf make_pipeline(StandardScaler(), SVC(kernelrbf, C10, gammascale)) scores cross_val_score(clf, X, y, cvGroupKFold(n_splits5), groupsgroups) print(CV 准确率:, scores.mean())逻辑说明StandardScaler对特征标准化SVM 对尺度敏感这步不能省。GroupKFold按试次分组交叉验证保证同一试次的窗口不跨折这是避免数据泄漏的关键。参数上C10控制惩罚强度gammascale是 1/(n_features * X.var()) 的自适应设置先跑通再网格搜索。4.2 一维 CNN 直接吃原始信号如果特征工程效果到瓶颈可以试一维 CNN 端到端。输入形状(T, C)用时间卷积提局部模式import torch import torch.nn as nn class EMGNet(nn.Module): def __init__(self, n_channels8, n_classes6): super().__init__() self.conv nn.Sequential( nn.Conv1d(n_channels, 32, kernel_size5, padding2), nn.BatchNorm1d(32), nn.ReLU(), nn.MaxPool1d(2), nn.Conv1d(32, 64, kernel_size3, padding1), nn.BatchNorm1d(64), nn.ReLU(), nn.AdaptiveAvgPool1d(1), ) self.fc nn.Linear(64, n_classes) def forward(self, x): # x: (B, C, T) x self.conv(x).squeeze(-1) return self.fc(x)参数说明kernel_size5对应约 5 ms 的感受野适合捕捉肌电的局部爆发BatchNorm1d稳定训练AdaptiveAvgPool1d(1)把时间维压成 1得到通道级全局特征。注意输入要先转置成(B, C, T)PyTorch 的 Conv1d 通道维在中间。训练时学习率 1e-3、Adam、batch 64 起步类别不平衡就加weight到 CrossEntropyLoss。4.3 训练集/验证集划分与数据泄漏防范肌电数据划分必须按「受试者」或「试次」分组不能按窗口随机分。跨受试者场景下用留一受试者交叉验证LOSO每次留一个受试者做测试其余训练。这样得到的准确率才反映真实泛化能力。同一受试者内划分也要保证训练和测试的试次不重叠。很多论文里 99% 的准确率就是随机分窗口分出来的换到新受试者直接掉到 60%这个差距就是数据泄漏的代价。5. 避坑与排查肌电分类里最容易翻车的五件事5.1 准确率异常高先怀疑数据泄漏现象交叉验证准确率 98% 以上换新数据暴跌。原因按窗口随机划分同一试次的相邻窗口高度相关训练集和测试集共享信息。解决用GroupKFold按试次或受试者分组重跑验证接受一个更真实的数字。5.2 通道顺序或单位不一致导致特征失真现象模型在 A 受试者上正常B 受试者上完全失效。原因不同 session 的电极贴放位置或通道顺序不同甚至单位从伏变成毫伏。解决读数据时打印每通道的均值和量级跨 session 对齐通道映射统一单位。5.3 工频陷波把有用信号一起削掉现象滤波后信号幅度明显变小分类变差。原因陷波 Q 值太低50 Hz 附近带宽过宽连带削掉 40–60 Hz 的肌电能量。解决把q提到 30–50或改用自适应工频抑制先看滤波前后频谱对比再定。5.4 窗口长度和步长选错样本量或时间分辨率崩了现象样本太少训不动或预测延迟大到没法实时用。原因窗长 500 ms 以上样本少且延迟高窗长 50 ms 以下特征估计方差大。解决手势识别从 200 ms 窗、100 ms 步长起步实时场景再压到 150 ms 并测端到端延迟。5.5 类别不平衡被整体准确率掩盖现象整体准确率 85%但少数类召回率接近 0。原因数据不平衡模型偏向多数类。解决看混淆矩阵和每类 F1用类别权重或过采样别只盯整体准确率。6. 进阶技巧用受试者自适应把跨人准确率拉回来跨受试者准确率低是肌电分类的固有难点因为每个人的肌肉解剖和电极阻抗都不同。一个实用技巧是受试者自适应先用所有受试者数据预训练一个模型再用目标受试者的少量标注数据微调。具体做法是冻结卷积层只微调全连接层和 BatchNorm 的统计量def adapt(model, support_loader, epochs20, lr1e-4): support_loader: 目标受试者少量标注数据 for p in model.conv.parameters(): p.requires_grad False optimizer torch.optim.Adam(filter(lambda p: p.requires_grad, model.parameters()), lrlr) model.train() for _ in range(epochs): for x, y in support_loader: optimizer.zero_grad() loss nn.CrossEntropyLoss()(model(x), y) loss.backward() optimizer.step() return model参数说明lr1e-4比预训练小一个量级避免破坏已学到的通用特征epochs20在少量数据上够收敛又不过拟合。实测里每个新受试者只要 5–10 个试次的标注跨人准确率能提 10–20 个百分点。验证方法上固定一个受试者做测试对比自适应前后的混淆矩阵看哪些类被救回来了。我自己踩过的最大教训是别一上来就堆深度模型先用 SVM 加正确的分组验证把基线跑实再决定要不要上 CNN。很多「模型不行」的问题其实是数据划分和预处理没做对。希望帮到你。本文还有配套的精品资源点击获取
返回列表