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

Однако подавляющее большинство статей и руководств по классификации текста в Интернете представляют собой двоичную классификацию текста, такую ​​как фильтрация спама в электронной почте (спам против ветчины), анализ настроений (положительный или отрицательный). В большинстве случаев наши реальные проблемы намного сложнее. Таким образом, это то, что мы собираемся сделать сегодня: классифицировать жалобы потребительского финансирования на 12 заранее определенных классов. Данные можно скачать с data.gov.

Мы используем Python и Jupyter Notebook для разработки нашей системы, полагаясь на Scikit-Learn для компонентов машинного обучения. Если вы хотите увидеть реализацию в PySpark, прочтите следующую статью.

Постановка проблемы

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

При поступлении новой жалобы мы хотим отнести ее к одной из 12 категорий. Классификатор предполагает, что каждая новая жалоба относится к одной и только одной категории. Это проблема классификации текста на несколько классов. Не могу дождаться, чтобы увидеть, чего мы можем достичь!

Исследование данных

Прежде чем углубляться в обучение моделям машинного обучения, мы должны сначала рассмотреть несколько примеров и количество жалоб в каждом классе:

import pandas as pd
df = pd.read_csv('Consumer_Complaints.csv')
df.head()

Для этого проекта нам понадобятся всего две колонки - «Продукт» и «Описание жалоб потребителей».

  • Ввод: Consumer_complaint_narrative

Пример: «У меня есть устаревшая информация в моем кредитном отчете, которую я ранее оспаривал, но которая еще не была удалена, эта информация старше семи лет и не соответствует требованиям кредитной отчетности»

  • Вывод: продукт

Пример: кредитная отчетность

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

Также мы создаем пару словарей для будущего использования.

После очистки мы будем работать с первыми пятью строками данных:

from io import StringIO
col = ['Product', 'Consumer complaint narrative']
df = df[col]
df = df[pd.notnull(df['Consumer complaint narrative'])]
df.columns = ['Product', 'Consumer_complaint_narrative']
df['category_id'] = df['Product'].factorize()[0]
category_id_df = df[['Product', 'category_id']].drop_duplicates().sort_values('category_id')
category_to_id = dict(category_id_df.values)
id_to_category = dict(category_id_df[['category_id', 'Product']].values)
df.head()

Несбалансированные классы

Мы видим, что количество жалоб на товар несбалансировано. Жалобы потребителей более пристрастны к взысканию долгов, кредитной отчетности и ипотеке.

import matplotlib.pyplot as plt
fig = plt.figure(figsize=(8,6))
df.groupby('Product').Consumer_complaint_narrative.count().plot.bar(ylim=0)
plt.show()

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

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

Текстовое представление

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

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

В частности, для каждого термина в нашем наборе данных мы вычислим меру, называемую Term Frequency, Inverse Document Frequency, сокращенно tf-idf. Мы будем использовать sklearn.feature_extraction.text.TfidfVectorizer , чтобы вычислить tf-idf вектор для каждого описания жалоб потребителей:

  • sublinear_df установлен в True, чтобы использовать логарифмическую форму для частоты.
  • min_df - минимальное количество документов, в которых должно присутствовать слово для сохранения.
  • norm установлен в l2, чтобы гарантировать, что все наши векторы функций имеют евклидову норму, равную 1.
  • ngram_range установлен в (1, 2), чтобы указать, что мы хотим рассматривать как униграммы, так и биграммы.
  • stop_words установлен на "english", чтобы удалить все распространенные местоимения ("a", "the", ...), чтобы уменьшить количество шумных функций.
from sklearn.feature_extraction.text import TfidfVectorizer
tfidf = TfidfVectorizer(sublinear_tf=True, min_df=5, norm='l2', encoding='latin-1', ngram_range=(1, 2), stop_words='english')
features = tfidf.fit_transform(df.Consumer_complaint_narrative).toarray()
labels = df.category_id
features.shape

