SimHash
事实上,传统比较两个文本相似性的方法,大多是将文本分词之后,转化为特征向量距离的度量,比如常见的欧氏距离、海明距离或者余弦角度等等。两两比较固然能很好地适应,但这种方法的一个最大的缺点就是,无法将其扩展到海量数据。例如,试想像Google那种收录了数以几十亿互联网信息的大型搜索引擎,每天都会通过爬虫的方式为自己的索引库新增的数百万网页,如果待收录每一条数据都去和网页库里面的每条记录算一下余弦角度,其计算量是相当恐怖的。
我们考虑采用为每一个web文档通过hash的方式生成一个指纹(fingerprint)。传统的加密式hash,比如md5,其设计的目的是为了让整个分布尽可能地均匀,输入内容哪怕只有轻微变化,hash就会发生很大地变化。我们理想当中的哈希函数,需要对几乎相同的输入内容,产生相同或者相近的hashcode,换句话说,hashcode的相似程度要能直接反映输入内容的相似程度。很明显,前面所说的md5等传统hash无法满足我们的需求。
simhash是locality sensitive hash(局部敏感哈希)的一种,最早由Moses Charikar在《similarity estimation techniques from rounding algorithms》一文中提出。Google就是基于此算法实现网页文件查重的。
海明距离的定义,为两个二进制串中不同位的数量。上述三个文本的simhash结果,其两两之间的海明距离为(p1,p2)=4,(p1,p3)=16以及(p2,p3)=12。事实上,这正好符合文本之间的相似度,p1和p2间的相似度要远大于与p3的。
如何实现这种hash算法呢?以上述三个文本为例,整个过程可以分为以下六步:
1、选择simhash的位数,请综合考虑存储成本以及数据集的大小,比如说32位
2、将simhash的各位初始化为0
3、提取原始文本中的特征,一般采用各种分词的方式。比如对于"the cat sat on the mat",采用两两分词的方式得到如下结果:{"th", "he", "e ", " c", "ca", "at", "t ", " s", "sa", " o", "on", "n ", " t", " m", "ma"}
4、使用传统的32位hash函数计算各个word的hashcode,比如:"th".hash = -502157718
,"he".hash = -369049682,……
5、对各word的hashcode的每一位,如果该位为1,则simhash相应位的值加它的权重(通常是出现的频率);否则减它的权重
6、对最后得到的32位的simhash,如果该位大于1,则设为1;否则设为0
整个过程可以描述为:
按照Charikar在论文中阐述的,64位simhash,海明距离在3以内的文本都可以认为是近重复文本。当然,具体数值需要结合具体业务以及经验值来确定。
利用鸽舍原理在快速查找出位数不同的数目小于等于3的算法的描述如下:
1)先复制原表T为Tt份:T1,T2,….Tt
2)每个Ti都关联一个pi和一个πi,其中pi是一个整数,πi是一个置换函数,负责把pi个bit位换到高位上。
3)应用置换函数πi到相应的Ti表上,然后对Ti进行排序
4)然后对每一个Ti和要匹配的指纹F、海明距离k做如下运算:
a) 然后使用F’的高pi位检索,找出Ti中高pi位相同的集合
b) 在检索出的集合中比较f-pi位,找出海明距离小于等于k的指纹
5)最后合并所有Ti中检索出的结果
代码:
#!/usr/bin/python #-*- coding:utf-8 -*- from __future__ import division,unicode_literals import sys import re import hashlib import collections import datetime reload(sys) sys.setdefaultencoding('utf-8') import codecs import itertools lib_newsfp_file = sys.argv[1] #读入库中存储的所有新闻 result_file = sys.argv[2] test_news_fp = {} lib_news_fp = {} bucket = collections.defaultdict(set) offsets = [] def cacu_frequent(list1): frequent = {} for i in list1: if i not in frequent: frequent[i] = 0 frequent[i] += 1 return frequent def load_lib_newsfp_file(): global lib_news_fp fin = codecs.open(lib_newsfp_file,'r','utf-8') for line in fin: lines = line.strip() if len(lines) == 0: continue Arr = lines.split(' ') if len(Arr) < 3: continue lib_news_fp[Arr[0]] = Arr[3] def get_near_dups(check_value): ans = set() for key in get_keys(int(check_value)): dups = bucket[key] for dup in dups: total_value,url = dup.split(',',1) if isSimilar(int(check_value),int(total_value)) == True: ans.add(url) break #与一条重复 退出查找 if ans: break return list(ans) def ini_Index(): global bucket getoffsets() print offsets objs = [(str(url),str(values)) for url,values in lib_news_fp.items()] for i,q in enumerate(objs): addindex(*q) def addindex(url,value): global bucket for key in get_keys(int(value)): v = '%d,%s' % (int(value),url) bucket[key].add(v) def deleteindex(url,value): global bucket for key in get_keys(int(value)): v = '%d,%s' %(int(value),url) if v in bucket[key]: bucket[key].remove(v) def getoffsets(f = 64 , k = 4): global offsets offsets = [f // (k + 1) * i for i in range(k + 1)] def get_keys(value, f = 64): for i, offset in enumerate(offsets): if i == (len(offsets) - 1): m = 2 ** (f - offset) - 1 else: m = 2 ** (offsets[i + 1] - offset) - 1 c = value >> offset & m yield '%x:%x' % (c , i) def bucket_size(): return len(bucket) def isSimilar(value1,value2,n = 4,f = 64): ans = 0 x = (value1 ^ value2) &((1 << f) - 1) while x and (ans <= n): ans += 1 x &= x - 1 if ans <= n: return True return False def load_test_file(): global test_news_fp for line in sys.stdin: features = [] result = line.strip().split(' ') url = result[0] content = result[2].split() title = result[1].split() features.extend(content) features.extend(title) total_features = cacu_frequent(features) test_news_fp[url] = build_by_features(total_features) def load_test_newsfp_file(): global test_news_fp for line in sys.stdin: lines = line.strip() if len(lines) == 0: continue Arr = lines.split(' ') if len(Arr) < 3: continue test_news_fp[Arr[0]] = Arr[3] def build_by_features(features,f=64,hashfunc=None): v = [0]*f masks = [1 << i for i in range(f+f)] if hashfunc is None: def _hashfunc(x): return int(hashlib.md5(x).hexdigest(),16) hashfunc = _hashfunc if isinstance(features,dict): total_features = features.items() else: total_features = features for fea in total_features: if isinstance(fea,basestring): h = hashfunc(fea.encode('utf-8')) w = 1 else: h = hashfunc(fea[0].encode('utf-8')) w = fea[1] for i in range(f): v[i] += w if h & masks[i+32] else -w ans = 0 for i in range(f): if v[i] >= 0: ans |= masks[i] return ans sum = 0 def process(): global test_news_fp global sum fout = codecs.open(result_file,'w','utf-8') load_lib_newsfp_file() # load_test_file() ini_Index() check_features = test_news_fp.items() lib_features = lib_news_fp.items() i = 0 for check_fp in check_features: # print i ans = [] ans = get_near_dups(check_fp[1]) if ans: for url in ans: output_str = str(check_fp[0])+' '+str(url) fout.write(output_str+' ') #break #print check_fp[0],'is duplicate' sum = sum + 1 #del test_news_fp[check_fp[0]] print i i += 1 fout.close() if __name__ == '__main__': # process() begin = datetime.datetime.now() load_test_newsfp_file() # load_test_file() # getoffsets() # print offsets # load_lib_newsfp_file() process() end = datetime.datetime.now() print '耗时:',end - begin,' 重复新闻数:',sum,' 准确率: ', sum/2589
MinHash
1.概述
Jaccard index是用来计算相似性,也就是距离的一种度量标准。假如有集合A、B,那么, 也就是说,集合A,B的Jaccard系数等于A,B中共同拥有的元素数与A,B总共拥有的元素数的比例。很显然,Jaccard系数值区间为[0,1]。
那么对集合A、B,hmin(A) = hmin(B)成立的条件是A ∪ B 中具有最小哈希值的元素也在 ∩ B中。这里
有一个假设,h(x)是一个良好的哈希函数,它具有很好的均匀性,能够把不同元素映射成不同的整数。
所以有,Pr[hmin(A) = hmin(B)] = J(A,B),即集合A和B的相似度为集合A、B经过hash后最小哈希值相
等的概率。
简单粗暴的方法
统计分词后的文本中出现的各个词,直接计算两个文本的jaccard距离。(相同的词出现的越多,文本重复的概率越大。)
1 #!/usr/bin/python 2 #-* coding:utf-8 -*- 3 4 4 import sys 5 5 import re 6 6 import hashlib 7 7 import collections 8 8 import datetime 9 9 import codecs 10 10 11 11 reload(sys) 12 12 sys.setdefaultencoding('utf-8') 13 13 14 14 import threading 15 15 from Queue import Queue 16 16 queue = Queue() 17 17 thread_flag_list = [0,0,0,0,0] 18 18 19 19 res_file = sys.argv[1] 20 20 21 21 news_list = [] 22 22 def load(): 23 23 global news_list 24 24 for line in sys.stdin: 25 25 line = line.strip() 26 26 if len(line) == 0: 27 27 continue 28 28 Arr = line.split(' ') 29 29 30 30 if len(Arr) < 3: 31 31 continue 32 32 33 33 url = Arr[0] 34 34 title = Arr[1] 35 35 content = Arr[2] 36 36 37 37 term_list = content.split(' ') 38 38 term_set = set(term_list) 39 39 news_list.append([url,term_set]) 40 40 41 41 42 42 def calculate(news_f,news_s): 43 43 set1 = news_f[1] 44 44 set2 = news_s[1] 45 45 46 46 set_join = set1 & set2 47 47 set_union = set1 | set2 48 48 49 49 simi_value = float(len(set_join))/float(len(set_union)) 50 50 return simi_value 51 51 52 52 def run_thread(start_id,thread_id): 53 53 global queue 54 54 global thread_flag_list 55 55 news_first = news_list[start_id] 56 56 for i in range(start_id+1,len(news_list)): 57 57 news_second = news_list[i] 58 58 simi_value = calculate(news_first,news_second) 59 59 if simi_value > 0.8: 60 60 url1 = news_first[0] 61 61 url2 = news_second[0] 62 62 output_str = url1+' '+url2+' '+str(simi_value) 63 63 queue.put(output_str) 64 64 thread_flag_list[thread_id] = 0#标记线程结束 65 65 66 66 def process(): 67 67 global queue 68 68 global thread_flag_list 69 69 fout = codecs.open(res_file,'w','utf-8') 70 70 id_max = len(news_list) 71 71 id_now = 0 72 72 while True: 73 73 run_flag = False 74 74 thread_list = [] 75 75 for i in range(0,len(thread_flag_list)): 76 76 if thread_flag_list[i] == 0: 77 77 if id_now == id_max: 78 continue 79 79 thread_flag_list[i] = 1 80 80 print 'now run is:',id_now 81 81 82 82 thread = threading.Thread(target=run_thread,args=(id_now,i)) 83 83 thread_list.append(thread) 84 84 85 85 id_now = id_now + 1 86 86 else: 87 87 run_flag = True 88 88 89 89 for thread in thread_list: 90 90 thread.setDaemon(True) 91 91 thread.start() 92 92 93 93 while not queue.empty(): 94 94 elem = queue.get() 95 95 print elem 96 96 fout.write(elem+' ') 97 97 98 98 if run_flag != True and id_now == id_max: 99 99 break 100 100 101 101 fout.close() 102 102 103 103 if __name__ == '__main__': 104 104 load() 105 105 print 'load done' 106 106 process() 107 107