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

Введение

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

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

LDA используется в основном для тематического моделирования, кластеризации текстовых документов по сходству. Размер документа может варьироваться от одного слова (не идеально) до размера всей публикации. Содержание кластеров LDA определяется с использованием терминов (слов) в каждом документе и частоты, а иногда даже порядка (с использованием n-граммов), в котором они появляются. Документы, которые считаются похожими друг на друга, группируются вместе, и мы предполагаем, что каждый кластер является представителем темы, хотя мы не знаем, что это за тема как таковая, до тех пор, пока кластер не будет создан. Важно отметить, что модель не понимает ни содержания, ни контекста документов в этих кластерах и, следовательно, не может фактически присвоить кластерам ярлык темы. Вместо этого он маркирует каждый кластер, используя целое число индекса (0, n); n - количество тем, которые мы сообщаем модели. искать. Человек, или очень умное водное млекопитающее, должен проанализировать кластеры и определить, как следует маркировать каждый кластер.

В этом посте мы очистим некоторые данные Twitter и напишем модель LDA для кластеризации этих данных. Затем мы воспользуемся pyLDAvis для создания интерактивной визуализации кластеров.

Ключевые зависимости: pandas, nltk, gensim, numpy, pyLDAvis

Вот некоторые определения, с которыми следует ознакомиться заранее:

  1. документ: текстовый объект (например, твит).
  2. словарь: список всех уникальных токенов (слов, терминов) в нашей коллекции документов, каждый из которых имеет уникальный целочисленный идентификатор.
  3. мешок слов: набор всех наших документов, каждый документ сокращен до списка матриц, по одной матрице для каждого слова в документе - Используя gensim doc2bow, каждая матрица представлена ​​как кортеж с уникальным целочисленным идентификатором термина с индексом 0 и количеством раз, которое он встречается в документе с индексом 1. (например, документ Коробка находилась в большей коробке будет сокращено до примерно такого вида [(the, 2), (box, 2), (was, 1), (in, 1), ( больше , 1)], но заменяя термин уникальным словарным идентификатором термина)
  4. оценка согласованности: значение с плавающей запятой в диапазоне от 0 до 1, используемое для оценки того, насколько хорошо наша модель и количество кластеров соответствуют нашим данным.
  5. кластер: узел, представляющий группу документов, предполагаемую тему.

1. Данные

Ранее в этом году я начал собирать пару сотен тысяч политических твитов, конечной целью которых было провести различные анализы твитов и их метаданных в преддверии президентских выборов в США 2020 года.

Набор данных этого поста будет состоять из 3500 твитов, в которых будет упоминаться хотя бы один из следующего: @berniesanders, @kamalaharris, @joebiden, @ewarren (Twitter обрабатывает Берни Сандерс, Камала Харрис, Джо Байден, и Элизабет Уоррен соответственно). Я собрал эти твиты в начале ноября 2019 года и сделал их доступными для скачивания здесь. Мы изучим эти данные и попытаемся выяснить, о чем люди писали в Твиттере в начале ноября.

Я не буду вдаваться в подробности, как собирать твиты, но я добавил код, который использовал ниже. Для успешного запуска кода требуется доступ к tweepy API. Я не собирал ретвиты и не собирал твиты, написанные не на английском языке (модель требует гораздо большей настройки, чтобы приспособиться к нескольким языкам).

class Streamer(StreamListener):
    def __init__(self):
        super().__init__()
        self.limit = 1000 # Number of tweets to collect.
        self.statuses = []  # Pass each status here.

    def on_status(self, status):
        if status.retweeted or "RT @" 
        in status.text or status.lang != "en":
            return   # Remove re-tweets and non-English tweets.
        if len(self.statuses) < self.limit:
            self.statuses.append(status)
            print(len(self.statuses))  # Get count of statuses
        if len(self.statuses) == self.limit:
            with open("/tweet_data.csv", "w") as    file: 
                writer = csv.writer(file)  # Saving data to csv. 
                for status in self.statuses:
                    writer.writerow([status.id, status.text,
              status.created_at, status.user.name,         
              status.user.screen_name, status.user.followers_count, status.user.location]) 
            print(self.statuses)
            print(f"\n*** Limit of {self.limit} met ***")
            return False
        if len(self.statuses) > self.limit:
            return False


