Python: tf-idf-cosine: aby znaleźć podobieństwo dokumentu

93

Śledziłem samouczek, który był dostępny w części 1 i części 2 . Niestety autor nie miał czasu na ostatnią sekcję, która polegała na wykorzystaniu podobieństwa cosinusowego, aby faktycznie znaleźć odległość między dwoma dokumentami. Podążałem za przykładami w artykule za pomocą poniższego linku ze stackoverflow , w zestawie jest kod wymieniony w powyższym linku (tylko po to, aby ułatwić życie)

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

w wyniku powyższego kodu mam następującą macierz

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

Nie jestem pewien, jak wykorzystać to wyjście, aby obliczyć podobieństwo cosinusowe, wiem, jak zaimplementować podobieństwo cosinusowe w odniesieniu do dwóch wektorów o podobnej długości, ale tutaj nie jestem pewien, jak zidentyfikować te dwa wektory.

dodaj średniki
źródło
3
Dla każdego wektora w trainVectorizerArray musisz znaleźć podobieństwo cosinusowe z wektorem w testVectorizerArray.
excray
@excray Dzięki, z twoim pomocnym punktem udało mi się to rozgryźć, czy powinienem udzielić odpowiedzi?
dodaj średniki
@excray Ale mam małe pytanie, w rzeczywistości obliczenia tf * idf nie mają do tego zastosowania, ponieważ nie używam ostatecznych wyników, które są pokazane w macierzy.
dodaj średniki
4
Oto trzecia część zacytowanego samouczka, która szczegółowo odpowiada na Twoje pytanie pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud
@ ClémentRenaud Skorzystałem z podanego przez Ciebie łącza, ale ponieważ moje dokumenty są większe, zaczyna generować błąd MemoryError. Jak możemy sobie z tym poradzić?
ashim888

Odpowiedzi:

173

Po pierwsze, jeśli chcesz wyodrębnić funkcje zliczania i zastosować normalizację TF-IDF i normalizację euklidesową w wierszach, możesz to zrobić w jednej operacji za pomocą 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>

Teraz, aby znaleźć odległości cosinusowe jednego dokumentu (np. Pierwszego w zbiorze danych) i wszystkich pozostałych, wystarczy obliczyć iloczyn skalarny pierwszego wektora z wszystkimi pozostałymi, ponieważ wektory tfidf są już znormalizowane wierszowo.

Jak wyjaśnił Chris Clark w komentarzach i tutaj Cosinus Podobieństwo nie bierze pod uwagę wielkości wektorów. Wartość znormalizowana wierszami ma wielkość 1, więc liniowe jądro jest wystarczające do obliczenia wartości podobieństwa.

Scipy Sparse Matrix API jest trochę dziwne (nie tak elastyczne jak gęste N-wymiarowe tablice numpy). Aby otrzymać pierwszy wektor, musisz przeciąć macierz wierszami, aby uzyskać podmacierz z jednym wierszem:

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

scikit-learn już zapewnia metryki parami (zwane także jądrem w języku uczenia maszynowego), które działają zarówno dla gęstych, jak i rzadkich reprezentacji kolekcji wektorów. W tym przypadku potrzebujemy iloczynu skalarnego, znanego również jako jądro liniowe:

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

W związku z tym, aby znaleźć 5 najważniejszych powiązanych dokumentów, możemy użyć argsorti niektórych ujemnych wycinków tablicowych (większość powiązanych dokumentów ma najwyższe wartości podobieństwa cosinusowego, stąd na końcu tablicy posortowanych indeksów):

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

Pierwszym wynikiem jest sprawdzenie poprawności: dokument zapytania jest najbardziej podobny z wynikiem podobieństwa cosinusowego równym 1 i ma następujący tekst:

