多模态生理信号时间对齐与情感识别Python工程实现 简介本资源是一套完整的毕业设计级多模态情感识别项目源码面向人工智能、生物医学工程及人机交互方向的本科生与研究生解决基于生理信号融合的情感状态自动判别问题。项目整合脑电EEG、眼动追踪与皮肤电GSR三类信号覆盖信号预处理、多源特征提取与融合、SVM/随机森林/神经网络等分类建模、Russell情感圆环模型映射及交叉验证评估全流程适用于心理健康监测、智能驾驶情绪反馈等前沿应用场景。压缩包共38个文件含14个核心Python模块如MultimodalModel.py、Trainer.py、feature_extract.py、9张结果可视化图Figure_*.png、6个XML配置与数据定义文件、4个文本说明与训练日志以及.pth模型权重和config.yaml参数配置整体8.99MB结构清晰、模块解耦便于复现、调试与二次开发。已有89人学习下载提供完整可运行代码、带注释的训练/测试流程、特征处理工具链及评估结果输出是开展多模态生理信号分析实践的高价值参考实现。1. 多模态情感识别不是“拼信号”而是让EEG、眼动、GSR在时间轴上真正对齐一个能跑通的Python工程级实现你手头有一套脑电EEG、眼动EOG、皮肤电GSR同步采集的数据采样率各不相同——EEG是256Hz眼动是60HzGSR是100Hz原始数据没打标标签文件是独立的txt时间戳单位还不统一有的毫秒有的帧号你试过直接concat特征向量喂进LSTM结果验证集准确率卡在58%不上不下连baseline都压不住。这不是模型不行是多模态还没真正“模”起来。本项目源码包MML_ZYC.zip就是一套从原始生理信号落地到可复现情感分类器的完整Python工程它不讲Russell圆环理论但内置了跨模态时间重采样对齐模块不堆SOTA模型名但用MultimodalCLIP.py实现了模态间注意力引导的特征投影不依赖商用SDK所有预处理带通滤波、ICA去眼电伪迹、GSR峰值检测全在RawData.py和feature_extract.py里写死参数、留注释、可调试。适合正在做毕设、需要交源码报告可视化图Figure_17.png到Figure_20.png全是训练过程曲线和混淆矩阵的工科生也适合想快速验证多模态融合策略是否work的算法工程师——它不是论文复现玩具而是一个删掉.idea和.gitignore就能扔进服务器跑通的生产级骨架。2. 数据加载与跨模态时间对齐为什么90%的多模态翻车始于第一步多模态情感识别最玄学的环节从来不是模型结构而是三路信号在时间维度上的“步调一致”。EEG、眼动、GSR不仅采样率不同触发机制也不同EEG设备可能用TTL脉冲打标眼动仪用帧号计时GSR记录器却只存绝对时间戳。若强行按文件顺序逐行读取、直接拼接相当于把256帧/秒的视频和60帧/秒的动画硬叠在一起——后续所有特征提取、模型训练都是在错误的时间基底上建楼。本项目用RawData.py data_process.py双模块解耦处理前者专注单模态原始信号解析与基础滤波后者负责跨模态时间轴归一化。关键不在“怎么读”而在“以谁为锚”。2.1 RawData.py三路信号的独立清洗流水线该模块按模态拆分为三个核心类EEGReader、EOGReader、GSRReader每个类封装对应传感器的原始数据解析逻辑。以EEGReader为例它不假设输入是.edf或.mat而是直接处理文本格式的train2.txt项目正文明确列出每行含128通道电压值时间戳# RawData.py 片段 class EEGReader: def __init__(self, file_path: str, fs: int 256): self.fs fs # 显式声明采样率避免隐式推断 self.data, self.timestamps self._load_txt(file_path) def _load_txt(self, path: str) - Tuple[np.ndarray, np.ndarray]: # 假设train2.txt每行t1 t2 ... t128 timestamp_ms raw np.loadtxt(path) # 最后一列是毫秒级时间戳其余是128通道值 data raw[:, :-1] # shape: (N_samples, 128) ts_ms raw[:, -1] # shape: (N_samples,) return data, ts_ms def preprocess(self, data: np.ndarray) - np.ndarray: # 标准1-45Hz带通滤波针对情绪相关频段 b, a butter(4, [1, 45], btypebandpass, fsself.fs) filtered filtfilt(b, a, data, axis0) # ICA去眼电伪迹仅对前2通道Fp1/Fp2因EOG主要污染额叶 ica FastICA(n_components2, random_state42) ica_out ica.fit_transform(filtered[:, :2]) # 将ICA分离出的眼电成分置零再重构 ica_out[:, 0] 0 # 假设第一主成分为EOG噪声 clean_frontal ica.inverse_transform(ica_out) filtered[:, :2] clean_frontal return filtered注意preprocess()中ICA仅作用于前2通道这是血泪经验——全通道ICA会破坏EEG的空间拓扑结构导致后续空间特征如通道间相位同步失效。项目用FastICA而非sklearn.decomposition.FastICA的默认参数n_components2强制降维避免过拟合噪声。2.2 data_process.py以GSR事件为时间锚点的重采样引擎GSR信号的皮电反应SCR具有明确的生理起始点即刺激后约1-3秒出现的峰值且其时间分辨率100Hz介于EEG256Hz和EOG60Hz之间天然适合作为跨模态对齐的“时间锚”。data_process.py中的TemporalAligner类正是以GSR的SCR峰值时间为基准反向截取各模态对应窗口# data_process.py 片段 class TemporalAligner: def __init__(self, gsr_reader: GSRReader, eeg_reader: EEGReader, eog_reader: EOGReader): self.gsr gsr_reader self.eeg eeg_reader self.eog eog_reader def align_to_gsr_peaks(self, window_sec: float 5.0) - Dict[str, np.ndarray]: # 步骤1在GSR信号中检测SCR峰值使用find_peaks最小间隔2s防误检 gsr_signal self.gsr.get_smoothed_signal() # 已低通滤波至10Hz peaks, _ find_peaks(gsr_signal, distanceint(2 * self.gsr.fs), heightnp.std(gsr_signal)*3) aligned_data {} for peak_idx in peaks[:100]: # 取前100个峰值避免内存爆炸 # 步骤2计算该峰值对应的时间戳ms gsr_ts self.gsr.timestamps[peak_idx] # 步骤3在EEG中查找最接近gsr_ts的时间戳索引并截取±2.5s窗口 eeg_closest_idx np.argmin(np.abs(self.eeg.timestamps - gsr_ts)) eeg_start max(0, eeg_closest_idx - int(2.5 * self.eeg.fs)) eeg_end min(len(self.eeg.data), eeg_closest_idx int(2.5 * self.eeg.fs)) eeg_window self.eeg.data[eeg_start:eeg_end] # 步骤4对EOG执行同样逻辑但需重采样至EEG采样率256Hz eog_closest_idx np.argmin(np.abs(self.eog.timestamps - gsr_ts)) eog_start max(0, eog_closest_idx - int(2.5 * self.eog.fs)) eog_end min(len(self.eog.data), eog_closest_idx int(2.5 * self.eog.fs)) eog_window self.eog.data[eog_start:eog_end] # 重采样scipy.signal.resample(eog_window, len(eeg_window)) # 步骤5GSR窗口±2.5s直接截取并重采样至256Hz gsr_start max(0, peak_idx - int(2.5 * self.gsr.fs)) gsr_end min(len(self.gsr.data), peak_idx int(2.5 * self.gsr.fs)) gsr_window self.gsr.data[gsr_start:gsr_end] # 统一重采样至256Hz以EEG为基准 eeg_resamp eeg_window eog_resamp resample(eog_window, len(eeg_window)) gsr_resamp resample(gsr_window, len(eeg_window)) # 拼接为 (seq_len, 12821) 的三维张量 fused np.concatenate([eeg_resamp, eog_resamp, gsr_resamp.reshape(-1,1)], axis1) aligned_data[fsample_{peak_idx}] fused return aligned_data逻辑说明window_sec5.0定义情感响应窗口2.5秒前2.5秒后符合GSR的典型潜伏期distanceint(2 * self.gsr.fs)确保SCR峰值间隔≥2秒过滤高频噪声假峰所有模态最终统一重采样至EEG的256Hz避免模型层引入插值误差输出fused张量维度为(seq_len, 131)其中128为EEG通道、2为EOG水平/垂直、1为GSR幅值——这是后续MultimodalModel.py的输入契约。2.3 Dataset.py带标签注入的懒加载数据集Dataset.py继承torch.utils.data.Dataset但关键创新在于__getitem__中动态注入情感标签。项目未提供原始标签文件而是通过train.txt项目正文列出间接关联该文件每行格式为sample_id emotion_label如sample_12345 valence:high arousal:low。Dataset类在初始化时解析此文件构建sample_id → label_vector映射表# Dataset.py 片段 class MultimodalDataset(Dataset): def __init__(self, aligned_data: Dict[str, np.ndarray], label_file: str, label_map: Dict[str, int] None): self.aligned_data aligned_data # {key: fused_array} self.label_map label_map or {valence:high: 0, valence:low: 1, arousal:high: 2, arousal:low: 3} # 解析train.txt生成id2label字典 self.id2label {} with open(label_file, r) as f: for line in f: parts line.strip().split() if len(parts) 2: continue sample_id, *labels parts # 合并多标签valence:high arousal:low → 0_3 label_key _.join([str(self.label_map.get(l, -1)) for l in labels]) self.id2label[sample_id] label_key self.keys list(aligned_data.keys()) def __getitem__(self, idx: int) - Tuple[torch.Tensor, torch.Tensor]: key self.keys[idx] # 加载对齐后的多模态张量 x torch.from_numpy(self.aligned_data[key]).float() # (seq_len, 131) # 获取对应标签one-hot编码 label_str self.id2label.get(key, 0) label_vec torch.zeros(len(self.label_map)) for l in label_str.split(_): if l.isdigit(): label_vec[int(l)] 1.0 return x, label_vec def __len__(self) - int: return len(self.keys)参数说明label_map支持自定义情感维度映射如将Valence-Arousal二维空间离散化为4类label_str.split(_)允许多标签共存如同时标注高愉悦度高唤醒度适配多维情感模型torch.zeros(len(self.label_map))生成稀疏标签向量避免硬编码类别数提升扩展性。3. 多模态特征融合架构从简单拼接走向模态间注意力引导当三路信号在时间轴上完成对齐下一步是决定“如何融合”。粗暴的torch.cat([eeg_feat, eog_feat, gsr_feat], dim-1)看似简单实则忽略了一个本质问题不同模态对情感状态的贡献权重是动态变化的。例如在观看恐怖片片段时GSR的幅值突增可能是最强线索而在执行认知任务时EEG的θ波功率变化可能更具判别力。本项目采用两阶段融合策略先用LoadFeatures.py提取各模态时频域特征再由MultimodalCLIP.py实现模态间对比学习驱动的特征投影最终输入MultimodalModel.py的混合Transformer。3.1 LoadFeatures.py为每种模态定制特征提取器该模块不追求通用性而是针对生理信号特性设计专用特征。例如EEG特征提取聚焦频带能量与非线性动力学指标而GSR仅需统计SCR幅度与恢复时间# LoadFeatures.py 片段 def extract_eeg_features(eeg_seq: np.ndarray, fs: int 256) - np.ndarray: 提取EEG的5大频带能量Hjorth参数 # 分频段δ(1-4Hz), θ(4-8Hz), α(8-13Hz), β(13-30Hz), γ(30-45Hz) bands [(1,4), (4,8), (8,13), (13,30), (30,45)] features [] for low, high in bands: b, a butter(4, [low, high], btypebandpass, fsfs) band_signal filtfilt(b, a, eeg_seq, axis0) energy np.mean(band_signal**2, axis0) # 每通道能量 features.append(energy) # Hjorth参数活动性、移动性、复杂性 activity np.var(eeg_seq, axis0) diff1 np.diff(eeg_seq, axis0) mobility np.sqrt(np.var(diff1, axis0) / activity) diff2 np.diff(diff1, axis0) complexity np.sqrt(np.var(diff2, axis0) / np.var(diff1, axis0)) features.extend([activity, mobility, complexity]) return np.concatenate(features) # (128*8,) 1024维 def extract_gsr_features(gsr_seq: np.ndarray) - np.ndarray: 提取GSR的3个核心指标峰值幅值、上升时间、半高宽 # 使用find_peaks检测主SCR peaks, props find_peaks(gsr_seq, heightnp.std(gsr_seq)*2, distancefs//2) if len(peaks) 0: return np.array([0, 0, 0]) # 无峰值则返回零向量 # 取第一个峰值最显著 peak_idx peaks[0] peak_amp gsr_seq[peak_idx] # 上升时间从基线到峰值的时间ms baseline np.mean(gsr_seq[max(0, peak_idx-100):peak_idx-10]) rise_start np.argmax(gsr_seq[:peak_idx] baseline*1.1) rise_time (peak_idx - rise_start) / fs * 1000 # 转为毫秒 # 半高宽峰值一半处的宽度ms half_max baseline (peak_amp - baseline) / 2 left np.argmax(gsr_seq[:peak_idx] half_max) right peak_idx np.argmax(gsr_seq[peak_idx:] half_max) fwhm (right - left) / fs * 1000 return np.array([peak_amp, rise_time, fwhm])逻辑说明extract_eeg_features()输出1024维向量128通道×8特征保留通道空间信息为后续空间注意力做准备extract_gsr_features()仅返回3维统计量因GSR是单通道信号时序建模意义弱于瞬态特征所有滤波器阶数固定为4butter(4,...)平衡计算效率与阻带衰减避免高阶滤波引入相位失真。3.2 MultimodalCLIP.py用对比损失拉近同情感样本的模态距离MultimodalCLIP.py是本项目的灵魂模块它借鉴CLIP思想但不用于图文匹配而是构建“EEG-GSR”、“EOG-GSR”等跨模态对比对。核心是ModalityProjector类将各模态特征映射到同一语义空间# MultimodalCLIP.py 片段 class ModalityProjector(nn.Module): def __init__(self, input_dim: int, proj_dim: int 128): super().__init__() self.projector nn.Sequential( nn.Linear(input_dim, 256), nn.ReLU(), nn.Dropout(0.3), nn.Linear(256, proj_dim) ) def forward(self, x: torch.Tensor) - torch.Tensor: return F.normalize(self.projector(x), dim-1) # L2归一化 class MultimodalCLIP(nn.Module): def __init__(self, eeg_dim: int 1024, eog_dim: int 128, gsr_dim: int 3): super().__init__() self.eeg_proj ModalityProjector(eeg_dim) self.eog_proj ModalityProjector(eog_dim) self.gsr_proj ModalityProjector(gsr_dim) def forward(self, eeg_feat: torch.Tensor, eog_feat: torch.Tensor, gsr_feat: torch.Tensor) - Tuple[torch.Tensor, ...]: eeg_emb self.eeg_proj(eeg_feat) # (B, 128) eog_emb self.eog_proj(eog_feat) # (B, 128) gsr_emb self.gsr_proj(gsr_feat) # (B, 128) return eeg_emb, eog_emb, gsr_emb # 对比损失函数在Trainer.py中调用 def clip_loss(eeg_emb, eog_emb, gsr_emb, temperature: float 0.07): # 构建相似度矩阵EEG vs GSR logits_eeg_gsr (eeg_emb gsr_emb.T) / temperature # (B, B) labels torch.arange(len(eeg_emb), deviceeeg_emb.device) # 计算InfoNCE损失对角线为正样本 loss_eeg_gsr F.cross_entropy(logits_eeg_gsr, labels) loss_gsr_eeg F.cross_entropy(logits_eeg_gsr.T, labels) # 同理计算EOG-GSR损失 logits_eog_gsr (eog_emb gsr_emb.T) / temperature loss_eog_gsr F.cross_entropy(logits_eog_gsr, labels) loss_gsr_eog F.cross_entropy(logits_eog_gsr.T, labels) return (loss_eeg_gsr loss_gsr_eeg loss_eog_gsr loss_gsr_eog) / 4参数说明proj_dim128是经验设定足够区分4类情感又避免高维嵌入导致对比学习不稳定temperature0.07沿用CLIP原论文值经项目实测在本数据上收敛最快损失函数计算eeg-gsr和eog-gsr两组对比因GSR作为生理唤醒指标与EEG/EOG的语义关联更强故未加入eeg-eog对比实测会降低性能。3.3 MultimodalModel.py混合Transformer与门控融合的主干网络MultimodalModel.py接收MultimodalCLIP输出的三路嵌入通过CrossModalAttention模块实现动态权重分配再经TemporalTransformer捕获长程依赖# MultimodalModel.py 片段 class CrossModalAttention(nn.Module): def __init__(self, embed_dim: int 128, num_heads: int 4): super().__init__() self.attn nn.MultiheadAttention(embed_dim, num_heads, batch_firstTrue) self.norm nn.LayerNorm(embed_dim) def forward(self, query: torch.Tensor, key_value: torch.Tensor) - torch.Tensor: # query: (B, 1, D), key_value: (B, 2, D) → 融合eoggsr作为KV attn_out, _ self.attn(query.unsqueeze(1), key_value, key_value) return self.norm(attn_out.squeeze(1) query) class MultimodalModel(nn.Module): def __init__(self, num_classes: int 4, dropout: float 0.3): super().__init__() self.clip MultimodalCLIP() self.cross_attn CrossModalAttention() self.temporal_transformer nn.TransformerEncoder( nn.TransformerEncoderLayer(d_model128, nhead4, dropoutdropout, batch_firstTrue), num_layers2 ) self.classifier nn.Sequential( nn.Linear(128, 64), nn.ReLU(), nn.Dropout(dropout), nn.Linear(64, num_classes) ) def forward(self, eeg_seq: torch.Tensor, eog_seq: torch.Tensor, gsr_seq: torch.Tensor) - torch.Tensor: # Step1: 提取各模态特征调用LoadFeatures.py逻辑 eeg_feat extract_eeg_features(eeg_seq.numpy()) # 需在forward外预计算 eog_feat extract_eog_features(eog_seq.numpy()) gsr_feat extract_gsr_features(gsr_seq.numpy()) # Step2: CLIP投影实际代码中feat已转为tensor eeg_emb, eog_emb, gsr_emb self.clip( torch.from_numpy(eeg_feat).float().to(eeg_seq.device), torch.from_numpy(eog_feat).float().to(eog_seq.device), torch.from_numpy(gsr_feat).float().to(gsr_seq.device) ) # Step3: 以EEG为QueryEOGGSR为Key/Value进行跨模态注意力 kv torch.stack([eog_emb, gsr_emb], dim1) # (B, 2, 128) fused_emb self.cross_attn(eeg_emb, kv) # (B, 128) # Step4: 时序Transformer需将fused_emb扩展为序列 seq_emb fused_emb.unsqueeze(1) # (B, 1, 128) temporal_out self.temporal_transformer(seq_emb) # (B, 1, 128) # Step5: 分类 return self.classifier(temporal_out.squeeze(1))逻辑说明CrossModalAttention强制以EEG为Query因EEG包含最丰富的情感神经表征kv torch.stack([eog_emb, gsr_emb], dim1)将EOG/GSR合并为2步长序列避免单独建模增加参数temporal_transformer仅1步长输入实为“位置编码FFN”的变体专注模态内语义增强非传统时序建模。4. 训练与评估全流程从config.yaml配置到results.txt结果解析本项目将超参、路径、模型选择全部收口于config/config.yaml杜绝硬编码。Trainer.py作为训练入口严格遵循“数据加载→模型构建→损失定义→循环训练→验证评估”标准流程。Tester.py则提供离线推理与可视化能力输出results.txt及Figure_17.png等四张核心图表。4.1 config.yaml一份配置文件管住所有变量config/config.yaml采用分层设计dataset、model、training三大节清晰隔离关注点# config/config.yaml dataset: root_dir: ./data/ # 原始数据根目录 train_file: train.txt # 标签文件路径 align_window_sec: 5.0 # 时间对齐窗口秒 eeg_fs: 256 # EEG采样率 eog_fs: 60 # EOG采样率 gsr_fs: 100 # GSR采样率 model: clip_proj_dim: 128 # CLIP投影维度 transformer_layers: 2 # Transformer层数 num_classes: 4 # 情感类别数valence/arousal二分组合 training: batch_size: 32 epochs: 100 lr: 0.001 weight_decay: 1e-4 clip_grad_norm: 1.0 # 梯度裁剪阈值 save_best_only: true # 仅保存最佳模型 checkpoint_dir: ./checkpoints/ log_dir: ./logs/提示修改align_window_sec需同步调整LoadFeatures.py中extract_gsr_features的find_peaks参数否则SCR检测可能失败。项目默认5.0秒已通过实测验证。4.2 Trainer.py带早停与学习率调度的健壮训练器Trainer.py的核心是train_epoch()与validate_epoch()方法其中validate_epoch()不仅计算准确率还生成混淆矩阵并保存为Figure_17.png# Trainer.py 片段 def validate_epoch(self, model: nn.Module, val_loader: DataLoader) - Dict[str, float]: model.eval() all_preds, all_labels [], [] with torch.no_grad(): for x_batch, y_batch in val_loader: x_batch [x.to(self.device) for x in x_batch] # [eeg, eog, gsr] y_pred model(*x_batch) preds torch.argmax(y_pred, dim1).cpu().numpy() labels torch.argmax(y_batch, dim1).cpu().numpy() all_preds.extend(preds) all_labels.extend(labels) # 计算指标 acc accuracy_score(all_labels, all_preds) f1 f1_score(all_labels, all_preds, averageweighted) cm confusion_matrix(all_labels, all_preds) # 绘制混淆矩阵并保存 plt.figure(figsize(8,6)) sns.heatmap(cm, annotTrue, fmtd, cmapBlues, xticklabels[VH_AL, VL_AL, VH_AH, VL_AH], yticklabels[VH_AL, VL_AL, VH_AH, VL_AH]) plt.title(Confusion Matrix) plt.ylabel(True Label) plt.xlabel(Predicted Label) plt.savefig(./results/Figure_17.png, dpi300, bbox_inchestight) plt.close() return {accuracy: acc, f1_score: f1, confusion_matrix: cm}逻辑说明xticklabels和yticklabels按项目约定的4类情感缩写标注VH_ALValence High Arousal Low确保图表可读性bbox_inchestight避免标签被截断这是生成Figure_17.png时的关键细节f1_score(..., averageweighted)适配多标签不平衡场景比宏平均更鲁棒。4.3 Tester.py一键生成报告与可视化图谱Tester.py提供test_model()函数加载best_model.pth后执行三重验证定量报告输出results.txt含准确率、F1、精确率、召回率及每类详细指标定性分析生成Figure_18.png训练损失曲线、Figure_19.png验证准确率曲线、Figure_20.png特征可视化t-SNE图案例回溯对错误样本打印原始信号片段如sample_12345的EEG波形GSR峰值定位误判根源。# Tester.py 片段 def test_model(model_path: str, test_loader: DataLoader, output_dir: str ./results/): model torch.load(model_path) model.eval() # 1. 定量指标 metrics calculate_metrics(model, test_loader) with open(f{output_dir}/results.txt, w) as f: f.write(fAccuracy: {metrics[accuracy]:.4f}\n) f.write(fF1-Score: {metrics[f1_score]:.4f}\n) f.write(fPrecision: {metrics[precision]:.4f}\n) f.write(fRecall: {metrics[recall]:.4f}\n) f.write(\nPer-class metrics:\n) for cls, m in metrics[per_class].items(): f.write(f{cls}: P{m[precision]:.4f}, R{m[recall]:.4f}, F1{m[f1]:.4f}\n) # 2. 绘制训练曲线需从log_dir读取历史loss/acc plot_training_curves(log_dir./logs/, save_pathf{output_dir}/Figure_18.png) # 3. t-SNE可视化取1000个样本的CLIP嵌入 embeddings, labels extract_embeddings(model, test_loader, n_samples1000) plot_tsne(embeddings, labels, save_pathf{output_dir}/Figure_20.png)参数说明n_samples1000限制t-SNE计算量避免内存溢出项目实测1000点已能清晰分离4类簇plot_training_curves()自动解析./logs/下的train_loss.log和val_acc.log无需手动导出results.txt采用固定字段命名方便CI/CD脚本解析如自动化测试平台可直接grep Accuracy: results.txt提取数值。5. 避坑指南那些让多模态情感识别项目停滞不前的5个真实陷阱多模态情感识别的坑往往藏在数据预处理的毫秒级偏差、特征维度的隐式错位、甚至文件编码的BOM头里。以下是本项目实测踩过的5个具体问题每个都附带现象、根因与可立即执行的解决方案。5.1 现象train2.txt加载后EEG数据形状为(N, 129)而非(N, 128)导致RawData.py中data[:, :-1]报错原因train2.txt文件开头存在UTF-8 BOM头EF BB BFnp.loadtxt()将其解析为第一列使总列数1。解决在RawData.py的_load_txt()函数中强制指定encodingutf-8-sig自动剥离BOMdef _load_txt(self, path: str) - Tuple[np.ndarray, np.ndarray]: # 读取时自动去除BOM with open(path, r, encodingutf-8-sig) as f: lines f.readlines() # 手动解析每行避免loadtxt的BOM敏感 data_list, ts_list [], [] for line in lines: if not line.strip(): continue parts list(map(float, line.strip().split())) data_list.append(parts[:-1]) # 丢弃最后一列时间戳 ts_list.append(parts[-1]) return np.array(data_list), np.array(ts_list)5.2 现象GSR信号find_peaks()检测不到任何SCRextract_gsr_features()返回全零向量原因原始GSR数据单位为微西门子μS但采集设备导出时未校准实际值在0.1~0.5范围远低于np.std(gsr_seq)*2的默认阈值。解决在GSRReader.preprocess()中添加自适应基线校正def preprocess(self, data: np.ndarray) - np.ndarray: # 先平滑Savitzky-Golay滤波 smoothed savgol_filter(data, window_length51, polyorder3) # 计算动态基线滑动窗口均值窗口5秒 baseline uniform_filter1d(smoothed, sizeint(5 * self.fs)) # 归一化(signal - baseline) / baseline放大相对变化 normalized (smoothed - baseline) / (baseline 1e-6) return normalized注意normalized输出为无量纲比值find_peaks的height参数需相应下调至0.05。5.3 现象MultimodalCLIP训练时损失不下降logits_eeg_gsr矩阵全为负值原因ModalityProjector的F.normalize()在forward中执行但clip_loss()计算eeg_emb gsr_emb.T时若某批次eeg_emb全为零因梯度消失归一化后仍为零向量点积恒为0。解决在ModalityProjector.forward()中添加梯度检查与防零机制def forward(self, x: torch.Tensor) - torch.Tensor: proj self.projector(x) # 防零若proj范数1e-6加小扰动 norm torch.norm p a hrefhttps://download.csdn.net/download/yanglamei1962/90617217 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p