streaming = tweepy.Stream(auth=setup.api.auth, listener=Streamer())

items = ["@berniesanders", "@kamalaharris", "@joebiden", "@ewarren"]  # Keywords to track

stream_data = streaming.filter(track=items)

При этом текстовые данные твита вместе с его метаданными (идентификатор, дата создания, имя, имя пользователя, количество подписчиков и местоположение) передаются в CSV с именем tweet_data.

import pandas as pd
df = pd.read_csv(r"/tweet_data.csv", names= ["id", "text", "date", "name", "username", "followers", "loc"])

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

Уборка

Мы удалим все знаки препинания, специальные символы и URL-ссылки, а затем применим lower () к каждому твиту. Это обеспечивает определенный уровень согласованности в наших документах (помните, что каждый твит рассматривается как документ). Я также удалил экземпляры «berniesanders», «kamalaharris», «joebiden» и «ewarren», поскольку они искажают нашу частоту использования терминов, поскольку каждый документ будет содержать по крайней мере один из этих элементов.

import string
ppl = ["berniesanders", "kamalaharris", "joebiden", "ewarren"]
def clean(txt):
    txt = str(txt.translate(str.maketrans("", "", string.punctuation))).lower() 
    txt = str(txt).split()
    for item in txt:
        if "http" in item:
            txt.remove(item)
        for item in ppl:
            if item in txt:
                txt.remove(item)
    txt = (" ".join(txt))
    return txt
    
df.text = df.text.apply(clean)

2. Подготовка данных

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

import gensim
from gensim.utils import simple_preprocess
from gensim.parsing.preprocessing import STOPWORDS as stopwords
import nltk
nltk.download("wordnet")
from nltk.stem import WordNetLemmatizer as lemm, SnowballStemmer as stemm
from nltk.stem.porter import *
import numpy as np
np.random.seed(0)

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

LDA включает в себя случайный процесс, то есть наша модель требует способности создавать случайные величины, отсюда и импорт numpy. Добавление numpy.random.seed (0) позволяет воспроизводить нашу модель, поскольку она будет генерировать и использовать одни и те же случайные переменные вместо создания новых при каждом запуске кода.

СЕКРЕТНЫЕ СЛОВА Gensim - это список терминов, которые считаются нерелевантными или могут сбить с толку наш мешок слов. В НЛП «стоп-слова» относятся к набору терминов, которые мы не хотим использовать в нашей модели. Этот список будет использован для удаления этих нерелевантных терминов из наших документов. Мы можем напечатать (игнорируемые слова), чтобы просмотреть условия, которые будут удалены.

Вот термины в игнорируемых словах.

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

stopwords = stopwords.union(set(["add_term_1", "add_term_2"]))

Лемматизация и стемминг

Давайте напишем код для нашей подготовки данных.

import warnings 
warnings.simplefilter("ignore")
import gensim
from gensim.utils import simple_preprocess
from gensim.parsing.preprocessing import STOPWORDS as stopwords
import nltk
nltk.download("wordnet")
from nltk.stem import WordNetLemmatizer as lemm, SnowballStemmer as stemm
from nltk.stem.porter import *
import numpy as np
np.random.seed(0)

Инициализировать стеммер.

stemmer = stemm(language="english")

Напишите функцию, которая будет одновременно лемматизировать и ограничивать наши документы. В GeeksforGeeks есть примеры использования nltk для лемматизации и примеры использования nltk для стемминга.

def lemm_stemm(txt):
    return stemmer.stem(lemm().lemmatize(txt, pos="v"))

Напишите функцию, которая будет удалять игнорируемые слова из наших документов, одновременно применяя lemm_stemm ().

def preprocess(txt):
    r = [lemm_stemm(token) for token in simple_preprocess(txt) if       token not in stopwords and len(token) > 2]
    return r

Назначьте наши очищенные и подготовленные документы новой переменной.

proc_docs = df.text.apply(preprocess)

3. Изготовление модели

Теперь, когда мы подготовили наши данные, мы можем приступить к написанию нашей модели.

Словарь

Как упоминалось во введении, словарь (в LDA) - это список всех уникальных терминов, встречающихся в нашем наборе документов. Мы будем использовать пакет корпусов gensim для создания нашего словаря.

dictionary = gensim.corpora.Dictionary(proc_docs)
dictionary.filter_extremes(no_below=5, no_above= .90)
len(dictionary)

Параметры filter_extremes () служат второй линией защиты от игнорируемых слов или других часто используемых терминов, которые мало добавляют смысла в смысл предложения. Игра с этими параметрами может помочь в точной настройке модели. Я не буду вдаваться в подробности по этому поводу, но приложил приведенный ниже снимок экрана из документации по словарю gensim, объясняющий параметры.

В нашем словаре 972 уникальных токена (термина).

Мешок слов

Как сказано во введении, набор слов (в LDA) - это совокупность всех наших документов, разбитых на матрицы. Матрицы состоят из идентификатора термина и того, сколько раз он встречается в документе.

n = 5 # Number of clusters we want to fit our data to
bow = [dictionary.doc2bow(doc) for doc in proc_docs]
lda = gensim.models.LdaMulticore(bow, num_topics= n, id2word=dictionary, passes=2, workers=2)
print(bow)

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

for id, topic in lda.print_topics(-1):
    print(f"TOPIC: {id} \n WORDS: {topic}")

Глядя на каждую тематическую группу, мы можем понять, что они представляют. Взгляните на тему 1 и тему 4.

Относительно темы 1: В теме 1 ключевые термины ченкуйгур и анакаспарян относятся к Дженк Уйгур и Ана Каспарян , соведущий Младотурков (политической комментаторской фирмы и шоу). Тема 1 также включает ключевые термины право, козырь и нра.

15 ноября произошла стрельба в школе в старшей школе Саугуса недалеко от Санта-Клариты, штат Калифорния. Это трагическое событие получило широкое освещение в СМИ и в сети. Молодые турки (TYT) являются активными сторонниками более строгих законов об оружии и регулярно бодаются с NRA и другими вооруженными группировками. TYT даже возглавил кампанию под названием #NeverNRA.

Этот тематический кластер можно обозначить как «TYT против NRA» или что-то подобное.

Относительно темы 4: термины «ченкуйгур» и «анакаспариан» повторяются в теме 4. Тема 4 также включает «ониунгурк», относящийся к младотуркам, и «берни», относящийся к Берни Сандерсу.

12 ноября Дженк Уйгур публично поддержал кандидата Берни Сандерса. Это одобрение было повторено аккаунтом TYT в Твиттере. Затем Берни Сандерс публично поблагодарил их за поддержку. Также 14 ноября г-н Уйгур объявил, что баллотируется в Конгресс. Оба эти события привлекли заметное внимание в Твиттере.

Этот тематический кластер можно обозначить как «TYT и Берни Сандерс» или что-то подобное.

Аналогичные объяснения есть и для других тематических кластеров.

4. Оценка, визуализация, заключение

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

# Eval via coherence scoring
from gensim import corpora, models
from gensim.models import CoherenceModel
from pprint import pprint
coh = CoherenceModel(model=lda, texts= proc_docs, dictionary = dictionary, coherence = "c_v")
coh_lda = coh.get_coherence()
print("Coherence Score:", coh_lda)

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

Визуализируйте с помощью pyLDAvis

Наконец, мы можем визуализировать наши кластеры с помощью pyLDAvis. Этот пакет создает карту расстояний кластеров с кластерами, нанесенными по осям x и y. Эту карту расстояний можно открыть в Юпитере, вызвав pyLDAvis.display (), но также можно открыть в Интернете, вызвав pyLDAvis.show ().

import pyLDAvis.gensim as pyldavis
import pyLDAvis
lda_display = pyldavis.prepare(lda, bow, dictionary)
pyLDAvis.show(lda_display)

Вот скриншот нашей карты расстояний pyLDAvis.

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

Заключение

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

### All Dependencies ###

import pandas as pd
from wordcloud import WordCloud as cloud
import matplotlib.pyplot as plt
import string
import gensim
from gensim.utils import simple_preprocess
from gensim.parsing.preprocessing import STOPWORDS as stopwords
import nltk
nltk.download("wordnet")
from nltk.stem import WordNetLemmatizer as lemm, SnowballStemmer as stemm
from nltk.stem.porter import *
import numpy as np
np.random.seed(0)
from gensim import corpora, models
from gensim.models import CoherenceModel
from pprint import pprint
import pyLDAvis.gensim as pyldavis
import pyLDAvis


