qinghuan
← 机器学习与统计推断 / 全部笔记

单元 09 / 机器学习与统计推断

P0 · 必会主线L1 · 基础

训练集、验证集与测试集:防止数据泄漏

按将来会遇到的新数据选择随机、分组或时间切分,并只用训练数据学习预处理需要的统计量。

本篇目录 · 按需要跳转

这篇重点学什么

本篇重点:只用训练部分学习预处理参数,再复用参数处理其他数据(fit / transform);交叉验证时每折都在训练部分重新拟合,检查新实体上的效果,并保存数据及处理结果的版本。

开始读正文 ↓
学过相近内容?查看哪些可以跳过

两条路线有些基础相同。已经学会的部分可以跳过,另一条路线只用来补你当前需要的内容。

数据划分与评测集

共同基础这组原则只学一次:先确定以后要处理哪些新数据,再据此给相关样本分组;同组不跨数据集,测试集不用来调参,并划清时间先后,避免测试内容的相同或近似版本提前进入训练。

另一条路线补充什么?

写清系统允许和禁止做什么,把同一原始案例的变体归为一组,记录失败类型,并固定回归测试的版本与通过条件。

读完这篇,你能做什么

能重复生成组间不重叠的数据划分,检查训练与验证各包含哪些数据,并确认预处理只从训练集学习规则。

学习理由、准备要求与知识点

为什么学它

如果提前用到了测试信息,分数就可能虚高。怎样划分数据,也决定了评估是在测哪一种新场景。

先读这些内容

知识点清单

  • 训练验证测试职责
  • 评估目标与抽样单位
  • 随机与分层切分
  • 分组独立性
  • 时间顺序与未来信息
  • 预处理拟合边界
  • 重复内容泄漏
  • 切分审计与可重复性

运行前先确认Java 17;Python 3.14 CPU 教学实验,无外部数据、预训练权重、GPU 或多机训练。源代码基线与局限见源码模块。

01先弄懂它是什么

三份数据,各自承担一件事

训练完的模型到底学会了规律,还是只记住了做过的题?要分清这两件事,需要让数据承担不同工作。

数据用来做什么
训练集调整模型参数
验证集挑选设置和方案
测试集检查最终定好的流程

如果反复查看测试分数,再根据分数修改模型,这份测试数据就已经参与了选择,不能继续当作完全独立的检验。

数据的用途决定它的角色。 文件叫“测试集”,不代表它在使用过程中一直保持独立。

按将来会遇到的情况划分

划分数据前,先明确要预测什么:

  • 老用户的新记录,与从未见过的新用户,是两个不同的问题。
  • 如果要预测未来,就要考虑时间先后,不能把未来的信息接到过去的输入中。
  • 如果记录属于同一个用户,或者是同一篇文档的片段、近似副本,就要检查这些关联内容是否跨进了训练和测试。

若把所有记录随便打散,同一用户的数据、同一文档的近似副本就可能同时进入训练和测试。模型可以借助熟悉的身份或重复内容拿高分,却未必能处理真正的新情况。

数据泄漏,可能发生在整个处理流程中

数据泄漏指训练模型或选择方案时,用到了按评估规则本来不该知道的信息,导致分数显得过好。需要检查的情况包括:

  • 把要预测的答案混进输入。
  • 先用全部数据计算标准化参数。
  • 使用目标事件发生后才生成的特征。
  • 让重复样本同时进入训练和测试。
  • 把未来记录接到过去的输入中。
  • 反复根据测试分数调整模型或参数。

计算均值、建立词表时,规则从哪份数据学来,也要遵守训练与测试的边界;不能先利用全部数据学规则,再把数据分开。

模型没有直接看到测试标签,不等于整个流程没有泄漏。 数据划分、预处理、特征生成和方案选择,都要检查。

文中这几个词,先认一认

