Python: tf-idf-cosine: найти сходство документа

93

Я следил за учебником, который был доступен в частях 1 и 2 . К сожалению, у автора не было времени на последний раздел, в котором использовалось косинусное сходство, чтобы на самом деле найти расстояние между двумя документами. Я следил за примерами в статье с помощью следующей ссылки из stackoverflow , включая код, упомянутый в приведенной выше ссылке (просто чтобы облегчить жизнь)

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

в результате приведенного выше кода у меня есть следующая матрица

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

Я не уверен, как использовать этот вывод для вычисления косинусного сходства, я знаю, как реализовать косинусное сходство по отношению к двум векторам одинаковой длины, но здесь я не уверен, как идентифицировать два вектора.

добавить точку с запятой
источник
3
Для каждого вектора в trainVectorizerArray вам нужно найти косинусное сходство с вектором в testVectorizerArray.
excray
@excray Спасибо, с вашей полезной точкой мне удалось разобраться, я должен ответить?
добавить точку с запятой
@excray Но у меня есть небольшой вопрос, вычисление фактически tf * idf для этого бесполезно, потому что я не использую окончательные результаты, показанные в матрице.
добавить точку с запятой
4
Вот 3-я часть цитируемого вами учебника, которая подробно отвечает на ваш вопрос pyevolve.sourceforge.net/wordpress/?p=2497
Клеман Рено
@ ClémentRenaud Я перешел по указанной вами ссылке, но поскольку мои документы стали больше, он начинает выдавать ошибку MemoryError. Как мы можем с этим справиться?
ashim888

Ответы:

173

Во-первых, если вы хотите извлечь функции подсчета и применить нормализацию TF-IDF и евклидову нормализацию по строкам, вы можете сделать это за одну операцию с помощью 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>

Теперь, чтобы найти косинусные расстояния одного документа (например, первого в наборе данных) и всех остальных, вам просто нужно вычислить скалярные произведения первого вектора со всеми остальными, поскольку векторы tfidf уже нормализованы по строкам.

Как объяснил Крис Кларк в комментариях, и здесь косинусное сходство не учитывает величину векторов. Нормализованные по строкам имеют величину 1, поэтому линейного ядра достаточно для вычисления значений подобия.

API-интерфейс scipy sparse matrix немного странный (не такой гибкий, как плотные N-мерные массивы numpy). Чтобы получить первый вектор, вам нужно разрезать матрицу по строкам, чтобы получить подматрицу с одной строкой:

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

scikit-learn уже предоставляет попарные метрики (также известные как ядра на языке машинного обучения), которые работают как для плотных, так и для разреженных представлений векторных коллекций. В этом случае нам нужен точечный продукт, который также известен как линейное ядро:

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

Следовательно, чтобы найти 5 лучших связанных документов, мы можем использовать argsortи некоторую отрицательную нарезку массива (большинство связанных документов имеют самые высокие значения косинусного сходства, следовательно, в конце отсортированного массива индексов):

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

Первым результатом является проверка работоспособности: мы находим документ запроса как наиболее похожий документ с показателем сходства косинуса 1, который имеет следующий текст:

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

Второй наиболее похожий документ - это ответ, в котором цитируется исходное сообщение, поэтому в нем много общих слов:

>>> 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
огризель
источник
Последующий вопрос: если у меня очень большое количество документов, функция linear_kernel на шаге 2 может стать узким местом производительности, поскольку она линейна по отношению к количеству строк. Есть мысли, как уменьшить его до сублинейного?
Shuo
Вы можете использовать запросы типа «что-то вроде этого» из Elastic Search и Solr, которые должны давать приблизительные ответы с профилем сублинейной масштабируемости.
ogrisel 09
7
Это даст вам косинусное сходство каждого документа с любым другим документом, а не только с первым cosine_similarities = linear_kernel(tfidf, tfidf):?
ionox0
2
Да, это даст вам квадратную матрицу попарного сходства.
ogrisel
10
В случае, если другие задаются вопросом, как и я, в этом случае linear_kernel эквивалентно cosine_similarity, потому что TfidfVectorizer создает нормализованные векторы. См. Примечание в документации: scikit-learn.org/stable/modules/metrics.html#cosine-similarity
Крис Кларк,
22

С помощью комментария @ excray мне удалось выяснить ответ. Что нам нужно сделать, так это написать простой цикл for для перебора двух массивов, представляющих данные поезда и тестовые данные.

Сначала реализуйте простую лямбда-функцию для хранения формулы для вычисления косинуса:

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

А затем просто напишите простой цикл for для итерации по вектору to, логика для каждого «Для каждого вектора в trainVectorizerArray вы должны найти косинусное сходство с вектором в 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()

Вот результат:

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
хорошо .. Я тоже учусь с самого начала, и на ваш вопрос и ответ легче всего следить. Я думаю, что вы можете использовать np.corrcoef () вместо своего собственного метода.
wbg
Какова цель transformer.fitопераций и tfidf.todense()? Вы получили значения подобия из цикла, а затем продолжили выполнение tfidf? Где используется ваше вычисленное значение косинуса? Ваш пример сбивает с толку.
минералы
Что именно возвращается косинусом, если вы не против объяснить. В вашем примере вы получаете 0.408и 0.816, что это за значения?
buydadip
20

Я знаю, что это старый пост. но я попробовал пакет http://scikit-learn.sourceforge.net/stable/ . вот мой код, чтобы найти сходство косинуса. Вопрос заключался в том, как вы рассчитаете косинусное сходство с этим пакетом, и вот мой код для этого

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

Предположим, что запрос - это первый элемент train_set, а doc1, doc2 и doc3 - это документы, которые я хочу ранжировать с помощью косинусного сходства. тогда я могу использовать этот код.

Также были очень полезны уроки, представленные в вопросе. Вот все части для него часть-I , часть-II , часть-III

вывод будет следующим:

[[ 1.          0.07102631  0.02731343  0.06348799]]

здесь 1 означает, что запрос совпадает с самим собой, а три других - это оценки за сопоставление запроса с соответствующими документами.

Гунджан
источник
1
cosine_similarity (tfidf_matrix_train [0: 1], tfidf_matrix_train) Что, если это 1 изменится на более тысячи. Как мы можем с этим справиться ??
ashim888
1
ValueError: Incompatible dimension for X and Y matrices: X.shape[1] == 1664 while Y.shape[1] == 2
How
17

Позвольте представить вам еще один урок, написанный мной. Он отвечает на ваш вопрос, но также дает объяснение, почему мы делаем некоторые из вещей. Я тоже постарался сделать его кратким.

Итак, у вас есть list_of_documentsодин массив строк, а другой document- просто строка. Вам нужно найти такой документ из list_of_documentsнаиболее похожих на document.

Объединим их вместе: documents = list_of_documents + [document]

Начнем с зависимостей. Становится понятно, почему мы используем каждую из них.

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

Один из подходов, который можно использовать, - это метод «набора слов» , когда мы обрабатываем каждое слово в документе независимо от других и просто складываем их все вместе в большой мешок. С одной точки зрения, он теряет много информации (например, как связаны слова), но с другой точки зрения он упрощает модель.

В английском и любом другом человеческом языке есть много «бесполезных» слов, таких как «a», «the», «in», которые настолько распространены, что не имеют большого значения. Они называются стоп-словами, и их рекомендуется удалить. Еще одна вещь, которую можно заметить, - это то, что такие слова, как «анализировать», «анализатор», «анализ» действительно похожи. У них общий корень, и все они могут быть преобразованы в одно слово. Этот процесс называется стеммингом, и существуют разные стеммеры, которые различаются по скорости, агрессивности и так далее. Таким образом, мы преобразуем каждый из документов в список основных слов без стоп-слов. Также мы отбрасываем все знаки препинания.

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]

Так чем же нам поможет этот набор слов? Представьте себе , у нас есть 3 сумки: [a, b, c], [a, c, a]и [b, c, d]. Мы можем преобразовать их в векторы в базисе [a, b, c, d] . Таким образом , мы в конечном итоге с векторами: [1, 1, 1, 0], [2, 0, 1, 0]и [0, 1, 1, 1]. То же самое и с нашими документами (только векторы будут длиннее). Теперь мы видим, что мы удалили много слов и вырезали другие, чтобы уменьшить размерность векторов. Здесь просто любопытное наблюдение. В более длинных документах будет намного больше положительных элементов, чем в более коротких, поэтому хорошо нормализовать вектор. Это называется термином частота TF, люди также использовали дополнительную информацию о том, как часто это слово используется в других документах - обратная частота документа IDF. Вместе у нас есть метрика TF-IDF, у которой есть несколько разновидностей. Этого можно добиться с помощью одной строчки в 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)

На самом деле векторизатор позволяет делать много вещей, таких как удаление стоп-слов и строчные буквы. Я сделал их на отдельном этапе только потому, что sklearn не имеет неанглийских стоп-слов, а nltk есть.

Итак, мы рассчитали все векторы. Последний шаг - найти, какой из них больше всего похож на последний. Есть разные способы добиться этого, один из них - евклидово расстояние, которое не так велико по причине, обсуждаемой здесь . Другой подход - косинусное подобие . Мы перебираем все документы и вычисляем косинусное сходство между документом и последним:

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

Теперь минимум будет иметь информацию о лучшем документе и его оценке.

Сальвадор Дали
источник
3
Подпишите, это не то, о чем просила опера: поиск лучшего документа по запросу, а не "лучший документ" в корпусе. Пожалуйста, не делайте этого, люди вроде меня будут тратить время на попытки использовать ваш пример для задачи op и попадут в безумие изменения размера матрицы.
минералы
А чем он отличается? Идея полностью та же. Извлечение функций, вычисление косинусного расстояния между запросом и документами.
Сальвадор Дали,
Вы вычисляете это на матрицах одинаковой формы, попробуйте другой пример, где у вас есть матрица запроса разного размера, набор поездов оператора и набор тестов. Мне не удалось изменить ваш код, чтобы он работал.
минералы
@SalvadorDali Как уже указывалось, приведенное выше отвечает на другой вопрос: вы предполагаете, что запрос и документы являются частью одного и того же корпуса, что неверно. Это приводит к неправильному подходу к использованию расстояний векторов, полученных из одного и того же корпуса (с одинаковыми размерами), что в общем случае не обязательно. Если запрос и документы принадлежат разным корпусам, векторы, которые они порождают, могут не находиться в одном пространстве, и вычисление расстояний, как описано выше, не имеет смысла (они даже не будут иметь одинаковое количество измерений).
gented
12

Это должно вам помочь.

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

и вывод будет:

[[ 0.34949812  0.81649658  1.        ]]
Сэм
источник
9
как получить длину?
gogasca 02
3

Вот функция, которая сравнивает ваши тестовые данные с обучающими данными, с преобразователем Tf-Idf, снабженным обучающими данными. Преимущество состоит в том, что вы можете быстро выполнить поворот или группировку, чтобы найти n ближайших элементов, и что вычисления выполняются по матрице.

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
Поль Ожье
источник
pandas.pydata.org/pandas-docs/stable/reference/api/… объясняет, что делает pd.melt
Golden Lion
для индекса в np.arange (0, len (score)): value = score.loc [index, 'score']
Golden Lion