Python: tf-idf-cosine: para encontrar la similitud de documentos

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 involucró el uso de la similitud de coseno para encontrar realmente la distancia entre dos documentos. Seguí los ejemplos en el 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 de coseno, sé cómo implementar la similitud de coseno con respecto a dos vectores de longitud similar, pero aquí no estoy seguro de cómo identificar los dos vectores.

En primer lugar, si desea extraer las características de conteo y aplicar la normalización TF-IDF y la normalización euclidiana en filas, puede hacerlo en una sola 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 '' 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 de puntos del primer vector con todos los demás, ya que los vectores tfidf ya están normalizados en filas. La API de matriz dispersa escéptica es un poco rara (no es tan flexible como las matrices numpy de N-dimensional densas). Para obtener el primer vector, debe dividir la matriz por filas para obtener una submatriz con una sola fila:

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

scikit-learn ya proporciona métricas de pares (también conocidos como núcleos en lenguaje de aprendizaje automático) que funcionan tanto para representaciones densas como dispersas de colecciones de vectores. En este caso, necesitamos un producto de puntos que también se conoce como el 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 principales documentos relacionados, podemos usar argsort y algunos cortes de matriz negativos (la mayoría de los documentos relacionados tienen los valores más altos de similitud de coseno, 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 comprobación de validez: 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: 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 ---- 

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

 >>> print twenty.data[958] From: rseymour@reed.edu (Robert Seymour) Subject: Re: WHAT car is this!? Article-ID: 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 

Sé que es un viejo post. pero probé el paquete http://scikit-learn.sourceforge.net/stable/ . Aquí está mi código para encontrar la similitud coseno. La pregunta era cómo calculará 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 

Aquí supongamos que la consulta es el primer elemento de train_set y doc1, doc2 y doc3 son los documentos que deseo 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 con sí misma y las otras tres son las puntuaciones para hacer coincidir la consulta con los documentos respectivos.

Con la ayuda del comentario de @exray, logro encontrar la respuesta. Lo que debemos hacer es escribir un bucle simple para iterar sobre las dos matrices que representan los datos del tren y de la 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, 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á la salida:

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

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

Así que tienes un list_of_documents que es solo una matriz de cadenas y otro document que es solo una cadena. list_of_documents encontrar dicho documento en la list_of_documents documentos que es el más similar al document .

Vamos a combinarlos: documents = list_of_documents + [document]

Empecemos por 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 usar es un enfoque de bolsa de palabras , donde tratamos cada palabra en el documento de manera independiente de las demás y simplemente las juntamos todas en la bolsa grande. Desde un punto de vista, pierde mucha información (como la forma en que se conectan las palabras), pero desde otro punto de vista hace que el modelo sea simple.

En inglés y en cualquier otro lenguaje humano hay muchas palabras “inútiles” como ‘a’, ‘the’, ‘in’ que son tan comunes que no tienen mucho significado. Se llaman palabras de alto 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 denomina derivación y existen diferentes derivadores que difieren en velocidad, agresividad, etc. Así que transformamos cada uno de los documentos en una lista de tallos de palabras sin palabras de detención. 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? Imagina que tenemos 3 bolsas: [a, b, c] , [a, c, a] y [b, c, d] . Podemos convertirlos a vectores en la base [a, b, c, d] . Entonces terminamos con los 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 más largos). Ahora vemos que eliminamos muchas palabras y derivamos otras también para disminuir las dimensiones de los vectores. Aquí solo hay una observación interesante. Los documentos más largos tendrán elementos más positivos que cortos, por eso es bueno normalizar el vector. Esto se denomina término frecuencia TF, las personas también usaron información adicional sobre la frecuencia con la que se usa la palabra en otros documentos: frecuencia inversa de documentos IDF. Juntos tenemos un TF-IDF métrico 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 clave y hacer minúsculas. Los he hecho en un paso separado solo porque sklearn no tiene palabras de parada que no estén en inglés, pero nltk sí.

Así que tenemos todos los vectores calculados. El último paso es encontrar cuál es el más similar al último. Hay varias formas de lograrlo, una de ellas es la distancia euclidiana, que no es tan buena por la razón que se explica aquí . Otro enfoque es la similitud coseno . Recorremos 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 puntaje.

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

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 girar rápidamente o agruparse para encontrar los n elementos más cercanos, y que los cálculos no están basados ​​en la 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