给定2个句子字符串,计算余弦相似度


72

Python:tf-idf-cosine:查找文档相似度,可以使用tf-idf余弦计算文档相似度。如果不导入外部库,是否有任何方法可以计算2个字符串之间的余弦相似度?

s1 = "This is a foo bar sentence ."
s2 = "This sentence is similar to a foo bar sentence ."
s3 = "What is this string ? Totally not related to the other two lines ."

cosine_sim(s1, s2) # Should give high cosine similarity
cosine_sim(s1, s3) # Shouldn't give high cosine similarity value
cosine_sim(s2, s3) # Shouldn't give high cosine similarity value

4
我没有答案,但是如果您想要有意义的结果,像word2vec(code.google.com/p/word2vec)之类的东西可能是一个好的开始。
static_rtti

3
@static_rtti word2vec与余弦相似度无关。那就是嵌入。在这里,他给出了两个要用来计算余弦相似度的字符串。
米通保罗

如果有人在寻找语义相似性gensim可能会有所帮助。
mie.ppa

Answers:


162

一个简单的纯Python实现是:

import math
import re
from collections import Counter

WORD = re.compile(r"\w+")


def get_cosine(vec1, vec2):
    intersection = set(vec1.keys()) & set(vec2.keys())
    numerator = sum([vec1[x] * vec2[x] for x in intersection])

    sum1 = sum([vec1[x] ** 2 for x in list(vec1.keys())])
    sum2 = sum([vec2[x] ** 2 for x in list(vec2.keys())])
    denominator = math.sqrt(sum1) * math.sqrt(sum2)

    if not denominator:
        return 0.0
    else:
        return float(numerator) / denominator


def text_to_vector(text):
    words = WORD.findall(text)
    return Counter(words)


text1 = "This is a foo bar sentence ."
text2 = "This sentence is similar to a foo bar sentence ."

vector1 = text_to_vector(text1)
vector2 = text_to_vector(text2)

cosine = get_cosine(vector1, vector2)

print("Cosine:", cosine)

印刷品:

Cosine: 0.861640436855

这里所用的余弦公式描述这里

这不包括通过tf-idf对单词进行加权,但是为了使用tf-idf,您需要具有一个相当大的语料库才能从中估计tfidf的权重。

您还可以通过使用更复杂的方法从一段文本中提取单词,对其进行词干或词义化等来进一步开发它。


1
“猫吃老鼠”和“啮齿动物经常被猫吃掉”怎么样?你的代码错误地返回0
mbatchkarov

63
当然,SO问题不是最终解决建模句子语义相似性问题的地方。问题是关于测量两个文本位之间的(表面)相似性,这就是代码的作用。
vpekar 2013年

11
该代码正确返回了0,因为它可以测量两个文本的表面相似度,而不能测量其含义。
vpekar 2013年

我同意你的第一点,不同意第二点。SO不会进行长期的理论性科学讨论,这就是为什么我不谈论技术方面的原因。尽管您的答案是正确的,但问题显然在于表面相似性。请再看一下问题中的例句。
mbatchkarov 2013年

8
您专门询问了余弦。我专门回答了这个问题。余弦被实现为“现实地”。如果您要说“如何比余弦测量相似度更好”,那么这是一个不同的问题。
vpekar 2013年

51

简短的答案是“不,不可能以原则上可行的方式做到这一点”。这是自然语言处理研究中尚未解决的问题,也恰好是我的博士论文的主题。我将简要概述一下我们的位置,并为您提供一些出版物:

词义

