Python: tf-idf-cosine: pour trouver la similitude du document

90

Je suivais un tutoriel qui était disponible dans les parties 1 et 2 . Malheureusement, l'auteur n'a pas eu le temps pour la dernière section qui impliquait d'utiliser la similitude cosinus pour trouver réellement la distance entre deux documents. J'ai suivi les exemples de l'article à l'aide du lien suivant de stackoverflow , inclus le code mentionné dans le lien ci-dessus (juste pour vous faciliter la vie)

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()

à la suite du code ci-dessus, j'ai la matrice suivante

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]]

Je ne sais pas comment utiliser cette sortie pour calculer la similitude cosinus, je sais comment implémenter la similitude cosinus par rapport à deux vecteurs de longueur similaire mais ici je ne sais pas comment identifier les deux vecteurs.

ajouter des points-virgules
la source
3
Pour chaque vecteur dans trainVectorizerArray, vous devez trouver la similitude cosinus avec le vecteur dans testVectorizerArray.
excray
@excray Merci, avec votre point utile, je parviens à le comprendre, dois-je mettre la réponse?
ajouter des points-virgules
@excray Mais j'ai une petite question, le calcul actuall tf * idf n'a aucune utilité pour cela, car je n'utilise pas les résultats finaux qui sont affichés dans la matrice.
ajouter des points-virgules
4
Voici la 3ème partie du tutoriel que vous citez qui répond en détail à votre question pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud
@ ClémentRenaud j'ai suivi avec le lien que vous avez fourni mais comme mes documents sont plus volumineux, cela commence à lancer MemoryError Comment pouvons-nous gérer cela?
ashim888

Réponses:

169

Tout d'abord, si vous souhaitez extraire des fonctionnalités de comptage et appliquer la normalisation TF-IDF et la normalisation euclidienne par ligne, vous pouvez le faire en une seule opération avec 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>

Maintenant, pour trouver les distances cosinus d'un document (par exemple le premier du jeu de données) et de tous les autres, il vous suffit de calculer les produits scalaires du premier vecteur avec tous les autres car les vecteurs tfidf sont déjà normalisés en ligne.

Comme l'explique Chris Clark dans les commentaires et ici , la similarité cosinus ne prend pas en compte l'ampleur des vecteurs. Les lignes normalisées ont une magnitude de 1 et le noyau linéaire est donc suffisant pour calculer les valeurs de similarité.

L'API de matrice clairsemée scipy est un peu bizarre (pas aussi flexible que les tableaux numpy denses à N dimensions). Pour obtenir le premier vecteur, vous devez découper la matrice par ligne pour obtenir une sous-matrice avec une seule ligne:

>>> tfidf[0:1]
<1x130088 sparse matrix of type '<type 'numpy.float64'>'
    with 89 stored elements in Compressed Sparse Row format>

scikit-learn fournit déjà des métriques par paires (alias noyaux dans le langage de l'apprentissage automatique) qui fonctionnent à la fois pour les représentations denses et éparses des collections vectorielles. Dans ce cas, nous avons besoin d'un produit scalaire également connu sous le nom de noyau linéaire:

>>> 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])

Par conséquent, pour trouver les 5 principaux documents connexes, nous pouvons utiliser argsortun découpage de tableau négatif (la plupart des documents associés ont les valeurs de similitude cosinus les plus élevées, donc à la fin du tableau d'indices triés):

>>> 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 ])

Le premier résultat est un contrôle de cohérence: nous trouvons le document de requête comme le document le plus similaire avec un score de similarité cosinus de 1 qui contient le texte suivant:

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

Le deuxième document le plus similaire est une réponse qui cite le message d'origine et contient donc de nombreux mots communs:

