Python: 使用tf-idf-cosine算法来寻找文档相似性。

112

我正在跟随一个在第一部分第二部分提供的教程。不幸的是,作者没有时间完成最后一节内容,该部分涉及使用余弦相似度实际找到两个文档之间的距离。我按照文章中的示例结合stackoverflow上的以下链接进行了操作,其中包括上述链接中提到的代码(只是为了使生活更加轻松)。

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from nltk.corpus import stopwords
import numpy as np
import numpy.linalg as LA

train_set = ["The sky is blue.", "The sun is bright."]  # Documents
test_set = ["The sun in the sky is bright."]  # Query
stopWords = stopwords.words('english')

vectorizer = CountVectorizer(stop_words = stopWords)
#print vectorizer
transformer = TfidfTransformer()
#print transformer

trainVectorizerArray = vectorizer.fit_transform(train_set).toarray()
testVectorizerArray = vectorizer.transform(test_set).toarray()
print 'Fit Vectorizer to train set', trainVectorizerArray
print 'Transform Vectorizer to test set', testVectorizerArray

transformer.fit(trainVectorizerArray)
print
print transformer.transform(trainVectorizerArray).toarray()

transformer.fit(testVectorizerArray)
print 
tfidf = transformer.transform(testVectorizerArray)
print tfidf.todense()

通过上述代码,我得到了以下矩阵

Fit Vectorizer to train set [[1 0 1 0]
 [0 1 0 1]]
Transform Vectorizer to test set [[0 1 1 1]]

[[ 0.70710678  0.          0.70710678  0.        ]
 [ 0.          0.70710678  0.          0.70710678]]

[[ 0.          0.57735027  0.57735027  0.57735027]]

我不确定如何使用此输出来计算余弦相似度,我知道如何实现对于两个长度相似的向量进行余弦相似度,但这里我不确定如何识别这两个向量。


3
对于trainVectorizerArray中的每个向量,您需要找到与testVectorizerArray中的向量的余弦相似度。 - excray
@excray 谢谢,有了你的帮助,我成功地解决了问题,我应该把答案放上去吗? - add-semi-colons
@excray 但我有一个小问题,实际上 tf*idf 计算对此没有用处,因为我并没有使用矩阵中显示的最终结果。 - add-semi-colons
5
以下是你所引用的教程的第三部分,它详细回答了你的问题。http://pyevolve.sourceforge.net/wordpress/?p=2497 - Clément Renaud
@ClémentRenaud,我按照您提供的链接进行了操作,但由于我的文档较大,它开始抛出MemoryError错误。我们该如何处理? - ashim888
6个回答

197

首先,如果您想提取计数特征并应用TF-IDF标准化和逐行欧几里得标准化,您可以使用TfidfVectorizer一次完成:

>>> from sklearn.feature_extraction.text import TfidfVectorizer
>>> from sklearn.datasets import fetch_20newsgroups
>>> twenty = fetch_20newsgroups()

>>> tfidf = TfidfVectorizer().fit_transform(twenty.data)
>>> tfidf
<11314x130088 sparse matrix of type '<type 'numpy.float64'>'
    with 1787553 stored elements in Compressed Sparse Row format>

现在,要找到一个文档(例如数据集中的第一个文档)与其他所有文档的余弦距离,您只需要计算第一个向量与所有其他向量的点积,因为tfidf向量已经进行了行归一化。
如Chris Clark在评论和这里所解释的那样,余弦相似性不考虑向量的大小。行归一化的向量大小为1,因此线性核足以计算相似度值。
scipy稀疏矩阵API有点奇怪(不像密集的N维numpy数组那样灵活)。要获取第一个向量,您需要按行切片矩阵,以获取仅包含单个行的子矩阵:
>>> tfidf[0:1]
<1x130088 sparse matrix of type '<type 'numpy.float64'>'
    with 89 stored elements in Compressed Sparse Row format>

scikit-learn已经提供了成对度量(在机器学习术语中称为核函数),适用于向量集合的密集和稀疏表示。在这种情况下,我们需要一个点积,也就是线性核函数:

>>> from sklearn.metrics.pairwise import linear_kernel
>>> cosine_similarities = linear_kernel(tfidf[0:1], tfidf).flatten()
>>> cosine_similarities
array([ 1.        ,  0.04405952,  0.11016969, ...,  0.04433602,
    0.04457106,  0.03293218])

因此,要查找前5个相关文档,我们可以使用argsort和一些负数组切片(最相关的文档具有最高的余弦相似度值,因此在排序后的索引数组末尾):
>>> related_docs_indices = cosine_similarities.argsort()[:-5:-1]
>>> related_docs_indices
array([    0,   958, 10576,  3277])
>>> cosine_similarities[related_docs_indices]
array([ 1.        ,  0.54967926,  0.32902194,  0.2825788 ])

第一个结果是一个健全性检查:我们发现查询文档与余弦相似度得分为1的最相似文档具有以下文本内容:
>>> print twenty.data[0]
From: lerxst@wam.umd.edu (where's my thing)
Subject: WHAT car is this!?
Nntp-Posting-Host: rac3.wam.umd.edu
Organization: University of Maryland, College Park
Lines: 15

 I was wondering if anyone out there could enlighten me on this car I saw
the other day. It was a 2-door sports car, looked to be from the late 60s/
early 70s. It was called a Bricklin. The doors were really small. In addition,
the front bumper was separate from the rest of the body. This is
all I know. If anyone can tellme a model name, engine specs, years
of production, where this car is made, history, or whatever info you
have on this funky looking car, please e-mail.

Thanks,
- IL
   ---- brought to you by your neighborhood Lerxst ----

第二个最相似的文档是回复原始消息并引用了许多共同单词的消息。
>>> print twenty.data[958]
From: rseymour@reed.edu (Robert Seymour)
Subject: Re: WHAT car is this!?
Article-I.D.: reed.1993Apr21.032905.29286
Reply-To: rseymour@reed.edu
Organization: Reed College, Portland, OR
Lines: 26

In article <1993Apr20.174246.14375@wam.umd.edu> lerxst@wam.umd.edu (where's my
thing) writes:
>
>  I was wondering if anyone out there could enlighten me on this car I saw
> the other day. It was a 2-door sports car, looked to be from the late 60s/
> early 70s. It was called a Bricklin. The doors were really small. In
addition,
> the front bumper was separate from the rest of the body. This is
> all I know. If anyone can tellme a model name, engine specs, years
> of production, where this car is made, history, or whatever info you
> have on this funky looking car, please e-mail.

Bricklins were manufactured in the 70s with engines from Ford. They are rather
odd looking with the encased front bumper. There aren't a lot of them around,
but Hemmings (Motor News) ususally has ten or so listed. Basically, they are a
performance Ford with new styling slapped on top.

>    ---- brought to you by your neighborhood Lerxst ----

Rush fan?

--
Robert Seymour              rseymour@reed.edu
Physics and Philosophy, Reed College    (NeXTmail accepted)
Artificial Life Project         Reed College
Reed Solar Energy Project (SolTrain)    Portland, OR

一个后续问题:如果我有大量的文档,步骤2中的linear_kernel函数可能会成为性能瓶颈,因为它与行数成线性关系。您对如何将其减少到亚线性有什么想法吗? - Shuo
您可以使用 Elastic Search 和 Solr 的“更多类似于此”的查询,这些查询应该能够提供近似答案,并具有次线性可扩展性配置文件。 - ogrisel
9
这会给你每个文档与其他所有文档之间的余弦相似度,而不仅仅是第一个文档: cosine_similarities = linear_kernel(tfidf, tfidf) - ionox0
2
是的,这将为您提供一种成对相似性的方阵。 - ogrisel
11
如果其他人和我一样好奇的话,这里的linear_kernel在这种情况下等同于cosine_similarity,因为TfidfVectorizer生成的向量是归一化的。请参阅文档中的注释:http://scikit-learn.org/stable/modules/metrics.html#cosine-similarity - Chris Clark
显示剩余2条评论

25

在@excray的评论的帮助下,我成功地找到了答案。我们需要做的实际上是编写一个简单的for循环,以迭代表示训练数据和测试数据的两个数组。

首先实现一个简单的lambda函数来保存余弦计算公式:

cosine_function = lambda a, b : round(np.inner(a, b)/(LA.norm(a)*LA.norm(b)), 3)

然后只需编写一个简单的for循环来遍历to向量,逻辑是对于trainVectorizerArray中的每个向量,您必须找到与testVectorizerArray中的向量的余弦相似度。

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from nltk.corpus import stopwords
import numpy as np
import numpy.linalg as LA

train_set = ["The sky is blue.", "The sun is bright."] #Documents
test_set = ["The sun in the sky is bright."] #Query
stopWords = stopwords.words('english')

vectorizer = CountVectorizer(stop_words = stopWords)
#print vectorizer
transformer = TfidfTransformer()
#print transformer

trainVectorizerArray = vectorizer.fit_transform(train_set).toarray()
testVectorizerArray = vectorizer.transform(test_set).toarray()
print 'Fit Vectorizer to train set', trainVectorizerArray
print 'Transform Vectorizer to test set', testVectorizerArray
cx = lambda a, b : round(np.inner(a, b)/(LA.norm(a)*LA.norm(b)), 3)

for vector in trainVectorizerArray:
    print vector
    for testV in testVectorizerArray:
        print testV
        cosine = cx(vector, testV)
        print cosine

transformer.fit(trainVectorizerArray)
print
print transformer.transform(trainVectorizerArray).toarray()

transformer.fit(testVectorizerArray)
print 
tfidf = transformer.transform(testVectorizerArray)
print tfidf.todense()

以下是输出结果:

Fit Vectorizer to train set [[1 0 1 0]
 [0 1 0 1]]
Transform Vectorizer to test set [[0 1 1 1]]
[1 0 1 0]
[0 1 1 1]
0.408
[0 1 0 1]
[0 1 1 1]
0.816

[[ 0.70710678  0.          0.70710678  0.        ]
 [ 0.          0.70710678  0.          0.70710678]]

[[ 0.          0.57735027  0.57735027  0.57735027]]

1
不错,我也从头开始学习,你的问题和答案最容易理解。我认为你可以使用np.corrcoef()代替你自己编写的方法。 - wbg
transformer.fit操作和tfidf.todense()的目的是什么? 你从循环中获得了相似性值,然后继续进行tfidf?你计算的余弦值在哪里使用?你的例子很混乱。 - minerals
如果您不介意的话,可以解释一下cosine返回的是什么。在您的示例中,您得到了0.408和0.816,这些值是什么? - buydadip

21

我知道这是一个旧帖子,但我尝试了http://scikit-learn.sourceforge.net/stable/包。这是我的代码来找到余弦相似度。问题是如何使用这个包计算余弦相似度,这里是我的代码。

from sklearn.feature_extraction.text import CountVectorizer
from sklearn.metrics.pairwise import cosine_similarity
from sklearn.feature_extraction.text import TfidfVectorizer

f = open("/root/Myfolder/scoringDocuments/doc1")
doc1 = str.decode(f.read(), "UTF-8", "ignore")
f = open("/root/Myfolder/scoringDocuments/doc2")
doc2 = str.decode(f.read(), "UTF-8", "ignore")
f = open("/root/Myfolder/scoringDocuments/doc3")
doc3 = str.decode(f.read(), "UTF-8", "ignore")

train_set = ["president of India",doc1, doc2, doc3]

tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix_train = tfidf_vectorizer.fit_transform(train_set)  #finds the tfidf score with normalization
print "cosine scores ==> ",cosine_similarity(tfidf_matrix_train[0:1], tfidf_matrix_train)  #here the first element of tfidf_matrix_train is matched with other three elements

假设查询是train_set的第一个元素,doc1、doc2和doc3是我想要使用余弦相似度排名的文档,那么我可以使用以下代码。

此外,问题中提供的教程非常有用。以下是所有部分:part-Ipart-IIpart-III

输出将如下所示:

[[ 1.          0.07102631  0.02731343  0.06348799]]

这里的1表示查询与自身匹配,而其他三个数字则是将查询与相应文档进行匹配的得分。


1
余弦相似度(cosine_similarity)(tfidf_matrix_train[0:1],tfidf_matrix_train)如果将其更改为超过数千个,我们该如何处理? - ashim888
1
如何处理“ValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2”错误? - Pyd

17
我给你提供另一个由我撰写的教程,它回答了您的问题,同时也解释了我们为什么要做一些事情。我还尝试使其简洁明了。
所以您有一个list_of_documents,它只是一个字符串数组,还有另一个document,它只是一个字符串。您需要从list_of_documents中找到与document最相似的文档。
让我们将它们合并在一起:documents = list_of_documents + [document] 让我们从依赖关系开始。它将变得清晰,为什么我们使用每个依赖项。
from nltk.corpus import stopwords
import string
from nltk.tokenize import wordpunct_tokenize as tokenize
from nltk.stem.porter import PorterStemmer
from sklearn.feature_extraction.text import TfidfVectorizer
from scipy.spatial.distance import cosine

其中一种方法是使用词袋模型,我们将文档中的每个单词独立对待,并将它们全部放在一个大袋子里。从某种程度上来说,这会丢失很多信息(例如单词之间的联系),但从另一个角度来看,它使模型更简单。

在英语和其他任何人类语言中,有很多“无用”的单词,如'a'、'the'、'in',它们非常常见,因此没有太多意义。它们被称为停用词,删除它们是一个好主意。另一个可以注意到的事情是,像'analyze'、'analyzer'、'analysis'这样的单词非常相似。它们有一个共同的词根,都可以转换成一个单词。这个过程被称为词干提取,存在不同的词干提取器,它们在速度、侵略性等方面有所不同。因此,我们将每个文档转换为去除停用词的单词词干列表。同时,我们舍弃所有的标点符号。

porter = PorterStemmer()
stop_words = set(stopwords.words('english'))

modified_arr = [[porter.stem(i.lower()) for i in tokenize(d.translate(None, string.punctuation)) if i.lower() not in stop_words] for d in documents]

所以这个词袋会如何帮助我们呢?想象有3个袋子:[a, b, c][a, c, a][b, c, d]。我们可以将它们转换为基向量 vectors in the basis [a, b, c, d]。因此,我们最终得到向量:[1, 1, 1, 0][2, 0, 1, 0][0, 1, 1, 1]。与我们的文档类似(只是向量会更长)。现在我们看到,我们删除了很多单词,并将其他单词词干化,以减少向量的维度。这里有一个有趣的观察结果。较长的文档将比较短的文档具有更多正元素,这就是为什么规范化向量很好的原因。这被称为术语频率TF,人们还使用了关于单词在其他文档中使用频率的其他信息 - 逆文档频率IDF。结合起来,我们有一个度量值TF-IDF which have a couple of flavors。这可以通过sklearn中的一条线实现 :-)
modified_doc = [' '.join(i) for i in modified_arr] # this is only to convert our list of lists to list of strings that vectorizer uses.
tf_idf = TfidfVectorizer().fit_transform(modified_doc)

