CS336这门课一路写下来,前面几个实验都在模型内部打转——注意力、层归一化、分布式训练,到Assignment 4突然要你把注意力放到训练集的源头:数据本身。我刚开始也觉得,去重不就是把重复行删掉吗?直到自己动手编数据流水线才发现,这一步直接决定模型会不会把训练集背下来,也关系到下游评测结果到底可不可信。这篇笔记不打算复述作业原文,只围绕精确哈希去重、MinHash近似去重、LSH参数调优和评估实验这四块,讲讲我是怎么理解、怎么实现、以及在反复跑实验过程中踩过的坑。如果你正在写CS336的Assignment 4,或者想在真实文本数据集上做一套可复现的清洗流程,这篇应该能用得上。
1. 这期作业和课程主线的关系:前三个作业练模型,第四个作业管数据质量
CS336的课名很长,核心就一句话:从零构建语言模型。前几期作业的轨迹是:用numpy自己写Transformer内部模块、训练一个小规模语言模型、再把训练扩展到多卡/分布式环境。一整套下来,你对"模型怎么算、怎么训"已经有了底。但真实的大语言模型训练工程里,决定最终效果的还有另一半——喂进去的数据长什么样。Assignment 4就是补上这一半:在最终训练之前,如何科学地去重。
1.1 为什么把Dedup作为课程的收官作业
数据去重其实是一项典型的"看起来简单、做起来全是细节"的任务。它可以做得极浅:读入每一行文本,算个哈希值,丢进集合里看看有没有出现过,完事。但放到现代语言模型训练场景里,它要进一步回答:完全相同的文本怎么去;相同网页加了时间戳、导航栏、广告尾巴之后变成的"近似重复文本"怎么去;以及训练集和验证集之间潜在的交叉重叠怎么防。CS336把它放进作业里,是因为它和前面作业关心的核心概念——过拟合、泛化、记忆——是强相关的。
我自己做的时候一个直观的感受是:前几次作业拼的是"在约束下把模块实现出来",而这次拼的是"先想清楚度量标准,再下手实现"。因为去重算法的效果不是单靠跑通代码就能看出来的,你必须设计一套评估方法,量化精确去重和近似去重各自抓到了多少重复文档,误伤了多少非重复文档。
1.2 重复样本如何偷偷拉低模型质量
语言模型训练的损失函数是预测下一个token,如果一个文本片段在训练集里出现两次,那它贡献的梯度就相当于别的样本的两倍。表面上这只是在改变采样分布,实际效果却很微妙:模型会把更多容量用在记住那些高频重复的片段上,而不是把钱花在泛化模式上。我在自己的小规模实验里观察过类似现象:训练集里某类重复文本占比提高后,模型在生成阶段明显更倾向于复述训练集中的片段,而不是做出基于语义的归纳。
更隐蔽的问题是评估集污染。如果测试文本或者下游任务的提示文本,它的近似版本出现在预训练语料里,那么模型在实际评估前就已经"见过答案",指标会虚高。这在学术研究和真实产品迭代里都是很危险的坑。因此,训练数据流水线里至少要有两道去重关:一道是针对训练语料内部,让语料本身不臃肿;另一道是把训练语料与评测集做交叉比对,把与评测集高相似的训练样本提前剔除。
1.3 这次作业真正想考核的底层能力
作业虽然给了脚手架代码,但核心算法逻辑需要自己补全。与其说它在考你会不会背MinHash论文,不如说它在考察四件事:理解不同粒度的哈希去重之间的差别;能把n-gram集合相似度问题转化为可计算的签名问题;能解释并实现LSH的band切分原理;最后,能设计实验,用Precision/Recall去证明你的实现不是拍脑袋写出来的。
所以这篇笔记的章节顺序,我也按这条理解链路往下走。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 精确去重:从逐行哈希到一套通用的文本规范化流程
精确去重是整个作业里最"诚实"的模块。它不搞近似,只负责回答一个简单问题:这条文档以前是不是出现过一模一样的?但你以为这就是一句 if text in seen: skip 吗?真这么想会漏掉很多看起来不同、实际完全一样的文本。
2.1 去重粒度决定了流水线的召回上限
常见的做法有三档:整篇文档级、段落级、句子级。CS336给出的数据通常是JSONL格式,每一行是一篇完整文档。最省事的做法是文档级去重——把整行的文本内容作为哈希输入。
但文档级去重有一个很容易被忽略的问题:如果文本内部包含不同版本的换行符、行尾空格、或者编码不一致,哪怕视觉上完全一样,哈希值也会不同。比如一个网页抓下来的正文里,某一段末尾有个不可见字符,另一份副本没有,这两个文本就变成"精确不重复"了。
所以做精确哈希之前,我强烈建议先做一次文本规范化。规范化这一步不是为算法服务,是为"召回率"服务。你规范得越多,能匹配上的重复文本就越多;但规范得过度,也可能把代码里大小写敏感的信息错误合并。文本去重的通用经验是:
- 统一换行符,
\r\n和\n都压成\n - 去掉首尾空白
- 对Unicode做NFKC规范化,避免全角半角差异
- 是否转小写要按数据场景定,英文Web文本一般可以转,代码数据不要转
- 是否压缩连续的空白字符(多个空格、多个空行),取决于你想让格式差异大到什么程度被忽略
我在实际实现时,把这些清洗操作包装成一个 normalize_text 函数,它返回的是规范化后的字符串。后续所有哈希都以这个规范化结果为输入。
2.2 基于集合的两阶段哈希和增量式哈希
拿到规范化文本之后,最简单的精确去重可以抽象成两步:
- 计算每一篇文档的哈希指纹,例如
hashlib.md5(text.encode("utf-8")).hexdigest()。 - 把所有指纹放进一个全局集合,如果当前指纹已存在,就判定为重复,否则保留这篇文档并加入集合。
