
简介这份资源面向生物医学工程、人工智能与心理学方向的学习者和开发者围绕多模态生理信号情感识别这一交叉课题提供从数据到模型的完整实践参考。压缩包共39个文件约548.13MB包含mat原始与预处理数据、pkl数据集与标签、py与ipynb代码脚本以及jpg、png实验图表和md说明文档覆盖数据读取、预处理、特征提取、模型训练与结果分析等环节。资源以心率变异性、皮肤电导、脑电与眼电等信号为对象代码部分涉及多模态融合与注意力模型报告部分则记录实验设计、方法选择与结果讨论便于读者理解生理信号如何映射到情感状态。目前已有4135人学习下载适合希望复现完整课程大作业或入门多模态情感计算的研究者可据此掌握数据管线搭建、模型对比与结果可视化的具体做法。1. 多模态生理信号情感识别从玄学到可复现的工程路径做情感识别的人大多有过这样的经历单靠一路皮肤电信号EDA跑出来的模型在实验室数据集上准确率能到 90%一换被试就掉到 60% 以下。这不是模型不行而是单一模态本身携带的信息量有限——心率变化可能来自情绪波动也可能只是被试刚爬了层楼。多模态生理信号情感识别的核心思路就是把脑电EEG、皮肤电EDA、心电ECG、肌电EMG、呼吸RSP等几路信号放在一起建模让它们互相补位。这个方向在近两年的多模态情感分析研究里持续升温也是多模态融合论文里被反复验证有效的落地场景。这篇文章面向的是想真正把一套多模态生理信号情感识别流程跑通的人——不管你是做科研需要一份可复现的代码和报告还是做工程需要评估这个方案值不值得投入。我会把数据预处理、特征提取、融合策略、代码结构、报告输出这几段拆开讲清楚参数怎么设、坑在哪、报告怎么写都落到能直接抄的程度。2. 数据与信号多模态生理信号情感识别的原料从哪来2.1 常用数据集与模态选择多模态生理信号情感识别不是随便凑几路信号就行模态选择直接决定后面融合策略的天花板。目前公开数据集里被引用最多的是 DEAP、AMIGOS、DREAMER、MAHNOB-HCI 这几个。DEAP 记录 32 名被试观看音乐视频时的 EEG 和外周生理信号EDA、PPG、EMG、RSP 等标注了 valence、arousal、dominance、liking 四个维度是入门首选。AMIGOS 增加了多人社交场景适合做跨被试和群体情感研究。DREAMER 用可穿戴设备采集更贴近实际部署条件。选模态时有个常见误区觉得信号越多越好。实际上 EEG 和 EDA 的采样率、时间尺度、噪声特性差异很大硬拼在一起反而引入更多噪声。我一般建议先用 EEG EDA 两路跑通基线再逐步加 ECG 或 RSP。下面这张表是我在实际项目里总结的模态组合与典型表现模态组合典型准确率被试内典型准确率跨被试适用场景EEG 单模态85%–92%58%–68%实验室研究EDA 单模态70%–80%55%–62%可穿戴设备EEG EDA90%–95%65%–75%平衡精度与成本EEG EDA ECG91%–96%68%–78%高精度需求四模态以上92%–97%70%–80%科研探索注意跨被试准确率才是真正衡量泛化能力的指标被试内的高分很多时候是过拟合的假象。2.2 数据预处理的完整代码流程拿到原始信号后预处理是最耗时也最容易翻车的一步。下面这段代码覆盖了滤波、降采样、分段、归一化的完整流程基于 Python 和 MNE 库import numpy as np import mne from scipy.signal import butter, filtfilt # 参数说明 # fs_eeg: EEG采样率DEAP为128HzAMIGOS为256Hz # fs_peripheral: 外周信号采样率通常低于EEG # window_sec: 时间窗长度情感识别常用2-4秒 # overlap: 窗口重叠比例0.5表示50%重叠 def bandpass_filter(signal, lowcut, highcut, fs, order4): 巴特沃斯带通滤波去除工频和基线漂移 nyq 0.5 * fs b, a butter(order, [lowcut/nyq, highcut/nyq], btypeband) return filtfilt(b, a, signal, axis-1) def preprocess_eeg(raw_signal, fs_eeg128): EEG预处理1-45Hz带通 50Hz陷波 # 带通滤波保留Delta到Gamma主要频段 filtered bandpass_filter(raw_signal, 1.0, 45.0, fs_eeg) # 陷波去除工频干扰国内为50Hz notch mne.filter.notch_filter(filtered, fs_eeg, freqs50.0, verboseFalse) return notch def preprocess_eda(eda_signal, fs_eda128): EDA预处理0.05-5Hz低通保留SCL和SCR成分 return bandpass_filter(eda_signal, 0.05, 5.0, fs_eda) def segment_signal(signal, fs, window_sec4, overlap0.5): 滑动窗口分段返回 (n_windows, window_len, n_channels) win_len int(window_sec * fs) step int(win_len * (1 - overlap)) n_windows (signal.shape[-1] - win_len) // step 1 segments np.stack([ signal[..., i*step : i*step win_len] for i in range(n_windows) ], axis0) return segments def normalize_per_subject(data): 按被试做z-score归一化消除个体差异 mean data.mean(axis(0, 2), keepdimsTrue) std data.std(axis(0, 2), keepdimsTrue) 1e-8 return (data - mean) / std这段代码的逻辑链条是先滤波去噪再分段最后归一化。几个关键参数需要根据你的数据集调整。lowcut1.0是 EEG 的常规下限如果你关注慢波可以降到 0.5highcut45.0留了余量给 Gamma 频段。EDA 的0.05-5Hz是标准范围SCL皮肤电水平对应 0.05Hz 以下SCR皮肤电反应在 0.05-5Hz 之间这个带通同时保留了两种成分。窗口长度 4 秒是情感识别里的经验值——太短捕捉不到情感变化太长会混入多个情感状态。重叠 50% 是为了增加样本量但要注意训练集和测试集分段时不能有重叠泄漏。归一化这一步很多人会忽略。不同被试的皮肤电基线差异可以到几倍不做 per-subject 归一化模型学到的就是谁的信号强而不是什么情感。我见过不少论文被试内准确率很高一查代码发现归一化用了全局统计量测试集信息泄漏了。2.3 特征提取从时域到频域再到非线性预处理完的信号不能直接喂给分类器需要提取有物理意义的特征。EEG 主要看频段功率Delta1-4Hz、Theta4-8Hz、Alpha8-13Hz、Beta13-30Hz、Gamma30-45Hz情感效价和唤醒度跟这些频段在不同脑区的能量分布强相关。EDA 看 SCR 峰值数量、幅度、上升时间。ECG 看 HRV心率变异性的时域和频域指标。from scipy.signal import welch from scipy.stats import skew, kurtosis def extract_eeg_features(segment, fs128): 提取EEG频段功率 统计特征 segment形状: (n_channels, window_len) bands {delta: (1,4), theta: (4,8), alpha: (8,13), beta: (13,30), gamma: (30,45)} features [] for ch in range(segment.shape[0]): freqs, psd welch(segment[ch], fsfs, npersegfs*2) for band_name, (low, high) in bands.items(): idx np.logical_and(freqs low, freqs high) # 频段平均功率 features.append(np.mean(psd[idx])) # 统计特征补充 features.extend([np.mean(segment[ch]), np.std(segment[ch]), skew(segment[ch]), kurtosis(segment[ch])]) return np.array(features) def extract_eda_features(segment, fs128): 提取EDA的SCR特征峰值数、平均幅度、上升时间 from scipy.signal import find_peaks # 一阶差分找SCR上升沿 diff np.diff(segment[0]) peaks, props find_peaks(diff, height0.01, distancefs*1) n_scr len(peaks) mean_amp np.mean(props[peak_heights]) if n_scr 0 else 0 # 上升时间从谷值到峰值的时间 rise_times [] for p in peaks: start max(0, p - fs*2) valley np.argmin(segment[0][start:p1]) start rise_times.append((p - valley) / fs) return np.array([n_scr, mean_amp, np.mean(rise_times) if rise_times else 0, np.std(segment[0])])频段功率用 Welch 法估计 PSDnpersegfs*2对应 2 秒的段长频率分辨率 0.5Hz对 4 秒窗口来说够用。SCR 检测用一阶差分找上升沿height0.01是幅度阈值distancefs*1保证两个峰值至少间隔 1 秒——生理上 SCR 不应期大约就是这个量级。这些阈值需要根据你的信号幅度量纲调整如果 EDA 单位是微西门子0.01 可能偏小如果是伏特可能偏大。建议先画几段信号的 SCR 检测结果目视检查。3. 融合策略多模态特征怎么合才不掉点3.1 三种融合层次的选型理由多模态融合算法大致分三个层次早期融合特征级、中期融合决策级、晚期融合混合级。早期融合把不同模态的特征拼成一个长向量再分类实现简单但要求模态间时间对齐严格且高维特征容易过拟合。晚期融合让每个模态单独出预测再投票或加权平均鲁棒性好但丢失了模态间的交互信息。中期融合用注意力机制或图网络建模模态间关系是当前多模态融合论文的主流方向但实现复杂度高。我的建议是如果你刚起步先做早期融合跑基线确认数据 pipeline 没问题然后做晚期融合对比看模态间是否有互补性最后再上中期融合。不要一上来就搞复杂的跨模态注意力数据量不够时它比早期融合还差。import torch import torch.nn as nn class EarlyFusionClassifier(nn.Module): 早期融合拼接多模态特征后过MLP def __init__(self, eeg_dim, eda_dim, n_classes2, dropout0.3): super().__init__() self.net nn.Sequential( nn.Linear(eeg_dim eda_dim, 128), nn.BatchNorm1d(128), nn.ReLU(), nn.Dropout(dropout), nn.Linear(128, 64), nn.BatchNorm1d(64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, n_classes) ) def forward(self, eeg_feat, eda_feat): x torch.cat([eeg_feat, eda_feat], dim-1) return self.net(x) class AttentionFusion(nn.Module): 中期融合跨模态注意力 def __init__(self, eeg_dim, eda_dim, hidden64): super().__init__() self.eeg_proj nn.Linear(eeg_dim, hidden) self.eda_proj nn.Linear(eda_dim, hidden) self.cross_attn nn.MultiheadAttention(hidden, num_heads4, batch_firstTrue) self.classifier nn.Linear(hidden * 2, 2) def forward(self, eeg_feat, eda_feat): eeg_h self.eeg_proj(eeg_feat).unsqueeze(1) # (B,1,H) eda_h self.eda_proj(eda_feat).unsqueeze(1) # EEG查询EDAEDA查询EEG双向交互 eeg_attended, _ self.cross_attn(eeg_h, eda_h, eda_h) eda_attended, _ self.cross_attn(eda_h, eeg_h, eeg_h) fused torch.cat([eeg_attended.squeeze(1), eda_attended.squeeze(1)], dim-1) return self.classifier(fused)早期融合的 MLP 结构里BatchNorm1d放在Linear之后、激活之前是标准做法dropout0.3是生理信号小数据集的经验值——再高会欠拟合再低容易过拟合。中期融合的AttentionFusion用了双向 cross-attention让 EEG 和 EDA 互相查询num_heads4对应 64 维隐藏层每个头 16 维。如果你的模态超过两路可以把它们堆成序列维度用 self-attention 统一处理这也是多模态统一处理思路的一种落地方式。3.2 训练流程与评估协议训练时最大的坑是评估协议。生理信号情感识别必须做被试独立subject-independent划分即测试集的被试不能出现在训练集里。很多开源代码用的是随机划分准确率虚高 15-20 个百分点。from sklearn.model_selection import LeaveOneGroupOut from sklearn.preprocessing import StandardScaler from sklearn.svm import SVC from sklearn.metrics import accuracy_score, f1_score import numpy as np def subject_independent_eval(features, labels, subjects): 留一被试交叉验证 logo LeaveOneGroupOut() accs, f1s [], [] for train_idx, test_idx in logo.split(features, labels, subjects): X_train, X_test features[train_idx], features[test_idx] y_train, y_test labels[train_idx], labels[test_idx] # 标准化只在训练集上fit scaler StandardScaler() X_train scaler.fit_transform(X_train) X_test scaler.transform(X_test) clf SVC(kernelrbf, C1.0, gammascale) clf.fit(X_train, y_train) pred clf.predict(X_test) accs.append(accuracy_score(y_test, pred)) f1s.append(f1_score(y_test, pred, averageweighted)) return np.mean(accs), np.std(accs), np.mean(f1s)LeaveOneGroupOut按被试分组每次留一个被试做测试。StandardScaler只在训练集上 fit 再 transform 测试集这是防止信息泄漏的铁律。SVM 用 RBF 核C1.0和gammascale是 sklearn 的默认值在小样本上通常够用如果欠拟合可以把 C 调到 10过拟合就降到 0.1。返回的np.std(accs)很重要——如果标准差超过 10 个百分点说明模型对不同被试的适应性差异大需要检查归一化或考虑域适应方法。4. 避坑与排查多模态生理信号情感识别的 5 个血泪教训4.1 现象被试内准确率 95%跨被试掉到 55%原因归一化用了全局统计量或者分段时训练集和测试集有重叠窗口。生理信号的个体差异极大不做 per-subject 归一化模型学的是个体特征而非情感特征。解决所有归一化操作只在训练集上 fit测试集用训练集的参数 transform。分段时确保同一被试的窗口不跨训练/测试集。如果跨被试仍然差考虑加域适应层如 DANN或在特征层面做被试间对齐。4.2 现象EDA 特征全是零SCR 检测不到峰值原因EDA 信号量纲不对。DEAP 数据集里 EDA 单位是微西门子值域在 0-20 左右但有些数据集用伏特值域在 0-0.001。find_peaks的height0.01阈值在后者上永远触发不了。解决先打印信号的min、max、std确认量纲。阈值设成0.1 * std这种相对值而不是绝对值。或者先做 z-score 归一化再检测。4.3 现象EEG 频段功率在 Alpha 段异常高分类器只学 Alpha原因EEG 的 Alpha 波在闭眼放松时能量极强如果实验范式没有控制眼状态Alpha 功率会主导特征向量模型学到的是睁眼还是闭眼而不是情感。解决检查实验范式是否控制了眼状态。如果无法控制做眼电伪迹去除ICA 或回归法或者在特征层面做 per-channel 归一化让每个通道的贡献均衡。4.4 现象融合后比单模态还差原因模态间时间对齐没做好。EEG 采样率 128HzEDA 可能只有 32Hz直接拼接时如果按各自采样率分段同一个窗口对应的实际时间长度不同。解决统一重采样到相同采样率通常取最低的那个再做时间对齐。或者用模态特定的时间窗EEG 用 2 秒、EDA 用 8 秒在决策层融合。4.5 现象报告里准确率很高但混淆矩阵显示只预测了一个类原因类别不平衡。情感数据集里中性样本通常远多于高兴或悲伤模型退化成多数类预测器。解决看 F1 和混淆矩阵不要只看准确率。用class_weightbalanced或过采样少数类。报告里必须同时给出准确率、F1、混淆矩阵三个指标。5. 报告输出与进阶技巧让结果可复现、可验证5.1 自动化报告生成一份合格的多模态生理信号情感识别报告至少包含数据集描述、预处理参数、特征列表、模型结构、评估协议、逐被试结果、混淆矩阵、消融实验。手动写这些很容易漏我一般用脚本自动生成。import json from datetime import datetime def generate_report(config, results, output_pathreport.md): 根据配置和结果自动生成Markdown报告 lines [] lines.append(f# 多模态生理信号情感识别实验报告) lines.append(f生成时间{datetime.now().strftime(%Y-%m-%d %H:%M)}) lines.append(f\n## 实验配置) lines.append(f- 数据集{config[dataset]}) lines.append(f- 模态{, .join(config[modalities])}) lines.append(f- 窗口长度{config[window_sec]}秒) lines.append(f- 融合策略{config[fusion]}) lines.append(f\n## 评估结果) lines.append(f| 指标 | 均值 | 标准差 |) lines.append(f|------|------|--------|) for metric, (mean, std) in results[metrics].items(): lines.append(f| {metric} | {mean:.4f} | {std:.4f} |) lines.append(f\n## 逐被试结果) lines.append(f| 被试ID | 准确率 | F1 |) lines.append(f|--------|--------|-----|) for subj, acc, f1 in results[per_subject]: lines.append(f| {subj} | {acc:.4f} | {f1:.4f} |) with open(output_path, w, encodingutf-8) as f: f.write(\n.join(lines)) print(f报告已生成{output_path})这个脚本把配置和结果结构化输出成 Markdown 表格方便直接贴进论文或项目文档。config字典里记录所有超参数results里存逐被试指标。关键点是逐被试结果必须列出——只给平均值的报告没有说服力审稿人或同事无法判断模型在哪些被试上失效。5.2 消融实验的设计消融实验是验证多模态融合有效性的核心手段。我一般做四组对比EEG 单模态、EDA 单模态、早期融合、中期融合。每组跑同样的被试独立评估协议记录准确率和 F1。如果中期融合比早期融合提升不到 2 个百分点而实现复杂度高很多那就不值得上中期融合。还有一个容易被忽略的消融维度特征集。频段功率、统计特征、非线性特征熵、Hjorth 参数各自贡献多少把特征分组做消融能帮你砍掉冗余特征缩短训练时间。5.3 一个具体技巧用 SHAP 解释融合模型的决策多模态融合模型常被诟病是黑匣子。用 SHAP 值可以看每个模态、每个特征对预测的贡献。import shap from sklearn.ensemble import GradientBoostingClassifier # 用树模型做代理解释 clf GradientBoostingClassifier(n_estimators100, max_depth3) clf.fit(X_train, y_train) explainer shap.TreeExplainer(clf) shap_values explainer.shap_values(X_test) # 按模态分组看贡献 eeg_shap np.abs(shap_values[1][:, :eeg_dim]).mean() eda_shap np.abs(shap_values[1][:, eeg_dim:]).mean() print(fEEG平均SHAP贡献{eeg_shap:.4f}) print(fEDA平均SHAP贡献{eda_shap:.4f})GradientBoostingClassifier作为代理模型max_depth3防止过拟合。shap_values[1]取正类的 SHAP 值按模态维度切分后取绝对值均值就能量化每个模态的贡献。如果 EDA 的贡献接近零说明在这个数据集上 EDA 没提供额外信息可以考虑去掉。我自己踩过最深的坑是早期太迷信复杂融合结构花了两个月调 cross-attention 的层数和头数最后发现把归一化做对、把评估协议改对简单早期融合就能提升 10 个百分点。多模态生理信号情感识别这个方向数据质量 and 评估严谨性远比模型结构重要。希望帮到你。本文还有配套的精品资源点击获取