Python: tf-idf-cosine: per trovare la somiglianza del documento


90

Stavo seguendo un tutorial che era disponibile nella Parte 1 e nella Parte 2 . Sfortunatamente l'autore non ha avuto il tempo per la sezione finale che prevedeva l'uso della somiglianza del coseno per trovare effettivamente la distanza tra due documenti. Ho seguito gli esempi nell'articolo con l'aiuto del seguente link da stackoverflow , incluso il codice menzionato nel link sopra (proprio per semplificarti la vita)

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

come risultato del codice sopra ho la seguente matrice

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

Non sono sicuro di come utilizzare questo output per calcolare la somiglianza del coseno, so come implementare la somiglianza del coseno rispetto a due vettori di lunghezza simile ma qui non sono sicuro di come identificare i due vettori.


3
Per ogni vettore in trainVectorizerArray, devi trovare la somiglianza del coseno con il vettore in testVectorizerArray.
esclude il

@excray Grazie, con il tuo punto utile sono riuscito a capirlo, devo mettere la risposta?
aggiungi punto e virgola

@excray Ma ho una piccola domanda, il calcolo effettivo tf * idf non serve a niente, perché non sto usando i risultati finali mostrati nella matrice.
aggiungi punto e virgola

4
Ecco la terza parte del tutorial che citi che risponde in dettaglio alla tua domanda pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud

@ ClémentRenaud ho seguito con il link che hai fornito, ma poiché i miei documenti sono più grandi inizia a lanciare MemoryError Come possiamo gestirlo?
ashim888

Risposte:


169

Prima di tutto, se desideri estrarre le funzionalità di conteggio e applicare la normalizzazione TF-IDF e la normalizzazione euclidea per riga, puoi farlo in un'unica operazione con 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>

Ora per trovare le distanze del coseno di un documento (ad es. Il primo nel set di dati) e di tutti gli altri è sufficiente calcolare i prodotti puntiformi del primo vettore con tutti gli altri poiché i vettori tfidf sono già normalizzati per riga.

Come spiegato da Chris Clark nei commenti e qui, la somiglianza del coseno non tiene conto della grandezza dei vettori. Le righe normalizzate hanno magnitudo 1 e quindi il kernel lineare è sufficiente per calcolare i valori di similarità.

L'API scipy sparse matrix è un po 'strana (non flessibile come gli array numpy N-dimensionali densi). Per ottenere il primo vettore devi tagliare la matrice per riga per ottenere una sottomatrice con una singola riga:

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

scikit-learn fornisce già metriche a coppie (note anche come kernel nel linguaggio del machine learning) che funzionano per rappresentazioni sia dense che sparse di raccolte di vettori. In questo caso abbiamo bisogno di un prodotto puntuale noto anche come kernel lineare:

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

