Обзор

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

В этой статье описывается способ использования методов машинного обучения для анализа большой коллекции новостных статей и выявления в них набора общих «тем». Набор данных, используемый для проекта, содержит более миллиона заголовков новостей, которые были опубликованы за 15 лет Австралийской радиовещательной корпорацией.

Конкретный метод машинного обучения, используемый в этой статье, называется «тематическое моделирование». Это способ просмотреть большую коллекцию документов и обнаружить в ней набор общих «тем». Задача состоит в том, как выделить высококачественные темы, значимые для человека, но при этом отдельные и отличные друг от друга.

Типичный подход, который использует специалист по данным для решения этой задачи, представляет собой двухэтапный процесс. Сначала преобразуйте текст в набор числовой информации, с которой может работать компьютер. Это называется предварительной обработкой, и результатом является преобразование большого объема текста в большую матрицу чисел, которая собирает наиболее важную информацию из этого текста. Затем вторым шагом является обучение модели машинного обучения, которая может идентифицировать набор различных шаблонов, представленных в этих числах. Эти шаблоны представляют собой кластеры информации — набор различных тем, содержащихся в документах.

Этот документ подробно описывает способ сделать это и структурирован следующим образом:

  1. Описание того, как собрать и предварительно обработать текст из датасета новостных заголовков — так, чтобы он трансформировался в многомерное векторное пространство чисел.
  2. Описание того, как обучить модель машинного обучения на этом векторном пространстве с использованием общего алгоритма тематического моделирования, называемого скрытым распределением Дирихле (LDA).
  3. Описание метрики под названием «Оценка согласованности», которая измеряет эффективность машинного обучения на основе того, насколько различны темы и насколько они значимы для человека.
  4. Использование этой метрики для улучшения модели машинного обучения путем оценки количества различных тем, которые могут быть представлены в наборе данных заголовков новостей (поскольку это неизвестно в начале), а также путем тестирования двух разных программных реализаций алгоритма LDA. .
  5. Наконец, появился способ использовать эту модель машинного обучения, чтобы предсказать, к какой теме будет принадлежать новый документ, который не является частью набора заголовков новостей, которые использовались для обучения модели машинного обучения.

Получить набор данных заголовков новостей

Набор данных представляет собой список из более чем миллиона заголовков новостей, опубликованных за 15 лет. Он был загружен в виде CSV-файла с Kaggle (https://www.kaggle.com/therohk/million-headlines). Заголовки были получены от Австралийской радиовещательной корпорации (http://www.abc.net.au/) с 19 февраля 2003 г. по 31 декабря 2017 г.

Этот набор данных также описывается как корпус или набор документов, и нам нужен способ манипулировать этим корпусом с помощью Python, чтобы мы могли построить статистическую модель тем, содержащихся в этом корпусе. Поэтому мы используем библиотеку анализа данных Python (pandas). Это пакет Python, который позволяет нам создать фрейм данных из CSV-файла, загруженного с Kaggle, с данными. Фрейм данных — это гибкая, выразительная структура данных, которая позволяет легко манипулировать «реляционными» или «помеченными» данными.

In [1]:

import pandas as pd
data = pd.read_csv('abcnews-date-text.csv', error_bad_lines=False);
data_text = data[['headline_text']]
data_text['index'] = data_text.index
documents = data_text

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

In [2]:

len(documents)

Выход[2]:

1103665

Теперь давайте быстро взглянем на данные, чтобы визуально проверить их. Это всегда хорошая идея перед анализом данных и построением модели машинного обучения. Если данные не являются чистыми, ни один из наших результатов не будет точным. Это может быть случай GIGO — или Garbage In / Garbage Out. Однако, глядя на несколько заголовков, кажется, что набор данных чистый.

In [3]:

documents[:5]

Выход[3]:

headline_textindex0aba принимает решение об отказе от лицензии на общественное вещание…01act Свидетели пожара должны быть осведомлены о диффамации12ag призывает к саммиту по защите инфраструктуры23Сотрудники Air NZ бастуют в Австралии с целью повышения заработной платы34Забастовка Air NZ затрагивает австралийских путешественников4

Предварительная обработка текста перед обучением тематической модели

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

  1. Сначала мы берем каждый заголовок и запускаем для него определение предложений, чтобы разбить текст на предложения, а затем мы разбиваем предложения на слова. Это называется токенизацией — это процесс разбиения контента на небольшие, пригодные для использования фрагменты текста. Наиболее распространенный подход к токенизации английского языка заключается в разделении строк на основе наличия пробелов, таких как пробелы и разрывы строк. Пунктуация также удалена, и все слова преобразованы в строчные буквы алфавита для согласованности.
  2. Мы будем удалять слова, содержащие менее 3 символов.
  3. Мы будем отфильтровывать стоп-слова, которые являются часто встречающимися словами, такими как «the», «and» и «a». Эти слова мало что дают при моделировании темы, поскольку алгоритмы не полагаются на структуру предложения.
  4. Выполним лемматизацию слов. Это означает, что слова от третьего лица заменяются на первое лицо, а глаголы в прошедшем/будущем времени заменяются на настоящее время.
  5. Мы выполним стемминг, который сводит слова к их корню или более простой форме, например. собаки на собака или с бег на бег. Подробнее о стемминге и лемматизации см. здесь: https://nlp.stanford.edu/IR-book/html/htmledition/stemming-and-lemmatization-1.html

В этом проекте мы будем использовать следующие библиотеки Python.

1) генсим

gensim — это библиотека Python для тематического моделирования, название которой происходит от создать подобное. Он предназначен для поиска короткого списка документов, наиболее похожих на данный документ. Он использует латентные семантические методы для выполнения неконтролируемого семантического моделирования из простого текста. Дизайнерские решения описаны в этой научной статье 2010 года: https://radimrehurek.com/gensim/lrec2010_final.pdf.

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

2) NLTK или «Инструментарий естественного языка»

NLTK — это библиотека, используемая для создания программ Python, которые могут работать с наборами данных на естественном языке. Он имеет встроенную поддержку более 50 различных инструментов для обработки естественного языка, включая предварительно обученные модели машинного обучения, а также набор библиотек обработки текста для классификации, токенизации, поиска корней, тегов, синтаксического анализа и семантических рассуждений.

3) НумПи

NumPy — это библиотека Python, которая добавляет структуры данных для обработки больших многомерных массивов и матриц. Он также предоставляет набор математических функций, которые могут работать с этими структурами данных.

In [4]:

import gensim
from gensim.utils import simple_preprocess
from gensim.parsing.preprocessing import STOPWORDS
from nltk.stem import WordNetLemmatizer, SnowballStemmer
from nltk.stem.porter import *
import numpy as np
np.random.seed(2018)
C:\Users\atanu\Anaconda3\lib\site-packages\gensim\utils.py:1212: UserWarning: detected Windows; aliasing chunkize to chunkize_serial
  warnings.warn("detected Windows; aliasing chunkize to chunkize_serial")

Далее мы будем использовать Natural Language Toolkit (NLTK) для загрузки WordNet, который описан в Википедии как «лексическая база данных для английского языка» или как комбинация словаря и тезауруса для английского языка.

In [5]:

import nltk
nltk.download('wordnet')
[nltk_data] Error loading wordnet: <urlopen error [Errno 11001]
[nltk_data]     getaddrinfo failed>

Выход[5]:

False

Использование стемминга для приведения слов к их корневым формам

