Python: tf-idf-cosine: para encontrar la similitud del documento

93

Estaba siguiendo un tutorial que estaba disponible en la Parte 1 y la Parte 2 . Desafortunadamente, el autor no tuvo tiempo para la sección final que involucraba el uso de la similitud de coseno para encontrar la distancia entre dos documentos. Seguí los ejemplos del artículo con la ayuda del siguiente enlace de stackoverflow , incluido el código mencionado en el enlace anterior (solo para hacer la vida más 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 del código anterior tengo la siguiente 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]]

No estoy seguro de cómo usar esta salida para calcular la similitud del coseno, sé cómo implementar la similitud del coseno con respecto a dos vectores de longitud similar, pero aquí no estoy seguro de cómo identificar los dos vectores.

añadir punto y coma
fuente
3
Para cada vector en trainVectorizerArray, debe encontrar la similitud del coseno con el vector en testVectorizerArray.
excray
@excray Gracias, con tu punto de ayuda me las arreglé para resolverlo, ¿debo poner la respuesta?
añadir punto y coma
@excray Pero tengo una pequeña pregunta, el cálculo actuall tf * idf no sirve para esto, porque no estoy usando los resultados finales que se muestran en la matriz.
añadir punto y coma
4
Aquí está la tercera parte del tutorial que cita que responde a su pregunta en detalle pyevolve.sourceforge.net/wordpress/?p=2497
Clément Renaud
@ ClémentRenaud seguí con el enlace que proporcionaste, pero como mis documentos son más grandes, comienza a arrojar MemoryError. ¿Cómo podemos manejar eso?
ashim888

Respuestas:

173

En primer lugar, si desea extraer funciones de recuento y aplicar la normalización TF-IDF y la normalización euclidiana por filas, puede hacerlo en una operación 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>

Ahora, para encontrar las distancias de coseno de un documento (por ejemplo, el primero en el conjunto de datos) y todos los demás, solo necesita calcular los productos escalares del primer vector con todos los demás, ya que los vectores tfidf ya están normalizados por filas.

Como explica Chris Clark en los comentarios y aquí, Cosine Similarity no tiene en cuenta la magnitud de los vectores. Los normalizados por filas tienen una magnitud de 1, por lo que el núcleo lineal es suficiente para calcular los valores de similitud.

La API de matriz dispersa scipy es un poco extraña (no tan flexible como las matrices numpy densas N-dimensionales). Para obtener el primer vector, necesita cortar la matriz en filas para obtener una submatriz con una sola fila:

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

scikit-learn ya proporciona métricas por pares (también conocidas como kernels en el lenguaje del aprendizaje automático) que funcionan tanto para representaciones densas como dispersas de colecciones de vectores. En este caso, necesitamos un producto escalar que también se conoce como kernel lineal:

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

Por lo tanto, para encontrar los 5 documentos relacionados principales, podemos usar argsorty algunos cortes de matriz negativa (la mayoría de los documentos relacionados tienen los valores de similitud de coseno más altos, por lo tanto, al final de la matriz de índices ordenados):

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

El primer resultado es una verificación de cordura: encontramos el documento de consulta como el documento más similar con una puntuación de similitud de coseno de 1 que tiene el siguiente texto:

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

El segundo documento más similar es una respuesta que cita el mensaje original, por lo tanto, tiene muchas palabras en común:

>>> 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
fuente
Una pregunta de seguimiento: si tengo una gran cantidad de documentos, la función linear_kernel en el paso 2 puede ser el cuello de botella en el rendimiento, ya que es lineal al número de filas. ¿Alguna idea sobre cómo reducirlo a sublineal?
Shuo
Puede utilizar las consultas "más como esto" de Elastic Search y Solr que deberían producir respuestas aproximadas con un perfil de escalabilidad sublineal.
ogrisel
7
¿Le daría esto la similitud de coseno de cada documento con cualquier otro documento, en lugar de solo el primero cosine_similarities = linear_kernel(tfidf, tfidf):?
ionox0
2
Sí, esto le dará una matriz cuadrada de similitudes por pares.
ogrisel
10
En caso de que otros se pregunten como yo, en este caso linear_kernel es equivalente a cosine_similarity porque el TfidfVectorizer produce vectores normalizados. Vea la nota en los documentos: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Chris Clark
22