(4569, 12633)

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

Мы можем использовать sklearn.feature_selection.chi2, чтобы найти термины, которые наиболее коррелируют с каждым из продуктов:

from sklearn.feature_selection import chi2
import numpy as np
N = 2
for Product, category_id in sorted(category_to_id.items()):
  features_chi2 = chi2(features, labels == category_id)
  indices = np.argsort(features_chi2[0])
  feature_names = np.array(tfidf.get_feature_names())[indices]
  unigrams = [v for v in feature_names if len(v.split(' ')) == 1]
  bigrams = [v for v in feature_names if len(v.split(' ')) == 2]
  print("# '{}':".format(Product))
  print("  . Most correlated unigrams:\n. {}".format('\n. '.join(unigrams[-N:])))
  print("  . Most correlated bigrams:\n. {}".format('\n. '.join(bigrams[-N:])))

# ‘Банковский счет или услуга’:
. Наиболее коррелирующие униграммы:
. банк
. овердрафт
. Наиболее коррелированные биграммы:
. комиссия за овердрафт
. текущий счет
# «Потребительский кредит»:
. Наиболее коррелирующие униграммы:
. машина
. автомобиль
. Наиболее коррелированные биграммы:
. автомобиль xxxx
. toyota financial
# ‘Кредитная карта’:
. Наиболее коррелирующие униграммы:
. citi
. карта
. Наиболее коррелированные биграммы:
. годовая плата
. кредитная карта
# ‘Кредитная отчетность’:
. Наиболее коррелирующие униграммы:
. Experian
. эквифакс
. Наиболее коррелированные биграммы:
. транс-союз
. кредитный отчет
# «Взыскание задолженности»:
. Наиболее коррелирующие униграммы:
. сборник
. долг
. Наиболее коррелированные биграммы:
. взыскать долг
. коллекторское агентство
# ‘Денежные переводы’:
. Наиболее коррелирующие униграммы:
. у
. PayPal
. Наиболее коррелированные биграммы:
. Вестерн Юнион
. денежный перевод
# ‘Ипотека’:
. Наиболее коррелирующие униграммы:
. модификация
. ипотека
. Наиболее коррелированные биграммы:
. ипотечная компания
. изменение кредита
# ‘Другие финансовые услуги’:
. Наиболее коррелирующие униграммы:
. стоматологический
. паспорт
. Наиболее коррелированные биграммы:
. Помогите оплатить
. заявленная зарплата
# «Кредит до зарплаты»:
. Наиболее коррелирующие униграммы:
. заимствовал
. день выплаты жалованья
. Наиболее коррелированные биграммы:
. большая картинка
. кредит до зарплаты
# ‘Предоплаченная карта’:
. Наиболее коррелирующие униграммы:
. служить
. предоплата
. Наиболее коррелированные биграммы:
. доступ к деньгам
. предоплаченная карта
# ‘Студенческий кредит’:
. Наиболее коррелирующие униграммы:
. студент
. навигатор
. Наиболее коррелированные биграммы:
. студенческие ссуды
. студенческий заем
# ‘Виртуальная валюта’:
. Наиболее коррелирующие униграммы:
. ручки
. https
. Наиболее коррелированные биграммы:
. xxxx поставщик
. деньги хотят

Все они имеют смысл, не правда ли?

Мультиклассовый классификатор: особенности и дизайн

  • Чтобы обучить контролируемые классификаторы, мы сначала преобразовали «Повествование о жалобах потребителей» в вектор чисел. Мы исследовали векторные представления, такие как взвешенные векторы TF-IDF.
  • Получив это векторное представление текста, мы можем обучить контролируемые классификаторы обучать невидимому «повествованию о жалобах потребителей» и предсказывать «продукт», на который они попадают.

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

  • Наивный байесовский классификатор: для подсчета слов лучше всего подходит полиномиальный вариант:
from sklearn.model_selection import train_test_split
from sklearn.feature_extraction.text import CountVectorizer
from sklearn.feature_extraction.text import TfidfTransformer
from sklearn.naive_bayes import MultinomialNB
X_train, X_test, y_train, y_test = train_test_split(df['Consumer_complaint_narrative'], df['Product'], random_state = 0)
count_vect = CountVectorizer()
X_train_counts = count_vect.fit_transform(X_train)
tfidf_transformer = TfidfTransformer()
X_train_tfidf = tfidf_transformer.fit_transform(X_train_counts)
clf = MultinomialNB().fit(X_train_tfidf, y_train)

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

print(clf.predict(count_vect.transform(["This company refuses to provide me verification and validation of debt per my right under the FDCPA. I do not believe this debt is mine."])))

['Взыскание долгов']

df[df['Consumer_complaint_narrative'] == "This company refuses to provide me verification and validation of debt per my right under the FDCPA. I do not believe this debt is mine."]

print(clf.predict(count_vect.transform(["I am disputing the inaccurate information the Chex-Systems has on my credit report. I initially submitted a police report on XXXX/XXXX/16 and Chex Systems only deleted the items that I mentioned in the letter and not all the items that were actually listed on the police report. In other words they wanted me to say word for word to them what items were fraudulent. The total disregard of the police report and what accounts that it states that are fraudulent. If they just had paid a little closer attention to the police report I would not been in this position now and they would n't have to research once again. I would like the reported information to be removed : XXXX XXXX XXXX"])))

[«Кредитная отчетность»]

df[df['Consumer_complaint_narrative'] == "I am disputing the inaccurate information the Chex-Systems has on my credit report. I initially submitted a police report on XXXX/XXXX/16 and Chex Systems only deleted the items that I mentioned in the letter and not all the items that were actually listed on the police report. In other words they wanted me to say word for word to them what items were fraudulent. The total disregard of the police report and what accounts that it states that are fraudulent. If they just had paid a little closer attention to the police report I would not been in this position now and they would n't have to research once again. I would like the reported information to be removed : XXXX XXXX XXXX"]

Не слишком потрепанный!

Выбор модели

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

Мы протестируем следующие четыре модели:

  • Логистическая регрессия
  • (Полиномиальный) Наивный Байесовский
  • Линейная машина опорных векторов
  • Случайный лес
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import LinearSVC
from sklearn.model_selection import cross_val_score
models = [
    RandomForestClassifier(n_estimators=200, max_depth=3, random_state=0),
    LinearSVC(),
    MultinomialNB(),
    LogisticRegression(random_state=0),
]
CV = 5
cv_df = pd.DataFrame(index=range(CV * len(models)))
entries = []
for model in models:
  model_name = model.__class__.__name__
  accuracies = cross_val_score(model, features, labels, scoring='accuracy', cv=CV)
  for fold_idx, accuracy in enumerate(accuracies):
    entries.append((model_name, fold_idx, accuracy))
cv_df = pd.DataFrame(entries, columns=['model_name', 'fold_idx', 'accuracy'])
import seaborn as sns
sns.boxplot(x='model_name', y='accuracy', data=cv_df)
sns.stripplot(x='model_name', y='accuracy', data=cv_df, 
              size=8, jitter=True, edgecolor="gray", linewidth=2)
plt.show()

cv_df.groupby('model_name').accuracy.mean()

имя_модели
LinearSVC: 0,822890
LogisticRegression: 0,792927
MultinomialNB: 0,688519
RandomForestClassifier: 0.443826
Имя: точность, dtype: float64

LinearSVC и логистическая регрессия работают лучше, чем два других классификатора, при этом LinearSVC имеет небольшое преимущество со средней точностью около 82%.

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

Продолжая нашу лучшую модель (LinearSVC), мы рассмотрим матрицу путаницы и покажем расхождения между предсказанными и фактическими метками.

