SEED脑电情绪识别源码包:从mat读取到SVM/LSTM复现指南
简介面向计算机、自动化等专业课程设计的SEED数据集EEG情绪识别系统Python源码及报告包源自大三学期经导师指导、严格评审并获96.5分的高分科研项目。内容覆盖原始EEG数据读取、特征处理、LDS/SVM与CNN建模等完整流程包含可运行Python源码、设计报告、说明文档及CNN/SVM结果记录既适合教师作为教学案例也适合学生直接作为课程设计、期末大作业或项目实战模板基础较好者可在此基础上修改实现类似功能。资源包共18个文件以Python代码、XML工程配置、TXT/MD说明文档、DOCX结果记录和TensorBoard事件文件为主总计约10.69MB目录区分代码、数据、结果与文档便于快速查阅与复用。目前已有97人学习下载对需要完整可运行情绪识别课设方案、参考高分报告写法或入门EEG建模的读者具有明确借鉴价值。1. 课设选它值在哪SEED脑电情绪识别源码包一个周末能复现出接近80%准确率如果你这学期课设选题是“SEED数据集EEG情绪识别系统”正在纠结要不要下载这份python源码包我建议先有一个预期这套资源里最容易翻车的不是模型训练而是mat文件读取和特征提取的前两公里。SEED是公开的62通道脑电情绪数据集包含15名被试、每人三次session、15段中文影片刺激对应正/中/负三类情绪标签。源码包把读数据、预处理、五频段微分熵特征、SVM与LSTM训练、报告文档串成了完整链路配合pip安装的依赖库两天之内能复现出一份能上台讲的课设报告。它适合python基础过关、第一次接触生理信号处理的本科学生也适合想拿脑电项目当简历素材的求职者。2. 数据入口与预处理62通道mat文件怎么读、50Hz工频怎么滤2.1 SEED数据集的结构15人、62通道、15段影片意味着什么SEED全称是SJTU Emotion EEG Dataset由上交大BCMI实验室公开发布。它和MNIST那种“一load就能用”的数据集完全不一样原始数据是连续记录的脑电波形按session组织每个session对应一个被试在一天内看完15个中文电影片段的全过程。片段与片段之间还有休息、广告残留所以第一步不是建模而是把数据从mat文件里挖出来并切干净。参数数值说明被试数量15人每人3个session分不同天采集通道数量62通道国际10-20扩展系统采样率1000Hz网上也常见降采样到200Hz的版本刺激材料15段影片每段约4分钟情绪诱发性经过预筛选情绪标签正/中/负三分类不包含效价和唤醒度分数数据格式.mat同时存在v5和v7.3两种存储版本理解“15×3个session”很重要。每个session内15段影片的顺序是固定的标签文件也是按这个顺序排列的。很多新手跳过这一层直接把文件名sort之后硬对齐结果准确率只有33%左右因为标签错位了。拿到包的第一件事是确认标签序列和刺激视频编号的映射关系而不是看模型代码。一个session的连续脑电波形长度大约是一万秒的量级不是这里需要重算15段×4分钟×60秒3600秒采样率1000Hz对应360万个采样点62通道就是223M个点float64存储大概1.7GB。这也是为什么SEED官方还提供了200Hz降采样版本的原因——文件小五倍训练速度也快五倍。如果你的机器内存紧张优先用降采样版本特征提取结果差异很小。2.2 读取mat文件v5与v7.3两种写法的兼容方案SEED的mat文件有个历史遗留问题不同批次的下载文件存储版本不一致。用scipy.io.loadmat读v5格式没问题但遇到v7.3会直接抛NotImplementedError原因是v7.3内部是HDF5容器scipy没有实现解析器。正确的做法是做一个结构探测兼容两种读法import numpy as np from scipy import io def load_seed_mat(path): 兼容读取SEED的.mat文件自动切换v5和v7.3两种格式 try: raw io.loadmat(path) # v5格式scipy直接读成dict字段一般是data和labels return raw except NotImplementedError: # v7.3格式scipy读不了改走h5py import h5py with h5py.File(path, r) as f: print(文件中包含的keys:, list(f.keys())) data np.array(f[data]) # h5py读出后通常是(采样点, 通道)顺序 labels np.array(f[labels]) # HDF5存储时维度顺序和MATLAB相反这里按需转置 return {data: data.T, labels: labels.flatten()} mat load_seed_mat(eeg.mat) print(原始数据形状:, mat[data].shape)这段代码的坑点全在注释里。h5py读取v7.3时得到的数组维度可能是按HDF5的C-order存储的即(采样点, 通道)而MATLAB原生顺序是(通道, 采样点)。如果你不转置后面所有按通道索引的逻辑都会静默出错模型训练时甚至不会报错只是结果差到怀疑人生。另外打印keys这行别删。不同来源的SEED文件字段名有小概率不一样有的是EEG、data、fltData混着来。先看一遍结构再往下走能省掉半小时排错。2.3 分段与滤波1秒窗口、0.5-70Hz带通和50Hz陷波脑电信号里有几个成分必须处理掉。首先是最低频段的漂移电极接触不良或头部微小移动都会产生基线漂移直接影响DE特征里的方差估计其次是50Hz工频干扰国内市电频率是50Hz脑电放大器再高级也压不干净最后是高过70Hz的肌电伪迹。常见做法是带通0.5-70Hz加一个50Hz陷波顺序上先陷波再带通。from scipy import signal def preprocess_eeg(eeg, fs1000, band(0.5, 70)): 对连续的62通道EEG做去工频和带通滤波 eeg: (channels, samples) # 先陷波50Hz工频Q值取30陷波带宽足够窄不会伤到旁边的alpha波 b_notch, a_notch signal.iirnotch(50, 30, fs) eeg signal.filtfilt(b_notch, a_notch, eeg, axis1) # 再带通0.5Hz以上去掉漂移70Hz以下去掉高频肌电 b_bp, a_bp signal.butter(4, [band[0], band[1]], btypebandpass, fsfs) eeg signal.filtfilt(b_bp, a_bp, eeg, axis1) return eeg.astype(np.float32)选filtfilt而不是lfilter是因为filtfilt是零相位滤波不会把波形的时间轴拉偏。脑电特征提取非常依赖时间对齐用lfilter会在每个切片上引入相位延迟同样的窗口切出来的特征意义就变了。Q值选择上30对固定的50Hz工频是合适的如果你发现滤波后信号被削掉太多把Q降到10让陷波带宽更宽一点。分段这一步放在滤波之后。窗口取1秒步长0.5秒也就是50%重叠。1秒窗口能在频率分辨率和时序粒度之间取到平衡点太短则低频段只有几个频点太长则样本数不够。用重叠窗口是为了把4分钟影片的样本量翻倍给后面的SVM喂更多数据。def segment_windows(eeg, win1000, hop500): eeg: (channels, samples)返回 (n_windows, channels, win) n_ch, n_samples eeg.shape starts np.arange(0, n_samples - win, hop) segments np.zeros((len(starts), n_ch, win), dtypenp.float32) for i, s in enumerate(starts): segments[i] eeg[:, s:s win] return segments3. 特征提取微分熵DE为什么是SEED默认答案五频段参数怎么定3.1 从功率谱到微分熵高斯分布假设下的简化公式SEED这类情绪识别任务里最常用的特征不是时域统计量而是频域里的微分熵Differential Entropy。逻辑说起来不复杂不同情绪状态下脑电各频段的能量分布会变化比如负面情绪时gamma频段能量常有变化放松状态下alpha波更活跃。把信号变换到频域后每个频段的能量方差就能反映这种差异而微分熵正好是衡量一段信号不确定性的指标。对高斯分布微分熵有闭式解DE等于0.5乘以log2(2πeσ²)。这里的σ²就是频段内的平均功率。写成代码就是de 0.5 * np.log2(2 * np.pi * np.e * power)。用log2时单位是比特用自然对数时单位是奈特。SEED相关论文里习惯用log2所以我也建议跟随这个设定报告里和文献对比数字时口径一致。SEED上经典做法是把0.5-50Hz切成五个频段delta、theta、alpha、beta、gamma。各频段生理含义不同delta和theta和睡眠、记忆加工有关alpha和放松状态相关beta和警觉注意力相关gamma和高级认知加工相关。直接用全频段一锅炖会丢失频段间的区分度分开算DE再拼接等效于给了模型五个并行的“观察维度”。频段边界有几种常见划分方式我见过最通用的是delta1-3Hz、theta4-7Hz、alpha8-13Hz、beta14-30Hz、gamma31-50Hz。也有把alpha拆成慢快两个子带的但对课设来说五频段已经够了太细的划分反而让特征维度爆炸。3.2 五频段DE特征提取代码实现def de_feature_from_windows(windows, fs1000, win_len1.0): 输入: (n_windows, channels, samples) 的1秒窗口 输出: (n_windows, channels * n_bands) 的DE特征矩阵 from scipy.signal import stft bands { delta: (1, 3), theta: (4, 7), alpha: (8, 13), beta: (14, 30), gamma: (31, 50), } n_win, n_ch, _ windows.shape n_bands len(bands) feats np.zeros((n_win, n_ch * n_bands), dtypenp.float32) for w in range(n_win): # stft返回三部分频率轴、时间轴、复数时频矩阵 # 对单个1秒窗口nperseg1000时频分辨率大约是1Hz f, t, Zxx stft(windows[w], fsfs, npersegint(fs * win_len), noverlapint(fs * 0.5)) col 0 for ch in range(n_ch): # 每个通道取幅值平方得到功率谱 psd np.abs(Zxx[ch]) ** 2 for name, (f_lo, f_hi) in bands.items(): idx np.logical_and(f f_lo, f f_hi) # 频带内平均功率当作高斯分布的方差估计 power np.mean(psd[idx]) de 0.5 * np.log2(2 * np.pi * np.e * power 1e-8) feats[w, ch * n_bands col] de col 1 return feats代码里有一个容易被忽略的细节stft默认加的是汉宁窗nperseg1000对应1秒窗长频率分辨率约1Hz这正好匹配delta频段1-3Hz的粒度。如果你把窗口缩到0.5秒频率分辨率降到2Hzdelta频段里就只剩两个频点DE估计会抖动。加1e-8是为了防止power为零时log2计算报错。实测中功率谱密度不会真的为0但滤波边界上的频点可能出现极小值这个epsilon要加。3.3 特征形状与归一化模型输入长什么样按上面的流程一个1秒窗口会得到62通道×5频段310维特征向量。一个4分钟视频片段1秒窗长、0.5秒步长大约产出480个窗口15个片段合计约7200条样本。这个数量级对SVM和浅层LSTM都足够不需要再做数据增强。特征拼接顺序建议固定成“通道优先、频段次之”的顺序也就是先通道0的delta到gamma再通道1的delta到gamma。这个顺序不会影响SVM的结果但对LSTM来说如果后面你想把特征重排成(时间步, 特征维)保持一致的顺序能少踩很多维度排列的坑。归一化必须且只能对训练集做fit再用同一组均值和方差去转换验证集和测试集。很多新手对全量数据做StandardScaler这在有重叠窗口的任务里等于变相泄漏了测试集信息。另外EEG的个体差异很大不同被试的DE分布差异明显按被试分组做归一化比全局归一化更符合跨被试评估的语义。from sklearn.preprocessing import StandardScaler # X_all来自DE特征矩阵这里假设已经做好了按时间窗口的切分 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test)这段代码没什么神奇之处真正要记住的是fit_transform只出现在训练集上。如果哪次你在代码里看到对X_all直接fit_transform那就要警惕报告里的准确率是不是虚高。4. 模型训练SVM基线到LSTM输入怎么切、参数怎么搜4.1 SVM基线RBF核、C和gamma网格搜索SEED上的基线模型首选RBF核SVM。理由有三个DE特征维度310不算高维SVM不需要深度模型那样的大规模数据样本量7000左右RBF核能把非线性的情绪决策边界学出来SVM没有梯度下降的调参过程对课设来说结果稳定可复现。相比随机森林SVM在脑电特征上的泛化表现通常更好。from sklearn.svm import SVC from sklearn.model_selection import GridSearchCV, GroupKFold # X_scaled: (n_samples, 310), y: (n_samples,), groups: 每条样本所属的视频片段ID param_grid {C: [0.1, 1, 10], gamma: [0.001, 0.01, 0.1]} svm SVC(kernelrbf, class_weightbalanced, probabilityTrue, random_state42) # GroupKFold: 按视频片段分组同一个片段的重叠窗口不会同时进训练集和验证集 gkf GroupKFold(n_splits5) grid GridSearchCV(svm, param_grid, cvgkf, scoringaccuracy, n_jobs-1) grid.fit(X_scaled, y) print(最优参数:, grid.best_params_) print(交叉验证准确率:, grid.best_score_)class_weightbalanced这一步建议保留。虽然SEED三类别样本基本均衡但按片段分组交叉验证时每一折的类别分布可能有波动加了它能让模型对少数类更稳健。C和gamma的搜索范围以上面的网格为起点。C控制误分类惩罚力度C太大会过拟合到某个被试的噪声模式gamma控制RBF核的影响半径太小则模型偏向线性太大则每个样本自成一类验证集直接崩盘。我一般先跑一个粗略网格锁定最优区间后再细化搜索。4.2 LSTM路线把310维DE向量重组成时间序列SVM忽略了一个重要信息情绪状态是随时间连续变化的前一秒的脑电状态对下一秒有影响。LSTM的价值就在这里它按时间步读取特征序列可以建模几十秒内的情绪演化轨迹。但代价是数据组织方式变复杂了你需要把(samples, 310)重组成(sequences, time_steps, 310)的3D张量。import numpy as np def make_sequences(X, y, seq_len8): 把窗口级DE特征按每8个连续窗口组成一个样本序列 n len(X) // seq_len n_used n * seq_len X_seq X[:n_used].reshape(n, seq_len, -1) # 序列标签取该段最后一个窗口的标签或者众数二选一 y_seq y[:n_used].reshape(n, seq_len)[:, -1] return X_seq, y_seq X_seq, y_seq make_sequences(X_train_scaled, y_train, seq_len8) print(LSTM输入形状:, X_seq.shape) # (序列数, 8, 310)seq_len选8代表8秒的上下文窗口。这个数字不是拍脑袋SEED上的电影片段情绪诱发是渐进的8到16秒能覆盖一个完整的情绪诱发单元。太短则LSTM学不到时序依赖太长则样本数量骤降而且跨到情绪标签切换的边界上。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from tensorflow.keras.optimizers import Adam model Sequential([ LSTM(64, return_sequencesTrue, input_shape(seq_len, 310)), Dropout(0.3), LSTM(32), Dropout(0.3), Dense(3, activationsoftmax) ]) model.compile(optimizerAdam(1e-3), losssparse_categorical_crossentropy, metrics[accuracy]) model.summary()第一层LSTM的return_sequencesTrue必须保留否则第二层LSTM接收不到完整的时间步序列。Dropout放在LSTM层之间比放在最后效果好能抑制对特定时间步的过拟合。整个模型参数量不大CPU也能在几分钟内跑完一个epoch如果你有NVIDIA GPU训练时间会缩短到十几秒级别。4.3 结果对照两种路线在报告里怎么摆写课设报告时不建议只放一个模型的准确率把SVM和LSTM放在同一个测试协议下对比答辩老师会更容易认可。路线模型输入复现参考准确率传统基线RBF-SVM310维DE向量75%-85%序列模型双层LSTM(8, 310)序列80%-90%这里要明确说明是subject-dependent还是subject-independent设置。同一个被试的session数据训练并测试属于前者准确率普遍偏高跨被试训练准确率可能掉到65%以下。报告里把两种设置都写出来比只挑好看的数字更有说服力。5. 避坑清单复现这个源码包最容易翻车的五个地方5.1 mat文件版本读不出来NotImplementedError还是Unknown mat file type现象scipy.io.loadmat直接抛异常或者读出来是个空dict。原因SEED的mat文件有两种存储格式v5是传统MATLAB格式v7.3是HDF5容器。scipy只解析了v5v7.3需要h5py。另外有些来源的文件字段名不是标准的data和labels。解决按2.2小节的兼容方案写一个load_seed_mat函数先打印keys再决定怎么取值。h5py读出来的数组要检查维度顺序必要时做转置。5.2 标签错位准确率稳定在33%左右现象模型能训练loss也在下降但测试准确率只有33%和三分类随机猜一样。原因15段影片的标签是按刺激播放顺序排列的很多人用文件名排序后重新编号把顺序打乱了。解决建立视频片段编号到标签的固定映射表不要依赖文件名的字母排序。SEED的标签字典里每个编号对应明确情绪直接硬编码到代码里比用list.sort()安全得多。5.3 滑窗重叠导致数据泄漏验证集准确率虚高到95%现象自己分割窗口时用了50%重叠交叉验证准确率高达95%换到另一个被试上只有60%。原因相邻窗口共享一半原始数据GroupKFold没按片段分组时同一个视频片段的训练窗口和验证窗口高度相似模型等于“背题”。解决交叉验证的group参数必须传入视频片段ID而不是放任KFold随机切分。用无重叠窗口也能缓解泄漏但会损失一半样本优先考虑GroupKFold。5.4 跨被试训练翻车按被试留一准确率掉到65%以下现象把15个被试数据全混在一起训练交叉验证表现还行但按新被试留出测试时效果很差。原因EEG信号存在明显个体差异不同人的脑电幅度、频率分布差异远大于情绪类别差异。解决报告里明确区分subject-dependent和subject-independent两种评估协议。源码包里如果只给了前者的结果你自己补跑一个“留一被试验证”能体现对问题的理解深度答辩时是加分项。5.5 报告与代码结果对不上模型文件改了图表还是旧的现象论文里的混淆矩阵和实际跑出来的数字对不上答辩老师现场让跑一次结果当场翻车。原因改完参数或训练集后没有重新生成图表报告里的截图是旧的。解决写一个eval_report.py加载训练好的模型和测试集一次性输出准确率、Kappa系数、混淆矩阵和训练曲线报告里直接引用这份脚本的输出。6. 验证与呈现让复现结果对得上号再谈源定位扩展拿到这份源码包之后我建议你先把训练好的模型权重保留下来单独写一个验证脚本。脚本的目标不是训练而是把指标全部打印出来形成一份可复现的结果快照。这个习惯能解决大部分课设答辩的尴尬场景老师问“你这个准确率怎么来的”你直接把脚本跑一遍中间不用P图也不用改数字。from sklearn.metrics import accuracy_score, cohen_kappa_score, confusion_matrix import joblib # 加载训练好的SVM模型和对应的scaler model joblib.load(svm_best.pkl) scaler joblib.load(scaler.pkl) # X_test_raw必须是未归一化的原始DE特征 X_test_scaled scaler.transform(X_test_raw) y_pred model.predict(X_test_scaled) print(Accuracy:, accuracy_score(y_test, y_pred)) print(Kappa:, cohen_kappa_score(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))Kappa系数在这里比准确率更有说服力因为它扣除了一致性概率对类别不平衡不敏感。课设报告里列表格时同时放Accuracy和Kappa比单独一个准确率显得专业。如果你还有余力往深了做SEED数据支持源定位分析比如最小范数估计MNE可以把传感器级别的脑电信号反演到大脑皮层源空间再做源级别的DE特征提取。这是一个明显的进阶方向能把你和“只会套SVM”的课设区分开但前提是先把前面五章的基础链路跑通否则源定位的参数调试会让你彻底迷失。从那以后我拿到任何脑电课设源码包都会强制先走一遍“读文件、看keys、查形状、GroupKFold验证、结果导出”这条链路全部跑通才谈得上调模型。课设项目做得好不好往往就差在这些纪律上。希望帮到你。本文还有配套的精品资源点击获取