帕金森脑电识别MATLAB工程:SVM分类与EEGLAB预处理全解析

发布时间:2026/10/25 14:17:46
帕金森脑电识别MATLAB工程:SVM分类与EEGLAB预处理全解析

简介:这是一套面向脑电信号分析研究者的帕金森病识别系统源码与数据集,基于MATLAB GUI与EEGLAB工具,完整覆盖从脑电数据采集分析、特征提取到SVM分类诊断的流程。压缩包共18个文件,具体包括6个mat数据文件(含帕金森患者与正常对照的原始脑电及特征数据)、6个m脚本(实现特征计算、GUI逻辑与SVM训练)、5个fig界面文件(可视化操作界面)以及1个doc说明文档,整体约21.95MB,结构清晰便于查阅。目前已有157人参与学习并下载。通过该资源可快速上手构建自己的脑电识别实验,学习如何利用EEGLAB预处理数据、设计交互界面、提取特征并调用SVM分类器;预览中还提供数据缩放与模型优化相关的辅助脚本,便于理解SVM分类前的处理环节,适合课程设计、毕业设计或科研入门参考,也可作为项目模板。

1. 帕金森脑电识别:一套不用从零搭的MATLAB完整工程

帕金森病的脑电信号分类,听着是纯科研方向,实际动手时最花时间的往往不是分类算法本身,而是数据文件怎么组织、GUI界面怎么调通、特征怎么和SVM的输入格式对齐。这套工程正好把这三件事打包成了一份完整的MATLAB项目:基于MATLAB 2016编写,集成EEGLAB辅助工具包,从帕金森患者脑电数据的采集分析起步,到计算需要的特征值,再到SVM诊断分类,全程都有对应的脚本、界面和测试数据。解压之后不需要再到处找脑电数据,wenti.mat、zheng.mat这些文件已经把患者组和正常组分好,能直接跑通实验链路。适合正在做脑电课题的学生、想把SVM快速用到真实脑电特征上的研究者,以及需要参考MATLAB GUI工程写法的开发人员。

2. 工程文件拆解:fig、m、mat三类文件的分工和数据链路

2.1 入口脚本和fig界面的对应关系

解压“帕金森脑电识别系统的建设和研究.rar”以后,你大概率会和我一样先愣一下:unsstitled.fig、a1.m到a4.m、scaleForSVM.m、wenti.mat、zheng.mat、preictal2.mat……文件名不统一,界面文件、脚本、数据文件混在一起。先说结论:这不是单个主程序一跑到底的工程,而是“GUI总控 + 分步处理脚本 + 数据/特征文件”的组合,得理清依赖再按顺序运行。

先说入口。项目里有四个主要脚本a1.m、a2.m、a3.m、a4.m和一个unsstitled.m,分别对应a1.fig、a2.fig、a3.fig、a4.fig和unsstitled.fig。按照MATLAB GUIDE工程惯例,fig文件存界面布局,m文件存回调逻辑。unsstitled.m大概率是GUI主入口,里面通过按钮回调去调用a1到a4完成不同阶段的处理。命名里的unsstitled其实应该是untitled,属于打包时保留了GUIDE默认文件名的情况,不影响运行,但会影响你在GUIDE里重新编辑界面时的显示名称。

