中文分词、文本的数学表示和相似度计算

1,845 阅读6分钟

这是我参与11月更文挑战的第5天,活动详情查看:2021最后一次更文挑战

在日常生活中,我们经常会看到聊天机器人的应用,比如线上淘宝客服,微信自动回复,线下店铺机器人接待等。聊天机器人可以帮助我们从重复、繁琐的工作中解放出来。想要开发出一款聊天机器人,需要首先掌握三个基础概念,分别是:中文分词文本的数学表示文本的相似度计算

中文分词

中文分词就是将一句句子拆分成独立的词语,机器人接收到了一句话,首先需要将其拆分成一个个词语,便于根据关键字来做出相应的回应。Python 提供的 Jieba 分词库可以帮助我们完成这项工作。

使用 Jieba 得到句子分词的示例:

import jieba
s = 'Python是一种面向对象的动态类型语言。'
[ print(c) for c in jieba.cut(s)]

结果:

Python
是
一种
面向对象
的
动态
类型
语言
。

文本的数学表示

计算机直接使用的语言是机器码,机器码就是使用二进制编码的指令。因此需要先将文本翻译成机器可识别的机器码,再交给机器处理。

在数学中,可以用向量来表示一个词,这称之为词向量

比如有这样一个词典:

Python、 是、 一种、 面向对象、 的、 动态、 类型、 语言

再给定以下三个词:

Python、 面向对象、 人工智能

这三个词的词向量分别为:

  • Python。(1, 0, 0, 0, 0, 0, 0, 0)
  • 面向对象。(0, 0, 0, 1, 0, 0, 0, 0)
  • 人工智能。(0, 0, 0, 0, 0, 0, 0, 0)

可见,如果词典上的某一位置的词与给定的词匹配,则置为 1,否则置为 0。

使用 Python 完成词向量转换:

# 词库
word_vector_list = ["Python", "是", "一种", "面向对象", "的", "动态", "类型", "语言"]
# 要转成词向量的词
word1 = "Python"
word2 = "面向对象"
word3 = "人工智能"

# 定义词向量转换方法
def get_word_vector_result(word):
    return [ 1 if(w == word) else 0 for w in word_vector_list]

print(get_word_vector_result(word1))
print(get_word_vector_result(word2))
print(get_word_vector_result(word3))

结果:

[1, 0, 0, 0, 0, 0, 0, 0]
[0, 0, 0, 1, 0, 0, 0, 0]
[0, 0, 0, 0, 0, 0, 0, 0]

词向量如何表示现在已经知道了,那如何将一句句子表示成向量呢?

还是使用刚才的词典:

Python、 是、 一种、 面向对象、 的、 动态、 类型、 语言

再给定以下两句句子:

Python是一种高级语言

我们在学习Python

这两句句子的向量分别为:

  • Python是一种高级语言。(1, 1, 1, 0, 0, 0, 0, 1)
  • 我们在学习Python。(1, 0, 0, 0, 0, 0, 0, 0)

在得到句子的向量之前,需要先拆分出每一句句子的分词,再逐一检查词典中的分词是否存在于给定句子的分词中,若存在则置为 1,否则置为 0。

实际上句子向量的转换本质就是词向量的转换,只不过句子是由多个分词构成。

使用 Python 完成句向量转换:

import jieba
# 词库
word_vector_list = ["Python", "是", "一种", "面向对象", "的", "动态", "类型", "语言"]
# 用户输入的语句
s1 = "Python是一种高级语言"
s2 = "我们在学习Python"

# 转化成向量的方法
def get_vector(data):
    data_iter = list(jieba.cut(data))
    return [ 1 if(w in data_iter) else 0 for w in word_vector_list]

# 打印向量
print(get_vector(s1))
print(get_vector(s2))

Python 提供了 gensim 工具来帮助我们完成向量的转换:

from gensim.models import word2vec

# 从 xxx.txt 读取句子,文件中存的是一个个分词
sentences = word2vec.Text8Corpus('xxx.txt')

# 将句子转换为向量,句子的分词数量较小时,需要加上 min_count=1 参数
model = word2vec.Word2Vec(sentences, min_count=1)

# 将向量结果保存到 word2vec.model 中
model.save('word2vec.model')

# 得到分词的向量
model.wv['分词1', '分词2']

文本的相似度计算