>>> print twenty.data[0]
From: [email protected] (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 ----

Drugim najbardziej podobnym dokumentem jest odpowiedź, która cytuje oryginalną wiadomość, dlatego zawiera wiele popularnych słów:

>>> print twenty.data[958]
From: [email protected] (Robert Seymour)
Subject: Re: WHAT car is this!?
Article-I.D.: reed.1993Apr21.032905.29286
Reply-To: [email protected]
Organization: Reed College, Portland, OR
Lines: 26

In article <1993Apr20.174246.14375@wam.umd.edu> [email protected] (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              [email protected]
Physics and Philosophy, Reed College    (NeXTmail accepted)
Artificial Life Project         Reed College
Reed Solar Energy Project (SolTrain)    Portland, OR
ogrisel
źródło
Kolejne pytanie: jeśli mam bardzo dużą liczbę dokumentów, funkcja linear_kernel w kroku 2 może być wąskim gardłem wydajności, ponieważ jest liniowa w stosunku do liczby wierszy. Jakieś przemyślenia, jak zredukować to do podliniowej?
Shuo,
Możesz użyć zapytań „więcej podobnych” w Elastic Search i Solr, które powinny dać przybliżone odpowiedzi z nieliniowym profilem skalowalności.
ogrisel
7
Czy to daje podobieństwa cosinus każdego dokumentu z każdym innym dokumencie, a nie tylko pierwszej z nich: cosine_similarities = linear_kernel(tfidf, tfidf)?
ionox0
2
Tak, to da ci kwadratową macierz podobieństw parami.
ogrisel
10
Na wypadek, gdyby inni zastanawiali się tak jak ja, w tym przypadku linear_kernel jest równoważne cosine_similarity, ponieważ TfidfVectorizer tworzy znormalizowane wektory. Zobacz notatkę w dokumentacji: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark,
22

Dzięki pomocy komentarza @ excray udaje mi się znaleźć odpowiedź: To, co musimy zrobić, to napisać prostą pętlę for, aby iterować po dwóch tablicach, które reprezentują dane pociągu i dane testowe.

Najpierw zaimplementuj prostą funkcję lambda, aby zachować wzór na obliczenie cosinusa:

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

A potem po prostu napisz prostą pętlę for, aby wykonać iterację po wektorze to, logika jest dla każdego „Dla każdego wektora w trainVectorizerArray musisz znaleźć podobieństwo cosinusowe z wektorem w 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()

Oto wynik:

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]]
dodaj średniki
źródło
1
fajnie… ja też uczę się od początku, a twoje pytanie i odpowiedź są najłatwiejsze do zrozumienia. Myślę, że możesz użyć np.corrcoef () zamiast własnej metody roll-your-own.
wbg
Jaki jest cel transformer.fitoperacji i tfidf.todense()? Otrzymałeś wartości podobieństwa z pętli, a następnie kontynuujesz wykonywanie tfidf? Gdzie jest używana obliczona wartość cosinusa? Twój przykład jest zagmatwany.
minerały
Co dokładnie powraca cosinus, jeśli nie masz nic przeciwko wyjaśnieniu. W swoim przykładzie otrzymujesz 0.408i 0.816jakie są te wartości?
buydadip
20

Wiem, że to stary post. ale wypróbowałem pakiet http://scikit-learn.sourceforge.net/stable/ . oto mój kod, aby znaleźć podobieństwo cosinusowe. Pytanie brzmiało, jak obliczyć podobieństwo cosinusowe z tym pakietem, a oto mój kod do tego

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

Załóżmy, że zapytanie jest pierwszym elementem train_set, a doc1, doc2 i doc3 to dokumenty, które chcę uszeregować za pomocą podobieństwa cosinusowego. wtedy mogę użyć tego kodu.

Również tutoriale zawarte w pytaniu były bardzo przydatne. Oto wszystkie części do tego część I , część II , część III

wynik będzie następujący:

[[ 1.          0.07102631  0.02731343  0.06348799]]

tutaj 1 oznacza, że ​​zapytanie jest dopasowane do siebie, a pozostałe trzy to wyniki za dopasowanie zapytania do odpowiednich dokumentów.

Gunjan
źródło
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) Co się stanie, jeśli ta 1 zostanie zmieniona na więcej niż tysiące. Jak możemy sobie z tym poradzić?
ashim888
1
jak sobie radzićValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
pyd
17

Pozwólcie, że przedstawię kolejny tutorial napisany przeze mnie. Odpowiada na twoje pytanie, ale także wyjaśnia, dlaczego robimy niektóre rzeczy. Starałem się też, żeby był zwięzły.

Więc masz jedną, list_of_documentsktóra jest po prostu tablicą ciągów, a drugą, documentktóra jest po prostu łańcuchem. Musisz znaleźć taki dokument z list_of_documentsnajbardziej podobnego do document.

Połączmy je razem: documents = list_of_documents + [document]

Zacznijmy od zależności. Stanie się jasne, dlaczego używamy każdego z nich.

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

Jednym z podejść, które można zastosować, jest worek słów , w którym każde słowo w dokumencie traktujemy niezależnie od innych i po prostu wrzucamy je wszystkie razem do dużej torby. Z jednego punktu widzenia traci wiele informacji (np. Sposób łączenia słów), ale z innego punktu widzenia upraszcza model.

