NLP之句子相似度之入门篇_AI蜗牛之家的博客-程序员信息网_nlp判断两句话相似性

技术标签: 自然语言处理  


如下在师兄的博文基础上修改: 静觅 » 自然语言处理中句子相似度计算的几种方法

1.基于统计的方法

1.1.编辑距离计算

编辑距离,英文叫做 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 及以内的结果,运行结果如下:

['你在干什么', '你在干啥子']

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

1.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

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

1.3.TF 计算

第三种方案就是直接计算 TF 矩阵中两个向量的余弦相似度了,实际上就是求解两个向量夹角的余弦值,就是点乘积除以二者的模长,公式如下: c o s θ = a ⋅ b ∣ a ∣ ∣ b ∣ cosθ=\frac {a \cdot b}{|a||b|} cosθ=abab
更多关于余弦相似度:

上面我们已经获得了 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

1.4.TFIDF 计算

另外除了计算 TF 系数我们还可以计算 TFIDF 系数,TFIDF 实际上就是在词频 TF 的基础上再加入 IDF 的信息,IDF 称为逆文档频率,不了解的可以看下阮一峰老师的讲解:http://www.ruanyifeng.com/blog/2013/03/tf-idf.html,里面对 TFIDF 的讲解也是十分透彻的。
关于idf(t)应该这样认识:一个词语在文档集合中出现了n次,文档集合总数为N。idf(t)来源于信息论。那么每篇文档出现这个词语的概率为: n / N n/N n/N,所以这篇文档出现这个词语的信息量为: − l o g ( n / N ) -log(n/N) log(n/N)。这个和信息熵有些类似 ( − P ( x ) l o g P ( x ) ) (-P(x)logP(x)) (P(x)logP(x)),在数据挖掘的过滤法进行特征选择时,需要用到互信息,其实是计算信息增益,还有决策树。把 − l o g ( n / N ) -log(n/N) log(n/N)变换一下, l o g ( N / n ) log(N/n) log(N/n),为了避免0的出现,进行平滑处理,就是上面的公式(就像朴素贝叶斯需要拉普拉斯平滑处理一样)。

下面我们还是借助于 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 系数我们也可以进行相似度的计算。

1.5.BM25

BM25 is a bag-of-words retrieval function that ranks a set of documents based on the query terms appearing in each document, regardless of the inter-relationship between the query terms within a document (e.g., their relative proximity). It is not a single function, but actually a whole family of scoring functions, with slightly different components and parameters. One of the most prominent instantiations of the function is as follows.

BM25算法,通常用来作搜索相关性平分。一句话概况其主要思想:对Query进行语素解析,生成语素qi;然后,对于每个搜索结果D,计算每个语素qi与D的相关性得分,最后,将qi相对于D的相关性得分进行加权求和,从而得到Query与D的相关性得分。

BM25算法的一般性公式如下:
S c o r e ( Q , d ) = ∑ i n W i ⋅ R ( q i , d ) Score(Q,d) = \sum _i ^n W_i \cdot R(q_i,d) Score(Q,d)=inWiR(qi,d)
其中, Q Q Q表示Query, q i q_i qi表示 Q Q Q解析之后的一个语素(对中文而言,我们可以把对Query的分词作为语素分析,每个词看成语素qi。); d d d表示一个搜索结果文档; W i W_i Wi表示语素 q i q_i qi的权重; R ( q i , d ) R(q_i,d) R(qid)表示语素 q i q_i qi与文档 d d d的相关性得分。

下面我们来看如何定义 W i W_i Wi。判断一个词与一个文档的相关性的权重,方法有多种,较常用的是IDF。这里以IDF为例,公式如下:
I D F ( q i ) = l o g N − n ( q i ) + 0.5 n ( q i ) + 0.5 IDF(q_i) = log \frac {N-n(q_i)+0.5} {n(q_i)+0.5} IDF(qi)=logn(qi)+0.5Nn(qi)+0.5

其中, N N N为索引中的全部文档数, n ( q i ) n(q_i) n(qi)为包含了qi的文档数。

根据IDF的定义可以看出,对于给定的文档集合,包含了 q i q_i qi的文档数越多, q i q_i qi的权重则越低。也就是说,当很多文档都包含了 q i q_i qi时, q i q_i qi的区分度就不高,因此使用 q i q_i qi来判断相关性时的重要度就较低。

我们再来看语素 q i q_i qi与文档d的相关性得分 R ( q i , d ) R(q_i,d) R(qid)。首先来看BM25中相关性得分的一般形式:
R ( q i , d ) = f i ( k 1 + 1 ) f i + K ⋅ d f i ( k 2 + 1 ) q f i + k 2 R(q_i,d) = \frac {f_i(k_1+1)} {f_i+K} \cdot \frac{df_i(k_2+1)}{qf_i+k_2} R(qi,d)=fi+Kfi(k1+1)qfi+k2dfi(k2+1)
K = k 1 ⋅ ( 1 − b + b ⋅ d l a v b d l ) K = k_1\cdot(1-b+b \cdot \frac{dl}{avbdl}) K=k1(1b+bavbdldl)
其中, k 1 k_1 k1 k 2 k_2 k2 b b b为调节因子,通常根据经验设置,一般 k 1 k_1 k1=2, b b b=0.75 f i f_i fi q i q_i qi d d d中的出现频率, q f i qf_i qfi q i q_i qi在Query中的出现频率。 d l dl dl为文档d的长度, a v g d l avgdl avgdl为所有文档的平均长度。由于绝大部分情况下, q i q_i qi在Query中只会出现一次,即 q f i qf_i qfi=1,因此公式可以简化为:
R ( q i , d ) = f i ( k 1 + 1 ) f i + K R(q_i,d) = \frac {f_i(k_1+1)} {f_i+K} R(qi,d)=fi+Kfi(k1+1)
K K K的定义中可以看到,参数 b b b的作用是调整文档长度对相关性影响的大小。 b b b越大,文档长度的对相关性得分的影响越大,反之越小。而文档的相对长度越长, K K K值将越大,则相关性得分会越小。这可以理解为,当文档较长时,包含 q i q_i qi的机会越大,因此,同等fi的情况下,长文档与qi的相关性应该比短文档与 q i q_i qi的相关性弱。

综上,BM25算法的相关性得分公式可总结为:
S c o r e ( Q , d ) = ∑ i n I D F ( q i ) ⋅ f i ( k 1 + 1 ) f i + k 1 ⋅ ( 1 − b + b ⋅ d l a v b d l ) Score(Q,d) = \sum _i ^n IDF(q_i) \cdot \frac {f_i(k_1+1)} {f_i+k_1\cdot(1-b+b \cdot \frac{dl}{avbdl})} Score(Q,d)=inIDF(qi)fi+k1(1b+bavbdldl)fi(k1+1)
BM25考虑了4个因素:IDF因子,文档长度因子,文档词频因子和查询词频因子。lucene内部的BM25要比上面公式的简单一些,个人认为并不是很好
从BM25的公式可以看到,通过使用不同的语素分析方法、语素权重判定方法,以及语素与文档的相关性判定方法,我们可以衍生出不同的搜索相关性得分计算方法,这就为我们设计算法提供了较大的灵活性。

这里提供一份简单的源码demo,详见我的github:BM25

更多相关推到详见:经典检索算法:BM25原理(格式有有点乱,建议粘贴到Typora里面看)

2.基于深度学习的方法

2.1.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 计算的方式是非常好的。

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

6.

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

参考文献

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u014665013/article/details/90045408

智能推荐

idea中配置的gitignore不生效_~会飞的猪~的博客-程序员信息网_改变成未track状态 idea

解决方法就是先把本地缓存删除(改变成未track状态),然后再提交。使用以下几个命令即可快速解决注意:以下命令需要在你项目中右键点击 Git Bash Here进行命令窗口输入git rm -r --cached .git add .git commit -m 'update .gitignore'...

以太网协议号字段定义_dillanzhou的博客-程序员信息网_以太网帧协议号

当前使用的以太网协议一般指EthernetII协议,它是Xerox与DEC、Intel等公司在1982年制定的以太网标准帧格式,在当时是一种事实工业标准。到1985年,IEEE又发布了802.3/802.2以太网标准。这两种标准都规定以太网MAC地址为6字节,但对以太网首部第13、14字节的定义却不同。在EthernetII中,对这两字节的定义为上层协议类型字段,而在802.3中,却将这两字节定义...

CSS3 3D_huo_bao的博客-程序员信息网

三维变换使用基于二维变换的相同属性,如果您熟悉二维变换,你们发现3D变形的功能和2D变换的功能相当类似。CSS3中的3D变换主要包括以下几种功能函数: 3D位移:CSS3中的3D位移主要包括translateZ()和translate3d()两个功能函数; 3D旋转:CSS3中的3D旋转主要包括rotateX()、rotateY()、rotateZ()和rotate3d()四个功能