Quindi, per trovare i primi 5 documenti correlati, possiamo usare argsorte alcune sezioni di array negative (la maggior parte dei documenti correlati ha valori di somiglianza del coseno più alti, quindi alla fine dell'array degli indici ordinati):

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

Il primo risultato è un controllo di integrità: troviamo il documento di query come il documento più simile con un punteggio di somiglianza del coseno di 1 che ha il seguente testo:

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

Il secondo documento più simile è una risposta che cita il messaggio originale, quindi ha molte parole comuni:

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

Una domanda di follow-up: se ho un numero molto elevato di documenti, la funzione linear_kernel nel passaggio 2 può essere il collo di bottiglia delle prestazioni, poiché è lineare al numero di righe. Qualche idea su come ridurlo a sublineare?
Shuo

Puoi utilizzare le query "più simili a questa" di Ricerca elastica e Solr che dovrebbero fornire risposte approssimative con un profilo di scalabilità sub-lineare.
ogrisel

7
Questo ti darebbe la somiglianza del coseno di ogni documento con ogni altro documento, invece del solo primo cosine_similarities = linear_kernel(tfidf, tfidf):?
ionox0

2
Sì, questo ti darà una matrice quadrata di somiglianze a coppie.
ogrisel

10
Nel caso altri si stessero chiedendo come ho fatto io, in questo caso linear_kernel è equivalente a cosine_similarity perché TfidfVectorizer produce vettori normalizzati. Vedi la nota nella documentazione: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark

22

Con l'aiuto del commento di @ excray, riesco a capire la risposta, quello che dobbiamo fare è in realtà scrivere un semplice ciclo for per iterare sui due array che rappresentano i dati del treno e i dati del test.

Per prima cosa implementa una semplice funzione lambda per contenere la formula per il calcolo del coseno:

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

Quindi scrivi un semplice ciclo for per iterare sul vettore to, la logica è per ogni "Per ogni vettore in trainVectorizerArray, devi trovare la somiglianza del coseno con il vettore in 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()

Ecco l'output:

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
bello .. anch'io sto imparando dall'inizio e la tua domanda e risposta sono le più facili da seguire. Penso che tu possa usare np.corrcoef () invece del tuo metodo roll-your-own.
wbg

Qual è lo scopo delle transformer.fitoperazioni e tfidf.todense()? Hai ottenuto i tuoi valori di somiglianza dal ciclo e poi continui a fare tfidf? Dove viene utilizzato il valore del coseno calcolato? Il tuo esempio è confuso.
minerali

Che cos'è esattamente il coseno che ritorna se non ti dispiace spiegare. Nel tuo esempio ottieni 0.408e 0.816, quali sono questi valori?
buydadip

20

So che è un vecchio post. ma ho provato il pacchetto http://scikit-learn.sourceforge.net/stable/ . ecco il mio codice per trovare la somiglianza del coseno. La domanda era come calcolerai la somiglianza del coseno con questo pacchetto ed ecco il mio codice per questo

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

Supponiamo che la query sia il primo elemento di train_set e doc1, doc2 e doc3 sono i documenti che voglio classificare con l'aiuto della somiglianza del coseno. allora posso usare questo codice.

Anche i tutorial forniti nella domanda sono stati molto utili. Ecco tutte le parti per esso parte-I , parte-II , parte-III

l'output sarà il seguente:

[[ 1.          0.07102631  0.02731343  0.06348799]]

qui 1 rappresenta che la query è abbinata a se stessa e gli altri tre sono i punteggi per la corrispondenza della query con i rispettivi documenti.


1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) E se quell'1 fosse cambiato in più di migliaia. Come possiamo gestirlo ??
ashim888

1
come gestireValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
pyd

17

Lascia che ti dia un altro tutorial scritto da me. Risponde alla tua domanda, ma spiega anche perché stiamo facendo alcune delle cose. Ho anche cercato di renderlo conciso.

Quindi hai un list_of_documentsche è solo un array di stringhe e un altro documentche è solo una stringa. È necessario trovare tale documento da quello list_of_documentsche è il più simile a document.

Combiniamoli insieme: documents = list_of_documents + [document]

Cominciamo con le dipendenze. Diventerà chiaro il motivo per cui utilizziamo ciascuno di essi.

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

Uno degli approcci che possono essere utilizzati è un approccio bag-of-word , in cui trattiamo ogni parola nel documento indipendentemente dalle altre e le buttiamo tutte insieme nella big bag. Da un punto di vista, perde molte informazioni (come il modo in cui le parole sono collegate), ma da un altro punto di vista rende semplice il modello.

In inglese e in qualsiasi altra lingua umana ci sono molte parole "inutili" come "a", "the", "in" che sono così comuni da non possedere molto significato. Si chiamano stop words ed è una buona idea rimuoverle. Un'altra cosa che si può notare è che parole come "analizzare", "analizzatore", "analisi" sono molto simili. Hanno una radice comune e tutti possono essere convertiti in una sola parola. Questo processo è chiamato stemming e esistono diversi stemmer che differiscono per velocità, aggressività e così via. Quindi trasformiamo ciascuno dei documenti in un elenco di radici di parole senza parole di arresto. Inoltre scartiamo tutta la punteggiatura.

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]