抽样单位
划分数据时需要放在同一组的对象,例如同一个用户的全部记录或同一篇文档的所有片段。
数据泄漏
训练或选方案时,提前用到了正式评估中本来不该知道的信息,导致分数显得过好。
拟合预处理
先从数据里算出均值、建立词表等转换规则,再用这些规则处理数据。规则从哪份数据学来,也要遵守训练与测试的边界。

还有陌生词?到术语小抄里看一个具体例子 ↗

02看看它怎么工作

随机切分适合样本近似独立同分布、且将来实际使用时的数据条件与之相符的情况。分层切分尝试保留各标签的比例,但不能自动消除组间关联。分组切分把属于同一对象的所有记录放在同一侧,用来检查模型对新对象的预测表现。时间切分用过去的数据训练、未来的数据验证,必要时在分界处留出时间间隔,防止前后两侧的窗口重叠而泄漏信息。

预处理也有“学习规则”和“使用规则”两步,通常叫 fit 与 transform。例如先只用训练数据计算标准化的均值和方差,再用固定结果处理验证集、测试集。若先用全部数据算均值,即使没看标签,也提前用到了评估数据的分布。特征选择、缺失值填充、词表和降维,只要会从数据里学统计信息,都要检查同样的边界。

交叉验证分成多轮,每轮(每一折)各有自己的训练部分和验证部分。凡是要从数据中学习的步骤,都必须只用当轮训练部分重新拟合。例如,不能先用全部数据选好特征,再只对模型做交叉验证;这样仍会泄漏。Pipeline 可以把预处理和模型串起来,让交叉验证在每轮训练部分一起拟合,但它不会自动修复分组错误、重复数据或带未来信息的原始字段。

组的数量和记录数量不是同一个比例。若一个用户有一千条记录,其他用户各有两条,留出百分之二十的用户不一定留出百分之二十的行。报告中应同时记录组数、行数与关键标签分布;切分后才发现验证集缺少某类,也不能偷偷把困难样本挪回训练而不说明选择过程。

固定种子能重现某个切分,但不能证明该切分具有代表性。本篇先排序唯一组名,再按固定种子打乱组,保证相同组集合与实现下成员选择稳定;行的原始顺序不决定抽到哪些组。Java 与 Python 的随机算法不同,相同整数种子不意味着两种语言生成完全相同的划分。

先跑通一个最小例子

Python stdlib 小实验构造每用户两条记录,比较按行交错划分与按组划分,再检查训练均值只来自训练组。

experiment.py · python
import random, math
rows=[(f"user{i}",10*i*i+j) for i in range(5) for j in range(2)]
naive_train=rows[::2]; naive_valid=rows[1::2]
assert {g for g,_ in naive_train}&{g for g,_ in naive_valid}  # 行不重叠仍可能用户重叠。
groups=sorted({g for g,_ in rows})
random.Random(2026).shuffle(groups)
held=set(groups[:2])
train=[r for r in rows if r[0] not in held]
valid=[r for r in rows if r[0] in held]
assert not ({g for g,_ in train}&{g for g,_ in valid})
assert len(train)+len(valid)==len(rows)
assert len({g for g,_ in valid})==2
mean=sum(x for _,x in train)/len(train)  # 只能在训练部分估计。
centered=[x-mean for _,x in train]
assert math.isclose(sum(centered),0.,abs_tol=1e-10)
full_mean=sum(x for _,x in rows)/len(rows)
assert not math.isclose(mean,full_mean)
print("PASS 6; train_groups =",len(groups)-2,"valid_groups =",2)

核对运行结果打印 PASS 6;按行划分存在组重叠,分组划分无重叠,训练均值与全量均值不同。

正在加载流程图…

切分先于数据驱动的预处理拟合,测试隔离覆盖整个流程而非仅模型权重。
查看流程图源码(Mermaid)
flowchart TD
 A["明确部署时的新信息"] --> B["选择行 组或时间作为边界"]
 B --> C["划分并审计重叠"]
 C --> D["只在训练部分拟合预处理"]
 D --> E["固定变换评估验证集"]
 E --> F["方案确定后使用独立测试集"]

