给定两个句子字符串,计算它们之间的余弦相似度。

86

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

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(https://code.google.com/p/word2vec/)的东西可能是一个不错的起点。 - static_rtti
3
word2vec与余弦相似度无关,它涉及到嵌入。这里他提供了两个字符串,想要计算它们之间的余弦相似度。 - mithunpaul
如果有人正在寻找语义相似性,gensim 可以提供帮助。 - mie.ppa
请参阅 https://dev59.com/58Tra4cB1Zd3GeqPySOn。 - alvas
8个回答

186
一个简单的纯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,您需要有一个足够大的语料库来估计tf-idf权重。
您还可以进一步开发它,通过使用更复杂的方法从文本中提取单词,将其词干化或词形还原等。

2
“Felines feed on mice”和“Rodents are often eaten by cats”这两句话怎么样?你的代码返回了0,有误。 - mbatchkarov
70
当然,SO问题并不是解决建模句子语义相似性问题的地方。该问题涉及测量两个文本片段之间的(表面)相似性,这就是代码所做的事情。 - vpekar
11
代码正确地返回了0,因为它衡量的是两个文本之间的表面相似性,而不是它们的含义。 - vpekar
@2er0 你是不是想问如何在衡量两个句子相似度时使用外部语义知识? - vpekar
11
你特别询问了余弦函数,我对此进行了具体回答。余弦函数的实现尽可能地“真实”。如果你的意思是说,“如何比余弦函数更好地度量相似性”,那么这就是一个不同的问题。 - vpekar
显示剩余5条评论

52
短答案是“不,无法以原则性的方式解决这个问题,即使稍微好一点也不行”。这是自然语言处理研究中尚未解决的问题,也恰好是我博士论文的主题。我将简要总结我们的进展,并指向一些出版物:
单词的意义
这里最重要的假设是可以获得一个向量来表示句子中每个单词。该向量通常被选为捕捉单词可能出现的上下文。例如,如果我们只考虑“吃”、“红色”和“毛茸茸”三个上下文,那么单词“猫”可能被表示为[98, 1, 87],因为如果你阅读一篇非常长的文本(按今天的标准,几十亿个单词并不罕见),单词“猫”会在“毛茸茸”和“吃”的上下文中经常出现,但在“红色”的上下文中出现的次数不那么多。同样地,“狗”可能被表示为[87,2,34],“雨伞”可能是[1,13,0]。把这些向量想象成3D空间中的点,“猫”显然比“雨伞”更接近“狗”,因此“猫”也意味着与“狗”更相似,而不是与“雨伞”。
这项工作自90年代初开始调查(例如Greffenstette的this研究),并取得了一些惊人的好成果。例如,以下是我最近建立的一个同义词词库中的几个随机条目,是通过让我的计算机阅读维基百科而生成的:
theory -> analysis, concept, approach, idea, method
voice -> vocal, tone, sound, melody, singing
james -> william, john, thomas, robert, george, charles

这些相似单词列表完全不需要人工干预-您输入文本,几个小时后回来即可。
短语的问题
您可能会问为什么我们不对更长的短语进行同样的操作,例如“ginger foxes love fruit”。这是因为我们没有足够的文本。为了能够可靠地确定X与何物相似,我们需要看到许多实例中使用X的上下文。当X是像“voice”这样的单词时,这并不太困难。然而,随着X变得更长,找到自然出现的X的机会呈指数级减少。举个例子,Google有大约10亿个包含“fox”一词的页面,但没有一个页面包含“ginger foxes love fruit”,尽管它是一个完全有效的英语句子,我们都理解它的意思。
组合
为了解决数据稀疏性问题,我们想要执行组合,即获取单词向量(从真实文本中很容易获得),并以捕捉其含义的方式将它们放在一起。坏消息是迄今为止没有人能够做到这一点。

最简单和最明显的方法是将每个单词向量相加或相乘。这会导致一个不良的副作用,即"猫追逐狗"和"狗追逐猫"在你的系统中意味着相同的含义。此外,如果你在做乘法,你必须特别小心,否则每个句子都将以[0,0,0,...,0]表示,这将使其失去意义。

进一步阅读

我不会讨论到目前为止已经提出的更复杂的组合方法。我建议你阅读Katrin Erk的 "Vector space models of word meaning and phrase meaning: a survey"。这是一个非常好的高层次调查,可以让你入门。不幸的是,在出版商的网站上不能免费获得它,请直接联系作者获取一份副本。在那篇论文中,你会找到许多更具体的方法的参考资料。其中比较易懂的是Mitchel和Lapata(2008)Baroni和Zamparelli(2010)


这个答案的要点是强调这样一个事实,即虽然存在天真的方法(例如加法、乘法、表面相似度等),但这些方法基本上是有缺陷的,一般不应该期望它们具有很好的性能。

出于好奇,您用什么方法构建了这个词库? - JesseBuesking
3
这是一个分布式词表,使用Byblo构建而成。 在这个特定的实例中,每个单词的特征是在整个维基百科中距其5个单词窗口内出现的其他单词,并基于这些特征计算相似度。我们还构建了其他词表,其中特征是目标单词具有语法关系的其他单词。这通常效果更好,但需要对语料库进行至少部分解析,这需要很长时间。 - mbatchkarov
@OlegAfanasyev - 你是指使用CNN或RNN的自编码器吗?我要实现它,如果需要,你能提供任何指导吗? - user1531248
最后2个论文链接现在已失效。 - desertnaut
谷歌约有10亿个包含单词“狐狸”的网页,但没有一个包含“姜色狐狸喜欢水果”的页面 —— 现在有了 :D - Speeeddy
显示剩余2条评论

8

我有类似的解决方案,但可能对Pandas有用。

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)

