目录
场景在搜索引擎和推荐引擎中,对相似文章去重是一个非常重要的环节,另外是拍照识花、摇一摇搜歌等场景都可以使用它快速检索。
基于敏感性哈希的检索更擅长处理字面上的相似而不是语义上的相似。
- 向量空间模型
- ANN检索加速思路
- 局部敏感哈希编码
- 随机超平面划分哈希编码思路
- Google的SimHash编码思路以及抽屉原理
- 聚类
- 乘积量化
- 局部敏感哈希编码
ANN与向量空间模型
对于高纬度数据近邻检索常见方式
将所有文档中的关键词都提取出来,如果总共n个关键词,那么就是一个n纬度的向量。具体到一篇文章,假如文档包含关键词数量是k,其中(0<=k<=n),如果文档包含的第k个关键词的权重为w,那么权重向量k位置的元素就是w,这个权重w一般是根据TF-IDF计算得出,如果文档不包含第k个关键词,那么权重w就是0。
关于TF-IDF词频-逆文档频率参考往期:
ElasticSearch学习篇15_《检索技术核心20讲》进阶篇之TopK检索-CSDN博客
本质就转化为计算两个向量的相似度,可用余弦相似度、欧式距离等计算,另外n纬向量放入到空间中就是一个点,也可理解为空间中的近邻检索ANN,在十几维量级的低维空间中,我们可以使用 k-d 树进行 k 维空间的近邻检索,这种思路就是前面说的精确检索的思路,它的性能还是不错的,但是纬度上来之后,因为基础k-d树特点当纬度大于20的时候可能出现线性灾难,搜索大量的邻域导致性能很慢。
关于KD树、KDB、BKD树参考往期:
ElasticSearch学习篇10_Lucene数据存储之BKD动态磁盘树(论文Bkd-Tree: A Dynamic Scalable kd-Tree)_bkd树-CSDN博客
向量空间模型可用来表示文字、图片等内容,从而进行文本、图像近邻搜索ANN,一些常见的ANN加速算法思想基本分为两类
- 缩小候选集
- 压缩向量存储空间
ANN搜索加速技术思想
局部敏感哈希-缩小候选集数量
借助非精确检索的思路,可以将高纬空间中的点进行区域划分,然后给每个区域生成一个较短的编码,查询的时候先根据规则定位区域,达到缩小候选集的目的,再从该区域高效近邻检索。
这个规则就类似哈希,但是普通的哈希函数值不太可控,文章中变化很少的关键词就会导致哈希值很大的变动。
因此必须找出一个特殊的哈希规则,使得相似的数据哈希之后,得到的哈希值也是相近的,被称为局部敏感性哈希(Locality-Sensitive Hashing)
随机超平面划分哈希编码
以二维空间举例,找出一条线,将区域划分为两部分,处在区域1的点编码为1,处在另外区域的点编码为0,这条线需要尽可能合理,因此可以随机找出n条线,一条线切分下,会对点增加一位编码,这样n条线就会给一个点产生n位的编码。
对于多维空间,就需要找超平面,如n个纬度就找n个超平面,然后这些超平面将空间中的点切割,位于超平面两侧的点(通过超平面法向量的余弦相似度计算)分别被编码0、1,这样可以将高维空间的点映射为一纬的编码。
如果两个点的哈希值是一样的,那么两个点大概率距离的非常近,即使哈希值不一样,只要他们在n个比特位中大大部分是相同的(具体的计算算法如海明距离),说明他们大概率相近。举个例子如果两篇文章内容是100%相同的,那么他们的哈希值就是相同的,也就相当于编码相同。
这种一般哈希编码有个缺点就是随着超线、超平面的划分编码,会丢失某些纬度权重信息。
SimHash编码
谷歌提出的局部敏感哈希策略,简化哈希函数,保留多维数据(点)项的权重信息。
它使用一个普通的哈希函数代替了n次随机超平面划分,这个哈希函数作用对象不是整体所有的高纬度数据项,而是一个个处理高纬度数据项,通过数据项哈希值编码与数据项权重计算的时候就能保留权重。
构造SimHash的具体过程,以一篇文档进行局部敏感哈希编码来说明
- 文档分词,关键词项带权重:分词并计算每个关键词的权重w。
- 生成Hash值:使用一般的Hash函数,针对每个关键词生成如64位的0、1哈希值编码。
- 每个关键词哈希值变化:将哈希值的0改为-1,如10110 -> 1 -1 1 1 -1
- 每个关键词哈希值按位乘上词权重:如词权重3,变为 3 -3 3 3 -3。
- 按位相加所有关键词的哈希值:计算文档所有关键词哈希值按位加的结果。
- 文档哈希值编码转换:将编码转为0、1。
这样,文档的最终哈希编码是收到权重比较大的词影响比较多的,另外就是哈希函数比较简单,代替了复杂的随机选取超平面方式。
抽屉原理
对于文章查重领域,如果两个文章的SimHash编码的距离(使用汉明距离计算)小于k,那么就认为它们是相似的。举个例子当k=3的时候,需要找出k小于3的文章,然后遍历逐一计算对比,效率比较低,有没有加速方案?
一个直观的想法,使用SimHash编码某一位bit作为key,使用文章内容作为value构建倒排索引,以64位的SimHash编码为例,key的数量为128个,构造的倒排索引key如
- 1xxxxx、0xxxxx
- x1xxxx、x0xxxx
- …
查找的时候,逐位查找64次,将第n为比特位对应key对应的文章全部取出来,然后计算对比。
这种方式效率不是很高,因为即使两篇文章64位bit任意两个位置的bit相同的话就会被召回,即使不太相似。
Google提出优化的抽屉原理:将文章的SimHash编码分4段,如果想找出和此文章bit位差异不超过3个的文章,那么4个段其中一定至少有一个段的bit是完全一致的。因此查询就被转化为了4段中有一段完全相同的文章会被召回。
按照这个思路,
- 分段构建倒排索引:将每个文档的SimHash编码划分为四段,每段的16位bit作为一个倒排索引,
- 分段查询:查询的时候,当前文档的SimHash编码也会被划分为四段,如果找出汉明距离k<3的文档,只需召回四段中任一段完全相同的文档即可,然后从四段倒排索引找回的结果合并。
通过使用 SimHash 函数和分段检索(抽屉原理),使得 Google 能在百亿级别的网页中快速完成过滤相似网页的功能,从而保证搜索结果的数量。
思考1:对于 SimHash,如果将海明距离在 4 之内的文章都定义为相似的,那我们应该将哈希值分为几段进行索引和查询呢?分为5段,因为4为不同的bit位最多能影响四段,虽然除不尽,每段可以是12或者13,查询的时候也按照这种规则划分5段即可。
对比编辑距离、杰卡德算法计算文本相似性
对比杰卡德、莱文斯坦、SimHash三种计算文章相似度的效果,总结特点以及适用场景。
SimHash的一种实现参考:SimHash结合汉明距离判断文本相似性
下面选取一套初中语文的试卷,试卷包含大概20道试题,大概1w字符
手动创造不同的CASE,以下是包含完整CASE描述的表格:
CASE | 描述 | 原文内容长度 | 内容长度 | simHash相似度 | simHash耗时 | levenshtein相似度 | levenshtein耗时 | jaccard相似度 | jaccard耗时 |
---|---|---|---|---|---|---|---|---|---|
CASE1 | 将21题和22题互相换位置 | 8556 | 8556 | 100.0 | 274ms | 0.84 | 298ms | 1.0 | 7ms |
CASE2 | 删除试卷中间的11题 | 8556 | 8401 | 96.88 | 102ms | 0.98 | 274ms | 1.0 | 1ms |
CASE3 | 删除10题之后的所有内容 | 8556 | 1921 | 65.63 | 30ms | 0.22 | 41ms | 1.0 | 2ms |
CASE4 | 完全打乱试题顺序 | 8556 | 8560 | 100.0 | 45ms | 0.45 | 183ms | 1.0 | 1ms |
CASE5 | 删除11题之前的所有内容 | 8556 | 6635 | 92.19 | 40ms | 0.77 | 143ms | 1.0 | 1ms |
CASE6 | 前11道题内容不相同,后面内容相同 | 8556 | 9184 | 89.06 | 40ms | 0.74 | 194ms | 0.9035971223021583 | 1ms |
CASE7 | 在11题后面中间加入一道试题 | 8556 | 8741 | 98.44 | 37ms | 0.97 | 186ms | 1.0 | 1ms |
绘制成图表
在选取初中语文试卷内容1w字符文本相似度计算场景下,综合CASE分析文本相似度计算效率以及严格程度
- 效率:杰卡德 > SimHash > 莱文斯坦编辑距离
- 严格程度:莱文斯坦编辑距离 > SimHash > 杰卡德
针对CASE2、CASE3分析,即使试卷内容1和试卷内容2相差了一道或者十多道试题,但是杰卡德计算的文本相似度仍为100,SimHash计算的文本相似度较为准确,但是效率又要比莱文斯坦效率要高,使用SimHash可以避免查重中将不是完全一致的试卷内容(差别几道试题)误查,同时又兼顾相似度计算效率。
SimHash、杰卡德(Jaccard)和莱文斯坦(Levenshtein)是三种常用的文本相似度计算算法,它们各有特点和适用场景:
- SimHash:局部敏感性哈希
- 特点:SimHash是一种局部敏感哈希算法,主要用于快速计算大规模文本数据的相似性。它通过将文本转换为一个固定长度的二进制哈希值来表示文本特征。SimHash的优点是计算速度快,适合处理大规模数据。
- 适用场景:SimHash常用于海量数据的去重、近似重复检测和相似文档查找等场景。由于其计算效率高,特别适合需要快速处理和比较大量文本的应用。
- 杰卡德(Jaccard)相似系数:
- 特点:杰卡德相似系数用于衡量两个集合的相似度,定义为两个集合交集的大小除以并集的大小。对于文本相似性,通常将文本分割成词或字符的集合,然后计算这些集合的杰卡德相似度。
- 适用场景:杰卡德相似度适合用于比较短文本或关键词集合的相似性,如文档分类、标签推荐等。由于其计算简单,适合用于需要快速评估文本相似性的场合。
- 莱文斯坦(Levenshtein)距离:
- 特点:莱文斯坦距离,又称编辑距离,表示将一个字符串转换为另一个字符串所需的最小编辑操作次数(插入、删除、替换)。它能够精确地衡量两个字符串之间的差异。
- 适用场景:莱文斯坦距离适合用于需要精确比较字符串差异的场合,如拼写检查、DNA序列比对、文本纠错等。由于其计算复杂度较高,通常用于较短文本的比较。
总结来说,SimHash适合大规模文本的快速相似性检测,杰卡德相似度适合集合间的相似性比较,而莱文斯坦距离适合精确的字符串差异分析。选择合适的算法需要根据具体的应用场景和数据特征来决定。
汉明距离
主要是计算等长的两个二进制字符串之间差别的位数
def hamming_distance(str1, str2):if len(str1) != len(str2):raise ValueError("Strings must be of the same length")distance = 0for ch1, ch2 in zip(str1, str2):if ch1 != ch2:distance += 1return distance# 示例
str1 = "1101"
str2 = "1001"
print(hamming_distance(str1, str2)) # 输出: 1
聚类-缩小候选集数量
图片如何相似性检索,检索图片和检索文章一样,首先要先用向量空间模型将图片表示出来,这样图像就变成了高纬度空间的一个点,搜素图片转化为了高纬度空间的ANN。
如何从图片抽取向量空间模型,如果把图像的每个像素看作一个纬度,像素上的RGB值作为纬度值,是一种思路,但是一张图片的纬度大概是百万级别,检索起来很复杂,因此另外一种方式就是使用CNN等进行图像特征提取,转为一个512或者1024纬度的向量空间模型。
如何加速ANN检索效率,有了向量空间模型,就可以使用ANN加速技术比如SimHash来加速检索,比如将高纬空间的点划分到有限区域,从而达到缩小候选集的目的。但是SimHash哈希函数比较简单,更适合计算字面上的相似性而不是语义上的相似性,同时SimHash是一种粒度很粗的非精确检索方案,他能将上百万的纬度压缩为64位bit,损失不少精度。因此一般使用聚类方案加速ANN检索,常见的一种是K-Means(K-平均算法)方案
K-Means聚类算法构建聚类计算步骤
- 初始聚类中心:随机从数据中选取k个数据作为初始聚类中心
- 计算距离:计算其他数据与ki 的距离,加入到最邻近的ki 聚类中
- 根据距离均值重新选取聚类中心:根据聚类中的点到聚类中心距离的均值,重新选一个聚类中心
重复2-3步,即重新计算其他还数据到聚类中心的距离,然后将节点划分到最近的聚类中,然后在更新聚类中心。
K-Means 聚类算法的优化目标是,类内的点到类中心的距离均值总和最短。构建好之后,以聚类中心数据ID作为key,单个聚类的数据创建倒排索引。
K-Means查询的时候,直接找出待查询数据距离最近的聚类中心ki 然后从倒排索引取出topK候选集,
- 如果不足topK数量,那么可以再查询邻近聚类的候选集。
- 如果数量很多同时topK又取得非常大,那么一个一个计算和待查询数据距离代价也很大,可以采用层级子聚类来继续缩小候选集。
乘积量化-压缩向量模型存储空间
对于向量的相似检索,除了检索算法本身,优化向量存储空间也是一个优化方向,因为向量的相似度计算需要加载进内存。
以一个1024纬度的向量距离,每个向量纬度是一个浮点数占4 Bytes = 32 bits,那么一个向量占用1KB空间,如果是上亿级的数据,存储向量需要占几百个G。(100 000 000 KB = 100 GB)
为了更好的将向量加载进去内存,需要对向量存储空间优化,一种思路是使用上面的聚类思想,减少加载进内存的数量,只把查询向量和聚类向量加载进内存,而不是聚类下所有向量,这样可能会损失结果粒度。另外一种思路就是使用向量量化-乘积量化来压缩。
乘积量化的概念
- 乘积:高纬空间向量可以看作是多个低纬空间向量相乘的结果,可以理解为笛卡尔集,如数轴的x、y轴分别表示一纬空间,数轴区域中的点(xi.yi)就是二维空间的点,假如xi的值为1、2、3,yi的值为5、6、7,那么组合笛卡尔集的二纬空间的点个数为9个。
- 量化:将区域划分为子区域,然后在编码,这样就能将区域转为1纬编码,上面聚类就是一种量化方式。
乘积量化就是将高纬空间划分为多个子空间,然后对子空间编码,针对子空间在进行聚类技术分为多个子区域,然后给每个子区域编码即聚类ID。好处:省空间,二纬空间存储的点数量为9,但是只存储一纬空间x、y的话,只需要存储的数量为6个一纬点。
举例假设一组1024纬度的向量进行乘积量化
- 首先将1024纬度向量拆分为4个256纬度子向量
- 在每一个256纬度子向量进行聚类,找出1-256和聚类ID,因此只使用8 bits就能表示1-256的聚类ID。
所以经过上述过程,1024纬度的向量使用 4 * 8 bits = 32 bit就能表示。
乘积量化PQ过程思想
乘积量化向量相似查询的时候,涉及到三个向量
- 样本向量:1024纬度,乘积量化过程会被压缩为4段,每段进行聚类,每段会得到一个聚类中心ID,范围为1-256,最后所有样本处理完后,大概会得到 256 * 4 个聚类中心向量。
- 聚类中心向量:每段都有256个聚类中心ID,256 * 4 个聚类中心向量。
- 查询向量:1024纬度,查询过程会被压缩4段。
构造的时候,样本向量会被压缩为4段,会得到256 * 4的聚类中心向量,而样本向量也从1024纬度被压缩为32纬,1024 => 4 * 256 => 4 * 2 ^ 8,压缩含义就是以每段聚类中心ID的8为bit编码代替当前子向量段。
当计算查询向量和样本向量的距离时,
- 向量切分子空间:我们将查询向量和样本向量都分为 4 段子空间。
- 子空间计算聚类ID:计算查询向量切分的4段子空间的聚类ID,生成压缩向量。然后预生成一个距离表,该距离表纬度是256 * 4,记录了查询子向量和 子空间各个中心向量的距离。
- 遍历全部样本数量计算查询向量距离:根据预处理阶段的距离表可以查询出查询向量每段到各个聚类ID的距离,数据库中全部的样本向量属于哪个聚类ID也可以计算出,然后就是求出每段距离:d1、d2、d3、d4,最终通过相关运算如欧式距离等计算两个向量的距离,进而返回topK。
PQ主要是为了压缩空间,计算距离算法不太关注。
其他参考:理解 product quantization 算法
这样,求查询子向量和样本子向量的距离,就转换为求查询子向量和对应的聚类中心向量的距离。那我们只需要将样本子向量所属的聚类中心的聚类 ID 作为 key 去查距离表,就能在 O(1) 的时间代价内知道这个距离了。
这里讲述的只是大致思想,具体的如何计算找出紧邻的topK向量还有 基于倒排的乘积量化IVFPQ缩小遍历全部样本空间向量,以及SDC、ADC向量相似检索算法还有很多小细节。
基于倒排索引的IVFPQ思想
上面说的计算查询向量和所有样本向量之间的距离是遍历全部的样本空间,还有一种维护样本向量倒排索引的方式加速检索。
构建倒排索引的具体的思路
- 样本向量切分子空间:1024纬切分为4段,每段256纬度
- 计算子空间聚类ID:可以采用KMeans聚类算法,最终每个子空间(段)聚类256个,每个子空间使用聚类ID代表当前样本子空间特征,最终每个子空间得到8位的聚类ID
- 建立倒排索引:得到的32位新向量,为每个子空间创建一个倒排索引,每个倒排索引的key可以设置为当前段的聚类ID,value设置为在当前段被聚类到该ID的所有样本数量。
查询的时候,查询向量进行切分子空间,PQ,然后逐段查倒排,这样将四个段的样本数量就是最有可能相近的样本数量,在进行距离计算找出TopK。
另外还有一种根据聚类ID向量残差创建倒排索引的做法,这样做精度会高一些。