按顺序看,每一步做了什么

  1. 明确评估新用户、新时间还是新记录。
  2. 选择与目标匹配的划分单位。
  3. 检查 ID、组和重复内容重叠。
  4. 在训练部分拟合变换。
  5. 记录种子、成员清单与分布统计。

03对照实现代码

scikit-learn 1.7.2:GroupShuffleSplit 按组再映射回行 (新标签页)↗

scikit-learn 1.7.2;sklearn/model_selection/_split.py · GroupShuffleSplit._iter_indices

以下为原创精简教学实现,不是原样库源码;只演示本文说明的机制,省略真实实现的优化、调度和完整兼容边界。

核心机制代码 · python
import random
def split_groups(groups,held_count,seed):
    unique=sorted(set(groups))
    if not 0<held_count<len(unique):
        raise ValueError("group count")
    random.Random(seed).shuffle(unique)  # 先选择组,不逐行独立抽样。
    held=set(unique[:held_count])
    train=[i for i,g in enumerate(groups) if g not in held]
    valid=[i for i,g in enumerate(groups) if g in held]  # 整个组一起映射回行。
    return train,valid

scikit-learn 1.7.2 的 GroupShuffleSplit._iter_indices 要求提供 groups,先通过 unique 获得唯一组和每行对应的组索引,再对唯一组调用上层随机切分机制。最后用组索引成员关系将训练组、测试组映射回原数据行。

真实源码因此保证一次划分中某个组不会同时进入两侧,但并不保证多次随机划分的测试组彼此不重叠。若做多次重复评估,不能把各次结果当作完全独立的全新样本,而忽略它们可能共用组和记录。

原创模型保留按组选择再回映行的机制,省略框架对比例、随机状态对象、数组类型与多次迭代的完整支持。它使用 Python 标准库随机序列,并不声称与 scikit-learn 在相同种子下产生相同索引。

分组切分器只认识调用者提供的组标识。若同一实体使用了两个不同 ID,或跨组存在复制内容,它仍可能泄漏;如果真正部署目标是未来预测,单纯分组随机切分也可能忽略时间。工具提供形式约束,研究者负责定义正确的独立单位。

代码先调用谁,再调用谁

  1. 接收每条记录的 groups
  2. 提取唯一组及行到组映射
  3. 随机划分组索引
  4. 把组成员映射为训练和测试行
  5. 调用者审计标签、时间和重复内容

04动手写一个例子

固定组数的可重复数据划分

输入具有唯一行 ID 和非空组名的记录,指定留出的组数 k 与种子;组不能跨侧,结果覆盖每行且保留各侧原始行顺序。

输入 1
a组2行、b组1行,留出1组
输出 1
某一完整组进入验证集
输入 2
只有1组却要留出1组
输出 2
拒绝空训练组集合

先验证每行和唯一 ID,收集排序后的唯一组名。排序消除原始行顺序对随机输入序列的影响,再对组列表进行带种子的打乱。取前 k 个组作为验证组,剩余组用于训练,要求两侧都至少有一个组。

第二遍扫描原始行,根据组名是否属于验证集合分流。这样组间无重叠,所有行恰好进入一侧,同时各侧行顺序可追溯到原数据。结果列表复制后不可修改,避免下游不小心改变切分成员。

复杂度不仅取决于行数,也取决于组名排序。验证集合使用哈希查找获得预期常数成本;排序组名的字符比较成本需要另计。算法只处理显式组,不检测文本近重复或未来信息,这些应由后续审计补充而不是从无组重叠直接推导无泄漏。

为什么这样写是对的每条已处理记录恰好进入与其组决定一致的一侧,所有同组记录始终同侧。

