来自 YouTube
1.相似度搜索的传统方法(Jaccard, w-shingling, Levenshtein)
1.Jaccard 距离
-
公式
Jaccard ( A , B ) = 1 − ∣ A ∩ B ∣ ∣ A ∪ B ∣ \text{Jaccard}(A, B) = 1 - \frac{|A \cap B|}{|A \cup B|} Jaccard(A,B)=1−∣A∪B∣∣A∩B∣
其中, A 和 B 是两个集合。Jaccard 距离用于衡量两个集合的相似性,值在 0 到 1 之间,0 表示完全相同,1 表示完全不同。
-
使用场景
- 文本比较:计算两个文档中词汇的相似度。
- 聚类:用于计算文档、图像等对象的相似性。
2.w-shingling
-
定义
w-shingling是一种将文本划分为重叠子字符串(或n-grams)的方法,w 代表子字符串的长度。
例如,对于字符串 "abcde" 和 w = 3 ,可以得到 shingle 集合 {abc, bcd, cde}。
-
使用场景
- 通常结合 Jaccard 相似度衡量不同的文本相似性
- 文本相似度:通过生成w-shingles,然后计算Jaccard相似度来判断文本的相似度。
- 近似重复文档检测。
3.Levenshtein 距离(编辑距离)
-
定义
Levenshtein距离计算两个字符串之间通过插入、删除或替换操作变成相同字符串所需的最小操作次数。
-
使用场景
- 拼写检查:判断两个字符串(单词)之间的编辑距离。
- DNA序列比对:分析生物序列的相似性。
2.基于向量的相似度搜索(TF-IDF, BM25, SBERT)
1.TF-IDF
-
公式
-
TF(词频):某词 t 在文档 d 中出现的次数除以文档总词数:
TF ( t , d ) = 词 t 在文档 d 中出现的次数 文档 d 的总词数 \text{TF}(t, d) = \frac{\text{词 } t \text{ 在文档 } d \text{ 中出现的次数}}{\text{文档 } d \text{ 的总词数}} TF(t,d)=文档 d 的总词数词 t 在文档 d 中出现的次数
-
IDF(逆文档频率):词 t 在文档集合 D 中出现的频率的倒数:
IDF ( t , D ) = log ( ∣ D ∣ ∣ { d ∈ D : t ∈ d } ∣ ) \text{IDF}(t, D) = \log \left( \frac{|D|}{|\{d \in D : t \in d\}|} \right) IDF(t,D)=log(∣{d∈D:t∈d}∣∣D∣)
-
TF-IDF
TF-IDF ( t , d , D ) = TF ( t , d ) × IDF ( t , D ) \text{TF-IDF}(t, d, D) = \text{TF}(t, d) \times \text{IDF}(t, D) TF-IDF(t,d,D)=TF(t,d)×IDF(t,D)
-
-
解决的问题
- 衡量某个词在单个文档中的重要性,常用于文本检索和关键词提取
- 通过IDF,减少在大多数文档中普遍出现的词(如"the","and")的权重。
-
不足
- 没有考虑词的位置、词序关系以及文档长度的影响
- 对于长文档,TF-IDF会产生偏差,因为词频可能更高,但未必意味着其重要性更大。(比如有一篇文章专门讨论狗,狗出现的很多,但是未必重要)
- 只能衡量词和文档的静态相关性,无法考虑用户查询的动态需求。
2.BM25
-
公式
BM25 是 TF-IDF 的改进
BM25 ( t , d ) = IDF ( t ) × TF ( t , d ) ⋅ ( k 1 + 1 ) TF ( t , d ) + k 1 ⋅ ( 1 − b + b ⋅ ∣ d ∣ avgdl ) \text{BM25}(t, d) = \text{IDF}(t) \times \frac{\text{TF}(t, d) \cdot (k_1 + 1)}{\text{TF}(t, d) + k_1 \cdot \left(1 - b + b \cdot \frac{|d|}{\text{avgdl}}\right)} BM25(t,d)=IDF(t)×TF(t,d)+k1⋅(1−b+b⋅avgdl∣d∣)TF(t,d)⋅(k1+1)
- TF ( t , d ) \text{TF}(t, d) TF(t,d) 是词 t t t 在文档 d d d 中的词频。
- IDF ( t ) = log ( N − n ( t ) + 0.5 n ( t ) + 0.5 ) \text{IDF}(t) = \log \left( \frac{N - n(t) + 0.5}{n(t) + 0.5} \right) IDF(t)=log(n(t)+0.5N−n(t)+0.5) 是词 t t t 的逆文档频率,其中 N N N 是总文档数, n ( t ) n(t) n(t) 是包含词 t t t 的文档数。
- ∣ d ∣ |d| ∣d∣ 是文档 d d d 的长度, avgdl \text{avgdl} avgdl 是所有文档的平均长度。
- k 1 k_1 k1 和 b b b 是调节参数,通常 k 1 ∈ [ 1.2 , 2.0 ] k_1 \in [1.2, 2.0] k1∈[1.2,2.0] , b ≈ 0.75 b \approx 0.75 b≈0.75 。
-
解决的问题
- 在TF-IDF 的基础上,BM25 加入了文档长度归一化的机制,避免了长文档在词频上的不公平优势。
- BM25 是一种平衡频率和文档长度的动态模型,适用于实际的文档检索。
-
不足
尽管在信息检索中表现优越,但对于词序、词义关系等更复杂的语义问题处理能力有限。
3.SBERT
-
SBERT(Sentence-BERT) 是基于 BERT 模型的一种变体,专门用于生成句子的向量表示,以便更好地进行句子间的相似度计算和语义搜索
-
原理
SBERT的主要目标是通过预训练的 BERT 模型,结合句子对比学习策略,生成高质量的句子嵌入(sentence embeddings),并在向量空间中表示这些句子的语义。
与原始 BERT 只能处理单个句子的方式不同 ,SBERT 能高效计算句子相似度。
-
步骤
- 句子嵌入生成 :通过双塔结构(Siamese Network),输入的句子对分别通过共享权重的BERT编码器生成句子的嵌入向量。
- 相似度计算 :SBERT 使用余弦相似度、欧几里得距离等来衡量句子嵌入之间的相似性。
- 训练方式 :SBERT 使用对比损失(Contrastive Loss)、**三元组损失(Triplet Loss)**或者基于自然语言推理(NLI)的数据集训练,以优化嵌入质量。
双塔结构
是一种神经网络结构,通常用于计算两个输入之间的相似性。它的核心思想是通过共享权重的两个子网络 分别对两个输入进行编码,然后比较它们的输出向量来评估相似性。
工作原理
-
输入 → 两个文本或图像等
-
共享权重编码器
两个子网络(塔)是共享参数的,即 他们使用完全相同的网络权重。
对于输入 A 和 B,得到向量表示 E A E_A EA 和 E B E_B EB
-
相似度计算
对 E A E_A EA 和 E B E_B EB 计算相似性,可以用余弦相似度 、欧几里得距离 、曼哈顿距离等度量方式来衡量两个输入在向量空间中的接近程度。
-
损失函数
训练时,使用 对比损失 (Contrastive Loss)或 三元组损失(Triplet Loss)来让相似输入的嵌入向量更接近,不相似输入的嵌入向量远离。
-
共享权重的优势
通过共享参数,双塔结构保证了两个输入被同样处理,这避免了输入之间的不对称处理问题。
-
解决的问题
- 句子相似度计算:BERT 只能处理句子对的相似度,而 SBERT 可以通过向量计算高效地处理大量句子的相似度问题,极大加速了计算过程。
- 语义搜索:SBERT 可以将文本数据转化为语义向量,通过快速的余弦相似度计算,实现语义层面的搜索。
- 嵌入聚类:通过生成高质量的句子向量,SBERT 可以用于句子的聚类分析。
-
不足
- 计算开销:SBERT 在生成句子嵌入时仍然依赖于BERT模型,计算成本相对较高,尤其在处理长文本时。
- 上下文依赖性不足:SBERT 生成的句子嵌入是固定的,缺乏对上下文动态变化的建模能力。
- 无法处理词序关系:SBERT 主要关注句子级别的语义嵌入,对于词序或细粒度的信息捕捉能力有限。
-
代码
3.faiss-相似度搜索介绍与如何选择索引
不同的索引在不同向量数量的查询时间对比。注意,纵轴是对数时间
-
数据集
pythonimport shutil import urllib.request as request from contextlib import closing # first we download the Sift1M dataset with closing(request.urlopen('ftp://ftp.irisa.fr/local/texmex/corpus/sift.tar.gz')) as r: with open('sift.tar.gz', 'wb') as f: shutil.copyfileobj(r, f) import tarfile # the download leaves us with a tar.gz file, we unzip it tar = tarfile.open('sift.tar.gz', "r:gz") tar.extractall() import numpy as np # now define a function to read the fvecs file format of Sift1M dataset def read_fvecs(fp): a = np.fromfile(fp, dtype='int32') d = a[0] return a.reshape(-1, d + 1)[:, 1:].copy().view('float32') # data we will search through xb = read_fvecs('./sift/sift_base.fvecs') # 1M samples # also get some query vectors to search with xq = read_fvecs('./sift/sift_query.fvecs') # take just one query (there are many in sift_learn.fvecs) xq = xq[0].reshape(1, xq.shape[1])
1.Flat Indexes
除此之外,也有 IndexFlatIP(**IP, Inner Product,**内积)
质量很高,但是速度很慢
python
d = 128
k = 10
import faiss
index = faiss.IndexFlatIP(d)
index.add(xb)
%%time
D, I = index.search(xq, k)
'''
CPU times: user 16.2 ms, sys: 2.14 ms, total: 18.4 ms
Wall time: 21.8 ms
'''
baseline = I[0].tolist()
2.LSH(局部敏感哈希)
与字典(尽量最小化碰撞)不同,LSH 尝试最大化碰撞。然后将搜索范围限制在一个桶内
python
nbits = d*4
index = faiss.IndexLSH(d, nbits)
index.add(xb)
%%time
D, I = index.search(xq, k)
'''
CPU times: user 3.3 ms, sys: 2.47 ms, total: 5.76 ms
Wall time: 5.37 ms
'''
np.in1d(baseline, I)
'''
array([ True, True, True, True, False, False, True, False, False,
True])
'''
可以用 nbits 在速度与准确率直接找平衡
nibts 越高,召回率越高
但是,nbits 越高,速度越慢
3.HNSW
这是一个NSW图,图中两个顶点的距离为 4 跳。
以此类推,下面是 HNSW
同样需要四跳
python
M = 16 # 每个顶点的连接数
ef_search = 8 # 搜索的深度
ef_construction = 64 # 构建时的扩展因子,决定了在构建图时为每个点找到最近邻的搜索深度
index = faiss.IndexHNSWFlat(d, M)
index.hnsw.efSearch = ef_search
index.hnsw.efConstruction = ef_construction
index.add(xb)
%%time
D, I = index.search(xq, k)
'''
CPU times: user 366 μs, sys: 261 μs, total: 627 μs
Wall time: 1.27 ms
'''
np.in1d(baseline, I)
'''
array([ True, True, False, False, False, True, False, False, True,
True])
'''
# 以上不太准,调整 M 和 ef_search
M = 32 # 每个顶点的连接数
ef_search = 32 # 搜索的深度
ef_construction = 64 # 构建时的扩展因子,决定了在构建图时为每个点找到最近邻的搜索深度
......
%%time
D, I = index.search(xq, k)
'''
CPU times: user 345 μs, sys: 133 μs, total: 478 μs
Wall time: 375 μs
'''
np.in1d(baseline, I)
'''
array([ True, True, False, True, True, True, True, False, True,
True])
''
对比不同的 M、efConstruction 和 efSearch 组合的召回率
可以发现,
- M 的大小对效果提升最明显
- 其次是 efSearch
- 最后是 efConstruction
- 总结,efConstruction 最后大一些,M在 32-64 以上就可以了,efSearch = 32 是中位数,最好和 M 一起调节
不同的 M、efSearch 对查找时间的对比
- 可以发现,efSearch = 32, M=32 就不错
4.IVF
其实这个算法就像聚类,通过调整 探针 nprobe 来权衡准确率是查找时间
python
nlist = 128 # 质心数量
quantizer = faiss.IndexFlatIP(d)
index = faiss.IndexIVFFlat(quantizer, d, nlist)
index.train(xb) # 需要训练!
index.add(xb)
index.nprobe = 1
%%time
D, I = index.search(xq, k)
'''
CPU times: user 928 μs, sys: 305 μs, total: 1.23 ms
Wall time: 576 μs
'''
np.in1d(baseline, I)
'''
array([ True, False, False, True, True, False, False, True, False,
True])
'''
# 把探针改成 2
index.nprobe = 2
%%time
D, I = index.search(xq, k)
'''
CPU times: user 2.78 ms, sys: 2.16 ms, total: 4.93 ms
Wall time: 5.13 ms
'''
np.in1d(baseline, I)
'''
array([ True, True, True, True, True, False, True, True, True,
True])
'''
不同探针和质心下的召回率和搜索时间
不同质心下的索引大小