LLM 的大规模文本去重技术
LLM document 的去重在大规模场景要做好还是比较复杂。
两种思路,粗筛走 MinHashLSH,精筛走 Embedding 去重。
MinHashLSH 路线
朴素思想
- 直接对每个 doc 做 n-gram (然后把一个 doc 看作是多个 n-gram 的集合)
- 定义 K 个 Hash function,对每个 n-gram 做 hash,拿到 doc 到 hash 的集合
- 每个 hash function 对所有 n-gram 做 hash,取最小值,得到一个 length=k 的 minhash 集合
- LSH 分桶,k 维切成 b 个 band,每个 band 整体 hash 一次,对应一个 band_hash
- 按 band_idx 和 band_hash 进行 groupBy,相同的 band_hash 表示两个 doc 至少在一个 band 里相似,加入比较的 candidates,没有相同 band_hash 的不需要再处理了
- 对相同 band_hash 的 doc 做一个连通图处理,假设边是 (u,v),接下来通过多轮的连通处理,首先定一批节点,每个节点负责一批 doc_id,每一轮:
- (u,v) 的边发送给 u 和 v 的两个节点
- 每个节点内会产生新的连通,即新的 (u,v),同时每个节点的数据不断累积,doc_id 对应的簇(doc_id 集合)会越来越大
- 连通多轮之后(一般 2-3 轮就够了),做决策,哪些 doc_id 要留下
- 遍历原始数据,留下不重复的 doc_id
用 codex 做了一个流程图:
这里要注意一个点:最后每个 doc_id 等价于多个 band_hash 值的集合,所以判断两者 doc_id 重复是通过 Jaccard 相似度来判断。而通常为了计算性能考虑,不一定会把一个 doc_id 的所有连通图都打通来计算完整的 Jaccard,比如 BigCode(https://huggingface.co/blog/zh/dedup) 里只要有一个 band_hash 相同就认为这是重复的。所以这里会引入一个概率关系:
| 真实 Jaccard | 被判为候选重复的概率 |
|---|---|
| 0.50 | 2.4% |
| 0.60 | 14% |
| 0.70 | 51% |
| 0.80 | 94% |
| 0.85 | 99.6% |
Spark
Spark 是最简单直接,也是早期最多人用的方法。基于 (band_idx, band_hash) 进行 groupBy,一样的值放在一起比较,阿里云的 Serverless Spark 实现参考 https://help.aliyun.com/zh/emr/emr-serverless-spark/use-cases/minhash-lsh-based-large-scale-text-duplication-scheme,但是这个连通图涉及到数据 shuffle,不建议。
NeMo Curator(NVIDIA 开源,基于 Ray)
ref: https://docs.nvidia.com/nemo/curator/curate-text/process-data/deduplication/fuzzy
- 本质也是 follow 朴素思想,但是换成了 GPU 库,LSH 的时候每次只处理了 5 个 band(中间结果落 parquet)
- minhash 换成了 cuDF,连通图换成了 cuGraph 的多 GPU 弱连通算法(同时对重复的校验没有那么严格)
Data-Juicer 实现(Ray 对朴素思想的实现 + 热点优化)
- 遍历数据,算 band hash 然后先每个 actor 内自行合并一次,得到一个单机的 2 级连通图,让所有 doc_id 指向最小的 doc_id
- 按 doc_id 做一次 reshuffle,每个 actor 有自己的一批 doc_id,每个 edge 发送到两个 actor
- 每个 actor 经过多轮 reshuffle 后,会有一个 doc_id->簇 的映射,此时决定哪些是重复值
这种 case 对于 doc_id 簇过大的情况怎么办(比如 Licence Text):有一个 proxy 的思路(rebalance),大致思想是不要把完整簇放到一个节点里,每个节点放一个属于自己的小簇,再把每个小簇的根节点作为 proxy 连到真正的根节点,同时每次真正根节点的变化可能带来 actor 的通信。
SimHash
以文本为例:
- 对文本分词或提取特征。
- 给每个特征计算普通哈希。
- 根据词频、TF-IDF 等给特征赋权重。
- 将各特征的哈希按位加权投票。
- 每一位的总值大于 0 记为 1,否则记为 0,得到固定长度的 SimHash。
Embedding 路线
Kmeans
比较容易的是复用 Faiss 的方法(同时也支持 GPU),Faiss 里的 Kmeans 原理如下:
- 抽样数据,训一个 Kmeans Cluster,得到 k 个 cluster centroids
- 遍历旧数据,算 cluster,分发,在每一个 centroids 里做 embedding 去重(相当于是一个矩阵乘法)
- 如果 k 个 centroids 还是比较大,继续做 k 个 cluster(两次肯定是够了)
Embedding Similarity
另一个就是构建分布式的 Embedding Service,通过建索引的方式,常见索引有两类:
-
HNSW
-
IV