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

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