实际上,向量化器 允许做很多事情,例如去除停用词和转换为小写。我将它们分别作为一个步骤完成,因为sklearn没有非英语停用词,但nltk有。

所以我们已经计算出了所有的向量。最后一步是找到哪个向量与最后一个向量最相似。有多种方法可以实现这一点,其中之一是欧几里得距离,但由于在这里讨论的原因,它并不是很好。另一种方法是余弦相似度。我们迭代所有文档,并计算文档与最后一个文档之间的余弦相似度:

l = len(documents) - 1
for i in xrange(l):
    minimum = (1, None)
    minimum = min((cosine(tf_idf[i].todense(), tf_idf[l + 1].todense()), i), minimum)
print minimum

现在,最小值将包含关于最佳文档及其分数的信息。

3
这不是 OP 所要求的内容:搜索给定查询条件下的最佳文档,而不是在一个语料库中找到“最佳文档”。请不要这样做,像我这样的人会浪费时间尝试使用您的示例来完成任务,然后被卷入矩阵调整的疯狂中。 - minerals
那么它有什么不同之处呢?这个想法完全一样。提取特征,计算查询和文档之间的余弦距离。 - Salvador Dali
你正在计算相同形状的矩阵,尝试一个不同的例子,其中查询矩阵具有不同的大小,操作员的训练集和测试集。我无法修改您的代码使其正常工作。 - minerals
@SalvadorDali 正如所指出的,上述回答涉及到不同的问题:您假设查询和文档是同一语料库的一部分,这是错误的。这会导致使用从相同语料库(具有相同维度)派生的向量的距离的错误方法,这通常不是必要的。如果查询和文档属于不同的语料库,则它们产生的向量可能不在同一空间中,并且像您上面所做的那样计算距离将毫无意义(它们甚至不具有相同的维数)。 - gented
请注意,这里的“cosine”是指余弦距离,而不是余弦相似度。 - bfontaine