Чтобы построить модель темы из нашего корпуса, нам нужно убедиться, что алгоритмы могут одинаково обрабатывать все варианты одного и того же слова. Например, статьи, содержащие слова «банк», «банки», «банковское дело», «банкир», «банковское дело» и т. д., скорее всего, посвящены одной и той же теме банковского дела. Таким образом, статистической модели нужно сказать, что все варианты одного и того же слова по существу одинаковы. Стемминг — это процесс сведения слова к корню или более простой форме, которая не обязательно является словом сама по себе. Цель состоит в том, чтобы улучшить производительность алгоритмов НЛП, организовав под одним заголовком все различные формы вариантов слова, которые имеют общее значение.

Существует множество различных алгоритмов стемминга, каждый из которых преследует свои цели. Некоторые из них агрессивны, сокращая слова до наименьшего возможного корня, в то время как другие более легки, предпочитая делать простые вещи, такие как удаление s или ing из слов. Распространенный алгоритм стемминга называется стеммер Портера, названный в честь Мартина Портера, который его разработал, но сейчас большинство людей используют улучшенную версию этого алгоритма, называемую стеммером Портера2. Набор инструментов для естественного языка (NTLK) включает в себя стеммер Porter2, но ошибочно называет его стеммером Snowball, хотя на самом деле Snowball — это фреймворк, разработанный Мартином Портером для описания алгоритмов стемминга для различных языков помимо английского. Наиболее агрессивный алгоритм стемминга называется стеммером Ланкастера. Он сокращает слова до корней, настолько коротких, что человек едва может их прочитать. Стеммер Lancaster удобен при работе с очень большими наборами текстовых данных, поскольку он уменьшает объем компьютерного оборудования, необходимого для создания стволов.

В этом проекте мы просто будем использовать алгоритм SnowballStemer, входящий в состав Natural Language Toolkit (NTLK), который задокументирован здесь: http://snowballstem.org. В следующем примере показано, как парадигматический модуль может преобразовать несколько слов-образцов в их основу, используя один из модульных тестов, поставляемых в NTLK.

In [6]:

stemmer = SnowballStemmer('english')
original_words = ['caresses', 'flies', 'dies', 'mules', 'denied','died', 'agreed', 'owned', 
           'humbled', 'sized','meeting', 'stating', 'siezing', 'itemization','sensational', 
           'traditional', 'reference', 'colonizer','plotted']
singles = [stemmer.stem(plural) for plural in original_words]
pd.DataFrame(data = {'original word': original_words, 'stemmed': singles})

Выход[6]:

Лемматизация использует словарный запас, чтобы лучше понять слова

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

Когда следует использовать стемминг или лемматизацию? Если важно, чтобы результатом редукции было словарное слово, то используйте лемматизацию. Stemming — это, по сути, грубый эвристический процесс, который обрезает концы слов в надежде, что они будут правильными в большинстве случаев. Лемматизация — это способ делать что-то правильно с использованием известного словаря (например, поиск в словаре). и анализ того, как слова используются в тексте. Если лемматизатор запускается на слове «пила», он попытается вернуть либо «видеть», либо «видеть» в зависимости от того, используется ли слово как глагол или как существительное. Но стеммер может возвращать только «s». Однако, если скорость важнее точности, используйте стеммер, так как лемматизатор должен искать в словаре, а стеммеры выполняют простые операции со строками.

Более подробную техническую информацию о различиях между лемматизацией и стеммингом см. здесь: https://nlp.stanford.edu/IR-book/html/htmledition/stemming-and-lemmatization-1.html.

Так же, как мы сделали выше со SnowballStemer, мы покажем, как алгоритм WordNetLemmatizer, включенный в набор средств естественного языка (NTLK), может преобразовать несколько слов-образцов в их сокращенную форму. Метод лемматизации NLTK основан на встроенной в WordNet функции преобразования.

In [7]:

print(WordNetLemmatizer().lemmatize('go', pos='v'))
print(WordNetLemmatizer().lemmatize('going', pos='v'))
print(WordNetLemmatizer().lemmatize('gone', pos='v'))
print(WordNetLemmatizer().lemmatize('went', pos='v'))
go
go
go
go

Объединение словарной лемматизации со стеммером

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

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

In [9]:

def lemmatize_stemming(text):
    return stemmer.stem(WordNetLemmatizer().lemmatize(text, pos='v'))
def preprocess(text):
    result = []
    for token in gensim.utils.simple_preprocess(text):
        if token not in gensim.parsing.preprocessing.STOPWORDS and len(token) > 3:
            result.append(lemmatize_stemming(token))
    return result

Мы выберем случайный заголовок для использования в качестве примера в этом проекте. Это 3000-й заголовок из набора данных. Первоначальный заголовок: «Британец делает вторую большую попытку гребли». Предварительно обработанная версия этого заголовка — «британец», «сделать», «второй», «ряд», «попытка».

In [10]:

doc_sample = documents[documents['index'] == 3000].values[0][0]
print('original document: ')
words = []
for word in doc_sample.split(' '):
    words.append(word)
print(words)
print('\n\n tokenized, lemmatized, and stemmed document: ')
print(preprocess(doc_sample))
original document: 
['briton', 'makes', 'second', 'big', 'rowing', 'attempt']

 tokenized, lemmatized, and stemmed document: 
['briton', 'make', 'second', 'row', 'attempt']

Теперь мы предварительно обрабатываем заголовки и сохраняем результаты после токенизации, стемминга и лемматизации в переменную «processed_docs».

И, как и выше, мы проведем быструю проверку работоспособности, визуально проверив обработанные заголовки. Это еще один пример того, как мы защищаемся от GIGO — или Garbage In/Garbage Out.

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

In [11]:

processed_docs = documents['headline_text'].map(preprocess)

In [12]:

processed_docs[:10]

Выход[12]:

0            [decid, communiti, broadcast, licenc]
1                               [wit, awar, defam]
2           [call, infrastructur, protect, summit]
3                      [staff, aust, strike, rise]
4             [strike, affect, australian, travel]
5               [ambiti, olsson, win, tripl, jump]
6           [antic, delight, record, break, barca]
7    [aussi, qualifi, stosur, wast, memphi, match]
8            [aust, address, secur, council, iraq]
9                         [australia, lock, timet]
Name: headline_text, dtype: object

Подготовка к обучению алгоритма тематического моделирования

Мы будем использовать машинное обучение для моделирования тем в нашем корпусе заголовков новостей. Однако алгоритмы машинного обучения не могут работать с текстом напрямую — они работают с числами. Поэтому нам нужен способ преобразовать наш корпус заголовков в числовой формат, понятный алгоритмам. Один из способов сделать это — использовать модель Bag of Words, извлеченную из корпуса. Это преобразует каждый заголовок (или строку в корпусе) в вектор чисел — словарь, описывающий появление слов в заголовке.

Этот метод построения модели набора слов из текста (или сокращенно BoW) также известен как извлечение признаков или кодирование признаков. Концептуально это двухэтапный процесс: 1) Сначала создайте словарь всех (или большинства) слов, встречающихся в корпусе 2) Затем сопоставьте каждый документ в корпусе (или заголовок в данном случае) с вектором, который измеряет, является ли каждый из известные слова присутствуют в документе или нет.

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

Итак, как описано здесь, мы сначала создадим словарь «известных слов», которые встречаются в обучающем наборе.

In [13]:

dictionary = gensim.corpora.Dictionary(processed_docs)

In [14]:

