为什么按 epoch 随机切分会得到假的高准确率
一个容易犯的错
脑电分类的常见做法是:把录音切成很多 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 秒”。
所以我们先把同一个人所有片段的预测概率平均成“这个人的概率”,再做评估。这样得到的指标才对应真实使用场景。
如果你和别人的准确率差很多,先别急着调模型 —— 先检查一下双方的交叉验证是怎么切分的。这是最常见、也最容易解释的差异来源。