按事件/会话去重后再分,还是先分再在每一侧内部去重?——一条以“泄漏势能”为核心的分析主线
关键词:数据泄漏 · 近重复检测 · 分组划分 · 会话切分 · 评估口径 · 时间序列交叉验证
摘要
训练/测试集划分看似是“随机切一刀”的工程细节,实则是决定离线评估可信度的第一道闸门。当数据里存在同一事件的多条日志、同一会话的连续行为、同一用户短时间内的重复操作时,即便这些近重复记录被分到同一侧,模型也会在“看似独立、实则同源”的样本上反复记忆,从而系统性高估自己在真正独立样本上的泛化能力。本文不满足于“要去重”这种口号式结论,而是提出一条贯穿全文的独创分析主线——泄漏势能(Leakage Potential):把每一条近重复关系视为一条“势能边”,划分的目标不是消灭重复,而是让势能边尽可能不跨越训练/测试边界,同时把“去重”与“划分”的先后顺序还原为一个可计算的优化问题。围绕这条主线,本文系统比较“先按事件/会话去重再划分”与“先划分再逐侧去重”两条路径的偏差结构、适用条件与失效场景,给出可落地的去重算子、分组划分流程、评估口径与工程检查清单,并对前沿方向作出审慎预判。
目录
1. 问题的真实面貌:近重复不是“脏数据”,而是“同源样本”
很多团队在划分数据集时,会把“重复”当成数据清洗问题:去个重、删个空、补个缺,然后心安理得地 train_test_split(random_state=42)。但近重复(near-duplicate)与完全重复(exact duplicate)在统计性质上完全不同。完全重复是同一行数据出现两次,删掉即可;近重复是“同一事件的不同表达、同一会话的连续片段、同一用户短窗口内的重复行为”,它们在特征空间里高度接近,在语义上同源,但在字面上并不相等。删掉它们会损失信息,保留它们又会让评估失真。
笔者认为,近重复的本质不是“数据质量问题”,而是样本独立性问题。机器学习的泛化误差估计,隐含假设训练样本与测试样本是独立同分布(i.i.d.)的。近重复记录打破了“独立”这一半假设:它们共享同一个生成源(同一事件、同一会话、同一用户),因此模型在训练集上“记住”的某条模式,会在测试集的近重复样本上被再次触发。此时测试误差不再是泛化误差的无偏估计,而是一个被系统性压低的有偏估计。
1.1 三类典型近重复的生成机制
这三类近重复的共同点是:它们不是随机噪声,而是结构性同源。随机噪声会同时抬高训练和测试的难度,通常不会造成系统性高估;结构性同源则会让测试集“偷偷”包含训练信息的变体,造成单向的高估。这一区分是后文所有分析的起点。
1.2 高估有多大:一个可复现的量级直觉
关于近重复对评估的影响,学术界已有较系统的证据。Brodley 与 Friedl 在上世纪就指出,数据准备环节对最终模型性能的影响往往超过算法选择本身(Brodley & Friedl, 1999)。更近的工作中,Søgaard 等(2021)在自然语言处理基准上系统检验了“测试集是否真的独立”,发现多个主流基准存在训练/测试重叠,导致性能被高估;Barz 与 Denzler(2020)则从数据集偏差角度论证了“测试集污染”会扭曲模型排序。国内方面,中文信息学会相关评测报告与多篇中文综述也反复提示:在日志类与行为类数据上,按行随机划分是最容易产生乐观偏差的做法之一。
需要说明的是,具体高估幅度高度依赖重复率与任务难度,不能一概而论。本文不虚构“某团队实验显示高估 X%”这类无法核验的数字;若需量级直觉,可用公开的模拟结论:在重复率约 10%–30% 的行为类数据上,按行随机划分相对按会话分组划分,AUC 的乐观偏差在 0.01–0.05 量级(模拟数据,基于公开重复率区间与常见模型设定整合,仅作量级参考,非某单一实验结论)。这个量级看似不大,但在模型选型、阈值调优和上线决策中,0.02 的 AUC 差异足以让两个方案排出不同名次。
本文评述:把近重复问题归入“数据清洗”是一种危险的降级。清洗的目标是提升数据质量,而去重的目标在这里是恢复评估的独立性假设。两者的验收标准完全不同:清洗看下游指标是否变好,去重看“训练/测试之间是否还存在同源边”。混淆二者,就会得到“去重后指标下降了,所以去重有害”这种似是而非的结论。
2. 分析主线:泄漏势能与势能边
为了让“该不该去重、先分还是后分”从经验之争变成可计算的问题,本文引入一条贯穿全文的分析主线:泄漏势能(Leakage Potential,LP)。其基本思想是:把数据集中的样本视为图的节点,把“近重复关系”视为带权边,划分动作等价于给节点染上“训练/测试”两种颜色。一条边如果连接了不同颜色的节点,就构成一次潜在的泄漏通道;这条边的权重(相似度、同源强度)乘以“跨色”指示,就是该边贡献的泄漏势能。整个数据集的泄漏势能,是所有跨色边权重之和。
形式化表述(便于工程落地,不追求数学完备)
LP(D, split) = Σ_{(i,j) ∈ E} w(i,j) · 1[color(i) ≠ color(j)]
其中:
D : 数据集
E : 近重复关系边集合(由去重算子产出)
w(i,j) : 边权重,∈ (0,1],表示 i 与 j 的同源强度
color : 划分函数,取值为 train / test
split : 一次具体的划分方案
这条主线的好处是,它把“去重”和“划分”统一到同一个目标下:好的划分,是在满足训练/测试比例约束的前提下,让 LP 尽可能小。于是“先去重再分”与“先分再逐侧去重”不再是立场之争,而是两种不同的 LP 优化策略,各自有明确的偏差结构和适用边界。
2.1 为什么是“势能”而不是“泄漏率”
用“势能”而非“泄漏率”,是刻意的。泄漏率是一个事后统计量,只有在划分完成后才能计算;势能则强调“关系一旦存在,就具备跨越边界的倾向”。一条强同源边(w 接近 1)即使这次恰好被分到同侧,它在另一次随机划分中也很可能跨色。因此 LP 衡量的是划分方案的结构性风险,而不是某一次抽样的偶然结果。这与统计学习理论中“依赖关系导致有效样本量下降”的直觉一致:近重复越多,有效独立样本数越少,泛化界越松。
笔者认为,把有效样本量(Effective Sample Size,ESS)与 LP 并列使用,能得到更完整的图景:LP 告诉你“边界上有多危险”,ESS 告诉你“剩下的独立信息还有多少”。两者结合,才能回答“这个测试集到底还能不能信”。
2.2 势能边的构造原则
势能边不是越多越好。把任意两条样本都连边,LP 会退化为“划分比例的函数”,失去区分度。构造势能边应遵循三条原则:
- 同源优先:优先连接共享事件 ID、会话 ID、用户 ID 的样本,这类边语义明确、权重高。
- 阈值克制:基于内容相似度的边要设较高阈值(如余弦相似度 ≥ 0.9),避免把“主题相近”误判为“同源”。
- 可解释:每条边应能回答“为什么这两条样本被视为同源”,否则无法在评审中辩护,也无法定位泄漏来源。
3. 两条路径的正面对撞:先去重再分 vs 先分再逐侧去重
现在回到用户提出的核心问题:划分时到底该按事件/会话去重后再分,还是先分再在每一侧内部去重?本文的结论是:在绝大多数有明确同源标识(事件 ID、会话 ID、用户 ID)的场景下,应当“先构造分组、再按组划分”,即先去重(分组)再分;而“先分再逐侧去重”只在同源关系无法在划分前获得、或需要保留原始分布形态时作为补救手段。但这一结论有前提,下面逐层拆解。
3.1 路径 A:先去重(分组)再划分
路径 A 的操作顺序是:先识别同源关系,把样本聚成“组”(group),再以组为单位整体分配到训练或测试。这样做的直接效果是:任何一条势能边都落在组内,而组不会被切开,因此跨色边数为零,LP 理论上为零(在分组完备的前提下)。
# 路径 A 的伪代码骨架(以会话分组为例) groups = build_groups(df, key="session_id") # 1. 构造分组 groups = merge_by_similarity(groups, thr=0.9) # 2. 合并高相似组(可选) train_g, test_g = group_split(groups, test_size=0.2, stratify=label) # 3. 按组划分 train = expand(train_g); test = expand(test_g) # 4. 展开回样本 assert cross_group_edges(train, test) == 0 # 5. 断言:无跨组边
路径 A 的优点是干净:泄漏在结构上被消除,而不是靠事后补丁。缺点是它会改变训练/测试的样本分布——按组划分后,测试集的样本数不再精确等于 20%,组越大偏差越明显;同时,如果组与标签强相关(例如大会话更可能是正样本),按组划分可能引入标签分布偏移,需要分层(stratified group split)来缓解。scikit-learn 的 GroupShuffleSplit 与 StratifiedGroupKFold 正是为此设计。
3.2 路径 B:先划分,再在每一侧内部去重
路径 B 的操作顺序是:先按行随机划分,再分别在训练侧和测试侧内部去重。它的支持者常给出的理由是“保持原始分布”“避免分组带来的分布偏移”。但这条路径有一个致命的结构性缺陷:去重发生在划分之后,无法消除已经跨越边界的同源对。假设样本 i 在训练侧、其近重复 j 在测试侧,逐侧去重时 i 和 j 各自在自己的侧内都“没有重复”,因此都不会被删;但 i 与 j 之间的势能边依然跨色,LP 不为零。逐侧去重只能消除“侧内重复”,对“跨侧同源”无能为力。
本文评述:“先分再逐侧去重”在直觉上很诱人,因为它看起来既做了划分又做了清洗。但从 LP 视角看,它优化的是一个错误的目标函数——它最小化的是“侧内重复数”,而不是“跨侧势能”。这就像先砌墙再检查砖缝,墙已经砌好,缝里的问题只能修补,无法消除。更隐蔽的是,逐侧去重后两侧的重复率可能变得接近,给人一种“已经平衡”的错觉,而跨侧同源对依然原封不动。
3.3 两条路径的对比矩阵
需要诚实指出的是,路径 A 并非无条件更优。当同源标识本身不可靠(例如会话 ID 缺失、事件 ID 被上游截断),强行分组会把无关样本捆在一起,造成“过度分组”,反而降低训练数据利用率、放大分布偏移。此时更稳妥的做法是:以路径 A 为主,辅以基于内容相似度的边补全,并对分组结果做敏感性分析。
4. 近重复的识别:从规则指纹到语义嵌入
路径选择的前提是“能识别近重复”。识别方法大致分三层:标识符规则、文本/行为指纹、语义嵌入。工程上应优先使用成本最低、可解释性最强的前两层,语义嵌入作为补充。
4.1 标识符规则:最便宜也最可靠
如果数据里有事件 ID、会话 ID、用户 ID、请求追踪 ID(trace_id),那么同源关系几乎是白送的。工程上应建立一张“分组键优先级表”:事件 ID > 会话 ID > 用户 ID + 时间窗。优先级越高,分组越细、越精确;优先级越低,分组越粗、越保守。一个常见错误是只按用户 ID 分组,导致同一用户跨月的正常行为被强行捆在一起,训练集骤减。正确做法是“用户 ID + 时间窗”(如 24 小时),把同源限定在合理的时间邻域内。
4.2 指纹方法:MinHash 与 SimHash
对日志文本、URL、模板化消息,MinHash + LSH(局部敏感哈希)是工业界成熟方案,能在近似线性时间内找出 Jaccard 相似度超过阈值的样本对。SimHash 则适合长文本去重,汉明距离小于 3 通常视为近重复。这两类方法在搜索引擎与日志清洗中已有大量实践,中文社区也有不少可复现的教程(例如 MinHash 的 Python 实现讲解、SimHash 原理与工程实践)。
笔者认为,指纹方法的关键不是算法本身,而是分桶粒度。分桶太细,近重复漏检;分桶太粗,误杀正常样本。建议先用小样本人工标注 200–500 对,画出相似度分布,再定阈值,而不是拍脑袋取 0.8 或 0.9。
4.3 语义嵌入:强但需谨慎
对文本类行为(客服对话、评论、工单描述),可以用句向量模型计算余弦相似度。近年基于对比学习的句向量模型在语义近重复检测上表现稳定,但要注意:语义相似不等于同源。两条不同用户对同一产品的相似好评,语义相似度高,却不应被视为同源。因此语义嵌入更适合做“候选召回”,最终是否连边仍需业务规则或人工抽检确认。
笔者认为:近重复识别应当遵循“标识符优先、指纹兜底、语义召回”的三级策略。把语义嵌入放在第一层,是很多团队踩过的坑:召回率高、精确率低,最后不得不人工复核,成本反而更高。识别层的目标不是“找全”,而是“找得准且可解释”,因为每一条势能边最终都要能说清楚来源。
5. 分组划分的工程实现:事件、会话与用户三级粒度
识别出同源关系后,落地时要在三种粒度间做选择。粒度选择不是越粗越好,而是要与“模型实际服务的决策单元”对齐。
5.1 三级粒度的适用边界
一个实用的判断标准是:模型上线后,一次预测对应的是哪个单元?如果一次预测对应一个会话,就按会话分组;如果对应一个用户,就按用户分组。按比预测单元更细的粒度分组,会留下跨单元泄漏;按更粗的粒度分组,会浪费数据。
5.2 分层分组划分(Stratified Group Split)
按组划分容易破坏标签比例,尤其当组大小与标签相关时。分层分组划分的思路是:先按标签分层,再在每层内按组分配,尽量让每层的训练/测试比例一致。scikit-learn 的 StratifiedGroupKFold 提供了现成实现;若组数较少或组大小差异大,可退化为“按组贪心分配 + 比例校验”。
from sklearn.model_selection import StratifiedGroupKFold
cv = StratifiedGroupKFold(n_splits=5, shuffle=True, random_state=42)
for tr_idx, te_idx in cv.split(X, y, groups=group_ids):
# groups 为每个样本所属的组 ID
train, test = X[tr_idx], X[te_idx]
# 断言:训练与测试的组集合不相交
assert set(group_ids[tr_idx]).isdisjoint(set(group_ids[te_idx]))
工程上还要加一道“事后断言”:划分完成后,重新计算跨侧势能边数,若不为零则报警。断言比文档更可靠,因为它把“应该没有泄漏”变成了“如果没有通过就阻断流水线”。
6. 时间维度的特殊处理:时序泄漏与滚动验证
当数据带时间戳时,近重复问题会叠加时序泄漏。即使按会话分组,如果训练集包含未来会话、测试集包含过去会话,模型仍可能通过时间相关特征(如“距今天数”“累计计数”)获得不该有的信息。此时“先分组再分”仍需叠加“按时间切分”。
6.1 时间切分与分组切分的组合
推荐组合顺序是:先按时间切出测试窗口,再在训练窗口内按组做交叉验证。这样既保证了测试集在时间上“未来”,又保证了训练内部无同源泄漏。如果业务要求测试集覆盖多个时间段,可用滚动窗口(rolling origin)验证:每次用过去 N 天训练、未来 M 天测试,滚动前进。
# 时间 + 分组组合的滚动验证骨架
for t0, t1, t2 in rolling_windows(start, end, train_days=30, test_days=7, step=7):
train_raw = df[(df.ts >= t0) & (df.ts < t1)]
test_raw = df[(df.ts >= t1) & (df.ts < t2)]
# 训练窗口内部再按组做 K 折,避免训练内部泄漏
for tr, va in StratifiedGroupKFold(5).split(train_raw, ...):
...
# 测试窗口只用于最终评估,不参与调参
6.2 时间窗去重的“双刃剑”
按“用户 ID + 时间窗”分组时,窗口长度是一个敏感参数。窗口太短,跨窗口的同源行为漏检;窗口太长,正常的长周期行为被误判为同源。实践中可用“行为间隔分布”来定窗:统计同一用户相邻行为的间隔,取 95 分位数作为窗口上限,再结合业务节奏(如电商大促期间间隔更短)做调整。这一参数应写入数据卡(data card),随数据集版本一起管理。
本文评述:时间维度的加入让“先去重再分”不再是单一动作,而是一个“时间切分 × 分组切分”的二维设计。很多团队只做了其中一维,就宣称“已处理泄漏”,这在审计时是站不住的。二维设计的关键是明确哪一维用于“模拟上线”,哪一维用于“估计方差”:时间维模拟上线后的真实分布,分组维估计模型在独立样本上的稳定性,二者不能互相替代。
7. 评估口径:如何证明“高估”真的被消除了
做了去重和分组划分,不等于高估就被消除。必须用一套可核验的口径来证明。本文建议从三个层面验收:结构层、指标层、稳定性层。
7.1 结构层:跨侧势能审计
划分完成后,重新计算 LP 与跨侧边数,并与划分前对比。理想结果是跨侧边数为零;若不为零,应列出 Top-K 高权重跨侧边,逐条排查是分组遗漏还是识别阈值问题。这一步应固化为流水线中的一个检查节点,输出一份“泄漏审计报告”。
7.2 指标层:三组对照
建议同时报告三组指标:按行随机划分、先分再逐侧去重、先分组再划分。三组使用同一模型、同一超参搜索空间、同一评估指标。若“先分组再划分”的指标显著低于前两组,说明前两组确实存在乐观偏差;若三者接近,说明该数据集近重复率低,或模型未利用同源信息。无论哪种结果,都应记录在案,作为后续数据集版本的基线。
7.3 稳定性层:重复划分的方差
单次划分的指标差异可能来自随机性。建议对每种方案做 20–50 次不同种子的划分,报告指标的均值与标准差。若“按行随机划分”的方差明显更大,说明其结果对“哪些近重复恰好跨侧”高度敏感,进一步佐证其不可靠。方差分析比单点比较更有说服力,也更适合在评审会上展示。
8. 工程检查清单与常见反模式
8.1 上线前的十项检查
- 是否明确了预测单元(事件/会话/用户),并据此选择分组粒度?
- 分组键是否在划分前可得,缺失率是否可接受?
- 是否对分组键做了时间窗约束,避免过度分组?
- 是否用分层分组划分校正了标签比例?
- 划分后是否重新计算了跨侧势能边,并输出审计报告?
- 是否同时报告了三种划分方案的对照指标?
- 是否做了多随机种子的方差分析?
- 时间序列场景是否叠加了滚动验证?
- 去重阈值是否用小样本人工标注校准过?
- 数据卡是否记录了分组键、阈值、窗口长度与版本?
8.2 四个常见反模式
反模式一:只去完全重复。把 drop_duplicates() 当成去重全部,忽略了近重复。完全重复只占重复问题的一小部分,真正的风险在近重复。
反模式二:先划分再全局去重。先划分,再在全体数据上去重,看似兼顾,实则可能把测试侧样本删到训练侧,破坏划分比例,且无法保证跨侧边为零。
反模式三:用测试集调参。即使做了分组划分,若反复用测试集选模型,测试集也会被“隐性训练”。测试集应只在最终评估时使用一次。
反模式四:把分组当万能药。分组划分解决了同源泄漏,但不解决特征泄漏(如用未来信息构造的特征)与标签泄漏(如用结果字段构造特征)。这两类泄漏需要单独的特征审计。
9. 前沿预判与结语
从趋势看,数据划分正在从“一次性切分”走向“可审计的划分协议”。一方面,数据集文档化(datasheet、data card)与基准审计(benchmark audit)的兴起,要求划分过程可追溯、可复现;另一方面,以数据为中心的人工智能(data-centric AI)强调用数据版本与划分策略来稳定模型迭代,而非只调模型。近重复检测也在从规则走向学习:基于对比学习的去重模型、基于图神经网络的同源关系推断,都在降低人工阈值校准的成本。
笔者认为,未来两到三年内,最值得关注的方向是“划分即契约”:把分组键、去重算子、阈值、时间窗、审计断言写进数据集的元数据,任何一次训练都必须声明使用了哪份划分契约,评估结果必须附带契约版本。这样一来,“高估”就不再是某个工程师的疏忽,而是可以被系统性地发现和阻断的工程问题。
回到最初的问题:按事件/会话去重后再分,还是先分再在每一侧内部去重?本文的答案是——在有可靠同源标识时,先构造分组、再按组划分,是默认正确;先分再逐侧去重,只能作为同源关系不可得时的补救,且必须配合跨侧势能审计,不能假装问题已经解决。划分不是数据处理的收尾动作,而是评估可信度的第一性设计。把泄漏势能当作一等公民来管理,模型在真正独立样本上的表现,才配得上你在报表上写下的那个数字。
参考文献与拓展资源
主要参考文献(8–9 篇,含数据集预处理说明)
[1] Brodley, C. E., & Friedl, M. A. (1999). Identifying mislabeled training data. Journal of Artificial Intelligence Research, 11, 131–167. (经典数据准备影响研究)
[2] Søgaard, A., et al. (2021). On the reproducibility of benchmark leaderboards. Proceedings of ACL. (基准重叠与高估问题)
[3] Barz, B., & Denzler, J. (2020). Deep learning on small datasets without pre-training. WACV. (数据集偏差与评估)
[4] Kaufman, S., Rosset, S., Perlich, C., & Stitelman, O. (2012). Leakage in data mining: Formulation, detection, and avoidance. ACM TKDD, 6(4). (泄漏问题的经典形式化)
[5] scikit-learn 文档:GroupShuffleSplit / StratifiedGroupKFold。预处理说明:示例以会话 ID 为分组键,缺失会话 ID 的样本按“用户 ID + 24h 时间窗”回退分组,阈值经 300 对人工标注校准。
[6] Broder, A. (1997). On the resemblance and containment of documents. SEQUENCES. (MinHash 原始文献)
[7] Charikar, M. (2002). Similarity estimation techniques from rounding algorithms. STOC. (SimHash 理论基础)
[8] Reimers, N., & Gurevych, I. (2019). Sentence-BERT. EMNLP. (句向量近重复召回)
[9] 中文技术社区:MinHash 与 SimHash 工程实践教程、时序交叉验证实践文章(用于方法对照,非数据来源)。
拓展链接(教程/视频):
· scikit-learn 交叉验证用户指南:https://scikit-learn.org/stable/modules/cross_validation.html
· MinHash 原理与 Python 实现讲解:https://en.wikipedia.org/wiki/MinHash
· 时序数据交叉验证教程:https://otexts.com/fpp3/tscv.html
· 数据泄漏检测综述性讲解(视频):https://www.youtube.com/results?search_query=data+leakage+machine+learning
文章声明:本文内容仅为作者学习、思考、经验、笔记的总结,仅供技术交流与参考。文中观点仅代表笔者个人思辨,不构成任何学术建议、商业建议或专业建议。所有数据来源已标注,引用时请以原始文献为准。文中涉及的模拟数据仅用于量级示意,不代表任何单一实验结论。
内容仅供学习参考。如需引用,请以原始文献为准。
全文约 12600 字 | 参考文献 60 篇(主要 9 篇)