这里最重要的假设是有可能获得代表每个单词的向量在疑问句中。通常选择此向量来捕获单词可能出现的上下文。例如,如果我们仅考虑“ eat”,“ red”和“ fluffy”这三个上下文,则单词“ cat”可能表示为[98,1 (87),因为如果您要阅读很长的一段文字(按照今天的标准,几十亿个单词并不少见),那么“猫”一词经常出现在“蓬松”和“吃”的上下文中,但在“红色”的背景下并不常见。同样,“ dog”可以表示为[87,2,34],“ umbrella”可以表示为[1,13,0]。将这些向量成像为3D空间中的点,“猫”显然比“雨伞”更接近“狗”,因此“猫”更接近“狗”

工作的这条线,因为90年代初(例如已被调查这个由Greffenstette工作),并已取得了一些效果出奇的好。例如,这是我最近通过计算机阅读维基百科建立的同义词库中的一些随机条目:

theory -> analysis, concept, approach, idea, method
voice -> vocal, tone, sound, melody, singing
james -> william, john, thomas, robert, george, charles

这些类似单词的列表完全是在没有人工干预的情况下获得的-您输入文字并在几个小时后返回。

短语问题

您可能会问,为什么我们对更长的短语不做同样的事情,例如“狐狸爱吃水果”。这是因为我们没有足够的文字。为了使我们能够可靠地确定X类似于什么,我们需要查看在上下文中使用X的许多示例。当X是单个单词(例如“ voice”)时,这并不难。但是,随着X变长,找到X的自然出现的机会呈指数增长。为了进行比较,尽管Google句子是完全有效的英语句子,但Google大约有1B页包含“ fox”一词,而不是包含“ ginger foxes love fruit”的单个页面,我们都知道这是什么意思。

组成

为了解决数据稀疏性的问题,我们想要执行合成,即为单词提取矢量,这些单词很容易从真实文本中获取,并以一种能够捕捉其含义的方式将它们放在一起。坏消息是,到目前为止,没有人能够做到这一点。

最简单,最明显的方法是将各个单词向量相加或相乘。这会导致不良后果,即“猫追狗”和“猫追狗”对您的系统意味着相同。同样,如果您要相乘,则必须格外小心,否则每个句子最终都会以[0,0,0,...,0]表示,这很不利。

进一步阅读

到目前为止,我不会讨论更复杂的合成方法。我建议您阅读Katrin Erk的“单词含义和短语含义的向量空间模型:调查”。这是一个非常好的高级调查,可以帮助您入门。不幸的是,它不能在发行者的网站上免费获得,请直接向作者发送电子邮件以获取副本。在该论文中,您将找到许多更具体方法的参考。Mitchel和Lapata(2008)以及Baroni和Zamparelli(2010)更容易理解。


@vpekar发表评论后编辑:此答案的底线是强调以下事实:尽管存在幼稚的方法(例如加法,乘法,表面相似性等),但它们从根本上来说是有缺陷的,并且通常不应期望从中获得出色的性能他们。


出于好奇,您使用什么方法来构建同义词库?
JesseBuesking

3
这是由Byblo构建的分布式同义词库。在这个特定的实例中,每个标记都具有在整个Wikipedia中围绕它的5个单词的窗口中出现的其他标记的特征,并且基于这些特征计算相似度。我们建立了其他叙词表,其中特征是目标词与语法有关系的其他词。这通常效果更好,但至少需要对语料库进行部分解析,这需要很长时间。
mbatchkarov

为什么我们不能使用RNN处理合成?这样,我们将考虑单词顺序及其各自的含义。
Oleg Afanasyev,

你当然可以。该答案现在已有5年历史了。当时RNN刚刚开始变得越来越大,而且并不是我真正关注的
话题

@OlegAfanasyev-您的意思是说带有CNN或RNN的自动编码器?我要实施,如果照常需要什么,您能提供任何指导吗?
user1531248

3

我有类似的解决方案,但可能对熊猫有用

import math
import re
from collections import Counter
import pandas as pd

WORD = re.compile(r"\w+")


def get_cosine(vec1, vec2):
    intersection = set(vec1.keys()) & set(vec2.keys())
    numerator = sum([vec1[x] * vec2[x] for x in intersection])

    sum1 = sum([vec1[x] ** 2 for x in list(vec1.keys())])
    sum2 = sum([vec2[x] ** 2 for x in list(vec2.keys())])
    denominator = math.sqrt(sum1) * math.sqrt(sum2)

    if not denominator:
        return 0.0
    else:
        return float(numerator) / denominator


def text_to_vector(text):
    words = WORD.findall(text)
    return Counter(words)

df=pd.read_csv('/content/drive/article.csv')
df['vector1']=df['headline'].apply(lambda x: text_to_vector(x)) 
df['vector2']=df['snippet'].apply(lambda x: text_to_vector(x)) 
df['simscore']=df.apply(lambda x: get_cosine(x['vector1'],x['vector2']),axis=1)

1

感谢@vpekar的实施。这很有帮助。我只是发现在计算余弦相似度时,它错过了tf-idf权重。计数器(单词)返回一个字典,其中包含单词列表及其出现。

cos(q,d)= sim(q,d)=(q·d)/(| q || d |)=(sum(qi,di)/(sqrt(sum(qi2)))*(sqrt( sum(vi2))),其中i = 1至v)

  • qi是查询中第i项的tf-idf权重。
  • di是tf-idf
  • 文档中术语i的权重。| q | 和| d | 是q和d的长度。
  • 这是q和d的余弦相似度。。。。。。或等效地,q和d之间的角度的余弦值。

请随时在这里查看我的代码。但是首先,您必须下载anaconda软件包。它将在Windows中自动为您设置python路径。在Eclipse中添加此python解释器。


1

好吧,如果您知道像Glove / Word2Vec / Numberbatch这样的词嵌入,您的工作就完成了一半。如果不能,请允许我解释如何解决。将每个句子转换为单词标记,并将每个标记表示为高维向量(使用预先训练的单词嵌入,或者甚至可以自己训练它们!)。因此,现在您只是不捕获它们的表面相似性,而是提取构成整个句子的每个单词的含义。在此之后,计算它们的余弦相似度并设置好。


假设第一个句子和100中的第6个单词为嵌入大小,第二个句子中的第4个单词为100D,则将句子1(6 * 100)和句子2(4 * 100)的每个单词嵌入后的处理。我可以做单词向量的总和或指导我(如果有)吗?
user1531248

1

尝试这个。从https://conceptnet.s3.amazonaws.com/downloads/2017/numberbatch/numberbatch-en-17.06.txt.gz下载文件'numberbatch-en-17.06.txt'并解压缩。函数“ get_sentence_vector”使用单词向量的简单总和。但是,可以通过使用权重与每个单词的Tf-Idf成正比的加权和来改善此效果。

import math
import numpy as np

std_embeddings_index = {}
with open('path/to/numberbatch-en-17.06.txt') as f:
    for line in f:
        values = line.split(' ')
        word = values[0]
        embedding = np.asarray(values[1:], dtype='float32')
        std_embeddings_index[word] = embedding

def cosineValue(v1,v2):
    "compute cosine similarity of v1 to v2: (v1 dot v2)/{||v1||*||v2||)"
    sumxx, sumxy, sumyy = 0, 0, 0
    for i in range(len(v1)):
        x = v1[i]; y = v2[i]
        sumxx += x*x
        sumyy += y*y
        sumxy += x*y
    return sumxy/math.sqrt(sumxx*sumyy)


def get_sentence_vector(sentence, std_embeddings_index = std_embeddings_index ):
    sent_vector = 0
    for word in sentence.lower().split():
        if word not in std_embeddings_index :
            word_vector = np.array(np.random.uniform(-1.0, 1.0, 300))
            std_embeddings_index[word] = word_vector
        else:
            word_vector = std_embeddings_index[word]
        sent_vector = sent_vector + word_vector

    return sent_vector

def cosine_sim(sent1, sent2):
    return cosineValue(get_sentence_vector(sent1), get_sentence_vector(sent2))

我确实跑了给定的句子,发现以下结果

s1 = "This is a foo bar sentence ."
s2 = "This sentence is similar to a foo bar sentence ."
s3 = "What is this string ? Totally not related to the other two lines ."

print cosine_sim(s1, s2) # Should give high cosine similarity
print cosine_sim(s1, s3) # Shouldn't give high cosine similarity value
print cosine_sim(s2, s3) # Shouldn't give high cosine similarity value

0.9851735249068168
0.6570885718962608
0.6589335425458225
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.