为什么按 epoch 随机切分会得到假的高准确率

2026 年 9 月 28 日交叉验证数据泄漏方法学

一个容易犯的错

脑电分类的常见做法是:把录音切成很多 2 秒片段,每个片段一个标签,然后随机切分训练集和测试集。

# ❌ 错误做法
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2)

看起来没问题,但如果 X 里包含同一个人的多个片段,这就等于开卷考试。

为什么

同一个人相邻的 2 秒片段几乎是一样的:脑电的背景节律在秒级尺度上高度自相关。所以:

  • 训练集里已经有“这个人”的 100 个片段
  • 测试集里再放他的另外 30 个片段
  • 模型不需要学到任何与抑郁相关的知识,只要认出这是同一个人就能答对

结果就是 90%+ 的漂亮准确率,但换一批没见过的人,立刻掉到随机水平。

正确的做法:按“人”切

from sklearn.model_selection import StratifiedGroupKFold

# 每个片段都带上它属于哪个被试
groups = np.array([...])   # 例如 'MDD_S12' / 'HC_S5'

cv = StratifiedGroupKFold(n_splits=5)
for train_idx, test_idx in cv.split(X, y, groups):
    ...

这样能保证同一名被试的所有片段只在同一折里。

还有一层:内容重复

我们后来发现数据里还有更隐蔽的问题:10 名健康被试的 TASK 文件逐字节完全相同(同一份录音存了 10 份)。另外还有两组“整人重复”。

这意味着即使按被试分了组,如果两个“不同被试”共享同一份数据,它们仍然可能一个在训练集、一个在测试集 —— 依然是泄漏。

解法:用 MD5 找出内容相同的文件,再用并查集把共享数据的被试绑成同一组。

# 并查集:MD5 相同的被试合并到同一组
for md5, subjects in shared.items():
    for s in subjects[1:]:
        union(subjects[0], s)

group_labels = np.array([find(s) for s in subjects])
cv.split(X, y, group_labels)     # 用合并后的组分折

效果

在我们的数据上,去掉那 9 个共享文件之后:

配置 内容组数 准确率 折间标准差
不去重 53 87.5% ±10.2%
严格去重 62 90.3% ±2.9%

准确率反而升高,折间波动从 ±10.2% 降到 ±2.9%。

因为不合理的分组不仅让训练集被污染,也让每一折的类别比例失衡 —— 评估本身就失真了。

顺带一提:评估单位应该是“人”

临床上要诊断的是一个人,不是“2 秒”。

所以我们先把同一个人所有片段的预测概率平均成“这个人的概率”,再做评估。这样得到的指标才对应真实使用场景。

如果你和别人的准确率差很多,先别急着调模型 —— 先检查一下双方的交叉验证是怎么切分的。这是最常见、也最容易解释的差异来源。