count = 0
for k, v in dictionary.iteritems():
    print(k, v)
    count += 1
    if count > 10:
        break
0 broadcast
1 communiti
2 decid
3 licenc
4 awar
5 defam
6 wit
7 call
8 infrastructur
9 protect
10 summit

В этом проекте используется неконтролируемое машинное обучение, потому что заголовки новостей не являются помеченными данными. Однако, несмотря на то, что используются неконтролируемые методы, мы можем применить некоторые человеческие суждения к процессу обучения. Мы будем отфильтровывать токены, которые не представляют темы в этом наборе данных — либо потому, что они появляются слишком редко, либо потому, что появляются слишком часто. И тогда мы будем хранить только самые частые токены, чтобы не иметь дело с длинным хвостом тем, которые могут не интересовать пользователей.

Для этого мы будем отфильтровывать токены, которые появляются менее чем в 15 заголовках или встречаются более чем в 50% заголовков (например, общеупотребительные слова, такие как «the», «and», …). И после фильтрации этих токенов мы оставим только 100 000 наиболее частых оставшихся токенов.

In [15]:

dictionary.filter_extremes(no_below=15, no_above=0.5, keep_n=100000)

Теперь мы используем набор слов gensim — для создания словаря, частота которого соответствует частоте появления каждого токена в наборе данных. Это сохраняется в переменной «bow_corpus».

После этого проверяем токены в нашем образце документа, выбранном ранее. Мы видим, что токены 316 («попытка»), 443 («вторая»), 709 («сделать»), 1171 («британец») и 3364 («строка») появляются ровно один раз в образце документа, то есть что мы ожидаем.

In [16]:

bow_corpus = [dictionary.doc2bow(doc) for doc in processed_docs]
bow_corpus[3000]

Вышли[16]:

[(316, 1), (443, 1), (709, 1), (1171, 1), (3364, 1)]

In [17]:

bow_doc_3000 = bow_corpus[3000]
for i in range(len(bow_doc_3000)):
    print("Word {} (\"{}\") appears {} time.".format(bow_doc_3000[i][0], 
                                                     dictionary[bow_doc_3000[i][0]], 
                                                     bow_doc_3000[i][1]))
Word 316 ("attempt") appears 1 time.
Word 443 ("second") appears 1 time.
Word 709 ("make") appears 1 time.
Word 1171 ("briton") appears 1 time.
Word 3364 ("row") appears 1 time.

Построение тематической модели с использованием алгоритма LDA

Доступно несколько алгоритмов тематического моделирования, например, латентный семантический анализ (LSA), иерархический процесс Дирихле (HDP) и латентное распределение Дирихле (LDA). Алгоритм LDA довольно популярен, поскольку он показал хорошие результаты на практике и поэтому получил широкое распространение. Здесь мы будем использовать алгоритм LDA.

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

  1. Корпус новостных заголовков
  2. Словарь общеупотребительных слов в корпусе
  3. Количество тем
  4. Кроме того, альфа и эта являются гиперпараметрами, влияющими на разреженность тем. Согласно документам Gensim, оба по умолчанию имеют значение 1.0/num_topics ранее.
  5. chunksize — это количество документов, которые будут использоваться в каждом тренировочном фрагменте.
  6. update_every определяет, как часто должны обновляться параметры модели.
  7. pass - общее количество тренировочных пропусков

В пакете Python Gensim есть простая в использовании реализация алгоритма LDA. Итак, мы начнем с этой реализации алгоритма, загрузив библиотеки python из gensim, а затем применив их к набору данных заголовков новостей.

In [18]:

from gensim import corpora, models
lda_model = gensim.models.LdaMulticore(bow_corpus, num_topics=10, id2word=dictionary, passes=2, workers=2)

In [19]:

for idx, topic in lda_model.print_topics(-1):
    print('Topic: {} \nWords: {}'.format(idx, topic))
Topic: 0 
Words: 0.029*"elect" + 0.019*"death" + 0.018*"say" + 0.017*"hospit" + 0.016*"tasmanian" + 0.015*"labor" + 0.013*"deal" + 0.013*"china" + 0.011*"polit" + 0.011*"talk"
Topic: 1 
Words: 0.019*"nation" + 0.018*"coast" + 0.016*"help" + 0.016*"countri" + 0.015*"state" + 0.015*"chang" + 0.014*"health" + 0.013*"tasmania" + 0.013*"hour" + 0.013*"indigen"
Topic: 2 
Words: 0.019*"canberra" + 0.018*"market" + 0.014*"rise" + 0.014*"west" + 0.014*"australian" + 0.013*"turnbul" + 0.013*"price" + 0.013*"share" + 0.011*"victoria" + 0.010*"bank"
Topic: 3 
Words: 0.063*"polic" + 0.024*"crash" + 0.019*"interview" + 0.019*"miss" + 0.018*"shoot" + 0.016*"arrest" + 0.015*"investig" + 0.013*"driver" + 0.012*"search" + 0.011*"offic"
Topic: 4 
Words: 0.029*"charg" + 0.027*"court" + 0.021*"murder" + 0.018*"woman" + 0.018*"face" + 0.016*"die" + 0.016*"alleg" + 0.015*"brisban" + 0.015*"live" + 0.015*"jail"
Topic: 5 
Words: 0.035*"australia" + 0.022*"melbourn" + 0.021*"world" + 0.017*"open" + 0.014*"final" + 0.013*"donald" + 0.012*"sydney" + 0.010*"leagu" + 0.010*"take" + 0.010*"road"
Topic: 6 
Words: 0.027*"south" + 0.025*"kill" + 0.015*"island" + 0.013*"fall" + 0.011*"attack" + 0.010*"forc" + 0.009*"shark" + 0.009*"east" + 0.007*"northern" + 0.007*"bring"
Topic: 7 
Words: 0.019*"council" + 0.015*"power" + 0.013*"farmer" + 0.011*"industri" + 0.011*"guilti" + 0.011*"christma" + 0.010*"region" + 0.009*"research" + 0.009*"look" + 0.009*"energi"
Topic: 8 
Words: 0.037*"trump" + 0.032*"australian" + 0.019*"queensland" + 0.014*"leav" + 0.014*"australia" + 0.012*"say" + 0.011*"show" + 0.011*"royal" + 0.010*"game" + 0.009*"meet"
Topic: 9 
Words: 0.035*"govern" + 0.021*"test" + 0.018*"rural" + 0.014*"break" + 0.012*"school" + 0.012*"news" + 0.011*"busi" + 0.010*"violenc" + 0.010*"say" + 0.009*"worker"

Визуализация результатов тематической модели

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

In [20]:

# 1. Wordcloud of Top N words in each topic
from matplotlib import pyplot as plt
from wordcloud import WordCloud, STOPWORDS
import matplotlib.colors as mcolors
cols = [color for name, color in mcolors.TABLEAU_COLORS.items()]  # more colors: 'mcolors.XKCD_COLORS'
cloud = WordCloud(stopwords=gensim.parsing.preprocessing.STOPWORDS,
                  background_color='white',
                  width=2500,
                  height=1800,
                  max_words=10,
                  colormap='tab10',
                  color_func=lambda *args, **kwargs: cols[i],
                  prefer_horizontal=1.0)
topics = lda_model.show_topics(formatted=False)
fig, axes = plt.subplots(5, 2, figsize=(10,10), sharex=True, sharey=True)
for i, ax in enumerate(axes.flatten()):
    fig.add_subplot(ax)
    topic_words = dict(topics[i][1])
    cloud.generate_from_frequencies(topic_words, max_font_size=300)
    plt.gca().imshow(cloud)
    plt.gca().set_title('Topic ' + str(i+1), fontdict=dict(size=16))
    plt.gca().axis('off')

plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.margins(x=0, y=0)
plt.tight_layout()
plt.show()
<Figure size 1000x1000 with 10 Axes>

In [21]:

fig

Вышли[21]:

Оценить, насколько хорошо тематическая модель может быть интерпретирована человеком

Нам нужен способ измерить, насколько хороша тематическая модель. Одна метрика для этого называется согласованностью тем, и она измеряет, насколько хорошо модель может быть интерпретирована человеком. Таким образом, оценка согласованности позволяет сравнивать различные тематические модели. И, к счастью, gensim предоставляет удобный способ расчета этого показателя. Ниже вы можете видеть, что показатель согласованности для нашей тематической модели составляет 0,22.

После расчета мы оценим способы дальнейшего улучшения тематической модели.

In [22]:

from gensim.models.coherencemodel import CoherenceModel
# Compute Perplexity
# print('\nPerplexity: ', lda_model.log_perplexity(corpus))  # a measure of how good the model is. lower the better.
# Compute Coherence Score
coherence_model_lda = CoherenceModel(model=lda_model, texts=processed_docs, dictionary=dictionary, coherence='c_v')
coherence_lda = coherence_model_lda.get_coherence()
print('\nCoherence Score: ', coherence_lda)
Coherence Score:  0.23043337647143503

Улучшение тематической модели за счет уменьшения влияния нерепрезентативных слов

Техника, использованная выше с «мешком слов», основана на анализе частоты появления слов в документах. Таким образом, используемый принцип заключается в том, что документ может быть представлен словами, которые встречаются в нем чаще всего. Это слова с высокой «частотой терминов» или tf — и математически это можно выразить так:

TF(t,doc) = (Количество раз, когда термин «t» появляется в документе «doc») / (Общее количество терминов в документе «doc»).

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

Однако, несмотря на то, что этот принцип кажется разумным, на практике он работает не очень хорошо, поскольку некоторые слова, такие как «это», «а», «и», … очень часто встречаются во всех документах, но не отражают содержание документа. документы. Таким образом, частота терминов сама по себе не очень полезная метрика, чтобы резюмировать документ или абзац, используя всего несколько ключевых слов.

Способ исправить это — также включить показатель того, насколько уникально слово, т. е. насколько редко это слово встречается во всех документах в корпусе. Эта метрика называется «обратной частотой документа» или idf — и математически это может быть выражено как:

IDF(t) = log_e(Общее количество документов/Количество документов с термином t в нем).

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

При вычислении TF все термины считаются одинаково важными. Однако известно, что некоторые термины, такие как «is», «of» и «that», могут встречаться много раз, но не имеют большого значения. Таким образом, нам нужно уменьшить количество частых терминов и увеличить количество редких. Таким образом, обычно используемой метрикой является произведение tf x idf для каждого слова в словаре, который мы создали из корпуса документов. Это известно как вес «tf-idf» (т. е. вес «термин частота-обратная частота документа»). Эта метрика может быть рассчитана для каждого слова, встречающегося в документе, путем умножения того, насколько часто это слово встречается в этом документе, на то, насколько это слово уникально во всем корпусе документов.

Слова с высокой оценкой tf-idf в документе часто встречаются в документе, а также предоставляют большую часть информации об этом конкретном документе. И именно поэтому вес tf-idf часто используется при поиске информации и анализе текста. Этот вес является статистической мерой, используемой для оценки того, насколько важно слово для документа в коллекции или корпусе. Важность возрастает пропорционально количеству раз, которое слово встречается в документе, но компенсируется частотой этого слова в корпусе.

ПРИМЕР:

  1. Рассмотрим документ, содержащий 100 слов, в котором слово «собака» встречается 7 раз.
  2. Тогда термин частота (т. е. tf) для «собаки» равен (7/100) = 0,07.
  3. Теперь предположим, что у нас есть миллион документов и слово «собака» встречается в тысяче из них.
  4. Затем обратная частота документа (т. е. idf) рассчитывается как log(1 000 000 / 1 000) = 3.
  5. Таким образом, вес Tf-idf является произведением этих величин: 0,07*3=0,21.

In [23]:

from gensim import corpora, models
tfidf = models.TfidfModel(bow_corpus)

In [24]:

corpus_tfidf = tfidf[bow_corpus]

In [25]:

from pprint import pprint
for doc in corpus_tfidf:
    pprint(doc)
    break
[(0, 0.5892908644709983),
 (1, 0.38929657403503015),
 (2, 0.4964985198530063),
 (3, 0.5046520328695662)]

Перестроение тематической модели с использованием весов TF-IDF

Теперь мы создадим другую модель LDA, используя веса TF-IDF.

In [26]:

lda_model_tfidf = gensim.models.LdaMulticore(corpus_tfidf, num_topics=10, id2word=dictionary, passes=2, workers=4)

In [27]:

for idx, topic in lda_model_tfidf.print_topics(-1):
    print('Topic: {} Word: {}'.format(idx, topic))
Topic: 0 Word: 0.020*"polic" + 0.019*"charg" + 0.016*"murder" + 0.013*"court" + 0.012*"alleg" + 0.011*"death" + 0.011*"jail" + 0.011*"woman" + 0.010*"arrest" + 0.009*"assault"
Topic: 1 Word: 0.014*"news" + 0.014*"crash" + 0.012*"rural" + 0.010*"north" + 0.008*"south" + 0.007*"korea" + 0.007*"ash" + 0.006*"west" + 0.006*"octob" + 0.006*"die"
Topic: 2 Word: 0.008*"grandstand" + 0.008*"marriag" + 0.007*"septemb" + 0.007*"novemb" + 0.007*"violenc" + 0.006*"malcolm" + 0.006*"domest" + 0.006*"andrew" + 0.005*"quiz" + 0.005*"say"
Topic: 3 Word: 0.009*"christma" + 0.006*"health" + 0.006*"elect" + 0.006*"rugbi" + 0.006*"farm" + 0.005*"stori" + 0.005*"mental" + 0.005*"august" + 0.005*"tasmanian" + 0.005*"indigen"
Topic: 4 Word: 0.018*"countri" + 0.017*"hour" + 0.010*"interview" + 0.009*"govern" + 0.006*"abbott" + 0.005*"wall" + 0.005*"cut" + 0.005*"asylum" + 0.005*"say" + 0.004*"union"
Topic: 5 Word: 0.022*"trump" + 0.007*"michael" + 0.007*"friday" + 0.007*"juli" + 0.006*"video" + 0.005*"anim" + 0.005*"care" + 0.005*"know" + 0.005*"steal" + 0.005*"updat"
Topic: 6 Word: 0.010*"weather" + 0.007*"plead" + 0.007*"cattl" + 0.007*"hobart" + 0.005*"histori" + 0.005*"wild" + 0.005*"warn" + 0.005*"coast" + 0.004*"queensland" + 0.004*"sydney"
Topic: 7 Word: 0.008*"final" + 0.007*"leagu" + 0.007*"sport" + 0.006*"peter" + 0.005*"decemb" + 0.005*"open" + 0.005*"rain" + 0.005*"world" + 0.005*"dairi" + 0.005*"murray"
Topic: 8 Word: 0.010*"donald" + 0.009*"australia" + 0.007*"kill" + 0.006*"wednesday" + 0.005*"syria" + 0.005*"islam" + 0.005*"zealand" + 0.005*"australian" + 0.005*"russia" + 0.005*"suicid"
Topic: 9 Word: 0.012*"market" + 0.011*"podcast" + 0.011*"drum" + 0.010*"turnbul" + 0.009*"share" + 0.006*"dollar" + 0.006*"australian" + 0.006*"tuesday" + 0.006*"mother" + 0.006*"price"

