自然语言处理中句子相似度计算的几种方法

在做自然语言处理的过程中,我们经常会遇到需要找出相似语句的场景,或者找出句子的近似表达,这时候我们就需要把类似的句子归到一起,这里面就涉及到句子相似度计算的问题,那么本节就来了解一下怎么样来用 Python 实现句子相似度的计算。

基本方法

句子相似度计算我们一共归类了以下几种方法:

  • 编辑距离计算

  • 杰卡德系数计算

  • TF 计算

  • TFIDF 计算

  • Word2Vec 计算

下面我们来一一了解一下这几种算法的原理和 Python 实现。

编辑距离计算

编辑距离,英文叫做 Edit Distance,又称 Levenshtein 距离,是指两个字串之间,由一个转成另一个所需的最少编辑操作次数,如果它们的距离越大,说明它们越是不同。许可的编辑操作包括将一个字符替换成另一个字符,插入一个字符,删除一个字符。

例如我们有两个字符串:string 和 setting,如果我们想要把 string 转化为 setting,需要这么两步:

  • 第一步,在 s 和 t 之间加入字符 e。

  • 第二步,把 r 替换成 t。

所以它们的编辑距离差就是 2,这就对应着二者要进行转化所要改变(添加、替换、删除)的最小步数。

那么用 Python 怎样来实现呢,我们可以直接使用 distance 库:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

s1 = 'string'
s2 = 'setting'
print(edit_distance(s1, s2))复制代码

这里我们直接使用 distance 库的 levenshtein() 方法,传入两个字符串,即可获取两个字符串的编辑距离了。

运行结果如下:

2复制代码

这里的 distance 库我们可以直接使用 pip3 来安装:

pip3 install distance复制代码

这样如果我们想要获取相似的文本的话可以直接设定一个编辑距离的阈值来实现,如设置编辑距离为 2,下面是一个样例:

import distance

def edit_distance(s1, s2):
    return distance.levenshtein(s1, s2)

strings = [
    '你在干什么',
    '你在干啥子',
    '你在做什么',
    '你好啊',
    '我喜欢吃香蕉'
]

target = '你在干啥'
results = list(filter(lambda x: edit_distance(x, target) <= 2, strings))
print(results)复制代码

这里我们定义了一些字符串,然后定义了一个目标字符串,然后用编辑距离 2 的阈值进行设定,最后得到的结果就是编辑距离在 2 及以内的结果,运行结果如下:

['你在干什么', '你在干啥子']复制代码

通过这种方式我们可以大致筛选出类似的句子,但是发现一些句子例如“你在做什么” 就没有被识别出来,但他们的意义确实是相差不大的,因此,编辑距离并不是一个好的方式,但是简单易用。

杰卡德系数计算

杰卡德系数,英文叫做 Jaccard index, 又称为 Jaccard 相似系数,用于比较有限样本集之间的相似性与差异性。Jaccard 系数值越大,样本相似度越高。

实际上它的计算方式非常简单,就是两个样本的交集除以并集得到的数值,当两个样本完全一致时,结果为 1,当两个样本完全不同时,结果为 0。

算法非常简单,就是交集除以并集,下面我们用 Python 代码来实现一下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np

def jaccard_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 求交集
    numerator = np.sum(np.min(vectors, axis=0))
    # 求并集
    denominator = np.sum(np.max(vectors, axis=0))
    # 计算杰卡德系数
    return 1.0 * numerator / denominator

s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(jaccard_similarity(s1, s2))复制代码

这里我们使用了 Sklearn 库中的 CountVectorizer 来计算句子的 TF 矩阵,然后利用 Numpy 来计算二者的交集和并集,随后计算杰卡德系数。

这里值得学习的有 CountVectorizer 的用法,通过它的 fit_transform() 方法我们可以将字符串转化为词频矩阵,例如这里有两句话“你在干嘛呢”和“你在干什么呢”,首先 CountVectorizer 会计算出不重复的有哪些字,会得到一个字的列表,结果为:

['么', '什', '你', '呢', '嘛', '在', '干']复制代码

这个其实可以通过如下代码来获取,就是获取词表内容:

cv.get_feature_names()复制代码

接下来通过转化之后,vectors 变量就变成了:

[[0 0 1 1 1 1 1]
 [1 1 1 1 0 1 1]]复制代码

它对应的是两个句子对应词表的词频统计,这里是两个句子,所以结果是一个长度为 2 的二维数组,比如第一句话“你在干嘛呢”中不包含“么”字,那么第一个“么”字对应的结果就是0,即数量为 0,依次类推。

后面我们使用了 np.min() 方法并传入了 axis 为 0,实际上就是获取了每一列的最小值,这样实际上就是取了交集,np.max() 方法是获取了每一列的最大值,实际上就是取了并集。

二者分别取和即是交集大小和并集大小,然后作商即可,结果如下:

0.5714285714285714复制代码

这个数值越大,代表两个字符串越接近,否则反之,因此我们也可以使用这个方法,并通过设置一个相似度阈值来进行筛选。

TF计算

第三种方案就是直接计算 TF 矩阵中两个向量的相似度了,实际上就是求解两个向量夹角的余弦值,就是点乘积除以二者的模长,公式如下:

cosθ=a·b/|a|*|b|复制代码

上面我们已经获得了 TF 矩阵,下面我们只需要求解两个向量夹角的余弦值就好了,代码如下:

from sklearn.feature_extraction.text import CountVectorizer
import numpy as np
from scipy.linalg import norm

def tf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = CountVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 计算TF系数
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))

s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tf_similarity(s1, s2))复制代码

在在这里我们使用了 np.dot() 方法获取了向量的点乘积,然后通过 norm() 方法获取了向量的模长,经过计算得到二者的 TF 系数,结果如下:

0.7302967433402214复制代码

TFIDF计算

另外除了计算 TF 系数我们还可以计算 TFIDF 系数,TFIDF 实际上就是在词频 TF 的基础上再加入 IDF 的信息,IDF 称为逆文档频率,不了解的可以看下阮一峰老师的讲解:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html,里面对 TFIDF 的讲解也是十分透彻的。

下面我们还是借助于 Sklearn 中的模块 TfidfVectorizer 来实现,代码如下:

from sklearn.feature_extraction.text import TfidfVectorizer
import numpy as np
from scipy.linalg import norm

def tfidf_similarity(s1, s2):
    def add_space(s):
        return ' '.join(list(s))

    # 将字中间加入空格
    s1, s2 = add_space(s1), add_space(s2)
    # 转化为TF矩阵
    cv = TfidfVectorizer(tokenizer=lambda s: s.split())
    corpus = [s1, s2]
    vectors = cv.fit_transform(corpus).toarray()
    # 计算TF系数
    return np.dot(vectors[0], vectors[1]) / (norm(vectors[0]) * norm(vectors[1]))

s1 = '你在干嘛呢'
s2 = '你在干什么呢'
print(tfidf_similarity(s1, s2))复制代码

这里的 vectors 变量实际上就对应着 TFIDF 值,内容如下:

[[0.         0.         0.4090901  0.4090901  0.57496187 0.4090901 0.4090901 ]
 [0.49844628 0.49844628 0.35464863 0.35464863 0.  0.35464863 0.35464863]]复制代码

运行结果如下:

0.5803329846765686复制代码

所以通过 TFIDF 系数我们也可以进行相似度的计算。

Word2Vec计算

Word2Vec,顾名思义,其实就是将每一个词转换为向量的过程。如果不了解的话可以参考:https://blog.csdn.net/itplus/article/details/37969519。

这里我们可以直接下载训练好的 Word2Vec 模型,模型的链接地址为:https://pan.baidu.com/s/1TZ8GII0CEX32ydjsfMc0zw,是使用新闻、百度百科、小说数据来训练的 64 维的 Word2Vec 模型,数据量很大,整体效果还不错,我们可以直接下载下来使用,这里我们使用的是 news_12g_baidubaike_20g_novel_90g_embedding_64.bin 数据,然后实现 Sentence2Vec,代码如下:

import gensim
import jieba
import numpy as np
from scipy.linalg import norm

model_file = './word2vec/news_12g_baidubaike_20g_novel_90g_embedding_64.bin'
model = gensim.models.KeyedVectors.load_word2vec_format(model_file, binary=True)

def vector_similarity(s1, s2):
    def sentence_vector(s):
        words = jieba.lcut(s)
        v = np.zeros(64)
        for word in words:
            v += model[word]
        v /= len(words)
        return v

    v1, v2 = sentence_vector(s1), sentence_vector(s2)
    return np.dot(v1, v2) / (norm(v1) * norm(v2))复制代码

在获取 Sentence Vector 的时候,我们首先对句子进行分词,然后对分好的每一个词获取其对应的 Vector,然后将所有 Vector 相加并求平均,这样就可得到 Sentence Vector 了,然后再计算其夹角余弦值即可。

调用示例如下:

s1 = '你在干嘛'
s2 = '你正做什么'
vector_similarity(s1, s2)复制代码

结果如下:

0.6701133967824016复制代码

这时如果我们再回到最初的例子看下效果:

strings = [
    '你在干什么',
    '你在干啥子',
    '你在做什么',
    '你好啊',
    '我喜欢吃香蕉'
]

target = '你在干啥'

for string in strings:
    print(string, vector_similarity(string, target))复制代码

依然是前面的例子,我们看下它们的匹配度结果是多少,运行结果如下:

你在干什么 0.8785495016487204
你在干啥子 0.9789649689827049
你在做什么 0.8781992402695274
你好啊 0.5174225914249863
我喜欢吃香蕉 0.582990841450621复制代码

可以看到相近的语句相似度都能到 0.8 以上,而不同的句子相似度都不足 0.6,这个区分度就非常大了,可以说有了 Word2Vec 我们可以结合一些语义信息来进行一些判断,效果明显也好很多。

所以总体来说,Word2Vec 计算的方式是非常好的。

另外学术界还有一些可能更好的研究成果,这个可以参考知乎上的一些回答:https://www.zhihu.com/question/29978268/answer/54399062。

以上便是进行句子相似度计算的基本方法和 Python 实现,本节代码地址:https://github.com/AIDeepLearning/SentenceDistance。

嗨~ 给大家重磅推荐一本书!上市两月就已经重印 4 次的 Python 爬虫书!它就是由静觅博客博主崔庆才所作的《Python3网络爬虫开发实战》!!!同时文末还有抽奖赠书活动,不容错过!!!

书籍介绍

本书《Python3网络爬虫开发实战》全面介绍了利用 Python3 开发网络爬虫的知识,书中首先详细介绍了各种类型的环境配置过程和爬虫基础知识,还讨论了 urllib、requests 等请求库和 Beautiful Soup、XPath、pyquery 等解析库以及文本和各类数据库的存储方法,另外本书通过多个真实新鲜案例介绍了分析 Ajax 进行数据爬取,Selenium 和 Splash 进行动态网站爬取的过程,接着又分享了一些切实可行的爬虫技巧,比如使用代理爬取和维护动态代理池的方法、ADSL 拨号代理的使用、各类验证码(图形、极验、点触、宫格等)的破解方法、模拟登录网站爬取的方法及 Cookies 池的维护等等。

此外,本书的内容还远远不止这些,作者还结合移动互联网的特点探讨了使用 Charles、mitmdump、Appium 等多种工具实现 App 抓包分析、加密参数接口爬取、微信朋友圈爬取的方法。此外本书还详细介绍了 pyspider 框架、Scrapy 框架的使用和分布式爬虫的知识,另外对于优化及部署工作,本书还包括 Bloom Filter 效率优化、Docker 和 Scrapyd 爬虫部署、分布式爬虫管理框架Gerapy 的分享。

全书共 604 页,足足两斤重呢~ 定价为 99 元!

作者介绍

看书就先看看谁写的嘛,我们来了解一下~

崔庆才,静觅博客博主(https://cuiqingcai.com),博客 Python 爬虫博文阅读量已过百万,北京航空航天大学硕士,天善智能、网易云课堂讲师,微软小冰大数据工程师,有多个大型分布式爬虫项目经验,乐于技术分享,文章通俗易懂 ^_^

附皂片一张 ~(@^_^@)~

更多详请点击➡️juejin.cn/post/684490…