教程 | 用Scikit-Learn實現多類別文字分類

七月線上實驗室發表於2018-05-14

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

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

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


輸入: 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()

不平衡類

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

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

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

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


文字表達

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

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

具體來說,對於我們資料集中的每一項,我們將計算一種被稱為詞頻、反向文件頻率的值,其縮寫為 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:])))

它們都有道理,難道不是嗎?


多類別分類器:特徵和設計

  • 為了訓練監督式分類器,我們首先將「消費者投訴敘述」轉化為數字向量。我們研究了向量表示,例如 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"]

不是太寒酸!


模型選擇

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

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

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

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

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

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

它們符合我們的預期。

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

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

【機器學習集訓營 四期】今晚(5月14日)開課,北京、上海、深圳、廣州、杭州、瀋陽、濟南、鄭州、成都九城同步開營。線上+線下授課模式,BAT專家面對面、手把手教學;

10個工業專案實戰輔導 + 一對一面試求職輔導,真正幫你從零轉型機器學習工程師!

相關文章