GroupKFold 与分层分组验证
保证同一患者、用户或设备的全部记录只出现在训练侧或验证侧。
学习前需要掌握
背景与问题
一个用户多次访问、一个患者多张影像、一个设备多次采样时,相邻记录往往共享稳定特征。随机按行划分可能让模型在验证时“认出”训练中出现过的实体,无法衡量对新实体的泛化。
分组验证将同一实体的全部记录绑定在一起。groups 的定义应来自真实依赖关系,而不是为了凑平衡而随意生成。
概念定义
groups 标识相互依赖的样本集合;GroupKFold 以组为不可拆分单位。
直观理解
一个人的多次记录必须整组进入同一考场。
核心原理
GroupKFold 把组分配给验证折,并确保同一折中训练与验证的组没有交集。标准分组 K 折中每个组恰好作为验证组一次;可选折数不能超过独立组数量。
类别比例与组隔离是不同约束。StratifiedGroupKFold 尝试在不拆组的条件下维持类别比例,但组大小与组内类别分布可能使完美分层无法实现;必须检查实际折分布。
当目标是新患者或新客户,组隔离通常比按行随机分层更符合问题。若目标是已知患者的未来记录,还要加入时间边界;仅按组隔离不能说明时间外推表现。
groups 应传给划分过程,不应默认作为预测特征。用户标识可以是拆分依据,却可能没有部署泛化意义;编码或选择这些字段前,应明确预测时是否可用以及是否只是记忆实体。
数学表达
核心表达
分组隔离通常优先于简单分层。若同时关心类别比例,可用 StratifiedGroupKFold。
变量含义
变量定义与本页概念定义一致,代码中的同名变量保持相同语义。
计算步骤
- 1定义群组 ID
- 2检查每组样本数
- 3选 GroupKFold
- 4传 groups 参数
代码对应位置
示例 1直接检查训练验证患者集合:以患者为依赖单位验证隔离,不把行索引不重复误当作无实体泄漏。
案例:6 名患者每人两次采样
下方构造 12 行、6 名患者,3 折时每折留出 2 名患者。程序不仅看行索引不重叠,还直接检查患者标识集合交集为零,并检查 6 个患者都被留出过。
如果同一患者有 100 行、其他患者只有 2 行,单看行级准确率还可能由大组主导。实际报告应说明行级与组级汇总口径,必要时另报患者平均指标,避免评价单位与部署目标不一致。
计算与实现步骤
- 定义群组 ID
- 检查每组样本数
- 选 GroupKFold
- 传 groups 参数
- 验证无交集
- 报告组级分布
代码实现
示例 1
直接检查训练验证患者集合
example_01.py用途:以患者为依赖单位验证隔离,不把行索引不重复误当作无实体泄漏。
import numpy as np
from sklearn.model_selection import GroupKFold
X = np.arange(12).reshape(-1, 1)
y = np.tile([0, 1], 6)
patients = np.repeat(np.arange(6), 2)
seen = set()
for number, (train, valid) in enumerate(GroupKFold(3).split(X, y, patients), start=1):
training_groups = set(patients[train])
validation_groups = set(patients[valid])
assert training_groups.isdisjoint(validation_groups)
seen.update(validation_groups)
print(f"fold={number}, train_rows={len(train)}, valid_groups={len(validation_groups)}, overlap=0")
print("held_out_groups:", len(seen))代码解析
解析始终位于完整代码下方,并按实际代码段逐项对应。
输入数据与任务
以患者为依赖单位验证隔离,不把行索引不重复误当作无实体泄漏。
X- 完整特征矩阵,每行一个样本、每列一个特征。
y- 目标标签或连续目标值。
Step 1 · 1–2 行
导入当前步骤需要的数值计算、预处理、模型或评价工具。依赖集中写在代码开头,便于复现。
import numpy as np
from sklearn.model_selection import GroupKFoldStep 2 · 4–14 行
输出中间参数、形状或最终指标,用于核对代码是否符合预期。
X = np.arange(12).reshape(-1, 1)
y = np.tile([0, 1], 6)
patients = np.repeat(np.arange(6), 2)
seen = set()
for number, (train, valid) in enumerate(GroupKFold(3).split(X, y, patients), start=1):
training_groups = set(patients[train])
validation_groups = set(patients[valid])
assert training_groups.isdisjoint(validation_groups)
seen.update(validation_groups)
print(f"fold={number}, train_rows={len(train)}, valid_groups={len(validation_groups)}, overlap=0")
print("held_out_groups:", len(seen))预期输出或运行结果
本地 scikit-learn 1.8.0 的核对输出: fold=1, train_rows=8, valid_groups=2, overlap=0 fold=2, train_rows=8, valid_groups=2, overlap=0 fold=3, train_rows=8, valid_groups=2, overlap=0 held_out_groups: 6
常见错误 · 4 条
- 只检查训练行与验证行没有重复,却遗漏同一实体的不同记录。
- 把 groups 放进 X 后以为已完成分组,实际上仍用普通 KFold。
- 假定分组后类别比例一定平衡,或在组数不足时强行设更多折。
- 确认样本轴、特征轴以及 X 与 y 的第一维完全对应。
实际应用
- 医疗
- 用户/设备多记录
常见错误
- 只检查训练行与验证行没有重复,却遗漏同一实体的不同记录。
- 把 groups 放进 X 后以为已完成分组,实际上仍用普通 KFold。
- 假定分组后类别比例一定平衡,或在组数不足时强行设更多折。
推荐学习资料
参考库不会生成虚假资源或无效外部链接。