如何通過Scikit-Learn實現多類別文字分類?

機器之心發表於2018-03-05

網際網路的絕大多數的文字分類都是二進位制的,本文要解決的問題更為複雜。作者使用 Python 和 Jupyter Notebook 開發系統,並藉助 Scikit-Learn 實現了消費者金融投訴的 12 個預定義分類。本專案的 GitHub 地址見文中。

GitHub 地址:https://github.com/susanli2016/Machine-Learning-with-Python/blob/master/Consumer_complaints.ipynb

商業活動中有很多文字分類應用。例如,新聞報導通常是按照主題進行構架;內容或產品通常是根據類別新增標籤;可以根據使用者如何線上討論某個產品或品牌將其分為多個群組......

然而,網際網路上絕大多數的文字分類文章和教程都是二進位制文字分類,比如垃圾郵件過濾,情感分析。大多數情況下,現實世界的問題更為複雜。因此,這就是我們今天要做的事情:將消費者的金融投訴分為 12 個預定義的類別。

我們使用 Python 和 Jupyter Notebook 開發系統,機器學習方面則藉助 Scikit-Learn。如果你想要 PySpark 實現,請閱讀下篇文章。

問題表述

該問題是監督式文字分類問題,我們的目標是調查哪種監督式機器學習方法最適合解決它。

當出現新投訴時,我們希望將其分配到 12 個類別中的一個。分類器假設每個新投訴都被分配到一個且僅一個的類別之中。這是多類別文字分類問題。我迫不及待想看到我們能實現什麼!

資料探索

在深入訓練機器學習模型之前,我們首先應該看一些例項,以及每個類別的投訴數量:

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

如何通過Scikit-Learn實現多類別文字分類?

對於這個專案,我們只需要兩欄——「產品」和「消費者投訴敘述」。

輸入: Consumer_complaint_narrative

例項:「我的信用報告中有過時的資訊,我以前有爭議的是這些資訊已超過七年未被刪除,並且不符合信用報告的要求」

輸出:product

例項:信用報告

我們將刪除「消費者投訴敘述」欄中的缺失值,並新增一列來將產品編碼為整數,因為分類變數通常用整數表示比用字串更好。

我們還建立了幾個字典供將來使用。

清理完成後,這是我們將要處理的前五行資料:

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()

如何通過Scikit-Learn實現多類別文字分類?

不平衡類

我們看到每件產品的投訴數量不平衡。消費者的投訴更集中於收取欠款、信用報告和抵押方面。

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

如何通過Scikit-Learn實現多類別文字分類?

當我們遇到這樣的問題時,我們使用標準演算法解決這些問題必然會遇到困難。常規演算法往往偏向於多數類別,而不考慮資料分佈。在最糟糕的情況下,少數類別被視為異常值並被忽略。對於某些情況,如欺詐檢測或癌症預測,我們則需要仔細配置我們的模型或人為地平衡資料集,比如欠取樣或過取樣每個類別。

但是,在學習不平衡資料的情況下,我們最感興趣的是多數類。我們想有一個分類器,能夠對多數類提供較高的預測精度,同時對少數類保持合理的準確度。因此我們會保持原樣。

文字表達

分類器和學習演算法不能直接處理原始形式的文字文件,因為它們大多數都期望大小固定的數字特徵向量而不是具有可變長度的原始文字文件。因此,在預處理步驟中,文字被轉換為更易於管理的表達。

從文字中提取特徵的一種常見方法是使用詞袋模型:對於每個文件,我們案例中的投訴敘述、單詞的出現(通常是頻率)被考慮在內,而它們出現順序則被忽略。

具體來說,對於我們資料集中的每一項,我們將計算一種被稱為詞頻、反向文件頻率的值,其縮寫為 tf-idf。我們將使用 sklearn.feature_extraction.text.TfidfVectorizer 為每個消費者投訴敘述計算一個 tf-idf 向量。

  • sublinear_df 設為 True 從而使用頻率的對數形式。

  • min_df 是單詞必須存在的最小文件數量。

  • norm 設為 l2,以確保我們所有特徵向量的歐幾里德範數為 1。

  • ngram_range 設為 (1, 2),表示我們想要考慮 unigrams 和 bigrams。

  • 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 個特徵表達,代表不同的 unigrams 和 bigrams 的 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:])))

