1. 从零开始认识DEAP数据集与情绪分析如果你对脑机接口、情感计算或者心理健康监测感兴趣那你大概率听说过DEAP数据集。我第一次接触它是在一个情绪识别的项目里当时为了找一个靠谱的、包含真实生理信号的数据集可没少花功夫。市面上很多所谓的“情绪数据集”其实都是基于面部表情或者语音的这些信号很容易被“演”出来比如你明明不开心但为了拍照可以挤出笑容。但生理信号不一样你的心跳、皮肤电、大脑电波这些是自主神经系统控制的很难伪装。DEAPDatabase for Emotion Analysis using Physiological Signals就是为了解决这个问题而生的它记录的是人在观看音乐视频时最真实的生理反应。简单来说DEAP是一个多模态的、公开的情绪分析数据库。它由英国的一个研究团队创建核心数据来自32位身心健康的参与者男女各半。实验过程挺有意思的让每个人看40段一分钟长的音乐视频片段同时用专业设备采集他们的脑电EEG和多种外周生理信号比如皮肤电反应、血容量脉搏、呼吸、肌电等等。看完之后参与者要立刻给这段视频引发的情绪打分打分维度包括效价Valence简单理解就是愉悦/不愉悦、唤醒度Arousal平静/兴奋、优势度Dominance感觉受控/感觉在掌控中和喜爱度Liking。这样一来每一段生理信号数据都对应着一组真实的主观情绪标签这正是我们做机器学习模型训练所需要的“标准答案”。为什么说它适合新手和研究者快速上手呢首先它完全公开经过申请就能获得不像有些医疗数据有极高的使用门槛。其次它已经经过了基础的预处理官网提供了两个版本的预处理数据分别用Matlab和Python处理过文件格式是.mat和.dat拿过来几乎可以直接用省去了最让人头疼的原始信号去噪、降采样等繁琐步骤。最后它的结构非常清晰规范32个参与者每人40次试验每次试验的数据和标签都规整地放在数组里大大降低了数据处理的复杂度。我刚开始用的时候最怕数据乱七八糟要自己花大量时间清洗DEAP在这方面做得非常友好。2. 实战第一步获取数据与理解文件结构拿到数据是第一步也是最关键的一步。很多朋友卡在申请环节其实流程并不复杂。你需要去DEAP的官方网站找到最终用户许可协议EULA下载打印后签名扫描成电子版。然后用你的机构邮箱比如学校的.edu或.stu邮箱发送到指定的联系邮箱。根据我的经验通常1到3个工作日就能收到回复和下载授权。用个人邮箱发送可能会被忽略所以一定要用机构邮箱这也能证明你的学术或研究用途。数据下载下来后你会看到一个包含多个文件夹的目录。别慌我们一步步拆解。对于绝大多数入门和进阶研究我们主要关注data_preprocessed_python或data_preprocessed_matlab这个文件夹。这里面有32个.dat文件Python版对应32位参与者。每个文件都包含两个核心部分data和labels。我当初第一次加载数据时被这个多维数组的维度搞晕过这里详细解释一下帮你避坑。以Python预处理版本为例我们用pickle加载一个文件看看import pickle import numpy as np # 加载第一位参与者的数据 with open(data_preprocessed_python/s01.dat, rb) as f: data pickle.load(f, encodinglatin1) # 注意编码 # 查看数据结构 print(type(data)) # 通常是一个字典 print(data.keys()) # 输出dict_keys([data, labels, ...]) # 获取数据和标签 eeg_data data[data] # 生理信号数据 labels data[labels] # 情绪标签现在来看维度data数组形状是(40, 40, 8064)。这三维分别代表第一维4040次试验Trials即参与者观看了40段视频。第二维4040个数据通道Channels。这里要注意前32个通道是脑电EEG信号对应国际10-20系统的32个电极位置。后面的8个通道是外周生理信号包括皮肤电、血容量脉搏等。很多新手会误以为40个通道全是脑电其实不是。第三维8064每个通道的数据点数量。这是由63秒的录制时长乘以128Hz的采样频率得到的63 * 128 8064。原始数据是512Hz这里已经为我们降采样到了128Hz并且去除了眼电等伪迹非常贴心。labels数组形状是(40, 4)。这二维分别代表第一维40同样对应40次试验。第二维4对应4个情绪维度顺序依次是效价Valence、唤醒度Arousal、优势度Dominance、喜爱度Liking。每个值都是1到9之间的连续分数。理解这个结构至关重要因为后续所有的特征提取和模型输入都基于此。比如如果你只想用脑电数据那么切片就是eeg_data[:, :32, :]。如果你想研究效价和唤醒度的二分类问题那么标签就需要从labels中提取前两列。3. 数据预处理为模型准备好“食材”直接从DEAP下载的预处理数据好比是已经洗好、切好的“净菜”但我们下锅前可能还需要根据菜谱进行二次加工。这一步的目标是把原始的生理信号时间序列转换成更适合机器学习模型“消化”的格式。我通常会做以下几个操作数据分段、基线校正、滤波和标准化。数据分段与基线校正虽然数据已经是63秒一段但通常我们分析时会剔除最开始几秒的适应期。更常见的是进行基线校正。在DEAP实验中每段视频开始前有3秒的基线记录屏幕上显示十字准星。我们可以用这3秒的数据对应采样点3秒 * 128Hz 384个点作为基线将后续60秒的试验数据减去这段基线的平均值以消除个体静息状态的差异。操作起来并不难def baseline_correction(trial_data, baseline_end384): 对单个试验的数据进行基线校正。 trial_data: 形状为 (40, 8064) 或 (32, 8064) 的单个试验数据 baseline_end: 基线结束的采样点索引3秒处 baseline_mean trial_data[:, :baseline_end].mean(axis1, keepdimsTrue) corrected_data trial_data - baseline_mean return corrected_data # 假设我们取第一个试验的脑电数据32个通道 single_trial_eeg eeg_data[0, :32, :] # 形状 (32, 8064) corrected_trial baseline_correction(single_trial_eeg)滤波尽管数据已经预处理过但根据你的研究频段兴趣可能还需要进一步的滤波。例如脑电分析常关注几个经典频带Delta (1-4 Hz), Theta (4-8 Hz), Alpha (8-13 Hz), Beta (13-30 Hz), Gamma (30-45 Hz)。我们可以用scipy或mne库进行带通滤波。这里以提取Alpha波为例from scipy import signal def bandpass_filter(data, lowcut, highcut, fs128, order4): 对数据进行带通滤波。 data: 一维时间序列数据 fs: 采样频率 (128 Hz) nyquist 0.5 * fs low lowcut / nyquist high highcut / nyquist b, a signal.butter(order, [low, high], btypeband) filtered_data signal.filtfilt(b, a, data) # 使用filtfilt实现零相位延迟 return filtered_data # 对单个通道的数据进行Alpha波段滤波 single_channel_data corrected_trial[0, :] # 取第一个通道 alpha_data bandpass_filter(single_channel_data, lowcut8, highcut13, fs128)标准化/归一化生理信号存在显著的个体差异比如有些人的皮肤电反应天生就比较活跃。为了不让模型被这些个体差异带偏我们需要对数据进行标准化。通常有两种做法一是对每个参与者的所有试验数据进行标准化去除个体差异二是对所有参与者的所有数据一起标准化更关注全局模式。我一般使用每个通道内的Z-score标准化这对后续的深度学习模型尤其友好。def normalize_data(data): 对数据进行Z-score标准化假设输入data形状为 (n_trials, n_channels, n_samples) 我们对每个通道的所有试验和所有时间点进行标准化。 mean data.mean(axis(0, 2), keepdimsTrue) # 按通道求均值 std data.std(axis(0, 2), keepdimsTrue) # 按通道求标准差 normalized_data (data - mean) / (std 1e-8) # 加一个小数防止除零 return normalized_data # 对整个数据集进行标准化 normalized_eeg_data normalize_data(eeg_data[:, :32, :]) # 只对脑电部分做完成这些步骤后你的数据就已经从“原始信号”变成了“干净的特征信号”可以准备下锅烹饪输入模型了。4. 特征工程从信号中提取情绪的“密码”特征工程是情绪识别项目的灵魂。直接把这8064个时间点扔进模型不仅计算量大而且模型很难学到有效模式容易过拟合。我们需要从这些波动起伏的曲线中提炼出能代表情绪状态的关键数学特征。根据我的经验可以从时域、频域和时频域三个角度来提取。时域特征直接从信号幅度随时间的变化中提取。这类特征计算简单物理意义直观。均值、方差、标准差反映信号的总体水平和波动范围。情绪激动时某些生理信号如皮肤电的均值可能会升高。峰度、偏度描述信号分布的形状。偏度反映对称性峰度反映尖锐程度可能与情绪的突发变化有关。Hjorth参数包括活动性Activity、移动性Mobility、复杂性Complexity是脑电分析中常用的描述信号“活跃度”和“不规则度”的特征。频域特征将信号从时间域转换到频率域看能量在不同频率上的分布。情绪状态与脑电的特定频带能量高度相关。波段功率计算Delta, Theta, Alpha, Beta, Gamma等经典脑电波段的绝对功率或相对功率该波段功率占总功率的比例。例如放松状态通常伴随Alpha波增强。功率谱密度计算信号的功率谱密度可以提取谱重心、谱熵等特征描述频谱的集中度和混乱度。时频域特征兼顾时间和频率信息适合分析非平稳信号生理信号大多是非平稳的。常用方法是小波变换。小波系数能量对信号进行小波分解得到不同尺度对应不同频率下的系数计算每个尺度系数的能量作为特征。这能捕捉到情绪变化过程中不同频段能量随时间动态演变的过程。下面是一个结合时域和频域特征提取的示例代码块我们可以为每个通道的每段数据计算一组特征import numpy as np from scipy import stats, signal def extract_features(single_trial_data, fs128): 为单个试验的单个通道数据提取特征。 single_trial_data: 一维数组形状为 (n_samples,) 返回一个特征向量。 features [] n_samples len(single_trial_data) # 1. 时域特征 features.append(np.mean(single_trial_data)) features.append(np.var(single_trial_data)) features.append(stats.skew(single_trial_data)) features.append(stats.kurtosis(single_trial_data)) # Hjorth参数 diff1 np.diff(single_trial_data) diff2 np.diff(diff1) activity np.var(single_trial_data) mobility np.sqrt(np.var(diff1) / activity) complexity np.sqrt(np.var(diff2) / np.var(diff1)) / mobility features.extend([activity, mobility, complexity]) # 2. 频域特征 (使用Welch方法估计功率谱) freqs, psd signal.welch(single_trial_data, fsfs, nperseg256) # 定义频带边界 band_defs {delta: (1, 4), theta: (4, 8), alpha: (8, 13), beta: (13, 30), gamma: (30, 45)} total_power np.sum(psd) for band_name, (low, high) in band_defs.items(): idx np.logical_and(freqs low, freqs high) band_power np.sum(psd[idx]) # 计算相对功率 rel_power band_power / total_power if total_power 0 else 0 features.append(rel_power) return np.array(features) # 示例提取第一个试验、第一个通道的特征 feat_vector extract_features(normalized_eeg_data[0, 0, :]) print(f提取的特征数量{len(feat_vector)})实际操作中我们需要对所有试验的所有通道循环调用这个函数最终得到一个形状为(n_trials, n_channels * n_features_per_channel)的大特征矩阵。这个矩阵就是我们模型训练的输入X。别忘了对应的标签y需要从labels数组中提取并进行二值化例如将效价5的视为“积极”5的视为“消极”。5. 构建与训练你的第一个情绪分类模型特征准备好之后就到了最激动人心的建模环节。我们可以把问题简化成一个二分类任务比如根据效价Valence和唤醒度Arousal的高低将情绪划分到四个象限高唤醒高效价、高唤醒低效价、低唤醒高效价、低唤醒低效价。这里我们以效价二分类高 vs 低为例带你走通一个完整的机器学习流程。首先我们需要准备数据集。将特征矩阵和标签提取出来并划分训练集和测试集。非常重要的一点为了避免“数据泄露”我们必须按参与者来划分而不是随机打乱所有试验。因为同一个参与者的不同试验数据之间可能存在相关性如果随机划分同一个人的数据既出现在训练集又出现在测试集模型会“作弊”导致测试结果虚高。正确的做法是“留出法”比如用前28位参与者的数据训练后4位参与者的数据测试。from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler, LabelBinarizer from sklearn.metrics import accuracy_score, confusion_matrix, classification_report import numpy as np # 假设我们已经从所有数据中提取了特征矩阵X和标签y # X形状: (总试验数, 特征数) # y形状: (总试验数, ) 已经是二值化标签0表示低效价1表示高效价 # 按参与者划分数据集。假设每个参与者40个试验总试验数 32 * 40 1280 n_participants 32 trials_per_participant 40 # 创建一个数组来标识每个试验属于哪个参与者 participant_ids np.repeat(range(n_participants), trials_per_participant) # 使用前28个参与者作为训练集后4个作为测试集 train_idx np.where(participant_ids 28)[0] test_idx np.where(participant_ids 28)[0] X_train, X_test X[train_idx], X[test_idx] y_train, y_test y[train_idx], y[test_idx] print(f训练集大小{X_train.shape} 测试集大小{X_test.shape})接下来我们选择一个经典的分类器比如支持向量机SVM。SVM在小样本、高维特征的数据上往往表现不错。记得要对特征进行标准化因为SVM对特征的尺度很敏感。from sklearn.svm import SVC from sklearn.pipeline import make_pipeline # 创建管道先标准化再训练SVM svm_pipeline make_pipeline(StandardScaler(), SVC(kernelrbf, C1.0, gammascale, random_state42)) # 训练模型 svm_pipeline.fit(X_train, y_train) # 在测试集上预测 y_pred svm_pipeline.predict(X_test) # 评估模型 accuracy accuracy_score(y_test, y_pred) print(f测试集准确率{accuracy:.4f}) print(\n分类报告) print(classification_report(y_test, y_pred)) print(\n混淆矩阵) print(confusion_matrix(y_test, y_pred))如果准确率在55%-65%之间别灰心这在基于生理信号的情绪识别领域是相当不错的结果因为情绪本身是高度主观和复杂的生理信号与情绪的映射关系也不是一一对应。比随机猜测50%高出的部分就说明我们的模型确实学到了一些规律。为了提升性能我们可以尝试以下方法特征选择使用递归特征消除RFE或基于模型的特征重要性如树模型来筛选出最相关的特征去除噪声。尝试其他模型比如随机森林、梯度提升树如XGBoost、或者简单的神经网络MLP。随机森林通常能提供一个不错的基线且能给出特征重要性。深度学习端到端方法跳过手工特征工程直接使用卷积神经网络CNN或长短时记忆网络LSTM处理原始信号或简单的时频图。这需要更多的数据和计算资源但可能是性能的上限。# 示例使用随机森林 from sklearn.ensemble import RandomForestClassifier rf_pipeline make_pipeline(StandardScaler(), RandomForestClassifier(n_estimators100, max_depth10, random_state42)) rf_pipeline.fit(X_train, y_train) y_pred_rf rf_pipeline.predict(X_test) print(f随机森林测试集准确率{accuracy_score(y_test, y_pred_rf):.4f}) # 查看特征重要性如果用了特征工程 if hasattr(rf_pipeline.named_steps[randomforestclassifier], feature_importances_): importances rf_pipeline.named_steps[randomforestclassifier].feature_importances_ # 可以排序并可视化最重要的特征6. 进阶探索从二分类到更精细的情绪建模当你跑通了一个基础的情绪二分类模型后可能会觉得意犹未尽。DEAP数据集还能支持更多有趣的探索方向让你的项目深度和广度都上一个台阶。多分类与维度回归我们之前把连续的效价分数1-9二值化了这损失了很多信息。情绪本身是一个连续的空间。我们可以尝试四分类结合效价和唤醒度划分成四个象限高兴、愤怒、悲伤、放松。回归任务直接预测效价或唤醒度的具体分数1-9。这比分类更难但更贴近情绪的真实连续特性。可以尝试使用支持向量回归SVR或神经网络回归模型。多模态融合DEAP的强大之处在于它是多模态的。我们之前只用了40个通道中的前32个EEG。别忘了还有8个外周生理信号通道GSR皮肤电、BVP血容量脉搏等。这些信号对唤醒度尤其敏感。你可以尝试特征层融合将EEG特征和外周生理信号特征拼接成一个更大的特征向量然后输入分类器。决策层融合分别用EEG数据和外周生理数据训练两个模型然后将它们的预测概率进行加权平均或投票得到最终结果。这种方法往往能提升模型的鲁棒性。基于深度学习的端到端方法这是目前研究的热点。手工设计特征需要很强的领域知识而深度学习可以自动从原始数据中学习表征。对于DEAP这样的时序数据常用的网络结构有1D-CNN直接在时间序列上做卷积捕捉局部模式。LSTM/GRU捕捉信号在时间上的长程依赖关系。CNN-LSTM混合模型先用CNN提取局部特征再用LSTM捕捉时序动态效果通常更好。下面是一个使用PyTorch构建简单CNN-LSTM混合模型的示例框架import torch import torch.nn as nn import torch.optim as optim class EEGEmotionNet(nn.Module): def __init__(self, n_channels32, n_samples8064, num_classes2): super(EEGEmotionNet, self).__init__() # 1D CNN部分提取空间通道间和局部时间特征 self.conv1 nn.Conv1d(in_channelsn_channels, out_channels64, kernel_size3, padding1) self.bn1 nn.BatchNorm1d(64) self.pool1 nn.MaxPool1d(kernel_size2) self.conv2 nn.Conv1d(in_channels64, out_channels128, kernel_size3, padding1) self.bn2 nn.BatchNorm1d(128) self.pool2 nn.MaxPool1d(kernel_size2) # 计算经过两次池化后的时间维度长度 self._to_linear None self._get_conv_output((n_channels, n_samples)) # LSTM部分捕捉长时序依赖 self.lstm nn.LSTM(input_size128, hidden_size64, num_layers1, batch_firstTrue, bidirectionalTrue) # 全连接分类层 self.fc nn.Sequential( nn.Dropout(0.5), nn.Linear(64 * 2, 32), # 双向LSTMhidden_size*2 nn.ReLU(), nn.Linear(32, num_classes) ) def _get_conv_output(self, shape): 辅助函数计算卷积层输出后的维度 batch_size 1 input torch.autograd.Variable(torch.rand(batch_size, *shape)) output self.pool2(self.bn2(self.conv2(self.pool1(self.bn1(self.conv1(input)))))) self._to_linear output.shape[1] * output.shape[2] def forward(self, x): # x形状: (batch, channels, samples) # CNN x torch.relu(self.bn1(self.conv1(x))) x self.pool1(x) x torch.relu(self.bn2(self.conv2(x))) x self.pool2(x) # 形状: (batch, 128, time_steps) # 为LSTM准备将通道维度转为特征维度时间维度保留 x x.permute(0, 2, 1) # 形状: (batch, time_steps, 128) # LSTM lstm_out, (h_n, c_n) self.lstm(x) # 取最后一个时间步的输出或者使用隐藏状态 x lstm_out[:, -1, :] # 取最后一个时间步的输出 # 分类 x self.fc(x) return x # 模型使用示例需要先将数据转为PyTorch Tensor # model EEGEmotionNet(n_channels32, n_samples8064, num_classes2) # criterion nn.CrossEntropyLoss() # optimizer optim.Adam(model.parameters(), lr0.001)这条路走起来会更复杂需要调试超参数、防止过拟合但一旦跑通你对数据和模型的理解会上一个全新的层次。7. 避坑指南与项目心得在DEAP数据集上做项目我踩过不少坑也积累了一些经验希望能帮你少走弯路。第一个大坑是数据划分。前面强调过一定要按参与者划分训练集和测试集千万不能随机打乱试验。我早期项目就犯过这个错误在测试集上得到了接近80%的“虚高”准确率高兴了半天结果发现是数据泄露模型只是记住了特定参与者的生理模式而不是真正的情绪模式。重新按参与者划分后准确率立刻掉到了60%左右这才是真实水平。第二个是标签处理。DEAP的标签是1-9的连续值。直接做回归很难做分类则需要确定阈值。常见做法是以5为分界点中位数但这样会引入很多边界模糊的样本比如4.5和5.5情绪差别不大却被分到两类。有些研究会剔除4.8到5.2之间的“中性”或“模糊”样本只保留高置信度的样本进行二分类这样模型学到的边界会更清晰。你需要根据自己项目的目标来决定如何处理。第三个是特征爆炸和过拟合。如果你为每个通道提取了20个特征32个通道就是640维特征而你的样本数可能只有1280个32人*40次试验。这属于典型的高维小样本问题极易过拟合。务必使用特征选择如SelectKBest, RFE或降维如PCA来压缩特征维度同时配合交叉验证来评估模型泛化能力。正则化如SVM的C参数树模型的max_depth也要调好。第四个是计算资源。如果你想尝试深度学习尤其是处理原始的8064长度时间序列对GPU内存要求不低。可以考虑先对数据进行下采样比如从128Hz降到64Hz或者使用滑动窗口将长序列切分成多个短序列样本这样既能增加样本量又能降低单次输入的长度。最后也是最重要的一点理解你的数据和模型在做什么。不要只盯着准确率这个数字。多看看混淆矩阵你的模型是更容易把悲伤误判为愤怒还是容易把高兴误判为平静这背后可能反映了效价和唤醒度哪个维度更难识别。可视化一下重要特征看看是前额叶的Alpha波功率更重要还是皮肤电反应的方差更重要。这些分析不仅能帮你改进模型更能让你真正理解情绪与生理信号之间复杂而有趣的联系。情绪识别不是一个简单的工程问题它处在神经科学、心理学和计算机科学的交叉点每一个百分点的提升都可能意味着我们对“情感”这座冰山又多了解了一角。