基于hash的文档判重——simhash

时间:2022-01-08 09:50:50

本文环境:

  • python3.5

  • ubuntu 16.04

第三方库:

  • jieba

文件寄于github: https://github.com/w392807287/angelo_tools.git


simhash介绍

没多久就要写毕业论文了,据说需要查重,对文档重复判定还挺好奇的所以看了下相关的东西。发现simhash比较好用,实现简单。

顾名思义 simhash是一种hash算法,以前在我印象中hash算法是将一个对象映射成一个hash值,一般只要求当两个对象完全相同时才有相同的hash值,而两个相似的对象的hash值并不需要有任何关系。只相差一个字符hash出来的值也可能相差十万八千里。但是如果hash函数设计的足够巧妙,也可以让相似的对象拥有相同或者相似的hash值,使用hash来进行相似性搜索更方便快捷。
simhash就是这么一个神奇的算法。它满足:

  • 当两个对象的距离不大于d1时,它们的hash值相同的概率不小于p1,即如d(x, y) ≤ d1,则P(hash(x) = hash(y)) ≥ p1.
  • 当两个对象的距离不小于d2时,它们的hash值相同的概率不大于p2,即如d(x, y) ≥ d2,则P(hash(x) = hash(y)) ≥ p2.

simhash可以将文档hash到一个64位二进制数,使得相似的文档具有相似的二进制数。对于一个文档,我们可以把文中的每个词或者词组作为一个特征,统计各个特征出现的频率(当然也可以加入词性的权重,怎么去设置、统计特征可以视情况而定)。下面的例子中我们使用 jieba 做分词。

