海量数据相似性度量与聚类: LHS-MinHash
写本文的原因是近期在涉猎用户画像相关的无监督学习理论,刚好看到一篇运用LHS-MinHash做用户聚类的文章,却讲得过于笼统,对我这样的萌新(菜鸡)不太友好。于是我去搜索了关于LHS-MinHash和simhash的相关博客,有的写得非常不负责,甚至误导了我,有的写的比较详细,但部分细节总感觉有点断片,好像漏掉了什么。同时,这些博客的内容比较相似,原以为是互相借鉴的,后来发现它们都是复述Stanford的大数据课程,又没写清楚。因此我也来总结一篇,只求我自己搞清楚LHS-MinHash的原理和用途。因此,本篇文章的大部分内容将源于Stanford课程(下称课程)Mining of Massive Datasets的第三章,有兴趣的同学可以自行查阅。
对待社交网络中每天都在更新的千亿级别的无标签数据,传统的聚类方法效率太低了,比方说Kmeans,每个样本都必须与所有候选中心计算相似度,才能进行归类,因此算法的时间复杂度是
,其中
是聚类数,
是样本数(上千亿),太奢侈了,根本无法实现日均多次滚动,无法上线或产品化。聚类的核心是“发现相似物品”,这与其他的一些问题是异曲同工的,比如社区检测(Community Detection),海量数据查重等等。Stanford课程中举的例子就是海量网页的查重,为了高效地解决这个问题,课程依次提出了“Shingling”和“MinHash”算法,而针对数据量过大的问题,提出了“LSH hashing”,本文也将挑取其中的“MinHash”和“LSH hashing”进行总结。
预备知识:Jaccard 相似度
MinHash
对于一个网页(或文章),我们可以用Word2vec、用BOW、用k-shingle来表达,这些方法无一例外地占用非常大的空间,使得文章本身的存储就是一个问题,更不要提文章之间的相似度运算了。如果将word看成特征,那么文章就是在一个高维词空间中的向量,对于这样一种数据表达,很容易想到对它进行降维,minhash正是这么一个思路。为了讲清楚minhash,首先要定义一种集合的矩阵表达。
假设全集为
,四个子集分别是
,
,
,
。则可以定义一个叫“characteristic matrix”的矩阵
,矩阵的列对应一个子集,矩阵的行对应全集里的一个元素,若第
个子集中包含元素
,则
,否则
,如下图所示。为了方便理解,可以将characteristic matrix的行看成物品(product),列看成用户(customer),该矩阵的意义即用户的购买历史列表。显然,characteristic matrix是一个极度稀疏的高维矩阵。
Minhashing
为了方便解释,下面都把子集称为“用户”,把元素称为“商品”好了。所谓“MinHash”(最小哈希),就是指将上面的characteristic matrix的行随机打乱,然后取每一列中第一个非零元素的行号作为该用户的“MinHash Value”(最小哈希值),进行
次打乱产生
个“MinHash Value”,构成该用户的“MinHash Signature”(最小哈希签名)向量,由于签名向量的维度通常远低于商品个数,因此相当于做了降维。使用降维的特征向量来衡量用户之间的相似度。
以上是MinHash的综述,光这么讲很难理解,下面给出一个例子。假设随机打乱函数
,将矩阵的行顺序变成了
,如下图所示,
列的首个非零元素是
,则用
作为
的签名,即
。同理,可以得到
,
,
。
之所以称之为“MinHash”,我认为,characteristic matrix的每一列都可以看作是对一个用户的哈希化,不同的行排列方式能获得不一样的哈希串,如果两个用户的购买列表足够相似,那么两个哈希串也将是很相似的。而“最小”体现在取“第一个非零元素的所在行”。为什么要费尽心思定义这样一个奇怪的哈希值呢?
是由于一个神奇的定理:两个集合的MinHash值相等的概率,等于这两个集合的Jaccard相似度。
首先证明该定理。现在我们拿
和
为例,为它们的characteristic matrix行的构成定义三种情况:
,
和
。
- :该行两列都是1
- :该行只有一列是1,另一列是0
- :该行两列都是0
由于矩阵极其稀疏,可见大部分情况都是
,但它对我们的计算没有贡献,不用管。假设
情况的行数为
,
情况的行数为
,则两个用户的相似性为
,如果想不明白可以对照上面的韦恩图看看。
然后考虑
的概率。假设矩阵随机排列过,我们从头遍历矩阵,那么
在
前面的概率是
。如果最上面的行是
情况(更上面是
),那么显然
。如果最上面的行是
,则其中一个集合的MinHash Value就是1(第一行),另一个集合的MinHash Value不可能是1,于是
。因此一旦我们发现最上面的行是
,才有
,因此它的概率就是
在
前面的概率,即
,而它与两个用户的Jaccard相似度相等。
Minhash 签名
上面说了要对矩阵进行若干次随机重排,但是对于一个可能有上亿行的矩阵随机重排,是一件相当耗时的事情。课程提出了使用 个哈希函数处理“行号”,构造出一个 行的签名矩阵 。具体算法如下,简单来说,在初始化 矩阵之后,遍历原矩阵的每一行(商品行),找到非零的列(用户列),将 矩阵的对应列用 哈希计算的行号进行替换,替换规则是保留较小值。
例如哈希函数 和 ,此处的 表示行号 ,例如行号 , , 。
我们根据算法一步一步来,首先将 矩阵全部初始化为无穷。
然后我们看原矩阵的第0行, 和 列非零,进行替换。第一行 , ,而1比无穷小,因此全部替换为1。
然后我们看原矩阵的第1行,只有 列非零,继续替换。
然后我们看原矩阵的第2行, 和 列非零, 列显然是全替换,而 列由于 矩阵上原有的值都较小,所以进行保留,不做替换。
然后我们看原矩阵的第3行, 、 和 列非零,继续上面的规则替换,可以看到由于 较小,所以 矩阵的 、 和 列的 行都变成了0。
终于来到了最后一行, 列非零, 行变成了0。
上面这个矩阵就是最终的 矩阵,我们可以初步判定 和 的相似度为1,但实际上两个用户的Jaccard相似度是2/3,这是由于这个例子使用的哈希函数太少了,如果取的个数足够多,由签名矩阵计算的相似度会接近于真实的Jaccard相似度。
LSH
铺垫了那么久终于来到LSH(Locality-Sensitive Hashing)了。MinHash从商品维度上进行了降维,但我们想要计算用户相似度还是很困难,原因是用户也很多,某个著名的社交软件日活跃用户就有十亿,这样的用户pair还是太多了。举个例子,我们有一百万用户(这不过分吧),那么就要计算
总共上万亿个pair的相似度,那么十亿用户呢?(手动滑稽)一天是跑不完的。
如果我们的目的就是“计算任意两个用户之间的相似度”,那我们只能做并行,用一个集群计算相似度。但一般情况下,我们只想要知道“某些很相似的pair的相似度”,我们只需要关注那些“比较可能相似的pair”,而忽略剩下的pair。这就是LSH的核心思想。一般地,对一个用户(比如字符串)进行hash,它会被分到某个桶(bucket)中,经过hash而被分到同一个bucket的用户,我们有较大把握认为它们是相似的,称为“candidate pair”,只需要计算candidate pair的相似度就好了,其他的pair可以忽略。
MinHash签名矩阵的每一列实际上正是用户的hash,一种高效的做法是把签名矩阵分成若干个条状块,每一块有
行。对每一块都配一hash函数,按照用户的这
行数值进行hash,从而分桶。可以让每一快使用同一个hash函数,但它们使用的桶是不一样的,所以不用担心不同的块里面因为
行数值相同而被分到同一个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相应的哈希函数将该用户分到
个不同的群体(bucket)之中,从而达到聚类的效果。值得注意的是,这种聚类是有Overlap的,也就是说同一个用户,可能会被同时分到多个桶之中。
概率分析
现在我们来分析一下LSH的一些概率,从而进一步探究LSH的意义。假设上面的 行签名矩阵被分成了 个band,每个band有 行(即满足 ),现在考虑一个candidate pair(比如假设 和 是一对),已知它们的Jaccard相似度为 。
- 某一个band中, 和 的签名完全一样的概率:
- 某一个band中, 和 的签名至少有一行不一样的概率:
- 在所有band中,都有 和 的签名至少有一行不一样的概率:
- 至少有一个band, 和 的签名完全一样的概率:
我们主要关注最后两个概率,第三个概率描述了一种特殊情况,
和
不论在哪个band里,都没有完全一样,即使他们的Jaccard相似度很高。最后一个概率曲线如下图所示,横轴是
,纵轴是至少有一个band,
和
的签名完全一样的概率。举个例子,
和
的Jaccard相似度是0.8,
=5,
=20, 则
,
。也就是说如果两个用户有80%的相似性,虽然他们只有33%的可能性哈希签名完全一样,但签名矩阵被分成了20块,它们还是有20次机会成为一组candidate pair,只有
的概率无法成为一组candidate pair(这种情况被称为“false negative”),因此必须指出的是,该算法有一定概率产生假反例,即两个相似用户被判定为非相似。
这堆概率说明了什么呢?如果两个用户的Jaccard相似度很高,那么LSH将他们分到同一个bucket的概率就会很高,反过来说,如果两个用户被LSH分到了同一个bucket中,那么他们的真实购买历史也有很高的概率非常相似。这就是为什么LSH具有聚类的功能,它实现了聚类的核心:寻找相似的用户并将他们放在一起。然而,必须指出,LSH所聚出的类别,只是“比较高概率”是相似的用户群,我们最终还是要老老实实地用购买列表来计算相似度,只不过,从原来的“对所有用户pair都计算相似度”变成了“仅在较可能相似用户群体中计算相似度”,大大地减少了时间复杂度。
LSH算法步骤
总结一下我们在做的事情:寻找candidate pair作为候选相似用户,从而计算它们之间的相似性,进而判断出真正的相似用户。
- 对用户属性列表做分词或k-shingles,构造原矩阵(行是词,列是用户)
- 选择一个维度 (哈希函数个数),对原矩阵计算 行MinHash,构造MinHash签名矩阵
- 选择分块数 ,块行数 ( ),设定一个阈值
- 对签名矩阵使用LSH,将用户分桶,构造出candidate pair
- 检查每一个candidate pair是不是false negative/postive
- 对于签名相似的candidate pair,检查用户购买列表是否真的相似
LSH时间复杂度分析
MinHash首先要对所有商品行(
个商品)分别做
次行号哈希变换(
个哈希函数),因此复杂度是
,接着需要遍历商品行并用哈希过的行号进行替换,复杂度是
,因此整个签名矩阵的构建复杂度是
。接着就是LSH分桶,对于每个band(
个)都遍历一次用户列(有
个用户),将用户哈希到对应的桶,因此复杂度是
。因此整个LSH的复杂度应该是
。
如果不使用LSH,对所有用户进行聚类,并用用户在商品张成的基的投影计算余弦相似度,那么时间复杂度是
,可见LSH不仅降低了空间复杂度,还降低了时间复杂度。
LSH总结
- LSH的思想是,我们并不需要对所有的用户pair计算相似度。
- LSH算法被用于相似性检测,它对用户-商品矩阵计算MinHash签名,然后分band进行分桶,从而达到聚类的效果,在空间复杂度和时间复杂度上都要优于暴力聚类。
- LSH的聚类结果是有Overlap的。
- LSH的聚类是“比较有可能相似的物品集合”,最好用于初步筛选,还需进一步验证相似性
- LSH的聚类结果有一定概率有误:相似物品没有被聚在一起,非相似物品被聚在一起。
SimHash
MinHash签名有一个问题,它无法处理带权数据。也就是说它平等地对待每一个物品(行),假如我们具备一定的先验知识,得到了每个物品的重要性,或者某个权重,MinHash就无法处理了。这里介绍一种能处理带权物品的hash,SimHash。它的提出是基于普通hash函数的缺陷:即使两个字符串之间只有一点不同,hash过后的串也大不相同。换句话说,无法将相似性度量扩散到hash之后的串。SimHash的提出正是为了能够在hash过后的串上衡量两个原串的相似性,它能使得两个比较相似的串在hash过后的串也能具有较强的相似性(汉明距离),甚至完全一样。
- 将文章分词
- 计算每个词的权重(例如TF-IDF)和普通hash串
- 根据hash串的取值将词Embedding为正负权值交替的向量
- 将向量求和,并按照向量的正负得到最终的SimHash
下面举个例子。我们先将文章分词,将它当成一个Bag-of-Words模型,然后为每个词计算一个重要性,比如计算TF-IDF值,作为它的权值 ,同时把词本身进行hash,取后6位。接着,按照hash传embedding,串取1的位置赋 ,串取0的位置赋 ,比如串“001011”,则第1、2、4位取 ,第3、5、6位取 ,得到该单词的Embedding 。所有单词都计算完Embedding之后,做向量加法,然后根据正负,再二值化成01串。比如 [102,57,-40,-32,66,-7] 的第1、2、5位是正数,则取1,第3、4、6为是负数,则取0,最终得到“110010”,即该文档的Simhash。
SimHash与MinHash一样,都是将文档从词向量空间进行降维,最终的效果都是得到“文章-哈希签名”这样一个对应关系。只是SimHash支持带权基,MinHash不支持。