肌电信号分类数据集与代码:从数据组织到可复现分类流水线

发布时间:2026/10/20 6:10:31
肌电信号分类数据集与代码:从数据组织到可复现分类流水线
简介这份资源面向生物医学工程、康复医学与人机交互方向的学习者和研究者围绕表面肌电信号sEMG分类任务提供从特征抽取到分类器评估的完整实践材料适合具备一定机器学习基础、希望上手肌电信号分析的读者。压缩包共7个文件以6个Python脚本和1个数据压缩包为主脚本分别对应主成分分析、支持向量机递归消除法、相关性热力图等特征处理方法以及KNN、SVM、随机森林三种分类算法的实现数据包用于训练与测试。整体约14.8MB结构清晰便于按特征抽取与分类两条线索对照学习。目前已有537人学习下载。读者可借助这些代码理解PCA降维、SVM-RFE特征选择与相关性热力图去冗余的具体做法并对比不同分类器在sEMG数据上的表现为假肢控制、运动分析等课题积累可复用的实验流程与调参思路。1. 肌电信号分类数据集与代码从一段手臂信号到可复现的分类流水线肌电信号分类数据集及相关代码这个标题背后其实是一条很典型的落地链路采集表面肌电sEMG信号做预处理和特征提取再训练分类器识别手势或动作。很多做康复辅具、人机交互、假肢控制的团队卡住的地方往往不是模型本身而是数据集怎么组织、标签怎么对齐、代码怎么跑通。我见过不少项目模型结构抄得挺像结果一上手就翻车问题全出在数据切窗和通道对齐上。这篇笔记就按我实际做过的方案把数据集结构、预处理、特征工程、分类模型和排错路径讲清楚适合刚拿到 sEMG 数据不知从哪下手的新手也适合想核对参数边界的熟手。2. 数据集怎么组织通道、标签、采样率三件事先对齐2.1 表面肌电数据的典型结构表面肌电信号通常来自贴在皮肤表面的电极阵列常见的是 8 通道或 16 通道采样率从 200 Hz 到 2000 Hz 不等。做分类任务时数据集一般按「受试者-动作-试次」三层组织。每个试次是一段连续信号对应一个动作标签。我一般会把原始数据存成二维数组形状是(采样点, 通道数)标签单独存一列用整数编码动作类别。这里有个容易被忽略的点不同受试者的信号幅值差异很大电极贴的位置稍有偏移同一动作的波形就变了。所以数据集里必须保留受试者 ID后面做跨受试者验证时要用。常见做法是每个受试者单独一个文件夹里面按动作类别分子目录每个试次一个 CSV 或 npy 文件。这种结构虽然朴素但复现和排查都方便。import numpy as np import os # 假设数据目录结构data/受试者ID/动作类别/试次.npy # 每个 npy 文件形状为 (采样点, 通道数) def load_dataset(root_dir): X, y, subject_ids [], [], [] for subject in sorted(os.listdir(root_dir)): subject_dir os.path.join(root_dir, subject) if not os.path.isdir(subject_dir): continue for label_name in sorted(os.listdir(subject_dir)): label_dir os.path.join(subject_dir, label_name) if not os.path.isdir(label_dir): continue for trial_file in os.listdir(label_dir): if not trial_file.endswith(.npy): continue file_path os.path.join(label_dir, trial_file) signal np.load(file_path) # (采样点, 通道数) X.append(signal) y.append(int(label_name)) # 动作类别用整数编码 subject_ids.append(subject) return X, np.array(y), np.array(subject_ids)这段代码做的是最基础的加载逻辑很直白按受试者、动作、试次三层遍历把信号和标签分别收集。参数上要注意label_name必须是可转整数的字符串如果动作类别是文字描述需要先建一个映射表。subject_ids保留下来是为了后面做留一受试者交叉验证这个在 sEMG 分类里比随机划分更可信。2.2 采样率与窗口长度的取舍采样率决定了你能保留多高的频率成分。sEMG 的主要能量集中在 20 到 500 Hz按奈奎斯特采样定理采样率至少 1000 Hz 才比较稳妥。但实际项目中很多设备默认 200 Hz 或 500 Hz这时候高频细节丢了特征提取时就要避开那些依赖高频的指标。窗口长度是另一个关键参数。做实时分类时窗口太长延迟高太短则特征不稳定。我一般用 200 到 300 毫秒的窗口重叠 50%。以 1000 Hz 采样率算就是 200 到 300 个采样点一个窗。下面这个切窗函数把连续信号切成固定长度的样本同时保留重叠。def sliding_window(signal, window_size, step_size): signal: (采样点, 通道数) window_size: 窗口采样点数 step_size: 滑动步长 返回: (样本数, 窗口大小, 通道数) samples [] for start in range(0, signal.shape[0] - window_size 1, step_size): end start window_size samples.append(signal[start:end, :]) return np.array(samples) # 示例1000 Hz 采样300 ms 窗口50% 重叠 window_size 300 step_size 150 windows sliding_window(signal, window_size, step_size)切窗时有个血泪经验如果试次末尾不足一个窗口直接丢弃不要补零。补零会在特征里引入虚假的低能量段分类器很容易学到这个伪影。参数上step_size等于window_size时无重叠适合离线分析实时场景一般用 50% 重叠兼顾响应速度和稳定性。2.3 标签对齐与类别平衡标签对齐说的是窗口标签怎么定。一个试次对应一个动作切出来的所有窗口都继承这个动作标签。但如果试次之间有过渡段比如从一个动作换到另一个动作过渡段的信号既不像前一个动作也不像后一个硬打标签会污染数据。常见做法是每个试次掐头去尾去掉前 200 ms 和后 200 ms只保留中间稳定段。类别平衡方面sEMG 数据集经常出现某些动作试次多、某些少的情况。我一般先统计每个类别的窗口数如果差距超过 3 倍就用欠采样或加类别权重。加权重更简单在训练时给少数类更高的损失权重不用动数据本身。3. 特征提取与分类模型时域、频域和深度学习的选型3.1 时域特征计算快适合嵌入式时域特征是最早被用在 sEMG 分类里的计算量小适合跑在单片机或嵌入式设备上。常用的有均方根RMS、绝对值均值MAV、过零率ZC、波形长度WL。这些特征对窗口内信号做统计每个通道算一个值8 通道就是 8 维特征。def extract_time_domain(windows): windows: (样本数, 窗口大小, 通道数) 返回: (样本数, 通道数 * 4) features [] for w in windows: rms np.sqrt(np.mean(w ** 2, axis0)) mav np.mean(np.abs(w), axis0) zc np.sum(np.diff(np.sign(w), axis0) ! 0, axis0) wl np.sum(np.abs(np.diff(w, axis0)), axis0) features.append(np.concatenate([rms, mav, zc, wl])) return np.array(features)这段代码对每个窗口算四种时域特征然后拼成一个长向量。参数上没什么可调的但要注意zc对噪声敏感信号里如果有高频噪声过零率会虚高。我一般先做 20 到 450 Hz 的带通滤波再提特征。时域特征的分类器常用 LDA 或 SVM训练快小数据集上表现稳定。3.2 频域与时频特征中频段信息更丰富频域特征用 FFT 或功率谱密度能反映肌肉疲劳和收缩力度。中值频率MDF和平均功率频率MPF是两个经典指标。时频特征则用短时傅里叶变换或小波变换把信号拆成时间和频率的二维表示。实际项目中如果动作类别多、相似度高纯时域特征不够用我会加上频域特征。做法是对每个窗口做 FFT取 20 到 450 Hz 的功率谱然后算 MDF 和 MPF。这样每个通道多两维8 通道就是 16 维和时域特征拼起来特征维度到 48 维左右。维度上去了SVM 的核函数选择就重要了一般用 RBF 核gamma设成1/特征数。3.3 深度学习方案什么时候值得上深度学习在 sEMG 分类里不是万能药。数据量小的时候CNN 或 LSTM 很容易过拟合。我一般建议如果总窗口数少于 5000先用传统特征加 SVM 打基线。超过这个量级再考虑一维 CNN。下面是一个简单的一维 CNN 结构输入是(窗口大小, 通道数)输出是类别数。import torch import torch.nn as nn class EMGNet(nn.Module): def __init__(self, num_channels, num_classes): super().__init__() self.conv1 nn.Conv1d(num_channels, 32, kernel_size5, padding2) self.conv2 nn.Conv1d(32, 64, kernel_size5, padding2) self.pool nn.MaxPool1d(2) self.fc nn.Linear(64 * (window_size // 4), num_classes) self.relu nn.ReLU() self.dropout nn.Dropout(0.5) def forward(self, x): # x: (batch, 通道数, 窗口大小) x self.relu(self.conv1(x)) x self.pool(x) x self.relu(self.conv2(x)) x self.pool(x) x x.view(x.size(0), -1) x self.dropout(x) return self.fc(x)这个网络两层卷积加两次池化最后全连接分类。参数上kernel_size用 5 是常见起点dropout设 0.5 防过拟合。训练时学习率用 1e-3batch size 用 32 或 64。要注意输入维度顺序PyTorch 的 Conv1d 要求通道在前所以加载数据后要转置一下。3.4 交叉验证留一受试者比随机划分更靠谱sEMG 分类里随机划分窗口做交叉验证会高估性能因为同一个试次的窗口可能同时出现在训练集和测试集。正确做法是留一受试者交叉验证每次拿一个受试者的数据做测试其余受试者做训练。这样测出来的准确率更接近实际部署时的表现。from sklearn.model_selection import LeaveOneGroupOut from sklearn.svm import SVC from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline logo LeaveOneGroupOut() clf make_pipeline(StandardScaler(), SVC(kernelrbf, C1.0, gammascale)) scores [] for train_idx, test_idx in logo.split(features, labels, groupssubject_ids): clf.fit(features[train_idx], labels[train_idx]) score clf.score(features[test_idx], labels[test_idx]) scores.append(score) print(f平均准确率: {np.mean(scores):.3f})这段代码用LeaveOneGroupOut按受试者分组groups传受试者 ID。StandardScaler放在管道里保证标准化只在训练集上拟合。参数上SVM 的C控制正则强度gamma用scale让 sklearn 自动按特征方差调整。如果准确率波动大先检查特征里有没有受试者相关的偏置。4. 避坑与排查sEMG 分类里最容易翻车的五个地方4.1 现象训练准确率很高测试准确率断崖下跌原因通常是数据泄漏。同一个试次切出来的窗口高度相关随机划分时训练集和测试集里都有同一试次的窗口模型记住了试次特征而不是动作特征。解决方法是按试次或按受试者划分用GroupKFold或LeaveOneGroupOut分组变量传试次 ID 或受试者 ID。4.2 现象模型对某些受试者完全失效原因是受试者之间的信号分布差异大电极位置、皮肤阻抗、肌肉厚度都会影响幅值和频谱。解决方法是做受试者归一化比如按受试者做 z-score 标准化或者用对抗训练让特征对受试者不敏感。简单做法是每个受试者的数据单独减均值除标准差再合并。4.3 现象实时分类时延迟高、抖动大原因是窗口太长或重叠太多导致每次预测要等很久而且相邻窗口预测结果跳变。解决方法是缩短窗口到 150 到 200 ms降低重叠到 25%再加一个多数投票或指数平滑。投票窗口用 3 到 5 个连续预测取众数能明显减少抖动。4.4 现象特征维度高但准确率不升反降原因是特征冗余和过拟合。时域、频域特征拼在一起很多维度是相关的。解决方法是先做特征选择用SelectKBest或递归特征消除把维度降到 20 到 30 维。或者直接用 PCA 降维保留 95% 方差。我一般先看特征相关性矩阵相关系数超过 0.9 的成对特征只留一个。4.5 现象工频干扰导致频谱特征异常原因是 50 Hz 或 60 Hz 的电源干扰混进信号FFT 后在对应频点出现尖峰。解决方法是做 50 Hz 陷波滤波或者带通滤波时把上限设到 45 Hz。但注意陷波滤波会引入相位失真如果做实时控制相位失真可能影响响应。我一般优先用带通滤波实在不行再加陷波。5. 把分类器跑成可复现的基线一个具体技巧5.1 固定随机种子与记录完整配置sEMG 分类的可复现性一半靠固定随机种子一半靠记录完整配置。随机种子影响数据划分、权重初始化和 dropout。我一般会在脚本开头统一设种子Python、NumPy、PyTorch 都设一遍。import random import numpy as np import torch def set_seed(seed42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) torch.backends.cudnn.deterministic True torch.backends.cudnn.benchmark False set_seed(42)这段代码把常用库的种子都固定住。cudnn.deterministic设 True 会让卷积结果可复现但速度会慢一点。如果做最终实验建议打开如果只是调参可以关掉换速度。5.2 用配置文件管理参数参数散在代码里过两周自己都忘了当时设了什么。我习惯用一个 YAML 或 JSON 文件存所有超参数训练脚本读配置结果目录里再存一份配置副本。这样每次实验都能追溯到具体参数。# config.yaml data: window_size: 300 step_size: 150 sampling_rate: 1000 features: time_domain: true freq_domain: true model: type: svm C: 1.0 gamma: scale validation: method: leave_one_subject_out seed: 42配置文件的好处是换数据集或换模型时只改配置不动代码。我一般还会在结果目录里存一份config_used.yaml和模型文件放一起。这样半年后回头看能立刻知道当时跑了什么。5.3 基线先跑通再谈优化很多新手一上来就调深度学习结果连数据加载都没对齐。我的习惯是先用最简单的特征加 LDA 跑一个基线确认数据管道没问题再逐步加特征、换模型。基线不用高但必须可复现。基线跑通后每次改动只动一个变量记录准确率变化。这样即使最后模型没调好也知道哪一步带来了提升。我自己的教训是曾经在一个项目里花了三天调 CNN 结构最后发现是切窗时把通道顺序搞反了。从那以后我每次拿到新数据先画几个窗口的波形图肉眼确认通道和标签对得上再往下走。这个习惯帮我省了很多后悔药。希望帮到你。本文还有配套的精品资源点击获取