目标文档 “葫芦娃葫芦娃,一根藤上七朵花”,得到的特征与相应的频率:(葫芦娃,0.33),(一根,0.17,(藤上,0.17),(七朵,0.17),(花,0.17)。然后对特征值进行hash,方便演示这里映射到6位:

  • 葫芦娃:100100
  • 一根:010101
  • 藤上:101010
  • 七朵:111010
  • 花:001010
    然后根据二进制数的各个二进制位,我们队每个特征构造一个向量。如果一个特征映射到的二进制数的某一位是1,则其向量对应位置上的分量为该特征的频率,否则为频率的相反数。如:
    葫芦娃:(0.33,-0.33,-0.33,0.33,-0.33,-0.33)
    ……
    将向量相加,得到(0.33,-0.33,0,0,0,-0.66)
    对于每个分量,如果大于0就取1,否则取0,这样就能得到二进制数的simhash,即100000。

在文本中,出现频率高的特征,其对应的向量分量的绝对值更大,对最终向量相加的结果影响也更大。因此,如果两个文档相似,那么它们出现频率高的特征也应该比较接近,最终得到的hash值也就越接近。在google网页的检索中,64位hash中至多有3个二进制位不同可判定为相似文档。

算法实现

def simhash(cls, s, RE=None, cut_func=None):
if RE:
REX = RE
else:
REX = re.compile(u'[\u4e00-\u9fa5]+')
if not cut_func:
cut_func = cls.cut_func #jieba.cut
cut = [x for x in cut_func(s) if re.match(REX, x)]
ver = [[v * (int(x) if int(x) > 0 else -1) for x in k] for k, v in cls.hist(cut).items()]
ver = np.array(ver)
ver_sum = ver.sum(axis=0)
sim = ''.join(['1' if x > 0 else '0' for x in ver_sum])
return sim

首先我们用正则定义了感兴趣的区域,这里我们只取我们感兴趣的中文。然后我们定义了分词所用的函数,这里使用的是jieba分词。
然后我们得到分词的结果:
cut = [x for x in cut_func(s) if re.match(REX, x)]
得到向量矩阵:
ver = [[v * (int(x) if int(x) > 0 else -1) for x in k] for k, v in cls.hist(cut).items()]
为了方便计算我们引入numpy帮我们做矩阵计算:

ver = np.array(ver)
ver_sum = ver.sum(axis=0)

最后将计算结果转换为二级制hash。因为我们这里使用的32位md5给分词结果做的hash所以最后得到的hash值也是32位的:

11111101011001101110111100101101

其中我们用到了几个工具函数:

@classmethoddef
hist(cls, cut):
_cut = {x: 0 for x in set(cut)}
for i in cut:
_cut[i] += 1
return {cls.hash_bin(k): v/len(cut) for k, v in _cut.items()}

hist函数是将分词列表转换为特征频率向量的。

@classmethoddef
hash2bin(cls, hash):
d = ''
for i in hash:
try:
if int(i) > 7:
d = d + '1'
else:
d = d + '0'
except ValueError:
d = d + '1'
return d
@classmethoddef
hash_bin(cls, s):
h = hashlib.md5(s.encode()).hexdigest()
return cls.hash2bin(h)

其中hash_bin函数用来将字符Hash成二级制hash值,基础hash算法为32位md5。
hash2bin函数是将16进制hash值映射成二进制hash。
为了方便比较我们使用海明距离来判定两个hash值的相似度:

@staticmethoddef
haiming(s1, s2):
x = 0
for i in zip(s1, s2):
if i[0] != i[1]:
x += 1
return x

效果

1993年,南京大学有这样一个男生寝室,四个男生都没有女朋友,于是搞了个组合叫“名草无主四大天王”。这四大天王坚持每晚举行“卧谈会”,从各种学术上讨论如何摆脱光棍状态。这一年的11月,校园的梧桐树落叶凋零,令他们分外伤情。他们在11日这一天晚上卧谈时,符号学的灵感突然登门造访。11月11日,四个1字排开,不正是好像四根光秃秃的棍子吗?这四根光棍不正是在巧妙地诉说着“名草无名四大天王”的凄凉吗?

*

知乎上有个提问,小时候缺爱的女孩子,长大后该怎么办?或许在我这里,只是希望一直有人陪。喜宝说,我想要很多很多的爱,要不就是很多很多的钱,实在不行,有健康也是好的。我有个坏毛病,经常会半夜饿到不行,爬起来找吃的。是真的饿到胃疼,有时候直接饿醒了,每次看到电影里的台词,睡着了就不饿了,我是压根不相信。为什么会半夜饿?究其原因,是大学的时候没人陪我吃饭,每次都是一直等到有人陪我的时候,我才会去吃饭,最后把自己饿到胃疼,久而久之,就渐渐习惯了熬到很晚才吃饭。我不喜欢一个人吃饭,也不喜欢一个人逛街,更不喜欢一个人呆着,可是成长啊,往往是越不喜欢的便越要学会接受它。(二)讲讲上一段恋爱吧。我和他认识的时候,是因为贴吧聚餐,他主动找我要的微信,附带一个如沐春风般的笑容。我一直以为他是被我的美色打动,后来问他原因。他说,他第一次看见那么能吃的女孩子,他惊呆了,可是有觉得看我吃饭很意思,仿佛食物都有了灵魂,让人的心情莫名的好了起来。我们初相识,是因为他看见了我饿死鬼投胎的吃相。我们在一起,是因为他厨艺很好,好到什么程度呢?就是那种你吃过一顿,就能惦记一辈子的感觉。即便是现在回忆起他来,我的味蕾都会有反应。他总是给我做很多很多好吃的,午后阳光从窗子洒进来,窗帘是淡绿色的小碎花,空气里弥漫着饭香味,我们两个人坐在桌前,一边吃饭,一边聊天。我喜欢和他一起手挽着手去菜市场买菜,西红柿土豆黄瓜小白菜,手里拎着的这些果蔬食物,就好像我拥有的全世界。有一次,我们从菜市场回去的路上,明明是艳阳高照的天气,却突然间下起了冰雹,那是他第一次看见冰雹,被砸了一下之后,便立马丢了手里的菜,双手护住我,我傻了吧唧的去捡菜,被砸了一身。他立马臭骂了我一顿,说我是他见过,最好吃的女孩子了。

以上是简书一片文章中的节选。
两个的simhash是
11111101011001101110111100101101
00101101001010110001100000101110
海明距离为16。

知乎上有个提问,小时候缺爱的女孩子,长大后该怎么办?或许在我这里,只是希望一直有人陪。喜宝说,我想要很多很多的爱,要不就是很多很多的钱,实在不行,我有个坏毛病,经常会半夜饿到不行,爬起来找吃的。是真的饿到胃疼,有时候直接饿醒了,每次看到电影里的台词,睡着了就不饿了,我是压根不相信。究其原因,是大学的时候没人陪我吃饭,每次都是一直等到有人陪我的时候,我才会去吃饭,最后把自己饿到胃疼,久而久之我不喜欢一个人吃饭,也不喜欢一个人逛街,更不喜欢一个人呆着,可是成长啊,往往是越不喜欢的便越要学会接受它。我和他认识的时候,是因为贴吧聚餐,他主动找我要的微信,附带一个如沐春风般的笑容。我一直以为他是被我的美色打动,后来问他原因。他说,他第一次看见那么能吃的女孩子,他惊呆了,可是有觉得看我吃饭很意思,仿佛食物都有了灵魂,让人的心情莫名的好了起来。我们初相识,是因为他看见了我饿死鬼投胎的吃相。我们在一起,是因为他厨艺很好,好到什么程度呢?就是那种你吃过一顿,就能惦记一辈子的感觉。即便是现在回忆起他来,我的味蕾都会有反应。他总是给我做很多很多好吃的,午后阳光从窗子洒进来,窗帘是淡绿色的小碎花,空气里弥漫着饭香味,我们两个人坐在桌前,一边吃饭,一边聊天。我喜欢和他一起手挽着手去菜市场买菜,西红柿土豆黄瓜小白菜,手里拎着的这些果蔬食物,有一次,我们从菜市场回去的路上,明明是艳阳高照的天气,却突然间下起了冰雹,那是他第一次看见冰雹,被砸了一下之后,便立马丢了手里的菜,双手护住我,我傻了吧唧的去捡菜,被砸了一身。他立马臭骂了我一顿,说我是他见过,最好吃的女孩子了。

这段是第二段稍加修改,simhash为:
00100101001010110000100000101110

与第二段的海明距离为2
可以看出效果还是很明显的。


能序列化的东西都能hash,也就都能比较相似度。simhash属于局部敏感哈希(Local-Sensitive Hashing, LSH),下次讲讲如何比较图片的相似度,使用感知哈希(Perceptual Hashing)。