In [28]:

lda_model_tfidf.save('./saved-state/lda_model_tfidf')
gensim.corpora.MmCorpus.serialize('./saved-state/corpus_tfidf.mm', corpus_tfidf)

Визуализация результатов улучшенной тематической модели

Добавьте сюда второй раздел по визуализации данных — для улучшенной тематической модели. Это должно показать, как различать разные темы, используя слова в каждой теме и их соответствующие веса.

In [29]:

# 1. Wordcloud of Top N words in each topic
cols2 = [color for name, color in mcolors.TABLEAU_COLORS.items()]  # more colors: 'mcolors.XKCD_COLORS'
cloud2 = WordCloud(stopwords=gensim.parsing.preprocessing.STOPWORDS,
                  background_color='white',
                  width=2500,
                  height=1800,
                  max_words=10,
                  colormap='tab10',
                  color_func=lambda *args, **kwargs: cols2[i],
                  prefer_horizontal=1.0)
topics2 = lda_model_tfidf.show_topics(formatted=False)
fig2, axes2 = plt.subplots(5, 2, figsize=(10,10), sharex=True, sharey=True)
for i, ax in enumerate(axes2.flatten()):
    fig2.add_subplot(ax)
    topic_words = dict(topics2[i][1])
    cloud2.generate_from_frequencies(topic_words, max_font_size=300)
    plt.gca().imshow(cloud2)
    plt.gca().set_title('Topic ' + str(i+1), fontdict=dict(size=16))
    plt.gca().axis('off')
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.margins(x=0, y=0)
plt.tight_layout()
plt.show()

Насколько хороша эта улучшенная тематическая модель?

Оценить, насколько хорошо тематическая модель может быть интерпретирована человеком

Теперь мы можем пересчитать показатель связности темы. Ниже вы можете видеть, что использование весов TF-IDF для уменьшения влияния нерепрезентативных слов увеличило показатель согласованности для нашей тематической модели с 0,22 до 0,37.

In [30]:

%env PYTHONWARNINGS=ignore::DeprecationWarning
from gensim.models.coherencemodel import CoherenceModel
# Compute Perplexity
# print('\nPerplexity: ', lda_model_tfidf.log_perplexity(corpus_tfidf))  # a measure of how good the model is. lower the better.
# Compute Coherence Score
coherence_model_lda_tfidf = CoherenceModel(model=lda_model_tfidf, texts=processed_docs, dictionary=dictionary, coherence='c_v')
coherence_lda_tfidf = coherence_model_lda_tfidf.get_coherence()
print('\nCoherence Score: ', coherence_lda_tfidf)
env: PYTHONWARNINGS=ignore::DeprecationWarning
Coherence Score:  0.35672040693123513

In [31]:

import os
from gensim.models.wrappers import LdaMallet
os.environ.update({'MALLET_HOME':r'C:/mallet-2.0.8/'})
# os.environ.update({'PATH':r'C:\Program Files (x86)\Common Files\Oracle\Java\javapath;C:\WINDOWS\system32;C:\WINDOWS;C:\WINDOWS\System32\Wbem;C:\WINDOWS\System32\WindowsPowerShell\v1.0\;C:\WINDOWS\System32\OpenSSH\;C:\Program Files\Git\cmd;C:\Users\atanu\AppData\Local\Microsoft\WindowsApps;;C:\Program Files\Microsoft VS Code\bin'})
mallet_path = 'C:/mallet-2.0.8/bin/mallet'
lda_model_mallet = gensim.models.wrappers.LdaMallet(mallet_path, corpus=bow_corpus, num_topics=10, id2word=dictionary)

In [32]:

# Show Topics
pprint(lda_model_mallet.show_topics(formatted=False))
[(0,
  [('open', 0.02165142660143809),
   ('world', 0.01864308697507962),
   ('test', 0.017015792757001675),
   ('win', 0.016710033818169878),
   ('final', 0.015936402140722988),
   ('lead', 0.015010917030567686),
   ('australia', 0.013147634369767213),
   ('aussi', 0.011087352661128804),
   ('play', 0.009150195357389106),
   ('live', 0.00903527924615031)]),
 (1,
  [('plan', 0.0466766554293087),
   ('council', 0.03882715278644858),
   ('water', 0.025979106838140427),
   ('rise', 0.019120156490614725),
   ('coast', 0.01775710658842136),
   ('fear', 0.017201481666458567),
   ('concern', 0.01568027635576643),
   ('price', 0.015592874682648687),
   ('industri', 0.014375494235651558),
   ('green', 0.013106088983227203)]),
 (2,
  [('sydney', 0.023464554015781805),
   ('hous', 0.021793019925947885),
   ('home', 0.019995332319900087),
   ('power', 0.015914266070381048),
   ('work', 0.01573975370628518),
   ('melbourn', 0.014280578035411292),
   ('forc', 0.012871863771022958),
   ('worker', 0.012260019217385636),
   ('adelaid', 0.012165404080225225),
   ('secur', 0.01200560962635431)]),
 (3,
  [('govt', 0.03658498883963075),
   ('urg', 0.027530971828054057),
   ('fund', 0.027170295074804358),
   ('chang', 0.02241760597198263),
   ('health', 0.02138916197271635),
   ('hospit', 0.020261789492558722),
   ('call', 0.019703255777526334),
   ('seek', 0.018180169373803325),
   ('boost', 0.01775147928994083),
   ('govern', 0.017648428789012342)]),
 (4,
  [('charg', 0.040152565591961994),
   ('court', 0.03486232037941531),
   ('face', 0.02777743078543798),
   ('murder', 0.02325750856724087),
   ('jail', 0.021312963495017885),
   ('claim', 0.021006916251316836),
   ('accus', 0.019532892791858728),
   ('drug', 0.01827955646051158),
   ('trial', 0.014663202295146132),
   ('case', 0.014223909992962995)]),
 (5,
  [('interview', 0.03343125359218341),
   ('year', 0.023383783553653916),
   ('australian', 0.015496945313664133),
   ('chief', 0.009962321987355515),
   ('sign', 0.008874555633607936),
   ('return', 0.008719160440215425),
   ('give', 0.008612725376247951),
   ('award', 0.00817847031526066),
   ('time', 0.007754858760670115),
   ('life', 0.007469612789237286)]),
 (6,
  [('warn', 0.027296063290340226),
   ('flood', 0.017095545689818418),
   ('farmer', 0.01690197151093063),
   ('south', 0.01675258274244114),
   ('north', 0.014469669871862309),
   ('west', 0.011980558419424747),
   ('queensland', 0.011507143308014391),
   ('farm', 0.011117890883076988),
   ('storm', 0.010099522376754266),
   ('rain', 0.00972079028762598)]),
 (7,
  [('report', 0.03022595313643609),
   ('nation', 0.019296385048555704),
   ('rural', 0.01573926087782228),
   ('record', 0.014403472197030965),
   ('break', 0.013249060573535594),
   ('countri', 0.012871369524813448),
   ('hour', 0.01114722055663552),
   ('australia', 0.010961575803873786),
   ('news', 0.01050920008450037),
   ('student', 0.010261673747484728)]),
 (8,
  [('polic', 0.07450815810533636),
   ('kill', 0.030307705917326535),
   ('death', 0.027697029918881833),
   ('crash', 0.02701195891219776),
   ('attack', 0.024343679345623467),
   ('miss', 0.02051509933529597),
   ('woman', 0.018523661694244374),
   ('road', 0.016270950726319275),
   ('die', 0.015947959290735496),
   ('investig', 0.015820408532734257)]),
 (9,
  [('market', 0.01967509776187703),
   ('elect', 0.018847241867043847),
   ('labor', 0.014676761793826442),
   ('busi', 0.014601880356102838),
   ('deal', 0.014187952408686247),
   ('meet', 0.013645061985190115),
   ('talk', 0.01209543223229886),
   ('fall', 0.012080871952741493),
   ('share', 0.011573342208170396),
   ('leader', 0.010934769947582993)])]

