简介:本资源是面向人工智能与情感计算方向研究者、高校研究生及算法工程师的情绪识别实践项目,聚焦DEAP数据集的下载、预处理与多类别情绪分类任务。资源提供完整的Java工程实现,含28个核心Java源码文件(涵盖信号读取、特征提取、SVM/随机森林等模型训练模块)、5个已划分的train训练样本文件(book/kitchen/dvd/electronic/all),以及README.md说明文档、LICENSE协议和IDE配置文件,共38个文件,压缩包仅5.79MB,轻量易部署。已有1075人学习下载,适合开展生理信号情绪识别入门实验与课程设计。读者可直接复用数据加载逻辑、特征工程模板及跨被试分类流程,快速构建基于心率、皮电等8维生理信号的情绪识别系统,并参考目录结构中data与src的清晰分层,理解从原始DEAP数据到四类情绪(喜怒哀惧)监督训练的完整技术链路。
1. 项目概述:从DEAP数据集到情绪识别模型
最近在整理一个关于情绪识别的老项目,核心是基于DEAP数据集构建一个情感分类模型。这个项目在心理学、人机交互和计算神经科学领域一直是个经典课题,很多朋友在入门脑电信号分析或者多模态情感计算时,都会拿它来练手。DEAP数据集全称是“Database for Emotion Analysis using Physiological Signals”,它包含了32名参与者在观看音乐视频时的脑电、肌电、皮肤电等多通道生理信号,以及他们自我报告的情感维度评分(效价、唤醒度、支配度等)。我们的目标,就是利用这些生理信号数据,训练一个模型,能够自动识别出观看者当前的情感状态是高唤醒高愉悦,还是低唤醒低愉悦,或者其他组合。
听起来很酷,对吧?但实际操作起来,你会发现从数据下载、预处理、特征提取到模型训练,每一步都有不少坑。网上能找到的代码和教程,要么过于简略,要么环境依赖复杂到让人头疼。我这个项目“sentimentclassify-master”就是想把整个流程串起来,提供一个相对清晰、可复现的解决方案。它不仅仅是一个分类任务,更是一次对生理信号处理全流程的实战演练。无论你是心理学专业的学生想用数据验证理论,还是机器学习工程师想探索一个新的应用领域,这个项目都能给你提供一个扎实的起点。接下来,我就把整个项目的设计思路、关键步骤、踩过的坑以及一些实用的技巧,毫无保留地分享出来。
2. 核心思路与方案选型:为什么这么设计?
拿到DEAP数据集和情绪分类这个任务,首先得想清楚我们要解决的核心问题是什么。DEAP数据提供了原始的生理信号和情感标签,但原始信号是时序的、高维的、且带有大量噪声的。直接把它扔进一个深度学习模型(比如LSTM或CNN)理论上可行,但往往效果不佳且需要巨大的计算资源。因此,一个更经典、更可解释的流程是:信号预处理 -> 特征工程 -> 特征选择 -> 分类器训练。这个流程虽然传统,但对于DEAP这种中等规模的数据集来说,往往能取得稳定且不错的效果,同时也便于我们理解模型到底“学”到了什么生理指标。
2.1 数据标签的定义与处理
DEAP数据集的标签是其精髓所在。每个试次(一个音乐视频片段)结束后,参与者会在三个维度上进行9点自评:
- 效价:从负面情绪(1)到正面情绪(9)。
- 唤醒度:从平静/困倦(1)到兴奋/激动(9)。
- 支配度:从被控制感(1)到控制感(9)。
对于分类任务,我们通常将连续的评分转化为离散的类别。最常用的方法是二分类。例如,我们可以将效价和唤醒度的评分中值(5分)作为阈值,高于5分为“高”,低于或等于5分为“低”。这样,每个试次就可以被标记为四种类别之一:高唤醒高效价、高唤醒低效价、低唤醒高效价、低唤醒低效价。有些研究也会做三分类(高、中、低)或者直接进行回归预测。在我们的项目中,为了简化问题并突出流程,我选择了对“效价”进行二分类(高 vs 低),这是一个非常常见的基准任务。确定了标签,我们才能进行有监督学习。
2.2 技术栈与工具选型
为什么选择Python和这些库?这是基于生态和效率的考量。
- 核心科学计算:
NumPy和Pandas是处理数值数据和表格数据的绝对主力,没有替代品。 - 信号处理:
SciPy提供了丰富的信号处理函数(如滤波、频谱分析)。MNE-Python是一个专门用于脑电/生理信号处理的强大库,它的滤波、重参考、伪迹去除等功能非常专业,强烈推荐。 - 特征提取:除了手动计算时域、频域特征外,
tsfresh库可以自动从时间序列中提取大量特征,虽然计算量稍大,但在探索性阶段非常有用。 - 机器学习:
scikit-learn提供了从数据预处理、特征选择到模型训练、评估的一站式解决方案,API统一,文档完善,是快速原型开发的不二之选。 - 深度学习:
PyTorch或TensorFlow/Keras。对于DEAP数据,一开始不必上特别复杂的深度网络。一个简单的多层感知机或者一维CNN足以作为基线模型。我选择PyTorch主要是因为它动态图机制更灵活,调试方便。 - 可视化:
Matplotlib和Seaborn用于绘制信号波形、频谱图、特征分布和混淆矩阵等,对于分析和展示结果至关重要。
这个技术栈平衡了专业性、易用性和社区支持度,能确保我们从数据加载到模型部署的每一步都有成熟的工具支持。
3. 数据获取与预处理:一切的基础
3.1 DEAP数据集的下载与结构解析
首先,你得把数据弄到手。DEAP数据集官方页面通常提供两种下载方式:一种是完整的预处理后的数据(.dat文件,Python字典格式),另一种是原始数据。对于大多数机器学习应用,下载预处理后的数据(data_preprocessed_python)就足够了,它已经进行了降采样、滤波等基本处理。
下载解压后,你会看到类似这样的文件结构:
data_preprocessed_python/ ├── s01.dat ├── s02.dat ├── ... └── s32.dat每个.dat文件对应一个被试(共32个),可以用pickle.load来加载。加载后,你得到一个字典,通常包含以下键:
data: 生理信号数据,形状为(40, 40, 8064)。解释一下:第一个40代表40个试次(视频片段),第二个40代表40个数据通道(32个EEG脑电通道 + 8个外周生理信号通道,如GSR皮肤电、PPG光电容积脉搏波等),8064是每个通道的数据点数(采样率128Hz,持续63秒,所以128*63=8064)。labels: 情感标签,形状为(40, 4)。40个试次,每个试次有4个评分(效价、唤醒度、支配度、喜爱度)。通常我们只关注前两个。sampling_rate: 采样率,这里是128。
注意:不同版本的数据集结构可能有细微差别,一定要先写个小脚本打印出数据的维度和内容,彻底搞清楚你手里的数据结构,这是后续所有工作的基石。我曾经因为想当然地认为标签顺序是(效价,唤醒度),结果模型死活训不好,最后才发现顺序是(唤醒度,效价),白白浪费了一天时间。
3.2 关键预处理步骤详解
预处理的目标是“清洗”数据,去除噪声,为特征提取做准备。对于生理信号,尤其是EEG,预处理至关重要。
数据切片与基线校正:DEAP数据包含了视频观看前3秒的基线期。一种常见的做法是,用每个试次前3秒数据的均值作为基线,从整个63秒数据中减去这个基线值,以消除个体静态差异。
# 假设 data_trial 是一个试次的EEG数据,形状为 (40, 8064) baseline = data_trial[:, :384].mean(axis=1, keepdims=True) # 前3秒:128Hz * 3 = 384点 data_corrected = data_trial - baseline滤波:EEG信号的有效信息通常集中在0.5Hz到45Hz之间(Delta, Theta, Alpha, Beta, Gamma波段)。我们需要滤除高频噪声(如肌电)和低频漂移(如出汗引起的皮肤电慢变)。
- 高通滤波:例如0.5Hz,去除低频漂移。
- 低通滤波:例如45Hz,去除高频噪声。
- 工频陷波:50Hz或60Hz,去除电源干扰。 使用
SciPy的signal.butter设计滤波器,或者直接用MNE库的filter函数会更方便专业。
import mne # 创建一个虚拟的Info对象,仅用于滤波(MNE需要) info = mne.create_info(ch_names=[f'ch{i}' for i in range(40)], sfreq=128, ch_types='eeg') raw = mne.io.RawArray(data_corrected, info) raw.filter(0.5, 45., fir_design='firwin') # 带通滤波 raw.notch_filter(50.) # 陷波滤波(假设50Hz工频) data_filtered = raw.get_data()坏道与伪迹处理:这是难点。自动识别坏道(信号完全失效的通道)可以通过计算每个通道的方差或振幅是否超出合理范围来判断。对于眼动、眨眼等伪迹,可以采用独立成分分析(ICA)来识别并去除。在MNE中,ICA有完整的流程,但计算量较大。对于入门项目,如果数据质量尚可,可以暂时跳过复杂的ICA,但必须在报告中说明这一局限性。
重参考:EEG信号是相对的,需要选择一个参考点。DEAP数据通常是平均参考(所有电极的平均值作为参考)。加载的数据可能已经是平均参考后的,需要确认。
预处理后的数据,应该是一个干净、可用于后续分析的信号矩阵。每个试次的数据维度是(通道数, 时间点)。
4. 特征工程:把信号变成数字
特征工程是决定模型性能的上限。好的特征应该能有效区分不同情绪状态下的生理反应模式。
4.1 时域特征
直接从信号波形中提取。
- 均值、方差、偏度、峰度:描述信号幅值的统计特性。
- Hjorth参数:活动性、移动性、复杂性,是EEG分析中常用的描述信号“复杂度”的指标。
- 一阶差分统计量:计算信号相邻点差值的均值、方差等,反映信号的变化速度。
4.2 频域特征
情绪与大脑特定频段的能量变化密切相关。我们需要将信号从时域转换到频域。
- 功率谱密度估计:使用Welch方法计算每个通道信号的PSD。
from scipy import signal freqs, psd = signal.welch(eeg_signal, fs=128, nperseg=256) - 频带能量:将PSD积分到经典脑电波段。
- Delta (0.5-4 Hz): 与深度放松、睡眠相关。
- Theta (4-8 Hz): 与冥想、困倦、创造力相关。
- Alpha (8-13 Hz): 与放松、闭眼状态相关,情绪研究中,Alpha不对称性(左右脑Alpha功率差)常与情绪效价关联。
- Beta (13-30 Hz): 与专注、活跃思考相关。
- Gamma (>30 Hz): 与高阶认知、感觉整合相关。 计算每个波段在所有通道上的平均功率、总功率、峰值频率等。
- 频带功率比:例如 Theta/Beta 比率,在某些研究中与焦虑、注意力相关。
4.3 时频域特征
为了捕捉信号频率随时间的变化(比如情绪诱发瞬间的Gamma爆发),可以使用小波变换或短时傅里叶变换来提取时频特征,但这会极大增加特征维度。
4.4 特征拼接与扁平化
对于一个试次,我们为每个通道计算了多种特征(例如,5个频带能量 + 4个时域统计量 = 9个特征/通道)。对于40个通道,就会得到40 * 9 = 360个特征。我们将这些特征拼接成一个一维向量,作为这个试次的最终特征表示。
实操心得:不要一开始就追求最全的特征集。建议先从一组核心特征开始(例如,所有通道的Alpha和Beta波段功率),训练一个基线模型。然后逐步加入其他特征(如时域特征、不对称性特征),观察验证集性能是否提升。这样可以避免“维度灾难”,并理解哪些特征真正有效。我最初一股脑提取了800多个特征,结果模型过拟合严重,后来通过特征选择才挽救了回来。
5. 模型构建、训练与评估
5.1 数据准备与划分
将32名被试的数据全部混合打乱来划分训练集和测试集是错误的!因为同一个被试的数据在不同试次间可能存在相关性(个体生理特质)。正确的做法是按被试划分,例如,使用24个被试的数据做训练,4个做验证,4个做测试。这被称为“跨被试”评估,更能检验模型的泛化能力,但难度也更大。另一种是“被试内”划分,即每个被试的数据单独划分训练测试集,然后平均结果,这更容易取得高准确率,但泛化性存疑。我们的项目采用跨被试划分,更贴近实际应用场景。
from sklearn.model_selection import LeaveOneGroupOut import numpy as np # 假设 features_all 形状 (总试次数, 特征数), labels_all 形状 (总试次数,) # groups 是一个列表,标明每个试次属于哪个被试(1到32) logo = LeaveOneGroupOut() for train_idx, test_idx in logo.split(features_all, labels_all, groups=groups): X_train, X_test = features_all[train_idx], features_all[test_idx] y_train, y_test = labels_all[train_idx], labels_all[test_idx] # 在这个循环里,每次留出一个被试的数据作为测试集5.2 特征标准化与选择
- 标准化:不同特征(如方差和Gamma能量)的量纲和数值范围差异巨大,必须标准化。使用
StandardScaler,切记:用训练集的均值和方差来转换训练集和测试集,避免数据泄露。from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) # 使用训练集的参数 - 特征选择:高维特征中可能存在大量冗余或无关特征。我们可以使用:
- 方差阈值:移除方差极低的特征(几乎为常数)。
- 单变量统计检验:如ANOVA F值,选择与标签相关性最高的K个特征。
- 基于模型的特征重要性:如使用L1正则化的线性模型(逻辑回归的L1惩罚项),或者树模型(如随机森林)的特征重要性排序。
from sklearn.feature_selection import SelectKBest, f_classif selector = SelectKBest(score_func=f_classif, k=100) # 选择最好的100个特征 X_train_selected = selector.fit_transform(X_train_scaled, y_train) X_test_selected = selector.transform(X_test_scaled)
5.3 分类器选择与训练
对于特征工程后的表格数据,经典的机器学习分类器往往表现不俗。
- 支持向量机:在高维小样本数据上传统强项。可以尝试线性核和RBF核。
- 随机森林:能提供特征重要性,对异常值不敏感,不容易过拟合。
- 梯度提升树:如XGBoost或LightGBM,通常能取得当前最好的性能。
- 多层感知机:简单的深度学习模型,可以作为基线。
以随机森林为例:
from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, classification_report, confusion_matrix clf = RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42, class_weight='balanced') clf.fit(X_train_selected, y_train) y_pred = clf.predict(X_test_selected) acc = accuracy_score(y_test, y_pred) print(f“测试准确率: {acc:.3f}”) print(classification_report(y_test, y_pred))5.4 深度学习模型尝试
如果你想尝试端到端的深度学习,可以构建一个简单的1D-CNN模型,直接输入预处理后的原始信号片段(例如,每个通道的3秒片段)。
import torch.nn as nn class SimpleEEGCNN(nn.Module): def __init__(self, num_channels=40, num_classes=2): super().__init__() self.conv1 = nn.Conv1d(num_channels, 64, kernel_size=3, padding=1) self.pool = nn.MaxPool1d(2) self.conv2 = nn.Conv1d(64, 128, kernel_size=3, padding=1) self.fc1 = nn.Linear(128 * (time_points//4), 64) # time_points需根据池化后计算 self.fc2 = nn.Linear(64, num_classes) self.relu = nn.ReLU() self.dropout = nn.Dropout(0.5) def forward(self, x): # x shape: (batch, channels, time) x = self.pool(self.relu(self.conv1(x))) x = self.pool(self.relu(self.conv2(x))) x = x.view(x.size(0), -1) x = self.relu(self.fc1(x)) x = self.dropout(x) x = self.fc2(x) return x注意:直接使用原始信号需要更精细的预处理和更大的数据量,DEAP的跨被试设置下,深度学习模型很容易过拟合,需要大量的正则化(Dropout, BatchNorm)和数据增强(如加噪、时域缩放)。
6. 结果分析与常见问题排查
6.1 性能评估指标
对于二分类问题,不能只看准确率。特别是当数据类别不均衡时(DEAP数据中高/低效价的样本数可能不均)。
- 精确率、召回率、F1分数:通过
classification_report可以一目了然。 - 混淆矩阵:可视化模型在各类别上的错误情况。
- ROC曲线与AUC:评估模型在不同分类阈值下的整体性能,对类别不平衡不敏感。
6.2 典型问题与解决方案
在复现这个项目时,你几乎一定会遇到下面几个问题:
准确率始终在50%左右(随机猜测水平)
- 可能原因1:数据/标签没对应上。这是最常见的原因!请反复检查数据加载和标签提取的代码,确保每个试次的特征向量和标签是正确的配对。打印几个样本出来人工核对。
- 可能原因2:特征无效。你提取的特征可能无法区分情绪。尝试可视化不同情绪类别下,某个关键特征(如前额Alpha不对称性)的分布图,看是否有显著差异。如果没有,可能需要重新审视特征设计或预处理步骤。
- 可能原因3:数据泄露。在标准化或特征选择时,错误地使用了测试集的信息来拟合参数。确保所有预处理步骤都只在训练集上拟合,然后应用到测试集。
模型在训练集上表现很好,在测试集上很差(过拟合)
- 解决方案:
- 增加正则化:对于SVM或逻辑回归,增大C参数的倒数;对于随机森林,限制树的最大深度(
max_depth)、增加min_samples_split;对于神经网络,增加Dropout层、使用L2权重衰减。 - 减少特征数量:使用更严格的特征选择,降低特征维度。
- 增加数据:对于跨被试任务,数据量是固定的。可以考虑使用“被试内”划分先验证特征和模型的有效性,或者使用迁移学习思路。
- 增加正则化:对于SVM或逻辑回归,增大C参数的倒数;对于随机森林,限制树的最大深度(
- 解决方案:
不同被试间性能差异巨大
- 原因:这是生理信号分析的固有挑战——个体差异巨大。一个人的高唤醒EEG模式可能与另一个人的低唤醒模式相似。
- 解决方案:
- 被试归一化:对每个被试的数据分别进行标准化(Z-score),然后再合并。这可以消除个体间的基线差异。
- 被试自适应/迁移学习:训练一个通用模型,然后用目标被试的少量数据对模型进行微调。
- 使用对个体差异不敏感的特征:例如,寻找相对变化量(如相对于基线期的能量变化百分比),而不是绝对能量值。
计算特征或训练模型太慢
- 特征提取:使用
tsfresh时可以并行化提取。对于自定义特征,考虑使用NumPy的向量化操作,避免Python层级的for循环。 - 模型训练:
scikit-learn的许多算法支持n_jobs参数进行多核并行。对于深度学习,确保使用了GPU。
- 特征提取:使用
6.3 项目总结与扩展方向
这个“sentimentclassify-master”项目走完了一个完整的基于DEAP数据集的情绪识别流程。它更像一个坚实的脚手架,而不是一个天花板很高的成品。通过它,你掌握了生理信号处理的基本套路:数据理解、预处理、特征工程、机器学习建模。
如果你想在此基础上深入,这里有几个明确的扩展方向:
- 多模态融合:DEAP除了EEG,还有GSR、PPG、呼吸等外周生理信号。尝试融合这些多模态特征,看看是否能提升分类性能。早期融合(特征层拼接)和晚期融合(模型层投票/加权)都可以尝试。
- 维度回归:将分类任务改为回归任务,直接预测效价和唤醒度的连续值(1-9分)。这需要使用如SVR、随机森林回归等模型,并用均方误差等指标评估。
- 更先进的深度学习模型:尝试更复杂的网络结构,如EEGNet(专门为EEG设计的CNN)、LSTM或Transformer来捕捉时序依赖。但务必注意过拟合问题,并考虑使用数据增强技术。
- 实时情绪识别演示:将训练好的模型封装成一个简单的实时系统,输入一段模拟的EEG信号(或从其他设备读取),实时输出情绪状态预测。这能让你对整个应用闭环有更直观的感受。
最后,情绪识别是一个复杂且充满挑战的领域,生理信号与主观情感体验之间的关系并非一一对应。DEAP数据集是一个宝贵的资源,但模型在实验室环境下的表现,离真正的鲁棒应用还有很长距离。这个项目的价值,在于提供了一个完整的、可操作的实践框架,让你亲身体验从数据到智能决策的每一个环节,并理解其中的挑战与乐趣。希望你在复现和改造这个项目的过程中,能有自己的发现和收获。
本文还有配套的精品资源,点击获取