资讯详情

多模态情感识别PyTorch源码包解析:从预处理到模型训练

📅 2026/10/25 9:39:34 | 华诺云谱 👁 阅读
多模态情感识别PyTorch源码包解析:从预处理到模型训练
简介面向毕业设计场景的Python多模态情感识别源码融合脑电波EEG、眼动追踪与皮肤电GSR三类生理信号覆盖信号预处理、特征选择、机器学习分类及情感模型构建完整流程用于解决单一模态情感识别泛化性不足的问题适合计算机、生物医学工程方向的学生与研究者参考。压缩包共38个文件含14个Python脚本覆盖训练、测试、数据读取与特征提取等环节、9张可视化图片、6个XML配置和4个文本资料另附预训练权重、运行配置及辅助目录整体仅8.99MB结构清晰。目前已有89人学习。代码实现SVM、随机森林等分类器及PCA、LDA特征降维并包含Trainer、Tester、MultimodalCLIP等核心模块借助Russell情感维度将生理信号映射到具体状态同时提供评估与交叉验证代码配合详细注释与文档便于复现实验、扩展新模态或直接作为毕业设计/论文实验基线。1. 多模态情感识别项目一份能直接跑的PyTorch源码包如果你正在做多模态情感识别方向的毕业设计应该很快会遇到一个现实问题脑电、眼动、皮肤电这三类信号都能反映情绪但把它们放进同一个模型时数据采样率对不齐、特征量纲差太多、模型训完F1分数不到40%这些坑一个接一个。这份源码包的优势在于它把EEG、眼动、GSR的预处理、特征提取、模型训练、评估全链路做成了模块化工程不只是实验代码而是一个可以直接跑通、改参数就能复现结果的完整项目。包里包含MultimodalModel.py、MultimodalCLIP.py、Trainer.py、Tester.py以及训练好的best_model.pth适合需要快速产出实验结果、又不想从零造轮子的从业者和毕设学生。下面我按数据链路、模型架构、训练评估、常见坑位这条线逐个拆。2. 信号预处理与特征提取从RawData.py到feature_extract.py的完整链路2.1 原始数据读取与三条预处理主线拿到这个包第一步不是看模型而是先理顺data_process.py、RawData.py、LoadFeatures.py这三个文件的分工。我的阅读顺序是RawData.py它负责把data文件夹下的原始记录读进来输出统一格式的时间序列接着是data_process.py做滤波、重采样、滑窗切分最后是feature_extract.py把每个窗口转成特征向量喂给LoadFeatures.py去构建Dataset。这里有个关键点提醒你脑电、眼动、皮肤电的采样率天然不同。EEG设备常见128Hz或256Hz眼动追踪仪输出通常在30到60HzGSR设备则低至4到8Hz。如果直接把三条原始时间序列拼在一起特征维度里EEG占了大部分GSR几乎被淹没。所以data_process.py里最核心的动作是统一时间基准和采样率。常见做法是以最短的GSR信号为锚点把EEG和眼动重采样到同一个低频目标上比如统一到32Hz再用固定窗口切段。下面是重采样部分的核心逻辑我在自己的项目里也这么写# data_process.py 关键片段按统一时间轴重采样 import numpy as np from scipy import signal def resample_channel(raw, raw_fs, target_fs): raw: 单通道原始信号 raw_fs: 该通道原始采样率 target_fs: 对齐后目标采样率例如 32Hz duration len(raw) / raw_fs target_len int(duration * target_fs) # scipy.signal.resample 基于FFT长度变化后首尾会有振铃 resampled signal.resample(raw, target_len) return resampled这段代码里signal.resample用的是FFT插值速度很快但信号首尾会出现振铃效应。我一般会在重采样之前先做一次带通滤波比如EEG保留0.5到45Hz然后再重采样这样末尾的振荡对后面滑窗特征的影响会小很多。目标采样率不要设得太高统一到32Hz是我实测性能和计算量都比较平衡的点如果你后续要提取眼动注视点之类的时序特征也可以保留到50Hz。2.2 特征提取策略频带功率、眼动统计与GSR响应预处理完成后进入feature_extract.py。这个模块做的事情可以用一句话概括把每个时间窗口变成一个固定维度的向量。三类模态提取的特征类型差别很大你不能用同一套方法去处理它们。EEG侧通常提取频带功率或微分熵。频带功率就是计算delta、theta、alpha、beta、gamma这几个频段的能量占比。眼动侧提取瞳孔直径均值、注视时长、眨眼次数这类统计量。GSR侧则关注皮肤电导水平SCL和皮肤电反应SCR的幅值与上升时间。下面是EEG频带功率提取的示意代码实际包里的写法接近这个结构# feature_extract.py 关键片段EEG频带功率特征 from scipy.signal import welch def eeg_band_power(eeg_window, fs32): eeg_window: 单窗口单通道EEG信号 fs: 重采样后的目标采样率默认32Hz freqs, psd welch(eeg_window, fsfs, nperseg64) bands { delta: (0.5, 4), theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 45) } feat {} for name, (lo, hi) in bands.items(): mask (freqs lo) (freqs hi) feat[name _power] psd[mask].sum() return feat注意nperseg这个参数。它决定了单段FFT的长度直接影响频率分辨率。窗口采样率是32Hz时nperseg64意味着约2秒一个频段估计对delta这类低频带比较合适。如果你把nperseg调小到32频率分辨率变粗delta和theta会混在一起后面模型很难区分。这个坑我踩过血泪经验改特征之前先确认nperseg和窗口长度匹配。眼动和GSR的特征相对简单一般直接取统计量比如瞳孔直径的均值、标准差、一阶差分最大值GSR的SCR事件次数和平均幅值。最后把三类特征拼成一个长向量交给PCA或LDA做降维。这个包里在LoadFeatures.py之后有一个可选的特征选择环节如果你的数据量不大建议保留PCA把维度压到60到100之间能明显减少过拟合。2.3 数据划分与Dataset读取train.txt与train2.txttrain.txt和train2.txt这两个文件在包根目录下它们不是模型文件而是数据划分的索引列表。每一行对应一个样本窗口的路径train.txt是训练集索引train2.txt是验证集或测试集索引。Dataset.py和LoadFeatures.py根据这两个文件读取特征矩阵。这里要注意的是有的毕设项目喜欢把所有被试数据混合后随机划分这是多模态情感识别里最容易导致评估虚高的做法。同一个人的两个窗口如果分别落在训练集和测试集模型记住的是人的特征而不是情绪特征测试F1会虚高。这个包按窗口粒度划分我读下来它的设计思路是尽量让同一条连续记录的窗口不跨集合但分类边界未必做到被试级隔离。如果你要复现真实结果建议自己改成按被试分组。下一章我讲模型时再展开说训练集设计的影响。3. 多模态融合模型MultimodalCLIP.py的模态对齐思路与参数解读3.1 为什么不能简单拼接特征很多初看这个项目的人会问既然特征已经提取好了直接把EEG、眼动、GSR的特征拼成一个长向量丢给SVM或者一个全连接网络不就行了这个做法能跑通但效果往往平庸。原因在于三类信号对情绪的贡献方式不同。EEG的alpha和theta频带变化与放松、紧张这类激活度高度相关眼动主要反映注意力和愉悦度倾向GSR则更贴近压力和唤醒强度。它们描述的是情绪的不同维度简单拼接等于把不同尺度的信息强行放在一起模型需要额外学习每个模态的可信度而样本量有限时学不好。这个包里给出了另一种思路你不只是融合特征而是让各个模态先各自编码再通过一个对齐层把它们的表示空间统一起来。MultimodalModel.py负责主干分类MultimodalCLIP.py则实现了一个类似CLIP的对比学习分支。CLIP最初是图文对齐模型核心思想是让图像编码器和文本编码器输出的向量在同一个语义空间里靠近。这里借用同一思路让生理信号编码器输出的表示和情感标签文本编码器输出的表示在向量空间中对齐。3.2 MultimodalCLIP.py的对比学习落地写法下面是对比学习部分的示意代码我做了一些简化但保留了核心逻辑# MultimodalCLIP.py 关键片段模态对齐层 import torch import torch.nn as nn import torch.nn.functional as F class MultimodalCLIP(nn.Module): def __init__(self, modality_dim, embed_dim128, temperature0.07): super().__init__() # 将拼接后的生理特征投影到embedding空间 self.modality_proj nn.Linear(modality_dim, embed_dim) # 标签文本编码器这里用简单词嵌入全连接示意 self.text_encoder nn.Linear(text_dim, embed_dim) self.temperature temperature # 对比学习温度参数 def forward(self, feat, text_feat): # L2归一化把向量约束在单位超球面上 feat F.normalize(self.modality_proj(feat), dim-1) text_feat F.normalize(self.text_encoder(text_feat), dim-1) # 计算相似度矩阵除以温度控制分布锐度 logits feat text_feat.T / self.temperature return logitstemperature这个参数值得单独说。它控制着相似度分布的陡峭程度。值越小logits的数值越大softmax之后的分布越尖锐模型训练时对难分样本更敏感值越大分布越平滑训练稳定但收敛慢。0.07是CLIP原论文里图像文本对齐常用的一组经验值这个包直接沿用了。如果你发现训练初期loss震荡明显可以把温度调到0.1如果模型收敛后区分度不够再调回0.05左右重训一轮。对比学习分支的好处是它让模型在分类之前先学到了模态间的语义对应关系。比如一个高唤醒度、正愉悦度的样本EEG的beta功率高、瞳孔直径偏大、GSR出现了明显SCR这三个模态的特征表示会向同一个情感语义簇靠拢。这样即使某个模态在测试时噪声较大其他模态也能把表示拉回正确区域。3.3 读图看结构multimodal_full.png与config.yaml的参数对应包里的multimodal_full.png是一张整体结构图我建议读代码前先把这张图看一遍。图里从上到下大概是原始信号输入、预处理、特征提取、单模态编码器、模态对齐层、分类头这样的顺序。对应到项目文件里main.py是入口MultimodalModel.py是主干网络Trainer.py负责训练循环Tester.py负责加载best_model.pth做推理。config/config.yaml是整套实验的集中控制点。下面是我从该资源中梳理出的核心参数结构代码里对应的就是这一份# config/config.yaml 核心参数 data: train_txt: train.txt fs: 128 # EEG原始采样率 target_fs: 32 # 统一重采样后的采样率 window_s: 2 # 滑窗长度秒 stride_s: 0.5 # 滑窗步长秒 model: embed_dim: 128 # 模态对齐后的向量维度 temperature: 0.07 # CLIP对比学习温度 train: batch_size: 64 lr: 0.0001 epochs: 60 patience: 10 # 早停耐心值这几个参数决定了你的实验尺度。window_s设成2秒每个窗口包含大约64个时间点低频的GSR响应基本能覆盖到stride_s设0.5秒会造成窗口重叠数据量放大到接近4倍对缓解小数据集过拟合有帮助。如果你后续在自己的数据上复现我建议先保持这些默认值不改完整跑一轮看曲线形态再去动window_s和lr。4. 训练与评估闭环Trainer.py、Tester.py与指标读法4.1 Trainer.py的训练循环与早停策略Trainer.py是整个工程的发动机。它做的事情包括加载config.yaml参数、初始化模型和优化器、按epoch循环训练、每个epoch结束在验证集上评估、把验证集F1作为是否保存best_model.pth的唯一标准。下面是训练核心循环的示意代码# Trainer.py 关键片段一轮训练早停检查 for epoch in range(cfg.train.epochs): model.train() total_loss 0.0 for batch_x, batch_y in train_loader: pred model(batch_x) loss criterion(pred, batch_y) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() val_acc, val_f1 evaluate(model, val_loader) if val_f1 best_f1: torch.save(model.state_dict(), best_model.pth) best_f1 val_f1 patience_counter 0 else: patience_counter 1 if patience_counter cfg.train.patience: print(f早停于 epoch {epoch}) break注意这里保存模型用的是验证集F1不是准确率。情感分类的标签分布通常不平衡比如实验采集的样本里平静状态占一半以上高唤醒负情绪样本稀少如果按准确率选模型模型会退化成把所有样本都判成多数类。用F1做保存标准能兼顾精确率和召回率这点是值得借鉴的。patience10的意思是连续10个epoch验证集F1没有创新高就直接停。我在自己的实验里遇到过一个典型现象前15个epoch loss下降很快但验证集F1卡在0.4附近不动这时候不要急着改模型结构先看是不是学习率太大了把lr从0.0001降到0.00003重跑往往F1就松动上去了。4.2 评估指标的读法准确率、召回率、F1与交叉验证Tester.py加载best_model.pth后在测试集上输出准确率、召回率、F1三个数字。这里有个容易被忽视的点多模态情感识别通常按Russell情感圆环模型的两个维度来评估也就是愉悦度和激活度。愉悦度分正负激活度分高低组合起来是四分类。如果你只看总准确率会掩盖模型在某个象限上的严重偏科。我建议你在复现后把这四个类别的召回率单独打印出来看。比如模型对高激活正愉悦识别得很好召回率0.8但低激活负愉悦几乎全部漏检这说明GSR或眼动特征对这个类别的区分度不够需要回feature_extract.py里增加SCR相关特征而不是盲目调模型结构。包里results.txt输出的结果也是按这个思路组织的每行记录一个epoch的loss、准确率、F1和各类别召回率。4.3 结果产出的解读results.txt与Figure系列图训练完成后根目录下会生成results.txt和好几张Figure开头的图片。Figure_17到Figure_20这四张图是这个实验的可视化产物我拆包时看到它们对应的是损失曲线、验证集F1曲线、混淆矩阵以及不同模态组合的对比柱状图。这些图由printData.py和process_graph.py生成作用是让你直观看到训练过程是否健康。我拿到任何训练结果第一件事不是看准确率而是看loss曲线末尾有没有持续下降的迹象。如果训练完毕时loss还在下降但F1已经在原地抖动说明模型容量不够或特征区分度不足增加embed_dim大概率有效。如果loss已经平了但F1缓慢爬升则是优化器步长偏小把lr调大一倍重训即可。Figure系列图的价值就在这些判断上项目里的train2.txt和result3也是用来核对数据划分和中间结果的辅助文件。5. 多模态情感识别常见坑数据对齐、过拟合与标签不一致排查5.1 采样率不一致导致特征错位现象训练时loss正常下降验证集F1却始终在0.3到0.35之间横跳连续两个epoch没有提升早停触发。原因三类信号原始采样率不同预处理时如果只按时间戳拼接而不统一重采样EEG的128Hz数据会把GSR的低频变化完全淹没模型实际上只学到了EEG的模式。我在一开始直接拼接原始信号复现时验证准确率卡在35%左右和随机猜测一个水平。解决先在data_process.py里把每个模态分别重采样到同一个目标采样率再合并窗口。重采样之后用散点图检查三条信号在同一个窗口内的时间轴是否对齐这个检查我每次都会做。5.2 GSR信号响应滞后导致融合后效果反而变差现象单独用EEG特征训练时F1有0.55加入GSR特征后F1反而掉到0.48。原因GSR对情绪刺激的响应有2到5秒的潜伏期刺激出现后皮肤电导才慢慢上升。如果滑窗正好切在刺激刚出现的时刻GSR窗口内还没有反应模型把这个空窗口当成负样本学习等于往模型里灌噪声。解决对GSR信号做滞后补偿把整条GSR序列向前平移2秒左右或者用一个统计特征代表窗口前一段的SCR累积。我一般用后者因为平移的时间量本身也是一个超参数不同实验条件差别大。5.3 被试混杂划分导致F1虚高现象测试集F1达到0.78看起来结果很好但换一批被试重新采集数据后准确率直接掉到0.45。原因训练集和测试集来自同一个人的相邻窗口模型学到了个体特征而非情感共性。多模态生理信号里个体差异极大同一个情绪状态不同被试的EEG频谱分布完全不同。解决用GroupKFold按被试ID分组划分训练集和测试集保证同一个人的窗口不会同时出现在两边。这类项目里被试间的泛化能力才是真实水平的度量建议复现时强制走一遍这个分组逻辑。5.4 best_model.pth推理结果与训练结果不一致现象Tester.py加载best_model.pth后输出的准确率和训练时验证集结果差一大截。原因模型在训练时启用了dropout和BatchNorm推理时没有切换到eval模式导致神经元随机失活和BN统计量使用训练batch的均值和方差。解决推理代码里必须写model.eval()并用torch.no_grad()包住整个推理循环。下面的代码片段展示了正确写法# Tester.py 关键片段推理模式切换 checkpoint torch.load(best_model.pth, map_locationcpu) model.load_state_dict(checkpoint) model.eval() # 关键关闭dropout/BN训练行为 with torch.no_grad(): # 关闭梯度计算省显存 probs F.softmax(model(features), dim1) pred_label probs.argmax(dim1)5.5 环境依赖冲突cv2和numpy版本不匹配现象在Windows上跑main.py时报ImportError: DLL load failed while importing cv2或者numpy相关函数报错。原因多模态数据读取时常见opencv-python读取视频帧它与numpy 2.x存在兼容性问题尤其是python 3.8环境里装了新版本opencv容易出现二进制接口不匹配。解决创建独立conda环境固定numpy版本再装opencv。下面是实测可用的组合conda create -n mml python3.8 -y conda activate mml pip install numpy1.23.5 scipy scikit-learn opencv-python4.8.1.78 matplotlib pyyaml这套组合在这类生理信号处理项目里比较稳torch版本建议按你自己显卡配置装对应版本CPU环境下也能训练只是速度慢一些但跑通完全够。6. 复现验证与调参技巧让模型结果可落地的几个习惯拿到这个压缩包后我建议你按下面的顺序做一遍完整性复现而不要直接改代码。先把环境建好跑通main.py确认能产生results.txt和Figure系列图再跑Tester.py加载best_model.pth确认输出指标和results.txt里最后一轮记录一致最后再改参数每次只改一个。conda create -n mml python3.8 -y conda activate mml pip install torch1.13.1 numpy1.23.5 scipy scikit-learn opencv-python4.8.1.78 matplotlib pyyaml python main.py --config config/config.yaml python Tester.py --resume best_model.pth第一次跑通后我最推荐调的参数是window_s和temperature。窗口长度从2秒调到3秒GSR能捕捉到更完整的SCR波形眼动特征更平滑但总样本数会减少温度从0.07调到0.05模型对难分样本更敏感适合你的数据集本身情绪类别区分度不明显的情况。注意每次只改一个否则你无法判断是谁起的作用。最后分享一个习惯从那以后我每次拿到新的生理信号数据集都强制走一遍时间对齐检查和标签分布统计再决定要不要信任训练跑出来的F1数字。多模态情感识别这行数字好看往往不是模型的功劳而是数据预处理和评估方式的功劳把这两块想清楚复现和改参才不会翻车。希望帮到你。本文还有配套的精品资源点击获取
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