Python: tf-idf-cosine: um Dokumentähnlichkeit zu finden

90

Ich folgte einem Tutorial, das in Teil 1 und Teil 2 verfügbar war . Leider hatte der Autor nicht die Zeit für den letzten Abschnitt, in dem die Kosinusähnlichkeit verwendet wurde, um den Abstand zwischen zwei Dokumenten tatsächlich zu ermitteln. Ich habe die Beispiele im Artikel mit Hilfe des folgenden Links von stackoverflow befolgt , einschließlich des im obigen Link erwähnten Codes (nur um das Leben einfacher zu machen).

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

Aufgrund des obigen Codes habe ich die folgende Matrix

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

Ich bin nicht sicher, wie ich diese Ausgabe verwenden soll, um die Kosinusähnlichkeit zu berechnen. Ich weiß, wie man die Kosinusähnlichkeit in Bezug auf zwei Vektoren ähnlicher Länge implementiert, aber hier bin ich nicht sicher, wie ich die beiden Vektoren identifizieren soll.

Add-Semikolons
quelle
3
Für jeden Vektor in trainVectorizerArray müssen Sie die Kosinusähnlichkeit mit dem Vektor in testVectorizerArray ermitteln.
Exray
@excray Danke, mit deinem hilfreichen Punkt, den ich herausgefunden habe, sollte ich die Antwort geben?
Add-Semikolons
@excray Aber ich habe eine kleine Frage, die tf * idf-Berechnung hat hierfür keine Verwendung, da ich nicht die in der Matrix angezeigten Endergebnisse verwende.
Add-Semikolons
4
Hier ist der dritte Teil des Tutorials, das Sie zitieren und der Ihre Frage ausführlich beantwortet. Pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud
@ ClémentRenaud Ich folgte mit dem Link, den Sie angegeben haben, aber da meine Dokumente größer sind, wird MemoryError ausgelöst. Wie können wir damit umgehen?
ashim888

Antworten:

168

Wenn Sie zunächst Zählfunktionen extrahieren und die TF-IDF-Normalisierung und die zeilenweise euklidische Normalisierung anwenden möchten, können Sie dies in einem Arbeitsgang tun mit 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>

Um nun die Kosinusabstände eines Dokuments (z. B. des ersten im Datensatz) und aller anderen Dokumente zu ermitteln, müssen Sie nur die Punktprodukte des ersten Vektors mit allen anderen berechnen, da die tfidf-Vektoren bereits zeilennormalisiert sind.

Wie von Chris Clark in den Kommentaren und hier erklärt erklärt, berücksichtigt die Cosinus-Ähnlichkeit nicht die Größe der Vektoren. Zeilennormalisierte haben eine Größe von 1 und daher reicht der lineare Kernel aus, um die Ähnlichkeitswerte zu berechnen.

Die Scipy-Sparse-Matrix-API ist etwas seltsam (nicht so flexibel wie dichte N-dimensionale Numpy-Arrays). Um den ersten Vektor zu erhalten, müssen Sie die Matrix zeilenweise aufteilen, um eine Submatrix mit einer einzelnen Zeile zu erhalten:

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

scikit-learn bietet bereits paarweise Metriken (auch bekannt als Kernel in der Sprache des maschinellen Lernens), die sowohl für dichte als auch für spärliche Darstellungen von Vektorsammlungen funktionieren. In diesem Fall benötigen wir ein Punktprodukt, das auch als linearer Kernel bezeichnet wird:

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

Um die Top 5 verwandten Dokumente zu finden, können wir daher argsortein negatives Array-Slicing verwenden (die meisten verwandten Dokumente haben die höchsten Kosinus-Ähnlichkeitswerte, daher am Ende des Arrays mit sortierten Indizes):

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

Das erste Ergebnis ist eine Überprüfung der Integrität: Wir finden das Abfragedokument als das ähnlichste Dokument mit einer Kosinus-Ähnlichkeitsbewertung von 1, die den folgenden Text enthält:

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

Das zweitähnlichste Dokument ist eine Antwort, die die ursprüngliche Nachricht zitiert und daher viele gebräuchliche Wörter enthält:

>>> 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
quelle
Eine Folgefrage: Wenn ich eine sehr große Anzahl von Dokumenten habe, kann die Funktion linear_kernel in Schritt 2 der Leistungsengpass sein, da sie linear zur Anzahl der Zeilen ist. Irgendwelche Gedanken darüber, wie man es auf sublinear reduzieren kann?
Shuo
Sie können die "ähnlicheren" Abfragen von Elastic Search und Solr verwenden, die ungefähre Antworten mit einem sublinearen Skalierbarkeitsprofil liefern sollen.
Ogrisel
6
Würde dies Ihnen die Kosinusähnlichkeit jedes Dokuments mit jedem anderen Dokument geben, anstatt nur dem ersten : cosine_similarities = linear_kernel(tfidf, tfidf)?
Ionox0
2
Ja, dies gibt Ihnen eine quadratische Matrix paarweiser Ähnlichkeiten.
Ogrisel
10
Für den Fall, dass sich andere wie ich gefragt haben, entspricht linear_kernel in diesem Fall cosine_similarity, da der TfidfVectorizer normalisierte Vektoren erzeugt. Siehe den Hinweis in den Dokumenten: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark
22