时间 预期 O(n+g log g),n 为行数、g 为组数,另计组名比较长度。额外空间 O(n+g) 的结果、ID 集合和组集合。

两次扫描记录,排序并打乱 g 个组,再按集合成员关系分流;输出本身包含全部 n 行引用。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

Main.java · 完整可运行程序 · Java
import java.util.*;
public class Main {
  record Row(int id,String group){}
  record Split(List<Row> train,List<Row> valid){}
  static Split split(List<Row> rows,int heldCount,long seed){
    if(rows==null)throw new IllegalArgumentException("rows");
    Set<Integer> ids=new HashSet<>();Set<String> names=new TreeSet<>();
    for(Row r:rows){
      if(r==null||r.group()==null||r.group().isBlank()||!ids.add(r.id()))throw new IllegalArgumentException("row");
      names.add(r.group());
    }
    if(heldCount<=0||heldCount>=names.size())throw new IllegalArgumentException("groups");
    List<String> shuffled=new ArrayList<>(names);Collections.shuffle(shuffled,new Random(seed)); // 排序后再随机化组。
    Set<String> held=new HashSet<>(shuffled.subList(0,heldCount));
    List<Row> train=new ArrayList<>(),valid=new ArrayList<>();
    for(Row r:rows)(held.contains(r.group())?valid:train).add(r); // 同组整体分流,保留原始行顺序。
    return new Split(List.copyOf(train),List.copyOf(valid)); // 固定切分成员,避免后续修改。
  }
  static Set<String> groups(List<Row> rows){Set<String>s=new HashSet<>();for(Row r:rows)s.add(r.group());return s;}
  static void check(boolean ok){if(!ok)throw new AssertionError();}
  static void bad(Runnable r){try{r.run();}catch(IllegalArgumentException e){return;}throw new AssertionError();}
  public static void main(String[] args){
    List<Row> rows=List.of(new Row(1,"a"),new Row(2,"a"),new Row(3,"b"),new Row(4,"c"));
    Split s=split(rows,1,2026);Set<String> overlap=groups(s.train);overlap.retainAll(groups(s.valid));check(overlap.isEmpty());
    check(s.train.size()+s.valid.size()==rows.size()&&groups(s.valid).size()==1);
    check(s.equals(split(rows,1,2026)));
    List<Row> reverse=new ArrayList<>(rows);Collections.reverse(reverse);check(groups(s.valid).equals(groups(split(reverse,1,2026).valid)));
    bad(()->split(List.of(new Row(1,"a")),1,1));
    bad(()->split(List.of(new Row(1,"a"),new Row(1,"b")),1,1));
    System.out.println("PASS 6");
  }
}

这些特殊情况也要试一试

  • 组数必须足以让两侧非空。
  • 行 ID 重复视为输入错误。
  • 同种子只保证相同实现和组集合下可重复。
  • 无组重叠不等于无内容或时间泄漏。
进阶选读 · 同时约束组、时间和类别

场景需要评估未来新实体,且类别分布极不均衡。

做法先确定不可破坏的时间与实体边界,再评估各侧有效样本和标签覆盖;必要时重新设计数据收集与评估目标,而非偷偷混合边界。

复杂度简单规则扫描可线性;同时平衡多约束可能形成组合优化问题。

好处与代价更符合部署场景但可能减少可用数据,不能承诺总能达到任意精确比例。

05项目里怎么用

保存切分成员清单、数据版本与构建代码,比只保存随机种子更可靠。数据新增、排序变化和库版本变化都可能改变同种子的输出;明确 ID 清单可以让后续实验比较基于同一数据边界。敏感数据不宜直接公开,审计记录可以保留必要标识或受控摘要。

训练和验证用到的预处理参数,应与模型一起保存和管理。推理时继续使用训练得到的均值、词表和编码规则,不能临时用当前一批请求重新拟合,否则送进模型的数值或编码含义会悄悄改变。若新数据与训练数据的分布发生变化(分布漂移),应明确安排重新训练或适配,不能直接把评估数据加入拟合。

