海量数据相似性度量与聚类: LHS-MinHash

海量数据相似性度量与聚类: LHS-MinHash

  写本文的原因是近期在涉猎用户画像相关的无监督学习理论,刚好看到一篇运用LHS-MinHash做用户聚类的文章,却讲得过于笼统,对我这样的萌新(菜鸡)不太友好。于是我去搜索了关于LHS-MinHash和simhash的相关博客,有的写得非常不负责,甚至误导了我,有的写的比较详细,但部分细节总感觉有点断片,好像漏掉了什么。同时,这些博客的内容比较相似,原以为是互相借鉴的,后来发现它们都是复述Stanford的大数据课程,又没写清楚。因此我也来总结一篇,只求我自己搞清楚LHS-MinHash的原理和用途。因此,本篇文章的大部分内容将源于Stanford课程(下称课程)Mining of Massive Datasets的第三章,有兴趣的同学可以自行查阅。
  对待社交网络中每天都在更新的千亿级别的无标签数据,传统的聚类方法效率太低了,比方说Kmeans,每个样本都必须与所有候选中心计算相似度,才能进行归类,因此算法的时间复杂度是 O ( N K ) ,其中 K 是聚类数, N 是样本数(上千亿),太奢侈了,根本无法实现日均多次滚动,无法上线或产品化。聚类的核心是“发现相似物品”,这与其他的一些问题是异曲同工的,比如社区检测(Community Detection),海量数据查重等等。Stanford课程中举的例子就是海量网页的查重,为了高效地解决这个问题,课程依次提出了“Shingling”和“MinHash”算法,而针对数据量过大的问题,提出了“LSH hashing”,本文也将挑取其中的“MinHash”和“LSH hashing”进行总结。

预备知识:Jaccard 相似度

J a c c a r d ( X , Y ) = | X Y | | X Y |


MinHash

  对于一个网页(或文章),我们可以用Word2vec、用BOW、用k-shingle来表达,这些方法无一例外地占用非常大的空间,使得文章本身的存储就是一个问题,更不要提文章之间的相似度运算了。如果将word看成特征,那么文章就是在一个高维词空间中的向量,对于这样一种数据表达,很容易想到对它进行降维,minhash正是这么一个思路。为了讲清楚minhash,首先要定义一种集合的矩阵表达。
  假设全集为 { a , b , c , d , e } ,四个子集分别是 S 1 = { a , d } S 2 = { c } S 3 = { b , d , e } S 4 = { a , c , d } 。则可以定义一个叫“characteristic matrix”的矩阵 C ( r , c ) ,矩阵的列对应一个子集,矩阵的行对应全集里的一个元素,若第 c 个子集中包含元素 r ,则 C ( r , c ) = 1 ,否则 C ( r , c ) = 0 ,如下图所示。为了方便理解,可以将characteristic matrix的行看成物品(product),列看成用户(customer),该矩阵的意义即用户的购买历史列表。显然,characteristic matrix是一个极度稀疏的高维矩阵。


Minhashing

  为了方便解释,下面都把子集称为“用户”,把元素称为“商品”好了。所谓“MinHash”(最小哈希),就是指将上面的characteristic matrix的行随机打乱,然后取每一列中第一个非零元素的行号作为该用户的“MinHash Value”(最小哈希值),进行 d 次打乱产生 d 个“MinHash Value”,构成该用户的“MinHash Signature”(最小哈希签名)向量,由于签名向量的维度通常远低于商品个数,因此相当于做了降维。使用降维的特征向量来衡量用户之间的相似度。
  以上是MinHash的综述,光这么讲很难理解,下面给出一个例子。假设随机打乱函数 h ,将矩阵的行顺序变成了 b e a d c ,如下图所示, S 1 列的首个非零元素是 a ,则用 a 作为 S 1 的签名,即 h ( S 1 ) = a 。同理,可以得到 h ( S 2 ) = c h ( S 3 ) = b h ( S 4 ) = a


  之所以称之为“MinHash”,我认为,characteristic matrix的每一列都可以看作是对一个用户的哈希化,不同的行排列方式能获得不一样的哈希串,如果两个用户的购买列表足够相似,那么两个哈希串也将是很相似的。而“最小”体现在取“第一个非零元素的所在行”。为什么要费尽心思定义这样一个奇怪的哈希值呢?
  是由于一个神奇的定理:两个集合的MinHash值相等的概率,等于这两个集合的Jaccard相似度。
  首先证明该定理。现在我们拿 S 1 S 2 为例,为它们的characteristic matrix行的构成定义三种情况: X Y Z

  • X :该行两列都是1
  • Y :该行只有一列是1,另一列是0
  • Z :该行两列都是0

  由于矩阵极其稀疏,可见大部分情况都是 Z ,但它对我们的计算没有贡献,不用管。假设 X 情况的行数为 x Y 情况的行数为 y ,则两个用户的相似性为 J a c c a r d ( S 1 , S 2 ) = x / ( x + y ) ,如果想不明白可以对照上面的韦恩图看看。
  然后考虑 h ( S 1 ) = h ( S 2 ) 的概率。假设矩阵随机排列过,我们从头遍历矩阵,那么 X Y 前面的概率是 x / ( x + y ) 。如果最上面的行是 X 情况(更上面是 Z ),那么显然 h ( S 1 ) = h ( S 2 ) 。如果最上面的行是 Y ,则其中一个集合的MinHash Value就是1(第一行),另一个集合的MinHash Value不可能是1,于是 h ( S 1 ) h ( S 2 ) 。因此一旦我们发现最上面的行是 X ,才有 h ( S 1 ) = h ( S 2 ) ,因此它的概率就是 X Y 前面的概率,即 x / ( x + y ) ,而它与两个用户的Jaccard相似度相等。

Minhash 签名

  上面说了要对矩阵进行若干次随机重排,但是对于一个可能有上亿行的矩阵随机重排,是一件相当耗时的事情。课程提出了使用 n 个哈希函数处理“行号”,构造出一个 n 行的签名矩阵 S I G ( i , c ) 。具体算法如下,简单来说,在初始化 S I G 矩阵之后,遍历原矩阵的每一行(商品行),找到非零的列(用户列),将 S I G 矩阵的对应列用 h 哈希计算的行号进行替换,替换规则是保留较小值。


  例如哈希函数 h 1 ( i ) = ( i + 1 ) % 5 h 2 ( i ) = ( 3 i + 1 ) % 5 ,此处的 i 表示行号 R o w ,例如行号 R o w = 2 h 1 ( 2 ) = ( 2 + 1 ) % 5 = 3 h 2 ( 2 ) = ( 3 2 + 1 ) % 5 = 2


  我们根据算法一步一步来,首先将 S I G 矩阵全部初始化为无穷。


  然后我们看原矩阵的第0行, S 1 S 4 列非零,进行替换。第一行 h 1 = 1 h 2 = 1 ,而1比无穷小,因此全部替换为1。


  然后我们看原矩阵的第1行,只有 S 3 列非零,继续替换。


  然后我们看原矩阵的第2行, S 2 S 4 列非零, S 2 列显然是全替换,而 S 4 列由于 S I G 矩阵上原有的值都较小,所以进行保留,不做替换。


  然后我们看原矩阵的第3行, S 1 S 3 S 4 列非零,继续上面的规则替换,可以看到由于 h 2 = 0 较小,所以 S I G 矩阵的 S 1 S 3 S 4 列的 h 2 行都变成了0。


  终于来到了最后一行, S 3 列非零, h 1 行变成了0。


  上面这个矩阵就是最终的 S I G 矩阵,我们可以初步判定 S 1 S 4 的相似度为1,但实际上两个用户的Jaccard相似度是2/3,这是由于这个例子使用的哈希函数太少了,如果取的个数足够多,由签名矩阵计算的相似度会接近于真实的Jaccard相似度。

LSH

  铺垫了那么久终于来到LSH(Locality-Sensitive Hashing)了。MinHash从商品维度上进行了降维,但我们想要计算用户相似度还是很困难,原因是用户也很多,某个著名的社交软件日活跃用户就有十亿,这样的用户pair还是太多了。举个例子,我们有一百万用户(这不过分吧),那么就要计算 C 1000000 2 总共上万亿个pair的相似度,那么十亿用户呢?(手动滑稽)一天是跑不完的。
  如果我们的目的就是“计算任意两个用户之间的相似度”,那我们只能做并行,用一个集群计算相似度。但一般情况下,我们只想要知道“某些很相似的pair的相似度”,我们只需要关注那些“比较可能相似的pair”,而忽略剩下的pair。这就是LSH的核心思想。一般地,对一个用户(比如字符串)进行hash,它会被分到某个桶(bucket)中,经过hash而被分到同一个bucket的用户,我们有较大把握认为它们是相似的,称为“candidate pair”,只需要计算candidate pair的相似度就好了,其他的pair可以忽略。
  MinHash签名矩阵的每一列实际上正是用户的hash,一种高效的做法是把签名矩阵分成若干个条状块,每一块有 r 行。对每一块都配一hash函数,按照用户的这 r 行数值进行hash,从而分桶。可以让每一快使用同一个hash函数,但它们使用的桶是不一样的,所以不用担心不同的块里面因为 r 行数值相同而被分到同一个bucket里。
  一脸懵逼吗?不要紧,来看例子。假设我们使用了12个hash函数构造了一个签名矩阵,现在把它切成4块,每块3行,如下图所示。第一块里的第2和4列都是[0,2,1],所以即使在其他的band里面这两列(用户)的MinHash Value完全不同,他们也还是会被这个band的hash函数分到同一个bucket里面,成为“candidate pair”。然后看第1和第2列,[1,3,0]和[0,2,1],它们是不同的,但还是有可能在其他band里面相同。另外,假如第3列([0,1,3])在band2中是[0,2,1],而band2与band1使用了同一个hash函数,但它们的bucket是不一样的,所以第三列(用户)并不会被分到band1中第2和第4列所在的那个bucket中。
  这个分桶的操作就等价于聚类,用户的每一行哈希值都是一个特征,我们可以认为band1有着某种隐藏属性,例如“肥宅属性”,而用户2和用户4在肥宅范围内是很相似的,这就是LSH的物理意义。关于聚类的个数,就等于我们分得的桶的个数,这个数值并不是固定的,它与我们的训练数据有关,比如刚才举的这个例子,在band1里就能分出4个桶[1,3,0]、[0,2,1]、[0,1,3]、[2,2,1]。
  当我们构建好签名矩阵之后,我们的模型就是这个签名矩阵,而构造它所选取的哈希函数是固定的,相当于选择了固定的特征,那么,以后新进来一个用户(的列表)query,我们就可以按部就班地对它计算MinHash签名矩阵(向量),然后在各个band中,使用band相应的哈希函数将该用户分到 b 个不同的群体(bucket)之中,从而达到聚类的效果。值得注意的是,这种聚类是有Overlap的,也就是说同一个用户,可能会被同时分到多个桶之中。


概率分析

  现在我们来分析一下LSH的一些概率,从而进一步探究LSH的意义。假设上面的 n 行签名矩阵被分成了 b 个band,每个band有 r 行(即满足 b r = n ),现在考虑一个candidate pair(比如假设 S 1 S 2 是一对),已知它们的Jaccard相似度为 p

  1. 某一个band中, S 1 S 2 的签名完全一样的概率: p r
  2. 某一个band中, S 1 S 2 的签名至少有一行不一样的概率: 1 p r
  3. 在所有band中,都有 S 1 S 2 的签名至少有一行不一样的概率: ( 1 p r ) b
  4. 至少有一个band, S 1 S 2 的签名完全一样的概率: 1 ( 1 p r ) b

  我们主要关注最后两个概率,第三个概率描述了一种特殊情况, S 1 S 2 不论在哪个band里,都没有完全一样,即使他们的Jaccard相似度很高。最后一个概率曲线如下图所示,横轴是 p ,纵轴是至少有一个band, S 1 S 2 的签名完全一样的概率。举个例子, S 1 S 2 的Jaccard相似度是0.8, r =5, b =20, 则 0.8 5 = 0.33 1 ( 1 0.8 5 ) 20 = 0.9997 。也就是说如果两个用户有80%的相似性,虽然他们只有33%的可能性哈希签名完全一样,但签名矩阵被分成了20块,它们还是有20次机会成为一组candidate pair,只有 1 0.9997 = 1 / 3000 的概率无法成为一组candidate pair(这种情况被称为“false negative”),因此必须指出的是,该算法有一定概率产生假反例,即两个相似用户被判定为非相似。
  这堆概率说明了什么呢?如果两个用户的Jaccard相似度很高,那么LSH将他们分到同一个bucket的概率就会很高,反过来说,如果两个用户被LSH分到了同一个bucket中,那么他们的真实购买历史也有很高的概率非常相似。这就是为什么LSH具有聚类的功能,它实现了聚类的核心:寻找相似的用户并将他们放在一起。然而,必须指出,LSH所聚出的类别,只是“比较高概率”是相似的用户群,我们最终还是要老老实实地用购买列表来计算相似度,只不过,从原来的“对所有用户pair都计算相似度”变成了“仅在较可能相似用户群体中计算相似度”,大大地减少了时间复杂度。


