Python: tf-idf-cosine: para encontrar semelhanças de documentos

90

Eu estava seguindo um tutorial que estava disponível na Parte 1 e Parte 2 . Infelizmente, o autor não teve tempo para a seção final que envolvia o uso de similaridade de cosseno para realmente encontrar a distância entre dois documentos. Segui os exemplos do artigo com a ajuda do seguinte link de stackoverflow , incluído está o código mencionado no link acima (apenas para tornar a vida mais fácil)

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

como resultado do código acima, tenho a seguinte matriz

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

Não tenho certeza de como usar esta saída para calcular a similaridade de cosseno, eu sei como implementar a similaridade de cosseno em relação a dois vetores de comprimento semelhante, mas aqui não tenho certeza de como identificar os dois vetores.

adicionar ponto e vírgula
fonte
3
Para cada vetor em trainVectorizerArray, você deve encontrar a similaridade do cosseno com o vetor em testVectorizerArray.
excray
@excray Obrigado, com seu ponto útil eu consegui descobrir, devo colocar a resposta?
adicionar ponto e vírgula
@excray Mas eu tenho uma pequena questão, o cálculo reall tf * idf não tem uso para isso, porque não estou usando os resultados finais que são mostrados na matriz.
adicionar ponto e vírgula
4
Aqui está a 3ª parte do tutorial que você cita e que responde sua pergunta em detalhes pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud
@ ClémentRenaud Segui com o link que você forneceu, mas como meus documentos são maiores, começa a lançar MemoryError. Como podemos lidar com isso?
ashim888

Respostas:

169

Em primeiro lugar, se você deseja extrair recursos de contagem e aplicar normalização TF-IDF e normalização euclidiana por linha, você pode fazer isso em uma operação com 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>

Agora, para encontrar as distâncias de cosseno de um documento (por exemplo, o primeiro no conjunto de dados) e todos os outros, você só precisa calcular os produtos escalares do primeiro vetor com todos os outros, pois os vetores tfidf já estão normalizados por linha.

Conforme explicado por Chris Clark nos comentários e aqui semelhança de cossenos não leva em consideração a magnitude dos vetores. Normalizado por linha tem uma magnitude de 1 e, portanto, o Kernel Linear é suficiente para calcular os valores de similaridade.

A API de matriz esparsa scipy é um pouco estranha (não tão flexível quanto matrizes numpy N-dimensionais densas). Para obter o primeiro vetor, você precisa dividir a matriz em linha para obter uma submatriz com uma única linha:

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

O scikit-learn já fornece métricas de pares (também conhecidas como kernels, no jargão do aprendizado de máquina) que funcionam tanto para representações densas quanto esparsas de coleções de vetores. Nesse caso, precisamos de um produto escalar que também é conhecido como kernel linear:

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

Portanto, para encontrar os 5 principais documentos relacionados, podemos usar argsorte um pouco de divisão de matriz negativa (a maioria dos documentos relacionados tem valores de similaridade de cosseno mais altos, portanto, no final da matriz de índices classificados):

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

O primeiro resultado é uma verificação de sanidade: encontramos o documento de consulta como o documento mais semelhante com uma pontuação de similaridade de cosseno de 1, que possui o seguinte texto:

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

O segundo documento mais semelhante é uma resposta que cita a mensagem original, portanto, tem muitas palavras em comum:

>>> 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
fonte
Uma pergunta de acompanhamento: se eu tiver um número muito grande de documentos, a função linear_kernel na etapa 2 pode ser o gargalo de desempenho, uma vez que é linear ao número de linhas. Alguma ideia de como reduzi-lo ao sublinear?
Shuo
Você pode usar as consultas "mais assim" do Elastic Search e Solr que devem produzir respostas aproximadas com um perfil de escalabilidade sublinear.
ogrisel
7
Isso forneceria a similaridade de cosseno de cada documento com todos os outros documentos, em vez de apenas o primeiro cosine_similarities = linear_kernel(tfidf, tfidf):?
ionox0
2
Sim, isso lhe dará uma matriz quadrada de semelhanças entre pares.
ogrisel
10
No caso de outros estarem se perguntando como eu, neste caso linear_kernel é equivalente a cosine_similarity porque o TfidfVectorizer produz vetores normalizados. Veja a observação nos documentos: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark
22

