Machine Learning, NLP

Токенизация текста. Обучаем модель предсказывать негативные комментарии.

Время прочтения: 5 мин.

Во-первых, подготовим датасет с размеченными данными. Все комментарии мы разделим на два класса: негативные (target — 1) и позитивные (target — 0). Есть два типа классификации выборки: бинарная и мультиклассовая классификация. Для разметки текста на положительные и отрицательные комментарии мы будем использовать бинарную классификацию.

Во-вторых, подготовим текст для обучения, очистим его от лишних символов, цифр и иностранных букв, проведем Стемминг[1].

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

По результатам теста определим процент точности работы модели.

Итак, для начала импортируем библиотеки.


[1] Стемминг – это грубый эвристический процесс, который оставляет начальную форму слова. У слов есть разные окончания, например: «Ручка», «Ручку», «Ручкам» компьютер воспринимает это как 3 разных слова, поэтому от слов нужно удалить концовки.

import pandas as pd
import re
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from nltk.stem.snowball import SnowballStemmer
from sklearn.metrics import classification_report
import stop_words

Загружаем подготовленный файл «user_comments_with_target.csv» и файл, на котором будем применять модель «isu_2020.csv» с помощью pandas.

df_2019_comment = pd.read_csv('user_comments_with_target.csv', sep=';', encoding='cp1251', low_memory=False)
df_2020_comment = pd.read_csv('isu_2020.csv', sep=';', encoding='cp1251', low_memory=False)

Файл содержит лишние столбцы, оставляем только 2 нужных: сами комментарии и разметку. Пустые значения удаляем.

# Оставляем только 2 нужных столбца и удаляем все пустые значения
df_2019_comment = df_2019_comment[['user_comments', 'target']].dropna()
df_2020_comment = df_2020_comment[['user_comments']].dropna()

# Подготовка файла для обучения
# 2019
df_2019_comment = df_2019_comment[df_2019_comment['target'].isin(['0', '1'])]
df_2019_comment['target'] = df_2019_comment['target'].astype(int)
df_2019_comment = df_2019_comment.drop_duplicates('user_comments')
df_2019_comment = df_2019_comment[df_2019_comment['user_comments'].apply(lambda x:len(x) > 2)]
df_2019_comment = df_2019_comment.sort_values('target').reset_index(drop=True)

# Подготовка файла для предсказания
# 2020
df_2020_comment = df_2020_comment.drop_duplicates('user_comments')
df_2020_comment = df_2020_comment[df_2020_comment['user_comments'].apply(lambda x:len(x) > 2)]
df_2020_comment = df_2020_comment.reset_index(drop=True)

Мы создали функцию, которая проводит очистку текста и подготовку его к обучению:

  • переводит текст в нижний регистр;
  • удаляет латиницу, знаки препинания, символы и цифры;
  • применяет Стемминг.
stemmer = SnowballStemmer('russian')
stop_words_ru = stop_words.get_stop_words('russian')

def clear_txt(txt):
    txt = txt.lower()
    txt = re.sub('[/+_!@#$A-Za-z0-9\n.,:()""«»;-]', ' ', txt)
    new_txt = ''
    for t in txt.split(' '):
        if len(t) > 0:
            new_txt = new_txt + stemmer.stem(t) + ' '
    return new_txt[:-1]

Пример работы стемминга:

stemmer = SnowballStemmer('russian')
stemmer.stem('Ручкам')

'ручк'

stemmer.stem('Ручки')

'ручк'

Как видно из примера, стемминг удаляет концовки слов.

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

Пример работы леммитизации с использованием библиотеки pymorphy2:

import pymorphy2
pm = pymorphy2.MorphAnalyzer()
pm.parse('Ручкам')

[Parse(word='ручкам', tag=OpencorporaTag('NOUN,inan,femn plur,datv'), normal_form='ручка', score=1.0, methods_stack=((DictionaryAnalyzer(), 'ручкам', 9, 9),))]

pm.parse('Ручкам')[0].normal_form

'ручка'

pm.parse('Ручки')[0].normal_form

'ручка'

Слова «Ручкам» и «Ручки» pymorphy2 привел к начальной форме в нижнем регистре к слову «ручка».

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

Обучение и тестирование модели.

После обучения модели нам нужно понять насколько верно модель определяет комментарии. Мы будем делать это по фактическим контрольным данным. Для этого мы оставим часть датасета для тестирования.

В переменную «X» мы записываем все наши неочищенные комментарии («user_comments»), а в переменную «Y» правильные ответы/классы («target»).


X = df_2019_comment['user_comments']
y = list(df_2019_comment['target'])

Разделим весь датасет на 2 выборки, одна выборка будет использоваться для обучения (X_ train, y_train), а вторая для тестирования качества предсказания модели (X_test, y_test).

X_train, X_test, y_train, y_test =  train_test_split(X, y, test_size=0.2)
print('X train:', X_train.shape[0],
      '\ny train:', len(y_train),
      '\nX test:', X_test.shape[0],
      '\ny test:', len(y_test))

X train: 10942
y train: 10942
X test: 2736
y test: 2736

В Vectorizer в параметры передаем нашу функцию и стоп-слова, в которые мы добавили ФИО сотрудников, встречающихся в комментариях, приведенные к начальной форме, тем самым Vectorizer будет работать с очищенным текстом.

Vectorizer удобен тем, что он при работе применяет токенизацию[1],  векторизацию текста и скоринг TF-IDF[2].

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


[1] Токенизация по предложениям – это процесс разделения письменного языка на предложения-компоненты.

3  Векторизация – процесс преобразование слов в числовые векторы для использования в алгоритмах.

4  TF-IDF (term-frequency – inverse document frequency) – это статистическая мера для оценки важности слова.

stop_words_ru = stop_words.get_stop_words('russian')
tfidf_vectorizer = TfidfVectorizer(max_features=10000,
                                   preprocessor=clear_txt,
                                   stop_words=stop_words_ru)
X_train = tfidf_vectorizer.fit_transform(X_train)

Основная работа закончена, осталось обучить и применить модель.

В библиотеке scikit-learn есть реализация алгоритма Random Forest, с помощью которого мы будем обучать нашу модель. Random Forest уникальный алгоритм, придуманный еще в прошлом веке, который используется для многих задач и является самым популярным алгоритмом.

rfc = RandomForestClassifier()

# Обучение 
rfc.fit(X_train, y_train)

# Предсказание модели на тестовом датасете
y_pred = rfc.predict(tfidf_vectorizer.transform(X_test))

Результат точности предсказания негативных комментариев по метрике f1_score (мера точности в статистическом анализе бинарной классификации) равна 84%.

print(classification_report(y_test, y_pred))

             precision    recall  f1-score   support

          0       0.98      1.00      0.99      2532
          1       0.93      0.76      0.84       204

avg / total       0.98      0.98      0.98      2736

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

df_2020_comment['pred_labels'] = \
     rfc.predict(tfidf_vectorizer.transform(df_2020_comment['user_comments']))

Мы попросили экспертов провести перепроверку результата работы модели, и в 92% случаев модель смогла верно предсказать негативные комментарии.

Советуем почитать