比Datasketch好两个数量级?用 Fast Similarity Sketch 优化大规模文本去重(附 C++ 源码)

比datasketch好两个数量级? 用 Fast Similarity Sketch 优化大规模文本去重(附 C++ 源码)

项目地址:https://github.com/pzcddm/FastSketchLSH

论文背景:Fast Similarity Sketching(arXiv:1704.04370v4,FOCS’17 扩展版)

最近在做大规模文本去重时,我们基于 Fast Similarity Sketching 做了一个 Python 去重包 fastsketchlsh,也拿它和 datasketchrensa 做了对比。
这篇文章主要讲三件事:传统 k-mins 为什么慢,FastSketch 为什么快,以及它和 LSH 搭配后在工程里怎么落地。


先说结论

  • 经典 k-mins / MinHash 用来估计 Jaccard,再配 banding LSH 做候选召回,是业界非常常见的近似去重路线。
  • datasketch 很常用,但它是纯 Python 实现,规模一大通常会慢;rensa 是 k-mins 路线里的 SOTA 工程实现(Rust),速度明显更好。
  • 在 sketch 这一核心阶段,FastSimilaritySketch 相比 datasketch 甚至能到 200x
  • Fast Similarity Sketching 的关键理论是:在保持 alignment + Chernoff 级别集中界的前提下,把 sketch 构造期望复杂度降到:

O ( n + k log ⁡ k ) O(n + k\log k) O(n+klogk)

其中 k 是 sketch size。


1. 大规模去重:常见的几条路线

做“去重”前先想清楚:你到底要去的是哪一种“重复”。

1.1 精确去重(Exactly Match)

最常见也最简单:

  • 统一规范化(大小写、空白、标点、HTML 清洗等)后做哈希(MD5/SHA/xxhash)。
  • 适用:完全重复、或你能把数据规整成“完全相同”的形式。

优点:快、准、实现简单。
缺点:对“近似重复”(少量增删改、顺序变化、模板文本)不鲁棒。

1.2 近似去重(本文主要讲:集合相似度 Jaccard)

文本(或 token 序列) -> shingle / n-gram -> 看成集合 -> Jaccard 相似度

J ( A , B ) = ∣ A ∩ B ∣ ∣ A ∪ B ∣ J(A,B)=\frac{|A\cap B|}{|A\cup B|} J(A,B)=∣A∪B∣∣A∩B∣​

