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

В этом посте мы рассмотрим два разных подхода к тематическому моделированию и сравним их результаты. Это LDA (скрытый анализ Дерилихта) и NMF (факторизация неотрицательной матрицы). Давайте поговорим о каждом из них, прежде чем перейти к коду. Мы рассмотрим их определения и некоторую базовую математику, описывающую, как они работают.

LDA

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

Мы рассмотрим три небольших документа, в которых будут две важные темы: еда и домашние животные.

Документ 1 в основном посвящен еде (Тема A), Документ 2 - домашним животным (Тема B), а Документ 3 поровну разделен между A и B с одним неклассифицированным словом Fish. Используя существующую информацию, мы получим тему, к которой следует отнести Fish в Документе 3.

Сначала мы посчитаем, какова вероятность того, что слово появится в разных темах:

P («Рыба» | тема A) = 0,75.

P («Рыба» | тема B) = 0,25.

Теперь нам нужна вероятность тем в документе со словом в нем, которое будет:

P (тема A | Документ 3) = P (тема B | Документ 3) = 0,5

потому что они разделены поровну.

Взвешивая выводы из обеих вероятностей, мы назначим слово «Рыба» в Документе 3 теме A.

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

NMF

Неотрицательная матричная факторизация - это линейно-алгебраическая модель, которая преобразует многомерные векторы в низкоразмерное представление. Подобно анализу главных компонентов (PCA), NMF использует тот факт, что векторы неотрицательны. Разлагая их на меньшую размерную форму, NMF заставляет коэффициенты также быть неотрицательными.

Учитывая исходную матрицу A, мы можем получить две матрицы W и H, такие что A = WH. NMF обладает свойством кластеризации, так что W и H представляют следующую информацию об A:

A (Матрица документов и слов) - входные данные, содержащие, какие слова встречаются в каких документах.

W (Базисные векторы) - темы (кластеры), обнаруженные из документов.

H (матрица коэффициентов) - веса членства для тем в каждом документе.

Мы вычисляем W и H, оптимизируя целевую функцию (например, алгоритм EM), итеративно обновляя W и H до сходимости.

В этой целевой функции мы измеряем ошибку восстановления между A и произведением его факторов W и H на основе евклидова расстояния.

Используя целевую функцию, можно вывести правила обновления для W и H, и мы получим:

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

Код

