资讯详情

资讯详情

脑电情绪识别全流程:从EEG信号到分类模型与部署

简介基于EEG信号的脑电情绪分析系统设计源码面向脑机接口、情感计算与人机交互方向的研究者、开发者和高校学生提供从脑电信号采集、预处理到情绪识别与结果展示的完整可运行工程。包体共49个文件压缩包大小11.51MB核心由6个Python源文件和5个字节码文件组成覆盖信号去噪、特征提取、数据变换、可视化等关键环节8个HTML文件实现前端交互界面4个PyTorch模型和5个pkl文件保存了LSTM、MLP、随机森林等多种训练好的情绪分类模型XML、CSV、Excel等文件分别承担配置、实验数据记录与结果统计功能图像文件则直观展示预测效果与波形。已有483人学习浏览目录结构清晰便于对照源码理解从脑电预处理、特征工程到模型训练推理的完整技术链路。非常适合作为课程设计、毕业设计或入门实践蓝本可以直接复用数据处理脚本和分类模型大幅缩短环境搭建与算法调试时间。1. 为什么不建议新手直接跑脑电情绪分析从信号到情绪的工程落点脑电信号分析、情绪识别系统、Python、EEG这四个词放在一起外行的第一反应是“训练个神经网络把情绪分出来”拿到这套系统设计源码后你会发现真正有门槛的其实是从原始脑电到特征矩阵那一段脏活累活。这套源码解决的核心问题不是模型多新而是把脑电情绪识别做成了一条完整可复现的流水线原始EEG读取、预处理、特征提取、分类训练、HTML结果面板全部在一个工程里跑通。它的价值在于你有了一个能替换数据、能调整参数、能当作课程设计底座的完整框架而不是散落一地的片段代码。适合有Python基础、想快速跑通全流程的开发者也适合需要给项目交差的学生。2. 系统链路拆解一条EEG数据从采集到情感标签的必经之路这套源码最值得称道的一点是没有把情绪识别当成“扔进深度学习就完事”的黑匣子而是把完整链路拆成了五个可替换模块。这样做最大的好处是当你的采集设备、数据格式或实验范式不一样时只需要替换对应模块不需要推翻整条流水线重写。2.1 模块划分与数据流按照这套源码的组织方式整个系统大致分成五块数据读取、预处理、特征提取、模型训练、结果展示。数据读取负责把原始EEG文件加载成统一形状的数组预处理做滤波、去基线和伪迹处理特征提取把每个时间窗转成特征向量训练模块接收特征和标签训练分类器展示模块则把预测结果落到一个HTML面板上。这些模块之间的接口是硬约束因为EEG数据的形状非常固定一个trial包含若干通道每个通道是一段连续时间信号。无论后面跑SVM还是LSTM都要先过这一层形状校验。我见过不少人在这一步翻车——拿到数据不打印shape就开始写模型结果reshape搞了整整一晚上。模块输入输出数据读取.mat / .csv 原始文件trial × channel × time 三维数组预处理原始epoch数组滤波去伪迹后的epoch特征提取预处理后epochsample × feature 特征矩阵模型训练特征矩阵 标签分类模型文件结果展示模型 新数据情绪类别 / HTML面板这个模块划分还有一个好处每一段的中间结果都可以单独落盘保存。我调试时会分别把预处理前后、特征提取前后的数据保存成 npz 文件线上跑挂了能直接对比哪一步出了问题不用从头断点调试。2.2 为什么选差分熵三行公式讲清楚预处理之后的核心问题是“用什么数字代表这段脑电”。这套源码的特征主力是差分熵Differential EntropyDE和功率谱密度PSD。差分熵本质上是把一个频段内的功率谱积分后取对数再取负做的是信号复杂度的度量。在情绪分析里DE比单纯的时域均值好用主要两点原因。第一它天然做了对数压缩对个体差异不敏感。不同被试的头皮厚度、电极阻抗都不一样幅值差异很大但取对数之后绝对幅值的大小被拉平模型学到的是能量分布的结构而不是幅值。第二它和脑电的频段生理意义直接对应方便后续解释哪些频段在正负情绪里起了主要作用。频段划分是有讲究的。情绪识别里最常用的是五个频段delta(0.5~4Hz)、theta(4~8Hz)、alpha(8~13Hz)、beta(13~30Hz)、gamma(30~40Hz)。四个情绪维度的研究里alpha和gamma的变化通常和唤醒度相关theta和情绪效价相关。把这五个频段的DE拼接起来一个32通道系统每个时间窗能得到160维特征这个维度对SVM来说非常友好。2.3 数据读取loadmat之后我必做的一件事源码里数据读取部分不长但包含一个非常关键的步骤——打印shape并校验维度。我一般会用这样一段代码import numpy as np from scipy import io # 假设 subject_01.mat 中变量名为data结构为 trial x channel x time raw io.loadmat(subject_01.mat)[data] print(原始数组形状:, raw.shape) # 校验维度有的数据集存成 (channel, trial, time)需要转置 if raw.ndim 3 and raw.shape[0] raw.shape[1]: raw raw.transpose(1, 0, 2) print(检测到通道维度在首位已转置:, raw.shape) # 统一为 (trial, channel, time) 后拆分 trials, channels, times raw.shape print(f共 {trials} 个试验{channels} 个通道每段 {times} 个采样点)逻辑说明io.loadmat读取Matlab 7.3之前的版本没问题返回的是字典取键时注意变量名大小写。我的转置判断逻辑写成“通道数比试验数少”这种经验规则因为绝大多数公开数据集的试验数都大于通道数。如果发现读进来是四维数组说明数据里还混了被试维度需要额外做一层切片。参数说明这里的采样率fs决定了后面所有滤波器参数。若数据是128Hz采样、每段7680个点对应60秒时长如果换了采集设备这段逻辑不用改但后续预处理参数必须重新核对不然滤波器设计全是错的。3. 预处理与特征提取实战滤波去伪迹与可复现的特征矩阵预处理这一段我完全是照这套源码里的顺序实现的按三步走带通滤波、去基线、伪迹剔除中间任何一步落下后面的特征和模型都会出问题。3.1 预处理流水线从原始波形到干净的epoch带通滤波是为了去掉低频漂移和高频肌电干扰。EEG的有效信号基本落在0.5~40Hz之间所以用四阶巴特沃斯带通滤波就够了。我通常用scipy.signal.butter设计滤波器再用filtfilt做零相位滤波避免普通IIR滤波带来的相位偏移from scipy import signal fs 128 # 采样率单位Hz low, high 0.5, 40.0 # 带通范围 b, a signal.butter(4, [low, high], btypeband, fsfs) filtered signal.filtfilt(b, a, epoch, axis-1) print(输入形状:, epoch.shape, 输出形状:, filtered.shape)逻辑说明filtfilt对数据做正反两次滤波把相位延迟抵消掉这对后续特征提取至关重要。如果用lfilter做普通滤波信号会出现一个固定的相位偏置基线对齐会变得很麻烦。参数说明butter的阶数4是个平衡点阶数太高会引入相位畸变甚至数值不稳定阶数太低过渡带太宽、噪声滤不干净。如果滤波后波形首尾出现剧烈振铃优先检查是不是用了lfilter换成filtfilt能解决大部分问题。去基线这一步用的是每个trial起始前的静息窗口。情绪诱发实验的采集流程通常会在正式刺激前录几秒静息态作为基线预处理的正确做法是把整段减去基线均值。代码很简单# 取前2秒作为基线窗口 baseline_len int(2.0 * fs) baseline_mean filtered[..., :baseline_len].mean(axis-1, keepdimsTrue) detrended filtered - baseline_mean逻辑说明keepdimsTrue保留了通道维度这样每个通道减去自己的基线均值而不是全局减一个数避免引入通道间差异。3.2 特征提取的Python实现差分熵和功率谱密度特征提取是这套源码里工程量最大的一块。核心函数计算出各个频段的差分熵拼接成特征向量import numpy as np def compute_de(epoch, fs128, bandsNone): if bands is None: bands [(0.5, 4), (4, 8), (8, 13), (13, 30), (30, 40)] freqs np.fft.rfftfreq(epoch.shape[-1], d1/fs) spectrum np.fft.rfft(epoch, axis-1) power np.abs(spectrum) ** 2 de_list [] for low, high in bands: idx np.where((freqs low) (freqs high))[0] band_power np.trapz(power[..., idx], freqs[idx], axis-1) de np.log2(band_power 1e-8) # 加小常数防止log(0) de_list.append(de) return np.stack(de_list, axis-1)逻辑说明对每个频段先做功率积分再取对数得到近似的差分熵。np.trapz是梯形积分比单纯求均值更能保留窄带能量差异。频段索引直接用物理频率来写如果采样率从128改成256这个函数不需要改动逻辑。参数说明np.log2在这里是标准写法有的论文用自然对数np.log两者只是常数倍差异不影响分类器效果。1e-8的防零项不能省空频段下功率为零时不加这个会得到负无穷整个样本的特征就废了。3.3 参数怎么改一张表覆盖大多数调参需求我整理了一张常用参数表换设备、换数据集时基本够用参数默认推荐换场景怎么改失败表现滤波下限0.5Hz有严重基线漂移时可提到1Hz波形整体漂移滤波上限40Hz关注gamma时可提到45Hz高频能量缺失窗长4秒数据量大降到2秒特征方差变大、分类下降频段划分五频段只用alphabeta可提速分类准确率下降伪迹阈值±100uV换设备先看幅值分布大量epoch被丢弃特征组合DE PSD加Hjorth参数可提升特征维度过大、训练变慢这里的伪迹阈值是个容易忽略的坑。不同采集设备的幅值范围差别很大直接套用±100uV可能把有效数据全删光。我一般先对预处理后的数据做一次绝对值分布统计再看阈值该设在哪。记住一个原则预处理参数不是越严越好而是要让保留下的数据能反映真实信号分布。4. 模型训练与调参SVM还是LSTM以及泛化性能的真相预处理和特征做完之后模型选择其实没有想象中复杂。样本量在几千到几万这个量级时线性SVM和随机森林的表现已经足够好。LSTM和Transformer的优势只在长时序、大样本下才体现出来。这套源码的默认训练模型是SVM同时保留了LSTM作为可选后端这是一个我认为非常合理的工程判断。4.1 模型选型小样本SVM大样本再上LSTM脑电情绪识别的一个现实约束是样本量通常不大。一个被试的正式试验只有几十到上百个trial就算找40个被试总样本也就几千条。这种量级下SVM的RBF核足以刻画非线性边界而且训练速度快、不需要GPU。LSTM的优势在于能利用时间维度的序列信息但代价是需要更多数据且调参成本高。我的建议是先把SVM跑通作为Baseline记录F1分数之后再尝试LSTM。如果LSTM结果没有显著超过SVM说明问题不在模型而在前面特征工程做得不够细。这套源码的架构允许你在不修改特征提取的情况下切换后端模型这种做法我强烈建议保留。4.2 训练代码GroupKFold才是正经划分跑训练之前必须先把“分组”这件事想清楚。EEG数据里同一个被试的多个trial高度相关如果用普通的train_test_split同一个被试的片段会同时出现在训练集和验证集准确率直接虚高部署后立刻打回原形。from sklearn.svm import SVC from sklearn.model_selection import GroupKFold from sklearn.preprocessing import StandardScaler # X为特征矩阵y为标签groups记录每条样本属于哪个被试 scaler StandardScaler() gkf GroupKFold(n_splits5) for train_idx, val_idx in gkf.split(X, y, groups): X_train, X_val X[train_idx], X[val_idx] y_train, y_val y[train_idx], y[val_idx] # 先划分再标准化防止数据泄露 X_train scaler.fit_transform(X_train) X_val scaler.transform(X_val) clf SVC(kernelrbf, C10, gammascale, class_weightbalanced) clf.fit(X_train, y_train) print(当前折ACC:, clf.score(X_val, y_val))逻辑说明GroupKFold按被试分组切分保证同一个被试的所有trial只出现在训练侧或验证侧。StandardScaler必须在划分之后做fit_transform验证集只能用transform这个顺序错一点指标就不可信。参数说明C10在脑电特征上通常比默认的1更好gammascale会根据特征维数自动调整class_weightbalanced用在两类样本不均衡时很有效。如果发现每个折的准确率方差极大说明某个被试的特征分布特别离散优先查预处理而不是换模型。4.3 评估指标准确率之外还要看F1和混淆矩阵情绪识别的二分类任务里光看准确率会骗人。如果两类标签比例是70:30一个全猜多数类的模型就能拿到70%准确率。所以我还习惯补一段评估代码from sklearn.metrics import confusion_matrix, f1_score pred clf.predict(X_val) cm confusion_matrix(y_val, pred) print(混淆矩阵:\n, cm) print(F1:, f1_score(y_val, pred, averagebinary))逻辑说明混淆矩阵能直观看到模型把哪一类样本错分到了哪一类比如负情绪样本被大量判成正情绪说明特征的区分度不够或阈值设置有问题。F1同时权衡精确率和召回率比单一ACC更可信。实操里我发现情绪识别模型的混淆矩阵通常表现为“正情绪容易被识别负情绪容易被漏掉”。这种不对称性往往不是模型问题而是训练样本里两类情绪诱发强度不均导致的。这种情况下先检查实验范式的诱发材料强度分布比盲目调参更有效。5. 避坑专题脑电情绪分析里五个最贵的沉默成本做脑电情绪分析模型训练前的数据链路最容易出问题而且这些问题通常不会直接报错而是悄悄污染模型指标。我把这几年踩过、也帮别人排查过的高频问题整理成五条每一条都是实打实的花过时间的教训。5.1 现象一训练准确率95%跨被试测试只剩50%现象模型在随机划分的验证集上准确率95%部署到新一批被试上直接掉到50%左右跟抛硬币一样。原因随机划分导致同一个被试的多个trial被分到训练和验证两侧模型记住了被试的个体特征而非情绪模式。头皮电位中个体差异远大于情绪差异模型轻松“作弊”。解决必须用GroupKFold按被试分组划分确保验证集里出现的是完全没见过的人。如果分组后准确率大幅下降这反而是正常现象说明模型没有过拟合被试身份。5.2 现象二标准化发生在划分之前数据泄漏悄悄发生现象训练脚本里先对整个特征矩阵做了StandardScaler().fit_transform(X)然后才划分训练验证集验证集准确率偏高。原因验证集的均值和方差已经参与训练集统计量的计算验证时模型提前“见过”了验证集的分布信息。解决先划分再对训练集fit_transform验证集只调transform。这个顺序写错代码不会报错所以特别容易漏检。5.3 现象三标签阈值口径不一致现象同一批数据A同学用中位数0.5划分正负类B同学用5分位划分两人跑出来的结果完全不同还各自觉得自己是对的。原因公开数据集里情绪维度的评分是连续值标签定义没有统一标准。解决固定标签阈值并写入配置文件例如效价维度大于5为正情绪、小于5为负情绪。如果对阈值敏感就做一次阈值扫描找到稳定区间而不是拍脑袋定一个值。5.4 现象四基线段被当成正式数据参与训练现象情绪识别准确率异常高后来发现高到从生理上解释不通。原因分段时把刺激前静息段也当成了有效数据模型学到的不是情绪差异而是“静息 vs 任务状态”的差异。静息态和诱发态的能量分布差异非常明显模型抓住这个伪影就能刷高准确率。解决检查分段代码中t0和t1的偏移量确保只取刺激呈现后的时间窗。这个坑隐蔽的地方在于代码逻辑完全正确只是时间参数设置错误。5.5 现象五LSTM输入形状永远是最后一道坎现象把二维特征矩阵直接喂给LSTM报错维度不匹配新手在这里卡一下午。原因Keras和PyTorch的LSTM期望输入是三维(batch, timesteps, features)而特征提取器的输出通常是(samples, features)。解决在喂入模型前做一次显式reshapeimport numpy as np # X为(samples, features)假设窗长为4秒49个窗 X_3d X.reshape(X.shape[0], 49, X.shape[1] // 49) print(LSTM输入形状:, X_3d.shape)逻辑说明把特征列重新组织成时间步维度这样做的前提是你保留了时间窗顺序。如果特征矩阵在提取时打乱了顺序reshape出来的时间步是乱的模型学不到时序依赖。注意LSTM不一定比SVM好但一旦你决定用它输入维度、时间步、特征维这三者的转换关系必须在进入模型前打印出来检查一遍。6. 从离线到在线把训练好的模型封装成可用的HTML情绪面板训练完的模型如果只活在Jupyter里价值就少了一大半。这套源码里附带了一条从模型文件到HTML展示的落地路径我认为这恰恰是全项目最容易被忽略的亮点。6.1 模型导出与推理封装用joblib导出模型再写一个极简推理接口import joblib import numpy as np # 导出模型 joblib.dump(clf, emotion_svm.pkl) # 推理函数 def predict_emotion(feature_vector: np.ndarray) - str: scaler joblib.load(scaler.pkl) model joblib.load(emotion_svm.pkl) x scaler.transform(feature_vector.reshape(1, -1)) label model.predict(x)[0] return positive if label 1 else negative逻辑说明推理阶段和训练阶段必须共用同一个scaler不能用验证时临时fit出来的新scaler。这个细节很多人忽略导致上线后预测结果和离线对不上。6.2 HTML实时情绪面板后端用Flask起一个最小服务前端用fetch定时拉取预测结果渲染成简单的HTML面板from flask import Flask, jsonify, render_template app Flask(__name__) app.route(/) def index(): return render_template(panel.html) app.route(/api/predict, methods[GET]) def api_predict(): # 实际项目中这里会从采集设备读取实时特征 result predict_emotion(current_feature) return jsonify({emotion: result})HTML面板里只需要一个状态色块和一段文字用最基本的JavaScript定时器轮询接口即可不需要任何前端框架。这个简易面板的价值在于磨合适配特征提取的延迟排查数据管道是否流畅。6.3 上线前检查清单我每次把模型部署到演示环境前都会强制走一遍这个清单模型文件和scaler文件是否同时导出、版本是否匹配推理输入的通道顺序和训练时是否一致滤波参数是否与采集设备采样率匹配特征提取的窗长是否与训练时一致标签映射是否在前后端统一输入数组是否经过了和训练时相同的标准化流程从那以后我每次拿到新的EEG数据集都强制走一遍“先分组划分、再训练、再导模型、再联调”的完整流程任何一个环节对不上就停下来逐项排查。这套源码让这条流程变得容易重复执行也希望它能帮你在脑电情绪分析上少走一段弯路。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →