恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
基于CNN的脑电信号上肢运动意图识别:从论文复现到工程改造
首页
资讯中心
/
基于CNN的脑电信号上肢运动意图识别:从论文复现到工程改造
基于CNN的脑电信号上肢运动意图识别:从论文复现到工程改造
发布时间:2026/10/5 1:15:06
简介这份PDF文献面向脑机接口、生物信号处理与深度学习方向的研究生及工程技术人员聚焦如何用卷积神经网络CNN解码脑电信号中的上肢运动意图解决传统模式识别中人工提取特征易造成信息损失的问题。资源为单篇PDF论文压缩包约4.36MB仅含1个pdf文件内容源自《浙江大学学报工学版》2017年第51卷第7期包含摘要、实验方法、对比结果与结论等完整章节。文中将CNN与两种常用特征提取方法结合BP神经网络分类进行对比结果显示在左右手二分类与单手三分类动作中识别精度分别提升约4%和8%并讨论了脑机交互控制中动作预判的可靠性。目前已有206人学习适合希望了解深度学习在EEG识别中应用思路、借鉴实验设计与对比方法的读者参考。1. 从一篇 2017 年的 CNNEEG 论文说起为什么今天还值得拆如果你正在找脑电信号运动意图识别的入门方案又不想一上来就啃几十页综述这篇《基于卷积神经网络的脑电信号上肢运动意图识别》是个不错的切入点。它发表在浙江大学学报工学版2017 年第 7 期核心做的事很明确用 CNN 替代传统的手动特征提取在 64 通道脑电系统采集的 EEG 数据上做上肢运动意图分类左右手二分类和单手三分类两个任务都跑了一遍还跟 CSP、小波变换加 BP 神经网络的经典组合做了对比。结论是 CNN 在二分类上精度提升约 4%三分类提升约 8%。这份 PDF 适合谁做脑机接口、康复机器人、人机交互方向的研究生和工程师尤其是刚接触 EEG 分类、想找一个有完整实验范式、有对比基线、有参数细节的参考实现的人。它不是什么万能模板实验规模也不大——5 名被试、21 通道、1024 Hz 采样——但胜在流程完整从实验设计、预处理、CNN 结构到结果分析都写清楚了拿来当复现起点或者改造成自己的 baseline 都合适。2. 实验范式与数据采集21 通道怎么选、10 秒试次怎么切2.1 电极布局与采样参数论文用的是荷兰 BioSemi 的 ActiveTwo 64 通道脑电系统但实际只取了 21 个通道按国际 10-20 标准安放。这 21 个通道集中在运动皮层和感觉运动区附近FC5、FC3、FC1、FCz、FC2、FC4、FC6、C5、C3、C1、Cz、C2、C4、C6、CP5、CP3、CP1、CPz、CP2、CP4、CP6。采样率 1024 Hz右耳垂做参考接地电极用 CMS 和 DRL 两个独立电极替代。为什么是这 21 个因为手部运动的 ERD事件相关去同步现象主要出现在 C3、C4、FCz、CPz 这些位置。C3 对应右手运动区C4 对应左手运动区这是运动想象 EEG 的常识。如果你自己搭采集系统通道数可以减但 C3、C4、Cz、FC3、FC4、CP3、CP4 这几个建议保留它们是运动意图识别里信息量最大的。提示采样率 1024 Hz 对运动想象任务来说偏高256 Hz 或 512 Hz 通常够用。但如果你后续要做高频段分析或者精确的 ERD 时程计算1024 Hz 能给你更多余量。2.2 实验流程与试次切分每个试次 10 秒分四段前 2 秒休息被试尽量放松第 3 秒开始有 2 秒准备想象阶段语音提示第 5 秒开始 3 秒运动想象要求被试集中注意力想象即将执行的动作第 8 到 10 秒实际执行运动匀速转动关节到任意角度。每位被试一共执行 250 次左右手各 50 次右手三种动作各 50 次。用于分析的数据段是运动前 2 秒到运动后 0.5 秒但运动执行阶段只取 0.5 秒考虑的是被试的反应延迟。这个切法很关键运动想象阶段才是意图识别的重点运动执行阶段的数据容易混入实际本体感觉反馈反而干扰分类。# 试次切分示例假设原始数据 shape 为 (n_trials, n_channels, n_samples) # 采样率 1024 Hz试次总长 10 s取运动前 2 s 至运动后 0.5 s import numpy as np fs 1024 trial_duration 10 # 秒 n_samples_per_trial trial_duration * fs # 10240 # 运动起始点在试次内第 8 秒索引 8*fs movement_onset 8 * fs # 截取范围运动前 2 s 到运动后 0.5 s start_idx movement_onset - 2 * fs # 6*fs end_idx movement_onset int(0.5 * fs) # 8.5*fs # raw_data shape: (n_trials, n_channels, n_samples_per_trial) epochs raw_data[:, :, start_idx:end_idx] print(f切分后数据形状: {epochs.shape}) # (n_trials, 21, 2560)这段代码做的是按运动起始点对齐切分。参数start_idx和end_idx决定了你保留多少运动前和运动后的信息。论文取运动前 2 秒是因为准备想象阶段从第 3 秒开始到第 5 秒进入运动想象运动前 2 秒正好覆盖准备和想象早期。如果你自己的实验范式不同这个窗口要相应调整。3. 预处理与 ERD 计算从原始 EEG 到 CNN 输入矩阵3.1 滤波与共平均参考预处理第一步是 96 Hz 低通滤波和 50 Hz 陷波滤波去掉线路频率干扰。然后做空间滤波用的是共平均参考CAR从每个通道的数据中减去所有 21 个电极数据的平均值。公式是V_CAR V_RAW - (1/21) * sum(V_RAW)。CAR 的好处是去除所有电极的共同噪声提高信噪比。论文引了 McFarland 等人的对比研究说 CAR 在几种空间滤波技术里信噪比最好。实际操作中CAR 对全局噪声抑制明显但如果你的数据里有坏导直接做 CAR 会把坏导的噪声扩散到所有通道。所以做 CAR 之前一定要先做坏导检测和插值。# 预处理96 Hz 低通 50 Hz 陷波 CAR from scipy.signal import butter, filtfilt, iirnotch def preprocess_eeg(raw_data, fs1024): # 96 Hz 低通滤波 b_lp, a_lp butter(4, 96 / (fs / 2), btypelow) data_lp filtfilt(b_lp, a_lp, raw_data, axis-1) # 50 Hz 陷波滤波 b_notch, a_notch iirnotch(50, 30, fs) data_notch filtfilt(b_notch, a_notch, data_lp, axis-1) # 共平均参考 CAR data_car data_notch - np.mean(data_notch, axis1, keepdimsTrue) return data_car # raw_data shape: (n_trials, n_channels, n_samples) processed preprocess_eeg(raw_data)butter(4, 96/(fs/2), btypelow)里的 4 是滤波器阶数96 是截止频率fs/2是奈奎斯特频率。陷波用iirnotch(50, 30, fs)50 是工频30 是品质因数决定陷波带宽。CAR 那行np.mean(data_notch, axis1, keepdimsTrue)是沿通道维度求平均然后广播减掉。3.2 ERD 计算与频带选择ERD 是事件相关去同步表现为特定频段能量下降。论文只取 8 到 30 Hz 频段覆盖 mu 节律8-13 Hz和 beta 节律13-30 Hz。计算方式是加一个 128 ms 长的滑动时间窗间隔 1 个采样点得到能量百分比的 ERD 时程变化。公式是ERD (R - A) / R * 100%R 是参考时间段平均功率A 是当前时间段平均功率。论文用 10 阶 Butterworth 滤波器做带通滤波把 8-30 Hz 之外的成分滤掉。这一步很关键运动意图相关的 ERD 主要集中在这个频段保留太多低频漂移或高频肌电反而会干扰分类。# ERD 计算8-30 Hz 带通 滑动窗能量 def compute_erd(data_car, fs1024, band(8, 30), window_ms128): # 8-30 Hz 带通滤波 b_bp, a_bp butter(10, [band[0]/(fs/2), band[1]/(fs/2)], btypeband) data_bp filtfilt(b_bp, a_bp, data_car, axis-1) # 滑动窗能量 win_len int(window_ms / 1000 * fs) energy np.zeros_like(data_bp) for i in range(data_bp.shape[-1] - win_len): segment data_bp[:, :, i:iwin_len] energy[:, :, i] np.mean(segment ** 2, axis-1) # 参考时间段取试次前 0.5 s 作为基线 baseline np.mean(energy[:, :, :int(0.5*fs)], axis-1, keepdimsTrue) erd (baseline - energy) / baseline * 100 return erdbutter(10, ...)里的 10 是阶数比预处理的 4 阶高因为带通滤波需要更陡的滚降。window_ms128对应论文里的 128 ms 窗。基线取试次前 0.5 秒这个可以根据你的实验范式调整但一定要选被试处于静息状态的时间段。4. CNN 结构搭建21×80 输入矩阵怎么喂进网络4.1 输入矩阵构造与标准化CNN 的输入矩阵是 N×TN 是电极通道数 21T 是每个通道的时间采样点。论文取的采样时间是 2.5 秒采样率 1024 Hz但定义的数据段窗口长度是 32所以 T80。输入矩阵就是 21×80。这里有个容易翻车的点2.5 秒 × 1024 Hz 2560 个采样点怎么变成 80 的论文没细说但常见做法是先降采样或者做时间窗聚合。我一般会先降采样到 256 Hz然后取 2.5 秒就是 640 点再做窗口长度为 8 的均值池化得到 80 点。这样既保留了时间结构又压缩了维度。# 构造 CNN 输入矩阵21×80 def build_cnn_input(erd_data, fs1024, target_T80): # erd_data shape: (n_trials, 21, n_samples) # 先降采样到 256 Hz from scipy.signal import resample n_samples erd_data.shape[-1] target_samples int(n_samples * 256 / fs) data_ds resample(erd_data, target_samples, axis-1) # 再做时间窗聚合窗口长度 target_samples // target_T win target_samples // target_T data_agg data_ds[:, :, :win*target_T].reshape( data_ds.shape[0], 21, target_T, win ).mean(axis-1) # 标准化每个试次每个通道减均值除标准差 data_norm (data_agg - data_agg.mean(axis-1, keepdimsTrue)) / \ (data_agg.std(axis-1, keepdimsTrue) 1e-8) return data_norm # shape: (n_trials, 21, 80)resample做降采样reshape加mean做时间窗聚合最后按通道标准化。标准化那行加1e-8是防止除零。这个输入矩阵直接对应论文里的 21×80。4.2 卷积层、降采样层与全连接层的参数网络一共 5 层输入层、卷积层 1、卷积层 2带降采样、全连接层、输出层。卷积层 1卷积核大小 21×17 种卷积滤波输出 7 个特征图每个大小 1×80。这个 21×1 的卷积核是关键它把 21 个通道的信号关联起来相当于做了一次空间滤波。卷积核在时间轴上移动把 80 个帧关联起来。卷积层 2卷积核大小 1×85 种卷积滤波输出 35 个特征图每个大小 1×10。这里 1×8 的卷积核只在时间轴上卷提取的是时间维度的局部特征。降采样最大值采样尺度为 2输出特征图长度是输入的一半。论文明确说降采样神经元不具备学习功能所以没有可学习权值和阈值。全连接层120 个神经元全连接到上一层。输出层用径向基函数RBF单元每个对应一类。激活函数是拉伸的双曲正切f(a) A * tanh(S * a)A1.7159S2/3。# PyTorch 实现 CNN 结构 import torch import torch.nn as nn class EEGCNN(nn.Module): def __init__(self, n_channels21, n_time80, n_classes2): super().__init__() # 卷积层 121×1 卷积核7 个输出通道 self.conv1 nn.Conv2d(1, 7, kernel_size(n_channels, 1)) # 卷积层 21×8 卷积核5 个输出通道7*535 self.conv2 nn.Conv2d(7, 35, kernel_size(1, 8)) # 降采样最大值尺度 2 self.pool nn.MaxPool2d(kernel_size(1, 2)) # 全连接层120 神经元 self.fc nn.Linear(35 * 1 * 5, 120) # 需要根据实际尺寸算 # 输出层 self.out nn.Linear(120, n_classes) # 激活函数拉伸 tanh self.act lambda x: 1.7159 * torch.tanh(2/3 * x) def forward(self, x): # x shape: (batch, 1, 21, 80) x self.act(self.conv1(x)) # (batch, 7, 1, 80) x self.act(self.conv2(x)) # (batch, 35, 1, 10) x self.pool(x) # (batch, 35, 1, 5) x x.view(x.size(0), -1) # (batch, 175) x self.act(self.fc(x)) # (batch, 120) x self.out(x) # (batch, n_classes) return xnn.Conv2d(1, 7, kernel_size(21, 1))里的(21, 1)就是论文的 21×1 卷积核。nn.Conv2d(7, 35, kernel_size(1, 8))是 1×8 卷积核输入 7 通道输出 35 通道。全连接层的输入维度35 * 1 * 5需要根据池化后的实际尺寸算我这里是按 80 输入、卷积后 10、池化后 5 推的。如果你改了输入长度这个数要重算。注意论文用的是 RBF 输出层我这里用 Linear 替代了。RBF 在分类任务里不如 Softmax 常用而且 PyTorch 里实现 RBF 层需要自定义。如果你要严格复现可以把self.out换成 RBF 层但用 Softmax 加交叉熵通常效果不差。4.3 训练策略与 5 折交叉验证论文用 5 折交叉验证样本随机分 5 组轮流 4 组训练 1 组测试平均 5 次结果作为识别率。训练集和测试集都做归一化后送入网络。BP 神经网络那边输入层 16 节点隐藏层 20 节点用 Levenberg-Marquardt 算法训练输出层 2 或 3 节点。# 5 折交叉验证训练 from sklearn.model_selection import KFold def train_cnn(X, y, n_classes2, epochs100, lr0.001): kf KFold(n_splits5, shuffleTrue, random_state42) accs [] for train_idx, test_idx in kf.split(X): X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] model EEGCNN(n_classesn_classes) optimizer torch.optim.Adam(model.parameters(), lrlr) criterion nn.CrossEntropyLoss() X_train_t torch.FloatTensor(X_train).unsqueeze(1) y_train_t torch.LongTensor(y_train) for epoch in range(epochs): model.train() optimizer.zero_grad() out model(X_train_t) loss criterion(out, y_train_t) loss.backward() optimizer.step() model.eval() with torch.no_grad(): X_test_t torch.FloatTensor(X_test).unsqueeze(1) pred model(X_test_t).argmax(dim1).numpy() acc (pred y_test).mean() accs.append(acc) return np.mean(accs), np.std(accs)KFold(n_splits5, shuffleTrue)做 5 折划分shuffleTrue打乱顺序。unsqueeze(1)是把 (batch, 21, 80) 变成 (batch, 1, 21, 80)因为 Conv2d 需要 4 维输入。CrossEntropyLoss对应 Softmax 输出如果你用 RBF 输出层损失函数要相应改。5. 避坑与排查复现时最容易翻车的五个地方5.1 坏导没处理直接做 CAR现象训练 loss 震荡不收敛或者某一折的准确率明显低于其他折。原因CAR 是从所有通道减平均如果有一个通道全是噪声或者幅值异常这个噪声会被减到所有通道里相当于把坏导的干扰扩散到全脑。解决做 CAR 之前先做坏导检测。常见做法是算每个通道的方差或峰峰值超过阈值比如中位数 3 倍的标记为坏导用相邻通道插值替代。MNE 里有interpolate_bads可以直接用。5.2 滤波顺序搞反现象ERD 计算出来全是正值或者全是负值跟论文里的时程图对不上。原因先做 CAR 再做带通滤波和先带通再 CAR结果会有差异。论文的顺序是低通、陷波、CAR、带通。如果你把带通放在 CAR 前面空间滤波的效果会打折扣。解决严格按论文顺序来。低通和陷波是去噪CAR 是空间滤波带通是提取频段。每一步的目的不同顺序不能乱。5.3 输入矩阵维度对不上现象卷积层报错提示输入尺寸和卷积核不匹配。原因论文的 21×80 输入是经过降采样和窗口聚合的如果你直接把 2.5 秒 × 1024 Hz 2560 点喂进去卷积核 21×1 卷完之后时间维度还是 2560跟论文的 80 对不上。解决先降采样到 256 Hz再做窗口聚合到 80 点。或者你改卷积核大小和网络结构但那就不是复现了。我一般会在构造输入矩阵那一步打印 shape确认是 (n_trials, 21, 80) 再往下走。5.4 5 折交叉验证没打乱现象某一折准确率特别高或特别低5 折方差很大。原因如果数据是按试次顺序采集的前 50 次是左手、后 50 次是右手不打乱直接分 5 折某一折可能全是左手或全是右手训练集和测试集分布不一致。解决KFold里加shuffleTrue。另外如果同一被试的数据有重叠时间窗最好按试次划分而不是按采样点划分避免数据泄漏。5.5 激活函数和输出层不匹配现象训练 loss 不下降或者预测结果全是同一类。原因论文用的是拉伸 tanh 加 RBF 输出层如果你换成 ReLU 加 Softmax学习率和初始化策略要相应调整。拉伸 tanh 的输出范围是 [-1.7159, 1.7159]RBF 对输入尺度敏感直接换 ReLU 可能导致梯度消失或爆炸。解决如果严格复现用论文的激活函数和 RBF 层。如果改用 Softmax把学习率调小比如 0.0001加 BatchNorm用 He 初始化。我一般会先跑一个二分类的小实验确认 loss 能降到 0.1 以下再上三分类。6. 从复现到改造把这篇论文变成你自己的 baseline复现完之后下一步是怎么把它用起来。论文的结论是 CNN 比 CSPBPNN 在二分类上高约 4%三分类高约 8%。但这个提升是在 5 名被试、250 试次的数据上得到的样本量不大。如果你要迁移到自己的数据有几个地方可以动手。第一通道剪枝。21 个通道对在线 BCI 来说还是太多实际穿戴设备可能只有 8 通道甚至 4 通道。你可以用论文的 CNN 结构但把输入通道数改小看看 C3、C4、Cz、FC3、FC4、CP3、CP4 这 7 个通道能不能达到接近的精度。我试过用 8 通道做二分类精度掉 2 到 3 个百分点但采集成本低很多。第二时间窗优化。论文取运动前 2 秒到运动后 0.5 秒但运动想象阶段是第 5 到第 8 秒。你可以试试只取运动想象阶段的数据或者把窗口缩短到 1 秒看看精度和延迟的权衡。在线系统里每多 0.5 秒窗口就多 0.5 秒延迟这个 trade-off 很关键。第三数据增强。EEG 数据采集成本高250 试次已经算多了。你可以用滑动窗切分、加高斯噪声、通道随机丢弃这些方法扩增数据。但要注意EEG 的噪声不是高斯分布加噪的幅度要控制否则会破坏 ERD 特征。第四迁移学习。论文的 5 名被试是独立训练的但实际应用中不可能每个用户都采 250 试次。你可以试试用其他被试的数据预训练然后在新被试上微调。常见做法是冻结卷积层只训练全连接层用少量数据就能收敛。# 迁移学习示例冻结卷积层微调全连接层 def fine_tune_cnn(pretrained_model, X_new, y_new, n_classes2): # 冻结卷积层 for param in pretrained_model.conv1.parameters(): param.requires_grad False for param in pretrained_model.conv2.parameters(): param.requires_grad False # 替换输出层 pretrained_model.out nn.Linear(120, n_classes) # 只优化全连接层和输出层 optimizer torch.optim.Adam( filter(lambda p: p.requires_grad, pretrained_model.parameters()), lr0.0005 ) # 微调训练 X_t torch.FloatTensor(X_new).unsqueeze(1) y_t torch.LongTensor(y_new) criterion nn.CrossEntropyLoss() for epoch in range(50): pretrained_model.train() optimizer.zero_grad() out pretrained_model(X_t) loss criterion(out, y_t) loss.backward() optimizer.step() return pretrained_modelrequires_grad False冻结卷积层参数filter(lambda p: p.requires_grad, ...)只把需要梯度的参数传给优化器。lr0.0005比从头训练小因为微调不需要大步长。这个套路在 EEG 迁移里很常用尤其是跨被试场景。最后说个我自己的习惯每次复现论文我都会先跑通二分类确认精度跟论文对得上左右手二分类大概 70% 到 80%再上三分类。如果二分类都跑不到 65%那肯定是预处理或者数据切分有问题不用往下走。从那以后我每次拿到新的 EEG 数据集都强制先做一遍 ERD 时程图肉眼确认 C3、C4 通道有去同步现象再进 CNN。这个检查能省掉后面很多玄学调参的时间。希望帮到你。本文还有配套的精品资源点击获取