Allora come ci aiuterà questo sacco di parole? Immaginate abbiamo 3 borse: [a, b, c], [a, c, a]e [b, c, d]. Possiamo convertirli in vettori nella base [a, b, c, d] . Così finiamo con vettori: [1, 1, 1, 0], [2, 0, 1, 0]e [0, 1, 1, 1]. La cosa simile è con i nostri documenti (solo i vettori saranno più lunghi). Ora vediamo che abbiamo rimosso molte parole e ne abbiamo derivato altre anche per diminuire le dimensioni dei vettori. Qui c'è solo un'osservazione interessante. I documenti più lunghi avranno molti più elementi positivi di quelli più brevi, ecco perché è bello normalizzare il vettore. Questo è chiamato termine frequenza TF, le persone hanno anche usato informazioni aggiuntive sulla frequenza con cui la parola viene usata in altri documenti - frequenza inversa del documento IDF. Insieme abbiamo un TF-IDF metrico che ha un paio di gusti. Questo può essere ottenuto con una riga in 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)

In realtà il vectorizer consente di fare molte cose come rimuovere le parole di arresto e le minuscole. Li ho eseguiti in un passaggio separato solo perché sklearn non ha parole non inglesi non in inglese, ma nltk sì.

Quindi abbiamo tutti i vettori calcolati. L'ultimo passaggio è trovare qual è il più simile all'ultimo. Ci sono vari modi per ottenerlo, uno di questi è la distanza euclidea che non è così grande per il motivo discusso qui . Un altro approccio è la somiglianza del coseno . Ripetiamo tutti i documenti e calcoliamo la somiglianza del coseno tra il documento e l'ultimo:

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

Ora il minimo avrà informazioni sul documento migliore e sul suo punteggio.


3
Segno, questo non è ciò che stava chiedendo op: cercare il miglior documento dato la query non "il miglior documento" in un corpus. Per favore, non farlo, persone come me perderanno tempo cercando di usare il tuo esempio per l'attività operativa e verrai trascinato nella follia di ridimensionamento della matrice.
minerali

E come è diverso? L'idea è completamente la stessa. Estrai le caratteristiche, calcola la distanza del coseno tra una query e documenti.
Salvador Dali

Lo stai calcolando su matrici di forme uguali, prova un esempio diverso, in cui hai una matrice di query di dimensioni diverse, set di addestramento operativo e set di test. Non sono riuscito a modificare il tuo codice in modo che funzionasse.
minerali

@SalvadorDali Come sottolineato, quanto sopra risponde a una domanda diversa: stai assumendo che la query ei documenti facciano parte dello stesso corpus, il che è sbagliato. Ciò porta all'approccio sbagliato nell'usare distanze di vettori derivati ​​dallo stesso corpus (con le stesse dimensioni), cosa che generalmente non deve essere il caso. Se la query ei documenti appartengono a corpora diversi, i vettori da cui hanno origine potrebbero non vivere nello stesso spazio e calcolare le distanze come si fa sopra non avrebbe senso (non avranno nemmeno lo stesso numero di dimensioni).
Gented

12

Questo dovrebbe aiutarti.

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

e l'output sarà:

[[ 0.34949812  0.81649658  1.        ]]

9
come si ottiene la lunghezza?
gogasca

3

Ecco una funzione che confronta i dati di test con i dati di allenamento, con il trasformatore Tf-Idf dotato dei dati di allenamento. Il vantaggio è che puoi rapidamente ruotare o raggruppare per trovare gli n elementi più vicini e che i calcoli sono a matrice verso il basso.

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


for index in np.arange (0, len (score)): value = score.loc [index, 'score']
Golden Lion
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.