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

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

В ходе работы служб банка накапливается огромное количество текстовой информации, которую необходимо анализировать и структурировать. В нашем случае классифицировать необходимо обращения. Категории заданы заранее: “Сбой/ошибка в работе приложения”, “Неторговые операции”, “Негатив”, “Налогообложение”, “Отчётность брокера”, “Обновление анкетных данных”, “ИИС”, “Торговые операции”, “Передача выплат инвестору”, “Выбор опций”, “СМС-информирование”, “Отмена заявки”.

Для начала считаем данные:

import pandas as pd
raw_data = pd.read_excel('example.xlsx')
raw_data.head()

Таким образом, исходные данные представлены 5 столбцами.

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

import stop_words
russian_stopwords = stop_words.get_stop_words('ru')
russian_stopwords.extend(['...', '«', '»', 'здравствуйте','здравствуй','до свидания', 'добрый день', 'добрый вечер', 'доброе утро'])
import string
def remove_punctuation(text):
    return ''.join([ch if ch not in string.punctuation else ' ' for ch in text])
def remove_numbers(text):
    return ''.join([i if not i.isdigit() else ' ' for i in text])
import re
def remove_multiple_spaces(text):
    return re.sub(r'\s+', ' ', text, flags=re.I)

prep_text = [remove_multiple_spaces(remove_numbers(remove_punctuation(text.lower()))) for text in raw_data['Text'].astype('str')]
raw_data['text_prep'] = prep_text
raw_data.head()

Далее есть 2 классических варианта.

1. Использовать стемминг (нахождение основы слова).

2. Использовать лемматизацию. В большинстве случаев лемматизация (начальная форма слова) является лучшим решением, поэтому воспользуемся ей.

Для этого используем библиотеку pymorphy2.

import pymorphy2
raw_data = raw_data.dropna(subset=['text_prep'])
morph = pymorphy2.MorphAnalyzer()
lemm_texts_list = []
for text in raw_data['text_prep']:
    text_lem = [morph.parse(word)[0].normal_form for word in text.split(' ')]
    if len(text_lem) <= 2:
        lemm_texts_list.append('')
        continue
    lemm_texts_list.append(' '.join(text_lem))
raw_data['text_lemm'] = lemm_texts_list
raw_data = raw_data[raw_data['text_lemm'] != '']
raw_data.head()

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

Далее – обучение. В статье будет рассмотрено 2 классических метода: градиентный бустинг от CatBoost и логистическая регрессия.

Разбиваем данные на обучающую и тестовую выборку.

from sklearn.model_selection import train_test_split
X = raw_data ['text_lemm']
y = raw_data ['Разметка подробно']
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state= 42, test_size=0.3)

Обучение логистической регрессии:

from sklearn.pipeline import Pipeline
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfVectorizer
from sklearn.linear_model import LogisticRegression

logreg = Pipeline([
                ('vect', CountVectorizer(analyzer='char', ngram_range =([2,10]))),
                ('tfidf', TfidfTransformer()),
                ('clf', LogisticRegression(n_jobs=3,C=1e5, solver='saga', 
                                           multi_class='multinomial',
                                           max_iter=1000,
                                           random_state=42)),
])

logreg.fit(X_train, y_train)
y_pred = logreg.predict(X_test)

from sklearn.metrics import classification_report
from sklearn.metrics import f1_score
print(classification_report(y_test, y_pred, target_names=themes))
print(f"F1 Score: {f1_score(y_test, y_pred, average='weighted')}")

Используется CountVectorizer на ngram с analyzer = char. Именно такой подход дал наилучшую точность на маленьких текстах. В зависимости от данных значение ngram можно корректировать. Целевая метрика – F1.

Далее — CatBoost.

Создаем функцию с инициализацией и обучением модели.

from catboost import CatBoostClassifier, Pool
def fit_model(train_pool, test_pool, **kwargs):
    model = CatBoostClassifier(task_type='CPU', iterations = 5000,
                               eval_metric='TotalF1', od_type='Iter', 
                               od_wait=500, **kwargs)
    
    return model.fit(train_pool, eval_set=test_pool, 
                     verbose=100, plot=True, 
                     use_best_model=True)

Модель будет обучаться «на процессоре», валидационная метрика – F1. В параметрах обучения задаем валидационный пул и использование модели с наибольшим качеством.

Формируем обучающий и валидационный пулы.

train_pool = Pool(data=X_train, label=y_train, 
                  text_features=['text_lemm',])
valid_pool = Pool(data=X_test, label=y_test, 
                  text_features=['text_lemm',])

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

Обучаем модель.

model = fit_model(train_pool, valid_pool, learning_rate=0.35,
                  dictionaries = [{
                      'dictionary_id':'Word',
                      'max_dictionary_size': '50000'
                  }],
                 feature_calcers = ['BoW:top_tokens_count=10000'])

CatBoost дал нам прирост в качестве ~ 2%.

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