判断谁是主入口、谁是被调用函数,最快的办法是打开unsstitled.m,搜索按钮回调函数里有没有process_a1或者a1(这样函数调用痕迹。常见写法是这样:

% 在GUI按钮回调里调用a1.m里的处理函数 pushbutton1_Callback(hObject, eventdata, handles) % hObject是当前控件句柄,eventdata是事件数据,handles是全局句柄结构体 % 如果a1.m定义的是function process_a1(),直接调用 process_a1(); % 处理完以后刷新界面数据 guidata(hObject, handles); end

这里有个关键判断:如果a1.m是以function开头的函数文件,就可以直接在回调里调用;如果它是纯脚本形式(没有function关键字),那只能通过run('a1.m')或evalin('base', ...)执行,变量容易在工作区里冲突。我建议拿到压缩包后先把每一个m文件打开看前两行,凡是function开头的就是可调用函数,一上来就是变量赋值的就是脚本。这套工程里a1到a4大概率是函数,因为GUI回调要传handles结构体,纯脚本很难做参数传递。

2.2 mat数据文件里到底存了什么

数据文件是这套资源最值钱的部分,因为不需要额外去医学数据平台找帕金森脑电数据。wenti.mat和zheng.mat分别对应帕金森患者(问题组)和正常人(对照组)的脑电数据;wenti_tezheng.mat是患者组的特征文件,zhengchang_tezheng.mat是正常组的特征文件,tezheng就是“特征”的音译。preictal2.mat这个名字有意思,preictal是癫痫研究里“发作前期”的术语,出现在帕金森工程里大概率是数据集整理时沿用了公共脑电数据集的命名习惯,不是分类目标,不用纠结。

直接load文件看变量结构,比猜快得多:

% 查看特征文件的变量名、维度和类型 clear; clc; load('wenti_tezheng.mat'); whos % 假设输出显示变量名为features_wenti,类型double,大小30x20 % 含义:30个患者样本,每个样本20个特征 load('zhengchang_tezheng.mat'); whos % 正常组特征文件,行数代表正常样本个数,列数必须和患者组一致 % 假设输出是30x20,说明两组样本量正好平衡

这里要特别留意维度对齐问题。SVM训练时患者组和正常组拼成一个大矩阵,假设患者组是P×M,正常组是Q×M,那么特征维度M必须完全相同。如果一个文件是20维,另一个是21维,SVM训练时直接报维度不一致。这个工程的文件命名已经把对应关系标清楚了:wenti_tezheng对zhengchang_tezheng,wenti对zheng,a1.fig对a1.m,下载后不建议改任何文件名,改了就得同步修改GUI回调里的引用。

2.3 推荐运行顺序和样本标签设计

把文件职责搞清楚后,运行顺序就清晰了。我一般按下面几步走:

  1. 用unsstitled.m打开GUI主界面
  2. 在界面里触发a1.m加载原始数据(wenti.mat和zheng.mat)
  3. 由a2.m完成特征提取,生成wenti_tezheng和zhengchang_tezheng
  4. 用scaleForSVM.m做特征归一化
  5. 用a3.m或a4.m完成SVM模型的训练和结果可视化

如果不想在GUI里一步步点按钮,也可以在命令行直接跑。注意每个脚本执行前要确认前置数据已经加载到工作区:

% 按依赖顺序执行(假设各m文件是脚本形式) clear; clc; % 第一步:加载患者组和正常组的原始数据 load('wenti.mat'); load('zheng.mat'); % 第二步:提取特征并保存为特征文件 % 这一步假设a2.m内部会生成wenti_tezheng和zhengchang_tezheng两个变量 run('a2.m'); % 第三步:归一化特征(scaleForSVM.m内部会处理) run('scaleForSVM.m'); % 第四步:设置标签,患者组为1,正常组为0 labels = [ones(size(wenti_tezheng, 1), 1); zeros(size(zhengchang_tezheng, 1), 1)]; features = [wenti_tezheng; zhengchang_tezheng]; % 第五步:训练SVM模型 model = fitcsvm(features, labels, 'KernelFunction', 'rbf', 'Standardize', true);

这段代码里labels的设计是核心:前一部分患者组是1,后一部分正常组是0,顺序必须和拼接后的特征矩阵完全对应。如果wenti_tezheng在前而zhengchang_tezheng在后,那labels必须先1后0。一旦顺序错位,训练出来的模型分类结果就是反的,测试时看着准确率正常,实际全错。

提示:fitcsvm是MATLAB自带统计工具箱里的SVM实现,scaleForSVM.m是libsvm工具包的配套函数。如果工程统一用了libsvm的svmtrain和svmpredict,那建议全程保持一致,不要混用两套接口。

3. 特征提取与SVM分类:从EEG信号到诊断结论的关键逻辑

3.1 为什么选SVM做帕金森脑电分类

脑电分类任务里,样本量通常不大,几十到几百例是常态,特征维度却可能到几十甚至上百维。这种小样本高维数据场景下,SVM的目标函数自带正则化项,比BP神经网络更不容易过拟合,也比随机森林在小样本上的稳定性更好。线性不可分的问题通过RBF核函数映射到高维空间就能解,这是SVM被脑电研究广泛使用的原因。

具体到这个工程里,特征文件的行数几十、列数二十左右,用BP网络很容易出现训练集表现好、测试集崩掉的情况。SVM配合交叉验证和网格搜索,在小样本上能给出相对稳定的结果。而且libsvm的matlab接口成熟,训练一次只需要零点几秒,方便反复调参数。

3.2 scaleForSVM.m的归一化处理

归一化这一步卡住了不少人。SVM的目标函数里有距离计算,如果某个特征的数值范围是0到10000,另一个是0到1,数值范围大的特征会主导目标函数,模型学习不到真正有区分度的特征。scaleForSVM.m做的就是这件事:把特征缩放到统一区间。

这部分我讲下libsvm配套归一化的常见写法:

% 最小最大值归一化,把特征缩放到[tol, 1]区间 function [scale_data, ps] = scaleForSVM(data, ymin, ymax) % data: 原始特征矩阵,每一行是一个样本,每一列是一个特征 % ymin和ymax是缩放目标的下界和上界,通常设0和1 % ps是缩放参数,测试数据要用同一组参数变换,不能重新计算 [n, m] = size(data); scale_data = zeros(n, m); ps = struct('xmin', zeros(1, m), 'xmax', zeros(1, m)); for j = 1:m xmin = min(data(:, j)); xmax = max(data(:, j)); ps.xmin(j) = xmin; ps.xmax(j) = xmax; if xmax == xmin % 特征取值恒定时,直接置为0,避免除零 scale_data(:, j) = 0; else scale_data(:, j) = (data(:, j) - xmin) / (xmax - xmin) ... * (ymax - ymin) + ymin; end end end

这里最关键的是ps这个结构体。训练时用患者和正常组的全部特征计算缩放参数,测试新样本时必须用同一组xmin和xmax做变换,不能拿新样本重新求最小值和最大值。举例来说,训练时某个特征的范围是2到10,新样本这个特征的值是12,如果用新样本重新归一化,那12会被缩放到1,而训练集里的10对应的是1,特征空间完全对不上。这个问题几乎每个做SVM的人都会踩一次,后面避坑章节还会再提。

3.3 特征矩阵构建和标签对齐

脑电特征提取这一步,工程里把原始脑电变成了特征矩阵。常用的特征包括功率谱密度、近似熵、样本熵、小波包能量等。帕金森脑电研究里,节律功率变化和熵特征是比较常见的分类依据,患者组在特定频段上的功率分布和正常人不一样,熵值上也有差异。

假设a2.m生成的wenti_tezheng是一个N×M矩阵,每一行是一条脑电记录,每一列是一个特征。正常组也是同样的维度。拼接时要注意把标签和数据的顺序对齐:

% 假设已经从特征文件读入了wenti_tezheng和zhengchang_tezheng % 患者组行数 n1 = size(wenti_tezheng, 1); % 正常组行数 n2 = size(zhengchang_tezheng, 1); % 构造标签:患者=1,正常=0 labels = [ones(n1, 1); zeros(n2, 1)]; features = [wenti_tezheng; zhengchang_tezheng]; % 打乱样本顺序,防止训练时同类样本集中在一起 rng(42); % 固定随机种子,保证可复现 idx = randperm(n1 + n2); features = features(idx, :); labels = labels(idx, :);

很多人会漏掉打乱这一步。如果不打乱,SVM训练时前一半全是患者、后一半全是正常,交叉验证划分数据时可能出现某一折全是同一类样本,导致验证准确率忽高忽低。使用randperm生成随机索引来洗牌,同时记录下索引,预测结束后可以回溯每一条样本的原始类别。

3.4 交叉验证与c、g参数网格搜索

SVM里真正影响结果的是RBF核的两个超参数:c(惩罚系数)和g(gamma,RBF核宽度相关)。c控制对误分类的容忍度,c太大容易过拟合,c太小容易欠拟合;g控制单个样本的影响范围,g太大时RBF径向基作用范围窄,决策边界曲折,g太小则决策边界过于平滑。脑电特征这种噪声较多的数据,建议用网格搜索加交叉验证来确定这两个参数。

% 用libsvm的网格搜索方式选择c和g % 先准备c和g的搜索范围 c_range = 2.^(-5:1:5); % 从2^-5到2^5,步长翻倍 g_range = 2.^(-7:1:5); % 从2^-7到2^5 best_acc = 0; best_c = 0; best_g = 0; for c = c_range for g = g_range % 5折交叉验证准确率 acc = svmtrain(labels, features, ... sprintf('-t 2 -c %f -g %f -v 5', c, g)); if acc > best_acc best_acc = acc; best_c = c; best_g = g; end end end % 用最优参数重新训练最终模型 model = svmtrain(labels, features, ... sprintf('-t 2 -c %f -g %f -q', best_c, best_g));

网格搜索的两个嵌套循环里,-t 2指定RBF核,-v 5表示5折交叉验证。acc是交叉验证平均准确率,不用它选参数的话,很容易选出一个在训练集上接近满分、在测试集上翻车的模型。这份工程里c和g的范围不一定需要改成别的,但如果你的数据换了,建议把取值范围扩大到2还-10到2的10次方之间,结合准确率变化趋势判断边界。

提示:libsvm参数里的c和g一个是cost,一个是gamma,网上很多中文资料把g写成“核函数宽度”,愿意理解为RBF核的分布宽度参数就好。

4. EEGLAB预处理:让原始脑电数据满足SVM输入要求的完整做法

4.1 MATLAB 2016下的EEGLAB配置

EEGLAB是开源的脑电处理工具箱,在这个工程里承担的是数据导入和预处理角色。MATLAB 2016版本对应的EEGLAB版本建议选14到17之间的release,太新的版本有些函数在2016里会因缺少依赖报错。配置方法很简单:把eeglab文件夹放到某一路径下,在MATLAB里把路径添加进去。

% 在MATLAB命令行里添加EEGLAB路径并启动 addpath('D:\eeglab'); % 改成你实际的eeglab文件夹路径 eeglab nogui; % nogui模式不弹图形界面,适合批处理

addpath添加的是eeglab根目录,EEGLAB启动时内部会自己把子目录加进去。如果出现找不到pop_loadset这类函数的错误,多半是路径没加对。这里有件事容易被忽视:EEGLAB启动时会创建ALLERP和ALLEEG等全局变量,如果在启动之前工作区里已经有同名变量,可能起冲突,所以建议每次启动前先clear all。

4.2 滤波、坏导剔除和重参考

脑电数据分析的第一步通常是滤波。帕金森研究里常用的带宽是1到50Hz或1到60Hz,目的是去掉低频漂移和高频噪声。滤波直接改变后续特征值,所以不能随意设置。EEGLAB里用pop_eegfilt即可:

% 对单个数据集做带通滤波,范围1-50Hz EEG = pop_eegfilt(EEG, 1, 50, [], 0, 0, 1); % 参数说明:1和50是截止频率,第5个参数是滤波器阶数设为0表示自动,第6个参数是边带 % 第7个参数1表示绘图显示滤波前后对比,0则不绘图

滤波之后要处理的是坏导联。看各导联的方差和功率谱,方差特别大或者功率谱明显异常的通道标记为坏导,用周围导联插值替换。EEGLAB里没有一键自动插值函数,常见做法是手动识别后调用pop_interp实现球面插值。这一步在帕金森脑电里很重要,头皮电极容易受肌肉运动干扰,眨眼和咀嚼产生的伪迹会直接影响后续特征值的数值稳定性。

重参考一般选平均参考,但如果你用的数据已经做了参考处理,跳过这一步就行。EEGLAB里重参考的命令是pop_reref(EEG, []),空数组表示平均参考。

4.3 分段、伪迹剔除与特征文件对接

特征提取前要把连续脑电切成分段。帕金森脑电没有明显的单个事件,通常按固定长度切段,比如每段2秒或4秒。切段的目的是增加样本量:一段30秒的记录切成15个2秒段,30次记录就有450个样本。EEGLAB里的操作是:

% 按固定长度分段,每段2秒,假设采样率是250Hz EEG = pop_epoch(EEG, {}, [-1 1], 'epochlimits', [-1 1]); % 第一个参数是数据集,第二个{}表示不按事件切分 % 第三个[-1 1]是切段区间,单位是秒 % 如果数据没有event标记,建议直接用reshape或者pop_select

更通用的做法是用reshape把连续数据直接切段。采样率250Hz、每段2秒就是每段500个采样点:

% 假设SPM格式:通道数x采样点数 % 输入data是 channels x time 的连续脑电矩阵 data = EEG.data; % EEG.data是 [通道数 x 采样点数 x 分段数] 三维数组 [nch, nsamples, nseg] = size(data); % 如果已经是epoched数据,可以直接计算特征 % 如果是连续数据,先reshape nsamples_seg = 250 * 2; % 2秒,采样率250Hz nseg = floor(size(data, 2) / nsamples_seg); data_epoched = reshape(data(:, 1:nseg*nsamples_seg), nch, nsamples_seg, nseg);

伪迹剔除这一步,最简单的做法是基于幅度阈值,超过±100微伏的分段删掉,更精细的做法是用ICA去掉眼电成分。ICA运行慢且在样本量小的时候不稳定,我的建议是先用幅度阈值和标准差阈值粗筛,保留足够样本再说。剔除完成后,把每个分段的特征计算出来,写入wenti_tezheng或zhengchang_tezheng对应的位置,就能和SVM流程对接。

5. 避坑记录:这套工程里我踩过的五个坑

5.1 中文路径导致fig文件和数据文件加载失败

现象:在MATLAB里双击unsstitled.fig,界面能开,但点击按钮运行时直接报错Unable to read file,或者load('wenti.mat')提示文件不存在。

原因:MATLAB 2016的中文编码处理能力有限,路径里含中文或空格时,文件读取和函数调用会解析失败。压缩包如果解压到“C:\Users\我\桌面\项目”这种情况,几乎必现。

解决:把整个工程文件夹放到纯英文路径下,比如D:\PD_EEG\,文件夹名也用英文。所有数据文件、fig文件路径里不能出现中文。改完路径后,先clear all,再重新运行程序。

5.2 SVM训练集准确率100%,测试集却只有70%

现象:用全部特征训练SVM,回代训练样本,准确率接近100%,但换一批数据测试掉到70%上下,有时甚至不如随机猜测。

原因:这是典型过拟合。特征维度可能偏高、样本量偏小,或者参数c选得太大,模型把训练集的噪声当成规律记下来了。

解决:先用5折交叉验证评估,交叉验证准确率才是可信指标。然后用网格搜索重新选c和g,把c的搜索范围往小调,比如改成2的-3次方到2的3次方。多观察不同c值下交叉验证准确率的变化曲线,选一个训练准确率不是最高、交叉验证却最稳的参数组合。

5.3 特征矩阵维度对不上

现象:load两个特征文件后拼矩阵执行features = [wenti_tezheng; zhengchang_tezheng],报错Dimensions of matrices being concatenated are not consistent。

原因:两个特征文件的行数可以不同,但列数必须相同。通常是一边在特征提取时多算或少算了一个特征,或者某一边预处理时通道数不一致。

解决:先whos查看两个文件的尺寸,确认列数。多出来的列直接裁掉:wenti_tezheng(:, 21) = []。如果两边列数都不一样说明特征提取脚本参数有改动,需要回到提取特征那一步检查。以后每次生成特征文件后,第一时间加一行断言:

assert(size(wenti_tezheng, 2) == size(zhengchang_tezheng, 2), ... '特征维度不一致,请检查特征提取脚本');

5.4 EEGLAB版本和MATLAB 2016的兼容问题

现象:eeglab启动时报错,提到Undefined function 'biosig'或找不到某个io函数;或者pop_eegfilt运行到一半提示变量不存在。

原因:EEGLAB版本太新,内部依赖的第三方插件(如biosig、firfilt)在MATLAB 2016里语法不兼容。新版EEGLAB有些函数改用arguments块定义,2016不支持这种语法。

解决:换用EEGLAB_14或15版本,这两个版本在MATLAB 2016里稳定。如果只是用滤波和重参考功能,直接自己用MATLAB的designfilt实现带通滤波,不启EEGLAB主界面,代码可以安心跑:

% 不依赖EEGLAB的简单带通滤波 fs = 250; % 采样率 [b, a] = butter(4, [1 50] / (fs/2), 'bandpass'); filtered_data = filtfilt(b, a, raw_data')'; % 零相位滤波

5.5 按钮思想绑定错误:改了文件名导致回调失效

现象:把unsstitled.m重命名成main.m以后,GUI按钮点击没有任何反应,命令行不报错。

原因:fig文件内部存的是原文件名对应的回调函数名,GUIDE导出时回调名和文件名绑定。文件名一改,按钮找不到原来的回调函数入口。

解决:不要改fig和m文件的对应关系。如果一定要重命名,在GUIDE里打开fig,逐个按钮的Callback属性重新选择新的m文件关联,再用guidata(hObject, handles)保存更新。过程中注意fig文件里可能残留原函数的调用,需要全部替换一致。

6. 验证与替换:把新数据接进这套系统的关键操作

工程里的这套链路跑通以后,真正要用起来还得做两件事:一是验证模型稳定性,二是把自己的数据替换进去。

稳定性验证我习惯用分层交叉验证,保证每一折里患者和正常的比例和整体一致:

% 分层5折交叉验证 cv = cvpartition(labels, 'KFold', 5); accuracies = zeros(cv.NumTestSets, 1); for i = 1:cv.NumTestSets train_idx = cv.training(i); test_idx = cv.test(i); % 训练时要对训练数据做归一化,再对测试数据用同一组参数 [train_scaled, ps] = scaleForSVM(features(train_idx, :), 0, 1); test_scaled = (features(test_idx, :) - repmat(ps.xmin, sum(test_idx), 1)) ... ./ repmat(ps.xmax - ps.xmin, sum(test_idx), 1); model_fold = svmtrain(labels(train_idx), train_scaled, ... sprintf('-t 2 -c %f -g %f -q', best_c, best_g)); [~, acc, ~] = svmpredict(labels(test_idx), test_scaled, model_fold); accuracies(i) = acc(1); end % 输出每折准确率和平均值 fprintf('每折准确率:%.2f%% %.2f%% %.2f%% %.2f%% %.2f%%\n', accuracies); fprintf('平均准确率:%.2f%%\n', mean(accuracies));

交叉验证里最容易被忽略的是测试数据的归一化。训练集归一化后得到ps.xmin和ps.xmax,测试集必须套同一个ps,不能用测试集自己的最小值最大值再算一遍。我刚才避坑章节里说过,这一步错了,准确率再高都不可信。

把新数据换进来时,只需要改三处:一是load的数据文件名换成你导入预处理好的数据;二是特征提取脚本里的特征类型和参数要和你训练时的定义一致;三是labels的构造方式,患者组1、正常组0的顺序不能乱。新样本单独预测的代码更简单:

% 新样本预测,假设new_data是1xM的特征向量 new_scaled = (new_data - ps.xmin) ./ (ps.xmax - ps.xmin); [pred_label, prob_est] = svmpredict(1, new_scaled, model); % pred_label为1表示患者,0表示正常 % prob_est第一列是类别概率估计

这份工程给了我一个很重要的习惯:每次换数据源,我都会先跑一遍完整交叉验证流程,把每折准确率和平均准确率记录下来,再开始调参。不再直接用一百多的训练集准确率跟人汇报。从那以后再做脑电数据分类,我每次都会强制走一遍归一化参数保存、交叉验证记录、新样本预测验证这三步。希望这套拆解能帮你在自己的实验里少走几步弯路。

本文还有配套的精品资源,点击获取