一个“两个集合很相近”的直观例子:

  • A={a,b,c,d,e,f,g,h,i,j}(10 个元素)
  • B={a,b,c,d,e,f,g,h,i,x}(只把 j 换成 x
  • 交集 |A∩B|=9,并集 |A∪B|=11

所以:

J ( A , B ) = 9 11 ≈ 0.818 J(A,B)=\frac{9}{11}\approx 0.818 J(A,B)=119​≈0.818

这就是“很像但不完全相同”的典型情况。

然后用 sketch(压缩签名) 去近似估计相似度,再用 LSH 做候选召回。

这条路线的优点是:理论清晰、工程成熟、对“局部修改”的鲁棒性强;缺点是:当 token 集很大时,传统 MinHash 的计算量会飙升。

1.3 其他路线

  • SimHash(Hamming 距离)/ LSH on Hamming:在文本去重里常见问题是 FP 或 FN 偏高,阈值非常敏感。
  • Embedding + ANN(Faiss/HNSW):语义效果更强,但向量化、索引构建、检索的整体成本都更高,很多去重任务性价比并不占优。

2. 为什么 k-mins / MinHash + LSH 这么流行?

因为它解决了一个“又准又好用”的点:Jaccard 的无偏估计 + LSH 的候选召回。本文统一用 k 表示 sketch size(论文原文记为 t,即这里 k=t)。

2.1 k-mins / MinHash 的核心性质(和 Jaccard 的关系)

经典 MinHash 做法:用 k 个独立哈希函数(或 k 个随机置换),每一维取最小哈希值:

sig i ( A ) = min ⁡ a ∈ A h i ( a ) \text{sig}_i(A)=\min_{a\in A} h_i(a) sigi​(A)=a∈Amin​hi​(a)

然后用“逐维相等比例”估计 Jaccard:

J ^ ( A , B ) = 1 k ∑ i = 1 k 1 [ sig i ( A ) = sig i ( B ) ] \hat{J}(A,B)=\frac{1}{k}\sum_{i=1}^k \mathbf{1}[\text{sig}_i(A)=\text{sig}_i(B)] J^(A,B)=k1​i=1∑k​1[sigi​(A)=sigi​(B)]

关键结论:

Pr ⁡ [ sig i ( A ) = sig i ( B ) ] = J ( A , B ) \Pr[\text{sig}_i(A)=\text{sig}_i(B)] = J(A,B) Pr[sigi​(A)=sigi​(B)]=J(A,B)

直觉上:把 A ∪ B 按哈希值排序,最小元素落在交集 A ∩ B 的概率就是 Jaccard。

一个最小例子(帮助理解 kmins 在做什么):

  • k=4,集合 An=5 个 token:{a,b,c,d,e}
  • 用 4 个哈希函数分别扫这 5 个 token,每个函数只保留最小值:
    • h1(A) 最小值是 5
    • h2(A) 最小值是 3
    • h3(A) 最小值是 4
    • h4(A) 最小值是 2
  • S(A)=[5,3,4,2]
  • S(B)=[5,9,4,2],则 4 维里有 3 维相等,因此估计值为:

J ^ ( A , B ) = 3 4 = 0.75 \hat{J}(A,B)=\frac{3}{4}=0.75 J^(A,B)=43​=0.75

这个例子里你会直接看到:为了得到 4 维签名,我们确实做了“4 个哈希函数 x 5 个元素”的扫描。

2.2 为什么能和 LSH 结合:banding 概率曲线

把长度为 k 的签名切成 b 个 band,每个 band r 行(k = b * r)。如果两个集合在任意一个 band 完全相同,就作为候选对。

经典结论:若单行相等概率是 s(MinHash 情况下 s = J),候选概率为:

P ( candidate ) = 1 − ( 1 − s r ) b P(\text{candidate}) = 1-(1-s^r)^b P(candidate)=1−(1−sr)b

这就是常说的 S-curve,可通过调 b,r 在阈值附近做“软门槛”。


3. 为什么很多工程里 datasketch / 传统 MinHash 仍然慢?

这里只从工程和复杂度两个角度说结论,不展开底层运行时细节:

  1. datasketch 纯 Python 实现,工程上更慢
    在大规模循环和哈希更新上,纯 Python 的吞吐很难拉满。
  2. k-mins 本身复杂度不低
    经典 MinHash 复杂度是:

O ( k ⋅ n ) O(k\cdot n) O(k⋅n)

当每条文本 token 很多、或 n-gram 数量很大时,CPU 压力会明显上升。

  1. rensa 用 Rust 做到了很快,但仍然是 k-mins 路线
    rensa 是当前 k-mins 路线的 SOTA 工程基线;但算法工作量结构仍然继承了 k-mins。

所以实际痛点是:datasketch 慢,rensa 快但仍受 k-mins 复杂度结构约束,我们希望继续降复杂度。

3.1 一个关键问题:能不能不做 k 次全量扫描?

k-mins 的工作方式,本质上是“每个元素都过 k 个哈希维度”:

work kmins ∼ k ⋅ n \text{work}_{\text{kmins}} \sim k\cdot n workkmins​∼k⋅n

所以会自然出现一个问题:能不能只用一个(或期望常数个)哈希轮次,完成同样长度为 k 的 sketch?

Fast Similarity Sketching 的回答是:不再固定做 k 次全扫描,而是按轮次更新全部 k 个桶,桶填满就提前停止。
如果记轮次数为 R,则总工作量是:

O ( R ⋅ n ) O(R\cdot n) O(R⋅n)

而论文给出:

E [ R ] = O ⁣ ( 1 + k log ⁡ k n ) \mathbb{E}[R]=O\!\left(1+\frac{k\log k}{n}\right) E[R]=O(1+nklogk​)

于是得到:

E [ work ] = O ( n + k log ⁡ k ) \mathbb{E}[\text{work}] = O(n+k\log k) E[work]=O(n+klogk)

拿一组数字看量级差异:

  • n=10,000k=256
  • k-mins:大致是 k*n=2,560,000 次“元素-哈希维度”处理。
  • FastSketch:是 R*n。如果 R=2,就是约 20,000 次;如果 R=3,约 30,000 次。

FastSketch 的关键变化,不在单步操作,而在于把“固定做 k 轮”改成“桶填满就停”。

这也是它能把复杂度从

O ( k ⋅ n ) O(k\cdot n) O(k⋅n)

降到

O ( n + k log ⁡ k ) O(n+k\log k) O(n+klogk)

的原因。


4. Fast Similarity Sketching:更快且保留 MinHash 级别性质

本仓库的 FastSketchLSH,核心思想来自论文 Fast Similarity Sketching(arXiv:1704.04370v4)。核心点是:在保持 MinHash 级别统计性质的同时,把“k 次全扫”改成“少量轮次 + 提前停止”。

4.0 先看 FastSketch 伪代码(工程版)

Input : set A, sketch size k, regular rounds 2k # paper notation: 2t Output: sketch S[1..k] Initialize S[i] = EMPTY for i = 1..k # regular rounds for r = 1..2k: for each token a in A: h = hash(r, a) bin = 1 + (h mod k) key = (r, h) # 保证早轮次优先、同轮按哈希最小 S[bin] = min(S[bin], key) if all bins filled: break # fallback rounds: only for empty bins while exists empty bin: r = r + 1 for each token a in A: h = hash(r, a) bin = 1 + (h mod k) if S[bin] is EMPTY: key = (r, h) S[bin] = min(S[bin], key) return S 

注意:上面这段是工程实现写法(把轮次 r 作为哈希输入的一部分),更接近论文里说的 practical implementation。
如果严格按论文 Algorithm 1 的原始定义,会写成 2t 个哈希函数:前 t 个把元素映射到区间 [i, i+1),后 t 个映射到 [i-t, i-t+1) 做补位。两种写法目标一致:快速填满桶,并保持无偏估计性质。

4.1 先把符号讲清楚:什么是 S(A)[i]、alignment、Chernoff-style concentration

先给几个前置定义(这几个定义后面都会用到):

  • A,B:两条文本各自 tokenize 后得到的集合。
  • k:sketch size(签名维度)。
  • S(A), S(B)A,Bk 维 sketch 向量。
  • S(A)[i], S(B)[i]:第 i 维(第 i 个桶)上的 sketch 值。直观上它是“该维度里保留下来的最小 key/最小哈希值”,不是原始 token。

论文把“第 i 维是否匹配”定义为:

X i = 1 [ S ( A ) [ i ] = S ( B ) [ i ] ] X_i=\mathbf{1}[S(A)[i]=S(B)[i]] Xi​=1[S(A)[i]=S(B)[i]]

并令总匹配维度数:

X = ∑ i = 1 k X i X=\sum_{i=1}^{k} X_i X=i=1∑k​Xi​

有了这两个量,就能解释两个核心性质:

  1. alignment(对齐)
    对两个集合来说,第 i 维比较的是“同一套随机机制下的同一维实验”,而不是乱序对比。
    这保证了逐维比较有意义,并满足:

E [ X i ] = J ( A , B ) \mathbb{E}[X_i]=J(A,B) E[Xi​]=J(A,B)

下面给两条更容易读的证明思路:

路线 A:按轮次分解(第一轮、第二轮、…、兜底轮)

固定一个坐标 i。令并集为 U = A ∪ B,交集大小为 c = |A ∩ B|,并集大小为 m = |U|,于是有:

J = c m J=\frac{c}{m} J=mc​

  • 定义 E_r:坐标 i 在第 r 轮(r 从 1 到 2k,论文写作 2t)首次命中。
  • 定义 E*:前 2k 轮都未命中,最后由兜底步骤填充。
  • 事件族 {E_1, ..., E_{2k}, E*} 两两互斥且完备:

∑ r = 1 2 k Pr ⁡ [ E r ] + Pr ⁡ [ E ⋆ ] = 1 \sum_{r=1}^{2k}\Pr[E_r]+\Pr[E_\star]=1 r=1∑2k​Pr[Er​]+Pr[E⋆​]=1

在任一事件 EE_rE*)下,坐标 i 的 winning 元素记为 W_i
由于哈希/分桶过程对 U 中每个元素是同分布、与“是否属于交集”无关,W_i 落到交集的条件概率恒为:

