本文将介绍一下内容:

  • NLP中常见的词袋模型(Bag of Words)
  • 如何构造句向量(Sentence Embedding)
  • 利用词袋模型来计算句子间的余弦相似度(余弦相似度cosine similarity)
  • 使用编辑距离算法计算句子的相似度(编辑距离相似度)

一,什么是词袋模型

1,分句和分词

通常,NLP无法一下子处理完整的段落或句子,因此,第一步往往是分句和分词。这里只有句子,因此我们只需要分词即可。

对于英语句子,可以使用NLTK中的word_tokenize函数,对于中文句子,则可使用jiebaltp 模块。
故第一步为分词,代码如下(如果执行遇到“raise LookupError”报错,请转至另一篇博客解决):

from nltk import word_tokenize

sent1 = "I love sky, I love sea."
sent2 = "I like running, I love reading."

sents = [sent1, sent2]
texts = [[word for word in word_tokenize(sent)] for sent in sents]

# ------ output------
[['I', 'love', 'sky', ',', 'I', 'love', 'sea', '.'], ['I', 'like', 'running', ',', 'I', 'love', 'reading', '.']]
2,构建语料库(词袋)
all_list = []
for text in texts:
    all_list += text
corpus = set(all_list)
print(corpus)

# ------ output------
{'sky', ',', 'love', '.', 'reading', 'running', 'like', 'I', 'sea'}

二,构造句向量

1,建立语料库数字映射

可以看到,语料库中一共是8个单词及标点。接下来,对语料库中的单词及标点建立数字映射,便于后续的句子的向量表示。代码如下:

corpus_dict = dict(zip(corpus, range(len(corpus))))
print(corpus_dict)

# ------ output------
{'sky': 0, ',': 1, 'love': 2, '.': 3, 'reading': 4, 'running': 5, 'like': 6, 'I': 7, 'sea': 8}

虽然单词及标点并没有按照它们出现的顺序来建立数字映射,不过这并不会影响句子的向量表示及后续的句子间的相似度。

2,建立句子的向量表示

词袋模型的关键一步,就是建立句子的向量表示。这个表示向量并不是简单地以单词或标点出现与否来选择0,1数字,而是把单词或标点的出现频数作为其对应的数字表示,结合刚才的语料库字典,句子的向量表示的代码如下:

# 建立句子的向量表示
def vector_rep(text, corpus_dict):
    vec = []
    for key in corpus_dict.keys():
        if key in text:
            vec.append((corpus_dict[key], text.count(key)))
        else:
            vec.append((corpus_dict[key], 0))

    vec = sorted(vec, key= lambda x: x[0])

    return vec

vec1 = vector_rep(texts[0], corpus_dict)
vec2 = vector_rep(texts[1], corpus_dict)
print(vec1)
print(vec2)

# ------ output------
[(0, 1), (1, 1), (2, 2), (3, 1), (4, 0), (5, 0), (6, 0), (7, 2), (8, 1)]
[(0, 0), (1, 1), (2, 1), (3, 1), (4, 1), (5, 1), (6, 1), (7, 2), (8, 0)]

让我们稍微逗留一会儿,来看看这个向量。在第一句中I出现了两次,在预料库字典中,I对应的数字为7,因此在第一句中7出现2次,在列表中的元组即为(7,2),代表单词I在第一句中出现了2次。以上的输出可能并不那么直观,真实的两个句子的代表向量应为:

vec1 = [ i[1] for i in vec1]
vec2 = [ i[1] for i in vec2]

# ------ output------
[1, 1, 2, 1, 0, 0, 0, 2, 1]
[0, 1, 1, 1, 1, 1, 1, 2, 0]

词袋模型到此结束。接下来,我们会利用刚才得到的词袋模型,即两个句子的向量表示,来计算相似度。

三,使用词袋模型来计算句子间的余弦相似度

在NLP中,如果得到了两个句子的向量表示,那么,一般会选择用余弦相似度作为它们的相似度,而向量的余弦相似度即为两个向量的夹角的余弦值。

1,余弦相似度的计算公式
1)余弦相似性简介