In [33]:

# Compute Coherence Score
coherence_model_lda_mallet = CoherenceModel(model=lda_model_mallet, texts=processed_docs, dictionary=dictionary, coherence='c_v')
coherence_lda_mallet = coherence_model_lda_mallet.get_coherence()
print('\nCoherence Score: ', coherence_lda_mallet)
Coherence Score:  0.26195693528592806

In [34]:

# Train model with both Mallet and TF-IDF
lda_model_mallet_tfidf = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus_tfidf, num_topics=10, id2word=dictionary)
# Compute Coherence Score for model trained using both Mallet and TF-IDF
coherence_model_lda_mallet_tfidf = CoherenceModel(model=lda_model_mallet_tfidf, texts=processed_docs, dictionary=dictionary, coherence='c_v')
coherence_lda_mallet_tfidf = coherence_model_lda_mallet_tfidf.get_coherence()
print('\nCoherence Score: ', coherence_lda_mallet_tfidf)
Coherence Score:  0.4492683941571956

Визуализация перекрытия между моделями

Интерактивным инструментом для изучения созданных тем и связанных с ними ключевых слов является пакет pyLDAvis, который поставляется с интерактивной диаграммой.

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

Расстояние между центрами кругов представляет сходство между темами. Таким образом, два круга, расположенные рядом, представляют похожие темы.

Хорошая тематическая модель будет иметь довольно большие неперекрывающиеся кружки, разбросанные по всей диаграмме, а не сгруппированные в одном квадранте. Модель со слишком большим количеством тем, как правило, будет иметь много перекрытий, пузырьков небольшого размера, сгруппированных в одной области диаграммы.

Для каждой темы гистограмма справа перечисляла 30 наиболее релевантных терминов. Поэтому, если вы наведете курсор на один из пузырьков, слова и полосы с правой стороны обновятся. Эти слова являются репрезентативными ключевыми словами, формирующими выбранную тему.

In [35]:

# Visualize the topics
import pyLDAvis
import pyLDAvis.gensim
import gensim
# lda_model_tfidf = gensim.models.LdaMulticore.load('lda_model_tfidf')
pyLDAvis.enable_notebook()
vis_tfidf = pyLDAvis.gensim.prepare(lda_model_tfidf, corpus_tfidf, dictionary)
vis_tfidf
C:\Users\atanu\Anaconda3\lib\site-packages\pyLDAvis\_prepare.py:257: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.
To accept the future behavior, pass 'sort=False'.
To retain the current behavior and silence the warning, pass 'sort=True'.
  return pd.concat([default_term_info] + list(topic_dfs))

Вышли[35]:

In [36]:

# import importlib
# importlib.reload(gensim.models.wrappers)
# from gensim.models.wrappers import LdaMallet
lda_mallet2gensim = gensim.models.wrappers.ldamallet.malletmodel2ldamodel(lda_model_mallet_tfidf, iterations=1000)
vis_mallet_tfidf = pyLDAvis.gensim.prepare(lda_mallet2gensim, corpus_tfidf, dictionary)
vis_mallet_tfidf
C:\Users\atanu\Anaconda3\lib\site-packages\pyLDAvis\_prepare.py:223: RuntimeWarning: divide by zero encountered in log
  kernel = (topic_given_term * np.log((topic_given_term.T / topic_proportion).T))
C:\Users\atanu\Anaconda3\lib\site-packages\pyLDAvis\_prepare.py:240: RuntimeWarning: divide by zero encountered in log
  log_lift = np.log(topic_term_dists / term_proportion)
C:\Users\atanu\Anaconda3\lib\site-packages\pyLDAvis\_prepare.py:241: RuntimeWarning: divide by zero encountered in log
  log_ttd = np.log(topic_term_dists)
C:\Users\atanu\Anaconda3\lib\site-packages\pyLDAvis\_prepare.py:257: FutureWarning: Sorting because non-concatenation axis is not aligned. A future version
of pandas will change to not sort by default.
To accept the future behavior, pass 'sort=False'.
To retain the current behavior and silence the warning, pass 'sort=True'.
  return pd.concat([default_term_info] + list(topic_dfs))

Вышел[36]:

In [37]:

# TEST ! TEST !
print("Mallet Model topics")
for topic in lda_model_mallet_tfidf.show_topics(num_topics=5, num_words=10):
    print(topic)
    
print("Mallet Model converted to gensim topics")
for topic in lda_mallet2gensim.show_topics(num_topics=5, num_words=10):
    print(topic)
Mallet Model topics
(7, '0.348*"closer" + 0.019*"chang" + 0.017*"futur" + 0.017*"shoot" + 0.013*"test" + 0.010*"mine" + 0.010*"green" + 0.009*"stab" + 0.008*"expand" + 0.008*"aussi"')
(5, '0.302*"sport" + 0.015*"year" + 0.013*"attack" + 0.013*"drum" + 0.012*"council" + 0.011*"control" + 0.010*"appeal" + 0.010*"sell" + 0.010*"interview" + 0.008*"reform"')
(6, '0.197*"interview" + 0.047*"countrywid" + 0.030*"miss" + 0.017*"hous" + 0.015*"power" + 0.014*"rise" + 0.014*"entertain" + 0.014*"court" + 0.014*"harvest" + 0.011*"investig"')
(3, '0.185*"weather" + 0.028*"countrywid" + 0.015*"station" + 0.015*"plan" + 0.013*"road" + 0.013*"world" + 0.011*"dead" + 0.010*"return" + 0.010*"approv" + 0.009*"clubhous"')
(0, '0.172*"weather" + 0.042*"grandstand" + 0.029*"sale" + 0.020*"review" + 0.017*"drought" + 0.017*"job" + 0.014*"speak" + 0.014*"elect" + 0.011*"win" + 0.010*"mayor"')
Mallet Model converted to gensim topics
(1, '0.106*"busi" + 0.037*"interview" + 0.022*"report" + 0.020*"bell" + 0.018*"crash" + 0.018*"hospit" + 0.016*"media" + 0.015*"reax" + 0.014*"debat" + 0.012*"fire"')
(4, '0.147*"rural" + 0.047*"closer" + 0.021*"meet" + 0.020*"budget" + 0.018*"warn" + 0.017*"arrest" + 0.017*"school" + 0.015*"farmer" + 0.015*"flood" + 0.014*"polic"')
(9, '0.273*"closer" + 0.042*"news" + 0.024*"charg" + 0.015*"death" + 0.013*"halv" + 0.012*"fruit" + 0.010*"tour" + 0.009*"anzac" + 0.008*"grain" + 0.008*"fish"')
(8, '0.166*"sport" + 0.036*"open" + 0.026*"water" + 0.022*"die" + 0.016*"season" + 0.014*"close" + 0.013*"gold" + 0.013*"farm" + 0.013*"market" + 0.012*"cut"')
(2, '0.180*"entertain" + 0.116*"weather" + 0.027*"corner" + 0.022*"australia" + 0.020*"china" + 0.015*"rain" + 0.014*"bodi" + 0.011*"result" + 0.010*"assault" + 0.010*"wrap"')