14

这应该能帮助您。

from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.metrics.pairwise import cosine_similarity  

tfidf_vectorizer = TfidfVectorizer()
tfidf_matrix = tfidf_vectorizer.fit_transform(train_set)
print tfidf_matrix
cosine = cosine_similarity(tfidf_matrix[length-1], tfidf_matrix)
print cosine

输出结果将是:

[[ 0.34949812  0.81649658  1.        ]]

14
如何获得长度? - gogasca

4

下面是一个函数,它可以将您的测试数据与使用训练数据拟合的Tf-Idf转换器进行比较。优点在于您可以快速进行透视或分组以查找n个最接近的元素,并且计算是基于矩阵的。

def create_tokenizer_score(new_series, train_series, tokenizer):
    """
    return the tf idf score of each possible pairs of documents
    Args:
        new_series (pd.Series): new data (To compare against train data)
        train_series (pd.Series): train data (To fit the tf-idf transformer)
    Returns:
        pd.DataFrame
    """

    train_tfidf = tokenizer.fit_transform(train_series)
    new_tfidf = tokenizer.transform(new_series)
    X = pd.DataFrame(cosine_similarity(new_tfidf, train_tfidf), columns=train_series.index)
    X['ix_new'] = new_series.index
    score = pd.melt(
        X,
        id_vars='ix_new',
        var_name='ix_train',
        value_name='score'
    )
    return score

train_set = pd.Series(["The sky is blue.", "The sun is bright."])
test_set = pd.Series(["The sun in the sky is bright."])
tokenizer = TfidfVectorizer() # initiate here your own tokenizer (TfidfVectorizer, CountVectorizer, with stopwords...)
score = create_tokenizer_score(train_series=train_set, new_series=test_set, tokenizer=tokenizer)
score

   ix_new   ix_train    score
0   0       0       0.617034
1   0       1       0.862012

1
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.melt.html 解释了 pd.melt 的作用。 - Golden Lion
对于np.arange(0,len(score))中的索引:value=score.loc[index,'score'] - Golden Lion

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