### Word Cloud ###

df = pd.read_csv(r"/tweet_data.csv", names=["id", "text", "date", "name",
                                                                 "username", "followers", "loc"])


def clean(txt):
    txt = str(txt).split()
    for item in txt:
        if "http" in item:
            txt.remove(item)
    txt = (" ".join(txt))
    return txt


text = (df.text.apply(clean))


wc = cloud(background_color='white', colormap="tab10").generate(" ".join(text))

plt.axis("off")
plt.text(2, 210, "Generated using word_cloud and this post's dataset.", size = 5, color="grey")

plt.imshow(wc)
plt.show()
### Stream & Collect Tweets ###
class Streamer(StreamListener):
    def __init__(self):
        super().__init__()
        self.limit = 1000 # Number of tweets to collect.
        self.statuses = []  # Pass each status here.

    def on_status(self, status):
        if status.retweeted or "RT @" 
        in status.text or status.lang != "en":
            return   # Remove re-tweets and non-English tweets.
        if len(self.statuses) < self.limit:
            self.statuses.append(status)
            print(len(self.statuses))  # Get count of statuses
        if len(self.statuses) == self.limit:
            with open("/tweet_data.csv", "w") as    file: 
                writer = csv.writer(file)  # Saving data to csv. 
                for status in self.statuses:
                    writer.writerow([status.id, status.text,
              status.created_at, status.user.name,         
              status.user.screen_name, status.user.followers_count, status.user.location]) 
            print(self.statuses)
            print(f"\n*** Limit of {self.limit} met ***")
            return False
        if len(self.statuses) > self.limit:
            return False


streaming = tweepy.Stream(auth=setup.api.auth, listener=Streamer())

items = ["@berniesanders", "@kamalaharris", "@joebiden", "@ewarren"]  # Keywords to track

stream_data = streaming.filter(track=items)
### Data ###


df = pd.read_csv(r"/tweet_data.csv", names= ["id", "text", "date", "name",
                                                                 "username", "followers", "loc"])


### Data Cleaning ###

ppl = ["berniesanders", "kamalaharris", "joebiden", "ewarren"]


def clean(txt):
    txt = str(txt.translate(str.maketrans("", "", string.punctuation))).lower()
    txt = str(txt).split()
    for item in txt:
        if "http" in item:
            txt.remove(item)
        for item in ppl:
            if item in txt:
                txt.remove(item)
    txt = (" ".join(txt))
    return txt


df.text = df.text.apply(clean)



### Data Prep ###

# print(stopwords)

# If you want to add to the stopwords list: stopwords = stopwords.union(set(["add_term_1", "add_term_2"]))



### Lemmatize and Stem ###

stemmer = stemm(language="english")


def lemm_stemm(txt):
    return stemmer.stem(lemm().lemmatize(txt, pos="v"))


def preprocess(txt):
    r = [lemm_stemm(token) for token in simple_preprocess(txt) if       token not in stopwords and len(token) > 2]
    return r


proc_docs = df.text.apply(preprocess)



### LDA Model ###

dictionary = gensim.corpora.Dictionary(proc_docs)
dictionary.filter_extremes(no_below=5, no_above= .90)
# print(dictionary)

n = 5 # Number of clusters we want to fit our data to
bow = [dictionary.doc2bow(doc) for doc in proc_docs]
lda = gensim.models.LdaMulticore(bow, num_topics= n, id2word=dictionary, passes=2, workers=2)
# print(bow)

for id, topic in lda.print_topics(-1):
    print(f"TOPIC: {id} \n WORDS: {topic}")



### Coherence Scoring ###

coh = CoherenceModel(model=lda, texts= proc_docs, dictionary = dictionary, coherence = "c_v")
coh_lda = coh.get_coherence()
print("Coherence Score:", coh_lda)

lda_display = pyldavis.prepare(lda, bow, dictionary)
pyLDAvis.show(lda_display)

LDA - отличная модель для исследования текстовых данных, хотя для использования в производственной среде требуется значительная оптимизация (в зависимости от варианта использования). Пакеты gensim, nltk и pyLDAvis бесценны при написании, оценке и отображении моделей.

Большое спасибо за то, что позволили мне поделиться, еще не все. 😃