In [38]:

lda_model_mallet_4topics = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus_tfidf, num_topics=4, id2word=dictionary)
# Compute Coherence Score for model trained using both Mallet and TF-IDF
coherence_model_lda_mallet_tfidf_4topics = CoherenceModel(model=lda_model_mallet_4topics, texts=processed_docs, dictionary=dictionary, coherence='c_v')
coherence_lda_mallet_tfidf_4topics = coherence_model_lda_mallet_tfidf_4topics.get_coherence()
print('\nCoherence Score: ', coherence_lda_mallet_tfidf_4topics)
Coherence Score:  0.4473196035866921

In [39]:

# 1. Wordcloud of Top N words in each topic
cols2 = [color for name, color in mcolors.TABLEAU_COLORS.items()]  # more colors: 'mcolors.XKCD_COLORS'
cloud3 = WordCloud(stopwords=gensim.parsing.preprocessing.STOPWORDS,
                  background_color='white',
                  width=2500,
                  height=1800,
                  max_words=10,
                  colormap='tab10',
                  color_func=lambda *args, **kwargs: cols[i],
                  prefer_horizontal=1.0)
topics2 = lda_model_mallet_4topics.show_topics(formatted=False)
fig2, axes2 = plt.subplots(2, 2, figsize=(10,10), sharex=True, sharey=True)
for i, ax in enumerate(axes2.flatten()):
    fig2.add_subplot(ax)
    topic_words = dict(topics2[i][1])
    cloud3.generate_from_frequencies(topic_words, max_font_size=300)
    plt.gca().imshow(cloud3)
    plt.gca().set_title('Topic ' + str(i+1), fontdict=dict(size=16))
    plt.gca().axis('off')
plt.subplots_adjust(wspace=0, hspace=0)
plt.axis('off')
plt.margins(x=0, y=0)
plt.tight_layout()
plt.show()

In [40]:

def compute_coherence_values(dictionary, corpus, texts, limit, start=2, step=3):
    """
    Compute c_v coherence for various number of topics
    Parameters:
    ----------
    dictionary : Gensim dictionary
    corpus : Gensim corpus
    texts : List of input texts
    limit : Max num of topics
    Returns:
    -------
    model_list : List of LDA topic models
    coherence_values : Coherence values corresponding to the LDA model with respective number of topics
    """
    coherence_values = []
    model_list = []
    for num_topics in range(start, limit, step):
        model = gensim.models.wrappers.LdaMallet(mallet_path, corpus=corpus, num_topics=num_topics, id2word=dictionary)
        model_list.append(model)
        coherencemodel = CoherenceModel(model=model, texts=texts, dictionary=dictionary, coherence='c_v')
        coherence_score = coherencemodel.get_coherence()
        coherence_values.append(coherence_score)
        print("Num Topics = ", num_topics, " has Coherence Value of ", coherence_score)
    return model_list, coherence_values
# Can take a long time to run.
model_list, coherence_values = compute_coherence_values(dictionary=dictionary, corpus=corpus_tfidf, texts=processed_docs, start=2, limit=20, step=2)
Num Topics =  2  has Coherence Value of  0.609790471889881
Num Topics =  4  has Coherence Value of  0.5071486390468577
Num Topics =  6  has Coherence Value of  0.4800583185055712
Num Topics =  8  has Coherence Value of  0.4725015442584691
Num Topics =  10  has Coherence Value of  0.46992324128298135
Num Topics =  12  has Coherence Value of  0.43937880042165545
Num Topics =  14  has Coherence Value of  0.44909164638296806
Num Topics =  16  has Coherence Value of  0.468885293810879
Num Topics =  18  has Coherence Value of  0.4484850030764603

In [41]:

# Show graph
limit=20; start=2; step=2;
x = range(start, limit, step)
plt.plot(x, coherence_values)
plt.xlabel("Num Topics")
plt.ylabel("Coherence score")
plt.legend(("coherence_values"), loc='best')
plt.show()

In [42]:

# Print the coherence scores
for m, cv in zip(x, coherence_values):
    print("Num Topics =", m, " has Coherence Value of", round(cv, 4))
Num Topics = 2  has Coherence Value of 0.6098
Num Topics = 4  has Coherence Value of 0.5071
Num Topics = 6  has Coherence Value of 0.4801
Num Topics = 8  has Coherence Value of 0.4725
Num Topics = 10  has Coherence Value of 0.4699
Num Topics = 12  has Coherence Value of 0.4394
Num Topics = 14  has Coherence Value of 0.4491
Num Topics = 16  has Coherence Value of 0.4689
Num Topics = 18  has Coherence Value of 0.4485

Проверка наших тематических моделей — насколько хорошо они могут классифицировать статьи по одной из тем?

Оценка производительности путем классификации образца документа с использованием модели LDA Bag of Words

In [43]:

processed_docs[3000]

Вышел[43]:

['briton', 'make', 'second', 'row', 'attempt']

In [44]:

for index, score in sorted(lda_model[bow_corpus[3000]], key=lambda tup: -1*tup[1]):
    print("\nScore: {}\t \nTopic: {}".format(score, lda_model.print_topic(index, 10)))
Score: 0.300295352935791	 
Topic: 0.035*"australia" + 0.022*"melbourn" + 0.021*"world" + 0.017*"open" + 0.014*"final" + 0.013*"donald" + 0.012*"sydney" + 0.010*"leagu" + 0.010*"take" + 0.010*"road"
Score: 0.21737168729305267	 
Topic: 0.035*"govern" + 0.021*"test" + 0.018*"rural" + 0.014*"break" + 0.012*"school" + 0.012*"news" + 0.011*"busi" + 0.010*"violenc" + 0.010*"say" + 0.009*"worker"
Score: 0.19899491965770721	 
Topic: 0.029*"charg" + 0.027*"court" + 0.021*"murder" + 0.018*"woman" + 0.018*"face" + 0.016*"die" + 0.016*"alleg" + 0.015*"brisban" + 0.015*"live" + 0.015*"jail"
Score: 0.18333320319652557	 
Topic: 0.027*"south" + 0.025*"kill" + 0.015*"island" + 0.013*"fall" + 0.011*"attack" + 0.010*"forc" + 0.009*"shark" + 0.009*"east" + 0.007*"northern" + 0.007*"bring"
Score: 0.01666867546737194	 
Topic: 0.019*"council" + 0.015*"power" + 0.013*"farmer" + 0.011*"industri" + 0.011*"guilti" + 0.011*"christma" + 0.010*"region" + 0.009*"research" + 0.009*"look" + 0.009*"energi"
Score: 0.016668247058987617	 
Topic: 0.037*"trump" + 0.032*"australian" + 0.019*"queensland" + 0.014*"leav" + 0.014*"australia" + 0.012*"say" + 0.011*"show" + 0.011*"royal" + 0.010*"game" + 0.009*"meet"
Score: 0.016667695716023445	 
Topic: 0.019*"canberra" + 0.018*"market" + 0.014*"rise" + 0.014*"west" + 0.014*"australian" + 0.013*"turnbul" + 0.013*"price" + 0.013*"share" + 0.011*"victoria" + 0.010*"bank"
Score: 0.016666876152157784	 
Topic: 0.063*"polic" + 0.024*"crash" + 0.019*"interview" + 0.019*"miss" + 0.018*"shoot" + 0.016*"arrest" + 0.015*"investig" + 0.013*"driver" + 0.012*"search" + 0.011*"offic"
Score: 0.01666666567325592	 
Topic: 0.029*"elect" + 0.019*"death" + 0.018*"say" + 0.017*"hospit" + 0.016*"tasmanian" + 0.015*"labor" + 0.013*"deal" + 0.013*"china" + 0.011*"polit" + 0.011*"talk"
Score: 0.01666666567325592	 
Topic: 0.019*"nation" + 0.018*"coast" + 0.016*"help" + 0.016*"countri" + 0.015*"state" + 0.015*"chang" + 0.014*"health" + 0.013*"tasmania" + 0.013*"hour" + 0.013*"indigen"

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