Pr ⁡ [ X i = 1 ∣ E ] = Pr ⁡ [ W i ∈ A ∩ B ∣ E ] = c m = J \Pr[X_i=1\mid E]=\Pr[W_i\in A\cap B\mid E]=\frac{c}{m}=J Pr[Xi​=1∣E]=Pr[Wi​∈A∩B∣E]=mc​=J

按全概率公式:

Pr ⁡ [ X i = 1 ] = ∑ r = 1 2 k Pr ⁡ [ E r ] Pr ⁡ [ X i = 1 ∣ E r ] + Pr ⁡ [ E ⋆ ] Pr ⁡ [ X i = 1 ∣ E ⋆ ] = ( ∑ r = 1 2 k Pr ⁡ [ E r ] + Pr ⁡ [ E ⋆ ] ) J = J \Pr[X_i=1] =\sum_{r=1}^{2k}\Pr[E_r]\Pr[X_i=1\mid E_r]+\Pr[E_\star]\Pr[X_i=1\mid E_\star] =\left(\sum_{r=1}^{2k}\Pr[E_r]+\Pr[E_\star]\right)J =J Pr[Xi​=1]=r=1∑2k​Pr[Er​]Pr[Xi​=1∣Er​]+Pr[E⋆​]Pr[Xi​=1∣E⋆​]=(r=1∑2k​Pr[Er​]+Pr[E⋆​])J=J