Com a ajuda do comentário de @exray, consigo descobrir a resposta, O que precisamos fazer é realmente escrever um loop for simples para iterar sobre os dois arrays que representam os dados do trem e os dados de teste.

Primeiro implemente uma função lambda simples para manter a fórmula para o cálculo do cosseno:

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

Em seguida, basta escrever um loop for simples para iterar sobre o vetor to, a lógica é para cada "Para cada vetor em trainVectorizerArray, você deve encontrar a similaridade do cosseno com o vetor em 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()

Aqui está o resultado:

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]]
adicionar ponto e vírgula
fonte
1
Legal..Estou aprendendo desde o início também e sua pergunta e resposta são as mais fáceis de seguir. Eu acho que você pode usar np.corrcoef () em vez de seu próprio método.
wbg
Qual é o propósito das transformer.fitoperações e tfidf.todense()? Você obteve seus valores de similaridade do loop e continuou fazendo tfidf? Onde seu valor cosseno calculado é usado? Seu exemplo é confuso.
minerais
O que exatamente é o cosseno retornando, se você não se importar em explicar. No seu exemplo, você obtém 0.408e 0.816quais são esses valores?
buydadip
20

Eu sei que é um post antigo. mas tentei o pacote http://scikit-learn.sourceforge.net/stable/ . aqui está meu código para encontrar a similaridade do cosseno. A questão era como você vai calcular a similaridade de cosseno com este pacote e aqui está o meu código para isso

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

Aqui, suponha que a consulta seja o primeiro elemento de train_set e doc1, doc2 e doc3 são os documentos que desejo classificar com a ajuda da similaridade de cosseno. então posso usar este código.

Além disso, os tutoriais fornecidos na questão foram muito úteis. Aqui estão todas as partes, parte-I , parte-II , parte-III

a saída será a seguinte:

[[ 1.          0.07102631  0.02731343  0.06348799]]

aqui, 1 representa que a consulta é correspondida com ela mesma e os outros três são as pontuações para combinar a consulta com os respectivos documentos.

Gunjan
fonte
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) E se esse 1 for alterado para mais de milhares. Como podemos lidar com isso ??
ashim888
1
como lidar comValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
pyd
17

Deixe-me dar outro tutorial escrito por mim. Ele responde à sua pergunta, mas também explica por que estamos fazendo algumas das coisas. Também tentei ser conciso.

Então você tem um list_of_documentsque é apenas um array de strings e outro documentque é apenas um string. Você precisa encontrar esse documento no list_of_documentsque é mais semelhante adocument .

Vamos combiná-los: documents = list_of_documents + [document]

Vamos começar com dependências. Ficará claro por que usamos cada um deles.

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

Uma das abordagens que pode ser usada é um saco de palavras , em que tratamos cada palavra no documento independentemente das outras e apenas jogamos todas juntas no saco grande. De um ponto de vista, ele perde muitas informações (como como as palavras são conectadas), mas de outro ponto de vista, torna o modelo simples.

Em inglês e em qualquer outra língua humana, existem muitas palavras "inúteis" como 'a', 'the', 'in', que são tão comuns que não possuem muito significado. Eles são chamados de palavras de interrupção e é uma boa ideia removê-los. Outra coisa que se pode notar é que palavras como 'analisar', 'analisador', 'análise' são muito semelhantes. Eles têm uma raiz comum e todos podem ser convertidos em apenas uma palavra. Esse processo é chamado de stemming e existem diferentes stemmers que diferem em velocidade, agressividade e assim por diante. Assim, transformamos cada um dos documentos em uma lista de radicais de palavras sem palavras de interrupção. Também descartamos toda a pontuação.

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]