余弦相似性通过测量两个向量的夹角的余弦值来度量它们之间的相似性。0度角的余弦值是1,而其他任何角度的余弦值都不大于1;并且其最小值是-1。从而两个向量之间的角度的余弦值确定两个向量是否大致指向相同的方向。两个向量有相同的指向时,余弦相似度的值为1;两个向量夹角为90°时,余弦相似度的值为0;两个向量指向完全相反的方向时,余弦相似度的值为-1。这结果是与向量的长度无关的,仅仅与向量的指向方向相关。余弦相似度通常用于正空间,因此给出的值为-1到1之间。

注意这上下界对任何维度的向量空间中都适用,而且余弦相似性最常用于高维正空间。例如在信息检索中,每个词项被赋予不同的维度,而一个维度由一个向量表示,其各个维度上的值对应于该词项在文档中出现的频率。余弦相似度因此可以给出两篇文档在其主题方面的相似度。

2)公式解析

a.范数公式

nlp python 文本相似度 nlp句子相似度_nlp python 文本相似度


b.欧几里得点积公式

两个向量间的余弦值可以通过使用欧几里得点积公式求出:

nlp python 文本相似度 nlp句子相似度_余弦相似度_02


c.余弦相似性θ

给定两个属性向量,A和B,其余弦相似性θ由点积和向量长度给出,如下所示:

nlp python 文本相似度 nlp句子相似度_python_03

这里的分别代表向量A和B的各分量。
给出的相似性范围从-1到1:-1意味着两个向量指向的方向正好截然相反,1表示它们的指向是完全相同的,0通常表示它们之间是独立的,而在这之间的值则表示中间的相似性或相异性。
对于文本匹配,属性向量A和B通常是文档中的词频向量。余弦相似性,可以被看作是在比较过程中把文件长度正规化的方法。
在信息检索的情况下,由于一个词的频率(TF-IDF权)不能为负数,所以这两个文档的余弦相似性范围从0到1。并且,两个词的频率向量之间的角度不能大于90°。

2,代码实现
from math import sqrt
def similarity_with_2_sents(vec1, vec2):
    inner_product = 0
    square_length_vec1 = 0
    square_length_vec2 = 0
    for tup1, tup2 in zip(vec1, vec2):
        inner_product += tup1*tup2
        square_length_vec1 += tup1**2
        square_length_vec2 += tup2**2
    return (inner_product/sqrt(square_length_vec1*square_length_vec2))

cosine_sim = similarity_with_2_sents(vec1, vec2)
print('两个句子的余弦相似度为: %.4f。'%cosine_sim)

# ------ output------
两个句子的余弦相似度为: 0.7303。
3,整理代码如下
from nltk import word_tokenize
from math import sqrt

def similarity_with_2_sents(vec1, vec2):
    inner_product = 0
    square_length_vec1 = 0
    square_length_vec2 = 0
    for tup1, tup2 in zip(vec1, vec2):
        inner_product += tup1*tup2
        square_length_vec1 += tup1**2
        square_length_vec2 += tup2**2
    return (inner_product/sqrt(square_length_vec1*square_length_vec2))

# 建立句子的向量表示
def vector_rep(text, corpus_dict):
    vec = []
    for key in corpus_dict.keys():
        if key in text:
            vec.append((corpus_dict[key], text.count(key)))
        else:
            vec.append((corpus_dict[key], 0))

    vec = sorted(vec, key= lambda x: x[0])

    return vec


sent1 = "I love sky, I love sea."
sent2 = "I like running, I love reading."

sents = [sent1, sent2]
texts = [[word for word in word_tokenize(sent)] for sent in sents]

all_list = []
for text in texts:
    all_list += text
corpus = set(all_list)
print(corpus)

corpus_dict = dict(zip(corpus, range(len(corpus))))
print(corpus_dict)

vec1 = vector_rep(texts[0], corpus_dict)
vec2 = vector_rep(texts[1], corpus_dict)
print(vec1)
print(vec2)

vec1 = [ i[1] for i in vec1]
vec2 = [ i[1] for i in vec2]

cosine_sim = similarity_with_2_sents(vec1, vec2)
print('两个句子的余弦相似度为: %.4f。'%cosine_sim)