Теперь я покажу, как реализовать эти два алгоритма. Мы применим тематическое моделирование к набору данных ABC Millions Headlines (недавно опубликовано на Kaggle: https://www.kaggle.com/therohk/million-headlines)

Импорт:

Основными библиотеками, которые мы используем в этом проекте, являются Pandas и Numpy для их структур данных, Scipy для разреженных операций, Gensim (библиотека с открытым исходным кодом, которая имеет разные модули тематического моделирования) для LDA и SKLearn (библиотека машинного обучения с открытым исходным кодом. ) для NMF.

import pandas as pd;
import numpy as np;
import scipy as sp;
import sklearn;
import sys;
from nltk.corpus import stopwords;
import nltk;
from gensim.models import ldamodel
import gensim.corpora;
from sklearn.feature_extraction.text import CountVectorizer, TfidfTransformer;
from sklearn.decomposition import NMF;
from sklearn.preprocessing import normalize;
import pickle;

Загрузка и предварительная обработка данных

Мы используем набор данных заголовков ABC News. Некоторые строки плохо отформатированы (их очень мало), поэтому мы их пропускаем.

data = pd.read_csv('data/abcnews-date-text.csv', 
error_bad_lines=False);
# We only need the Headlines text column from the data
data_text = data[['headline_text']];

Сначала нам нужно удалить игнорируемые слова. Приведение всех значений к типу float упростит итерацию, поскольку удалит большинство краевых случаев:

data_text = data_text.astype('str');
for idx in range(len(data_text)):
    
    #go through each word in each data_text row, remove stopwords, and set them on the index.
    data_text.iloc[idx]['headline_text'] = [word for word in data_text.iloc[idx]['headline_text'].split(' ') if word not in stopwords.words()];
    
    #print logs to monitor output
    if idx % 1000 == 0:
        sys.stdout.write('\rc = ' + str(idx) + ' / ' + str(len(data_text)));
#save data because it takes very long to remove stop words
pickle.dump(data_text, open('data_text.dat', 'wb'))
#get the words as an array for lda input
train_headlines = [value[0] for value in data_text.iloc[0:].values];

Реализация LDA:

Инициализируйте количество тем, которые нам нужно сгруппировать:

num_topics = 10;

Мы будем использовать библиотеку gensim для LDA. Сначала мы получаем словарь из двух слов. Для каждого заголовка мы будем использовать словарь, чтобы получить сопоставление идентификатора слова с их количеством слов. Модель LDA использует оба этих сопоставления.

id2word = gensim.corpora.Dictionary(train_headlines);
corpus = [id2word.doc2bow(text) for text in train_headlines];
lda = ldamodel.LdaModel(corpus=corpus, id2word=id2word, num_topics=num_topics);

Создание тем LDA:

Мы переберем количество тем, получим верхние слова в каждом кластере и добавим их в DataFrame, а затем распечатаем эти слова.

def get_lda_topics(model, num_topics):
    word_dict = {};
    for i in range(num_topics):
        words = model.show_topic(i, topn = 20);
        word_dict['Topic # ' + '{:02d}'.format(i+1)] = [i[0] for i in words];
    return pd.DataFrame(word_dict);

Вызовите функцию и получите темы:

get_lda_topics(lda, num_topics)

Реализация NMF

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

#the count vectorizer module needs string inputs, not array, so I join them with a space. This is a very quick operation.
train_headlines_sentences = [' '.join(text) for text in train_headlines]

Теперь мы получаем матрицу проектирования Counts, для которой мы используем модуль CountVectorizer SKLearn. Преобразование вернет матрицу размера (Документы x Функции), где значением ячейки будет количество раз, когда функция (слово) появляется в этом документе.

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

vectorizer = CountVectorizer(analyzer='word', max_features=5000);
x_counts = vectorizer.fit_transform(train_headlines_sentences);

Затем мы устанавливаем TfIdf Transformer и преобразуем счетчики с помощью модели.

transformer = TfidfTransformer(smooth_idf=False);
x_tfidf = transformer.fit_transform(x_counts);

А теперь мы нормализуем значения TfIdf до единичной длины для каждой строки.

xtfidf_norm = normalize(x_tfidf, norm='l1', axis=1)

И, наконец, получите модель NMF и дополните ее предложениями.

#obtain a NMF model.
model = NMF(n_components=num_topics, init='nndsvd');
#fit the model
model.fit(xtfidf_norm)

Создание тем NMF:

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

def get_nmf_topics(model, n_top_words):
    
    #the word ids obtained need to be reverse-mapped to the words so we can print the topic names.
    feat_names = vectorizer.get_feature_names()
    
    word_dict = {};
    for i in range(num_topics):
        
        #for each topic, obtain the largest values, and add the words they map to into the dictionary.
        words_ids = model.components_[i].argsort()[:-20 - 1:-1]
        words = [feat_names[key] for key in words_ids]
        word_dict['Topic # ' + '{:02d}'.format(i+1)] = words;
    
    return pd.DataFrame(word_dict);

Вызовите функцию и получите темы:

get_nmf_topics(model, 20)

Результаты

В двух таблицах выше в каждом разделе показаны результаты LDA и NMF для обоих наборов данных. Между словами в каждой группе есть некоторая последовательность. Например, в теме № 02 в LDA показаны слова, связанные со стрельбой и инцидентами с применением насилия, что очевидно из таких слов, как «нападение», «убит», «стрельба», «авария» и «полиция». В других темах показаны другие закономерности.

С другой стороны, сравнение результатов LDA с NMF также показывает, что NMF работает лучше. Глядя на тему № 01, мы видим, что в одной категории сгруппировано много имен вместе со словом «интервью». Этот тип заголовка очень часто встречается в новостных статьях, его формулировка похожа на «Интервью с Джоном Смитом» или «Интервью с Джеймсом К. на…».

Мы также видим две темы, связанные с насилием. Во-первых, Тема №03 посвящена терминам, связанным с полицией, таким как «расследование», «пропавший без вести», «расследование», «арест» и «тело». Во-вторых, Тема № 08 сосредоточена на таких терминах нападения, как «убийство», «нанесение ножевого ранения», «виновен» и «убит». Это интересное разделение между темами, потому что, хотя термины в каждой из них очень тесно связаны, одна больше фокусируется на деятельности, связанной с полицией, а другая - на преступной деятельности. Наряду с первым кластером, который получает имена, результаты показывают, что NMF (с использованием TfIdf) работает намного лучше, чем LDA.

Окончательный код

Вы можете просмотреть мой окончательный код на GitHub или Kaggle или ниже: