大模型预训练过程中的 MinHash 学习笔记
大模型预训练过程中的 MinHash 学习笔记
背景:为什么要学这玩意儿?
之前在做大模型训练数据清洗的时候,遇到了一个很头疼的问题:10 亿条文本,怎么快速找出重复的?
一开始想得很简单,直接用 Python 的 set() 去重不就行了?结果发现:
- 内存直接炸了(10 亿个字符串放内存里,想想都恐怖)
- 而且我需要的不是 "完全相同" 的去重,而是 "差不多像" 的去重(比如两篇文章只是改了几个标点符号,也算重复)
后来组里的师兄提了一句:"你去看看 MinHash 和 LSH"。
当时我一脸懵逼,什么 Hash?什么 LSH?听起来像密码学...
然后花了一周时间啃论文、跑代码,终于搞明白了。这篇笔记记录一下我的理解路径,给后来的同学少走点弯路。
核心问题:什么是 "相似"?
在讲 MinHash 之前,得先理解一个概念:Jaccard 相似度。
假设有两个集合(比如两篇文章的词集合):
A = {"我", "喜欢", "吃", "苹果"}
B = {"我", "喜欢", "吃", "香蕉"}
它们的 Jaccard 相似度定义为:
Jaccard(A, B) = len(A & B) / len(A | B)
= 交集大小 / 并集大小
= 3 / 5 = 0.6
这个数字越接近 1,说明两个集合越像。
问题来了:如果我有 10 亿条文本,要两两计算 Jaccard,那计算量是 $O(n^2)$,根本算不完。
MinHash 的作用:把每个文本压缩成一个 "指纹"(一个短的数字数组),然后用这个指纹快速估算 Jaccard 相似度。
MinHash 的核心思想(用人话讲)
MinHash 的思路其实挺巧妙的,核心就两步:
1. 把文本变成 Shingle(滑动窗口)
不要直接用单词,而是用 n-gram(连续的 n 个字符)。
比如 "我喜欢吃苹果",如果用 3-gram:
shingles = {"我喜欢", "喜欢吃", "欢吃苹", "吃苹果"}
为什么要这么做?因为这样能捕捉到词序信息。
2. 对每个 Shingle 做多次哈希,取最小值
这一步是最魔幻的。
假设我们有 100 个不同的哈希函数 $h_1, h_2, ..., h_{100}$。
对于集合 A 的每个元素,都用这 100 个哈希函数算一遍,然后 每个哈希函数取最小的那个值。
最后得到一个长度为 100 的数组,这就是 A 的 MinHash 签名。
神奇之处:两个集合的 MinHash 签名越像,它们的 Jaccard 相似度就越高。
数学上可以证明:
P(minhash_A[i] == minhash_B[i]) = Jaccard(A, B)
也就是说,如果两个集合的 Jaccard 是 0.8,那么它们的 MinHash 签名在每个位置上相同的概率就是 80%。
代码实现(极简版)
我自己写了个最简单的 MinHash 实现,没用任何库(除了 hashlib):
import hashlib
def text_to_shingles(text, k=3):
"""把文本切成 k-gram"""
return set(text[i:i+k] for i in range(len(text) - k + 1))
def minhash_signature(shingles, num_hashes=100):
"""生成 MinHash 签名"""
signature = []
for seed in range(num_hashes):
min_hash = float('inf')
for shingle in shingles:
# 用 seed 作为盐值生成不同的哈希函数
h = int(hashlib.md5((str(seed) + shingle).encode()).hexdigest(), 16)
min_hash = min(min_hash, h)
signature.append(min_hash)
return signature
# 测试
text1 = "我喜欢吃苹果"
text2 = "我喜欢吃香蕉"
s1 = text_to_shingles(text1)
s2 = text_to_shingles(text2)
sig1 = minhash_signature(s1)
sig2 = minhash_signature(s2)
# 估算 Jaccard
similarity = sum(1 for a, b in zip(sig1, sig2) if a == b) / len(sig1)
print(f"估算的相似度: {similarity:.2f}")
这个代码跑起来应该能看到相似度在 0.6 左右(和真实的 Jaccard 差不多)。
踩过的坑
坑1:哈希函数太少
一开始我只用了 10 个哈希函数,结果发现估算误差特别大。
后来查资料发现,一般要用 100-200 个 哈希函数才能有比较稳定的估计。
坑2:Shingle 大小的选择
- 如果 k 太小(比如 k=1,就是单字),那 "我喜欢你" 和 "你喜欢我" 会被认为完全一样。
- 如果 k 太大(比如 k=10),那稍微改几个字就变成完全不同了。
- 经验值:中文用 k=3,英文用 k=5。
坑3:内存还是很大
虽然 MinHash 压缩了很多,但如果直接把 10 亿条的签名都存在内存里,还是不行。
所以后面还得配合 LSH (Locality Sensitive Hashing) 来做索引。
LSH 是另一个大坑,改天再写...
什么时候用 MinHash?
适合的场景:
- 文本去重(新闻、评论、爬虫数据)
- 抄袭检测(论文查重)
- 推荐系统(找相似用户/商品)
不适合的场景:
- 如果你只有几千条数据,直接暴力算 Jaccard 就行了,别折腾
- 如果你需要语义相似度(比如 "苹果好吃" 和 "这水果真甜"),MinHash 不行,得用 Embedding
总结
MinHash 的本质就是:用概率换空间,用哈希换速度。
它不追求 100% 准确,但能在 O(1) 时间内快速判断两个文本是不是 "大概像"。
这在大规模数据处理时简直是救命稻草。
如果你也在做数据清洗/去重相关的工作,强烈建议花点时间搞懂这个算法。
虽然学习曲线有点陡,但一旦理解了,你会发现很多看似不可能的问题都能优雅地解决。
Reference:
- 原始论文:Broder, A. Z. (1997). "On the resemblance and containment of documents"
- 我参考的教程:Stanford CS246 的课件(Google 搜 "Mining Massive Datasets" 就能找到)
写于北京某个麦当劳,边啃汉堡边写的。如果你也在做大模型预训练,咱们可以交流交流,wx:hsr03160316。

浙公网安备 33010602011771号