>>> 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
ogrisel
la source
Une question de suivi: si j'ai un très grand nombre de documents, la fonction linear_kernel à l'étape 2 peut être le goulot d'étranglement des performances, car elle est linéaire au nombre de lignes. Des réflexions sur la façon de le réduire à sous-linéaire?
Shuo
Vous pouvez utiliser les requêtes «plus comme ça» d'Elastic Search et Solr qui devraient donner des réponses approximatives avec un profil d'évolutivité sous-linéaire.
ogrisel
7
Cela vous donnerait-il la similitude cosinus de chaque document avec tous les autres documents, au lieu du seul premier cosine_similarities = linear_kernel(tfidf, tfidf):?
ionox0
2
Oui, cela vous donnera une matrice carrée de similitudes par paires.
ogrisel
10
Au cas où d'autres se demanderaient comme moi, dans ce cas, linear_kernel équivaut à cosine_similarity car le TfidfVectorizer produit des vecteurs normalisés. Voir la note dans la documentation: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark
22

Avec l'aide du commentaire de @ excray, je parviens à trouver la réponse, ce que nous devons faire est en fait d'écrire une simple boucle for pour itérer sur les deux tableaux qui représentent les données de train et les données de test.

Commencez par implémenter une fonction lambda simple pour contenir la formule du calcul du cosinus:

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

Et puis il suffit d'écrire une simple boucle for à itérer sur le vecteur to, la logique est pour chaque "Pour chaque vecteur dans trainVectorizerArray, vous devez trouver la similitude cosinus avec le vecteur dans 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()

Voici la sortie:

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]]
ajouter des points-virgules
la source
1
nice..J'apprends aussi depuis le début et votre question et réponse sont les plus faciles à suivre. Je pense que vous pouvez utiliser np.corrcoef () à la place de votre méthode roll-your-own.
wbg
Quel est le but des transformer.fitopérations et tfidf.todense()? Vous avez obtenu vos valeurs de similarité de la boucle et continuez à faire tfidf? Où votre valeur cosinus calculée est-elle utilisée? Votre exemple est déroutant.
minéraux
Quel est exactement le retour du cosinus si cela ne vous dérange pas d'expliquer. Dans votre exemple, vous obtenez 0.408et 0.816quelles sont ces valeurs?
buydadip
20

Je sais que c'est un vieux post. mais j'ai essayé le package http://scikit-learn.sourceforge.net/stable/ . voici mon code pour trouver la similitude cosinus. La question était de savoir comment calculerez-vous la similitude cosinus avec ce package et voici mon code pour cela

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

Supposons ici que la requête soit le premier élément de train_set et doc1, doc2 et doc3 sont les documents que je souhaite classer à l'aide de la similarité cosinus. alors je peux utiliser ce code.

Les didacticiels fournis dans la question ont également été très utiles. Voici toutes les pièces pour cela partie-I , partie-II , partie-III

la sortie sera la suivante:

[[ 1.          0.07102631  0.02731343  0.06348799]]

ici 1 représente que la requête est mise en correspondance avec elle-même et les trois autres sont les scores de correspondance de la requête avec les documents respectifs.

Gunjan
la source
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) Que faire si ce 1 est changé à plus de milliers. Comment pouvons-nous gérer cela ??
ashim888
1
comment gérerValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
pyd
17

Laissez-moi vous donner un autre tutoriel écrit par moi. Cela répond à votre question, mais explique également pourquoi nous faisons certaines choses. J'ai également essayé de le rendre concis.

Donc, vous avez un list_of_documentsqui est juste un tableau de chaînes et un autre documentqui n'est qu'une chaîne. Vous devez trouver un tel document à partir du list_of_documentsqui est le plus similaire à document.

Combinons-les ensemble: documents = list_of_documents + [document]

Commençons par les dépendances. Il deviendra clair pourquoi nous utilisons chacun d'eux.

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

L'une des approches qui peuvent être utilisées est une approche du sac de mots , où nous traitons chaque mot du document indépendamment des autres et les jetons tous ensemble dans le grand sac. D'un certain point de vue, cela perd beaucoup d'informations (comme la façon dont les mots sont connectés), mais d'un autre point de vue, cela simplifie le modèle.

En anglais et dans n'importe quelle autre langue humaine, il y a beaucoup de mots "inutiles" comme "a", "le", "dans" qui sont si courants qu'ils n'ont pas beaucoup de sens. Ils sont appelés mots vides et c'est une bonne idée de les supprimer. Une autre chose que l'on peut remarquer est que des mots comme «analyser», «analyseur», «analyse» sont vraiment similaires. Ils ont une racine commune et tous peuvent être convertis en un seul mot. Ce processus s'appelle la tige et il existe différents types de tige qui diffèrent par leur vitesse, leur agressivité, etc. Nous transformons donc chacun des documents en liste de tiges de mots sans mots vides. Nous supprimons également toute la ponctuation.

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]