# 『銀行賬戶或服務』:

 . 最相關的 unigrams:

 . 銀行

 . 透支

 . 最相關的 bigrams:

 . 透支費

 . 支票賬戶

 # 『消費者貸款』:

 . 最相關的 unigrams:

 . 車

 . 交通工具

 . 最相關的 bigrams:

 . 交通工具 xxxx

 . 豐田金融

 # 『信用卡』:

 . 最相關的 unigrams:

 . 花旗

 . 卡

 . 最相關的 bigrams:

 . 年費

 . 信用卡

 # 『信用報告』:

 . 最相關的 unigrams:

 . 益百利

 . equifax

 . 最相關的 bigrams:

 . 全聯公司

 . 信用報告

 # 『討回欠款』:

 . 最相關的 unigrams:

 . 收集

 . 債務

 . 最相關的 bigrams:

 . 討回全款

 . 討債公司

 # 『匯款』:

 . 最相關的 unigrams:

 . wu

 . paypal

 . 最相關的 bigrams:

 . 西聯匯款

 . 匯款

 # 『抵押』:

 . 最相關的 unigrams:

 . 修正

 . 抵押

 . 最相關的 bigrams:

 . 抵押公司

 . 貸款修改

 # 『其他金融服務』:

 . 最相關的 unigrams:

 . 牙齒

 . 護照

 . 最相關的 bigrams:

 . 幫助支付

 . 規定支付

 # 『發薪日貸款』:

 . 最相關的 unigrams:

 . 借款

 . 發薪日

 . 最相關的 bigrams:

 . 大圖片

 . 發薪日貸款

 # 『預付卡』:

 . 最相關的 unigrams:

 . 服務

 . 充值

 . 最相關的 bigrams:

 . 獲得資金

 . 預付卡

 # 『學生貸款』:

 . 最相關的 unigrams:

 . 學生

 . navient

 . 最相關的 bigrams:

 . student loans

 . student loan

 # 『虛擬貨幣』:

 . 最相關的 unigrams:

 . 手柄

 . https

 . 最相關的 bigrams:

 . 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."]

如何通過Scikit-Learn實現多類別文字分類?

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"]

如何通過Scikit-Learn實現多類別文字分類?

不是太寒酸!

模型選擇

我們現在準備嘗試不同的機器學習模型,評估它們的準確性並找出潛在問題的根源。

我們將對以下四種模型進行基準測試:

  • Logistic 迴歸

  • (多項式)樸素貝葉斯

  • 線性支援向量機

  • 隨機森林

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()

如何通過Scikit-Learn實現多類別文字分類?

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

模型名稱

  • 線性支援向量機:0.822890

  • Logistic 迴歸:0.792927

  • (多項式)樸素貝葉斯:0.688519

  • 隨機森林:0.443826

名稱:精確度,dtype:float64

線性支援向量機和 Logistic 迴歸比其他兩個分類器執行的更好,前者具有輕微的優勢,其中位精度約為 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()

如何通過Scikit-Learn實現多類別文字分類?

正如我們所希望的,絕大多數預測都在對角線結束(預測標籤=實際標籤)。然而,仍然存在大量錯誤分類,看看這些是由什麼造成的可能很有趣:

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('')

如何通過Scikit-Learn實現多類別文字分類?

如何通過Scikit-Learn實現多類別文字分類?

如你所見,一些錯誤分類的投訴涉及多個主題(比如涉及信用卡和信用報告的投訴)。這種錯誤總是發生。

再次,我們使用卡方檢驗來找到與每個類別最相關的項:

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)))

# 『銀行賬戶或服務』:

 . 最高的 unigrams:

 . 銀行

 . 賬戶

 . 最高的 bigrams:

 . 借記卡

 . 透支費用

# 『消費者貸款』:

 . 最高的 unigrams:

 . 交通工具

 . 車

 . 最高的 bigrams:

 . 個人貸款

 . 歷史 xxxx

# 『信用卡』:

 . 最高的 unigrams:

 . 卡

 . 發現

 . 最高的 bigrams:

 . 信用卡

 . 發現卡

# 『信用報告』:

 . 最高的 unigrams:

 . equifax

 . 全聯公司

 . 最高的 bigrams:

 . xxxx 賬戶

 . 全聯公司

# 『討回欠款』:

 . 最高的 unigrams:

 . 債務

 . 收集

 . 最高的 bigrams:

 . 賬戶信用

 . 時間提供

# 『匯款』:

 . 最高的 unigrams:

 . paypal

 . 匯款

 . 最高的 bigrams:

 . 匯款

 . 寄錢

# 『抵押』:

 . 最高的 unigrams:

 . 抵押

 . 國際支付寶

 . 最高的 bigrams:

 . 貸款修改

 . 抵押公司

# 『其他金融服務』:

 . 最高的 unigrams:

 . 護照

 . 牙齒

 . 最高的 bigrams:

 . 規定支付

 . 幫助支付

# 『發薪日貸款』:

 . 最高的 unigrams:

 . 發薪日

 . 貸款

 . 最高的 bigrams:

 . 發薪日貸款

 . 發薪日

# 『預付卡』:

 . 最高的 unigrams:

 . 充值

 . 服務

 . 最高的 bigrams:

 . 預付卡

 . 使用卡

# 『學生貸款』:

 . 最高的 unigrams:

 . navient

 . 貸款

 . 最高的 bigrams:

 . 學生貸款

 . sallie mae

# 『虛擬貨幣』:

 . 最高的 unigrams:

 . https

 . tx

 . 最高的 bigrams:

 . 想要錢

 . xxxx 提供者

它們符合我們的預期。

最後,我們列印出每個類的分類報告:

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

如何通過Scikit-Learn實現多類別文字分類?

原文連結:https://towardsdatascience.com/multi-class-text-classification-with-scikit-learn-12f1e60e0a9f

相關文章