☰
DEAP情绪识别复现指南:从数据集下载到SVM/CNN分类全流程
2026/10/11 19:24:50 网站建设 项目流程

简介:面向情感计算与脑机接口方向的研究者和入门学习者,聚焦基于DEAP数据集的情绪识别任务。数据集包含二十八名受试者观看四十段情感视频时采集的多通道脑电信号,以及心率、皮肤电导等外周生理信号,每段视频还配有八个维度的主观评分标签,可作为训练和验证情绪分类模型的标准依据。包内整合了完整的Python源码,覆盖数据读取、特征构建、支持向量机分类器训练与评估等关键环节,并附有标签数据、特征表格和说明文档,能够帮助读者快速跑通从原始生理信号到情绪标签的完整流程。资源共十一个文件,以脚本文件为核心,辅以数据文件、特征文件及授权许可和说明文件,压缩包整体约三点八五兆字节,轻量便携。目前已有三千零二十四人学习,适合希望基于真实脑电数据开展情绪分类实验的学生、研究人员和工程师,也可作为情感计算入门项目的参考模板。

1. DEAP情绪识别:为什么说它是入门生理信号情绪分类最值得复现的开源方案

DEAP 情绪识别是很多人在做生理信号情绪分类时接触的第一个开源方案:32 名被试观看 40 段音乐视频时的 EEG、EMG、GSR 等生理信号,连同 valence(效价)、arousal(唤醒度)、dominance(支配度)、liking 四个主观评分一起被打包公开,数据规模正好卡在「单机可处理」和「足够训练深度学习模型」之间,配合 DEAP 数据集下载页提供的源码,几乎每个做情绪识别的人都会拿它当第一块试验田。如果你正烦恼没有合规情绪数据、不清楚 EEG 这类高维时序从哪里下手、或者模型效果一直很玄学,这篇文章就把 deap数据集下载 到源码复现的路踩一遍给你看。下面按数据集读取、预处理、模型训练、避坑排查的顺序展开,每部分都给可以直接抄的代码和参数。

2. DEAP 数据集解剖:40 通道 × 32 被试的生理信号到底怎么读

2.1 先认清 data 和 labels 的四维矩阵

DEAP 的预处理数据是每个被试一个文件,按 s01.mat、s02.mat 这样的顺序排下去。打开任一文件,你会看到两个主要矩阵:data 和 labels。data 的形状是 40×40×8064,对应「40 个试次 × 40 个信号通道 × 8064 个采样点」。8064 这个数字来自 63 秒乘以 128 Hz,其中前 3 秒是基线状态——被试盯着屏幕上的十字什么都不做,属于静息脑电;后 60 秒才是观看音乐视频时的诱发信号。labels 矩阵形状是 40×4,每一行对应一个试次的主观评分,四列依次是 valence、arousal、dominance、liking。

为什么要先把结构讲清楚?因为后面所有复现代码的索引都建立在这套维度上。你要么写 data[trial, channel, sample],要么写 data[trial, :, :],一旦把通道维度放错位置,后面做基线校正、特征提取都会连锁出错。这也是 deap数据集下载 之后新人第一个翻车的点:文件读进来了,但不知道每个数字代表什么。

具体到通道,40 个通道并非全部来自大脑。前 32 个通道是 EEG,按国际 10-20 系统的标准位置排布;后面 8 个是外围生理信号,包括左右眼电 EOG、肌电 EMG、皮肤电 GSR、呼吸、体温等。如果只做脑电情绪分类,应该取 data[:, :32, :];如果做多模态融合,才把 40 个通道都保留。不少论文源码默认用全部 40 通道,理由是把外围信号也当作情绪相关特征,但要注意这 8 个通道的量纲与 EEG 完全不同,标准化之前千万不要混在一起算。

评分方面,DEAP 用的是 1 到 9 的整数等距量表,被试每看完一个视频就依次给四个维度打分。复现时最常见的是把 valence 二分类:大于 5 记为正性情绪,小于等于 5 记为负性情绪;或把 arousal 按中位数切成高低唤醒。为什么预处理数据选 128 Hz 而不是原始 512 Hz?因为情绪识别关注的频带集中在 theta/alpha/beta,最高到 45 Hz 左右,按奈奎斯特定理 128 Hz 采样率已经足够覆盖两倍信号频率。从 512 降到 128 既降低了文件体积,也顺便完成了抗混叠滤波。如果你用的是原始 .bdf 数据,就必须自己做降采样,这也是预处理数据受欢迎的原因之一。

表:DEAP 预处理数据集关键参数

参数数值说明
被试数量3216 男 16 女
音乐视频刺激数40每段约 60 s
EEG 通道数32按 10-20 系统放置
外围信号通道数8EOG、EMG、GSR、呼吸、体温
预处理采样率128 Hz原始数据为 512 Hz
单试次采样点数806463 s × 128 Hz
评分维度4valence / arousal / dominance / liking

2.2 用 scipy.io 读取 .mat 文件:最小可跑代码与参数说明

DEAP 官网下载的预处理数据是 MATLAB 格式,Python 侧直接用 scipy.io.loadmat 就能读,不需要额外装其它库。下面这段代码把单个被试数据完整读出来,并打印形状和评分范围:

import scipy.io import numpy as np mat = scipy.io.loadmat('data_preprocessed_matlab/s01.mat') data = mat['data'] # shape: (40, 40, 8064) labels = mat['labels'] # shape: (40, 4) print(f'data shape: {data.shape}') print(f'labels shape: {labels.shape}') print(f'valence 范围: {labels[:, 0].min():.1f} ~ {labels[:, 0].max():.1f}')

loadmat 返回的是 dict,key 与 .mat 文件里保存的变量名一致,所以 mat['data'] 和 mat['labels'] 是固定写法。这里不建议加 squeeze_me=True 参数去降维,因为去掉空维度后 data 的形状容易让人误解,不如在读完之后手动索引来得直观。如果一次要加载全部 32 个被试,建议先用 glob 把文件路径按编号排序,再逐个读入,避免操作系统返回顺序不同导致被试顺序错乱——这会直接影响后面 subject_ids 和 X 行的对应关系,属于埋得很深的坑。

另外一个老手也会偶尔翻车的点:部分镜像重打包的 .mat 文件里 data 是 uint16 或 int16 类型,后续做减均值、Welch 功率谱时,有些版本的 scipy 会直接报类型错误,有些则静默截断。最稳妥的做法是在加载后立刻转成 float64:

data = data.astype(np.float64)

转类型这一行看似多余,实际能省掉后面排查一两小时的麻烦。每个 .mat 文件体积约 90 MB,32 个被试的预处理数据总量约 2 GB 出头,这个体量在学术数据集里相当克制,CPU 上跑 SVM 只要几分钟,GPU 上跑 CNN 一小时以内能完成一轮完整训练。

2.3 标签顺序与视频刺激对齐:一个小细节

labels 矩阵每一行不是随机排列的,而是严格按实验播放顺序排的——labels[i] 对应第 i+1 个试次的评分。这条逻辑听起来简单,但在 deap数据集下载 后最容易出问题:一些第三方重打包数据集会把被试文件合并后重新排序,评分顺序和信号顺序不再一一对应。一旦你拿错行做标签,训练曲线再漂亮也是废的。

我自己的习惯是在加载完一个被试后立刻检查:打印 labels 第一列,确认是 1 到 9 之间的整数且长度等于 40;同时对比 data 第一维长度也是 40。两个长度不一致,优先怀疑文件损坏或读取方式不对,别急着往下做特征提取。这个习惯让我至少避开过两次“数据加载成功但标签错位”的坑。另外,arousal 的二分类通常也按 5 分界或中位数划分,但 valence 和 arousal 并不独立。DEAP 里的情绪模型可以映射到 valence-arousal 二维平面:高 arousal 高 valence 是兴奋,高 arousal 低 valence 是紧张,低 arousal 低 valence 是放松。很多源码会同时输出两个任务的准确率,再给出二维混淆矩阵,这比单一准确率更能说明模型学到了什么。

3. DEAP 情绪识别的预处理管线:从原始信号到特征矩阵该走哪几步

3.1 去基线:把 3 秒静息状态的“地板”掀掉

DEAP 每个试次的前 384 个采样点(3 秒 × 128 Hz)是被试安静状态下的信号,这部分混在刺激信号里会拉高所有频段的能量,所以特征提取前必须做基线校正。常见做法有两种:一是直接截掉前 3 秒,只用后 60 秒;二是先把前 3 秒的均值当作基线水平,从整个试次的每个通道上减掉,再截掉前 3 秒。我一直用第二种——先减基线均值再切除。直接截断的问题是脑电的直流漂移和个体静息水平差异会带进特征,跨被试实验时准确率明显下探;先减均值虽然不能完全消除个体差异,但能抹平同一被试内部的状态波动。实现代码如下:

def preprocess_trial(trial, fs=128, baseline_sec=3): # trial shape: (n_channels, n_samples) baseline_len = int(baseline_sec * fs) # 3秒 * 128Hz = 384 baseline_mean = trial[:, :baseline_len].mean(axis=1, keepdims=True) trial_corrected = trial - baseline_mean trial_corrected = trial_corrected[:, baseline_len:] return trial_corrected

这里唯一需要调整的参数是 baseline_sec,在 DEAP 上固定为 3,不要改。如果你的数据是自采的,基线长度要根据实验设计定,可能是 1 秒也可能是 5 秒。最终返回的数组形状是通道数 × 7680,因为丢掉了前 384 个点。这个函数的输入必须保证通道在第一维,否则 mean(axis=1) 会算出错误结果。顺带一提,去基线的顺序要在任何滤波之前做,先滤波再去基线会把基线均值偏移抹进刺激段,反而引入新的偏差。

3.2 特征提取:时域统计量加频带功率,还是只取 PSD

特征工程是 DEAP 情绪识别里最看功力的部分,也是最容易“做了很多却没用”的部分。DEAP 论文和大量开源源码里最常用的特征是功率谱密度 PSD:对每个试次的每个通道做 Welch 谱估计,再按 theta(4-8 Hz)、alpha(8-13 Hz)、beta(13-30 Hz)、gamma(30-45 Hz)频带取平均功率。另一条路线是先算时域统计量,比如均值、标准差、均方根、过零率,再叠加频带功率。这两套特征的差别在于:时域特征对情绪诱发的瞬时波动敏感,频域特征更稳健、更不容易被单帧噪声带偏。

我的建议是先只用频带功率跑一个基线,准确率如果低于 68%,再考虑补时域统计量做特征拼接。下面是频带功率特征的最小实现:

from scipy.signal import welch def extract_band_power(trial, fs=128, nperseg=256): # trial shape: (n_channels, n_samples) bands = { 'theta': (4, 8), 'alpha': (8, 13), 'beta': (13, 30), 'gamma': (30, 45) } features = [] for ch in range(trial.shape[0]): freqs, psd = welch(trial[ch], fs=fs, nperseg=nperseg) for band, (lo, hi) in bands.items(): mask = (freqs >= lo) & (freqs < hi) features.append(psd[mask].mean()) return np.array(features) # 每个试次得到 40通道*4频带=160维

welch 的两个参数对结果影响很大。nperseg 是每个窗口的采样点数,256 对应 2 秒窗口,频率分辨率约 0.5 Hz,足以区分 theta 和 alpha;如果你把 nperseg 调到 512,分辨率更高但方差变大,DEAP 这种单试次只有 60 秒的数据会吃亏。fs 必须写 128,不要写成 512——DEAP 预处理数据已经降过采样,用错采样率会让频带掩码错位,theta 的功率跑到其它频段去,这种错误最气人,因为代码不报错、准确率只是上不去。

顺带说明,Hjorth 参数(活动性、移动性、复杂度)在很多情绪识别论文里被当作补充特征,代码只有几行,但它和 PSD 的互补性一般,起步阶段优先级低于频带功率。如果想让特征的时域信息更充分,可以加一列标准差和一列均方根,这两个统计量几乎不增加计算量,却能给分类器提供幅度维度的区分信息。

3.3 把 1280 个试次组装成特征矩阵

一个被试 40 个试次,32 个被试总共 1280 个试次。把所有试次的特征向量堆成二维矩阵 X,同时把标签二值化成 y,这一步的组织方式决定了后续模型训练和交叉验证的写法。我习惯用一个循环同时处理多个被试:

def build_feature_matrix(subject_ids, base_dir): X_list, y_list = [], [] for sid in subject_ids: mat = scipy.io.loadmat(f'{base_dir}/s{sid:02d}.mat') data_trials = mat['data'].astype(np.float64) # (40, 40, 8064) labels = mat['labels'] # (40, 4) for t in range(data_trials.shape[0]): trial = preprocess_trial(data_trials[t]) features = extract_band_power(trial) X_list.append(features) # valence 二分类:大于 5 为正,否则为负 y_list.append(1 if labels[t, 0] > 5 else 0) return np.array(X_list), np.array(y_list) X, y = build_feature_matrix(range(1, 33), 'data_preprocessed_matlab') print(f'X shape: {X.shape}, y shape: {y.shape}')

到这里,X 是 (1280, 160),y 是 (1280,),可以喂给绝大多数 scikit-learn 分类器。build_feature_matrix 里的文件路径、被试编号格式要跟实际解压的目录结构一致;把文件夹命名为 data_preprocessed_matlab 就不用改代码。这个时候你应该再确认一件事:y 里两类的比例。如果正负样本比例偏离 50% 太多,后面要在模型里加 class_weight 或者改用 F1 作为主指标,只看 accuracy 会被类别先验骗过去。

4. 用 DEAP 源码跑通第一个分类模型:SVM 基线与 CNN 的取舍

4.1 划分训练集与测试集:打破“同被试试次”的诅咒

DEAP 的复现有一个隐藏陷阱:如果直接把 1280 个试次随机打乱后切分 train/test,同一被试的 40 个试次会同时出现在两边,模型学到的其实是这个人专属的脑电模式,跨被试就失效——准确率能虚高到 85% 以上,但换个被试数据立刻崩。正确做法是按被试划分:取前 24 个被试做训练,后 8 个被试做测试;或者做留一被试交叉验证(Leave-One-Subject-Out,LOSO)。前者快、后者统计上更可信。

import numpy as np def train_test_split_by_subject(subject_ids, X, y, train_subjects): test_mask = np.isin(subject_ids, train_subjects, invert=True) return X[~test_mask], X[test_mask], y[~test_mask], y[test_mask] subject_ids = np.repeat(np.arange(1, 33), 40) # 每个被试 40 试次 X_train, X_test, y_train, y_test = train_test_split_by_subject( subject_ids, X, y, train_subjects=range(1, 25))

这段代码的关键在 subject_ids 的构造:np.repeat(np.arange(1, 33), 40) 是把每个被试编号重复 40 次,得到 1、1、…、2、2、… 这样的序列。如果你写成 np.tile(np.arange(1, 33), 40),得到的是 1、2、3、…、32、1、2、…,恰好把 40 个试次打散,train/test 里又会混进同一被试的数据,前面做的按被试分组也会失效。新手最爱犯的错误之一就是这一行,每次看到测试准确率异常高,先检查它。

按被试划分后,训练集和测试集的类别分布可能不一致,比如前 24 个被试的 valence 平均值偏高。这时候 class_weight='balanced' 就派上用场了。你也可以在报告结果时单独列出测试集类别占比,避免别人质疑结果是被类别先验撑起来的。这种质疑在论文评审里非常常见,提前把类别分布写清楚能少很多麻烦。

4.2 SVM 基线:把准确率先稳定在 70% 附近

在 DEAP 上做 valence 二分类,SVM 几乎是性价比最高的基线。特征维度 160、样本量 1280,RBF 核在这组数据上一般能把准确率推到 68% 到 75% 之间。前提是特征先做标准化:StandardScaler 必须在训练集上拟合、再用同一个变换处理测试集。scikit-learn 的 make_pipeline 恰好能保证这一点,不会让你手写出“用全量数据 fit 再划分”的泄漏操作。