Alors, comment ce sac de mots nous aidera-t-il? Imaginez que nous avons 3 sacs: [a, b, c], [a, c, a]et [b, c, d]. Nous pouvons les convertir en vecteurs dans la base [a, b, c, d] . Donc , nous nous retrouvons avec des vecteurs: [1, 1, 1, 0], [2, 0, 1, 0]et [0, 1, 1, 1]. La même chose est avec nos documents (seuls les vecteurs seront beaucoup plus longs). Maintenant, nous voyons que nous avons supprimé beaucoup de mots et en a extrait d'autres également pour diminuer les dimensions des vecteurs. Ici, il y a juste une observation intéressante. Les documents plus longs auront beaucoup plus d'éléments positifs que les plus courts, c'est pourquoi il est agréable de normaliser le vecteur. C'est ce qu'on appelle le terme fréquence TF, les gens ont également utilisé des informations supplémentaires sur la fréquence à laquelle le mot est utilisé dans d'autres documents - fréquence IDF inverse du document. Ensemble, nous avons une métrique TF-IDF qui a quelques saveurs. Ceci peut être réalisé avec une ligne dans 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)

En fait, vectorizer permet de faire beaucoup de choses comme supprimer les mots vides et les minuscules. Je les ai fait dans une étape séparée seulement parce que sklearn n'a pas de mots vides non anglais, mais nltk a.

Nous avons donc tous les vecteurs calculés. La dernière étape consiste à trouver lequel est le plus similaire au dernier. Il existe différentes manières d'y parvenir, l'une d'entre elles est la distance euclidienne qui n'est pas si grande pour la raison évoquée ici . Une autre approche est la similitude cosinus . Nous itérons tous les documents et calculons la similitude cosinus entre le document et le dernier:

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

Maintenant, minimum aura des informations sur le meilleur document et son score.

Salvador Dali
la source
3
Signe, ce n'est pas ce que demandait op: rechercher le meilleur document à partir de la requête et non "le meilleur document" dans un corpus. S'il vous plaît ne le faites pas, les personnes comme moi perdront du temps à essayer d'utiliser votre exemple pour la tâche op et seront entraînées dans la folie du redimensionnement de la matrice.
minéraux
Et en quoi est-ce différent? L'idée est complètement la même. Extraire des entités, calculer la distance cosinus entre une requête et des documents.
Salvador Dali
Vous calculez cela sur des matrices de formes égales, essayez un exemple différent, où vous avez une matrice de requête qui est de taille différente, l'ensemble de train d'opérations et l'ensemble de test. Je n'ai pas pu modifier votre code pour qu'il fonctionne.
minéraux
@SalvadorDali Comme indiqué, ce qui précède répond à une question différente: vous supposez que la requête et les documents font partie du même corpus, ce qui est faux. Cela conduit à une mauvaise approche consistant à utiliser des distances de vecteurs dérivés du même corpus (avec les mêmes dimensions), ce qui n'est généralement pas le cas. Si la requête et les documents appartiennent à des corpus différents, les vecteurs dont ils proviennent peuvent ne pas vivre dans le même espace et calculer les distances comme vous le faites ci-dessus n'aurait aucun sens (ils n'auront même pas le même nombre de dimensions).
gented le
12

Cela devrait vous aider.

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

et la sortie sera:

[[ 0.34949812  0.81649658  1.        ]]
Sam
la source
9
comment obtenez-vous la longueur?
gogasca
3

Voici une fonction qui compare vos données de test aux données de formation, avec le transformateur Tf-Idf équipé des données de formation. L'avantage est que vous pouvez rapidement pivoter ou regrouper pour trouver les n éléments les plus proches, et que les calculs sont descendants par matrice.

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
Paul Ogier
la source
pandas.pydata.org/pandas-docs/stable/reference/api/… explique ce que fait pd.melt
Golden Lion
pour index dans np.arange (0, len (score)): value = score.loc [index, 'score']
Golden Lion