文本相似度
在自然语言处理中,经常会涉及到度量两个文本的相似性的问题,在诸如信息检索、数据挖掘、机器翻译、文档复制检测等领域中,如何度量句子或短语之间的相似度显得尤为重要。
文本相似度的衡量计算主要包括如下三种方法:
(1) 基于关键字匹配的传统方法,比如N-gram相似度。
(2) 将文本映射到向量空间,再利用余弦相似度等方法进行计算。
(3) 基于深度学习的方法,比如卷积神经网络的ConvNet、用户点击数据的深度学习语义匹配模型DSSM等。
随着深度学习的发展,文本相似度的方法已经逐渐不再是基于关键词匹配的传统方法,而是转向了深度学习,目前结合向量的深度学习使用较多,因此,这里我们采用第二种方式来计算文本的相似度,一般实现的步骤如下。
(1) 通过特征提取的模型或手动实现,找出这两篇文章的关键词。
(2) 从每篇文章中各取出若干个关键词(比如10个),把这些关键词合并成一个集合,然后计算每篇文章中各个词对于这个集合中的关键词的词频。为了避免文章长度的差异,可以使用相对词频。
(3) 生成两篇文章中各自的词频向量。
(4) 计算两个向量的余弦相似度,值越大则表示越相似。
我们都知道,文本是一种高维的语义空间,要想计算两个文本的相似度,可以先将它们转化为向量,站在数学角度上去量化其相似性,这样就比较简单了。那么,如何把文本转化成向量呢?一般,我们会使用词频(某一个给定词语在文档中出现的次数)来表示文本特征,若某个词在这些文本中出现的次数最多,则表示这个单词比较具有代表性。
例如,现在有如下两个英文句子:
John likes to watch movies.
John also likes to watch football games.
要想找出上述两个句子中的关键词,则需要先统计一下每个单词出现的次数。为此,NLTK库中提供了一个FreqDist类,主要负责记录每个词出现的次数。FreqDist类的结构比较简单,可以用一个有序词典实现,所以dict的方法在此类中也是适用的。例如,使用FreqDist类统计上述英文句子中每个单词的词频,具体代码如下。
In [29]: import nltk
from nltk import FreqDist
text1 = 'John likes to watch movies'
text2 = 'John also likes to watch football games'
all_text = text1 +" " + text2
# 分词
words = nltk.word_tokenize(all_text)
# 创建FreqDist对象,记录每个单词出现的频率
freq_dist = FreqDist(words)
freq_dist
Out[29]: FreqDist({'John': 2, 'likes': 2, 'to': 2, 'watch': 2,
'movies': 1, 'also': 1, 'football': 1, 'games': 1})
上述输出了创建的FreqDist对象,它的里面是一个字典结构,其中字典的键为分词后的单词,字典的值为该词出现的频率。例如,获取字典里面单词“John”出现的频率,代码如下。
In [30]: freq_dist['John']
Out[30]: 2
这里将从这些单词中选出出现频率最高的若干个单词,构造成关键词列表。如果希望达到这个目的,则需要调用FreqDist类的most_common()方法,返回出现次数比较频繁的词与频率。
例如,从freq_dist中取出出现频率最高的五个单词,代码如下。
In [31]: # 取出n个常用的单词
n = 5
# 返回常用单词列表
most_common_words = freq_dist.most_common(n)
most_common_words
Out[31]: [('John', 2), ('likes', 2), ('to', 2), ('watch', 2), ('movies', 1)]
选出关键词之后,我们需要记录一下这些单词的位置,为了简单起见,这里采用的是从字典中遍历取出每个单词,第一个单词的位置赋值为1,第二个单词的位置赋值为2,依此类推。
例如,查找常用单词的位置的示例如下。
# 查找常用单词的位置
In [32]: def find_position(common_words):
result = {}
pos = 0
for word in common_words:
result[word[0]] = pos
pos += 1
return result
# 记录常用单词的位置
pos_dict = find_position(most_common_words)
pos_dict
Out[32]: {'John': 0, 'likes': 1, 'to': 2, 'watch': 3, 'movies': 4}
上述示例输出了关键词与位置的字典,其中,“John”对应着位置0,“likes”对应着位置1……由此表明,根据这些位置可以确定一个关键词列表,也就是得到一个向量为['John', 'likes', 'to', 'watch', 'movies']。
如果句子中的某个单词存在于关键词列表中,就在关键词所在的位置上标记一次,结果为记录总次数,对于没有出现的单词则记为0即可,从而构成了一个词频列表。定义一个函数参照着关键词列表统计单词的词频,并以列表的形式进行返回,具体代码如下。
In [33]: def text_to_vector(words):
'''
将文本转换为词频向量
'''
# 初始化向量
freq_vec = [0] * n
# 在“常用单词列表”上计算词频
for word in words:
if word in list(pos_dict.keys()):
freq_vec[pos_dict[word]] += 1
return freq_vec
将文本text1和text2转化为词频向量,示例代码如下。
In [34]: # 词频向量
vector1 = text_to_vector(nltk.word_tokenize(text1))
vector1
Out[34]: [1, 1, 1, 1, 1]
In [35]: vector2 = text_to_vector(nltk.word_tokenize(text2))
vector2
Out[35]: [1, 1, 1, 1, 0]
现在只剩下最后一步,利用余弦相似度来比较两个向量的相似度。余弦相似度,又称为余弦相似性,通过计算两个向量的夹角余弦值来评估它们的相似度。计算两个向量的余弦相似度公式如下:
求得两个向量的夹角,并得出夹角对应的余弦值,此余弦值就可以用来表征这两个向量的相似性,如图1所示。
图1 求余弦值图例
余弦取值范围为[-1,1]。夹角越小,趋近于0度,余弦值越接近于1,它们的方向更加吻合,则越相似。当两个向量的方向完全相反,夹角余弦取最小值-1。当余弦值为0时,两向量正交,夹角为90度。由此可以看出,余弦相似度与向量的幅值无关,只与向量的方向相关。
NLTK库中提供了余弦相似度的实现函数cosine_distance(),位于cluster.util模块中,所以这里需要使用import引入该模块,之后在调用cosine_distance()函数时只要传入两个向量值就行。
例如,求两个词频向量的夹角余弦值,代码如下。
In [36]: from nltk.cluster.util import cosine_distance
cosine_distance(vector1, vector2)
Out[36]: 0.10557280900008414
从输出的余弦值可以看出,它接近于0,表明向量vector1和vector2正交,夹角接近于90度。由此表明,text1和text2文本的相似度并不高。