在机器人程序中通常会内置多个问答的模板,我们可以拿用户发送的句子从模板库里匹配相应的回答来回应用户,而用户发送的句子是不可预知的,因此可以找出模板库里与用户的句子最相似的语句来进行匹配,这就涉及到了文本的相似度计算。文本的相似度计算涉及到的知识点有:欧氏距离曼哈顿(街区)距离余弦相似度

欧氏距离

在直角坐标系中,给定两个点:a(x1, y1)、b(x2, y2),它们的距离计算公式如下:

d=(x1x2)2+(y1y2)2d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}

这样得到的距离就是欧式距离。将其扩展到 n 维空间下的距离计算公式如下:

d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2 + ... + (n_1 - n_2)^2}

那如何通过欧式距离计算两个句子的相似度呢?

给定以下两个句子的句向量:

  • Python是一种高级语言。(1, 1, 1, 0, 0, 0, 0, 1)
  • 我们在学习Python。(1, 0, 0, 0, 0, 0, 0, 0)

套用欧式距离计算公式,得:

d = \sqrt{(1 - 1)^2 + (1 - 0)^2 + ... + (1 - 0)^2}

d 越接近于 0 时,句子的相似度越高。

曼哈顿距离

曼哈顿距离,也叫作曼哈顿街区距离。直角坐标系中两点之间的曼哈顿距离计算公式如下:

d = |x_1 - x_2| + |y_1 - y_2|

也就是两点连线所在直角三角形的直角边之和。

扩展到 n 维空间下的距离计算公式如下:

d = |x_1 - x_2| + |y_1 - y_2| + ... + |n_1 - n_2|

同样地,d 越接近于 0 ,句子的相似度越高。

余弦相似度

余弦相似度的本质就是计算两个向量所成夹角的余弦值,n 维向量的余弦相似度计算公式为:

cosθ=x1y1+x2y2+...+xnynx12+x22+...+xn2y12+y22+...+yn2cos\theta = \frac{x_1y_1 + x_2y_2 + ... + x_ny_n}{\sqrt{x_1^2 + x_2^2 + ... + x_n^2} \cdot \sqrt{y_1^2 + y_2^2 + ... + y_n^2}}

将两个 n 维句子向量的值分别代入上式,得到的值就是这两个句子的余弦相似度。

余弦值的区间是 [-1, 1],当值越趋近 1 时,表示两个句子越相似,越趋近 -1 时,表示两个句子越不相似。

计算分词相似度的完整代码示例

import jieba
from gensim.models import word2vec

# 打开 fenci.txt,内容是一段原始文本
file1 = open('fenci.txt', encoding='utf-8')

# 将分词后的结果保存到 fen_ci.txt,
# open() 第二个参数是 mode,可传入 r/w/x/a
# 'r' -> readonly, 'w' -> truncating the file if it already exists , 'x' -> creating and writing to a new file, 'a' -> append
file2 = open('fenci_result.txt', mode='w', encoding='utf-8')

# 读取 fenci.txt 中的所有行
lines = file1.readlines()

# 将文本的空格、tab缩进、回车换行符都去掉,以便后续对整个文本内容进行分词
for line in lines:
    replaced_line = line.replace(' ', '').replace('\t', '').replace('\r', '').replace('\n', '')
    seg_list = jieba.cut(replaced_line)
    file2.write(' '.join(seg_list))

# 关闭资源
file1.close()
file2.close()

# 加载刚刚生成的语料库,就是已经生成的分词文件
sentences = word2vec.Text8Corpus('fenci_result.txt')

# 使用 word2vec 模型来训练机器(这里就是计算词向量),由于语料库中的分词数较少,需要加上 min_count=1 (最小分词数量=1)参数,否则会报错
model = word2vec.Word2Vec(sentences, min_count=1)

# 将模型命名为 word2vec.model,并保存为本地文件
model.save('word2vec.model')

# 得到词向量,要计算向量的分词必须存在于语料库中,否则会由于找不到分词而报错
word_vec_arr = model.wv['Python', '面向对象']

# 打印词向量
print(word_vec_arr)

# 计算两个分词的相似度
s1 = model.wv.similarity('Python', '面向对象')
s2 = model.wv.similarity('会', '不会')
s3 = model.wv.similarity('会', '会')

# 打印的结果:0.06410548、 -0.069218084、 1.0(完全一致)
print(s1, s2, s3)