Оценка производительности путем классификации образца документа с использованием модели LDA TF-IDF

In [45]:

for index, score in sorted(lda_model_tfidf[bow_corpus[3000]], key=lambda tup: -1*tup[1]):
    print("\nScore: {}\t \nTopic: {}".format(score, lda_model_tfidf.print_topic(index, 10)))
Score: 0.6209820508956909	 
Topic: 0.020*"polic" + 0.019*"charg" + 0.016*"murder" + 0.013*"court" + 0.012*"alleg" + 0.011*"death" + 0.011*"jail" + 0.011*"woman" + 0.010*"arrest" + 0.009*"assault"
Score: 0.24565459787845612	 
Topic: 0.018*"countri" + 0.017*"hour" + 0.010*"interview" + 0.009*"govern" + 0.006*"abbott" + 0.005*"wall" + 0.005*"cut" + 0.005*"asylum" + 0.005*"say" + 0.004*"union"
Score: 0.016671421006321907	 
Topic: 0.008*"final" + 0.007*"leagu" + 0.007*"sport" + 0.006*"peter" + 0.005*"decemb" + 0.005*"open" + 0.005*"rain" + 0.005*"world" + 0.005*"dairi" + 0.005*"murray"
Score: 0.016671055927872658	 
Topic: 0.010*"donald" + 0.009*"australia" + 0.007*"kill" + 0.006*"wednesday" + 0.005*"syria" + 0.005*"islam" + 0.005*"zealand" + 0.005*"australian" + 0.005*"russia" + 0.005*"suicid"
Score: 0.01667081192135811	 
Topic: 0.012*"market" + 0.011*"podcast" + 0.011*"drum" + 0.010*"turnbul" + 0.009*"share" + 0.006*"dollar" + 0.006*"australian" + 0.006*"tuesday" + 0.006*"mother" + 0.006*"price"
Score: 0.01667075976729393	 
Topic: 0.010*"weather" + 0.007*"plead" + 0.007*"cattl" + 0.007*"hobart" + 0.005*"histori" + 0.005*"wild" + 0.005*"warn" + 0.005*"coast" + 0.004*"queensland" + 0.004*"sydney"
Score: 0.016670577228069305	 
Topic: 0.009*"christma" + 0.006*"health" + 0.006*"elect" + 0.006*"rugbi" + 0.006*"farm" + 0.005*"stori" + 0.005*"mental" + 0.005*"august" + 0.005*"tasmanian" + 0.005*"indigen"
Score: 0.016670558601617813	 
Topic: 0.014*"news" + 0.014*"crash" + 0.012*"rural" + 0.010*"north" + 0.008*"south" + 0.007*"korea" + 0.007*"ash" + 0.006*"west" + 0.006*"octob" + 0.006*"die"
Score: 0.016669446602463722	 
Topic: 0.022*"trump" + 0.007*"michael" + 0.007*"friday" + 0.007*"juli" + 0.006*"video" + 0.005*"anim" + 0.005*"care" + 0.005*"know" + 0.005*"steal" + 0.005*"updat"
Score: 0.01666872762143612	 
Topic: 0.008*"grandstand" + 0.008*"marriag" + 0.007*"septemb" + 0.007*"novemb" + 0.007*"violenc" + 0.006*"malcolm" + 0.006*"domest" + 0.006*"andrew" + 0.005*"quiz" + 0.005*"say"

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

Оценка производительности путем классификации образца документа с использованием модели из 4 тем, обученной с помощью Mallet и TF-IDF

In [46]:

# for index, score in sorted(lda_model_mallet_4topics[corpus_tfidf[3000]], key=lambda tup: -1*tup[1]):
#     print("\nScore: {}\t \nTopic: {}".format(score, lda_model_mallet_4topics.print_topic(index, 4)))

Тестирование модели на невидимом документе

In [47]:

unseen_document = 'First-time fisherman calls Border Force after becoming stranded in crocodile-infested river'
bow_vector = dictionary.doc2bow(preprocess(unseen_document))
for index, score in sorted(lda_model_tfidf[bow_vector], key=lambda tup: -1*tup[1]):
    print("Score: {}\t Topic: {}".format(score, lda_model.print_topic(index, 5)))
Score: 0.4068733751773834	 Topic: 0.019*"nation" + 0.018*"coast" + 0.016*"help" + 0.016*"countri" + 0.015*"state"
Score: 0.31188517808914185	 Topic: 0.035*"govern" + 0.021*"test" + 0.018*"rural" + 0.014*"break" + 0.012*"school"
Score: 0.21122941374778748	 Topic: 0.037*"trump" + 0.032*"australian" + 0.019*"queensland" + 0.014*"leav" + 0.014*"australia"
Score: 0.01000365149229765	 Topic: 0.027*"south" + 0.025*"kill" + 0.015*"island" + 0.013*"fall" + 0.011*"attack"
Score: 0.010001669637858868	 Topic: 0.019*"canberra" + 0.018*"market" + 0.014*"rise" + 0.014*"west" + 0.014*"australian"
Score: 0.010001661255955696	 Topic: 0.029*"charg" + 0.027*"court" + 0.021*"murder" + 0.018*"woman" + 0.018*"face"
Score: 0.010001637041568756	 Topic: 0.019*"council" + 0.015*"power" + 0.013*"farmer" + 0.011*"industri" + 0.011*"guilti"
Score: 0.010001281276345253	 Topic: 0.063*"polic" + 0.024*"crash" + 0.019*"interview" + 0.019*"miss" + 0.018*"shoot"
Score: 0.010001139715313911	 Topic: 0.035*"australia" + 0.022*"melbourn" + 0.021*"world" + 0.017*"open" + 0.014*"final"
Score: 0.010000986978411674	 Topic: 0.029*"elect" + 0.019*"death" + 0.018*"say" + 0.017*"hospit" + 0.016*"tasmanian"

Использованная литература"¶"

?) Модель векторного пространства, Википедия: https://en.wikipedia.org/wiki/Vector_space_model

?) https://textminingonline.com/dive-into-nltk-part-iv-stemming-and-lemmatization

?) Wordnet: https://wordnet.princeton.edu/

?) Нежное введение в модель мешок слов: https://machinelearningmastery.com/gentle-introduction-bag-words-model/