局部敏感哈希
局部敏感哈希算法(Locality Sensitive Hashing,LSH)
LSH是一种利用hash的方法,对向量进行快速近邻检索的方法,能高效处理海量高维数据的最近邻问题。LSH也是一种降维技术。
一般的hash算法我们知道当两个内容比较接近但不完全相同时hash值可能有比较大的差别,比如md5签名算法。而“局部敏感哈希”的意思是:如果原来的数据相似,那么hash以后的数据也保持一定的相似性,反之,原来不近似的数据再hash之后也要保持不相似。局部敏感hash值相当于程度比较大的降维。
是不是和布隆过滤器有一些相同的地方?都采用了多个hash函数。
LSH的实现有多种,如:随机投影超平面、simhash、minhash
随机投影超平面
对于n维向量x,进行f次hash,得到f维的签名向量。每一次hash的步骤是
- 随机生成一个n维向量v
- 计算投影 x·v 的符号
- hash值是:大于0则为1,小于0则为0
这个算法相当于随机产生了f个n维超平面,每个超平面将向量v所在的空间一分为二,v在这个超平面上方则得到一个1,否则得到一个0,然后将得到的 f个0或1组合起来成为一个f维的签名。如果两个向量u,v的夹角为θ,则一个随机超平面将它们分开的概率为θ/π,因此u, v的签名的对应位不同的概率等于θ/π。所以,我们可以用两个向量的签名的不同的对应位的数量,即汉明距离,来衡量这两个向量的差异程度。
Bucketed Random Projection for Euclidean Distance
Bucketed Random Projection is an LSH family for Euclidean distance.
哈希函数定义为:\(h(\mathbf{x}) = \Big\lfloor \frac{\mathbf{x} \cdot \mathbf{v}}{r} \Big\rfloor\)
\(\mathbb v\)是随机单位向量,r 是设置的桶的长度,采用多个hash函数构成的hash family将输入\(\mathbb x\)映射到多个hash分桶。
这和随机投影超平面的区别仅是分桶多了一些,不仅是0/1.
投影 x·v 是将高维的x投影到了低维(一维),而高维距离较近的点在低维距离也会比较接近(但是三个点之间相对远近的关系可能发生变化,比如高维a比b离c近,投射到低维变成b比a离c近,但是整体上所有点之间的远近关系还能保持)。因此计算两个向量的hash签名的汉明距离就能粗略衡量两个高维向量之间的差异。
simhash
simhash可以认为是由随机投影超平面hash算法演变而来的。
对于文档词典的每个word的向量可以认为是随机初始化的,文档向量是multi-hot编码乘上权重,第i维输出可以认为是文档向量与所有word的第i维组成的向量的乘积。
不同之处在于simhash为每个word添加了重要度的乘积项。
算法流程:分词 => 散列 => 加权 => 降维
-
分词
对文档进行词汇划分,并给每个词向量设置一个1~5的权重.权重越大代表其越重要.
-
散列
对每个词向量生成一个固定长度的01散列串.如hash("文章")=[1 0 0 1 0 1]
-
加权
对散列值加权重,其中0当做-1,如权重为3,则加权后的结果为[3 -3 -3 3 -3 3]
-
合并
将文档包含的所有词的加权向量累加.
-
降维
合并后的结果中大于0的位置1,否则置0.
对于64位的simhash签名,汉明距离在3以内可认为相似度比较高.为了提高查找效率,可以将64位分成4块,对每块建立倒排索引.这样查找相似的文档时把每块分别作为前16位进行查找.
MinHash for Jaccard Distance
MinHash is an LSH family for Jaccard distance.
我们知道两个集合的Jaccard相似度为: \(S(A,B) = |A∩B|/|A∪B|\),距离定义为:\(d(\mathbf{A}, \mathbf{B}) = 1 - \frac{|\mathbf{A} \cap \mathbf{B}|}{|\mathbf{A} \cup \mathbf{B}|}\)。直接通过交并集进行计算比较耗费计算资源。两个集合的Jaccard相似度也等价于随机从两个集合中各挑选一个元素,这两个元素恰好相同的概率。minhash正是基于这一原理,将(待降维的)输入向量转换成集合,用集合的相似度度量输入向量的相似度。而集合的相似度通过如下方式来近似:
(1)采用随机的哈希函数h, 对集合的每一个元素作哈希运算,记录哈希值最小的元素对应的位置作为minhash值。由于哈希函数是随机的,所以集合中每个元素被选择作为输出的概率都是相等的。两个集合的minhash值相等的概率就是集合的相似度。
(2)重复,采用N个随机哈希函数,得到集合的N维的输出向量。
对于文档来说,如果两个文档足够相似,也就是说这两个文档中有很多元素(通常是词汇)是共有的,通过随机置换文档词典中词汇的先后顺序之后统计出来的最小值签名向量相等的概率也越大。
下面用一个文档的minhash过程作为示例:数据包含4个文档以及一个大小为7的词典,每个文档由若干个词组成。
表1
word\doc | D1 | D2 | D3 | D4 |
---|---|---|---|---|
w1 | 1 | 0 | 1 | 0 |
w2 | 1 | 1 | 0 | 1 |
w3 | 0 | 1 | 0 | 1 |
w4 | 0 | 0 | 0 | 1 |
w5 | 0 | 0 | 0 | 1 |
w6 | 1 | 1 | 1 | 0 |
w7 | 1 | 0 | 1 | 0 |
首先,对表1进行按行随机置换得到表2。
表2
word\doc | D1 | D2 | D3 | D4 |
---|---|---|---|---|
w2 | 1 | 1 | 0 | 1 |
w1 | 1 | 0 | 1 | 0 |
w4 | 0 | 0 | 0 | 1 |
w3 | 0 | 1 | 0 | 1 |
w7 | 1 | 0 | 1 | 0 |
w6 | 1 | 1 | 1 | 0 |
w5 | 0 | 0 | 0 | 1 |
置换词典中词的顺序并没有改变文档与词项的关系。对这个矩阵按行进行多次随机置换,每次置换之后,统计每一列(每个文档)第一个不为0的位置(行号)。多次置换完毕后得到每个文档的签名向量。
这样理解起来很直观,但是实际上当词典很大时做随机置换(permutation)的时间复杂度很高,通常我们可以使用一个针对row index的哈希函数来达到permutation的效果,虽然可能会有哈希碰撞的情况产生,但是只要碰撞的概率不大,对估计的结果没有大的影响。
至于哈希函数的选择,可以参考Spark中MinHash算法的实现,核心代码如下:
import org.apache.spark.mllib.linalg.SparseVector
import scala.util.Random
/**
* @param hashNum 签名向量的维度, hash函数的个数
*/
class MinHash(hashNum: Int) extends Serializable {
val HASH_PRIME=2038074743
val rand = new Random()
/**
* n个随机哈希函数的参数配置,h(x)=ax+b
*/
val randCoefs: Array[(Int, Int)] = Array.fill(hashNum) {
(1 + rand.nextInt(HASH_PRIME - 1), rand.nextInt(HASH_PRIME - 1))
}
def generateSignature(vector: SparseVector): Array[Int] = {
val indexes = vector.indices
val signatureVector = randCoefs.map {
case (a, b) =>
indexes.map(index => ((1 + index) * a + b) % HASH_PRIME).min
}
signatureVector
}
}
Locality Sensitive Hashing
前边的minhash等算法只是降低了向量维度,但是在进行近邻查找时依然要两两计算相似度。我们可以继续应用分桶的思想来进一步降低近邻查找的时间复杂度。将minhash得到的签名向量分成几段(band),每段分别进行哈希分桶。相似的向量的各段应尽可能落在相同的桶内,这样在做近邻查找时只需要分段哈希后取各个桶内对应的其它元素。这种做法其实也是倒排索引。
LSH应用:
- 查找相似新闻网页、文章、文件等
- 图像、音视频检索
- 指纹匹配
参考: