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

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

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

预备知识:Jaccard 相似度

Jaccard(X,Y)=|XY||XY|


MinHash

  对于一个网页(或文章),我们可以用Word2vec、用BOW、用k-shingle来表达,这些方法无一例外地占用非常大的空间,使得文章本身的存储就是一个问题,更不要提文章之间的相似度运算了。如果将word看成特征,那么文章就是在一个高维词空间中的向量,对于这样一种数据表达,很容易想到对它进行降维,minhash正是这么一个思路。为了讲清楚minhash,首先要定义一种集合的矩阵表达。
  假设全集为{a,b,c,d,e},四个子集分别是 S1={a,d}S2={c}S3={b,d,e}S4={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,将矩阵的行顺序变成了beadc,如下图所示,S1列的首个非零元素是a,则用a作为S1的签名,即h(S1)=a。同理,可以得到h(S2)=ch(S3)=bh(S4)=a


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

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

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

Minhash 签名

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


  例如哈希函数h1(i)=(i+1)%5h2(i)=(3i+1)%5,此处的i表示行号Row,例如行号Row=2h1(2)=(2+1)%5=3h2(2)=(32+1)%5=2


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


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


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


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


  然后我们看原矩阵的第3行,S1S3S4列非零,继续上面的规则替换,可以看到由于h2=0较小,所以 SIG 矩阵的S1S3S4列的h2行都变成了0。


  终于来到了最后一行,S3列非零,h1行变成了0。


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

LSH

  铺垫了那么久终于来到LSH(Locality-Sensitive Hashing)了。MinHash从商品维度上进行了降维,但我们想要计算用户相似度还是很困难,原因是用户也很多,某个著名的社交软件日活跃用户就有十亿,这样的用户pair还是太多了。举个例子,我们有一百万用户(这不过分吧),那么就要计算C10000002总共上万亿个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行(即满足br=n),现在考虑一个candidate pair(比如假设S1S2是一对),已知它们的Jaccard相似度为p

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

  我们主要关注最后两个概率,第三个概率描述了一种特殊情况,S1S2不论在哪个band里,都没有完全一样,即使他们的Jaccard相似度很高。最后一个概率曲线如下图所示,横轴是p,纵轴是至少有一个band,S1S2的签名完全一样的概率。举个例子,S1S2的Jaccard相似度是0.8,r=5,b=20, 则0.85=0.331(10.85)20=0.9997。也就是说如果两个用户有80%的相似性,虽然他们只有33%的可能性哈希签名完全一样,但签名矩阵被分成了20块,它们还是有20次机会成为一组candidate pair,只有10.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,块行数rbr=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(nN),接着需要遍历商品行并用哈希过的行号进行替换,复杂度是O(nN),因此整个签名矩阵的构建复杂度是O(nN)。接着就是LSH分桶,对于每个band(b个)都遍历一次用户列(有M个用户),将用户哈希到对应的桶,因此复杂度是O(bM)。因此整个LSH的复杂度应该是O(nN)+O(bM)
  如果不使用LSH,对所有用户进行聚类,并用用户在商品张成的基的投影计算余弦相似度,那么时间复杂度是O(M2N),可见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不支持。


参考资料

阅读更多
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页