Предварительная обработка NLP, BoW, TF-IDF, Naive Bayes, SVM, Spacy, Shapely, LSTM и др.

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

Анализ настроений

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

Данные

  1. Вы можете найти данные твитов, помеченных людьми, на сайте data.world. Данные содержат более 8000 твитов, которые были помечены как положительные, отрицательные, нейтральные или неизвестные (Я не могу сказать).
  2. Sentiment140 предоставляет данные о тренировках команды из Стэнфорда. Ярлыки для этого набора данных были автоматически аннотированы.

Предварительная обработка NLP

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

1. Сохраняйте только символы ASCII.

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

def ascii_only(str_):
    return str_.encode("ascii", "ignore").decode()

2. Сделать все строчными

Это просто.

def make_lower(str_):
    return str_.lower()

3. Удалите символы HTML, упоминания и ссылки.

Из-за ошибок в кодировке некоторые твиты содержат символы HTML, такие как  . Прежде чем мы удалим знаки препинания, мы сначала избавимся от этих слов, чтобы не оставлять тарабарщину после удаления знаков препинания. Другие алфавитные слова, которые мы хотим удалить, - это имена пользователей (например, @stereopickle) и гиперссылки (в этом наборе данных они обозначаются как {link}). Для этого мы будем использовать регулярное выражение.

import re 
def remove_nonwords(str_):
    return re.sub("[^A-Za-z0-9 ]\w+[^A-Za-z0-9]*", ' ', str_)

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

4. Удалите фирменные слова.

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

def remove_brandwords(str_):
    p = '''#?(iphone|ipad|sxsw|hcsm|google|apple|cisco|austin|
    atari|intel|mac|pc|blackberry|android|linux|ubuntu)[a-z0-9]*'''
    return re.sub(p, ' ', str_)

5. Удалите знаки препинания.

Еще одна простая очистка. Обратите внимание, что я удаляю здесь все знаки препинания для простоты, но на самом деле во многих твитах могут использоваться знаки препинания, чтобы выразить мнение, например :) и :(.

import string
punctuations = string.punctuation
punctuations = punctuations + '�' + string.digits

def remove_punctuations(str_, punctuations):
    table_ = str.maketrans('', '', punctuations)
    return str_.translate(table_)

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



6. Лемматизация и удаление игнорируемых слов

Наконец, мы лемматизируем наши словари и удалим стоп-слова с помощью NLTK. Таким образом, лемматизатор вернет словари в его базовую форму, лемму.

from nltk.stem import WordNetLemmatizer
from nltk.corpus import stopwords
sw = stopwords.words('english')
def lemmatize(str_, sw):
    wnl = WordNetLemmatizer()
    return ' '.join([wnl.lemmatize(w) for w in x.split() if w not in sw])

Подробнее о лемматизации и стемминге читайте здесь.



7. Выбор функции

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

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

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

Оценка модели

Оценка

Как правило, в наших данных возникает проблема высокого классового дисбаланса, поскольку в большинстве случаев люди с большей вероятностью будут писать о нейтральных или положительных твитах, чем о отрицательных. Но для большинства бизнес-проблем наша модель должна обнаруживать эти негативные твиты. Поэтому мы будем следить за ними, глядя на макро-усредненный показатель f1, а также на кривую точного отзыва. Функция ниже построит кривую ROC и кривую точности-отзыва, а также распечатает ключевые оценочные показатели.

Базовая модель

Мы можем использовать DummyClassifier scikit-learn, чтобы сначала увидеть, какой была бы наша базовая мера, если бы мы просто классифицировали ее на основе того, как часто встречается каждый класс.

from sklearn.dummy import DummyClassifier
dummy_classifier = DummyClassifier()
dummy_classifier.fit(tweets_train, labels_train)
y_pred_p = dummy_classifier.predict_proba(tweets_validation)    
y_pred = dummy_classifier.predict(tweets_validation)

Модель мешка слов (векторы подсчета)

Один из самых простых способов количественной оценки текстовых данных - просто подсчитать частоту каждого слова. CountVectorizer scikit-learn легко справится с этой задачей.

from sklearn.feature_extraction.text import CountVectorizer

countvec = CountVectorizer(ngram_range = (1, 2), min_df = 2)
count_vectors = countvec.fit_transform(tweets_train)

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

Векторы TF-IDF

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

from sklearn.feature_extraction.text import TfidfVectorizer

tfvec = TfidfVectorizer(ngram_range = (1, 2), min_df = 2)
tf_vectors = tfvec.fit_transform(tweets_train)

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

Наивно-байесовский

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

Допустим, у нас есть твит, в котором говорится… «Мне нравится мой новый телефон. Он действительно быстрый, надежный и хорошо продуманный! ». Этот твит явно имеет положительное отношение. В этом случае модель Наивного Байеса предполагает, что отдельные слова, такие как «любовь», «новый», «действительно», «быстрый», «надежный», независимо друг от друга вносят вклад в ее положительный класс. Другими словами, вероятность того, что твит будет положительным, когда в нем будет употреблено слово «надежный», не изменится другими словами. Это не означает, что эти слова внешне независимы. Некоторые слова могут чаще встречаться вместе, но это не означает, что то, насколько каждое слово влияет на свой класс, зависит.

Алгоритм Наивного Байеса прост в использовании и надежен, когда выполняется вышеприведенное предположение. Поскольку для тестирования нашей модели требуется векторизация, мы можем встроить конвейер в нашу модель.

from sklearn.naive_bayes import MultinomialNB
from sklearn.pipeline import Pipeline
mn_nb = MultinomialNB()
# change countvec to tfvec for tf-idf 
model = Pipeline([('vectorize', countvec), ('classify', mn_nb)])
# fitting training count vectors (change to tf_vectors for tf-idf)
model['classify'].fit(count_vectors, labels_train)
y_pred_p = model.predict_proba(tweets_validation)    
y_pred = model.predict(tweets_validation)
evaluating(labels_validation, y_pred, y_pred_p)

Из-за своего предположения о независимости между функциями Наивный Байес переоценивает уверенность в том, насколько каждая функция способствует метке, что делает его плохой оценкой. Так что относитесь к вероятности предсказаний с недоверием.

Машина опорных векторов (SVM)

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

from sklearn.svm import SVC
svm_classifier = SVC(class_weight = 'balanced', probability= True)
# don't forget to adjust the hyperparameters! 
# change countvec to tfvec for tf-idf 
svm_model = Pipeline([('vectorize', countvec), ('classify', svm_classifier)])
# fitting training count vectors (change to tf_vectors for tf-idf)
svm_model['classify'].fit(count_vectors, labels_train)
y_pred_p = svm_model.predict_proba(tweets_validation)    
y_pred = svm_model.predict(tweets_validation)
evaluating(labels_validation, y_pred, y_pred_p)

Оценка SHAP

Когда SVM использует трюк с ядром, с точки зрения интерпретируемости все оказывается в серой зоне. Но мы можем использовать значение Шепли, чтобы расшифровать, как отдельные особенности влияют на классификацию. Мы будем использовать дружественный интерфейс SHAP для визуализации значений Шепли. Для получения подробного руководства по этому поводу я рекомендую прочитать документацию по SHAP.

import shap
shap.initjs() 
sample = shap.kmeans(count_vectors, 10)
e = shap.KernelExplainer(svm_model.predict_proba, sample, link = 'logit')
shap_vals = e.shap_values(X_val_tf, nsamples = 100)
shap.summary_plot(shap_vals, 
                  feature_names = countvec.get_feature_names(),
                  class_names = svm_model.classes_)

LSTM

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

Для LSTM нам нужно вводить тексты в виде последовательности. Ниже приведены инструкции по запуску и оценке классификатора LSTM. Я объяснил каждый шаг кода.

Вложение слов (перчатка)

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

Для этого мы будем использовать GloVe, векторные представления, полученные командой из Стэнфорда. Я использую их 200-мерные словесные векторы, обученные на 2 миллиардах твитов. Вам нужно будет скачать векторы с их веб-сайта.

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

# adding the bolded part
model.add(Embedding(num_vocab, 200, weights = [vector_matrix], 
                    input_length = max_len))

Используя встраивание слов и LSTM, моя модель показала 20% -ное увеличение общей точности и 16% -ное увеличение макро-усредненной F1-оценки.

Мы рассмотрели основы построения модели анализа настроений с использованием данных твитов. Я закончу пост некоторыми вопросами, над которыми стоит подумать и над которыми стоит развиться.

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

Удачного обучения!