恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
Python多模态情感识别工程:EEG+眼动+GSR三路信号融合实战
首页
资讯中心
/
Python多模态情感识别工程:EEG+眼动+GSR三路信号融合实战
Python多模态情感识别工程:EEG+眼动+GSR三路信号融合实战
发布时间:2026/10/3 9:32:03
简介本资源为毕业设计级Python多模态情感识别项目源代码面向计算机、人工智能及人机交互方向的学生与研究人员解决如何融合脑电、眼动与皮肤电等生理信号进行情感状态分类的问题。项目覆盖信号预处理、特征选择与融合、机器学习分类器训练及情感模型构建等完整流程并配有评估模块与模块化代码结构便于二次开发与扩展。压缩包共38个文件约8.99MB以14个Python源码文件为核心辅以9张结果图、6个XML配置、4个文本说明及yaml配置、pth模型权重等涵盖数据处理、特征提取、模型训练与测试等环节。目前已有89人学习下载。读者可获得一套可直接运行的多模态情感识别工程代码理解PCA、LDA、SVM、随机森林等算法在生理信号上的落地方式并借助清晰注释与文档快速上手调试、复现实验或迁移到心理健康监测、智能客服等应用场景。1. 多模态情感识别源码拆包EEG眼动GSR 三路信号怎么落到 Python 工程里实验室里跑通一个情感识别 demo 不难难的是把脑电、眼动、皮肤电三路采样率不同、维度不同、噪声特性完全不同的信号塞进同一个训练管线还能让后来的人看懂、改得动。这份MML_ZYC.zip就是干这个的一套 Python 多模态情感识别工程把 EEG、眼动追踪、GSR 三路生理信号做特征提取、融合、分类输出情感状态判别结果。它适合正在做毕业设计、课程项目或论文复现的从业者——尤其是那种「数据采了、论文看了、代码不知道从哪下手」的阶段。整个工程按模块拆开data管数据加载common放特征提取和信号处理config走 YAML 配置MultimodalModel.py和MultimodalCLIP.py是模型定义main.py串训练入口Trainer.py和Tester.py分别管训练与评估。下面按「先看懂结构、再跑通流程、最后避坑」的顺序拆。2. 工程结构与数据流从 RawData 到 Dataset 的完整链路2.1 目录里每个文件到底管什么拿到一个陌生工程我习惯先看入口和配置再看数据流。这份源码的入口是main.py配置在config/config.yaml数据相关代码集中在data/目录下。先把关键文件列出来对照着看会快很多文件/目录职责备注main.py训练主入口读配置、建模型、启动 Trainer先看这个config/config.yaml超参、路径、模态开关改参数只动这里data/Dataset.py封装 PyTorch Dataset供 DataLoader 调用数据管线核心data/RawData.py原始信号读取与初步清洗对接采集设备格式data/LoadFeatures.py加载预提取特征跳过重复计算common/feature_extract.pyEEG/眼动/GSR 特征提取信号处理主战场common/data_process.py滤波、归一化、滑窗预处理common/utils.py通用工具函数日志、路径、指标MultimodalModel.py多模态融合模型定义融合策略在这MultimodalCLIP.py基于 CLIP 思路的对比学习模块进阶用法Trainer.py/Tester.py训练循环 / 评估改 loss、改指标看这best_model.pth训练好的权重可直接推理results/输出图表与指标含 Figure_17~20data/和common/是整条数据流的两个关键节点前者负责「把原始文件变成张量」后者负责「把张量里的噪声去掉、把有用特征抠出来」。理解这条链路后面调参和排错才有方向。2.2 三路信号的对齐与 Dataset 封装EEG、眼动、GSR 的采样率通常不在一个量级——EEG 常见 256Hz 或 512Hz眼动可能 60Hz 或 120HzGSR 更低往往 10~30Hz。直接拼接维度对不上必须先做时间对齐。常见做法是按最小采样率重采样或者用滑窗把三路信号切到统一时间窗比如 2 秒一窗、步长 1 秒每个窗内各自提特征再拼。data/Dataset.py里一般会做这几件事读标签、按被试或按试次划分、把三路特征拼成一个向量、返回(feature, label)。下面是我按这份工程结构还原的一个典型 Dataset 写法参数含义逐行标注import torch from torch.utils.data import Dataset import numpy as np class MultiModalDataset(Dataset): def __init__(self, eeg_feat, eye_feat, gsr_feat, labels, window_size256): # eeg_feat: (N, C, T) 脑电C 为通道数 # eye_feat: (N, D_eye) 眼动特征已按窗聚合 # gsr_feat: (N, D_gsr) 皮肤电特征 self.eeg torch.tensor(eeg_feat, dtypetorch.float32) self.eye torch.tensor(eye_feat, dtypetorch.float32) self.gsr torch.tensor(gsr_feat, dtypetorch.float32) self.labels torch.tensor(labels, dtypetorch.long) self.window_size window_size def __len__(self): return len(self.labels) def __getitem__(self, idx): # 三路特征在通道维拼接交给模型做融合 fused torch.cat([ self.eeg[idx].flatten(), # EEG 展平或保留时序由模型决定 self.eye[idx], self.gsr[idx] ], dim-1) return fused, self.labels[idx]逻辑说明__getitem__里把三路特征拼成一个长向量是最朴素的「早期融合」。如果模型里要做注意力融合就别在这里拼改成返回字典{eeg:..., eye:..., gsr:...}让MultimodalModel.py自己处理。参数上window_size决定时间分辨率窗太短特征不稳窗太长情感状态会被平均掉一般 1~3 秒是常见区间具体要看你的标注粒度。提示如果你的眼动和 GSR 是「试次级」特征一个试次一个值而 EEG 是「时间序列级」那对齐粒度要统一到试次否则 DataLoader 会报维度错。这是新手最容易翻车的地方。3. 特征提取与预处理滤波、滑窗、归一化怎么设参数3.1 EEG 频带特征与眼动/GSR 特征EEG 的情感相关信息主要集中在几个频带delta(1-4Hz)、theta(4-8Hz)、alpha(8-13Hz)、beta(13-30Hz)、gamma(30-45Hz)。常见做法是对每个通道做带通滤波后算各频带功率谱密度PSD再取对数。眼动特征一般是注视时长、扫视幅度、瞳孔直径均值/方差GSR 则提取皮肤电导水平SCL和皮肤电导反应SCR的峰值、上升时间等。common/feature_extract.py里通常用scipy.signal做滤波用welch算 PSD。下面这段是我按工程结构写的 EEG 频带特征提取参数都标了from scipy.signal import butter, filtfilt, welch import numpy as np def bandpass_filter(signal, fs, low, high, order4): # fs: 采样率low/high: 通带边界order: 滤波器阶数 nyq 0.5 * fs b, a butter(order, [low/nyq, high/nyq], btypeband) return filtfilt(b, a, signal, axis-1) def extract_eeg_bands(eeg, fs256): # eeg: (C, T) 单试次多通道 bands {delta: (1, 4), theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 45)} feats [] for name, (lo, hi) in bands.items(): filtered bandpass_filter(eeg, fs, lo, hi) # nperseg 取 2 秒窗重叠一半平衡频率分辨率与稳定性 f, psd welch(filtered, fsfs, npersegfs*2, noverlapfs) feats.append(np.log1p(psd.mean(axis-1))) # 每通道一个标量 return np.concatenate(feats, axis-1) # (C*5,)逻辑说明filtfilt做零相位滤波避免相位偏移影响后续特征welch的nperseg决定频率分辨率取 2 秒窗是常见折中。np.log1p压缩动态范围防止大功率通道主导。参数上order4是巴特沃斯滤波的常用阶数阶数太高会振铃太低则滚降不够。3.2 归一化与滑窗的坑三路信号量纲差异极大EEG 是微伏级GSR 是微西门子级眼动瞳孔直径是毫米级。不归一化直接拼模型会被数值大的模态带偏。常见做法是每个模态单独做 z-score按训练集统计量算均值和方差再应用到验证/测试集——千万别用全体数据算统计量那是数据泄漏。滑窗方面如果标签是试次级一个试次一个情感标签滑窗后每个窗继承试次标签但窗与窗之间高度重叠训练集和验证集如果按窗随机划分同一试次的窗会同时出现在两边导致验证指标虚高。正确做法是按试次或被试划分这一点在data_process.py里如果没有处理需要自己补。注意config.yaml里如果有normalize: true之类的开关先确认它是在划分之前还是之后做的。我见过不少工程把归一化写在 Dataset 里对全体数据做跑出来 F1 高得离谱一换被试就崩。4. 模型融合与训练MultimodalModel 的三种融合策略怎么选4.1 早期融合、晚期融合、注意力融合的取舍MultimodalModel.py是这份源码的核心。多模态融合大致三档早期融合特征拼接后进分类器、晚期融合各模态单独出预测再投票/加权、注意力融合用注意力权重动态分配模态贡献。早期融合实现最简单但对齐要求高、模态间量纲敏感晚期融合鲁棒但丢失跨模态交互注意力融合效果通常最好代价是参数量和训练数据需求都上去了。如果数据量不大几百个试次以内我一般先用早期融合跑基线确认管线通了再换注意力。下面是一个带模态注意力的融合头示例import torch import torch.nn as nn class AttentionFusion(nn.Module): def __init__(self, dim_eeg, dim_eye, dim_gsr, hidden64, n_class2): super().__init__() # 各模态先投影到同一维度 self.proj_eeg nn.Linear(dim_eeg, hidden) self.proj_eye nn.Linear(dim_eye, hidden) self.proj_gsr nn.Linear(dim_gsr, hidden) # 注意力打分每个模态一个标量权重 self.attn nn.Linear(hidden, 1) self.classifier nn.Linear(hidden, n_class) def forward(self, eeg, eye, gsr): h_eeg torch.relu(self.proj_eeg(eeg)) h_eye torch.relu(self.proj_eye(eye)) h_gsr torch.relu(self.proj_gsr(gsr)) stack torch.stack([h_eeg, h_eye, h_gsr], dim1) # (B, 3, H) w torch.softmax(self.attn(stack), dim1) # (B, 3, 1) fused (stack * w).sum(dim1) # (B, H) return self.classifier(fused)逻辑说明三个模态各自线性投影到hidden维attn给每个模态打一个分数softmax 归一化成权重加权求和后分类。hidden一般取 64 或 128太小欠拟合太大在小数据上过拟合。这个结构的好处是权重可解释——你可以打印w看模型更依赖哪路信号对写论文很有用。4.2 训练循环与关键超参Trainer.py管训练循环。几个必须关注的超参学习率常见 1e-3 到 1e-4、batch size受试次数限制常见 16 或 32、优化器Adam 起步、早停耐心值patience常见 10~20。类别不平衡时用加权交叉熵或 Focal Loss别硬上 accuracy 当指标。import torch import torch.nn as nn def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss 0 for feat, label in loader: feat, label feat.to(device), label.to(device) optimizer.zero_grad() logits model(feat) # 若模型接收字典这里改成 model(**feat) loss criterion(logits, label) loss.backward() # 梯度裁剪防止 EEG 长序列导致的梯度爆炸 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm1.0) optimizer.step() total_loss loss.item() return total_loss / len(loader)逻辑说明clip_grad_norm_的max_norm1.0是常用值EEG 序列长、梯度容易爆裁剪几乎是标配。criterion如果类别不平衡用nn.CrossEntropyLoss(weightclass_weights)class_weights按训练集类别频率的倒数算。提示best_model.pth是已经训练好的权重想直接推理就跳过训练用Tester.py加载它跑测试集。但要注意加载权重前确认模型结构没改过否则state_dict对不上会报 key 缺失。5. 避坑与排查这份源码跑不起来时先查这五条5.1 现象DataLoader 报维度不匹配原因三路特征拼接时维度顺序或长度不一致常见于眼动/GSR 是试次级而 EEG 是窗级。解决在Dataset.__getitem__里打印各路 shape确认拼接前维度对齐必要时把 EEG 也按试次聚合取均值或取最后时刻。5.2 现象验证集准确率异常高95%原因滑窗重叠导致同一试次的窗同时进了训练和验证集数据泄漏。解决按试次或被试划分数据集用GroupShuffleSplit或手动按 subject id 分组别用随机划分。5.3 现象训练 loss 不降或震荡原因学习率太大、归一化没做、或某路模态数值范围过大主导梯度。解决先把学习率降到 1e-4 试检查每路特征是否 z-score打印各模态数值范围EEG 微伏级和 GSR 微西门子级差几个数量级是常态。5.4 现象加载best_model.pth报 key 不匹配原因模型定义改过加了层或改了维度权重和结构对不上。解决用model.load_state_dict(state_dict, strictFalse)先看缺哪些 key或者回到原始MultimodalModel.py结构再加载。5.5 现象config.yaml改了参数但不生效原因main.py里可能硬编码了部分参数或者配置读取路径不对。解决在main.py开头打印读到的配置字典确认 YAML 被正确解析检查是否有argparse覆盖了 YAML 值。6. 进阶用 MultimodalCLIP 做对比学习与结果验证MultimodalCLIP.py是这份源码里比较有意思的部分——它借鉴 CLIP 的对比学习思路把不同模态映射到同一嵌入空间用模态间一致性作为辅助损失。这在标注数据少的时候特别有用即使标签不够模态间的对应关系本身就是监督信号。常见做法是对同一试次的 EEG 和眼动嵌入算 InfoNCE loss拉近正样本对、推远负样本对。import torch import torch.nn.functional as F def contrastive_loss(emb_a, emb_b, temperature0.07): # emb_a, emb_b: (B, D) 同一 batch 内两个模态的嵌入 emb_a F.normalize(emb_a, dim-1) emb_b F.normalize(emb_b, dim-1) logits emb_a emb_b.t() / temperature # (B, B) labels torch.arange(logits.size(0), devicelogits.device) # 对称 InfoNCEa-b 和 b-a 各算一次 loss_ab F.cross_entropy(logits, labels) loss_ba F.cross_entropy(logits.t(), labels) return (loss_ab loss_ba) / 2逻辑说明temperature0.07是 CLIP 原论文的常用值控制分布锐度F.normalize把嵌入投到单位球面点积即余弦相似度。这个 loss 可以和分类 loss 加权相加权重一般 0.1~0.5太大反而干扰分类。验证方面results/目录下的Figure_17.png到Figure_20.png通常是混淆矩阵、训练曲线、t-SNE 嵌入可视化。我一般会先看训练/验证 loss 曲线是否收敛且无大幅背离再看混淆矩阵确认是否有类别被系统性误判最后看 t-SNE 确认不同情感类别的嵌入是否可分。如果 t-SNE 上类别混在一起说明特征区分度不够回去查特征提取或换融合策略。从那以后我每次拿到多模态源码都强制先跑一遍「单模态基线」——只用 EEG、只用眼动、只用 GSR 各跑一次再跑融合。这样一旦融合效果不如单模态立刻知道是融合模块的问题而不是数据管线的问题。这个习惯帮我省了无数次瞎调参的时间。希望帮到你。本文还有配套的精品资源点击获取