因此:

E [ X i ] = Pr ⁡ [ X i = 1 ] = J \mathbb{E}[X_i]=\Pr[X_i=1]=J E[Xi​]=Pr[Xi​=1]=J

轮次上限 2k(论文记作 2t)只影响“首次命中发生在第几轮”的分布,不改变期望。

路线 B:排列对称性

同样固定坐标 i,令 W_i 为最终写入该坐标的元素。
FastSketch 的规则是 label-oblivious 的:只看哈希值或键值,不看元素身份。对 U 上任意置换 σ,都有分布不变性:

W i = d σ ( W i ) W_i \overset{d}{=} \sigma(W_i) Wi​=dσ(Wi​)

所以 U 中每个元素成为 winner 的概率相同。设该概率为 p,则

∑ x ∈ U Pr ⁡ [ W i = x ] = 1 ⇒ m p = 1 ⇒ p = 1 m \sum_{x\in U}\Pr[W_i=x]=1 \Rightarrow mp=1 \Rightarrow p=\frac{1}{m} x∈U∑​Pr[Wi​=x]=1⇒mp=1⇒p=m1​

于是

Pr ⁡ [ X i = 1 ] = Pr ⁡ [ W i ∈ A ∩ B ] = ∑ x ∈ A ∩ B Pr ⁡ [ W i = x ] = c ⋅ 1 m = c m = J \Pr[X_i=1] =\Pr[W_i\in A\cap B] =\sum_{x\in A\cap B}\Pr[W_i=x] =c\cdot \frac{1}{m} =\frac{c}{m} =J Pr[Xi​=1]=Pr[Wi​∈A∩B]=x∈A∩B∑​Pr[Wi​=x]=c⋅m1​=mc​=J

这一路的结论也是每一维的期望仍然等于 J
这里更适合作为直觉解释:严格证明还要结合算法构造细节。关键点是补位机制没有破坏均匀性,所以不会因为“空桶修复”引入偏差。

  1. Chernoff-style concentration(Chernoff 风格集中)
    直观上,X 不只是“期望正确”,还会高概率贴近 kJ,偏差概率随 k 指数下降。可写成典型形式:

Pr ⁡ ( ∣ X − k J ∣ > ϵ k J ) ≤ 2 e − c ϵ 2 k J , 0 < ϵ ≤ 1 \Pr\big(|X-kJ|>\epsilon kJ\big)\le 2e^{-c\epsilon^2 kJ},\quad 0<\epsilon\le 1 Pr(∣X−kJ∣>ϵkJ)≤2e−cϵ2kJ,0<ϵ≤1

其中 epsilon相对误差阈值:例如 epsilon = 0.1 表示“偏离超过 10% × kJ”。
c>0 是常数(与 k, J, epsilon 无关)。

这对 LSH 很关键:你按阈值做召回时,结果更稳定,不容易因为采样噪声大幅抖动。

4.2 核心算法(Similarity-S)

下面统一用 k 表示 sketch size:

  • 每一轮只用一组哈希(可理解为一个 seed 下的哈希映射),但这一轮会同时尝试更新全部 k 个桶。
  • 维护 k 个桶,每个桶保留一个最小 key。
  • 反复做“随机分桶 + 取最小”,直到所有桶被填满。
  • 若仍有空桶,用额外轮次补空,保证每个维度都定义良好。

一个直观例子(为什么它常常很快):

  • k=256,一条文本有 n=1200 个 token。
  • 第 1 轮里,1200 个 token 会被随机映射到 256 个桶并更新最小 key。
  • n 相对 k 足够大时,绝大多数桶会在第一轮就被命中并填满,此时直接停止,R≈1
  • 从仓库里的实测看,当 token 超过约 700 时,很多样本第一轮就能填满全部桶。

