Решение для автоматического извлечения ключевых слов из документов. Реализовано на Python с помощью NLTK и Scikit-learn.

Представьте, что у вас есть миллионы (а может, миллиарды) текстовых документов. Будь то заявки в службу поддержки, данные в социальных сетях или сообщения на форуме сообщества. Когда данные были сгенерированы, тегов не было. Вы сильно ломаете голову, присваивая теги этим случайным документам.

Пометка вручную нецелесообразна; Предоставление существующего списка тегов скоро устареет. Наем компании-поставщика для выполнения работы по маркировке обходится слишком дорого.

Вы можете спросить, а почему бы не использовать машинное обучение? например, глубокое обучение Neral Network. Но сначала NN нужны некоторые обучающие данные. Данные для обучения, которые подходят вашему набору данных.

Итак, есть ли решение, которое мы можем предложить для маркировки документов:

  1. Нет необходимости в предварительном запросе данных обучения.
  2. Минимальное ручное вмешательство и возможность автоматического запуска.
  3. Автоматически фиксируйте новые слова и фразы.

В этой статье рассказывается, как я извлекал ключевые слова, как это работает, обходные пути в Python.

Обратите внимание, что код в этой статье был запущен и протестирован в Jupyter Notebook. Если вы запускаете блок кода, но приветствуете ошибку отсутствия пакета импорта, этот пакет должен быть уже импортирован где-то впереди.

Основная идея

TF-IDF - это широко используемый алгоритм, который оценивает, насколько слово релевантно документу в наборе документов.

В моей предыдущей статье Измерение веса текста с помощью TF-IDF в Python и scikit-learn я использовал простой пример, чтобы показать, как вычислить значение TF-IDF для всех слов в документе. Как в чистом коде Python, так и с использованием пакета scikit-learn.

Согласно TF-IDF, эти уникальные и важные слова должны иметь высокие значения TF-IDF в определенном документе. Итак, теоретически мы должны иметь возможность использовать вес текста # для извлечения наиболее важных слов документа.

Например, документ, в котором говорится о scikit-learn, должен включать очень высокую плотность ключевых слов scikit-learn, в то время как другой документ, в котором говорится о «пандах», должен иметь высокое значение TF-IDF для pandas .

Целевые документы

Поскольку я не могу использовать здесь свою ежедневную рабочую базу данных, а также убедиться, что вы можете выполнить образец кода для извлечения ключевых слов на своем локальном компьютере с минимальными усилиями. Я обнаружил, что корпус документов Reuters от НЛТК является хорошей целью для извлечения ключевых слов.

Если вы не знакомы с корпусом NLTK, эта статья может быть полезна для запуска NLTK менее чем за час: Анализ шаблонов написания книг - начните с анализа текста NLTK и Python с примером использования.

Чтобы скачать корпус Reuters. запустить код Python:

import nltk
nltk.download("reuters")

Перечислите все идентификаторы документов из только что загруженного корпуса.

from nltk.corpus import reuters
reuters.fileids()

Ознакомьтесь с содержанием одного документа и его категорией.

fileid = reuters.fileids()[202]
print(fileid,"\n"
      ,reuters.raw(fileid),"\n"
      ,reuters.categories(fileid),"\n")

Корпус Reuters организован по частично совпадающим категориям. Мы также можем получить документы по названию категории. Чтобы узнать о полных операциях с корпусом NLTK, ознакомьтесь с этой замечательной статьей: Доступ к текстовым корпусам и лексическим ресурсам

Создать список игнорируемых слов

Чтобы сэкономить время и вычислительные ресурсы, лучше исключить такие стоп-слова, как «я», «я», «должен». NLTK предоставляет хороший список английских стоп-слов.

from nltk.corpus import stopwords
ignored_words = list(stopwords.words('english'))

И вы также можете расширить список своими собственными стоп-словами, которые не включены в список стоп-слов NLTK.

ignored_words.extend(
'''get see seeing seems back join 
excludes has have other that are likely like 
due since next 100 take based high day set ago still 
however long early much help sees would will say says said 
applying apply remark explain explaining
'''.split())

Создайте словарь ключевых слов - одно слово

Прежде чем использовать TF-IDF для извлечения ключевых слов, я создам свой собственный список словаря, включающий как одно слово (например, «Python»), так и два слова (например, «белый дом»).

Здесь я буду использовать CountVectorizer из scikit-learn, чтобы выполнить задание по извлечению одного слова.

from sklearn.feature_extraction.text import CountVectorizer
import pandas as pd
count_vec = CountVectorizer(
    ngram_range = (1,1)   #1
    ,stop_words = ignored_words
)
text_set     = [reuters.raw(fileid).lower() for fileid in reuters.fileids()] #2
tf_result    = count_vec.fit_transform(text_set)
tf_result_df = pd.DataFrame(tf_result.toarray()
                               ,columns=count_vec.get_feature_names()) #3