Mit Hilfe des Kommentars von @ excray gelingt es mir, die Antwort herauszufinden: Wir müssen tatsächlich eine einfache for-Schleife schreiben, um über die beiden Arrays zu iterieren, die die Zugdaten und Testdaten darstellen.

Implementieren Sie zunächst eine einfache Lambda-Funktion, um die Formel für die Kosinusberechnung zu speichern:

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

Und dann schreiben Sie einfach eine einfache for-Schleife, um über den to-Vektor zu iterieren. Die Logik gilt für jeden "Für jeden Vektor in trainVectorizerArray müssen Sie die Cosinus-Ähnlichkeit mit dem Vektor in testVectorizerArray finden."

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

Hier ist die Ausgabe:

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]]
Add-Semikolons
quelle
1
Ich lerne auch von Anfang an und Ihre Frage und Antwort sind am einfachsten zu befolgen. Ich denke, dass Sie np.corrcoef () anstelle Ihrer eigenen Methode verwenden können.
wbg
Was ist der Zweck der transformer.fitOperationen und tfidf.todense()? Sie haben Ihre Ähnlichkeitswerte aus der Schleife erhalten und fahren dann mit tfidf fort? Wo wird Ihr berechneter Kosinuswert verwendet? Ihr Beispiel ist verwirrend.
Mineralien
Was genau kehrt der Kosinus zurück, wenn es Ihnen nichts ausmacht, dies zu erklären? In Ihrem Beispiel erhalten Sie 0.408und 0.816, was sind diese Werte?
Buydadip
20

Ich weiß, es ist ein alter Beitrag. Ich habe jedoch das Paket http://scikit-learn.sourceforge.net/stable/ ausprobiert . Hier ist mein Code, um die Kosinusähnlichkeit zu finden. Die Frage war, wie Sie die Kosinusähnlichkeit mit diesem Paket berechnen und hier ist mein Code dafür

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

Angenommen, die Abfrage ist das erste Element von train_set und doc1, doc2 und doc3 sind die Dokumente, die ich mithilfe der Kosinusähnlichkeit bewerten möchte. dann kann ich diesen Code verwenden.

Auch die in der Frage bereitgestellten Tutorials waren sehr nützlich. Hier sind alle Teile dafür Teil I , Teil II , Teil III

Die Ausgabe wird wie folgt sein:

[[ 1.          0.07102631  0.02731343  0.06348799]]

Hier stellt 1 dar, dass die Abfrage mit sich selbst übereinstimmt, und die anderen drei sind die Bewertungen für die Zuordnung der Abfrage zu den jeweiligen Dokumenten.

Gunjan
quelle
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) Was ist, wenn diese 1 in mehr als Tausende geändert wird? Wie können wir damit umgehen?
ashim888
1
wie man damit ValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
umgeht
17

Lassen Sie mich Ihnen ein weiteres von mir geschriebenes Tutorial geben. Es beantwortet Ihre Frage, erklärt aber auch, warum wir einige der Dinge tun. Ich habe auch versucht, es kurz zu machen.

Sie haben also eine, list_of_documentsdie nur ein Array von Zeichenfolgen ist, und eine andere, documentdie nur eine Zeichenfolge ist. Sie müssen ein solches Dokument aus dem Dokument finden list_of_documents, das dem am ähnlichsten ist document.

Kombinieren wir sie miteinander: documents = list_of_documents + [document]

Beginnen wir mit Abhängigkeiten. Es wird klar, warum wir jeden von ihnen verwenden.

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

Einer der Ansätze, die verwendet werden können, ist eine Tüte mit Wörtern Word Ansatz, bei dem wir jedes Wort im Dokument unabhängig von anderen behandeln und alle zusammen in die große Tasche werfen. Unter einem Gesichtspunkt verliert es viele Informationen (wie zum Beispiel, wie die Wörter verbunden sind), aber unter einem anderen Gesichtspunkt macht es das Modell einfach.

Im Englischen und in jeder anderen menschlichen Sprache gibt es viele "nutzlose" Wörter wie "a", "the", "in", die so häufig vorkommen, dass sie nicht viel Bedeutung haben. Sie werden Stoppwörter genannt und es ist eine gute Idee, sie zu entfernen. Eine andere Sache, die man bemerken kann, ist, dass Wörter wie "analysieren", "Analysator", "Analyse" wirklich ähnlich sind. Sie haben eine gemeinsame Wurzel und alle können in nur ein Wort umgewandelt werden. Dieser Prozess wird als Stemming bezeichnet und es gibt verschiedene Stemmers, die sich in Geschwindigkeit, Aggressivität usw. unterscheiden. Also transformieren wir jedes der Dokumente in eine Liste von Wortstämmen ohne Stoppwörter. Außerdem verwerfen wir alle Satzzeichen.

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]

