Locality Sensitive Hashing ( LSH,局部敏感哈希 ) 详解

这篇文章想给大家介绍一个神奇的东东:LSH首先看看它有什么用先~它可以快速地找出海量数据中的相似数据点,听着有点抽象?那我们来举个实际的例子,比如说你有海量的网页(这里的网页是指你拥有的本地数据,不是指互联网上的),你现在想找和一个特定网页相似的其它网页,就比如你想在海量的网页中找出和我这篇博文相似的网页~最naive的方法就是去遍历整个数据集,一个一个页面去比较,看看哪一个页面和我的这个页面最相似,当然所谓的相似需要你自己去定义,可以用Cosine Similarity、Jaccard Coefficient等去衡量(可以参考我的另一篇博文:常用相似性、相关性度量指标),至于特征向量的构造,那就有很多方法了,如使用分词后的各词的TF-IDF值,这里就不展开了。

     现在假设你已经对各个页面提取好了特征,那下面的事情就是:给定一个特征,如何快速地从海量的特征中找到和这个特征相似的一些特征?这时候LSH就闪亮登场了(鼓掌ing...),先看看它的名字,Locality Sensitive Hashing,局部敏感哈希。哈希大家应该都知道,它的查找时间复杂度是O(1),有着极高的查找性能,那什么叫“局部敏感”的哈希?它的意思就是:如果原来的数据相似,那么hash以后的数据也保持一定的相似性,这玩意就叫局部敏感哈希。

      来看看我们通常的哈希,比如有一个hash function: f(x)=(x*7)%10,有两个数据x1=123,x2=124,现在用f(x)把它们hash一下,f(x1)=1,f(x2)=8,这想说明什么呢?看看原来的数据,是不是很相似?(假设用欧氏距离度量)再看hash后的数据,那就差得远了,说明这个hash它并没有保持相似性,那么这种就不是局部敏感哈希。

     那么LSH就是一种在hash之后能保持一定的相似性神奇玩意儿,这里为什么说是“保持一定的相似性”?因为我们知道,hash函数的值域一般都是有限的,但是要哈希的数据却是无法预知的,可能数据大大超过其值域,那么就不可能避免地会出现一个以上的数据点拥有相同的hash值,这有什么影响呢?假设有一个很好的hash function,好到可以在hash之后很好地保持原始数据的相似性,假设它的不同取值数是10个,然后现在我有11个截然不相似的数据点,想用这个hash function来hash一下,因为这个hash function的不同取值数是10个,所以必然会出现一个hash桶中有1个以上的数据点,那刚才不是说11个截然不相似的数据点吗?它们应该有不同的hash值才对啊。没错,的确希望它是这样,但实际上很难,只能在hash之后保持一定的相似性。其根本原因就是在做相似性度量的时候,hash function通常是把高维数据映射到低维空间上,为什么映射到低维空间上?因为高维空间上计算复杂度太高。

     降维会对相似性度量造成什么影响?数据的维数在某种程度上能反映其信息量,一般来说维数越多,其反映的信息量就越大,比如说对于一个人,假设有一个一维数据:(姓名),有一个三维数据(姓名,身高,体重),那么这个三维数据反映的信息是不是要比一维的多?如果我们知道的信息越多,是不是就能越准确地判定两个东西的相似性?所以,降维就在某种程度上造成的信息的丢失,在降维后的低维空间中就很难100%保持原始数据空间中的相似性,所以刚才是说“保持一定的相似性”。

      好家伙,一方面想把数据降维,一方面又希望降维后不丢失信息,这是不可能的,那么就要做一个妥协了,双方都让一步,那么就可以实现在损失一点相似性度量准确性的基础上,把数据降维,通常来说,这是值得的。说了这么多,那LSH究竟是如何做的呢?先一句话总结一下它的思想吧:它是用hash的方法把数据从原空间哈希到一个新的空间中,使得在原始空间的相似的数据,在新的空间中也相似的概率很大,而在原始空间的不相似的数据,在新的空间中相似的概率很小。

            其实在用LSH前通常会进行一些降维操作,我们先看看下面这张图:

         先说说整个流程,一般的步骤是先把数据点(可以是原始数据,或者提取到的特征向量)组成矩阵,然后通过第一步的hash functions(有多个哈希函数,是从某个哈希函数族中选出来的)哈希成一个叫“签名矩阵(Signature Matrix)”的东西,这个矩阵可以直接理解为是降维后的数据,然后再通过LSH把Signature Matrix哈希一下,就得到了每个数据点最终被hash到了哪个bucket里,如果新来一个数据点,假如是一个网页的特征向量,我想找和这个网页相似的网页,那么把这个网页对应的特征向量hash一下,看看它到哪个桶里了,于是bucket里的网页就是和它相似的一些候选网页,这样就大大减少了要比对的网页数,极大的提高了效率。注意上句为什么说是“候选网页”,也就是说,在那个bucket里,也有可能存在和被hash到这个bucket的那个网页不相似的网页,原因请回忆前面讲的“降维”问题。。但LSH的巧妙之处在于可以控制这种情况发生的概率,这一点实在是太牛了,下面会介绍。

       由于采用不同的相似性度量时,第一步所用的hash functions是不一样的,并没有通用的hash functions,因此下面主要介绍两种情况:(1)用Jaccard相似性度量时,(2)用Cosine相似性度量时。在第一步之后得到了Signature Matrix后,第二步就都一样了。

          先来看看用Jaccard相似性度量时第一步的hash functions。