4.3 关键复杂度结论(Lemma 1)

论文给出的期望复杂度:

O ( n + k log ⁡ k ) O(n + k\log k) O(n+klogk)

和传统 MinHash 对比:

O ( k ⋅ n ) → O ( n + k log ⁡ k ) O(k\cdot n) \rightarrow O(n + k\log k) O(k⋅n)→O(n+klogk)

本质是把 n 从乘法项变成加法项,大集合时收益非常大。

这里的 k log k 可以从 Coupon Collector 理解:要把 k 个桶基本填满,期望命中次数量级是

Θ ( k log ⁡ k ) \Theta(k\log k) Θ(klogk)

可以把它想成“随机往 k 个桶里扔球”:前面很快就能命中大部分桶,但最后几个空桶会越来越难补齐,因此总次数会多出一个 log k 因子。

4.4 为什么还能估计 Jaccard 并有 Chernoff 集中

上一节的结论可以直接记成:单维有

E [ X i ] = J \mathbb{E}[X_i]=J E[Xi​]=J

从而

E [ X / k ] = J \mathbb{E}[X/k]=J E[X/k]=J

更严格的概率尾界在论文里有完整证明。对工程使用来说,重点是 X 会稳定集中在 kJ 附近,所以 FastSketch 做 LSH 阈值筛时不会比 MinHash 更飘。


5. FastSketchLSH 这个 repo 具体做了什么?

FastSketchLSH 是面向大规模去重的研究/工程实现集合:

  • C++ 扩展fastsketchlsh_ext/
    • FastSimilaritySketch:FastSketch 签名生成
    • LSH:高吞吐 band-parallel LSH(batch build / batch query)
  • Python 原型与对照实现prototype/src/
    • kmins_sketch.py:经典 k-mins
    • fast_sketch.py:FastSketch 原型
  • 模拟实验prototype/simulation/
    • FastSketch vs k-mins 估计分布/方差
    • LSH 碰撞概率曲线对比
  • 去重对比脚本comparison/
    • 同一数据集下跑 fastsketch / rensa
  • 基准结果evaluation/results.md、README)

6. 我们的 FastSketch 实现原理(工程版本)

结合 C++ 实现(fastsketchlsh_ext/include/fastsketch.h + fastsketchlsh_ext/cpp/fastsketch.cpp),核心流程可理解为:

  1. 预哈希:先把 token 映射到 64-bit 基值。
  2. 轮次 i:每轮用一个 seed 对 token 计算 hash。
  3. 分桶:用 hash 低位映射到 k 个桶。
  4. 每桶取最小 key:保证更早轮次优先。
  5. 提前停止:桶全部填满就停止。
  6. 补空:如果仍有空桶,用额外轮次补齐。

这和论文 Similarity-S 的目标一致,但写法是工程优化版。
论文 Algorithm 1 的原始写法是“2t 个哈希函数 + 前后半区间映射”;这里实现的是“轮次入哈希”的 practical 版本,代码路径更直接。

实测里,当 token 数超过约 700 时,约 99% 的样本会在第一轮就填满所有桶,所以中长文本上的吞吐会比较高。


7. 为什么 FastSketch 也能和 LSH 配合得很好?

LSH 需要的核心是“逐维匹配概率稳定 + 总体匹配率可集中”。论文证明 FastSketch 具备与 MinHash 同级别的这两点,因此 banding 行为会非常接近经典 k-mins。

p band = Pr ⁡ [ 一个 band 的  r  行全相等 ] p_{\text{band}}=\Pr[\text{一个 band 的 }r\text{ 行全相等}] pband​=Pr[一个 band 的 r 行全相等]

若把 band 内 r 行当作独立,有:

p band ≈ J r p_{\text{band}}\approx J^r pband​≈Jr

但 FastSketch 的行与行并非严格独立。论文给了可用的界:对一个 band 里的 r 个坐标,

∏ h = 0 r − 1 max ⁡ ⁣ ( 0 , k J − h k − h ) ≤ p band ≤ J r \prod_{h=0}^{r-1}\max\!\left(0,\frac{kJ-h}{k-h}\right) \le p_{\text{band}} \le J^r h=0∏r−1​max(0,k−hkJ−h​)≤pband​≤Jr

例如常用参数 k=128, r=8, J=0.7 时,上界是 0.0576,下界约 0.0522,两者已经很接近。

在工程里我们据此用近似:

P ( candidate ) ≈ 1 − ( 1 − p band ) b ≈ 1 − ( 1 − J r ) b P(\text{candidate}) \approx 1-(1-p_{\text{band}})^b \approx 1-(1-J^r)^b P(candidate)≈1−(1−pband​)b≈1−(1−Jr)b

虽然论文为了理论完备性在 LSH 部分使用了 Tensoring 结构来处理相关性,但我们的实验表明:在常见 sketch size(如 k=128/256)下,这种弱相关性对 banding 的 S-curve 影响通常可以忽略。

“band 间近似独立”可这样理解:

  1. 每个 band 使用不重叠的坐标组;
  2. band key 进入独立哈希表;
  3. 仓库模拟曲线几乎贴着经典理论曲线,说明残余相关性在这些参数下很弱、可忽略。

所以这里是“工程上的近似独立”,不是严格数学独立;但对召回判别已经足够好用。
仓库模拟脚本:prototype/simulation/display_lsh_probdist.py

7.1 我们自己模拟的 S-curve:k-mins LSH 和 FastSketch LSH 几乎重叠

下面这张图是我们在仓库里模拟得到的(k=128, b=16, r=8, 阈值=0.7):

KminsLSH&FastSketchLSH_Distribution

图里的红线(k-mins LSH 理论曲线)和蓝线(FastSketch LSH 模拟)几乎重叠,说明两者在 LSH 判别行为上基本一致。
(对应模拟脚本:prototype/simulation/display_lsh_probdist.py,原始输出图在 prototype/simulation/figures/

如果按“0.7 附近开始判定相似”这个工程目标来理解,这条曲线可以直观看成:

  • J=0.9:候选概率几乎是 1(理论值约 0.9999,基本百分百判为像)
  • J=0.4:候选概率接近 0(理论值约 0.01,基本不会判为像)
  • J≈0.7:处在拐点附近(理论值约 0.61),是“是否相似”的主要分界区域

也就是说,FastSketch 在保留 k-mins/MinHash 这套 LSH 判别逻辑的同时,把 sketch 计算做得更快。

7.2 为什么我们没完全照搬论文那套 LSH 结构?

论文在 LSH 部分还给了更“理论优化”的方案(中间 sketch + 采样 + tensoring),目标是把查询复杂度做到更强的上界。
这套做法理论上更完整,但工程实现链路更复杂、常数开销也更大。

本仓库选择了更直接的经典 banding 路线:

  • 结构简单,易维护,容易和现有去重流水线集成;
  • 在我们的数据与参数下,S-curve 几乎贴合经典 k-mins 理论;
  • 结合 FastSketch 的快速建签名,端到端吞吐更有优势。

8. 结果:速度与准确性

8.0 实验设置

下面表格统一按单线程看结果,用来和其他实现做公平对比。即便在这个设置下,FastSketchLSH 也有明显加速。
同时 FastSketchLSH 本身支持多线程:FastSimilaritySketch.batch(..., num_threads=...)LSH(..., num_threads=...),在 OpenMP 可用时还能继续提速。

8.1 估计分布/方差(simulation)

prototype/simulation/README.md 示例(k=128,目标 J=0.5):

  • FastSketch 方差约 0.001447
  • KMinSketch 方差约 0.002175

同样 k 下,FastSketch 更集中。

一个比较稳定的经验是:k 越大、文档(token set)越大,FastSketch 的相对优势通常越明显。

8.2 去重流水线时间(evaluation)

当前最新本地 rerun(Apple Silicon,对应仓库里 one-shot 路径验证表):

DatasetThreadsFastSketch Sketch(s)FastSketch Build(s)FastSketch Query(s)FastSketch Total(s)Rensa Total(s)Total Speedup
PINECONE10.5000.0820.0000.5820.8911.53×
PINECONE80.1620.0210.0070.1900.3191.68×
SHUYUEJ10.3640.0210.0000.3850.9102.37×
SHUYUEJ80.1210.0070.0020.1300.3252.49×

单独看新 one-shot LSH 路径对 build+query 的改善:

DatasetOld Build+Query(s)New One-Shot(s)SpeedupFlags Equal
PINECONE0.1550.1061.47×yes
SHUYUEJ0.0450.0192.32×yes

对应总耗时加速比:

  • PINECONE: 1.53xthreads=1),1.68xthreads=8
  • SHUYUEJ: 2.37xthreads=1),2.49xthreads=8
  • 单线程 LSH 的 build+query 阶段,因为 insert_and_query_duplicates(...),又额外拿到了 1.47x2.32x 的优化