Con la ayuda del comentario de @excray, me las arreglo para encontrar la respuesta, lo que tenemos que hacer es escribir un bucle for simple para iterar sobre las dos matrices que representan los datos del tren y los datos de prueba.

Primero implemente una función lambda simple para mantener la fórmula para el cálculo del coseno:

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

Y luego simplemente escriba un bucle for simple para iterar sobre el vector to, la lógica es para cada "Para cada vector en trainVectorizerArray, debe encontrar la similitud del coseno con el vector en 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()

Aquí está el 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]]
añadir punto y coma
fuente
1
agradable ... También estoy aprendiendo desde el principio y tu pregunta y respuesta son las más fáciles de seguir. Creo que puede usar np.corrcoef () en lugar de su método roll-your-own.
wbg
¿Cuál es el propósito de las transformer.fitoperaciones y tfidf.todense()? ¿Obtuviste tus valores de similitud del ciclo y luego continúas haciendo tfidf? ¿Dónde se utiliza su valor de coseno calculado? Tu ejemplo es confuso.
minerales
¿Qué es exactamente el coseno regresando si no te importa explicarlo? En su ejemplo, obtiene 0.408y 0.816, ¿cuáles son estos valores?
buydadip
20

Sé que es una publicación antigua. pero probé el paquete http://scikit-learn.sourceforge.net/stable/ . aquí está mi código para encontrar la similitud del coseno. La pregunta era cómo calcularía la similitud de coseno con este paquete y aquí está mi código para eso

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

Supongamos aquí que la consulta es el primer elemento de train_set y doc1, doc2 y doc3 son los documentos que quiero clasificar con la ayuda de la similitud de coseno. entonces puedo usar este código.

También los tutoriales proporcionados en la pregunta fueron muy útiles. Aquí están todas las partes para ello parte I , parte II , parte III

la salida será la siguiente:

[[ 1.          0.07102631  0.02731343  0.06348799]]

aquí 1 representa que la consulta coincide consigo misma y las otras tres son las puntuaciones para hacer coincidir la consulta con los documentos respectivos.

Gunjan
fuente
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) ¿Qué pasa si ese 1 se cambia a más de miles? ¿Cómo podemos manejar eso?
ashim888
1
cómo manejarValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
pyd
17

Déjame darte otro tutorial escrito por mí. Responde a su pregunta, pero también explica por qué estamos haciendo algunas de las cosas. También traté de hacerlo conciso.

Entonces tienes una list_of_documentsque es solo una matriz de cadenas y otra documentque es solo una cadena. Debe encontrar dicho documento en el list_of_documentsque sea más similar a document.

Combinémoslos juntos: documents = list_of_documents + [document]

Comencemos con las dependencias. Quedará claro por qué usamos cada uno de ellos.

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 de los enfoques que se pueden utilizar es el enfoque de bolsa de palabras , en el que tratamos cada palabra del documento de forma independiente de las demás y simplemente las arrojamos todas juntas en la bolsa grande. Desde un punto de vista, pierde mucha información (como cómo se conectan las palabras), pero desde otro punto de vista hace que el modelo sea simple.

En inglés y en cualquier otro idioma humano hay muchas palabras "inútiles" como 'a', 'the', 'in' que son tan comunes que no poseen mucho significado. Se llaman palabras vacías y es una buena idea eliminarlas. Otra cosa que uno puede notar es que palabras como "analizar", "analizador", "análisis" son realmente similares. Tienen una raíz común y todos se pueden convertir en una sola palabra. Este proceso se llama derivados y existen diferentes lematizadores que difieren en la velocidad, agresividad y así sucesivamente. Entonces transformamos cada uno de los documentos en una lista de raíces de palabras sin palabras vacías. También descartamos toda la puntuación.

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]