转 11g Grid Control: Overview of the EMCTL Options Available for Managing the Agent_weixin_34378969的博客-程序员信息网

1.概念:The Enterprise Manager DBConsole consists of the following components:- A Standalone OC4J Management Service (DBConsole OMS) (server side )- A local Management Agent (EM agent) ...

读写AT24C512的问题(续)_嵌入式乌托邦的博客-程序员信息网_at24c512 连续写

前面说了对于像AT24C512这样的内部地址是2字节的,读写遇到问题了,今天解决了,直接上代码。 unsigned char tx[2] = {0x0,0x0}; unsigned char rx[2]; unsigned char wr[3]={0x0,0x0,0x77}; ret = i2c_master_send(data->client, wr, 3);//往0x0000地

SEO-站内优化规范_weixin_33895695的博客-程序员信息网

类别要求实际工作要求程序设计1.DIV+CSS布局2.站内导航连接性良好面包屑导航,翻页方式使用样式二,文章和产品上一页和下一页3.图片的ALT属性在编程时注意写4.超级链接的Title属性在编...

随便推点

搭建layui+mybaits分页插件pagehelper实现漂亮的前端分页功能_江西DJ烟仔ReMix的博客-程序员信息网

pagehelper实现原理很简单,就是在数据库执行sql语句前将这条语句截下来拼上分页参数的字符串,具体代码如下1、引入maven依赖或jar包、建立不要引入太高的版本、否则报错会让你莫名其妙(亲身体验过高版版的一些莫名其妙这里就不多说了) &amp;lt;dependency&amp;gt; &amp;lt;groupId&amp;gt;com.github.pagehelper&amp;lt;/groupId&amp;gt; ...

服务于流媒体的实时传输协议RTP_panicyf的博客-程序员信息网

服务于流媒体的实时传输协议RTP作者:清华大学信息学院多媒体中心 赵勇 曾珂 戴琼海 【提 要】流媒体作为新的网络应用主流发展极为迅速,本文从应用的角度描述了服务于流媒体的实时传输协议RTP协议,并介绍了RTP协议的典型实现方式。 【关键字】RTP 协议 RTCP 流媒体 1.背景   随着Internet的迅速增长和普及,人们对网络多媒体信息的需求也越来越大,为了适应网络多媒体化的发展趋势,

SSM通过URL实现单值和多值传参的两种方式_pathvariable传多个参数_叼着奶嘴上太空的博客-程序员信息网

这里主要有@PathVariable和@RequestParam注解两种传参方式本文的单值传参是根据查询功能写的,多值传参是根据更新功能写的,这里的更新功能中含有复选框的级联操作,需要的小伙伴可以去这里(复选框的级联操作)看看喔~~如果本文对您有帮助的话请点个小心心,谢谢撒!!![email protected]注解传参:接收请求路径中占位符的值单值传递:前端:function queryById(hosrId) { $.ajax({ type: "GET"

Element-UI使用基本介绍_宋军涛的博客-程序员信息网_elelementui

Element-Ul是饿了么前端团队推出的一款基于Vue.js 2.0的桌面端UI框架,手机端有对应框架是Mint UI。一般组件使用element-ui的基本使用还是十分简单的,首先根据文档的步骤进行安装,导入需要的样式即可,这一块直接看文档就已经说明的很清楚了:文档链接。接着,你只需要找到你想要的样式,然后点开详细代码,复制到你的.vue文件里那么就可以了,例如直接复制官方提供的radio组件(链接)的第一个代码:&lt;template&gt; &lt;div&gt; ...

如何使用SQL Server R绘制SQL Server 2017图形数据库_culuo4781的博客-程序员信息网

A few years ago, one common business case I came across in my professional career that required modelling of data into a many-to-many entity relationship type was the representation of a consultan...

Java的泛型及实现_一棵树~的博客-程序员信息网_reified generic

目录:1、泛型基础1.1、c++模板和java泛型的异同1.2、java泛型的好处2、类型擦除3、泛型的实现原理3.1、保证类型安全3.2、实现自动类型转换4、泛型中的继承关系5、泛型使用中的注意点5.1、运行时类型查询5.2、异常中使用泛型的问题5.3、不允许创建泛型类数组5.3、泛型类中的静态方法和静态变量5.3、类型擦除后的冲突参考...

推荐文章

热门文章

相关标签