补充一下 BOOKS3:这组结果目前沿用仓库里的 AMD EPYC 7352(x86_64, 200 GB)服务器 fair benchmark 表,和上面的本地 Apple Silicon rerun 不是同一台机器。

DatasetThreadsEngineSketch(s)Build(s)Query(s)Total(s)Sketch SpeedupTotal Speedup
BOOKS31rensa36.1900.0020.00036.193--
BOOKS31fastsketchlsh14.8430.0140.00614.8632.44×2.44×
BOOKS38rensa24.6050.0020.00024.607--
BOOKS38fastsketchlsh14.1910.0060.00314.1991.73×1.73×

9. 快速上手:安装与最小示例

9.1 安装

先确保 Python 版本在 3.11+

推荐:直接 PyPI 安装

pip install fastsketchlsh 

如果你要从源码构建(用于实验复现)

git clone https://github.com/pzcddm/FastSketchLSH.git cd FastSketchLSH/fastsketchlsh_ext pip install.cd.. pip install-r requirements.txt 

9.2 生成 sketch 并估计 Jaccard

from FastSketchLSH import FastSimilaritySketch, estimate_jaccard list_a =[f"a-{i}"for i inrange(16_000)] list_b =[f"a-{i}"for i inrange(8_000)]+[f"b-{i}"for i inrange(8_000)] sketcher = FastSimilaritySketch(size=256) sig_a = sketcher(list_a) sig_b = sketcher(list_b) estimated = estimate_jaccard(sig_a, sig_b)print(f"Estimated Jaccard similarity: {estimated:.4f}")

9.3 LSH:one-shot duplicate flags + query

from __future__ import annotations from datasets import load_dataset from FastSketchLSH import FastSimilaritySketch, LSH deftokenize(text:str)->list[str]:returnsorted({token for token in text.lower().split()if token}) dataset = load_dataset("lucadiliello/bookcorpusopen", split="train[:2048]") texts =[row["text"]for row in dataset if row.get("text")] token_sets =[tokenize(text)for text in texts] sketcher = FastSimilaritySketch(size=128, seed=42) sketch_matrix = sketcher.batch(token_sets) lsh = LSH(num_perm=128, num_bands=16) dup_flags = lsh.insert_and_query_duplicates(sketch_matrix).tolist() doc_idx =0 candidates = lsh.query(sketch_matrix[doc_idx])print(f"Candidates for {doc_idx}:", candidates)print("Duplicate flags:", dup_flags)print("Total duplicates detected:",sum(dup_flags))

9.4 复现端到端对比(fastsketch vs rensa)

python -m exps.end2end.run --engine fastsketch --dataset PINECONE python -m exps.end2end.run --engine rensa --dataset PINECONE # 批量跑 README 里的对比表bash exps/end2end/run_all_comparisons.sh 

10. 参数建议与常见坑

  1. size(也就是 sketch size, k):常用 128/256/512。k 越大越稳,但计算和内存也更高。
  2. LSH bands/rows:满足 k = b * r,按目标阈值调 S-curve。
  3. tokenization 很关键:分词、停用词、ngram 直接影响 Jaccard 语义。
  4. LSH 只做候选召回:如果目标 Jaccard 在 0.8/0.9 这类较高区间,LSH 往往已经比较准;但阈值设得更低时,通常还是要做二阶段验证(精确 Jaccard 或其他相似度)。
  5. 经验规律:在我们的实验里,k 越大、文档越长(token 越多),FastSketch 相对传统 O(k·n) 路线的速度优势通常越大;小 k、小文档下差距会缩小。

11. 总结

  • k-mins / MinHash + LSH 是经典去重主线,但复杂度瓶颈真实存在。
  • datasketch 纯 Python,常见场景会慢;在 sketch 阶段,FastSimilaritySketch 对它通常有一个数量级以上优势(常见 200x+)。
  • rensa 是 k-mins 路线 SOTA;按当前本地最新 rerun,FastSketchLSH 端到端总耗时仍有稳定加速(约 1.53x–2.49x),而且单线程 build/query 因 one-shot 路径进一步下降。
  • Fast Similarity Sketching 把复杂度做到 O(n + k log k),并保持 MinHash 级别理论性质。
  • FastSketchLSH 给了可复现、可落地的工程实现与实验结果。

项目地址

https://github.com/pzcddm/FastSketchLSH


致谢

感谢团队的持续投入与协作:

  • Zhencan Peng
  • Yixuan Cao
  • Fangcao Jian
  • Huitong Jin