假设现在有4个网页(看成是document),页面中词项的出现情况用以下矩阵来表示,1表示对应词项出现,0则表示不出现,这里并不用去count出现了几次,因为是用Jaccard去度量的嘛,原因就不用解释了吧。

 好,接下来我们就要去找一种hash function,使得在hash后尽量还能保持这些documents之间的Jaccard相似度

         目标就是找到这样一种哈希函数h(),如果原来documents的Jaccard相似度高,那么它们的hash值相同的概率高,如果原来documents的Jaccard相似度低,那么它们的hash值不相同的概率高。这玩意叫Min-Hashing。
         
        Min-Hashing是怎么做的呢?请看下图:

        首先生成一堆随机置换,把Signature Matrix的每一行进行置换,然后hash function就定义为把一个列C hash成一个这样的值:就是在置换后的列C上,第一个值为1的行的行号。呼,听上去好抽象,下面看列子:


       图中展示了三个置换,就是彩色的那三个,我现在解释其中的一个,另外两个同理。比如现在看蓝色的那个置换,置换后的Signature Matrix为:


            然后看第一列的第一个是1的行是第几行,是第2行,同理再看二三四列,分别是1,2,1,因此这四列(四个document)在这个置换下,被哈希成了2,1,2,1,就是右图中的蓝色部分,也就相当于每个document现在是1维。再通过另外两个置换然后再hash,又得到右边的另外两行,于是最终结果是每个document从7维降到了3维。我们来看看降维后的相似度情况,就是右下角那个表,给出了降维后的document两两之间的相似性。可以看出,还是挺准确的,回想一下刚刚说的:希望原来documents的Jaccard相似度高,那么它们的hash值相同的概率高,如果原来documents的Jaccard相似度低,那么它们的hash值不相同的概率高,如何进行概率上的保证?Min-Hashing有个惊人的性质:

       就是说,对于两个document,在Min-Hashing方法中,它们hash值相等的概率等于它们降维前的Jaccard相似度。下面来看看这个性质的Proof:
         设有一个词项x(就是Signature Matrix中的行),它满足下式:         

         就是说,词项x被置换之后的位置,和C1,C2两列并起来(就是把两列对应位置的值求或)的结果的置换中第一个是1的行的位置相同。那么有下面的式子成立:

         就是说x这个词项要么出现在C1中(就是说C1中对应行的值为1),要么出现在C2中,或者都出现。这个应该很好理解,因为那个1肯定是从C1,C2中来的。

         那么词项x同时出现在C1,C2中(就是C1,C2中词项x对应的行处的值是1)的概率,就等于x属于C1与C2的交集的概率,这也很好理解,属于它们的交集,就是同时出现了嘛。那么现在问题是:已知x属于C1与C2的并集,那么x属于C1与C2的交集的概率是多少?其实也很好算,就是看看它的交集有多大,并集有多大,那么x属于并集的概率就是交集的大小比上并集的大小,而交集的大小比上并集的大小,不就是Jaccard相似度吗?于是有下式:


         又因为当初我们的hash function就是

         往上面一代,不就是下式了吗?

         这就证完了,这标志着我们找到了第一步中所需要的hash function,再注意一下,现在这个hash function只适用于Jaccard相似度,并没有一个万能的hash function。有了hash functions,就可以求Signature Matrix了,求得Signature Matrix之后,就要进行LSH了。
         首先将Signature Matrix分成一些bands,每个bands包含一些rows,如下图所示:

         然后把每个band哈希到一些bucket中,如下图所示:

       注意bucket的数量要足够多,使得两个不一样的bands被哈希到不同的bucket中,这样一来就有:如果两个document的bands中,至少有一个share了同一个bucket,那这两个document就是candidate pair,也就是很有可能是相似的。
       下面来看看一个例子,来算一下概率,假设有两个document,它们的相似性是80%,它们对应的Signature Matrix矩阵的列分别为C1,C2,又假设把Signature Matrix分成20个bands,每个bands有5行,那么C1中的一个band与C2中的一个band完全一样的概率就是0.8^5=0.328,那么C1与C2在20个bands上都没有相同的band的概率是(1-0.328)^20=0.00035,这个0.00035概率表示什么?它表示,如果这两个document是80%相似的话,LSH中判定它们不相似的概率是0.00035,多么小的概率啊!
       再看先一个例子,假设有两个document,它们的相似性是30%,它们对应的Signature Matrix矩阵的列分别为C1,C2,Signature Matrix还是分成20个bands,每个bands有5行,那么C1中的一个band与C2中的一个band完全一样的概率就是0.3^5=0.00243,那么C1与C2在20个bands至少C1的一个band和C2的一个band一样的概率是1-(1-0.00243)……20=0.0474,换句话说就是,如果这两个document是30%相似的话,LSH中判定它们相似的概率是0.0474,也就是几乎不会认为它们相似,多么神奇。
       更为神奇的是,这些概率是可以通过选取不同的band数量以及每个band中的row的数量来控制的:

        除此之外,还可以通过AND和OR操作来控制,就不展开了。
        
        呼,LSH的核心内容算是介绍完了,下面再简单补充一下当相似性度量是Cosine相似性的时候,第一步的hash function是什么。它是用了一个叫随机超平面(Random Hyperplanes)的东西,就是说随机生成一些超平面,哈希方法是看一个特征向量对应的点,它是在平台的哪一侧:

           这个可以直观地想象一下,假设有两个相交的超平面,把空间分成了4个区域,如果两个特征向量所对应的点在同一域中,那么这两个向量是不是挨得比较近?因此夹角就小,Cosine相似度就高。对比一下前面用Jaccard相似度时Signature Matrix的生成方法,那里是用了三个转换,在这里对应就是用三个随机超平面,生成方法是:对于一个列C(这里因为是用Cosine相似度,所以列的值就不一定只是0,1了,可以是其它值,一个列就对应一个特征向量),算出该列对应的特征向量的那个点,它是在超平面的哪个侧,从而对于每个超平面,就得到+1或者-1,对于三个超平面,就得到三个值,就相当于把原来7维的数据降到三维,和前面用Jaccard相似度时的情况是不是很像?得到Signature Matrix后,就进行LSH,步骤是一样的~~~~~~~

          参考:Stanford CS246课程PPT关于Locality Sensitive Hashing的章节,http://cs246.stanford.edu