from sklearn.svm import SVC from sklearn.pipeline import make_pipeline from sklearn.preprocessing import StandardScaler from sklearn.metrics import accuracy_score, f1_score model = make_pipeline( StandardScaler(), SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced') ) model.fit(X_train, y_train) y_pred = model.predict(X_test) print(f'Accuracy: {accuracy_score(y_test, y_pred):.3f}') print(f'F1: {f1_score(y_test, y_pred, average="binary"):.3f}')

SVC 参数里值得解释的是 gamma='scale',它会按特征数的倒数自适应,省去手动搜 C 和 gamma 的环节;C=1.0 是默认值,在 1280 样本下够用,调到 10 反而容易过拟合;class_weight='balanced' 在 valence 类别不均衡时自动加权。如果你发现准确率低于 65%,先别急着换模型,回头检查特征提取里 fs 是不是写成 512、基线是否真的减掉了。基线跑通后,可以拿 sklearn 的 GridSearchCV 在小范围里搜一下 C,一般 C 在 1 到 10 之间、gamma 在 scale 附近表现最稳。

4.3 CNN 方案:把信号切窗后喂给 Conv1D

如果你不满足于 SVM,想试试深度学习,最常见的做法是把原始信号按 4 秒窗口切段,每段作为一个独立样本输入 Conv1D 网络。窗口重叠 50%,一个 60 秒试次大约能切出 29 个窗口,数据量从 1280 涨到接近 4 万,深度学习模型终于有足够的样本去学特征了。这也解决了 DEAP 原始数据太少、深度模型容易过拟合的核心矛盾。

from tensorflow.keras.models import Sequential from tensorflow.keras.layers import Conv1D, MaxPooling1D, Flatten, Dense, Dropout model = Sequential([ Conv1D(filters=64, kernel_size=16, activation='relu', input_shape=(512, 40)), # 4 秒窗口:512 时间步 × 40 通道 MaxPooling1D(pool_size=4), Flatten(), Dense(64, activation='relu'), Dropout(0.5), Dense(1, activation='sigmoid') ]) model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])

input_shape 第一维是时间步 512(4 秒 × 128 Hz),第二维是通道数 40。为什么把时间放前面?因为 Conv1D 默认在最后一维上卷积,如果你把通道放最后一维,卷积会跨通道混合,把空间位置信息打散。kernel_size=16 对应 125 ms 的感受野,足以覆盖 alpha 波的一个周期(约 8–12 Hz)。Dropout 放在全连接层前,专门压小样本下的过拟合。切窗时每个窗口的标签从原试次继承,切窗操作一定在数据集划分之后做,否则又会制造数据泄漏。训练轮数建议控制在 20 轮以内,配合 EarlyStopping 看验证损失,DEAP 这种规模的数据跑 50 轮以上基本都是过拟合,验证集准确率不再上升。

5. DEAP 数据集下载与源码复现的 6 个常见坑:现象、原因、解决

5.1 官网链接到期打不开,压缩包解压报 CRC 错误

现象:通过官网申请下载后,邮件里的链接过了一段时间失效;或者从网盘下载的大型压缩包解压到一半报错,提示 CRC 校验失败。

原因:官网下载链接通常有时效,邮件里的地址会在一定天数后失效。压缩包体积大,校园网或无线网络下载时容易断流,断流后如果下载工具没有断点续传,文件大概率损坏。

解决:收到官网邮件后第一时间把下载链接转存到自己的服务器或云盘,别只留在邮箱里。下载工具必须开启断点续传,完成后先算 MD5 再解压。如果镜像站点已经失效,就老老实实回到官网重新走一遍申请流程,这步没有后悔药。虽然这不是代码问题,但它能卡掉你一整个下午,值得写进清单第一位。

5.2 .mat 文件读出来维度对不上

现象:用 loadmat 读 s01.mat,data 的 shape 打印出来是 (1, 1, 40, 40, 8064),或者 (40, 8064, 40),和文档里的 (40, 40, 8064) 对不上。

原因:loadmat 默认会保留 .mat 文件里的所有维度,遇到 MATLAB v7.3 格式还会返回 HDF5 对象,而 h5py 坐标系下维度顺序是反的。各镜像站重打包时也可能调换数组轴。

解决:先打印 mat.keys() 确认变量名;如果值是 HDF5 对象,改用 h5py 读取并记住维度反序的问题;遇到多余的 (1, 1) 包在开头,直接 np.squeeze 掉。无论哪种情况,以打印出来的实际 shape 为准,不要盲信网上任何“固定形状”——这是所有 DEAP 复用项目里最常见的黑匣子之一。

5.3 把 valence 阈值的边界画错,类别分布大变

现象:训练集准确率很高,测试集在 55% 上下浮动,查代码发现阈值写成了 labels[t, 0] >= 6 而不是 > 5。

原因:DEAP 的评分是 1 到 9 的离散等距量表,5 是中间值。>5 和 >=6 几乎等价,但 >5 和 >=5 之间差着整整一个评分档位的样本量;在 1280 个样本的小数据集上,这个差异足以让测试准确率波动好几个百分点。

解决:在定阈值前先把 labels 第一列按值分组,画出每个评分的样本量,确认类别均衡度再下结论。如果想做个性化切割,可以按每个被试的 valence 中位数二值化,跨被试实验时这个操作能让类别更均衡。但如果你想严格复现 DEAP 论文的实验设定,就必须按论文的原始定义来,不要自创阈值——论文复现与工程落地之间最重要的区别就在这里。

5.4 无意中把同一被试的数据泄漏进验证集

现象:LOSO 交叉验证结果高达 85%,换成跨被试评估直接掉到 60%,差距大到没法解释。

原因:切窗构造样本时没有按被试分组;或者做数据增强(比如加高斯噪声)之后,同一个试次的增强样本同时出现在 train 和 test 中。模型记住的是被试身份而非情绪特征,所以换人就失效。

解决:所有切分严格以被试为单位,切窗和增强都必须放在划分之后再对训练集单独执行,永远不要让一个被试的数据横跨两个集合。特征标准化同理:StandardScaler 在训练集上 fit,测试集只 transform。如果你手写了标准化代码而不是用 pipeline,这一步最容易漏,建议一律用 make_pipeline 包住。

5.5 32 个被试全部读入内存,直接 OOM

现象:循环 loadmat 加载 32 个文件,每个文件约 90 MB,再加上中间转换数组,16 GB 内存的机器直接卡死,进程被系统杀掉。

原因:loadmat 把整个 (40, 40, 8064) 读进内存,.astype(np.float64) 又会临时复制一份,内存瞬间翻倍;而且很多人习惯先全部 load 进 list 再处理,内存占用雪上加霜。

解决:改成被试级流式读取——加载一个被试,提取特征后立即释放原始数组,用 del data 和 gc.collect() 回收,只保留 160 维的特征向量。1280×160 的 float64 只有几 MB,这才应该是常驻内存的东西。第 3 章的 build_feature_matrix 已经按这个思路实现,只要别把每轮的原始数组存到 list 里就不会爆内存。

5.6 通道顺序和脑电位置映射表对不上

现象:画脑地形图时,32 个 EEG 通道的坐标怎么都对不上标准 10-20 系统的位置,某些通道布局全乱。

原因:DEAP 的 32 个 EEG 通道不是按 Fz/Cz/Pz 这种标准顺序排列的,而是按采集设备硬件通道的物理顺序排布。如果你直接拿通用电极位置文件去套,自然错位。

解决:使用 DEAP 官方源码里固定的通道名列表或位置文件,不要自行按字母排序。画图时优先用 MNE 的 montage 功能,按 DEAP 自带的通道顺序映射。这个坑不影响分类准确率,但在论文里画图时会让你怀疑人生,最好提前规避。

6. 把 DEAP 二分类实验升级为跨被试泛化实验:LOSO 脚本与随机种子

二分类实验能跑通之后,下一步自然是评估模型是否真的“认识情绪”而不是“记住被试”。留一被试交叉验证(LOSO)是 DEAP 社区最常用的进阶评估方式:每次拿 31 个被试训练、1 个被试测试,循环 32 次后聚合结果。这样出来的指标不会因为某个被试特别配合而虚高。下面的脚本把整个评估过程封装起来,直接替代第 4 章的固定切分:

from sklearn.model_selection import LeaveOneGroupOut groups = np.repeat(np.arange(32), 40) # 每个被试一个组 logo = LeaveOneGroupOut() accs, f1s = [], [] for train_idx, test_idx in logo.split(X, y, groups): X_tr, X_te = X[train_idx], X[test_idx] y_tr, y_te = y[train_idx], y[test_idx] clf = make_pipeline( StandardScaler(), SVC(kernel='rbf', C=1.0, gamma='scale', class_weight='balanced') ) clf.fit(X_tr, y_tr) y_pred = clf.predict(X_te) accs.append(accuracy_score(y_te, y_pred)) f1s.append(f1_score(y_te, y_pred, average='binary')) print(f'LOSO Acc: {np.mean(accs):.3f} ± {np.std(accs):.3f}') print(f'LOSO F1: {np.mean(f1s):.3f} ± {np.std(f1s):.3f}')

注意标准差的含义:它反映的是不同被试之间的难度差异,不是模型随机性。如果你发现某一折的准确率特别低,可以把对应的被试编号打出来,回去看这个人的评分分布是否极端——这是 DEAP 自带的高方差来源,不应该被轻易归因于代码问题。如果你在 LOSO 上想把 SVM 换成随机森林或梯度提升树,只需要替换 clf 那两行,其余流程完全不用动,这样不同模型的对比也更公平。

关于随机种子,我必须说一个血泪教训:早期我为了让结果“好看”,每次跑之前不固定 seed,跑了五次拿到四个不同的准确率。后来才发现,很多离线实验的微小差异根本不是算法改进,而是随机划分和随机初始化的波动。把 seed 固定住、把被试分组固定住,你才能确定调参改的是模型而不是运气。具体到 LOSO,分组本身固定后,主要随机性在 SVM 求解器和切窗顺序,加一个 np.random.seed(42) 就能复现。我现在做生理信号情绪识别实验,固定分组、固定随机种子、报告标准差这三点已经成了默认习惯,没有这三点,任何准确率数字我都不会当作有效结论。希望这些经验能帮你少走几步弯路,愿你在 DEAP 数据集上第一次跑出的分数就是可信的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询