#------output-----------
{',', 'reading', 'sky', 'sea', 'running', 'love', '.', 'like', 'I'}
{',': 0, 'reading': 1, 'sky': 2, 'sea': 3, 'running': 4, 'love': 5, '.': 6, 'like': 7, 'I': 8}
[(0, 1), (1, 0), (2, 1), (3, 1), (4, 0), (5, 2), (6, 1), (7, 0), (8, 2)]
[(0, 1), (1, 1), (2, 0), (3, 0), (4, 1), (5, 1), (6, 1), (7, 1), (8, 2)]
两个句子的余弦相似度为: 0.7303。
4,利用gensim实现相似度计算

当然,在实际的NLP项目中,如果需要计算两个句子的相似度,我们只需调用gensim模块即可,它是NLP的利器,能够帮助我们处理很多NLP任务。下面为用gensim计算两个句子的相似度的代码:

sent1 = "I love sky, I love sea."
sent2 = "I like running, I love reading."

from nltk import word_tokenize
sents = [sent1, sent2]
texts = [[word for word in word_tokenize(sent)] for sent in sents]
print(texts)

from gensim import corpora
from gensim.similarities import Similarity

#  语料库
dictionary = corpora.Dictionary(texts)

# 利用doc2bow作为词袋模型
corpus = [dictionary.doc2bow(text) for text in texts]
similarity = Similarity('-Similarity-index', corpus, num_features=len(dictionary))
print(similarity)
# 获取句子的相似度
new_sensence = sent1
test_corpus_1 = dictionary.doc2bow(word_tokenize(new_sensence))

cosine_sim = similarity[test_corpus_1][1]
print("利用gensim计算得到两个句子的相似度: %.4f。"%cosine_sim)

注意,如果在运行代码时出现以下warning:

gensim\utils.py:1209: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
  warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")

gensim\matutils.py:737: FutureWarning: Conversion of the second argument of issubdtype from `int` to `np.signedinteger` is deprecated. In future, it will be treated as `np.int32 == np.dtype(int).type`.
  if np.issubdtype(vec.dtype, np.int):

如果想要去掉这些warning,则在导入gensim模块的代码前添加以下代码即可:

import warnings
warnings.filterwarnings(action='ignore',category=UserWarning,module='gensim')
warnings.filterwarnings(action='ignore',category=FutureWarning,module='gensim')

四,使用辑距离算法计算句子的相似度

1,编辑距离概念描述

编辑距离,又称Levenshtein Distance算法,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。俄罗斯科学家Vladimir Levenshtein在1965年提出这个概念。

2,实现原理

找出字符串的编辑距离,即把一个字符串s1最少经过多少步操作变成编程字符串s2,操作有三种,添加一个字符,删除一个字符,修改一个字符。

nlp python 文本相似度 nlp句子相似度_自然语言处理_04

3,实现代码
import numpy as np

def edit_distance(sentence1, sentence2):
    len1 = len(sentence1)  # 行数
    len2 = len(sentence2)  # 列数
    dp = np.zeros((len1 + 1,len2 + 1))
    for i in range(len1 + 1):
        dp[i][0] = i    
    for j in range(len2 + 1):
        dp[0][j] = j
    
    # 第二行到最后一行
    for i in range(1, len1 + 1):
        # 第二列到最后一列
        for j in range(1, len2 + 1):
            # 如果两个数相等则为0,否则为1
            # i=1; j=1
            if sentence1[i-1] == sentence2[j-1]:
                delta = 0
            else:
                delta = 1
            # 当前单元格
            dp[i][j] = min(dp[i-1][j] + 1, dp[i][j-1] + 1, dp[i-1][j-1] + delta)
            
    max_len = max(len1,len2)                
    return (max_len - dp[len1][len2]) / max_len

sentence1 = "你妈妈叫你回家吃饭了!"
sentence2 = "妈妈叫小明回家吃饭了."
similar_num = edit_distance(sentence1, sentence2)
print("最短编辑距离算法得到的相似度为:%f"%similar_num)

# ------output-------
最短编辑距离算法得到的相似度为:0.636364