读完这篇,你能做什么
能生成稳定文档指纹,计算近重复候选,保留来源记录,并说明跨集合污染检查的盲区。
学习理由、准备要求与知识点
为什么学它
重复内容会改变模型学到的数据比例;评测内容混进训练数据还可能让分数虚高。先识别文档与来源,才能解释提升来自哪里。
先读这些内容
知识点清单
- 原始字节与规范化文本的双重身份
- SHA-256 指纹与碰撞复核
- 词片段集合与 Jaccard 相似度
- 阈值图的连通分量
- 确定性代表文档选择
- 组级划分与评测集隔离
- 污染候选人工复核
- 数据版本与删除清单
运行前先确认Python 3.14 标准库与 Java17;只处理内置小语料。源码基线 CPython v3.14.0;近重复算法为原创教学模型,不复现论文的大规模系统。
01先弄懂它是什么
同一批文本为什么这次训练和上次效果不同?要回答这个问题,必须知道每条语料从哪里来、后来被怎样处理过。每条记录至少要有稳定文档编号、来源、获取时间、原始内容摘要、清洗规则版本和可用范围。原文与清洗后的文本应使用不同身份,否则规则一变,同一个编号就可能悄悄对应另一份输入。记录了来源,也仍需另外确认具体的收集和使用条件。
去重前先说清楚“重复”指什么。字节完全相同才是精确重复;删掉多余空白后相同,只能说在这套规范化规则下相同。允许少量增删或替换的是近重复,要另外规定怎么比、相似到多少才算。本篇把每两个相邻词组成一个片段,用片段集合的交集大小除以并集大小,得到 Jaccard:完全相同为一,没有共同片段为零。这种比较不记录片段重复次数,也不能证明两篇文档意思相同。
检查评测污染,就是查训练材料里是否已经出现了评测题目、答案或非常接近的改写。简单指纹只能找出它能识别的匹配,没有命中也不能证明完全干净。划分普通训练集和验证集时,要把一组近重复文档放在同一边,避免相似副本分散到两边。使用独立的外部评测集时,先固定评测内容,再检查候选训练材料与它的重叠,并留下命中证据;不要反复根据测试成绩调整清洗阈值。
文中这几个词,先认一认
- Shingle
- 连续几个词组成的小片段。本例取相邻两个词,并用元组保存,避免拼接字符串后分不清原来的边界。
- 连通分量
- 用相似关系连在一起的一组文档。可能甲像乙、乙像丙,但甲和丙本身并没有达到相似阈值。
- 污染候选
- 与已经固定的评测内容出现可解释重叠的训练样本,需要继续按任务和重叠类型检查;它只是待核查对象。
02看看它怎么工作
先保留原文,再为具体文本类型选择保守清洗。新闻正文可以折叠空白,Python代码却不能随意删除缩进;数字、大小写、标点也可能是任务标签的一部分。实验故意只折叠空白,不做大小写或数字替换。指纹输入统一使用 UTF-8,并将清洗规则版本写入清单。散列值用来定位候选,删除前仍比较规范化文本,以处理碰撞以及规则实现错误。
近重复检测先构造片段集合,再计算交并比。短于片段宽度的文档不能都变成空集合,否则大量不相关短文本会被当作相同;本例给非空短文本保留完整词元组,空文档在进入流程时拒绝。阈值只是一项配置,应在独立标注的小样本上检查误删与漏检。对于中文,分词规则同样属于版本化输入,不能直接把按空格分词的英文实验当成中文处理方案。
这里的 Java 程序把文档两两比较:相似度达到阈值,就把两篇连起来,再用并查集把相连的文档归成一组。每组选输入下标最小的文档作代表,因此输入顺序不变时,重跑会选到同一个代表。这种直接比较适合小规模核验;文本很多时,通常先找可能相似的候选,再准确计算相似度。
MinHash 或局部敏感哈希可以减少候选查找的成本,但这类概率方法可能漏掉匹配,需要检查漏检情况。论文中的去重收益还取决于具体实验设置,不能当成任何领域都会得到的提升比例。
先跑通一个最小例子
保存为 experiment.py,用 Python3.14 运行。内置英文短文只验证处理规则和污染命中逻辑,不代表真实语料质量。
展开收起代码 · experiment.py
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import hashlib
def normalize(text):
# 只折叠空白,不改大小写和数字语义
value = " ".join(text.split())
if not value:
raise ValueError("empty document")
return value
def fingerprint(text):
return hashlib.sha256(normalize(text).encode("utf-8")).hexdigest()
def shingles(text, width=2):
if width <= 0:
raise ValueError("width")
words = normalize(text).split()
# 短文本保留其完整词元组,不能统一变成空集合
if len(words) < width:
return {tuple(words)}
return {tuple(words[i:i+width]) for i in range(len(words)-width+1)}
def similarity(a, b):
left, right = shingles(a), shingles(b)
return len(left & right) / len(left | right)
docs = ["a b c", " a b c ", "a b d", "x y z"]
assert fingerprint(docs[0]) == fingerprint(docs[1])
assert normalize("x 1") != normalize("x 2")
assert abs(similarity("a b c", "a b d") - 1/3) < 1e-12
assert similarity("yes", "no") == 0
assert similarity("yes", "yes") == 1
# 指纹只是候选索引,重复删除仍比较规范化文本
seen, kept = {}, []
for index, doc in enumerate(docs):
value, key = normalize(doc), fingerprint(doc)
if value not in seen.setdefault(key, []):
kept.append(index)
seen[key].append(value)
assert kept == [0, 2, 3]
test = ["a b c", "new benchmark"]
hits = [(i, j) for i in kept for j, q in enumerate(test)
if similarity(docs[i], q) >= 0.9]
assert hits == [(0, 0)]
try:
normalize(" ")
except ValueError:
pass
else:
raise AssertionError("empty text must fail")
print("8 corpus identity and contamination checks passed")
核对运行结果输出 8 corpus identity and contamination checks passed;重复记录保留编号0,冻结评测命中(0,0)。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["原文与来源清单"] --> B["版本化规范化"]
B --> C["指纹定位与内容复核"]
C --> D["片段候选与相似度"]
D --> E["家族分组和稳定代表"]
E --> F["冻结评测重叠检查"]
F --> G["训练清单与删除证据"]按顺序看,每一步做了什么
- 固定文档编号、规则版本和冻结评测内容。
- 拒绝空文档,分别保存原始摘要与规范化摘要。
- 复核精确重复,按片段相似度构造近重复家族。
- 按家族划分普通集合,筛查外部评测重叠并输出证据。
03对照实现代码
CPython v3.14.0 · __get_openssl_constructor;__get_builtin_constructor;file_digest
下例是使用真实 hashlib 接口的原创指纹模型,不是 SHA-256 内核实现,也不是论文去重工具源码。
import hashlib
def identity(raw):
# 原始身份用于回溯,不让清洗规则覆盖历史
original = hashlib.sha256(raw.encode("utf-8")).hexdigest()
# 规范化规则必须与语料类型匹配并单独版本化
normalized = " ".join(raw.split())
# 指纹相等之后仍可比较规范化正文确认重复
cleaned = hashlib.sha256(normalized.encode("utf-8")).hexdigest()
return {"raw": original, "clean": cleaned, "rule": "space-v1"}
命名构造器按运行环境连接到可用的摘要实现;Python接口统一,并不意味着压缩轮函数由Python解释器执行。
SHA-256接收字节,因此编码属于数据身份协议。相同文本若采用不同编码,其摘要也可能不同。
流式update允许分块处理大文档,分块边界不改变连续字节流的摘要;它没有文本规范化功能。
指纹模型把原始与清洗身份分开,便于定位差异;它既不检测语义重复,也不能单独证明来源许可或评测独立性。
代码先调用谁,再调用谁
- 文本规则生成规范化字符串
- UTF-8编码后调用sha256构造器
- 摘要实现消费字节并返回hexdigest
04动手写一个例子
相似图的确定性家族归并
输入非空整数片段集合与阈值,返回每篇文档所属连通分量的最小下标。集合元素已由分词阶段无碰撞编码。
- 输入 1
[{1,2},{2,3},{3,4}], threshold=1/3- 输出 1
[0,0,0];两端相似度为0,仍通过中间文档相连
先独立定义相似关系,遍历每一对集合计算交集和并集。不能把输入顺序变化造成的保留差异误当成清洗收益。
每条合格边调用并查集合并,按树的秩连接并维护独立的最小下标字段。这样既能保持接近常数的归并开销,也能稳定选择家族代表。
最终压缩所有根,得到可用于组级划分的家族标签。该算法求的是连通分量,不是要求任意两篇都相似的团。
为什么这样写是对的处理任意边前缀后,并查集恰好表示这些边的连通分量;每个根的minimum字段等于该分量最小下标。
n为文档数,s为每个集合最大大小;哈希集合查询按期望常数计算。遍历文档对检查片段,路径压缩归并接近常数;最坏哈希行为不作常数保证。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.*;
public class Main {
static int root(int[] p, int x) {
// 路径压缩保持家族代表不变
return p[x] == x ? x : (p[x] = root(p, p[x]));
}
static int[] group(List<Set<Integer>> docs, double threshold) {
if (!Double.isFinite(threshold) || threshold <= 0 || threshold > 1)
throw new IllegalArgumentException("threshold");
int n = docs.size(); int[] p = new int[n], rank = new int[n], minimum = new int[n];
for (int i=0;i<n;i++) {
if(docs.get(i)==null || docs.get(i).isEmpty())
throw new IllegalArgumentException("empty");
p[i]=i; minimum[i]=i;
}
for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) {
int common=0;
for(int token:docs.get(i)) if(docs.get(j).contains(token)) common++;
long union=(long)docs.get(i).size()+docs.get(j).size()-common;
// 除法转浮点前用long形成并集大小,避免整数相加溢出
if((double)common/union >= threshold) {
int a=root(p,i), b=root(p,j);
if(a!=b){
if(rank[a]<rank[b]){int tmp=a;a=b;b=tmp;}
p[b]=a; minimum[a]=Math.min(minimum[a],minimum[b]);
if(rank[a]==rank[b])rank[a]++;
}
}
}
// 最小代表与树根分离,避免牺牲按秩归并的复杂度
int[] labels=new int[n];
for(int i=0;i<n;i++) labels[i]=minimum[root(p,i)];
return labels;
}
static void check(boolean ok){if(!ok) throw new AssertionError();}
public static void main(String[] args){
var chain=List.of(Set.of(1,2),Set.of(2,3),Set.of(3,4));
check(Arrays.equals(group(chain,1.0/3),new int[]{0,0,0}));
check(Arrays.equals(group(chain,0.5),new int[]{0,1,2}));
check(Arrays.equals(group(List.of(Set.of(1),Set.of(1)),1),new int[]{0,0}));
check(group(List.of(),1).length==0);
try {group(chain,Double.NaN); throw new AssertionError();}
catch(IllegalArgumentException expected){}
try {group(List.of(Set.of()),1); throw new AssertionError();}
catch(IllegalArgumentException expected){}
System.out.println("6 duplicate family checks passed");
}
}
这些特殊情况也要试一试
- 空集合拒绝,空文档列表返回空结果。
- 阈值必须有限且位于零到一之间,零不作为有效筛选阈值。
- 传递链可能合并相似度很低的两端,需要抽样检查过大分量。
进阶选读 · 候选检索与污染证据索引
场景两两比较无法处理上亿文档,需要有限资源下保留可解释的命中。
做法使用MinHash或倒排索引生成候选,准确复算相似度,再按稳定文档ID合并;记录候选策略、召回样本和保留原因,对巨型分量单独复核。
复杂度候选阶段依赖桶分布;极端模板文本仍可能形成平方数量候选,不能宣称普遍线性。
好处与代价更强归并降低重复但可能删除少数群体文本;需要按来源和长度分层统计误删。
05项目里怎么用
数据版本应由原始清单、清洗程序版本、参数和最终保留ID共同确定。只保存一个压缩文本包无法回答某篇内容为何进入或离开训练集。先做小规模可复核的规则,再扩展处理量;每个阶段统计文档数、字节数、Token数以及各来源占比,防止去重后分布悄悄偏移。
不要只看全局重复率。论坛模板、代码许可证头、数学公式和真正复制的正文应分别分析;同一片段频繁出现可能是格式规律,也可能是数据抓取错误。对污染候选保留位置与重叠片段,人工复核时区分题干、答案、通用常识和无关模板,报告命中与未覆盖策略。
外部评测内容固定后,每次修改训练清单,都用同一套规则重新检查重叠。比较不同训练结果时,也要使用同一套评测定义,并写清检查覆盖不到的翻译、语义改写或未知来源。删掉了哪些文档、为什么删除,都应留在记录里。每组保留哪篇代表文档也要有固定规则,例如按来源优先级和稳定文档编号排序,避免只因文件被读入的先后不同就换了代表。
容易踩的坑
- 用Python进程随机化hash作为跨运行数据指纹。
- 删除代码缩进或所有数字,制造并不存在的重复。
- 先随机切分近重复副本,再认为不同ID保证独立。
- 把零个指纹命中写成完全没有评测污染。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 规范化精确去重 | 快速且命中证据明确 | 漏掉局部复制与轻微改写 | 流水线第一层 |
| 片段近重复 | 能发现局部文本重用 | 阈值与分词影响误删 | 带人工抽样复核的扩展层 |
| 语义候选 | 覆盖改写方向 | 相近主题容易误判且证据较弱 | 额外污染筛查而非自动证明 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01为什么保存两个摘要?
展开参考答案与推导
原始摘要定位原始字节,清洗摘要定位规则下的训练输入,二者服务于不同的回溯问题。
02近重复是否具有传递性?
展开参考答案与推导
阈值相似度通常没有传递性;图的连通关系具有传递性,所以分量内不保证两两相似。
03为什么短文本不能统一表示为空集合?
展开参考答案与推导
空表示丢失了内容身份,会把不同短文的并集和交集都变成零,制造定义歧义与错误合并。
换个情况,还会处理吗
01去重后小语种比例显著下降怎么办?
展开参考答案与推导
按语言与来源抽样检查规则是否偏向模板或分词错误,分析误删后调整预处理并保留版本,不直接以整体重复率作质量结论。
02测试题含常见定义并命中训练段落,能立刻认定作弊吗?
展开参考答案与推导
不能;保留重叠位置,区分一般知识与题目答案泄漏,依据预先定义的污染标准报告。
03清洗运行顺序改变导致代表不同怎么办?
展开参考答案与推导
使用稳定文档ID和固定优先规则排序,将排序规则加入版本清单。
写代码试一试
01计算两组片段的相似度。
- 输入 / 条件
{1,2,3} 与 {2,3,4}- 输出 / 预期
0.5
提示 并集不要把公共元素计算两遍。
展开参考答案与推导
交集大小2,并集大小4,比例为二分之一。
02构造传递归并反例。
- 输入 / 条件
A={1,2},B={2,3},C={3,4},阈值1/3- 输出 / 预期
一个家族,但A与C相似度0
提示 分别计算三条候选边。
展开参考答案与推导
AB和BC各有一条公共片段、三个并集元素,形成链;AC没有公共元素。
03精确去重如何抵御指纹碰撞?
- 输入 / 条件
两个不同正文被分入同一个摘要桶- 输出 / 预期
保留两个不同正文
提示 摘要桶需要保存可复核内容。
展开参考答案与推导
桶内比较规范化正文,只删除正文相同的记录,或保存指向原文的可读取位置。
读完之后,独立完成
课后作业:自己做一遍
本篇共 12 道作业,先完成 9 道基础与综合任务,再按前置知识挑战进阶题。其中包含 2 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能生成稳定文档指纹,计算近重复候选,保留来源记录,并说明跨集合污染检查的盲区。
本篇重点加练
近重复阈值、传递分组与评测隔离彼此影响,需要检验规则改变对数据组成的后果。
- 词片段集合与 Jaccard 相似度
- 阈值图的连通分量
- 组级划分与评测集隔离
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
同一段文字存在换行、空格、编码与标点差异。定义原始身份和规范化身份,列出两者各能回答和不能回答的数据追溯问题。
- 提交内容
- 身份契约与文本样例
- 怎样算完成
- 原始数据可追溯
- 规范化规则有版本
练到的知识点原始字节与规范化文本的双重身份 · SHA-256 指纹与碰撞复核
- 作业 02代码推演
构造四篇短文及其词片段集合,手算两两 Jaccard 值,在两个自选阈值下画重复图并制定代表文档选择规则。
- 提交内容
- 集合、相似度表与分组图
- 怎样算完成
- 阈值包含规则明确
- 代表选择确定且可重放
练到的知识点词片段集合与 Jaccard 相似度 · 阈值图的连通分量 · 确定性代表文档选择
- 作业 03原理问答
比较相同词片段集合但重复次数不同的两篇文本,说明集合式 Jaccard 忽略什么;自拟需要计数信息与无需计数信息的两种去重目标。
- 提交内容
- 目标对照与文本案例
- 怎样算完成
- 集合和多重集合区分
- 指标选择对应具体目标
练到的知识点词片段集合与 Jaccard 相似度 · 阈值图的连通分量
综合应用
- 作业 04编程实践
实现 CPU 去重清单生成器,保留输入摘要、规则版本、代表映射与删除原因;将同一输入随机重排三次检查输出身份一致性。
- 提交内容
- 程序及三次清单
- 怎样算完成
- 摘要命中支持复核
- 删除与保留可解释
练到的知识点原始字节与规范化文本的双重身份 · SHA-256 指纹与碰撞复核 · 确定性代表文档选择 · 数据版本与删除清单
- 作业 05排错实验
构造由公共模板串联的重复候选链,检查直接按连通分量切分对保留集组成的影响;人工复核误报并保留理由。
- 提交内容
- 候选链与审计报告
- 怎样算完成
- 传递分组影响可量化
- 候选命中不等同污染定论
练到的知识点阈值图的连通分量 · 组级划分与评测集隔离 · 污染候选人工复核
- 作业 06面试场景
评测题与训练文档共享常见定义但解题信息不同。设计污染复核流程,区分逐字泄漏、模板相似和背景知识重叠。
- 提交内容
- 复核规则与判例表
- 怎样算完成
- 判断依据可追踪
- 争议样本有处理规则
练到的知识点词片段集合与 Jaccard 相似度 · 组级划分与评测集隔离 · 污染候选人工复核
- 作业 07编程实践
实现 CPU 重复图阈值扫描器,对六篇自建文本报告各阈值下的分组及跨训练/评测连接;保留合并发生的边。
- 提交内容
- 扫描程序与分组变化表
- 怎样算完成
- 阈值规则固定
- 泄漏路径可定位
练到的知识点阈值图的连通分量 · 组级划分与评测集隔离
- 作业 08设计评审
训练语料与评测材料共享来源但没有逐字重复。设计来源隔离与内容去重两道审计,说明各自能发现的依赖关系。
- 提交内容
- 来源图与双层审计规范
- 怎样算完成
- 检测范围分别声明
- 未命中不直接当无泄漏证明
练到的知识点组级划分与评测集隔离
- 作业 09排错实验
向语料加入同一公共页脚,再移除页脚,观察候选相似度和连通分组的变化;对 8 篇以内自造文本保存规范化前后记录。
- 提交内容
- 受控噪声实验
- 怎样算完成
- 仅改变公共片段
- 误合并原因可复核
练到的知识点词片段集合与 Jaccard 相似度 · 阈值图的连通分量
进阶挑战 · 掌握前置后选做
- 作业 10设计评审
设计数据删除请求后的实验影响分析:哪些训练快照、索引和指标需要重新生成?用三个版本的迷你数据集演示追踪。
- 提交内容
- 版本依赖图与演示清单
- 怎样算完成
- 删除传播范围明确
- 历史结果身份不被改写
练到的知识点组级划分与评测集隔离 · 污染候选人工复核 · 数据版本与删除清单
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 11思维拓展 · 跨域迁移
将语料去重迁移到医学报告或法律模板,统一格式本身具有意义时,哪些相似性应保留?构造任务相关的“该合并/不该合并”判例。
- 提交内容
- 领域判例与评价协议
- 怎样算完成
- 相似与信息等价分开
- 去重目标先于阈值选择
练到的知识点词片段集合与 Jaccard 相似度 · 组级划分与评测集隔离
- 作业 12思维拓展 · 反例构造
“每对跨集合文本相似度都低于阈值,就不存在来源泄漏”是否成立?构造内容变换或依赖链案例,并说明额外身份信息的作用。
- 提交内容
- 反例及可审计来源链
- 怎样算完成
- 反例满足给定相似度条件
- 新增证据与结论相关
练到的知识点阈值图的连通分量 · 组级划分与评测集隔离
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读参考文档英文
ACL2022 去重原论文 (新标签页)↗
阅读近重复与训练评测重叠的实验范围,不把其收益比例直接外推。
- 深入参考文档英文
CPython3.14.0 hashlib 源码 (新标签页)↗
确认摘要构造器与底层实现边界。
- 深入参考文档英文
Python3.14 hashlib 文档 (新标签页)↗
核对update、digest和字节输入语义。