В мире коммерции существует множество применений классификации текста. Например, новости часто сгруппированы по темам, контент или товары часто помечаются по категориям, а пользователей можно разделить на группы, в зависимости от того, как они отзываются о товаре в Интернете. Однако большинство статей в сети описывают бинарную классификацию, но чаще реальные задачи оказываются сложнее. В этой статье мы будем проводить мультиклассовую классификацию обращений в службу банка.
![](https://habrastorage.org/getpro/habr/upload_files/3da/d5e/f56/3dad5ef5601c8101ed04de3bf1dbac42.jpg)
В ходе работы служб банка накапливается огромное количество текстовой информации, которую необходимо анализировать и структурировать. В нашем случае классифицировать необходимо обращения. Категории заданы заранее: “Сбой/ошибка в работе приложения”, “Неторговые операции”, “Негатив”, “Налогообложение”, “Отчётность брокера”, “Обновление анкетных данных”, “ИИС”, “Торговые операции”, “Передача выплат инвестору”, “Выбор опций”, “СМС-информирование”, “Отмена заявки”.
Для начала считаем данные:
import pandas as pd
raw_data = pd.read_excel('example.xlsx')
raw_data.head()
![](https://habrastorage.org/getpro/habr/upload_files/1fb/ca0/572/1fbca0572e4d6dc61dd3d07ba23a70f0.png)
Таким образом, исходные данные представлены 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()
![](https://habrastorage.org/getpro/habr/upload_files/67a/6dc/c4b/67a6dcc4b03df37b55fb66742640fa65.png)
Далее есть 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()
![](https://habrastorage.org/getpro/habr/upload_files/62a/733/5f3/62a7335f3b56984ee6a3f198e29ef59e.png)
Самое время окунуться в специфику данных. Дело в том, что наши тексты очень короткие, и, помимо этого, пользователи часто могут писать самые разные общие фразы, которые никак не относятся к тематике обращений, например: «Добрый день», «С новым годом» и т.д. Поэтому мы игнорируем тексты, где после удаления стоп-слов остается только 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')}")
![](https://habrastorage.org/getpro/habr/upload_files/544/940/bc6/544940bc6abe7c1684f82d17bf022d56.png)
Используется 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'])
![](https://habrastorage.org/getpro/habr/upload_files/c8f/252/9a9/c8f2529a9582f31350efee5dca404be4.png)
CatBoost дал нам прирост в качестве ~ 2%.
Таким образом, если задача обязывает модель быть интерпретируемой – можно использовать логистическую регрессию, опираясь на указанные в статье параметры. В иных случаях стоит использовать CatBoost.