W języku angielskim i każdym innym języku ludzkim istnieje wiele „bezużytecznych” słów, takich jak „a”, „the”, „in”, które są tak powszechne, że nie mają zbyt dużego znaczenia. Nazywa się je słowami stop i dobrze jest je usunąć. Inną rzeczą, którą można zauważyć, jest to, że słowa takie jak „analizować”, „analizator”, „analiza” są bardzo podobne. Mają wspólny rdzeń i wszystkie można zamienić na jedno słowo. Proces ten nazywany jest wynikająca i istnieją różne stemmery które różnią się prędkością, agresywność i tak dalej. Dlatego przekształcamy każdy z dokumentów w listę rdzeni słów bez słów stopujących. Odrzucamy również całą interpunkcję.

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]

Jak więc pomoże nam ten worek słów? Wyobraźmy sobie, mamy 3 torby: [a, b, c], [a, c, a]i [b, c, d]. Możemy je przekonwertować na wektory w bazie [a, b, c, d] . Więc skończyć z wektorami: [1, 1, 1, 0], [2, 0, 1, 0]i [0, 1, 1, 1]. Podobnie jest z naszymi dokumentami (tylko wektory będą znacznie dłuższe). Teraz widzimy, że usunęliśmy wiele słów i wyprowadziliśmy inne, aby zmniejszyć wymiary wektorów. Tutaj jest tylko ciekawa obserwacja. Dłuższe dokumenty będą miały znacznie więcej pozytywnych elementów niż krótsze, dlatego warto znormalizować wektor. Nazywa się to terminem częstotliwości TF, ludzie używali również dodatkowych informacji o tym, jak często słowo jest używane w innych dokumentach - odwrotna częstotliwość dokumentu IDF. Razem mamy metrykę TF-IDF, który ma kilka smaków. Można to osiągnąć za pomocą jednej linii w 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)

W rzeczywistości wektoryzator pozwala na wiele rzeczy, takich jak usuwanie słów ignorowanych i małe litery. Zrobiłem to w osobnym kroku tylko dlatego, że sklearn nie ma nieangielskich słów kluczowych, ale nltk ma.

Więc mamy obliczone wszystkie wektory. Ostatnim krokiem jest znalezienie najbardziej podobnego do ostatniego. Można to osiągnąć na wiele sposobów, jednym z nich jest odległość euklidesowa, która nie jest tak duża z omówionego tutaj powodu . Innym podejściem jest podobieństwo cosinusowe . Iterujemy wszystkie dokumenty i obliczamy podobieństwo cosinusowe między dokumentem a ostatnim:

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

Teraz minimum będzie zawierało informacje o najlepszym dokumencie i jego punktacji.

Salvador Dali
źródło
3
Podpisz, że nie o to prosił op: szukanie najlepszego dokumentu na dane zapytanie, a nie „najlepszy dokument” w korpusie. Proszę, nie rób tego, ludzie tacy jak ja będą tracić czas na próbę użycia twojego przykładu do zadania operacyjnego i dadzą się wciągnąć w szaleństwo zmiany rozmiaru matrycy.
minerały
A jak to się różni? Idea jest zupełnie taka sama. Wyodrębnij cechy, oblicz cosinus między zapytaniem a dokumentami.
Salvador Dali
Obliczasz to na macierzach o równych kształtach, wypróbuj inny przykład, w którym masz macierz zapytań o różnym rozmiarze, zestaw pociągów op i zestaw testowy. Nie udało mi się zmodyfikować Twojego kodu, aby działał.
minerały
@SalvadorDali Jak wskazałem powyżej, powyższe odpowiedzi na inne pytanie: zakładasz, że zapytanie i dokumenty są częścią tego samego korpusu, co jest błędne. Prowadzi to do niewłaściwego podejścia do wykorzystywania odległości wektorów pochodzących z tego samego korpusu (o tych samych wymiarach), co generalnie nie musi mieć miejsca. Jeśli zapytanie i dokumenty należą do różnych korpusów, wektory, z których pochodzą, mogą nie znajdować się w tej samej przestrzeni, a obliczanie odległości, tak jak powyżej, nie miałoby sensu (nie będą miały nawet takiej samej liczby wymiarów).
gented
12

To powinno ci pomóc.

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

a wynik będzie:

[[ 0.34949812  0.81649658  1.        ]]
Sam
źródło
9
jak uzyskać długość?
gogasca
3

Oto funkcja, która porównuje dane testowe z danymi szkoleniowymi, z transformatorem Tf-Idf wyposażonym w dane uczące. Zaletą jest to, że możesz szybko przestawiać lub grupować według, aby znaleźć n najbliższych elementów, a obliczenia są w dół macierzy.

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
źródło
pandas.pydata.org/pandas-docs/stable/reference/api/… wyjaśnia, co robi pd.melt
Golden Lion
dla indeksu w np.arange (0, len (score)): value = score.loc [index, 'score']
Golden Lion