├── README.md ├── textSimilarity.py └── 文本相似度算法的胡乱整理.txt /README.md: -------------------------------------------------------------------------------- 1 | # TextSimilarity 2 | 这是一个类,里面包含的有关文本相似度的常用的计算算法,例如,最长公共子序列,最短标记距离,TF-IDF等算法 3 | 例如简单简单简单的用法:创建类实例,参数是两个文件目录,之后会生成两个字符串a.str_a, a.str_b 4 | 5 | a = TextSimilarity('/home/a.txt','/home/b.txt') 6 | # In[89]: 7 | a.minimumEditDistance(a.str_a,a.str_b) 8 | Out[89]: 0.3273657289002558 9 | 10 | # In[90]: 11 | a.JaccardSim(a.str_a,a.str_b) 12 | Out[90]: 0.17937219730941703 13 | 14 | # In[91]: 15 | a.splitWordSimlaryty(a.str_a,a.str_b,sim=a.pers_sim) 16 | Out[91]: 0.54331148827606712 17 | -------------------------------------------------------------------------------- /textSimilarity.py: -------------------------------------------------------------------------------- 1 | 2 | # coding: utf-8 3 | 4 | # In[87]: 5 | 6 | #基于分词的文本相似度的计算, 7 | #利用jieba分词进行中文分析 8 | import jieba 9 | import jieba.posseg as pseg 10 | from jieba import analyse 11 | import numpy as np 12 | import os 13 | 14 | 15 | ''' 16 | 文本相似度的计算,基于几种常见的算法的实现 17 | ''' 18 | class TextSimilarity(object): 19 | 20 | def __init__(self,file_a,file_b): 21 | ''' 22 | 初始化类行 23 | ''' 24 | str_a = '' 25 | str_b = '' 26 | if not os.path.isfile(file_a): 27 | print(file_a,"is not file") 28 | return 29 | elif not os.path.isfile(file_b): 30 | print(file_b,"is not file") 31 | return 32 | else: 33 | with open(file_a,'r') as f: 34 | for line in f.readlines(): 35 | str_a += line.strip() 36 | 37 | f.close() 38 | with open(file_b,'r') as f: 39 | for line in f.readlines(): 40 | str_b += line.strip() 41 | 42 | f.close() 43 | 44 | self.str_a = str_a 45 | self.str_b = str_b 46 | 47 | #get LCS(longest common subsquence),DP 48 | def lcs(self,str_a, str_b): 49 | lensum = float(len(str_a) + len(str_b)) 50 | #得到一个二维的数组,类似用dp[lena+1][lenb+1],并且初始化为0 51 | lengths = [[0 for j in range(len(str_b)+1)] for i in range(len(str_a)+1)] 52 | 53 | #enumerate(a)函数: 得到下标i和a[i] 54 | for i, x in enumerate(str_a): 55 | for j, y in enumerate(str_b): 56 | if x == y: 57 | lengths[i+1][j+1] = lengths[i][j] + 1 58 | else: 59 | lengths[i+1][j+1] = max(lengths[i+1][j], lengths[i][j+1]) 60 | 61 | #到这里已经得到最长的子序列的长度,下面从这个矩阵中就是得到最长子序列 62 | result = "" 63 | x, y = len(str_a), len(str_b) 64 | while x != 0 and y != 0: 65 | #证明最后一个字符肯定没有用到 66 | if lengths[x][y] == lengths[x-1][y]: 67 | x -= 1 68 | elif lengths[x][y] == lengths[x][y-1]: 69 | y -= 1 70 | else: #用到的从后向前的当前一个字符 71 | assert str_a[x-1] == str_b[y-1] #后面语句为真,类似于if(a[x-1]==b[y-1]),执行后条件下的语句 72 | result = str_a[x-1] + result #注意这一句,这是一个从后向前的过程 73 | x -= 1 74 | y -= 1 75 | 76 | #和上面的代码类似 77 | #if str_a[x-1] == str_b[y-1]: 78 | # result = str_a[x-1] + result #注意这一句,这是一个从后向前的过程 79 | # x -= 1 80 | # y -= 1 81 | longestdist = lengths[len(str_a)][len(str_b)] 82 | ratio = longestdist/min(len(str_a),len(str_b)) 83 | #return {'longestdistance':longestdist, 'ratio':ratio, 'result':result} 84 | return ratio 85 | 86 | 87 | def minimumEditDistance(self,str_a,str_b): 88 | ''' 89 | 最小编辑距离,只有三种操作方式 替换、插入、删除 90 | ''' 91 | lensum = float(len(str_a) + len(str_b)) 92 | if len(str_a) > len(str_b): #得到最短长度的字符串 93 | str_a,str_b = str_b,str_a 94 | distances = range(len(str_a) + 1) #设置默认值 95 | for index2,char2 in enumerate(str_b): #str_b > str_a 96 | newDistances = [index2+1] #设置新的距离,用来标记 97 | for index1,char1 in enumerate(str_a): 98 | if char1 == char2: #如果相等,证明在下标index1出不用进行操作变换,最小距离跟前一个保持不变, 99 | newDistances.append(distances[index1]) 100 | else: #得到最小的变化数, 101 | newDistances.append(1 + min((distances[index1], #删除 102 | distances[index1+1], #插入 103 | newDistances[-1]))) #变换 104 | distances = newDistances #更新最小编辑距离 105 | 106 | mindist = distances[-1] 107 | ratio = (lensum - mindist)/lensum 108 | #return {'distance':mindist, 'ratio':ratio} 109 | return ratio 110 | 111 | def levenshteinDistance(self,str1, str2): 112 | ''' 113 | 编辑距离——莱文斯坦距离,计算文本的相似度 114 | ''' 115 | m = len(str1) 116 | n = len(str2) 117 | lensum = float(m + n) 118 | d = [] 119 | for i in range(m+1): 120 | d.append([i]) 121 | del d[0][0] 122 | for j in range(n+1): 123 | d[0].append(j) 124 | for j in range(1,n+1): 125 | for i in range(1,m+1): 126 | if str1[i-1] == str2[j-1]: 127 | d[i].insert(j,d[i-1][j-1]) 128 | else: 129 | minimum = min(d[i-1][j]+1, d[i][j-1]+1, d[i-1][j-1]+2) 130 | d[i].insert(j, minimum) 131 | ldist = d[-1][-1] 132 | ratio = (lensum - ldist)/lensum 133 | #return {'distance':ldist, 'ratio':ratio} 134 | return ratio 135 | 136 | @classmethod 137 | def splitWords(self,str_a): 138 | ''' 139 | 接受一个字符串作为参数,返回分词后的结果字符串(空格隔开)和集合类型 140 | ''' 141 | wordsa=pseg.cut(str_a) 142 | cuta = "" 143 | seta = set() 144 | for key in wordsa: 145 | #print(key.word,key.flag) 146 | cuta += key.word + " " 147 | seta.add(key.word) 148 | 149 | return [cuta, seta] 150 | 151 | def JaccardSim(self,str_a,str_b): 152 | ''' 153 | Jaccard相似性系数 154 | 计算sa和sb的相似度 len(sa & sb)/ len(sa | sb) 155 | ''' 156 | seta = self.splitWords(str_a)[1] 157 | setb = self.splitWords(str_b)[1] 158 | 159 | sa_sb = 1.0 * len(seta & setb) / len(seta | setb) 160 | 161 | return sa_sb 162 | 163 | 164 | def countIDF(self,text,topK): 165 | ''' 166 | text:字符串,topK根据TF-IDF得到前topk个关键词的词频,用于计算相似度 167 | return 词频vector 168 | ''' 169 | tfidf = analyse.extract_tags 170 | 171 | cipin = {} #统计分词后的词频 172 | 173 | fenci = jieba.cut(text) 174 | 175 | #记录每个词频的频率 176 | for word in fenci: 177 | if word not in cipin.keys(): 178 | cipin[word] = 0 179 | cipin[word] += 1 180 | 181 | # 基于tfidf算法抽取前10个关键词,包含每个词项的权重 182 | keywords = tfidf(text,topK,withWeight=True) 183 | 184 | ans = [] 185 | # keywords.count(keyword)得到keyword的词频 186 | # help(tfidf) 187 | # 输出抽取出的关键词 188 | for keyword in keywords: 189 | #print(keyword ," ",cipin[keyword[0]]) 190 | ans.append(cipin[keyword[0]]) #得到前topk频繁词项的词频 191 | 192 | return ans 193 | @staticmethod 194 | def cos_sim(a,b): 195 | a = np.array(a) 196 | b = np.array(b) 197 | 198 | #return {"文本的余弦相似度:":np.sum(a*b) / (np.sqrt(np.sum(a ** 2)) * np.sqrt(np.sum(b ** 2)))} 199 | return np.sum(a*b) / (np.sqrt(np.sum(a ** 2)) * np.sqrt(np.sum(b ** 2))) 200 | @staticmethod 201 | def eucl_sim(a,b): 202 | a = np.array(a) 203 | b = np.array(b) 204 | #print(a,b) 205 | #print(np.sqrt((np.sum(a-b)**2))) 206 | #return {"文本的欧几里德相似度:":1/(1+np.sqrt((np.sum(a-b)**2)))} 207 | return 1/(1+np.sqrt((np.sum(a-b)**2))) 208 | @staticmethod 209 | def pers_sim(a,b): 210 | a = np.array(a) 211 | b = np.array(b) 212 | 213 | a = a - np.average(a) 214 | b = b - np.average(b) 215 | 216 | #print(a,b) 217 | #return {"文本的皮尔森相似度:":np.sum(a*b) / (np.sqrt(np.sum(a ** 2)) * np.sqrt(np.sum(b ** 2)))} 218 | return np.sum(a*b) / (np.sqrt(np.sum(a ** 2)) * np.sqrt(np.sum(b ** 2))) 219 | 220 | def splitWordSimlaryty(self,str_a,str_b,topK = 20,sim =cos_sim): 221 | ''' 222 | 基于分词求相似度,默认使用cos_sim 余弦相似度,默认使用前20个最频繁词项进行计算 223 | ''' 224 | #得到前topK个最频繁词项的字频向量 225 | vec_a = self.countIDF(str_a,topK) 226 | vec_b = self.countIDF(str_b,topK) 227 | 228 | return sim(vec_a,vec_b) 229 | 230 | @staticmethod 231 | def string_hash(self,source): #局部哈希算法的实现 232 | if source == "": 233 | return 0 234 | else: 235 | #ord()函数 return 字符的Unicode数值 236 | x = ord(source[0]) << 7 237 | m = 1000003 #设置一个大的素数 238 | mask = 2 ** 128 - 1 #key值 239 | for c in source: #对每一个字符基于前面计算hash 240 | x = ((x * m) ^ ord(c)) & mask 241 | 242 | x ^= len(source) # 243 | if x == -1: #证明超过精度 244 | x = -2 245 | x = bin(x).replace('0b', '').zfill(64)[-64:] 246 | #print(source,x) 247 | 248 | return str(x) 249 | 250 | 251 | def simhash(self,str_a,str_b): 252 | ''' 253 | 使用simhash计算相似度 254 | ''' 255 | pass 256 | 257 | -------------------------------------------------------------------------------- /文本相似度算法的胡乱整理.txt: -------------------------------------------------------------------------------- 1 | 中文文本相似度计算的算法: 2 | 3 | longest common subsequence 4 | https://rosettacode.org/wiki/Longest_common_subsequence#Python 5 | 6 | 7 | 1、最长公共子串、编辑距离(基于原文本进行查找测试,) 8 | 可以进行改进 9 | 10 | 11 | 2、分词后进行集合操作。 12 | Jaccard相似度、 13 | 14 | 3、是在分词后,得到词项的权重进行计算 15 | 结巴分词5--关键词抽取 http://www.cnblogs.com/zhbzz2007/p/6177832.html 16 | 余弦夹角算法、欧式距离、 17 | 18 | 19 | simhash 20 | 一个python的包接口 http://leons.im/posts/a-python-implementation-of-simhash-algorithm/ 21 | 22 | 23 | 24 | 1、分词,把需要判断文本分词形成这个文章的特征单词。最后形成去掉噪音词的单词序列并为每个词加上权重,我们假设权重分为5个级别(1~5)。比如:“ 美国“51区”雇员称内部有9架飞碟,曾看见灰色外星人 ” ==> 分词后为 “ 美国(4) 51区(5) 雇员(3) 称(1) 内部(2) 有(1) 9架(3) 飞碟(5) 曾(1) 看见(3) 灰色(4) 外星人(5)”,括号里是代表单词在整个句子里重要程度,数字越大越重要。 25 | 26 | 2、hash,通过hash算法把每个词变成hash值,比如“美国”通过hash算法计算为 100101,“51区”通过hash算法计算为 101011。这样我们的字符串就变成了一串串数字,还记得文章开头说过的吗,要把文章变为数字计算才能提高相似度计算性能,现在是降维过程进行时。hash算法的设置,就是python里面的字典。 27 | 28 | 29 | 30 | 3、加权,通过 2步骤的hash生成结果,需要按照单词的权重形成加权数字串,比如“美国”的hash值为“100101”,通过加权计算为“4 -4 -4 4 -4 4”;“51区”的hash值为“101011”,通过加权计算为 “ 5 -5 5 -5 5 5”。 31 | 32 | 4、合并,把上面各个单词算出来的序列值累加,变成只有一个序列串。比如 “美国”的 “4 -4 -4 4 -4 4”,“51区”的 “ 5 -5 5 -5 5 5”, 把每一位进行累加, “4+5 -4+-5 -4+5 4+-5 -4+5 4+5” ==》 “9 -9 1 -1 1 9”。这里作为示例只算了两个单词的,真实计算需要把所有单词的序列串累加。 33 | 34 | 5、降维,把4步算出来的 “9 -9 1 -1 1 9” 变成 0 1 串,形成我们最终的simhash签名。 如果每一位大于0 记为 1,小于0 记为 0。最后算出结果为:“1 0 1 0 1 1”。 35 | 36 | 37 | 总结 simhash用于比较大文本,比如500字以上效果都还蛮好,距离小于3的基本都是相似,误判率也比较低 38 | 每篇文档得到SimHash签名值后,接着计算两个签名的海明距离即可。根据经验值,对64位的 SimHash值,海明距离在3以内的可认为相似度比较高。 39 | 海明距离的求法:异或时,只有在两个比较的位不同时其结果是1 ,否则结果为0,两个二进制“异或”后得到1的个数即为海明距离的大小。 40 | 41 | 42 | Jaccard相似性系数 43 | 引用资料:http://www.ruanyifeng.com/blog/2013/03/cosine_similarity.html 44 | (1)使用TF-IDF算法,找出两篇文章的关键词; 45 | (2)每篇文章各取出若干个关键词(比如20个),合并成一个集合,计算每篇文章对于这个集合中的词的词频(为了避免文章长度的差异,可以使用相对词频); 46 | (3)生成两篇文章各自的词频向量; 47 | (4)计算两个向量的余弦相似度,值越大就表示越相似。 48 | (5) 或者计算两个向量的欧几里德距离,值越小越相近 49 | 50 | 51 | 导入文档 52 | 1、分词(python jieba分词) 53 | 已安装、需要简单学习使用 54 | 55 | 56 | 2、用tf-idf计算词语权重 python scikit-learn计算tf-idf词语权重 57 | 简单理解tf-idf的原理,使用库函数 58 | 样例使用地址: http://www.tuicool.com/articles/U3uiiu 59 | 60 | 3、使用各个算法计算文档的相似度 61 | --------------------------------------------------------------------------------