Entonces, ¿cómo nos ayudará esta bolsa de palabras? Imaginemos que tenemos 3 bolsas: [a, b, c], [a, c, a]y [b, c, d]. Podemos convertirlos en vectores en la base [a, b, c, d] . Así que terminamos con vectores: [1, 1, 1, 0], [2, 0, 1, 0]y [0, 1, 1, 1]. Lo mismo ocurre con nuestros documentos (solo los vectores serán mucho más largos). Ahora vemos que eliminamos muchas palabras y derivamos otras también para disminuir las dimensiones de los vectores. Aquí hay una observación interesante. Los documentos más largos tendrán muchos más elementos positivos que los más cortos, por eso es bueno normalizar el vector. Esto se llama frecuencia de término TF, la gente también utilizó información adicional sobre la frecuencia con la que se usa la palabra en otros documentos: frecuencia de documento inversa IDF. Juntos tenemos una métrica TF-IDF que tiene un par de sabores. Esto se puede lograr con una línea en 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 realidad, el vectorizador permite hacer muchas cosas, como eliminar palabras vacías y minúsculas. Los he hecho en un paso separado solo porque sklearn no tiene palabras vacías que no estén en inglés, pero nltk sí.

Entonces tenemos todos los vectores calculados. El último paso es encontrar cuál es el más parecido al anterior. Hay varias formas de lograrlo, una de ellas es la distancia euclidiana que no es tan grande por la razón discutida aquí . Otro enfoque es la similitud de coseno . Iteramos todos los documentos y calculamos la similitud de coseno entre el documento y el ú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

Ahora mínimo tendrá información sobre el mejor documento y su puntuación.

Salvador Dalí
fuente
3
Firme, esto no es lo que op estaba pidiendo: buscar el mejor documento dado, no "el mejor documento" en un corpus. Por favor, no lo hagas, las personas como yo perderán el tiempo tratando de usar tu ejemplo para la tarea operativa y serán arrastradas a la locura del cambio de tamaño de la matriz.
minerales
¿Y en qué se diferencia? La idea es completamente la misma. Extraiga entidades, calcule la distancia del coseno entre una consulta y los documentos.
Salvador Dali
Está calculando esto en matrices de formas iguales, pruebe con un ejemplo diferente, donde tiene una matriz de consulta que es de diferente tamaño, conjunto de tren de operaciones y conjunto de prueba. No pude modificar su código para que funcione.
minerales
@SalvadorDali Como se señaló, lo anterior responde a una pregunta diferente: está asumiendo que la consulta y los documentos son parte del mismo corpus, lo cual es incorrecto. Esto conduce al enfoque equivocado de usar distancias de vectores derivados del mismo corpus (con las mismas dimensiones), lo que generalmente no es el caso. Si la consulta y los documentos pertenecen a corpus diferentes, es posible que los vectores que originan no vivan en el mismo espacio y calcular las distancias como lo hace anteriormente no tendría sentido (ni siquiera tendrán el mismo número de dimensiones).
gented
12

Esto debería ayudarte.

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

y la salida será:

[[ 0.34949812  0.81649658  1.        ]]
Sam
fuente
9
¿cómo se obtiene la longitud?
gogasca
3

Aquí hay una función que compara sus datos de prueba con los datos de entrenamiento, con el transformador Tf-Idf equipado con los datos de entrenamiento. La ventaja es que puede pivotar o agrupar rápidamente para encontrar los n elementos más cercanos y que los cálculos se realizan en forma 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
fuente
pandas.pydata.org/pandas-docs/stable/reference/api/… explica lo que hace pd.melt
Golden Lion
para índice en np.arange (0, len (puntuación)): valor = puntuación.loc [índice, 'puntuación']
León de Oro