实战 _ 用 TF/IDF 算法对比相似度

原理

TF/IDF 方法于 1983 年题出,它先计算每个单词出现的频率,然后适当归一化。利用 TF-IDF 方法将任意长度的文档缩减为固定长度的数字列表,然后对比文本相似度,gensim 工具包提供该方法。

简单复习一下具体算法:

词频 TF

\[ tf_{i,j}=\frac{n_{i,j}}{\sum_kn_{k,j}} \]

其中 n 是句中词,i 是词的索引号,j 是文章索引号,k 是文章中所有词,上式计算的是词 i 在本篇出现的比率。请注意:在短文本的情况下,绝大多数词只出现一次,tf 就只和文章长短有关了。

逆向文档频率 IDF

\[ idf_{i}=log \frac{|D|}{|j:t_i\in d_j|} \]

其中分母是文章总数,分子是包含词 i 的文章数。

TF/IDF

\[ tfidf_{i,j}=tf_{i,j} \times idf_{i} \]

tfidf 值反映的是每个词在文档中的重要程度。请注意:这是一种基于计数的方法,不直接使用词义。

该算法的优点在于算法简单,计算量小;而缺点在于无法处理对同一概念的不同描述,另外,它是词袋类模型,不考虑词的先后顺序和关系。

详见 TF-IDF逆文本频率指数

流程

计算文本相似度,指的是从多个文档中找到与句子相似度最高的文档,常用于实现搜索,匹配,文本标准化等功能。具体流程如下:

  • 用待搜语料训练 TFIDF
  • 将待搜语料转成包含的关键字及关键字对应评分 M
  • 将搜索文本转换成关键字和评分 K
  • 逐条计算 M 中内容与 K 的相似度评分
  • 选最相近的前 N 条

代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
from jieba import lcut
from gensim.similarities import SparseMatrixSimilarity
from gensim.corpora import Dictionary
from gensim.models import TfidfModel

data = ['古典生物型霍乱',
'霍乱,由于O1群霍乱弧菌,埃尔托生物型所致',
'埃尔托生物型霍乱',
'霍乱暴发型',
'伤寒']

texts = [lcut(s) for s in data]
'''
此时 texts中每个元素是一个句子,句子又由词组成
[['古典', '生物', '型', '霍乱'],
['霍乱', ',', '由于', 'O1', '群', '霍乱弧菌', ',', '埃尔托', '生物', '型', '所致'],
['埃尔托', '生物', '型', '霍乱'],
['霍乱', '暴发型'],
['伤寒']]
'''

dictionary = Dictionary(texts)
'''
dictionary 结构类似字典,包含索引号和词的关系
{0: '古典', 1: '型', 2: '生物', 3: '霍乱', 4: 'O1', 5: '埃尔托', 6: '所致',
7: '由于', 8: '群', 9: '霍乱弧菌', 10: ',', 11: '暴发型', 12: '伤寒'}
'''

corpus = [dictionary.doc2bow(text) for text in texts]
'''
corpus 语料库,结构同texts,但每个子元素不是字,而是id及其在本句中出现的次数
[[(0, 1), (1, 1), (2, 1), (3, 1)],
[(1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 1), (8, 1), (9, 1), (10, 2)],
[(1, 1), (2, 1), (3, 1), (5, 1)],
[(3, 1), (11, 1)],
[(12, 1)]]
'''

tfidf = TfidfModel(corpus) # 用语料库训练模型
tf_texts = tfidf[corpus] # 将语料库作为被搜索数据
sparse_matrix = SparseMatrixSimilarity(tf_texts, len(dictionary)) # 构建工具类的实例
'''
sparse_matrix.index内容如下:
(0, 0) 0.9050976
(0, 1) 0.28727236
(0, 2) 0.28727236
(0, 3) 0.12548897
(1, 1) 0.10273404
(1, 2) 0.10273404
(1, 3) 0.044877227
...
此时sparse_matrix中存储的是各句中包含的关键字及其tfidf值
'''

keyword = '埃尔托生物霍乱'
kw_vector = dictionary.doc2bow(lcut(keyword))
'''
转换待搜索字符串,返回包含的关键字id及其出现次数
kw_vector: [(2, 1), (3, 1), (5, 1)]
'''
tf_kw = tfidf[kw_vector]
'''
获取关键词对应的的tfidf值
tf_kw: [(2, 0.476), (3, 0.208), (5, 0.854)]
'''

similarities = sparse_matrix.get_similarities(tf_kw)
for e, s in enumerate(similarities):
print('与 %d 相似度为:%.2f' % (e, s))
'''
计算搜索串与被搜索数据中每一项的相似度
与 0 相似度为:0.16
与 1 相似度为:0.22
与 2 相似度为:0.90
与 3 相似度为:0.03
与 4 相似度为:0.00
'''

完整代码

封装一下,以便拿来就用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
from jieba import lcut
from gensim.similarities import SparseMatrixSimilarity
from gensim.corpora import Dictionary
from gensim.models import TfidfModel
import numpy as np

class TFIDFSimilarity:
def __init__(self):
pass

def train(self, data):
'''
训练模型,需转入待匹配列表
'''
texts = [lcut(s) for s in data]
self.dictionary = Dictionary(texts)
corpus = [self.dictionary.doc2bow(text) for text in texts]
self.tfidf = TfidfModel(corpus)
tf_texts = self.tfidf[corpus]
num_features = len(self.dictionary.token2id)
self.sparse_matrix = SparseMatrixSimilarity(tf_texts, num_features)

def get_similarities(self, string, topN = 3):
'''
从模型中找最相近的 topN 个匹配项,返回其索引号和近似度
'''
text = lcut(string)
kw_vector = self.dictionary.doc2bow(text)
tf_kw = self.tfidf[kw_vector]
similarities = self.sparse_matrix.get_similarities(tf_kw)
index = np.argsort(similarities)[:-topN-1:-1]
return [(i,s) for i,s in zip(index,similarities[index])]

data = ['霍乱,由于O1群霍乱弧菌,霍乱生物型所致',
'古典生物型霍乱',
'霍乱,由于O1群霍乱弧菌,埃尔托生物型所致']

s_tools = TFIDFSimilarity()
s_tools.train(data)
s_tools.get_similarities('埃尔托生物霍乱',3)

技巧

  • 生成词典可以选择以词为单位,以字为单位,或拆分成所有可能的词...
  • 可去掉停用词和与主题无关的词
  • 可加入其它的语料训练 TFIDF,以学习词的重要性(不加入候选集即可)
  • 如果搜索句子中的词未出现在字典中,将自动忽略该词
  • 如包含英文,请注意大小写