Então, como esse saco de palavras nos ajudará? Imagine que temos 3 sacos: [a, b, c], [a, c, a]e [b, c, d]. Podemos convertê-los em vetores na base [a, b, c, d] . Então vamos acabar com vetores: [1, 1, 1, 0], [2, 0, 1, 0]e [0, 1, 1, 1]. O mesmo acontece com os nossos documentos (apenas os vetores serão muito mais longos). Agora vemos que removemos muitas palavras e originamos outras também para diminuir as dimensões dos vetores. Aqui há apenas uma observação interessante. Documentos mais longos terão muito mais elementos positivos do que mais curtos, por isso é bom normalizar o vetor. Isso é chamado de frequência de termo TF, as pessoas também usaram informações adicionais sobre a frequência com que a palavra é usada em outros documentos - IDF de frequência de documento inversa. Juntos, temos uma métrica TF-IDF que tem alguns sabores. Isso pode ser alcançado com uma linha em 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)

Na verdade, o vetorizador permite fazer várias coisas, como remover palavras de interrupção e letras minúsculas. Eu as fiz em uma etapa separada apenas porque sklearn não tem palavras irrelevantes que não sejam em inglês, mas nltk tem.

Portanto, temos todos os vetores calculados. A última etapa é descobrir qual é o mais semelhante ao último. Existem várias maneiras de se conseguir isso, uma delas é a distância euclidiana que não é tão grande pelo motivo discutido aqui . Outra abordagem é a similaridade do cosseno . Nós iteramos todos os documentos e calculamos a similaridade de cosseno entre o documento e o último:

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

Agora o mínimo terá informações sobre o melhor documento e sua pontuação.

Salvador Dalí
fonte
3
Assine, não era isso que op estava pedindo: pesquisar o melhor documento dada a consulta não "o melhor documento" em um corpus. Por favor, não faça isso, pessoas como eu perderão tempo tentando usar seu exemplo para a tarefa operacional e serão arrastadas para a loucura de redimensionamento de matriz.
minerais
E como é diferente? A ideia é completamente a mesma. Extraia recursos, calcule a distância cosseno entre uma consulta e documentos.
Salvador Dali
Você está calculando isso em matrizes de formatos iguais, tente um exemplo diferente, onde você tem uma matriz de consulta que é de tamanho diferente, conjunto de trem de operação e conjunto de teste. Não consegui modificar seu código para que funcionasse.
minerais
@SalvadorDali Como apontado, o anterior responde a uma pergunta diferente: você está assumindo que a consulta e os documentos fazem parte do mesmo corpus, o que está errado. Isso leva a uma abordagem errada de usar distâncias de vetores derivados do mesmo corpus (com as mesmas dimensões), o que geralmente não precisa ser o caso. Se a consulta e os documentos pertencerem a corpora diferentes, os vetores que eles originam podem não viver no mesmo espaço e calcular as distâncias como você fez acima não faria sentido (eles nem mesmo terão o mesmo número de dimensões).
acelerado em
12

Isso deve ajudá-lo.

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 a saída será:

[[ 0.34949812  0.81649658  1.        ]]
Sam
fonte
9
como você obtém comprimento?
gogasca
3

Esta é uma função que compara seus dados de teste com os dados de treinamento, com o transformador Tf-Idf equipado com os dados de treinamento. A vantagem é que você pode rapidamente girar ou agrupar para encontrar os n elementos mais próximos e que os cálculos são feitos em termos de matriz.

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
fonte
pandas.pydata.org/pandas-docs/stable/reference/api/… explica o que o pd.melt faz
Golden Lion
para índice em np.arange (0, len (pontuação)): valor = score.loc [index, 'score']
Golden Lion