3
我能给出的最简单的答案包括CounterVectorizer。
假设我们有3段文本。
text_1 = """ """

text_2 = """ """

text_3 = """ """

documents = [text_1, text_2, text_3]

要计算余弦相似度,我们需要每个文档中单词的计数矩阵。
import pandas as pd

# Create the Document Term Matrix
count_vectorizer = CountVectorizer(stop_words='english')
count_vectorizer = CountVectorizer()
sparse_matrix = count_vectorizer.fit_transform(documents)

# OPTIONAL: Convert Sparse Matrix to Pandas Dataframe if you want to see the word frequencies.
doc_term_matrix = sparse_matrix.todense()
df = pd.DataFrame(doc_term_matrix, 
                  columns=count_vectorizer.get_feature_names(), 
                  index=['text_1', 'text_2', 'text_3'])
df

而且,仅仅使用sklearn中的余弦相似度函数就可以完成工作。
from sklearn.metrics.pairwise import cosine_similarity
print(cosine_similarity(df, df))

3
请尝试以下方法。从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

2

好的,如果你了解像Glove / Word2Vec / Numberbatch这样的单词嵌入,你的工作已经完成了一半。如果没有,请让我解释一下如何解决这个问题。 将每个句子转换为单词标记,并将每个标记表示为高维向量(使用预训练的单词嵌入,或者您甚至可以自己训练它们!)。因此,现在您不仅捕获它们的表面相似性,而且提取组成整个句子的每个单词的含义。在此之后,计算它们的余弦相似度即可。


假设第一句话有6个单词,每个单词的嵌入大小为100,第二句话有4个单词,每个单词的嵌入大小也是100。在获取到每个单词的嵌入后,要做什么呢?我可以将单词向量相加吗?如果您有任何建议,请指导我。 - user1531248

1

不使用外部库,您可以尝试使用BLEU或其替代品。您可以参考其标准实现:SACREBLEU


1

感谢@vpekar的实现,它帮了我很大的忙。但我发现在计算余弦相似度时缺少了tf-idf权重。

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

  • qi是查询中词项i的tf-idf权重。
  • di是文档中词项i的tf-idf权重。|q|和|d|分别是查询和文档的长度。
  • 这是q和d之间的余弦相似度......或者等价地说,是q和d之间的夹角的余弦值。

请随意查看我的代码here。但首先您需要下载Anaconda软件包。它将自动在Windows中设置您的Python路径。将此Python解释器添加到Eclipse中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接