简介:情绪识别是脑机接口与情感计算领域的核心任务,通过对脑电(EEG)等生理信号的分析,可以客观量化人的效价与唤醒度状态。在实际工程中,数据预处理与特征工程往往比模型结构更能决定分类效果。基于公开的DEAP数据集,信号需经过基线校正、带通滤波及ICA去伪迹等步骤,再提取微分熵(DE)等频域特征,进而构建适用于小样本场景的传统机器学习模型(如SVM)或深度学习模型(如LSTM)。这一技术路线在用户情绪监测、智能交互、心理状态评估等场景具有广泛应用价值。以sentimentclassify master项目为实例,完整梳理了从DEAP数据读取、预处理、特征提取到模型训练与评估的全流程,并针对跨被试泛化、过拟合等常见问题给出可落地的排查方案,为从事生理信号处理与情绪识别研究的开发者提供了一套可直接参考的工程实践路径。
1. 项目概述与设计思路
1.1 这个项目到底在做什么
先把这个标题拆开说清楚:sentimentclassify master,是一个基于DEAP数据集的情绪识别分类项目。DEAP(Database for Emotion Analysis using Physiological signals)是目前情绪计算领域最常用的公开数据集之一,由伦敦玛丽女王大学发布,里面有32名受试者在观看40段音乐视频时的脑电(EEG)和外周生理信号记录。这个项目干的事情,就是把这些多模态生理信号喂给机器学习或深度学习模型,让模型学会判断一个人当前的情绪状态——通常是效价(Valence,积极/消极)和唤醒度(Arousal,兴奋/平静)这两个维度。
做情绪识别的人对这套流程应该不陌生:数据下载与解析、预处理、特征提取、模型训练、评估指标输出。但DEAP这个数据集有几个独特的坑,比如数据格式是.mat和.bdf混合、标签是连续值还是离散值、EEG信号怎么切片、怎么做跨被试泛化测试,这些细节处理不好,后面模型做得再花哨也白搭。这篇文章我就以sentimentclassify master这个项目的实际开发为主线,把从拿到原始数据到最终输出分类结果的全流程,包括每一步的代码逻辑、参数依据、踩过的坑,完整捋一遍。
这套方案适合谁?如果你是刚接触情绪识别、手里只有原始DEAP数据包不知道从哪下手,或者已经跑通了一个baseline但准确率上不去想找优化方向,这篇文章能给你一套可以直接参考执行的路径。对做脑机接口、多模态情感计算、生理信号处理的同学也值得一看。
1.2 为什么用DEAP而不是其他数据集
情绪识别领域其实不缺数据集,但DEAP能在这么长时间里保持高引用率,有它的道理。对比一下几个常见选择:
| 数据集 | 信号类型 | 被试人数 | 刺激材料 | 标签维度 | 可用性 |
|---|---|---|---|---|---|
| DEAP | EEG + 外周生理信号 | 32 | 40段音乐视频 | Valence/Arousal/Dominance/Liking | 需申请,免费 |
| SEED | EEG | 15 | 15段电影片段 | 正/中/负三类 | 需申请,免费 |
| MAHNOB-HCI | EEG + 外周信号 + 面部视频 | 30 | 20段视频 | Valence/Arousal等 | 需申请 |
| AMIGOS | EEG + 外周信号 + 视频 | 40 | 16段视频 | Valence/Arousal等 | 需申请 |
DEAP的优势在于三点:第一,同时提供了EEG和外周生理信号,这对于做多模态融合研究非常重要,你可以在同一份数据上对比单模态和多模态的效果差距;第二,40通道的信号覆盖足够全,32通道EEG按照国际10-20系统放置,外加8通道外周信号(EOG眼电、EMG肌电、GSR皮电、呼吸、体温等),这个通道数在公开情绪数据集里算非常豪华的配置;第三,每个实验包含3秒基线加60秒刺激,时间上的结构便于做基于时间窗的分析,无论是做片段级分类还是序列模型输入都很方便。
你可能会问,为什么不用SEED?SEED只有EEG,任务是被试看电影片段产生正、中、负三种情绪,类别少、任务简单,模型上限有限。DEAP的连续维度评分更适合做精细的情绪模型,比如回归到具体的效价值,或者把效价和唤醒度组合成四象限做分类,这些都是工业界更关心的可量化指标。
1.3 项目整体技术路线
sentimentclassify master的技术路线可以概括为:原始数据 → 信号预处理 → 特征提取 → 数据集构建 → 模型训练 → 评估与可视化。其中预处理环节处理坏导联和伪迹,特征提取环节计算微分熵(Differential Entropy, DE)、功率谱密度(Power Spectral Density, PSD)等频域特征,模型层支持传统机器学习(SVM、随机森林)和深度学习(LSTM、CNN)两类方案,方便对比。
为什么把特征提取的重点放在频域特征上?情绪相关的神经科学研究有个共识,不同情绪状态下,大脑各频段(delta、theta、alpha、beta、gamma)的振荡模式会发生显著变化。比如积极情绪往往伴随额叶区alpha波的不对称性变化,高唤醒状态下gamma波活动增强。微分熵是香农熵在连续信号上的推广,在固定频带内可以近似为对数功率谱的积分,对情绪状态的区分度在大量研究中被验证优于单纯的能量特征。所以这套项目把DE特征作为默认特征,同时保留PSD和时域统计特征作为对比选项,是合理的。
模型层面,项目同时提供传统机器学习和深度学习两条路径,这样做是有意的。传统方法(比如SVM加RBF核)在小样本、特征维度适中的场景下表现稳定,训练快,适合作为baseline快速验证特征有效性。深度学习模型则能够在足够数据量下自动挖掘信号中的深层时空模式,对跨被试泛化能力更强,但需要调的参数更多。两套方案互为参照,也方便你在自己的数据上做对比实验。
2. DEAP数据集全解析:从下载到数据解析
2.1 数据集的获取与文件结构
先说下载。DEAP数据集需要向伦敦玛丽女王大学提交申请,填一个表格说明研究用途,审批通过后会收到下载链接。下载的路径是固定的:http://www.eecs.qmul.ac.uk/mmv/datasets/deap/download.html,进去后能看到以下内容:
| 文件 | 大小 | 内容说明 |
|---|---|---|
| data_preprocessed_matlab.zip | 约1.5GB | 降采样到128Hz、经过预处理的32名被试数据 |
| data_original_bdf.zip | 约20GB+ | 原始BDF格式数据,512Hz采样,未经处理 |
| DEAP_GENEVA.zip | 约20GB | 基因数据(部分被试) |
| Video_Saliency.zip | 约700MB | 视频显著性数据 |
| Questionnaires.zip | 约500MB | 问卷原始数据 |
实际做分类任务时,绝大多数人会选data_preprocessed_matlab.zip,理由很直接:原始BDF文件需要专门的库(如pyedflib、MNE的BDF读取接口)来解析,而且要自己做滤波、去伪迹、降采样,这一整套流程的时间和代码成本都高;预处理好版本则是按128Hz重采样,每个被试一个.mat文件,结构清晰,加载直接能用。但是我要提醒一句,官方说的预处理只是做了基础的降采样和简单滤波,EEG信号里的眼电伪迹、肌肉伪迹并没有完全去除干净,所以拿到手之后,最好再用MNE做一次独立成分分析(ICA)去眼电。
下载完之后,你会在data_preprocessed_matlab目录下看到32个文件,命名规律是s01.mat、s02.mat一直到s32.mat。每个文件内部结构如下:
s01.mat ├── data: 40 x 40 x 8064的数组 ├── labels: 40 x 4的数组 └── stimulus: 40 x 3(视频ID、开始时间、结束时间等)data数组的解释:第一维40是40段实验视频,第二维40是40个信号通道,第三维8064是时间点。8064怎么来的?60秒刺激加上3秒基线,共63秒,乘以128Hz采样率,得到8064个采样点。前3秒是基线(即384个采样点),后面60秒是被试观看视频时的信号。通道排序在前32个是EEG通道,顺序是Fp1, AF3, F3, F7, FC5, FC1, C3, T7, CP5, CP1, P3, P7, PO3, O1, Oz, Pz, Fp2, AF4, F4, F8, FC6, FC2, C4, T8, CP6, CP2, P4, P8, PO4, O2, FC5(这里官方注释和实际顺序建议你用MNE的montage文件匹配一下), 以及外围生理信号8通道。
labels数组的解释:每行对应一段视频,4列分别是valence(效价,1-9分)、arousal(唤醒度,1-9分)、dominance(支配度,1-9分)、liking(喜欢程度,1-9分)。这个评分是被试看完每段视频后自己打的,是主观情绪标签的客观量化。
2.2 用Python正确加载DEAP的.mat文件
加载这个文件有个非常经典的坑:DEAP的.mat文件是用MATLAB 7.3版本保存的,也就是HDF5格式,直接用scipy.io.loadmat会报错。必须用h5py来读取。我自己第一次跑这个项目时就在这里卡了快一个小时,提示的错误是"NotImplementedError: Please use HDF reader for matlab v7.3 files",印象实在太深了。
正确的加载方式如下:
import h5py import numpy as np def load_deap_mat(file_path): """加载DEAP数据集的.mat文件(HDF5格式)""" with h5py.File(file_path, 'r') as f: data = f['data'][:] labels = f['labels'][:] # h5py读取时会多出1维,需要压缩 data = np.array(data).transpose(2, 1, 0) # 转成(40, 40, 8064) labels = np.array(labels).transpose(1, 0) # 转成(40, 4) return data, labels有人会问,为什么要transpose?因为HDF5存储时保持了MATLAB的列主序,直接用就会得到维度顺序错乱的数据。转置之后,data[i, j, k]的语义才是:第i个视频实验、第j个通道、第k个采样点。
2.3 数据划分策略:被试内还是被试间
处理DEAP数据时,一个绕不开的问题是数据划分的方式,它直接决定你的指标含义。常见两种方案:
被试内(within-subject):把每个被试的40个视频样本按比例划分训练和测试集,比如7:3。这种方式假设模型在一个人的数据上训练后测试同一个人的新数据,好处是数据量相对充足,但真实应用场景有限,因为实际使用时模型面对的往往是没见过的新用户。
被试间(cross-subject):用一部分被试的所有数据做训练,另一部分被试的数据做测试。这种方式更接近实际部署场景,但难度明显更大,因为不同人的EEG信号差异非常大(个体差异、电极佩戴位置误差、皮肤状态等都会带来偏移),模型很容易出现过拟合到训练被试的情况,测试准确率通常比被试内低10到20个百分点。
sentimentclassify master里的默认策略是被试内,方便快速验证特征和模型效果。但在项目中也预留了被试间划分的接口,只需要在配置文件中设置cross_subject=True,代码会自动按照被试ID进行GroupKFold划分。如果你在复现别人论文里的结果,一定要先搞清楚对方的划分策略,否则指标对比完全没意义。
3. 数据预处理:从原始信号到干净输入
3.1 基线校正的作用与实现
DEAP的每个实验开始前有3秒静息态基线,这个基线信号的意义在于提供一个情绪中性状态的参考。不同被试即使处于中性状态,他们的EEG绝对功率也可能差异很大,这和颅骨厚度、电极阻抗、皮肤状态都有关系。如果我们直接用原始信号做特征提取,这些个体差异会淹没真实的情绪相关信号变化。
基线校正的思路很简单:把刺激开始后每个采样点的值,减去基线段对应通道的平均值。精确点说,如果你要做的是频域特征,应该分别计算基线段和刺激段的特征,然后取两者的差值或比值。代码层面:
def baseline_correction(data, baseline_samples=384): """用一个通道一个通道地做基线扣除""" corrected = np.zeros_like(data) for trial in range(data.shape[0]): for ch in range(data.shape[1]): baseline_mean = np.mean(data[trial, ch, :baseline_samples]) corrected[trial, ch, :] = data[trial, ch, :] - baseline_mean return corrected一句话总结:基线校正解决的是"个体中性状态不同"的问题,让后续提取的特征更多反映情绪引起的相对变化,而不是绝对功率的个体差异。这一步别省。
3.2 滤波器的选择与参数依据
DEAP官方预处理虽然包含了带通滤波,但我习惯在特征提取前再手动限制一次频带范围。理由有两个:一是去掉50Hz工频干扰及其谐波(中国和欧洲电网是50Hz,部分公开代码用60Hz陷波算出来的结果在国内场景会有偏差);二是把分析频率限制在情绪研究最关注的0.5-45Hz范围,避免高频噪声影响特征质量。
使用MNE库来实现滤波非常方便:
import mne def filter_eeg(raw_data, sfreq=128, l_freq=0.5, h_freq=45.0): """对EEG数据进行带通滤波,参数按情绪研究常用频带设置""" info = mne.create_info(ch_names=['CH' + str(i) for i in range(raw_data.shape[1])], sfreq=sfreq, ch_types='eeg') raw = mne.io.RawArray(raw_data.T, info, verbose=False) raw.filter(l_freq=l_freq, h_freq=h_freq, fir_design='firwin', verbose=False) return raw.get_data().T这里l_freq设0.5而不是1,是为了保留delta频段(0.5-4Hz),因为delta波在DEAP数据集上对效价分类的贡献在很多研究里被验证是显著的,尤其对消极情绪。h_freq设45而不是50,既是避开工频干扰,也是为了限制在gamma频段的有效范围。如果你的后续分析包含gamma1(30-45Hz)但不关心更高频,这个参数是合理的。
3.3 伪迹去除:ICA的实际操作
预处理中最容易忽略但影响巨大的环节是眼电伪迹去除。人在观看视频时眼球会频繁移动和眨眼,这些动作产生的大幅值电信号会通过容积传导扩散到头皮所有电极,尤其在额叶区域。如果你不做处理,模型很可能学到的是"眨眼模式"而不是"情绪模式",这样的模型换一个被试或者换一个场景,效果会直线下降。
处理方案首选独立成分分析(ICA)。ICA能把混合信号分解成统计独立的源信号,其中眼电成分通常对应一到两个独立成分,识别并剔除它们后再重建信号,就能把眼动和眨眼的干扰压到很低。
实际操作步骤:
from mne.preprocessing import ICA def remove_eog_artifacts(raw, eog_channels=['35', '36']): """用ICA自动去除眼电伪迹,eog_channels是DEAP中EOG通道的索引""" ica = ICA(n_components=20, method='fastica', random_state=42, max_iter=500) ica.fit(raw, picks='eeg') # 在EOG通道上找到与眼电高度相关的独立成分 eog_indices, eog_scores = ica.find_bads_eog(raw, ch_name=eog_channels, threshold=2.5) print(f"检测到 {len(eog_indices)} 个眼电相关成分: {eog_indices}") ica.exclude = eog_indices raw_clean = ica.apply(raw) return raw_clean需要提醒的是,DEAP的data_preprocessed_matlab版本中,EOG通道在通道列表里的索引是35和36(1-based)。ICA的参数设置要注意:n_components设20意味着只保留20个主成分,这对40通道的数据来说是合理的降维,既能覆盖绝大部分信号方差,又避免过拟合和计算量爆炸。max_iter设500是因为fastica算法偶尔不收敛,给足迭代次数能减少报错。random_state固定42保证结果可复现,这点在做研究时很重要。
4. 特征提取:把原始信号变成模型能理解的语言
4.1 微分熵特征与频带划分
情绪识别领域目前最主流、效果最稳定的特征之一就是微分熵(Differential Entropy,DE)。DE的定义是对连续随机变量的香农熵的推广,对于一个服从高斯分布的信号,特定频段上的DE可以近似为:
[ DE = \frac{1}{2} \log(2\pi e \sigma^2) ]
其中(\sigma^2)是该频段信号功率谱密度的均值。这个公式在工程实现上有巨大的优势:只要计算出某个频带的平均功率,就能直接得到DE值,计算开销极小,而且对信号幅值变化不敏感,跨被试鲁棒性比原始功率谱更好。
DEAP研究中通常把EEG信号划分成五个频段:
| 频段名称 | 频率范围(Hz) | 与情绪的相关性 |
|---|---|---|
| Delta | 0.5-4 | 深度放松、睡眠状态,消极情绪时额叶区增强 |
| Theta | 4-8 | 记忆加工、情绪调节 |
| Alpha | 8-13 | 放松状态、积极情绪,额叶不对称性相关 |
| Beta | 13-30 | 激活状态、注意力、高唤醒度 |
| Gamma | 30-45 | 高级认知加工、积极情绪、跨脑区协同 |
每个被试每个通道每个频段计算一个DE值,特征维度就是32通道乘以5频段等于160维。再加上8个外周通道的特征(通常取时域统计值),总特征维度在170到200之间。这个维度对传统机器学习来说非常友好,不需要额外的特征选择步骤就能直接训练。
4.2 特征提取的完整代码实现
下面是sentimentclassify master项目中提取DE特征的核心代码,我给每一段都加了注释说明为什么这么做:
import numpy as np from scipy import signal def compute_de_feature(eeg_segment, sfreq=128): """ 输入:单个通道、单个时间窗的EEG信号 输出:五个频段的微分熵特征 """ # 1. 去掉直流分量,避免频谱泄露导致低频段功率虚高 eeg_segment = eeg_segment - np.mean(eeg_segment) # 2. 计算功率谱密度(使用Welch方法,比直接FFT更平滑) freqs, psd = signal.welch(eeg_segment, fs=sfreq, nperseg=128, noverlap=64) # 3. 定义频段边界 bands = { 'delta': [0.5, 4], 'theta': [4, 8], 'alpha': [8, 13], 'beta': [13, 30], 'gamma': [30, 45] } de_features = [] for band_name, (low, high) in bands.items(): # 找到频率落在当前频段内的PSD索引 idx = np.logical_and(freqs >= low, freqs <= high) # 计算该频段的平均功率 power = np.mean(psd[idx]) # 微分熵 = 0.5 * log(2 * pi * e * power) de = 0.5 * np.log(2 * np.pi * np.e * power + 1e-10) de_features.append(de) return np.array(de_features)实际提取整个数据集的特征时,先把每个被试每个视频的60秒信号切分成若干时间窗(常见是1秒或4秒的窗,步长可以重叠50%),每个时间窗独立计算特征,最后对窗内特征取平均作为该视频的特征向量。这样既能利用更多数据增强统计稳定性,又能为大样本模型(如LSTM)准备时间序列输入。具体代码逻辑:
def build_feature_dataset(data, sfreq=128, window_size=4, step_size=2): """ 从DEAP原始数据中构建特征矩阵 data: 单个被试的数据,形状为(40, 40, 8064) 返回特征矩阵和形状信息 """ window_len = int(window_size * sfreq) # 4秒窗 = 512个采样点 step_len = int(step_size * sfreq) # 2秒步长 = 256个采样点 n_trials = data.shape[0] n_channels = data.shape[1] # 刺激段从采样点384开始(3秒基线之后) stim_start = 384 all_features = [] for trial in range(n_trials): stim_data = data[trial, :, stim_start:] # 去掉基线 n_samples = stim_data.shape[1] trial_windows = [] for start in range(0, n_samples - window_len + 1, step_len): end = start + window_len window_features = [] for ch in range(n_channels): # 如果ch < 32,用DE特征;否则用外周信号的时域统计 if ch < 32: de_feat = compute_de_feature(stim_data[ch, start:end], sfreq) window_features.extend(de_feat) else: seg = stim_data[ch, start:end] window_features.extend([ np.mean(seg), np.std(seg), np.ptp(seg), np.sqrt(np.mean(np.square(seg))) ]) trial_windows.append(window_features) # 对同一视频的所有时间窗取平均,得到一个样本的最终特征 trial_features = np.mean(np.array(trial_windows), axis=0) all_features.append(trial_features) return np.array(all_features)用4秒窗、2秒步长,每段视频60秒就产生29个时间窗,对窗特征取平均后,每个视频得到一个固定长度的特征向量。这个窗口长度(4秒)是权衡后的选择:太短(比如1秒)会导致频谱分辨率不足,Welch方法在128Hz采样率下1秒窗的nperseg只有128,频率分辨率只有1Hz,想区分0.5-4Hz的delta频段显然不够;太长(比如8秒以上)则会把情绪的瞬时动态平滑掉,而且会让有效样本数减少。4秒窗配合128点的nperseg,频率分辨率是1Hz,虽然对delta频段还是有点糙,但实测下来在DEAP上的分类效果已经够用。
4.3 标签处理策略
DEAP的标签是1-9的连续评分,直接做分类任务需要先把连续值转成离散类别。最常见的做法是对效价和唤醒度分别以5为阈值做二分类:valence > 5记为积极(1),否则为消极(0);arousal > 5记为高唤醒(1),否则为低唤醒(0)。这样可以把问题定义为二分类任务,简单直观,也是大多数论文采用的方案。
如果你想把任务做得更细,可以把两个维度组合起来形成四分类:高唤醒高积极(HAHV)、高唤醒低积极(HALV)、低唤醒高积极(LAHV)、低唤醒低积极(LALV)。四分类的信息更丰富,但类别平衡性会变差,因为DEAP里40段视频在四个象限的分布并不均匀。按照DEAP论文提供的数据,四象限的样本比例大约是1.2:1.1:0.9:0.8,还算可以接受,但训练时建议使用加权损失函数或者设置class_weight,避免模型偏向样本多的类别。
在sentimentclassify master中,我把标签处理做成了可选配置,默认是valence二分类,因为大多数做情绪识别的新手对二分类更容易上手,准确率指标也更直观。等项目跑通后再切换四分类做进阶研究。
5. 模型构建与实验评估
5.1 基于SVM的baseline方案
特征准备好之后,第一个要跑的模型是支持向量机(SVM)。为什么选SVM当baseline?因为DEAP的特征维度不算高(约200维),样本量也不算大(每个被试40个视频,整个数据集1280个样本),SVM在这种小样本、中等维度的问题上往往能取得非常好的效果,而且训练速度快、参数少、可解释性强,用来验证特征有效性是最合适的。
以下是实际项目中的SVM训练代码:
from sklearn.svm import SVC from sklearn.model_selection import cross_val_score from sklearn.preprocessing import StandardScaler from sklearn.pipeline import make_pipeline def train_svm_baseline(features, labels): """训练SVM分类器并评估10折交叉验证准确率""" # 注意:SVM对特征尺度敏感,必须先做标准化 model = make_pipeline(StandardScaler(), SVC(kernel='rbf', C=8.0, gamma='scale', class_weight='balanced')) scores = cross_val_score(model, features, labels, cv=10, scoring='accuracy', n_jobs=-1) print(f'SVM 10折CV准确率: {scores.mean():.4f} ± {scores.std():.4f}') return model, scores参数设置的逻辑:kernel用RBF是因为DE特征在原始空间里通常不是线性可分的,RBF可以把特征映射到高维空间去构造决策边界;C设8.0是我在DEAP上多次实验的经验值,C太小会欠拟合,太大容易过拟合,8附近是个比较好的平衡点,如果你在自己实验中发现准确率明显偏低,可以按[1, 2, 4, 8, 16, 32]的对数网格搜一遍;gamma设scale意味着gamma = 1/(特征维度 * 特征方差),对标准化后的特征来说这是个很自然的默认值。
5.2 基于LSTM的深度模型方案
SVM的baseline跑完后,如果你想进一步提升效果,可以考虑LSTM模型。DEAP的每个视频按时间窗切成了一组序列数据,天然适合做序列建模。LSTM能捕捉情绪随时间演化的动态模式,比如被试在观看视频的前几秒可能还处于中性状态,后面才逐渐进入情绪状态,这种时间动态是传统特征平均方法无法建模的。
LSTM模型的输入形状是(样本数, 时间步数, 特征维度)。用4秒窗、2秒步长,每个视频有29个时间步,每个时间步的特征维度约200维。模型结构如下:
import torch import torch.nn as nn class EmotionLSTM(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, num_classes=2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.3) self.classifier = nn.Sequential( nn.Linear(hidden_size, 32), nn.ReLU(), nn.Dropout(0.3), nn.Linear(32, num_classes) ) def forward(self, x): _, (h_n, _) = self.lstm(x) # h_n的最后一层包含所有序列信息 out = self.classifier(h_n[-1]) return outLSTM的hidden_size设64、两层堆叠、dropout设0.3,这套配置在DEAP这种小规模数据集上比较稳妥。hidden_size太大(比如256)会在样本不足时出现过拟合,训练准确率可能接近100%但验证集只有60%多,这是典型的过拟合信号。训练时建议的学习率是1e-3,配合Adam优化器,batch_size设32,epoch设50,用早停法(patience=10)在验证集准确率不再提升时提前终止训练。
5.3 模型评估指标与结果解读
分类任务最直观的指标是准确率(Accuracy),但我强烈建议在情绪识别任务中同时关注F1-score和混淆矩阵,原因很简单:DEAP二分类的标签虽然大体均衡,但四分类时类别不平衡会严重影响准确率的可信度。想象一个极端的例子,某类样本占了60%,你全猜这类也能有60%准确率,看起来不错,实际上模型什么都没学到。
以DEAP上valence二分类为例,SVM方案典型的结果是准确率在85%-90%之间(被试内10折交叉验证)。这个数字看着不低,但要认识到这是在"同一个人的数据里切训练测试集"的情况下得到的,模型对这个人特有的信号模式有很强的记忆。一旦切到被试间评估,准确率通常会掉到60%-70%,这才是更具挑战性的泛化问题。
LSTM方案在被试内设置下通常能再提升2-4个百分点,达到88%-92%的水平。但这些差距是不是统计显著,还需要做多次重复实验(至少5次,用不同的随机种子)并报告均值和标准差。别只看一次运行的最好结果,那很可能是运气。
5.4 结果可视化与日志管理
模型训练完成后,把结果可视化是很有必要的。三张图建议一定要画:训练曲线(损失和准确率随epoch的变化)、混淆矩阵、不同模型的ROC曲线对比。
import matplotlib.pyplot as plt from sklearn.metrics import confusion_matrix, roc_curve, auc def plot_confusion_matrix(y_true, y_pred, class_names=['消极', '积极']): cm = confusion_matrix(y_true, y_pred) fig, ax = plt.subplots(figsize=(6, 5)) im = ax.imshow(cm, cmap='Blues') ax.set_xticks(range(len(class_names))) ax.set_yticks(range(len(class_names))) ax.set_xticklabels(class_names) ax.set_yticklabels(class_names) for i in range(len(class_names)): for j in range(len(class_names)): ax.text(j, i, cm[i, j], ha='center', va='center', fontsize=14) ax.set_xlabel('预测标签') ax.set_ylabel('真实标签') plt.colorbar(im) plt.tight_layout() plt.savefig('confusion_matrix.png', dpi=150)模型的权重参数、日志文件、预测结果也要规范地保存下来。sentimentclassify master里我用了一个简单的experiment目录结构:
experiments/ └── exp_20250115_valence_svm/ ├── config.json ├── train_log.csv ├── best_model.pkl ├── confusion_matrix.png └── metrics.json每次实验一个独立目录,配置参数、训练过程、结果全在里面,日后回溯对比非常方便。这一步看似是工程细节,但对做研究的同学来说,能节省大量对比实验的时间。
6. 常见问题与排查技巧实录
6.1 特征提取环节的高频报错与对策
我在跑DEAP项目的过程中,特征提取阶段遇到最多的一类问题都和数组维度有关。最常见的错误是ValueError: operands could not be broadcast together with shapes,出现这个错误几乎可以肯定是数组的维度顺序没搞对。DEAP数据经h5py读取后,如果忘记转置,data的形状就是(8064, 40, 40),你按(40, 40, 8064)去索引数据,拿到的很可能不是预期的通道信号,而是时间维截断后的碎片数据。排查方法很简单:先打印data.shape确认,再做特征提取前用matplotlib画一个通道的波形图,肉眼确认信号形态正常(EEG信号的幅度一般在±100微伏范围内,有明显节律性)再继续往下走。
另一个容易踩的坑是Welch方法输入的nperseg大于信号长度。如果你把时间窗设置为1秒,128Hz采样率下窗长就是128个采样点,此时nperseg必须小于等于128,否则会直接报错。如果确实需要用短窗,就把nperseg调小,但要注意频率分辨率会变差。我的建议是时间窗最短别低于2秒,这样nperseg取128比较稳妥。
6.2 模型训练时的过拟合与欠拟合现象
DEAP数据集规模不大(总共1280个样本),深度模型很容易出现过拟合。我在实验过程中见过最典型的过拟合信号是:训练集准确率在前10个epoch内冲到98%,但验证集准确率停在70%左右的平台期不涨了。这个现象出现时,先别急着加模型复杂度,按照下面的顺序排查:
| 排查方向 | 具体操作 | 预期效果 |
|---|---|---|
| 数据增强 | 对原始信号添加少量高斯噪声、随机通道丢弃 | 增加样本多样性,缓解过拟合 |
| 正则化增强 | 提高dropout到0.5、增加L2权重衰减到1e-4 | 限制模型容量 |
| 简化模型 | LSTM的hidden_size从128减到64、层数从3降到2 | 降低参数数量 |
| 早停 | patience从10减少到5 | 防止后期过拟合 |
还有一个值得注意的点:对EEG信号做数据增强要特别谨慎。比如随机通道丢弃,理论上可以提升鲁棒性,如果丢弃的通道恰好是情绪识别最关键的额叶通道,会引入噪声。DEAP上我实测下来,温和的高斯噪声(std为信号标准差的0.01倍)效果最好,既不破坏信号结构,又能起到正则化作用。
6.3 跨被试泛化差的原因分析
如果你自己切了被试间验证,发现准确率骤降,第一个排查点就是特征的个体差异。不同被试的绝对DE值差异可能非常大,这个差异不是情绪造成的,而是个体生理差异。处理方式有两种:一是做每被试标准化,即在特征层面把每个被试的特征变换到均值为0、方差为1的分布,这可以大幅消除个体差异的影响;二是使用对抗验证的方法,训练一个模型来区分数据来自哪个被试,如果这个模型的准确率远超随机水平(在32被试情况下远高于1/32),说明被试特定信息在特征中占比过高,需要考虑更强的去个体差异手段。
另外,跨被试场景下把数据划分为train/val/test时,要保证三个集合的被试完全不重叠。有人用GroupKFold但忘了在每个fold内部重新做标准化,导致数据泄露,测试指标虚高,这类问题在实验设计的细节里,排查起来很费时间。
6.4 结果复现与对比实验的注意事项
最后说说结果复现的问题。说实话,在情绪识别领域,不同论文之间的指标很难直接对比,因为数据的划分方式、预处理细节、特征提取参数甚至随机种子都可能不同。我在项目README里记录了完整的实验配置,包括每个环节的参数。你在自己的项目里对照实验时,至少要把随机种子、数据划分方式、时间窗参数、模型结构记录下来,这样才能保证几次实验之间可对比。
如果你要调超参数,建议用Nested Cross-Validation(嵌套交叉验证),外层做模型评估、内层做超参数搜索,避免在测试集上反复调参导置的信息泄露。虽然计算量翻倍,但指标的可信度完全不同。
7. 个人经验与扩展方向
7.1 我在完整复现这个项目后的几个体会
这一个项目做下来,最大的体会是:情绪识别模型的瓶颈早已不是模型结构,而是特征和数据处理。同一个SVM,在只滤波不ICA的粗糙数据上准确率可能是70%,做了完整的预处理后能跳到85%。很多新手花大量时间在调模型上,忽略了预处理才是拉差距的地方。强烈建议你先把EDA(探索性数据分析)做扎实——把每个通道的波形、频谱、不同情绪标签下的特征分布都可视化出来看一遍,这比直接上模型有意义得多。
第二点体会是:DE特征在DEAP上确实好用,但它不是万能的。如果你后续换到更复杂的数据集(比如SEED),或者要做真实的实时情绪识别系统,可能需要考虑更先进的特征提取方式,比如基于深度学习端到端学习的特征表示。不过作为baseline和特征验证工具,DE的计算效率和稳定性目前仍是最优选择之一。
7.2 后续可以怎么扩展
如果你不想止步于现状,有几个方向值得投入时间:
第一,多模态融合。DEAP同时提供了EEG和外周生理信号,很多研究只用了EEG,浪费了一半信息。把GSR、ECG、EMG特征和EEG特征拼接起来输入模型,往往能带来3到5个百分点的提升。融合方式建议从简单的特征级拼接开始,再尝试基于注意力机制的多模态融合网络。
第二,迁移学习。跨被试的泛化问题是真实场景中的核心痛点,可以考虑用域适应(Domain Adaptation)方法,比如在源域被试上预训练特征提取器,然后在目标域被试上微调。DEAP的32个被试天然适合做这种"留一被试交叉验证"的域适应实验。
第三,实时情绪识别。把整个流程从离线处理改为在线处理,即通过滑窗实时计算特征并输出情绪状态。这需要把Welch变换改成IIR滤波器的在线版本,把LSTM改成流式推理,延迟控制在几百毫秒以内。这个方向更接近实际产品化。
第四,多标签与回归任务。目前二分类已经能跑通,下一步可以尝试对valance、arousal做回归预测,用MAE或RMSE做指标。回归任务的信息量比分类更丰富,也更符合情绪连续性的心理学理论。
这个项目本身虽然以DEAP为基准,但流程是通用的。你只要把数据加载部分换成你自己的实验数据格式,后面的预处理、特征提取、模型训练、评估模块都能直接复用。这也是我把代码组织成模块化的初衷——为以后换数据、换任务留好余地。
最后再分享一个小技巧:在跑大规模对比实验之前,先在小规模的子集上(比如4个被试的数据)把整个pipeline完整跑通,确认每一步的输出形状和行为都符合预期,再放全量数据跑。这个小习惯能帮你省掉大量因为"跑了一晚上训练最后发现预处理有个小bug"造成的返工时间。
本文还有配套的精品资源,点击获取