--------------------- 
作者:Kenney_Qin 
来源:CSDN 
原文:https://blog.csdn.net/OrthocenterChocolate/article/details/38943491 
版权声明:本文为博主原创文章,转载请附上博文链接!

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
好的,以下是一个简单的Python实现: ``` import random def generate_random_vectors(num_vectors, dim): """ 生成随机向量 :param num_vectors: 向量数量 :param dim: 向量维度 :return: 随机向量列表 """ return [[random.uniform(-1, 1) for _ in range(dim)] for _ in range(num_vectors)] def hash_vector(vector, random_vectors): """ 将向量哈希为一个整数 :param vector: 向量 :param random_vectors: 随机向量列表 :return: 哈希值 """ result = 0 for i, random_vector in enumerate(random_vectors): if vector[i] > random_vector[i]: result |= 1 << i return result def lsh_similarity(text, texts, num_vectors=10, dim=100, num_permutations=50): """ 使用局部敏感哈希计算文本相似度 :param text: 待比对文本 :param texts: 比对文本列表 :param num_vectors: 随机向量数量 :param dim: 向量维度 :param num_permutations: 进行哈希的排列数量 :return: 相似度列表 """ # 生成随机向量 random_vectors = generate_random_vectors(num_vectors, dim) # 构建倒排索引 index = {} for i, t in enumerate(texts): for j in range(num_permutations): hash_value = hash_vector([ord(c) for c in t], random_vectors) if hash_value not in index: index[hash_value] = set() index[hash_value].add(i) # 计算待比对文本的哈希值 query_hash_value = hash_vector([ord(c) for c in text], random_vectors) # 查找相似文本 similar_texts = set() for hash_value in range(query_hash_value - 2, query_hash_value + 3): if hash_value in index: for i in index[hash_value]: similar_texts.add(i) # 计算相似度 similarities = [] for i in similar_texts: similarities.append((texts[i], calculate_similarity(text, texts[i]))) # 按相似度排序 similarities.sort(key=lambda x: x[1], reverse=True) return similarities def calculate_similarity(text1, text2): """ 计算文本相似度 :param text1: 文本1 :param text2: 文本2 :return: 相似度 """ # 这里使用简单的余弦相似度计算相似度 words1 = set(text1.split()) words2 = set(text2.split()) intersection = words1 & words2 union = words1 | words2 return len(intersection) / len(union) ``` 使用方法如下: ``` text = "待比对文本" texts = ["比对文本1", "比对文本2", ..., "比对文本10"] similarities = lsh_similarity(text, texts) for t, s in similarities: print(t, s) ``` 其中 `similarities` 列表中存储了每个相似文本及其相似度,按照相似度从高到低排序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值