LSH算法步骤

  总结一下我们在做的事情:寻找candidate pair作为候选相似用户,从而计算它们之间的相似性,进而判断出真正的相似用户。

  1. 对用户属性列表做分词或k-shingles,构造原矩阵(行是词,列是用户)
  2. 选择一个维度 n (哈希函数个数),对原矩阵计算 n 行MinHash,构造MinHash签名矩阵
  3. 选择分块数 b ,块行数 r b r = n ),设定一个阈值 t ( 1 / b ) 1 / r
  4. 对签名矩阵使用LSH,将用户分桶,构造出candidate pair
  5. 检查每一个candidate pair是不是false negative/postive
  6. 对于签名相似的candidate pair,检查用户购买列表是否真的相似

LSH时间复杂度分析

  MinHash首先要对所有商品行( N 个商品)分别做 n 次行号哈希变换( n 个哈希函数),因此复杂度是 O ( n N ) ,接着需要遍历商品行并用哈希过的行号进行替换,复杂度是 O ( n N ) ,因此整个签名矩阵的构建复杂度是 O ( n N ) 。接着就是LSH分桶,对于每个band( b 个)都遍历一次用户列(有 M 个用户),将用户哈希到对应的桶,因此复杂度是 O ( b M ) 。因此整个LSH的复杂度应该是 O ( n N ) + O ( b M )
  如果不使用LSH,对所有用户进行聚类,并用用户在商品张成的基的投影计算余弦相似度,那么时间复杂度是 O ( M 2 N ) ,可见LSH不仅降低了空间复杂度,还降低了时间复杂度。

LSH总结

  1. LSH的思想是,我们并不需要对所有的用户pair计算相似度。
  2. LSH算法被用于相似性检测,它对用户-商品矩阵计算MinHash签名,然后分band进行分桶,从而达到聚类的效果,在空间复杂度和时间复杂度上都要优于暴力聚类。
  3. LSH的聚类结果是有Overlap的。
  4. LSH的聚类是“比较有可能相似的物品集合”,最好用于初步筛选,还需进一步验证相似性
  5. LSH的聚类结果有一定概率有误:相似物品没有被聚在一起,非相似物品被聚在一起。

SimHash

  MinHash签名有一个问题,它无法处理带权数据。也就是说它平等地对待每一个物品(行),假如我们具备一定的先验知识,得到了每个物品的重要性,或者某个权重,MinHash就无法处理了。这里介绍一种能处理带权物品的hash,SimHash。它的提出是基于普通hash函数的缺陷:即使两个字符串之间只有一点不同,hash过后的串也大不相同。换句话说,无法将相似性度量扩散到hash之后的串。SimHash的提出正是为了能够在hash过后的串上衡量两个原串的相似性,它能使得两个比较相似的串在hash过后的串也能具有较强的相似性(汉明距离),甚至完全一样。

  1. 将文章分词
  2. 计算每个词的权重(例如TF-IDF)和普通hash串
  3. 根据hash串的取值将词Embedding为正负权值交替的向量
  4. 将向量求和,并按照向量的正负得到最终的SimHash

  下面举个例子。我们先将文章分词,将它当成一个Bag-of-Words模型,然后为每个词计算一个重要性,比如计算TF-IDF值,作为它的权值 w ,同时把词本身进行hash,取后6位。接着,按照hash传embedding,串取1的位置赋 w ,串取0的位置赋 w ,比如串“001011”,则第1、2、4位取 w ,第3、5、6位取 w ,得到该单词的Embedding w , w , w , w , w , w 。所有单词都计算完Embedding之后,做向量加法,然后根据正负,再二值化成01串。比如 [102,57,-40,-32,66,-7] 的第1、2、5位是正数,则取1,第3、4、6为是负数,则取0,最终得到“110010”,即该文档的Simhash。


  SimHash与MinHash一样,都是将文档从词向量空间进行降维,最终的效果都是得到“文章-哈希签名”这样一个对应关系。只是SimHash支持带权基,MinHash不支持。


参考资料

猜你喜欢

转载自blog.csdn.net/songbinxu/article/details/80879377