model = LinearSVC()
X_train, X_test, y_train, y_test, indices_train, indices_test = train_test_split(features, labels, df.index, test_size=0.33, random_state=0)
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
from sklearn.metrics import confusion_matrix
conf_mat = confusion_matrix(y_test, y_pred)
fig, ax = plt.subplots(figsize=(10,10))
sns.heatmap(conf_mat, annot=True, fmt='d',
            xticklabels=category_id_df.Product.values, yticklabels=category_id_df.Product.values)
plt.ylabel('Actual')
plt.xlabel('Predicted')
plt.show()

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

from IPython.display import display
for predicted in category_id_df.category_id:
  for actual in category_id_df.category_id:
    if predicted != actual and conf_mat[actual, predicted] >= 10:
      print("'{}' predicted as '{}' : {} examples.".format(id_to_category[actual], id_to_category[predicted], conf_mat[actual, predicted]))
      display(df.loc[indices_test[(y_test == actual) & (y_pred == predicted)]][['Product', 'Consumer_complaint_narrative']])
      print('')

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

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

model.fit(features, labels)
N = 2
for Product, category_id in sorted(category_to_id.items()):
  indices = np.argsort(model.coef_[category_id])
  feature_names = np.array(tfidf.get_feature_names())[indices]
  unigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 1][:N]
  bigrams = [v for v in reversed(feature_names) if len(v.split(' ')) == 2][:N]
  print("# '{}':".format(Product))
  print("  . Top unigrams:\n       . {}".format('\n       . '.join(unigrams)))
  print("  . Top bigrams:\n       . {}".format('\n       . '.join(bigrams)))

# ‘Банковский счет или услуга’:
. Лучшие униграммы:
. банк
. аккаунт
. Лучшие биграммы:
. дебетовая карта
. Комиссия за овердрафт
# «Потребительский кредит»:
. Лучшие униграммы:
. автомобиль
. машина
. Лучшие биграммы:
. личный заем
. история xxxx
# «Кредитная карта»:
. Лучшие униграммы:
. карта
. узнать
. Лучшие биграммы:
. кредитная карта
. откройте карту
# ‘Кредитная отчетность’:
. Лучшие униграммы:
. эквифакс
. трансюньон
. Лучшие биграммы:
. аккаунт xxxx
. trans union
# ‘Взыскание долга’:
. Лучшие униграммы:
. долг
. сборник
. Лучшие биграммы:
. кредит на счет
. предоставленное время
# ‘Денежные переводы’:
. Лучшие униграммы:
. PayPal
. передача
. Лучшие биграммы:
. денежный перевод
. отправить деньги
# ‘Ипотека’:
. Лучшие униграммы:
. ипотека
. условное депонирование
. Лучшие биграммы:
. изменение кредита
. ипотечная компания
# ‘Другие финансовые услуги’:
. Лучшие униграммы:
. паспорт
. стоматологический
. Лучшие биграммы:
. заявлено вознаграждение
. помогите оплатить
# ‘Кредит до зарплаты’:
. Лучшие униграммы:
. день выплаты жалованья
. кредит
. Лучшие биграммы:
. кредит до зарплаты
. день выплаты
# ‘Предоплаченная карта’:
. Лучшие униграммы:
. предоплата
. служить
. Лучшие биграммы:
. карта предоплаты
. используйте карту
# ‘Студенческий кредит’:
. Лучшие униграммы:
. навигатор
. кредиты
. Лучшие биграммы:
. студенческий кредит
. sallie mae
# ‘Виртуальная валюта’:
. Лучшие униграммы:
. https
. tx
. Лучшие биграммы:
. денег хочу
. xxxx провайдер

Они соответствуют нашим ожиданиям.

Наконец, мы распечатываем отчет о классификации для каждого класса:

from sklearn import metrics
print(metrics.classification_report(y_test, y_pred, target_names=df['Product'].unique()))

Исходный код можно найти на Github. Я с нетерпением жду любых отзывов или вопросов.