测试集被查看、比较过多少次,也应记录。试了很多方案后再挑测试分数最高的一个,就可能把碰巧适合这份测试集的方案选出来,这叫选择偏差;即使从未用测试样本训练模型,也会发生。可以保留独立的最终测试集,采用嵌套验证(内层选设置、外层做评估),或预先约定实验计划。无论用什么工具,都要检查研究过程是否提前用了本该隔离的信息。

容易踩的坑

  • 先全量标准化再切分。
  • 同一用户或文档副本跨侧。
  • 测试集反复调参仍称完全独立。
  • 把留出组比例误当作行比例。

几种做法,怎么选

表格有四列,窄屏可左右滑动查看代价和适用条件。

方案优势代价与限制适用条件
随机行切分简单且利用数据充分依赖行间独立假设近似独立的新记录
分组切分隔离实体共享信息行比例和类别可能不均衡新用户或新文档
时间切分贴近未来预测可用训练量及分布随窗口变动存在时间因果边界

06检验是否读懂了

先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。

先用自己的话解释

01验证集与测试集区别在文件名吗?

展开参考答案与推导

不在。验证数据用于选择方案,测试用于方案确定后的独立评估;如果根据测试反馈持续调整,它实际也参与了选择。

02不看测试标签就不会泄漏吗?

展开参考答案与推导

不是。全量预处理、重复内容、未来字段和实体关联都可能泄漏。应检查整个数据管线的信息来源,而不仅是标签列。

03分层切分能解决组泄漏吗?

展开参考答案与推导

不能自动解决。分层主要控制标签比例,若没有组约束,同一实体仍可能跨侧。不同约束服务于不同评估目标。

换个情况,还会处理吗

01随机切分分数很高,新用户表现差怎么办?

展开参考答案与推导

检查同一用户是否跨训练与验证,重新用用户组边界评估新用户场景。原分数可能只反映熟悉实体上的新记录能力。

02验证集标准化后均值不是零是否错误?

展开参考答案与推导

不一定。正确流程使用训练均值转换验证集,验证分布不同可以导致非零均值;不能为了让它居中就重新拟合而改变评估边界。

03同一种子为何划分改变?

展开参考答案与推导

数据、排序、组集合或随机实现可能发生变化。保留成员 ID 清单与代码版本,不能把种子当作完整数据版本标识。

写代码试一试

01识别行划分泄漏。

输入 / 条件
用户a两行分别进入训练和验证
输出 / 预期
存在组重叠

提示 先明确部署目标。

展开参考答案与推导

若目标是新用户泛化,这种划分让模型提前见过用户信息,应把两行整体移动;若目标不同,也要明确该依赖被允许的理由。

02正确计算标准化均值。

输入 / 条件
训练[0,2],验证[100]
输出 / 预期
训练均值1

提示 不要全量拟合。

展开参考答案与推导

只用训练两项求均值一,验证值变换后为九十九。全量均值会提前引入验证分布,改变训练处理参数。

03留出一组是否必然一半行?

输入 / 条件
a组100行,b组2行
输出 / 预期
不必然

提示 切分单位是组。

展开参考答案与推导

留出一个组可能得到一百行或两行,虽然组数都是一半。报告应同时列组数、行数和标签统计。

读完之后,独立完成

课后作业:自己做一遍

本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。

这次练什么

能重复生成组间不重叠的数据划分,检查训练与验证各包含哪些数据,并确认预处理只从训练集学习规则。

本篇重点加练