Wie hilft uns diese Wortsammlung? Stellen Sie sich vor wir 3 Taschen haben: [a, b, c], [a, c, a]und [b, c, d]. Wir können sie in der Basis in Vektoren umwandeln [a, b, c, d]. Also haben wir mit Vektoren am Ende: [1, 1, 1, 0], [2, 0, 1, 0]und [0, 1, 1, 1]. Ähnliches gilt für unsere Dokumente (nur die Vektoren werden viel zu lang sein). Jetzt sehen wir, dass wir viele Wörter entfernt und andere gestemmt haben, um auch die Dimensionen der Vektoren zu verringern. Hier gibt es nur interessante Beobachtungen. Längere Dokumente enthalten weitaus mehr positive Elemente als kürzere. Deshalb ist es hilfreich, den Vektor zu normalisieren. Dies wird als Häufigkeit TF bezeichnet. Die Benutzer verwendeten auch zusätzliche Informationen darüber, wie oft das Wort in anderen Dokumenten verwendet wird - inverse Dokumenthäufigkeit IDF. Zusammen haben wir eine metrische TF-IDF, die einige Geschmacksrichtungen hat. Dies kann mit einer Zeile in sklearn erreicht werden :-)

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)

Tatsächlich ermöglicht der Vektorisierer viele Dinge wie das Entfernen von Stoppwörtern und das Verkleinern. Ich habe sie nur in einem separaten Schritt ausgeführt, da sklearn keine nicht englischen Stoppwörter hat, nltk jedoch.

Wir haben also alle Vektoren berechnet. Der letzte Schritt besteht darin, herauszufinden, welcher dem letzten am ähnlichsten ist. Es gibt verschiedene Möglichkeiten, dies zu erreichen. Eine davon ist die euklidische Distanz, die aus dem hier diskutierten Grund nicht so groß ist . Ein anderer Ansatz ist die Kosinusähnlichkeit . Wir iterieren alle Dokumente und berechnen die Kosinusähnlichkeit zwischen dem Dokument und dem letzten:

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

Jetzt enthält Minimum Informationen über das beste Dokument und seine Punktzahl.

Salvador Dali
quelle
3
Sign, das ist nicht das, wonach op gefragt hat: Suche nach dem besten Dokument bei gegebener Abfrage, nicht "das beste Dokument" in einem Korpus. Bitte tu es nicht, Leute wie ich werden Zeit damit verschwenden, dein Beispiel für die Op-Aufgabe zu verwenden und in den Wahnsinn der Matrix-Größenänderung hineingezogen zu werden.
Mineralien
Und wie ist es anders? Die Idee ist völlig dieselbe. Extrahieren Sie Features und berechnen Sie den Kosinusabstand zwischen einer Abfrage und Dokumenten.
Salvador Dali
Sie berechnen dies auf Matrizen gleicher Form. Probieren Sie ein anderes Beispiel aus, bei dem Sie eine Abfragematrix unterschiedlicher Größe, den Zugsatz und den Testsatz von op haben. Ich konnte Ihren Code nicht so ändern, dass er funktioniert.
Mineralien
@SalvadorDali Wie bereits erwähnt, beantwortet das Obige eine andere Frage: Sie gehen davon aus, dass die Abfrage und die Dokumente Teil desselben Korpus sind, was falsch ist. Dies führt zu dem falschen Ansatz, Abstände von Vektoren zu verwenden, die von demselben Korpus (mit denselben Abmessungen) abgeleitet sind, was im Allgemeinen nicht der Fall sein muss. Wenn die Abfrage und die Dokumente zu verschiedenen Korpora gehören, befinden sich die von ihnen stammenden Vektoren möglicherweise nicht im selben Raum, und die Berechnung der Entfernungen wie oben wäre nicht sinnvoll (sie haben nicht einmal die gleiche Anzahl von Dimensionen).
Gented
12

Dies sollte Ihnen helfen.

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

und Ausgabe wird sein:

[[ 0.34949812  0.81649658  1.        ]]
Sam
quelle
9
Wie erhält man Länge?
Gogasca
3

Hier ist eine Funktion, die Ihre Testdaten mit den Trainingsdaten vergleicht, wobei der Tf-Idf-Transformator mit den Trainingsdaten ausgestattet ist. Der Vorteil ist, dass Sie schnell schwenken oder gruppieren können, um die n nächstgelegenen Elemente zu finden, und dass die Berechnungen in Bezug auf die Matrix nach unten erfolgen.

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
quelle
pandas.pydata.org/pandas-docs/stable/reference/api/… erklärt, was pd.melt macht
Golden Lion
für Index in np.arange (0, len (Punktzahl)): value = score.loc [Index, 'Punktzahl']
Golden Lion