参考

Read more

2026最新 Python+AI 入门指南:0基础也能快速上手,避开90%新手坑

2026最新 Python+AI 入门指南:0基础也能快速上手,避开90%新手坑

🎁个人主页:User_芊芊君子 🎉欢迎大家点赞👍评论📝收藏⭐文章 🔍系列专栏:AI 文章目录: * 【前言】 * 一、为什么2026年入门AI,首选Python?(新颖热点解读) * 二、Python+AI入门必备:前提+环境搭建(10分钟搞定) * 2.1 核心前提(不用啃硬骨头) * 2.2 环境搭建(Windows/Mac通用,避版本冲突) * 三、Python+AI入门实战:3个热门案例(附完整代码) * 案例1:数据处理(AI入门必备,80%AI开发第一步) * 案例2:机器学习入门(线性回归,房价预测) * 案例3:2026热门·大模型对接(LangChain快速调用) * 四、

By Ne0inhk
【 C/C++ 算法】入门动态规划-----一维动态规划基础(以练代学式)

【 C/C++ 算法】入门动态规划-----一维动态规划基础(以练代学式)

每日激励:“不设限和自我肯定的心态:I can do all things。 — Stephen Curry” 绪论 : 本章是动态规划算法的基础入门篇,我将通过三道简单题 + 一道中等难度的一维动态规划题来带你对动态规划有个初认识,并基本了解动态规划的最基本常见的写法,只有将基本写法了解了,对后续的难的题目自然也不会毫无头绪,后续还将持续更新更多相关的动规算法,敬请期待~🙃 ———————— 早关注不迷路,话不多说安全带系好,发车啦(建议电脑观看)。 👻动态规划🌥️ 这里通过大量练习得出下面动态规划做题步骤 简单的说动态规划理解成:某种状态的公式 + 提前求出来值的容器 求出当前位置的值然后放到容器中后后续使用 因为最开始的值一般是会看见的所以就能有初始值,从而启动动态规划 从上中可以主要提炼出: * 状态 * 容器的重要性 * 公式,可以换种说法:状态转移方程 这样严格😈的说:动态规划 = 状态定义 + 状态转移方程 + 初始条件 + 状态存储(容器) 下述步骤是通过写完下述四道题后的总结,所以同样需要道友🗡️大量的练习沉淀最终就能对动态规划的题目

By Ne0inhk
告别付费行情软件:利用 QMT 免费“白嫖”全市场 Tick 级数据 (附 Python 代码)

告别付费行情软件:利用 QMT 免费“白嫖”全市场 Tick 级数据 (附 Python 代码)

前言:作为一名量化开发者,我每年最心疼的支出不是服务器费用,而是数据源费用。Wind 终端一年几万,Tushare 积分也要钱,稍微好点的 Tick 数据源动辄上千。但其实,很多散户手里就握着一把“金钥匙”却不自知——那就是券商免费提供的 QMT。今天,我将教你如何把 QMT 变成一个免费、稳定、实时的行情数据中心。 为什么 QMT 的数据最“香”? 在数据获取这条路上,我们通常有三个选择: 1. 爬虫:容易被封 IP,数据不稳定,且有法律风险。 2. 第三方 API (Tushare/JoinQuant):好用但要钱,免费版有各种限制(如延迟、限流)。 3. 券商 QMT/PTrade:完全免费,速度极快(

By Ne0inhk
蓝桥杯(C 语言 / C++)备考全攻略:3-6 个月从入门到上岸,语法 / 算法 / 真题 + 模板直接用

蓝桥杯(C 语言 / C++)备考全攻略:3-6 个月从入门到上岸,语法 / 算法 / 真题 + 模板直接用

蓝桥杯作为国内极具含金量的编程竞赛,是大学生提升编程能力、丰富简历的重要选择。本文针对 C 语言 / C++ 方向,打造 3-6 个月备考计划,从语法基础到算法进阶,再到真题实战,梳理高频考点并提供可直接复用的代码模板,帮你高效备考、稳步上岸。 一、备考规划:3-6 个月阶段拆解(零基础友好) 1. 基础阶段(1-2 个月):夯实语法与工具         核心目标:掌握 C/C++ 基础语法,熟练使用编译器(Dev-C++/VS Code),能独立编写简单程序。         每日学习时长:2-3 小时。 (1)语法重点(按优先级排序) * 核心语法:变量 / 常量、数据类型(int/long long/double/

By Ne0inhk