目录
参考海量数据相似查找系列1 – Minhashing & LSH & Simhash 技术汇总
参考https://yongyuan.name/blog/ann-search.html
Scalable Nearest Neighbor Algorithms for High Dimensional Data
主要分为高维稀疏向量和稠密向量两大方向。
针对高维稀疏数据情况,如何通过哈希技术进行快速进行相似查找。
例如,推荐系统中item-user矩阵。如果你有item数量是百万级别,user是千万级别,这个矩阵是十分稀疏的。你如何计算每一个item的Top N相似item呢?
同样海量文本场景,文本集合可以看成doc-word 稀疏矩阵,如何求解每个文档的Top N相似文档?
如果采用两两比较的话,至少有两个问题:
常见的解决方法如下:
问题:虽然时间复杂度可以减小,但高频词可能导致倒排的拉链长度太长,导致效率下降。
例如,小写字母代表词,大写字母代表文档:
S1={a, d}, S2={c}, S3={b, d, e}, S4={a, c, d}
然后,把原来的词典{a, b, c, d, e} 顺序随机重排,例如得到{b, e, a, d, c},
定义一个函数h:计算集合S最小的minhash值,就是在这种顺序下最先出现1的元素。那么,
h(S1) = a, h(S2)=c, h(S3)=b, h(S4)=a
类似地,如果进行n次重排的话,就会有n个minhash函数,{h1(S), h2(S)…, hn(S)}, 那原来每个高维集合,就会被降到n维空间,比如S1->{h1(S1), h2(S1)…, hn(S1)}。
但是实际中因为重排比较耗时,会用若干随机哈希函数替代。比如设定一个哈希函数: h(x) = (i+1) % 5.
以{a, b, c, d, e}顺序,“i”表示各个索引,比如a的“i”值为1, b的“i”值为2等。
同样可以定义n个哈希函数【不需要重排,每个hash计算对应的值就行】,进行上述操作,那每个集合S就被降维到n维空间的签名。
这样,每个集合就可以用一个n维向量,每维是那个hash下的minhash来表示了
可以参考google那篇Google News Personalization: Scalable Online Collaborative Filtering
学习https://github.com/FALCONN-LIB/FALCONN/wiki/LSH-Primer
minhash解决了文章一开始提到的第二个问题,高维向量间计算复杂度问题(通过minhash 机制把高维降低到n维低纬空间)
但是还没解决第一个问题:两两比较,时间复杂度O(n^2)。
LSH 就是这样的机制,通过哈希机制,让相似向量尽可能出现一个桶中,而不相似的向量出现在不同的桶中。相似度计算只在么个桶中进行,每个桶彼此之间不做相似度计算。
在minhashing 签名的基础上做LSH。
例如: minhash签名维度是12,分成4组,每组3个元素。
拿band1来说,第二列和第四列向量是一样的(第二列是(0, 2, 1), 第四列是(0, 2, 1)),一定会哈希到相同的桶中(band1名下的桶),而第一列和第二列有可能不会在一个桶中(band1名下的桶)。
这里就是重点设置分多少个组,每组多少个元素问题。这个可以根据实际情况和一些经验case来定。
这些hash function需要满足以下两个条件:
1)如果d(x,y) ≤ d1, 则h(x) = h(y)的概率 ≥ p1【距离近的,在同一个桶概率大】; 2)如果d(x,y) ≥ d2, 则h(x) = h(y)的概率 ≤ p2【距离远的,在同一个桶概率小】;
其中d(x,y)表示x和y之间的距离,d1 < d2, h(x)和h(y)分别表示对x和y进行hash变换。
满足以上两个条件的hash functions称为(d1,d2,p1,p2)-sensitive。而通过一个或多个(d1,d2,p1,p2)-sensitive的hash function对原始数据集合进行hashing生成一个或多个hash table的过程称为Locality-sensitive Hashing。
simhash在工业界引起很大注意力是因为google 07那篇文章,把Simhash技术引入到海量文本去重领域。
Detecting Near-Duplicates for Web Crawling
google 通过Simhash把一篇文本映射成64bits的二进制串。
【注:汉明距离就是a和b两个二进制串算xor,然后看有多少个1】
c++中快速计算1的个数: https://stackoverflow.com/questions/14682641/count-number-of-1s-in-binary-format-of-decimal-number/14682688#14682688
#include <bitset>
#include <iostream>
#include <climits>
size_t popcount(size_t n) {
std::bitset<sizeof(size_t) * CHAR_BIT> b(n);
return b.count();
}
int main() {
std::cout << popcount(1000000);
}
因为simhash本质上是局部敏感hash,所以可以使用海明距离来衡量simhash值的相似度。
试想所有文档都用64bits代表,如果想要找到汉明距离小于等于3得文档,也有需要两两比较的问题,那这样又回到时间复杂度是O(n^2)这个问题。
参考:我的数学之美系列二 —— simhash与重复信息识别
!!!!!!思想!!!!!!
假设我们要寻找海明距离3以内的数值,根据抽屉原理,只要我们将整个64位的二进制串划分为4块,无论如何,匹配的两个simhash code之间至少有一块区域是完全相同的。
由于我们无法事先得知完全相同的是哪一块区域,因此我们必须采用存储多份table的方式。
我们可以将这种方法拓展成多种配置,不过,请记住,table的数量与每个table返回的结果呈此消彼长的关系,也就是说,时间效率与空间效率不可兼得,参看下图:
问题:一个80亿的64-bit指纹组成的集合Q,对于一个给定64-bit的指纹F,如何在a few millionseconds中找到Q中和F至多只有k(k=3)位差别的指纹。
思想:
选取一个d’使得 | d’-d | 十分小,因此如果两fingerprint在d’-bits上都相同,那么在d-bits也很可能相同。 |
简单的说,就是利用fingerprint少量特征位数比较从而首先缩小范围,然后再去确定是否差异小于k个bit。
算法:
建表:
\(\pi _i\)
将64-bit的fingerprint中的某\(p_i\)
位序列置换换到整个序列的最前面。即每个表存储都是整个Q的fingerprint的复制置换。查询:
\(p_i\)
位与\(\pi _i(F)\)
的前\(p_i)
位相同的fingerprint。\(\pi _i(F)\)
至多有k-bit不同。算法的重点在于对于集合Q的分表以及每个表所对应的置换函数,假设对于64-bit的fingerprint,k=3,存储16个table,划分参考下图:
将64-bit按照16位划分为4个区间,每个区间剩余的48-bit再按照每个12-bit划分为4个区间,因此总共16个table并行查找,即使三个不同的k-bit落在A、B、C、D中三个不同的区块,此划分方法也不会导致遗漏。
而真正需要比较的是前16+12=28位,所以,如果有2^34个指纹,那么候选将变为2^(34-28)个结果。
最优table数的选取:
github: https://github.com/spotify/annoy
Annoy的目标是建立一个数据结构,使得查询一个点的最近邻点的时间复杂度是次线性。Annoy 通过建立一个二叉树来使得每个点查找时间复杂度是O(log n)。
查找的过程就是不断看他在分割超平面的哪一边。从二叉树索引结构来看,就是从根节点不停的往叶子节点遍历的过程。通过对二叉树每个中间节点(分割超平面相关信息)和查询数据节点进行相关计算来确定二叉树遍历过程是往这个中间节点左孩子节点走还是右孩子节点走。通过以上方式完成查询过程。
解决方法:
每棵树都返回一堆近邻点后,如何得到最终的Top N相似集合呢? 首先所有树返回近邻点都插入到优先队列中,求并集去重, 然后计算和查询点距离,最终根据距离值从近距离到远距离排序,返回Top N近邻节点集合。
对于小数据集和中型规模的数据集(几个million-几十个million), FALCONN(LSH开源实现)和NMSLIB是一个非常不错的选择,
如果对于大型规模数据集(几百个million以上,亿级别),基于矢量量化的Faiss是一个明智的选择。
矢量量化方法,即vector quantization,其具体定义为:将一个向量空间中的点用其中的一个有限子集来进行编码的过程。在矢量量化编码中,关键是码本的建立和码字搜索算法。
比如常见的聚类算法,就是一种矢量量化方法。而在ANN近似最近邻搜索中,向量量化方法又以乘积量化(PQ, Product Quantization)最为典型。