组独立性与拟合边界容易被数据流水线悄悄破坏,需要审计整个处理生命周期。

  • 分组独立性
  • 预处理拟合边界

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    为“预测新用户表现”和“预测老用户下一周表现”分别定义训练、验证、测试职责;说明相同数据表为什么可能需要不同切分。

    提交内容
    两份切分目标说明
    怎样算完成
    • 目标人群明确
    • 抽样单位与目标对应

    练到的知识点训练验证测试职责 · 评估目标与抽样单位 · 随机与分层切分

  2. 作业 02代码推演

    给出含 user_id、时间、模板 ID 的 12 条自建记录,手工设计两个合法留出方案,标出不能跨集合拆开的依赖。

    提交内容
    记录表与分组图
    怎样算完成
    • 时间顺序可检查
    • 重复来源身份保留

    练到的知识点分组独立性 · 时间顺序与未来信息 · 重复内容泄漏

综合应用

  1. 作业 03编程实践

    实现切分审计器,输入三份带用户、时间和文本摘要的 CSV,输出身份重叠、近重复候选及预处理拟合来源;用注入的泄漏样本验证。

    提交内容
    CPU 审计程序及样例报告
    怎样算完成
    • 检测规则可解释
    • 异常位置可定位

    练到的知识点分组独立性 · 预处理拟合边界 · 重复内容泄漏 · 切分审计与可重复性

  2. 作业 04排错实验

    构造随时间漂移的单特征数据,对比用全部数据和仅训练段拟合预处理统计量的评估过程;不得用最终测试结果挑方案。

    提交内容
    实验记录与数据流程图
    怎样算完成
    • 统计量来源可追踪
    • 漂移与泄漏区别明确

    练到的知识点时间顺序与未来信息 · 预处理拟合边界

  3. 作业 05面试场景

    稀有类别只有两个来源组,团队要求每个集合都包含该类别且组完全隔离。审阅可行性,提出报告与实验目标的修订方案。

    提交内容
    约束审查与备选协议
    怎样算完成
    • 冲突约束不被隐藏
    • 修改后的推断目标清楚

    练到的知识点评估目标与抽样单位 · 随机与分层切分 · 分组独立性

  4. 作业 06设计评审

    同一用户的原文、摘要和增强改写进入三个表。设计能跨表追踪来源的切分契约,并标出标准化、词表与特征选择分别在哪里拟合。

    提交内容
    数据血缘图与拟合边界表
    怎样算完成
    • 派生记录继承来源身份
    • 每个拟合步骤有数据范围

    练到的知识点分组独立性 · 预处理拟合边界

  5. 作业 07编程实践

    构造六个用户组,给自写预处理器增加 fit 记录,执行两次候选模型比较;检查验证组是否意外参与拟合或来源组被拆开。

    提交内容
    CPU 审计脚本及调用日志
    怎样算完成
    • 每次 fit 输入可追踪
    • 分组约束逐次验证

    练到的知识点分组独立性 · 预处理拟合边界

  6. 作业 08面试场景

    一个词表不读取标签却由全部数据统计得到。分别评审部署时已知完整语料与预测未知新语料两种设定下的合法性。

    提交内容
    两种学习设定的审阅意见
    怎样算完成
    • 可用信息由任务设定声明
    • 无标签不自动等于无泄漏

    练到的知识点预处理拟合边界

进阶挑战 · 掌握前置后选做

  1. 作业 09设计评审

    为持续更新的数据集设计切分版本协议,要求删除请求、迟到记录与新模板不会悄悄改写旧实验;列出可验证的不变量。

    提交内容
    版本清单格式与审计用例
    怎样算完成
    • 旧结果可定位到快照
    • 变更影响可追溯

    练到的知识点训练验证测试职责 · 重复内容泄漏 · 切分审计与可重复性

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 跨域迁移

    把离线切分迁移到跨医院或跨设备预测:同一对象可能出现在多个来源中,单按来源切分是否足够?构造需要联合身份约束的案例。

    提交内容
    跨来源案例及审计规则
    怎样算完成
    • 对象身份与来源身份分开
    • 拟合数据限制随评估目标变化

    练到的知识点分组独立性 · 预处理拟合边界

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