简介:一套基于Python的多模态情感识别毕业设计项目源代码,融合脑电(EEG)、眼动追踪与皮肤电(GSR)三类生理信号,面向需要快速搭建情感分类系统的研究人员与本科生。代码采用模块化结构,覆盖原始信号预处理、噪声滤除、特征提取与降维(PCA/LDA)、SVM/随机森林/神经网络分类器训练,以及情感模型构建和性能评估(准确率、F1值)等完整流程;同时结合Russell情感环状模型解释生理信号到情感状态的映射关系,便于理论联系实际。压缩包共38个文件,以14个Python脚本为主体,分别承担数据加载、特征处理、模型定义、训练与测试等任务,另含模型权重(.pth)、参数配置(.yaml)、结果图表(.png)和文本记录(.txt)等辅助资源,整包仅8.99MB,目录层次清晰,适合按模块逐项研读与二次开发。已有89人学习浏览,对正在筹备毕业设计或研究多模态情感识别算法(如心理健康监测、智能客服等应用)的读者具有直接参考价值。
1. 多模态情感识别到底在识什么:从一个翻车Demo说起
多模态情感识别,就是把脑电、眼动和皮肤电三路生理信号,统一处理成可以进分类器的特征,最后对情绪状态做预测。这类毕业设计项目最困难的点,往往不是分类模型选什么,而是三路信号的「时间对齐」和「伪迹处理」这两道坎。我见过不少同学把 EEG 直接滤波,GSR 整段取均值,眼动不加过滤就拼进特征向量,结果训练集准确率很高,验证集却失灵,最后只能靠调随机种子续命。我在某次模拟项目里也是这样踩过坑,后来把流程改成「先理解每个模态的信号特性,再逐段预处理、滑窗、抽特征、融合、嵌套验证」,效果才算稳定下来。这篇文章就是按这条链路整理的,新手可以照着跑,熟手可以直接拿走参数和避坑清单。
2. 三个模态各自的脾气:EEG、眼动、GSR 的信号特点与预处理选型
2.1 EEG 是中枢信号的「重灾区」:伪迹与参考电极怎么选
EEG 反映的是大脑皮层群体神经元的电活动,幅度在微伏量级,采样率从 128Hz 到 1000Hz 都有。它的优势是时间分辨率高,能直接体现情绪相关的神经振荡变化;缺点是信噪比低,工频干扰、眨眼、头动、肌电都会污染信号。特别是额叶区域的眨眼伪迹,幅度远超真实脑电,如果不处理,模型学到的可能不是情绪而只是眨眼频率。
预处理顺序我一般固定为:先做带通滤波(0.5~45Hz),再剔除明显坏段,然后做独立成分分析(ICA)去眨眼和肌电,最后按需重参考。这里有个容易被忽略的点:ICA 不是数据越多越好。通道数少于 8 或者单段数据短于 30 秒时,ICA 分解极不稳定,算出的成分可能只是噪声的线性组合。这时候我宁可用阈值剔除加手动分段,把包含明显伪迹的片段丢弃,也不硬跑 ICA。
参考电极方面,单通道设备通常用前额或耳垂参考,多通道设备常见有平均参考和重参考两种选择。平均参考在离线分析里用得比较多,但它要求通道覆盖全脑比较均匀,否则会把参考本身的电位偏差引入全脑通道。我一般先以前额参考跑通全流程,把基线结果记录下来;如果情绪分类效果不明显,再切换平均参考对比一次,只在有必要时保留这个改动。这样变量少,答辩时也好解释。
EEG 特征这一块,我常选微分熵、功率谱密度或者短时傅里叶变换能量。微分熵对效价和唤醒度的区分度都不错;功率谱密度解释直观;短时傅里叶变换适合看某个时间段内的变化。三个模态里,EEG 特征维度最容易膨胀,多通道加上多频带,一次就能生成几十维特征,后面一定要配特征选择,不能全量喂给模型。
2.2 GSR 不是一条平滑曲线:Tonic 与 Phasic 分解才是关键
皮肤电信号(GSR/EDA)反映自主神经系统的唤起水平,幅度变化比 EEG 慢得多,主要能量集中在 0.05Hz 以下。很多人觉得它只有一条缓慢上升的曲线,滤波之后取均值就结束。实际上 GSR 应该拆成两个成分:Tonic(整体基线,缓慢漂移,对应长时间的唤醒水平)和 Phasic(叠加在基线之上的快速波动,对应具体刺激引发的即时反应)。不拆分,把原始均值直接当特征,模型学到的往往只是设备漂移或实验进程,和情绪本身关系不大。
分解的严格做法是使用专门的 EDA 分解工具,利用去卷积策略估计 Tonic 和 Phasic。我实际写代码时,考虑到可解释性和依赖控制,常先用移动平均得到 Tonic 近似,再用原始值减 Tonic 得到 Phasic。虽然不如专门工具精细,但在样本量和特征口径上都够用,而且每一行代码都在自己手里,答辩时能讲清楚。如果一定要更精细,也建议先用工具跑一次对比,确认自带实现不会显著劣化再替换。
Phasic 成分里,我最看重三个统计量:峰值幅度、上升时间、恢复时间。峰值幅度反映刺激强度;上升时间反映反应速度;恢复时间反映情绪回落的平稳程度。这三个量联合起来,比单纯看均值更能描述「事件后发生了什么」。提取时要注意:峰值检测前要给 Phasic 做一次轻平滑,否则基线噪声会在检测里变成一堆假峰。平滑窗口参考值在 200~400 毫秒,具体取决于采样率,目标是让真实事件峰不能被磨平。
2.3 眼动数据最容易被低估:采样率与有效性判断
眼动数据常见来源包括注视坐标、眼跳事件、瞳孔直径和眨眼事件,采样率从 60Hz 到 500Hz 不等。和 EEG、GSR 相比,眼动的优势是行为指标直接、不需要复杂信号分解,缺点是无效值非常多:眨眼瞬间、视线出界、设备追丢,都会产生零值或丢失片段。直接把瞳孔直径数组喂进特征提取,模型会学到「设备什么时候失效」而不是情绪模式。
我处理眼动的固定顺序是:先按事件类型过滤,只保留注视和眼跳样本;然后检查瞳孔直径的缺失比例,短于 500 毫秒的缺失用线性插值,超过 500 毫秒的片段直接丢弃;最后再做平滑和特征提取。这个 500 毫秒的阈值不是拍脑袋,瞳孔对情绪刺激的响应通常有几百毫秒时延,缺失短于响应尺度还能用插值补救,长于响应尺度就说明这段数据本身不可靠。
眼动特征至少要覆盖四类:注视时长均值与方差、眼跳幅度均值、单位时间眨眼次数、瞳孔直径均值与变化率。注视时长和认知负荷相关,眨眼次数和疲惫及唤醒相关,瞳孔变化率和唤醒度相关。这三路信号恰好覆盖中枢神经(EEG)、自主神经(GSR)和行为(眼动)三个层面,融合的互补性就建立在这个基础上。
3. 从原始信号到能进模型的特征:滑窗与特征提取的完整写法
3.1 滑窗分帧与时间对齐:先把三条数据拉到同一时间轴
情感识别里,我一般用 2~5 秒的滑窗作为基本样本单元。窗口太短,脑电频带分辨率不够,GSR 的 Phasic 峰值也来不及完整出现;窗口太长,一个窗口里可能混入多个情绪状态,标签就不干净。三种数据采样率不一样,先在各模态完成各自的滤波预处理,再统一插值到同一个时间基准上。
import numpy as np def sliding_window(data, fs, win_len=4.0, overlap=2.0): """对单通道/多通道时序数据做滑窗分帧。 data: shape=(n_samples,) 或 (n_samples, n_channels) fs: 采样率,单位 Hz win_len: 窗口长度,秒 overlap: 窗口重叠,秒;overlap < win_len 返回: windows 列表,每个元素是窗口内的数据切片 """ step = int(fs * (win_len - overlap)) win = int(fs * win_len) starts = range(0, data.shape[0] - win + 1, step) windows = [data[s:s + win] for s in starts] return windows # 假设三个模态已经各自完成滤波和坏段剔除,再统一到同一时间基准 common_fs = 128 # 统一到128Hz,便于后续特征处理 t_end = min(eeg_t[-1], gaze_t[-1], gsr_t[-1]) t_common = np.arange(0, t_end, 1.0 / common_fs) eeg_r = np.interp(t_common, eeg_t, eeg_raw) pupil_r = np.interp(t_common, gaze_t, pupil_raw) gsr_r = np.interp(t_common, gsr_t, gsr_raw)逻辑说明:滑窗的核心是 step 和 win 的计算一致。overlap 越大,样本越多,但相邻窗口相关性也越高,后面交叉验证如果按窗口随机划分,就会出现严重的信息重叠泄漏。我一般把 overlap 设为窗口的 50%,既保证样本量,又不至于让相邻样本完全重复。
参数说明:win_len 取 4 秒左右在情感生理识别里比较常见,既能覆盖一个完整的刺激事件,又不至于把多个情绪状态混在一个窗口里。如果任务只区分效价(积极/消极),4~5 秒窗口可靠;如果只做唤醒度高低的二分类,2 秒窗口也够,生成样本数更多。注意这里的 interp 只做时间对齐,各模态的高频细节必须在插值前用各自的滤波保留,否则高频成分会被插值抹平。
注意:先插值后滤波会让高频信息失真。正确顺序是先各自滤波、再按统一时间轴插值重采样。
3.2 EEG 微分熵特征:用一行代码算出频带能量
微分熵特征是这段代码的关键。微分熵在情绪识别研究里很常用,而且对短窗口比较稳定。实现上先对每个窗口做 Welch 功率谱估计,再按频带累计能量,取对数后得到特征。
from scipy.signal import welch def de_features(eeg_win, fs=128): """从单通道EEG窗口计算微分熵特征。 频带划分:delta(0.5-4), theta(4-8), alpha(8-13), beta(13-30), gamma(30-45) 返回: 长度为5的特征向量 """ freq, psd = welch(eeg_win, fs=fs, nperseg=min(256, len(eeg_win))) bands = [(0.5, 4), (4, 8), (8, 13), (13, 30), (30, 45)] de = [] for lo, hi in bands: mask = (freq >= lo) & (freq < hi) power = psd[mask].sum() # 微分熵近似等于 log(power),对尺度变化不敏感 de.append(np.log(power + 1e-12)) return np.array(de) # 对每个EEG窗口计算DE特征 eeg_feats = np.array([de_features(w, fs=common_fs) for w in eeg_windows])逻辑说明:welch 会把序列切段做平均功率谱估计,窗口太短时 nperseg 要相应调小,否则低频段分辨率不够。这个 DE 特征对电平漂移和微小幅度波动相对稳定,是比原始幅值特征稳得多的选择。把 DE 特征和均值、标准差等统计特征放在一起时,建议先做标准化,因为 log(power) 的量级和小数特征不在同一尺度。
参数说明:gamma 频带在脑电采集里非常容易混入肌电伪迹,如果数据预处理不够干净,gamma 带特征往往噪声很大,可以考虑直接去掉。频带的上下界不要随意改,不同研究用的划分有细微差别,答辩时能说出「参考了常见情绪识别频带划分」即可,但代码里要把上下界写清楚,避免后人无法复现。
3.3 皮肤电与眼动特征:别只用均值,把「变化过程」也抽出来
皮肤电和眼动特征相对更偏统计,但核心是不要只取均值,还要把变化过程量化出来。
from scipy.ndimage import uniform_filter1d def gsr_features(gsr_win, fs=128): """从GSR窗口提取特征。 先取低通近似tonic,再做tonic与phasic分解。 返回: tonic均值、phasic标准差、峰值幅度、上升时间等 """ # 简单的tonic估计:50点移动平均,大约0.4秒尺度 tonic = uniform_filter1d(gsr_win, size=int(fs * 0.4)) phasic = gsr_win - tonic # 计算phasic片段的峰值数量与平均峰值幅度 peaks = np.where((phasic[1:-1] > phasic[:-2]) & (phasic[1:-1] > phasic[2:]))[0] peak_vals = phasic[peaks] valid_peaks = peak_vals[peak_vals > np.std(phasic) + 1e-6] return np.array([ tonic.mean(), phasic.std(), valid_peaks.mean() if len(valid_peaks) else 0.0, (peaks[-1] - peaks[0]) / fs if len(peaks) > 1 else 0.0, ]) def gaze_features(gaze_win, fs=128): """从眼动窗口提取特征,输入为瞳孔直径序列或注视坐标。""" valid = gaze_win[~np.isnan(gaze_win)] if len(valid) < int(fs * 1): return np.full(5, np.nan) # 有效数据不足,置NaN,交给上游处理 return np.array([ valid.mean(), valid.std(), np.diff(valid).mean(), (valid[-1] - valid[0]) / len(valid), np.sum(np.abs(np.diff(valid)) > np.std(valid) * 2) / len(valid), ])逻辑说明:tonic 估计的窗口大小直接影响 phasic 的残差。0.4 秒在多数研究中能对应皮肤电快速波动的尺度,太小会把 phasic 压平,太大会把慢漂移留进 phasic。峰值检测上面没有做专门去抖动操作,实际用的时候建议先对 phasic 做一次带通滤波或平滑,否则基线抖动会被算成峰值。
参数说明:NaN 的处理这里特意留出来,是因为不同分类器对缺失值策略不同。树模型可以直接处理 NaN,SVM 不行。所以我在第 4 章的特征拼接里会专门做一次 NaN 填充和标准化。眼动特征里连续差异的均值近似反映瞳孔变化的方向,和情感唤醒度相关。但瞳孔直径也容易受光照影响,如果实验环境亮度不稳定,最好把这个特征标记为不确定特征。
4. 多模态融合分类:特征拼接之外,还得防数据泄漏
4.1 特征级融合与简单决策融合的最小实现
特征级融合是最直接的多模态融合方式:把三类特征向量拼接成一个长向量,进入模型。它的优点是把决策权完全交给分类器,让模型自己学习三种信号间的交互;缺点是特征维度变高、样本量不变,容易过拟合,并且不同模态特征尺度差异大,需要标准化。先看最小实现。
from sklearn.preprocessing import StandardScaler from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import RepeatedStratifiedKFold, cross_val_score # 假设已经得到 eeg_feats, gsr_feats, gaze_feats,每个都是 (n_windows, feat_dim) # 先做NaN填充:按列均值填充 def fill_nan(X): col_mean = np.nanmean(X, axis=0) inds = np.where(np.isnan(X)) X[inds] = np.take(col_mean, inds[1]) return X X_eeg = fill_nan(eeg_feats.copy()) X_gsr = fill_nan(gsr_feats.copy()) X_gaze = fill_nan(gaze_feats.copy()) X_concat = np.hstack([X_eeg, X_gsr, X_gaze]) y = labels # 每个窗口一个标签 scaler = StandardScaler() X_scaled = scaler.fit_transform(X_concat) model = RandomForestClassifier(n_estimators=300, max_depth=8, random_state=42) cv = RepeatedStratifiedKFold(n_splits=5, n_repeats=3, random_state=42) scores = cross_val_score(model, X_scaled, y, cv=cv, scoring='accuracy') print(f'Accuracy: {scores.mean():.4f} ± {scores.std():.4f}')逻辑说明:上面的代码有一个学术上不严谨的地方:StandardScaler 是在全部数据上 fit 的。严格来说,验证集的信息通过均值和方差进入了训练,属于一种轻微泄漏。正确做法是封装 Pipeline,让 StandardScaler 在每一折 fit 时只看到训练集部分。下面的 4.3 会给出正确写法,这里先跑通整体流程,便于定位问题是出在特征还是出在模型。
参数说明:n_estimators=300 对随机森林来说足够稳定,max_depth=8 能限制过拟合。RepeatedStratifiedKFold 的 n_repeats=3 是为了看到模型在不同划分下的方差。打印的均值加减标准差是答辩时最有用的指标,比单次准确率更有说服力。
4.2 模型选型与参数范围:从 SVM 到随机森林
特征维度不高、样本量中等时,我一般先跑 SVM(径向基核)和随机森林,对比后取稳定一方。SVM 在小样本分类上很强,但 RBF 核的两个参数 C 和 gamma 比较敏感;随机森林不用调参过于精细,但对特征噪声相对无碍。实际项目里,我先把特征维度压到 50 以内再跑 SVM,否则 RBF 核在原始高维特征上非常容易过拟合。
SVM 对特征尺度敏感,用 StandardScaler 是必须的。随机森林则没那么敏感,但特征重要性输出在答辩中特别好用。两者都属于可解释性和稳定性较好的模型,比深度模型更适合毕业设计场景。深度模型在样本量几千以下的多模态任务中,收益很有限。如果你确实想试深度学习,也建议先用上面的简单模型跑通基线,再对比一两个轻量网络,不要一上来就构造复杂融合结构。答辩时老师更关心的是你能否证明每个模态都提供了增量信息。
SVM 调参范围我一般固定在下面这个网格里:
| 参数 | 取值范围 | 说明 |
|---|---|---|
| C | 0.1 ~ 10 | 惩罚系数,越小越正则 |
| gamma | 0.001 ~ 0.1 | RBF核宽度,越小越平滑 |
| class_weight | balanced | 情绪类别不平衡时开启 |
随机森林我主要调 n_estimators 和 max_depth,前者固定在 200~500,后者在 6~16 之间。如果特征数量本身不多,max_depth 设小一点反而更稳。
4.3 数据泄漏的三个隐藏入口:标准化、特征选择和交叉验证
标准化泄漏是新手最常见的问题。如果先在全量数据上算均值和方差,再划分训练测试,测试集的分布信息已经参与了标准化。解决办法是使用 Pipeline,让 StandardScaler 在每一折的 fit 阶段只看训练数据。
特征选择同样存在泄漏。先用全部数据选出 top 特征再交叉验证,选特征这一步已经把未来信息透传过去了。解决办法是把特征选择也放进 Pipeline。我的习惯做法是写一个简单的嵌套验证结构:
from sklearn.pipeline import Pipeline from sklearn.feature_selection import SelectKBest, f_classif from sklearn.model_selection import StratifiedKFold pipe = Pipeline([ ('scaler', StandardScaler()), ('select', SelectKBest(f_classif, k=40)), ('clf', RandomForestClassifier(n_estimators=300, max_depth=8, random_state=42)) ]) cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) for train_idx, test_idx in cv.split(X_concat, y): X_train, X_test = X_concat[train_idx], X_concat[test_idx] y_train, y_test = y[train_idx], y[test_idx] pipe.fit(X_train, y_train) y_pred = pipe.predict(X_test) acc = (y_pred == y_test).mean()逻辑说明:SelectKBest 在每一折 fit 里只见到训练数据,k 值和分类器超参数没有在测试集上调,保证了验证的公正性。交叉验证的划分方式也需要留心:如果滑窗重叠率很高,相邻窗口内容几乎相同,按窗口随机划分会让训练集和验证集出现「近似重复样本」。最稳妥的办法是按实验区块或按被试划分,比如把每个被试的连续数据放在同一折里,避免同源数据同时出现在训练和验证。
参数说明:k=40 是起始值,实际操作中要根据总特征维度调整。如果总维度只有 60,k 取 40 没问题;如果总维度到 200,我建议从 30 开始试,观察验证集准确率随 k 的变化曲线。Scikit-learn 的 Pipeline 在这里最大的价值不是代码简洁,而是强制每一步都在折内执行,从机制上堵住泄漏。
5. 跑多模态识别的五个坑:现象、原因与排查方法
5.1 现象:训练集准确率高,验证集一塌糊涂
这个现象几乎每个刚把特征拼起来的人都会碰到。原因一般是两个:一是滑窗 overlap 过大,相邻窗口几乎重复,交叉验证按窗口抽样时,训练集和验证集里都出现近似同样的样本;二是特征维度远高于样本量,模型把噪声也记住了。
解决:先把 overlap 降为窗口的 25% 再重跑,观察准确率差的幅度;再把 SelectKBest 的 k 值调低到 15~30,看验证集是否明显好转。如果两者都无效,检查标签是不是按实验 block 顺序组织的,如果是,改按 block 做分组划分。一个简单检查方法是打印每个窗口的起始时间,统计同一情绪标签是否总连续出现在某几个时间块里,连续出现说明标签顺序和时间高度相关,随机划分会严重高估模型。
5.2 现象:加了眼动后效果反而变差
这不是眼动特征没用,而是没对齐或没做有效性过滤。常见原因:瞳孔直径缺失段被 interp 填得过于平滑,导致伪特征进入模型;或者眨眼计数直接变成噪音频带。
解决:先绘制不同情绪标签下瞳孔直径的时间曲线,确认是否存在肉眼可见的分组差异。如果没有,把瞳孔相关特征去掉,只保留注视和眼跳特征,再看模型是否恢复原先水平。这个对比过程在答辩时可以很好地展示特征筛选逻辑,比单纯说「融合更好」更有说服力。
5.3 现象:GSR 特征全是 NaN
NaN 多半来自原始 GSR 数据采集端已经断开或传了零值,触发了均值计算问题。排查步骤:打印 GSR 原始序列的零值和 NaN 位置,看看是否是长时段缺失。如果缺失超过总时长的 5%,建议直接丢弃该被试,而不是全用插值处理。
插值可以补齐微小间隙,无法补救长时间信号中断导致的生理信息丢失。如果缺失片段较短,可以用前后各 500 毫秒的均值替换,但要在预处理记录里标注这一段被填充过。答辩时如果被问到,可以诚实说明这一段数据质量有限,解释自己做了剔除或填充决策。
5.4 现象:EEG 特征在两次实验之间分布漂移
电极放置位置、导电膏状态、设备状态都会让特征分布漂移。比较明显的现象是同一被试不同实验时段跑出的模型准确率差异很大。解决:预处理时加入一个整体去趋势步骤,把每个窗口的均值减掉再做特征提取;同时记录阻抗值,把阻抗过高的片段直接删掉。
这些操作比在模型层做领域自适应更直接,也更容易在毕业设计条件下落地。如果同一被试数据分多次采集,建议在特征提取后分别统计各批次特征的均值和方差,差异过大时考虑只保留质量好的批次。
5.5 现象:特征拼接后维度爆炸,模型跑不动
脑电多通道加多频带,再叠加眼动和 GSR,特征很容易到几百维,而样本只有几十个窗口。解决:先对所有特征做相关性分析,把 Pearson 相关系数高于 0.9 的冗余特征只保留一个,再用 SelectKBest 压到 30~60 维。
不要迷信「维度高信息多」,在样本量不足时,特征维度和过拟合几乎成正比。我之前有个模拟项目就是通道数翻倍后,准确率反而下降,把特征压到 50 维后效果立刻恢复。相关性筛选的代码可以用 pandas 的 corr 矩阵快速实现,权重不要设太严,否则会丢有效信息。
6. 给验收加一道保险:稳定性和可解释性检查
6.1 稳定性验证:重复 K 折与特征重要性的置信区间
交叉验证只给出了平均准确率和标准差,但回答「特征是否稳定有效」还需要看特征重要性的波动。我习惯在每次重复的折里记录特征重要性,最后统计每个特征被选中或排名靠前的次数。若某个特征在三次重复里排名波动巨大,那它的贡献就不可靠,答辩时最好弱化这个结论。
from sklearn.model_selection import train_test_split from collections import Counter importance_votes = Counter() for repeat in range(3): X_tr, X_te, y_tr, y_te = train_test_split( X_concat, y, test_size=0.2, stratify=y, random_state=repeat ) selector = SelectKBest(f_classif, k=30).fit(X_tr, y_tr) for idx in selector.get_support(indices=True): importance_votes[idx] += 1 top_features = [f for f, c in importance_votes.most_common(20) if c >= 2]逻辑说明:经过三轮独立划分,最终只保留至少两次被选中的特征,它们对随机划分更鲁棒。这个 top 特征集合可以直接拿到答辩材料里,以表格形式呈现特征名、来源模态和选中次数,比一张混淆矩阵更能说明工作量和设计思路。特征名要自己提前维护好索引对应关系,不然到这里会分不清第几个特征来自哪个模态。
6.2 用混淆矩阵和特征贡献度回答「为什么有效」
答辩时老师通常会问「你的模型为什么比单模态好」。最直接的证据是:单模态、双模态、三模态三组对比准确率。准备一个三行一列的表,分别给 EEG-only、EEG+GSR、EEG+GSR+眼动,标注重复 K 折均值与标准差。如果三模态确实最优,再结合特征贡献度解释哪些类别靠 GSR、哪些类别靠眼动。
把被试和实验数据分开,用图表展示每个模态的边际贡献,这一页能覆盖大部分追问。我踩过最深的坑是直接拿三模态结果去汇报,被问「每个模态增量多少」时答不上来;后来补了消融实验,才算是把项目真正做完。从那以后凡是多模态项目,我一定先跑消融再谈主结果。希望帮到你。
本文还有配套的精品资源,点击获取