the_sum_s = tf_result_df.sum(axis=0) #4
the_sum_df = pd.DataFrame({ #5
    'keyword':the_sum_s.index
    ,'tf_sum':the_sum_s.values
})
the_sum_df = the_sum_df[
    the_sum_df['tf_sum']>2  #6
].sort_values(by=['tf_sum'],ascending=False)

Код # 1 указывает, что CountVectorizer будет считать только одно слово. иначе, 1грамм слова. Вы можете спросить, почему бы не использовать ngram_range = (1,2), а затем получить и одиночные, и биграммные слова одновременно? Это потому, что при захвате биграммы здесь будут встречаться такие фразы, как "they are", "I will" и "will be '. Это соединительные фразы, а не ключевые слова или ключевые фразы документа.

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

Код # 2, используя понимание Python, чтобы собрать всю статью Reuters в одну строку кода.

Код # 3, преобразуйте результат вектора счетчика в читаемый фрейм данных Pandas.

Код # 4, создайте список серий, включающий ключевые слова и его полное появление # в корпусе.

Код # 5, превратите Series в Dataframe для облегчения чтения и обработки данных.

Код # 6, возьмите слова, которые встречаются более 2 раз.

Если вы посмотрите на 10 лучших результатов, установленных the_sum_df[:10], вы увидите эти наиболее часто используемые слова:

Наиболее частые, но бессмысленные, мы можем легко пропорционально исключить их путем нарезки Python:

start_index     = int(len(the_sum_df)*0.01) # exclude the top 1%
my_word_df      = the_sum_df.iloc[start_index:]
my_word_df      = my_word_df[my_word_df['keyword'].str.len()>2]

А также удалите слова, состоящие менее чем из двух символов, такие как «vs», «lt» и т. Д.

Обратите внимание, что я использую .iloc вместо .loc. Поскольку исходный набор данных переупорядочен по значению TF (частота термина). iloc будет разрезать индекс индекса (или последовательность меток индекса). но loc будет разрезать метку индекса.

Создание словаря ключевых слов - фраза из 2 слов (фраза биграммы)

Чтобы составить список биграммных фраз, нам нужно учитывать не только частоту встречаемости вместе, но и их взаимосвязь с соседними словами.

Например, фраза they are, встречается вместе много раз, но they are может следовать только с ограниченными словами, такими как they are brothers, they are nice people, эти слова имеют высокую внутреннюю липкость, но низкую гибкость внешнего соединения.

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

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

NLTK предоставляет аналогичное решение для решения проблемы извлечения биграммных фраз.

from nltk.collocations import BigramAssocMeasures
from nltk.collocations import BigramCollocationFinder
from nltk.tokenize import word_tokenize
text_set_words  = [word_tokenize(reuters.raw(fileid).lower()) 
                   for fileid in reuters.fileids()] #1
bigram_measures = BigramAssocMeasures()
finder = BigramCollocationFinder.from_documents(text_set_words) #2
finder.apply_freq_filter(3) #3
finder.apply_word_filter(lambda w: 
                         len(w) < 3 
                         or len(w) > 15 
                         or w.lower() in ignored_words) #4
phrase_result = finder.nbest(bigram_measures.pmi, 20000) #5
colloc_strings = [w1+' '+w2 for w1,w2 in phrase_result] #6

Код # 1. В этом выражении понимания Python я использую word_tokenize для токенизации документа в список слов. Результат будет таким:

[
    ['word1','word2',...,'wordn'], 
    ['word1','word2',...,'wordn'],
    ...
    ['word1','word2',...,'wordn']
]

Код # 2, запустите объект поиска биграмм из списка токенизированных документов. есть еще одна функция from_words(), которая может обрабатывать список размеченных слов.

Код # 3, удалите кандидатов, у которых частота меньше 3.

Код # 4: удалите кандидатов, длина которых меньше 3 или больше 15, а также кандидатов из списка ignored_words.

Код # 5, используйте pmi функцию из BigramAssocMeasures, чтобы измерить вероятность словосочетания из двух слов. Вы можете узнать, как это работает, в разделе 5.4 Основы статической обработки естественного языка. А по этой ссылке перечислены все остальные функции измерения и источник.

Код # 6, преобразовать результат в более читаемый формат.

Заменив BigramAssocMeasures, BigramCollocationFinder на TrigramAssocMeasures и TrigramCollocationFinder, вы получите экстрактор фраз из 3 слов. В примере извлечения ключевых слов Reuters я пропущу фразу из трех слов. Я размещаю здесь образец кода на случай, если он вам понадобится.

from nltk.collocations import TrigramAssocMeasures
from nltk.collocations import TrigramCollocationFinder
from nltk.tokenize import word_tokenize
text_set_words  = [word_tokenize(reuters.raw(fileid).lower()) 
                   for fileid in reuters.fileids()]
trigram_measures = TrigramAssocMeasures()
finder = TrigramCollocationFinder.from_documents(text_set_words)
finder.apply_freq_filter(3)
finder.apply_word_filter(lambda w: 
                         len(w) < 3 
                         or len(w) > 15 
                         or w.lower() in ignored_words)
tri_phrase_result = finder.nbest(bigram_measures.pmi, 1000)
tri_colloc_strings = [w1+' '+w2+' '+w3 for w1,w2,w3 in tri_phrase_result] 
tri_colloc_strings[:10]

Волнительный момент, используйте TF-IDF для измерения веса ключевого слова.

Теперь давайте объединим отдельные слова и фразы из двух слов вместе, чтобы составить индивидуальный список словаря Reuters.

my_vocabulary = []
my_vocabulary.extend(my_word_df['keyword'].tolist()) 
my_vocabulary.extend(colloc_strings)

Давай заводим двигатель. Обратите внимание, что для запуска кода найдите машину с объемом ОЗУ не менее 16 ГБ. Расчет TF-IDF займет некоторое время и может занять большой кусок вашей памяти.

from sklearn.feature_extraction.text import TfidfVectorizer
vec          = TfidfVectorizer(
                    analyzer     ='word'
                    ,ngram_range =(1, 2)
                    ,vocabulary  =my_vocabulary)
text_set     = [reuters.raw(fileid) for fileid in reuters.fileids()]
tf_idf       = vec.fit_transform(text_set)
result_tfidf = pd.DataFrame(tf_idf.toarray()
                            , columns=vec.get_feature_names()) #1

После преобразования набора результатов в Dateframe в коде # 1 result_tfidf содержит значения TF-IDF всех ключевых слов:

Посмотрите результат

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

Выведите один из исходных документов, указав индекс fileid.

file_index= 202 # change number to check different articles
fileid = reuters.fileids()[file_index]
print(fileid,"\n"
        ,reuters.raw(fileid),"\n"
        ,reuters.categories(fileid),"\n")

Возвращает fileid, необработанное содержимое и его категорию. (хм, много лет назад США вели тарифную войну с Японией)

test/15223 
 WHITE HOUSE SAYS JAPANESE TARRIFFS LIKELY
  The White House said high U.S.
  Tariffs on Japanese electronic goods would likely be imposed as
  scheduled on April 17, despite an all-out effort by Japan to
  avoid them.
      Presidential spokesman Marlin Fitzwater made the remark one
  day before U.S. And Japanese officials are to meet under the
  emergency provisions of a July 1986 semiconductor pact to
  discuss trade and the punitive tariffs.
      Fitzwater said: "I would say Japan is applying the
  full-court press...They certainly are putting both feet forward
  in terms of explaining their position." But he added that "all
  indications are they (the tariffs) will take effect."

 ['trade']

Распечатайте 10 самых популярных ключевых слов из нашего только что приготовленного result_tfidf объекта фрейма данных.

test_tfidf_row = result_tfidf.loc[file_index]
keywords_df = pd.DataFrame({
    'keyword':test_tfidf_row.index,
    'tf-idf':test_tfidf_row.values
})
keywords_df = keywords_df[
    keywords_df['tf-idf'] >0
].sort_values(by=['tf-idf'],ascending=False)
keywords_df[:10]

10 самых популярных ключевых слов:

Похоже, white и house здесь дублируются с white house. Нам нужно удалить те отдельные слова, которые уже присутствовали во фразе из 2 слов.

bigram_words = [item.split() 
                    for item in keywords_df['keyword'].tolist() 
                    if len(item.split())==2]
bigram_words_set = set(subitem 
                        for item in bigram_words 
                        for subitem in item) 
keywords_df_new = keywords_df[~keywords_df['keyword'].isin(bigram_words_set)]

Приведенный выше код сначала создает слово set, которое содержит слова из двухсловной фразы. Затем отфильтрованы отдельные слова, которые уже используются во фразе из 2 слов пользователем ~xxxx.isin(xxxx).

Прочие соображения

Чем больше у вас текстовый корпус, тем лучше TF-IDF будет извлекать ключевые слова. Корпус Reuters содержит 10788 статей, и результаты показывают, что это работает. Я считаю, что это решение будет лучше работать для больших текстовых баз данных.

Приведенный выше код работает менее 2 минут на моем Macbook Air M1, что означает, что набор результатов ежедневного обновления работоспособен.

Если у вас есть данные размером в сотни ГБ или даже ТБ. вам может потребоваться переписать логику на C / C ++ или Go, а также можно использовать возможности графического процессора для повышения производительности.

Решение, описанное в этой статье, далеко от совершенства. Например, я не отфильтровал слова глагола и прилагательного. Основу решения можно расширить на другие языки.

Извлеченные ключевые слова

Позвольте распечатать еще раз окончательный результат.

keywords_df_new[:10]

tariffs получают наивысшее значение TF-IDF, а остальные ключевые слова выглядят хорошо, чтобы представить эту статью Reuters. Цель достигнута!