作者: @凌漪_ @板烧鱼仔 @Yuxn.
先介绍它的工作方式,可能能帮助你更好理解这个算法。
前提假设:如果两个集合非常相似 → 那么对这两个集合应用同一种变化,得到的变化结果也很相似 → 对这两个变化结果选取某种特征(比如选最小值),它们有很高的概率是相等的。
# 设置2个集合A和B
A = {1, 2, 3, 4, 5}
B = {3, 4, 5, 6, 7}
print('Jaccard相似度:', len(A & B) / len(A | B))
Jaccard相似度: 0.4285
我们拥有哈希函数(有点像加密算法),它能将整数x映射到一个不同的整数,并且不会产生冲突(c 是大素数)。不同的哈希函数相当于不同的投影规则
h a s h ( x ) = ( a x + b ) % c hash(x) = (ax + b )\% c hash(x)=(ax+b)%c
#设置某个哈希函数
def hash(x, a=1, b=1, p=11):
return (a * x + b) % p
#对集合里的元素进行哈希,并计算最小哈希值
def minhash(s, hash):
return min(hash(x) for x in s)
我们将某个文档A经过hash函数之后(Hash(A)),记录所得到的最小哈希值(minHash(A))。minHash(A)相当于此集合在某种观测视角下得到的结果。我们对文档B也进行计算,得到
Hash(A) = [5, 8, 0, 3, 6] minHash(A) = 0
Hash(B) = [0, 3, 6, 9, 1] minHash(B) = 0
最后,minHash(A) = minHash(B)的概率就是Jaccard相似度。这一步我们在之后会进行解释。
实际上,单次比较 minHash(A) 和 minHash(B),查看取值是否相等,这个事件的取值只有True or False。(就和抛硬币一样)只有我们经过多次hash函数操作(相当于经过了多次抛硬币),该事件发生的概率值理论上趋近于(A ∩ B)/ ( A ∪ B)。
因此,使用minhash方法对Jaccard的低维估计计算方式就是:
m i n H a s h ( A ) = m i n H a s h ( B )的次数 h a s h 函数的个数 \frac{minHash(A) = minHash(B)的次数}{hash函数的个数} hash函数的个数minHash(A)=minHash(B)的次数
import random
def pickRandomCoeffs(k):
randList = []
while k > 0:
randIndex = random.randint(0, 99999)
while randIndex in randList:
randIndex = random.randint(0, 99999)
randList.append(randIndex)
k = k - 1
return randList
基于不相同的随机数,作为a和b,生成不同hash函数
#生成n个不同的hash函数,确保每个hash函数的a系数和b系数不同:
num_hash_funcs = 10000
hash_funcs = []
a = pickRandomCoeffs(num_hash_funcs)
b = pickRandomCoeffs(num_hash_funcs)
for i in range(num_hash_funcs):
#增加不同的hash函数,确保每次生成的hash函数不同
hash_funcs.append(lambda x, a=a[i], b=b[i]: hash(x, a, b))
count = 0
for a, b in zip(minhash_values_A, minhash_values_B):
#如果hash函数得到的minhash相同
if a == b:
count += 1
print('进行比较的哈希函数个数:', len(hash_funcs))
print('Minhash相似度:', count/ len(hash_funcs))
进行比较的哈希函数个数: 5 Minhash相似度: 0.6
进行比较的哈希函数个数: 100 Minhash相似度: 0.38
进行比较的哈希函数个数: 1000 Minhash相似度: 0.411
进行比较的哈希函数个数: 5000 Minhash相似度: 0.4242
Jaccard相似度的理论值为: 0.4285。随着hash函数的个数增加,实验值逐渐逼近 0.4285
hash函数相当于一种对集合的变换方式,但实际应用时,如果我们有多个hash函数,需要对每个集合计算minhash,而哈希操作的速度也比较慢。
既然只是找到一种对集合的变换方式,再用另一种视角来观察这个集合的特征值,有没有另一种更方便的变换方法呢?
我们可以使用基于打乱排序的算法。
假设两个文档的内容如下:
文档A:{上山打老虎}
文档B:{老虎不在家}
将其转为one-hot编码如下:
原始下标 | 词 | 文档A | 文档B |
---|---|---|---|
0 | 上 | 1 | 0 |
1 | 山 | 1 | 0 |
2 | 打 | 1 | 0 |
3 | 老 | 1 | 1 |
4 | 虎 | 1 | 1 |
5 | 不 | 0 | 1 |
6 | 在 | 0 | 1 |
7 | 家 | 0 | 1 |
观测视角是:进行行变换,打乱排序,查找两个文档第一个为1的下标,判断它们是否相等
打乱行的排序:
原始下标 | idx | 词 | 文档A | 文档B |
---|---|---|---|---|
1 | 0 | 山 | 1 | 0 |
7 | 1 | 家 | 0 | 1 |
2 | 2 | 打 | 1 | 0 |
3 | 3 | 老 | 1 | 1 |
5 | 4 | 不 | 0 | 1 |
6 | 5 | 在 | 0 | 1 |
4 | 6 | 虎 | 1 | 1 |
0 | 7 | 上 | 1 | 0 |
文档A变成{1,0,1,1,0,0,1,1},第一个为1的jdx为0,(对应’山‘)
文档B变成{0,1,0,1,1,1,1,0},第一个为1的idx为1,(对应’家’)
0 != 1,表示在这次打乱中,两个文档第一个为1的下标不相等。
我们进行多次这样的打乱排序,并统计他们相等的次数,除以总次数,得到的就是minhash对jaccard相似度的估计。
具体代码如下:
import random
# 设置2个集合A和B
A = [0,0,1,1,1]
B = [1,0,1,1,0]
idx = [0,1,2,3,4]
cnt = 0
# 计算Jaccard相似度
print("Jaccard相似度理论值",sum([A[i] & B[i] for i in range(5)]) / sum([A[i] | B[i] for i in range(5)]))
for i in range(1000):
random.shuffle(idx)
newA = [A[i] for i in idx]
newB = [B[i] for i in idx]
# 第一个为1的位置
idxA = newA.index(1)
idxB = newB.index(1)
if idxA == idxB:
cnt += 1
print("Jaccard相似度实验值",cnt / 1000)
Jaccard相似度理论值 0.5
Jaccard相似度实验值 0.497
在使用的时候,也可以首先计算一次hash值,得到hash(X_i),然后对其进行线性变换,这样就省去了多次计算hash值的操作。
p e r ( x i ) = ( a ⋅ h a s h ( x i ) + b ) % c per(x_i) = (a\cdot{hash(x_i)} + b)\% c per(xi)=(a⋅hash(xi)+b)%c m i n h a s h ( x ) = m i n ( p e r ( x i ) ) minhash(x) = min(per(x_i)) minhash(x)=min(per(xi))
case1:
”横看成岭侧成峰,远近高低各不同“ ——苏轼《题西林壁》
minhash相当于在不同视角(投影角度)下观察两个集合的特征点是否相同,如果他们在多次观察下都具有相同的特征点,那么他们很有可能是分布相似的集合。
我们想象现在存在两座山,如果A山和B山从东西南北四个角度观察,他们的最高点高度相同,次高点、次次高点的高度都相同,那么这两座山很可能就是形状一样的山。
minhash在实际操作上很容易,但为什么minHash(A) = minHash(B)的概率就是Jaccard相似度呢?
case2:
P r ( m i n H a s h ( A ) = m i n H a s h ( B ) ) = A ∩ B A ∪ B = J a c c a r d ( A , B ) Pr(minHash(A) = minHash(B)) = \frac{A \cap B }{A \cup B } = Jaccard(A,B) Pr(minHash(A)=minHash(B))=A∪BA∩B=Jaccard(A,B)
为了方便理解,我们这里还是举一个直观的例子:
有一个班级,里面有30个小朋友,里面的小朋友不是会唱歌,就是会跳舞,还有的既会唱歌也会跳舞。
此时集合 A = { 会唱歌的小朋友 } A = \{会唱歌的小朋友\} A={会唱歌的小朋友}, B = { 会跳舞的小朋友 } B = \{会跳舞的小朋友\} B={会跳舞的小朋友}, A ∪ B A \cup B A∪B 构成了全集 D D D
我们寻找一种相同的投影维度(一种哈希函数)将小朋友们排序,并选出顺序第一的人。例如各自选出集合A和B中身高最高的小朋友,他们是同一个小朋友的概率,等价于在全班所有小朋友中都选出最高的小朋友,他既会唱歌也会跳舞的概率。
在这个例子中,身高就是一种将小朋友们向一个方向投影的哈希函数,同理还可以使用体重、力气、吃零食速度、谁哭得最响等等多个维度,最终我们多次投影得到计算的概率值就可以用来近似 A ∩ B A ∪ B \frac{A \cap B }{A \cup B } A∪BA∩B。
转化成数学公式,就是: P r ( 某一种投影 ( A ) = 某一种投影 ( B ) ) = A ∩ B A ∪ B Pr(某一种投影(A) = 某一种投影(B)) = \frac{A \cap B }{A \cup B } Pr(某一种投影(A)=某一种投影(B))=A∪BA∩B
当多次投影都能得到相同的值时,我们就可以认为这两个集合很相似啦!
使用MinHash算法计算两个集合的相似度 - unrealwalker - 博客园 (cnblogs.com)
MinHash 教程与 Python 代码 · 克里斯·麦科米克 — MinHash Tutorial with Python Code · Chris McCormick (mccormickml.com)
ansvver | 局部敏感哈希 - MinHash