(第一部分 機器學習基礎) 第01章 機器學習概覽 第02章 一個完整的機器學習項目(上) 第02章 一個完整的機器學習項目(下) 第03章 分類 第04章 訓練模型 第05章 支援向量機 第06章 決策樹 第07章 內建學習和随機森林 第08章 降維 (第二部分 神經網絡和深度學習) 第9章 啟動和運作TensorFlow 在第一章我們提到過最常用的監督學習任務是回歸(用于預測某個值)和分類(預測某個類别)。在第二章我們探索了一個回歸任務:預測房價。我們使用了多種算法,諸如線性回歸,決策樹,和随機森林(這個将會在後面的章節更詳細地讨論)。現在我們将我們的注意力轉到分類任務上。
MNIST
本章中,我們會使用 MNIST 這個資料集,它有 70000 張規格較小的手寫數字圖檔,由美國的高中生和美國人口調查局的職員手寫而成。這相當于機器學習當中的“Hello World”,人們無論什麼時候提出一個新的分類算法,都想知道該算法在這個資料集上的表現如何。機器學習的初學者遲早也會處理 MNIST 這個資料集。
Scikit-Learn 提供了許多輔助函數,以便于下載下傳流行的資料集。MNIST 是其中一個。用下面的代碼擷取 MNIST:
>>> from sklearn.datasets import fetch_mldata
>>> mnist = fetch_mldata('MNIST original')
>>> mnist
{'COL_NAMES': ['label', 'data'],
'DESCR': 'mldata.org dataset: mnist-original',
'data': array([[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
...,
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0],
[0, 0, 0, ..., 0, 0, 0]], dtype=uint8),
'target': array([ 0., 0., 0., ..., 9., 9., 9.])}
一般而言,由 sklearn 加載的資料集有着相似的字典結構,這包括:
-
鍵描述資料集DESCR
-
鍵存放一個數組,數組的一行表示一個執行個體,一清單示一個特征data
-
鍵存放一個标簽數組target
讓我們看一下這些數組
>>> X, y = mnist["data"], mnist["target"]
>>> X.shape
(70000, 784)
>>> y.shape
(70000,)
MNIST 有 70000 張圖檔,每張圖檔有 784 個特征。這是因為每個圖檔都是
28*28
像素的,并且每個像素的值介于 0~255 之間。讓我們看一看資料集的某一個數字。你隻需要将某個執行個體的特征向量,
reshape
為
28*28
的數組,然後使用 Matplotlib 的
imshow
函數展示出來。
%matplotlib inline
import matplotlib
import matplotlib.pyplot as plt
some_digit = X[36000]
some_digit_image = some_digit.reshape(28, 28)
plt.imshow(some_digit_image, cmap = matplotlib.cm.binary, interpolation="nearest")
plt.axis("off")
plt.show()
這看起來像個 5,實際上它的标簽告訴我們:
>>> y[36000]
5.0
圖3-1 展示了一些來自 MNIST 資料集的圖檔。當你處理更加複雜的分類任務的時候,它會讓你更有感覺。
圖3-1 MNIST資料集的一些數字圖檔
先等一下!你總是應該先建立測試集,并且在驗證資料之前先把測試集晾到一邊。MNIST 資料集已經事先被分成了一個訓練集(前 60000 張圖檔)和一個測試集(最後 10000 張圖檔)
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
讓我們打亂訓練集。這可以保證交叉驗證的每一折都是相似(你不會期待某一折缺少某類數字)。而且,一些學習算法對訓練樣例的順序敏感,當它們在一行當中得到許多相似的樣例,這些算法将會表現得非常差。打亂資料集将保證這種情況不會發生。
import numpy as np
shuffle_index = np.random.permutation(60000)
X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]
訓練一個二分類器
現在我們簡化一下問題,隻嘗試去識别一個數字,比如說,數字 5。這個“數字 5 檢測器”就是一個二分類器,能夠識别兩類别,“是 5”和“非 5”。讓我們為這個分類任務建立目标向量:
y_train_5 = (y_train == 5) # True for all 5s, False for all other digits.
y_test_5 = (y_test == 5)
現在讓我們挑選一個分類器去訓練它。用随機梯度下降分類器 SGD,是一個不錯的開始。使用 Scikit-Learn 的
SGDClassifier
類。這個分類器有一個好處是能夠高效地處理非常大的資料集。這部分原因在于SGD一次隻處理一條資料,這也使得 SGD 适合線上學習(online learning)。我們在稍後會看到它。讓我們建立一個
SGDClassifier
和在整個資料集上訓練它。
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
依賴于訓練集的随機程度(是以被命名為 stochastic,随機之義)。如果你想重制結果,你應該固定參數
SGDClassifier
random_state
現在你可以用它來查出數字 5 的圖檔。
>>> sgd_clf.predict([some_digit])
array([ True], dtype=bool)
分類器猜測這個數字代表 5(
True
)。看起來在這個例子當中,它猜對了。現在讓我們評估這個模型的性能。
對性能的評估
評估一個分類器,通常比評估一個回歸器更加玄學。是以我們将會花大量的篇幅在這個話題上。有許多度量性能的方法,是以拿來一杯咖啡和準備學習許多新概念和首字母縮略詞吧。
使用交叉驗證測量準确性
評估一個模型的好方法是使用交叉驗證,就像第二章所做的那樣。
實作交叉驗證
在交叉驗證過程中,有時候你會需要更多的控制權,相較于函數
或者其他相似函數所提供的功能。這種情況下,你可以實作你自己版本的交叉驗證。事實上它相當簡單。以下代碼粗略地做了和
cross_val_score()
相同的事情,并且輸出相同的結果。
cross_val_score()
from sklearn.model_selection import StratifiedKFold from sklearn.base import clone skfolds = StratifiedKFold(n_splits=3, random_state=42) for train_index, test_index in skfolds.split(X_train, y_train_5): clone_clf = clone(sgd_clf) X_train_folds = X_train[train_index] y_train_folds = (y_train_5[train_index]) X_test_fold = X_train[test_index] y_test_fold = (y_train_5[test_index]) clone_clf.fit(X_train_folds, y_train_folds) y_pred = clone_clf.predict(X_test_fold) n_correct = sum(y_pred == y_test_fold) print(n_correct / len(y_pred)) # prints 0.9502, 0.96565 and 0.96495
類實作了分層采樣(詳見第二章的解釋),生成的折(fold)包含了各類相應比例的樣例。在每一次疊代,上述代碼生成分類器的一個克隆版本,在訓練折(training folds)的克隆版本上進行訓,在測試折(test folds)上進行預測。然後它計算出被正确預測的數目和輸出正确預測的比例。
StratifiedKFold
讓我們使用
cross_val_score()
函數來評估
SGDClassifier
模型,同時使用 K 折交叉驗證,此處讓
k=3
。記住:K 折交叉驗證意味着把訓練集分成 K 折(此處 3 折),然後使用一個模型對其中一折進行預測,對其他折進行訓練。
>>> from sklearn.model_selection import cross_val_score
>>> cross_val_score(sgd_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([ 0.9502 , 0.96565, 0.96495]
哇!在交叉驗證上有大于 95% 的精度(accuracy)?這看起來很令人吃驚。先别高興,讓我們來看一個非常笨的分類器去分類,看看其在“非 5”這個類上的表現。
from sklearn.base import BaseEstimator
class Never5Classifier(BaseEstimator):
def fit(self, X, y=None):
pass
def predict(self, X):
return np.zeros((len(X), 1), dtype=bool)
你能猜到這個模型的精度嗎?揭曉謎底:
>>> never_5_clf = Never5Classifier()
>>> cross_val_score(never_5_clf, X_train, y_train_5, cv=3, scoring="accuracy")
array([ 0.909 , 0.90715, 0.9128 ])
沒錯,這個笨的分類器也有 90% 的精度。這是因為隻有 10% 的圖檔是數字 5,是以你總是猜測某張圖檔不是 5,你也會有90%的可能性是對的。
這證明了為什麼精度通常來說不是一個好的性能度量名額,特别是當你處理有偏差的資料集,比方說其中一些類比其他類頻繁得多。
混淆矩陣
對分類器來說,一個好得多的性能評估名額是混淆矩陣。大體思路是:輸出類别A被分類成類别 B 的次數。舉個例子,為了知道分類器将 5 誤分為 3 的次數,你需要檢視混淆矩陣的第五行第三列。
為了計算混淆矩陣,首先你需要有一系列的預測值,這樣才能将預測值與真實值做比較。你或許想在測試集上做預測。但是我們現在先不碰它。(記住,隻有當你處于項目的尾聲,當你準備上線一個分類器的時候,你才應該使用測試集)。相反,你應該使用
cross_val_predict()
函數
from sklearn.model_selection import cross_val_predict
y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)
就像
cross_val_score()
,
cross_val_predict()
也使用 K 折交叉驗證。它不是傳回一個評估分數,而是傳回基于每一個測試折做出的一個預測值。這意味着,對于每一個訓練集的樣例,你得到一個幹淨的預測(“幹淨”是說一個模型在訓練過程當中沒有用到測試集的資料)。
現在使用
confusion_matrix()
函數,你将會得到一個混淆矩陣。傳遞目标類(
y_train_5
)和預測類(
y_train_pred
)給它。
>>> from sklearn.metrics import confusion_matrix
>>> confusion_matrix(y_train_5, y_train_pred)
array([[53272, 1307],
[ 1077, 4344]])
混淆矩陣中的每一行表示一個實際的類, 而每一清單示一個預測的類。該矩陣的第一行認為“非 5”(反例)中的 53272 張被正确歸類為 “非 5”(他們被稱為真反例,true negatives), 而其餘 1307 被錯誤歸類為"是 5" (假正例,false positives)。第二行認為“是 5” (正例)中的 1077 被錯誤地歸類為“非 5”(假反例,false negatives),其餘 4344 正确分類為 “是 5”類(真正例,true positives)。一個完美的分類器将隻有真反例和真正例,是以混淆矩陣的非零值僅在其主對角線(左上至右下)。
>>> confusion_matrix(y_train_5, y_train_perfect_predictions)
array([[54579, 0],
[ 0, 5421]])
混淆矩陣可以提供很多資訊。有時候你會想要更加簡明的名額。一個有趣的名額是正例預測的精度,也叫做分類器的準确率(precision)。
公式 3-1 準确率
其中 TP 是真正例的數目,FP 是假正例的數目。
想要一個完美的準确率,一個繁複的方法是構造一個單一正例的預測和,并確定這個預測是正确的(
precision = 1/1 = 100%
)。但是這什麼用,因為分類器會忽略所有樣例,除了那一個正例。是以準确率一般會伴随另一個名額一起使用,這個名額叫做召回率(recall),也叫做敏感度(sensitivity)或者真正例率(true positive rate, TPR)。這是正例被分類器正确探測出的比率。
公式 3-2 Recall
FN 是假反例的數目。
如果你對于混淆矩陣感到困惑,圖 3-2 将對你有幫助。
圖3-2 混淆矩陣示意圖
準确率與召回率
Scikit-Learn 提供了一些函數去計算分類器的名額,包括準确率和召回率。
>>> from sklearn.metrics import precision_score, recall_score
>>> precision_score(y_train_5, y_pred) # == 4344 / (4344 + 1307)
0.76871350203503808
>>> recall_score(y_train_5, y_train_pred) # == 4344 / (4344 + 1077)
0.79136690647482011
當你去觀察精度的時候,你的“數字 5 探測器”看起來還不夠好。當它聲明某張圖檔是 5 的時候,它隻有 77% 的可能性是正确的。而且,它也隻檢測出“是 5”類圖檔當中的 79%。
通常結合準确率和召回率會更加友善,這個名額叫做“F1 值”,特别是當你需要一個簡單的方法去比較兩個分類器的優劣的時候。F1 值是準确率和召回率的調和平均。普通的平均值平等地看待所有的值,而調和平均會給小的值更大的權重。是以,要想分類器得到一個高的 F1 值,需要召回率和準确率同時高。
公式 3-3 F1 值
為了計算 F1 值,簡單調用
f1_score()
>>> from sklearn.metrics import f1_score
>>> f1_score(y_train_5, y_pred)
0.78468208092485547
F1 支援那些有着相近準确率和召回率的分類器。這不會總是你想要的。有的場景你會更關心準确率,而另外一些場景你會更關心召回率。舉例,如果你訓練一個分類器去檢測視訊是否适合兒童觀看,你會傾向選擇那種即便拒絕了很多好視訊、但保證所保留的視訊都是好(高準确率)的分類器,而不是那種高召回率、但讓壞視訊混入的分類器(這種情況下你或許想增加人工去檢測分類器選擇出來的視訊)。另一方面,加入你訓練一個分類器去檢測監控圖像當中的竊賊,有着 30% 準确率、99% 召回率的分類器或許是合适的(當然,警衛會得到一些錯誤的報警,但是幾乎所有的竊賊都會被抓到)。
不幸的是,你不能同時擁有兩者。增加準确率會降低召回率,反之亦然。這叫做準确率與召回率之間的折衷。
準确率/召回率之間的折衷
為了弄懂這個折衷,我們看一下
SGDClassifier
是如何做分類決策的。對于每個樣例,它根據決策函數計算分數,如果這個分數大于一個門檻值,它會将樣例配置設定給正例,否則它将配置設定給反例。圖 3-3 顯示了幾個數字從左邊的最低分數排到右邊的最高分。假設決策門檻值位于中間的箭頭(介于兩個 5 之間):您将發現4個真正例(數字 5)和一個假正例(數字 6)在該門檻值的右側。是以,使用該門檻值,準确率為 80%(4/5)。但實際有 6 個數字 5,分類器隻檢測 4 個, 是以召回是 67% (4/6)。現在,如果你
提高門檻值(移動到右側的箭頭),假正例(數字 6)成為一個真反例,進而提高準确率(在這種情況下高達 100%),但一個真正例 變成假反例,召回率降低到 50%。相反,降低門檻值可提高召回率、降低準确率。
圖3-3 決策門檻值與準确度/召回率折衷
Scikit-Learn 不讓你直接設定門檻值,但是它給你提供了設定決策分數的方法,這個決策分數可以用來産生預測。它不是調用分類器的
predict()
方法,而是調用
decision_function()
方法。這個方法傳回每一個樣例的分數值,然後基于這個分數值,使用你想要的任何門檻值做出預測。
>>> y_scores = sgd_clf.decision_function([some_digit])
>>> y_scores
array([ 161855.74572176])
>>> threshold = 0
>>> y_some_digit_pred = (y_scores > threshold)
array([ True], dtype=bool)
SGDClassifier
用了一個等于 0 的門檻值,是以前面的代碼傳回了跟
predict()
方法一樣的結果(都傳回了
true
)。讓我們提高這個門檻值:
>>> threshold = 200000
>>> y_some_digit_pred = (y_scores > threshold)
>>> y_some_digit_pred
array([False], dtype=bool)
這證明了提高門檻值會降調召回率。這個圖檔實際就是數字 5,當門檻值等于 0 的時候,分類器可以探測到這是一個 5,當門檻值提高到 20000 的時候,分類器将不能探測到這是數字 5。
那麼,你應該如何使用哪個門檻值呢?首先,你需要再次使用
cross_val_predict()
得到每一個樣例的分數值,但是這一次指定傳回一個決策分數,而不是預測值。
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
method="decision_function")
現在有了這些分數值。對于任何可能的門檻值,使用
precision_recall_curve()
,你都可以計算準确率和召回率:
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
最後,你可以使用 Matplotlib 畫出準确率和召回率(圖 3-4),這裡把準确率和召回率當作是門檻值的一個函數。
def plot_precision_recall_vs_threshold(precisions, recalls, thresholds):
plt.plot(thresholds, precisions[:-1], "b--", label="Precision")
plt.plot(thresholds, recalls[:-1], "g-", label="Recall")
plt.xlabel("Threshold")
plt.legend(loc="upper left")
plt.ylim([0, 1])
plot_precision_recall_vs_threshold(precisions, recalls, thresholds)
plt.show()
圖3-4 準确率和召回率和決策門檻值的關系
筆記:你也許會好奇為什麼準确率曲線比召回率曲線更加起伏不平。原因是準确率有時候會降低,盡管當你提高門檻值的時候,通常來說準确率會随之提高。回頭看圖 3-3,留意當你從中間箭頭開始然後向右移動一個數字會發生什麼: 準确率會由 4/5(80%)降到 3/4(75%)。另一方面,當門檻值提高時候,召回率隻會降低。這也就說明了為什麼召回率的曲線更加平滑。
現在你可以選擇适合你任務的最佳門檻值。另一個選出好的準确率/召回率折衷的方法是直接畫出準确率對召回率的曲線,如圖 3-5 所示。
圖3-5 準确率vs召回率
可以看到,在召回率在 80% 左右的時候,準确率急劇下降。你可能會想選擇在急劇下降之前選擇出一個準确率/召回率折衷點。比如說,在召回率 60% 左右的點。當然,這取決于你的項目需求。
我們假設你決定達到 90% 的準确率。你查閱第一幅圖(放大一些),在 70000 附近找到一個門檻值。為了作出預測(目前為止隻在訓練集上預測),你可以運作以下代碼,而不是運作分類器的
predict()
方法。
y_train_pred_90 = (y_scores > 70000)
讓我們檢查這些預測的準确率和召回率:
>>> precision_score(y_train_5, y_train_pred_90)
0.8998702983138781
>>> recall_score(y_train_5, y_train_pred_90)
0.63991883416343853
很棒!你擁有了一個(近似) 90% 準确率的分類器。它相當容易去建立一個任意準确率的分類器,隻要将門檻值設定得足夠高。但是,一個高準确率的分類器不是非常有用,如果它的召回率太低!
如果有人說“讓我們達到 99% 的準确率”,你應該問“相應的召回率是多少?”
ROC 曲線
受試者工作特征(ROC)曲線是另一個二分類器常用的工具。它非常類似于準确率/召回率曲線,但不是畫出準确率對召回率的曲線,ROC 曲線是真正例率(true positive rate,另一個名字叫做召回率)對假正例率(false positive rate, FPR)的曲線。FPR 是反例被錯誤分成正例的比率。它等于 1 減去真反例率(true negative rate, TNR)。TNR是反例被正确分類的比率。TNR也叫做特異性。是以 ROC 曲線畫出召回率對(1 減特異性)的曲線。
為了畫出 ROC 曲線,你首先需要計算各種不同門檻值下的 TPR、FPR,使用
roc_curve()
函數:
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
然後你可以使用 matplotlib,畫出 FPR 對 TPR 的曲線。下面的代碼生成圖 3-6.
def plot_roc_curve(fpr, tpr, label=None):
plt.plot(fpr, tpr, linewidth=2, label=label)
plt.plot([0, 1], [0, 1], 'k--')
plt.axis([0, 1, 0, 1])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plot_roc_curve(fpr, tpr)
plt.show()
圖3-6 ROC曲線
這裡同樣存在折衷的問題:召回率(TPR)越高,分類器就會産生越多的假正例(FPR)。圖中的點線是一個完全随機的分類器生成的 ROC 曲線;一個好的分類器的 ROC 曲線應該盡可能遠離這條線(即向左上角方向靠攏)。
一個比較分類器優劣的方法是:測量ROC曲線下的面積(AUC,area under the curve)。一個完美的分類器的 ROC AUC 等于 1,而一個純随機分類器的 ROC AUC 等于 0.5。Scikit-Learn 提供了一個函數來計算 ROC AUC:
>>> from sklearn.metrics import roc_auc_score
>>> roc_auc_score(y_train_5, y_scores)
0.97061072797174941
因為 ROC 曲線跟準确率/召回率曲線(或者叫 PR)很類似,你或許會好奇如何決定使用哪一個曲線呢?一個笨拙的規則是,當正例很少,或者當你關注假正例多于假反例的時候,優先使用 PR 曲線。其他情況使用 ROC 曲線。舉例子,回顧前面的 ROC 曲線和 ROC AUC 數值,你或許認為這個分類器很棒。但是這幾乎全是因為隻有少數正例(“是 5”),而大部分是反例(“非 5”)。相反,PR 曲線清楚顯示出這個分類器還有很大的改善空間(PR 曲線應該盡可能地靠近右上角)。
讓我們訓練一個
RandomForestClassifier
,然後拿它的的ROC曲線和ROC AUC數值去跟
SGDClassifier
的比較。首先你需要得到訓練集每個樣例的數值。但是由于随機森林分類器的工作方式,
RandomForestClassifier
不提供
decision_function()
方法。相反,它提供了
predict_proba()
方法。Skikit-Learn分類器通常二者中的一個。
predict_proba()
方法傳回一個數組,數組的每一行代表一個樣例,每一列代表一個類。數組當中的值的意思是:給定一個樣例屬于給定類的機率。比如,70%的機率這幅圖是數字 5。
from sklearn.ensemble import RandomForestClassifier
forest_clf = RandomForestClassifier(random_state=42)
y_probas_forest = cross_val_predict(forest_clf, X_train, y_train_5, cv=3,
method="predict_proba")
但是要畫 ROC 曲線,你需要的是樣例的分數,而不是機率。一個簡單的解決方法是使用正例的機率當作樣例的分數。
y_scores_forest = y_probas_forest[:, 1] # score = proba of positive class
fpr_forest, tpr_forest, thresholds_forest = roc_curve(y_train_5,y_scores_forest)
現在你即将得到 ROC 曲線。将前面一個分類器的 ROC 曲線一并畫出來是很有用的,可以清楚地進行比較。見圖 3-7。
plt.plot(fpr, tpr, "b:", label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.legend(loc="bottom right")
plt.show()
圖3-7 比較ROC曲線
如你所見,
RandomForestClassifier
的 ROC 曲線比
SGDClassifier
的好得多:它更靠近左上角。是以,它的 ROC AUC 也會更大。
>>> roc_auc_score(y_train_5, y_scores_forest)
0.99312433660038291
計算一下準确率和召回率:98.5% 的準确率,82.8% 的召回率。還不錯。
現在你知道如何訓練一個二分類器,選擇合适的标準,使用交叉驗證去評估你的分類器,選擇滿足你需要的準确率/召回率折衷方案,和比較不同模型的 ROC 曲線和 ROC AUC 數值。現在讓我們檢測更多的數字,而不僅僅是一個數字 5。
多類分類
二分類器隻能區分兩個類,而多類分類器(也被叫做多項式分類器)可以區分多于兩個類。
一些算法(比如随機森林分類器或者樸素貝葉斯分類器)可以直接處理多類分類問題。其他一些算法(比如 SVM 分類器或者線性分類器)則是嚴格的二分類器。然後,有許多政策可以讓你用二分類器去執行多類分類。
舉例子,建立一個可以将圖檔分成 10 類(從 0 到 9)的系統的一個方法是:訓練10個二分類器,每一個對應一個數字(探測器 0,探測器 1,探測器 2,以此類推)。然後當你想對某張圖檔進行分類的時候,讓每一個分類器對這個圖檔進行分類,選出決策分數最高的那個分類器。這叫做“一對所有”(OvA)政策(也被叫做“一對其他”)。
另一個政策是對每一對數字都訓練一個二分類器:一個分類器用來處理數字 0 和數字 1,一個用來處理數字 0 和數字 2,一個用來處理數字 1 和 2,以此類推。這叫做“一對一”(OvO)政策。如果有 N 個類。你需要訓練
N*(N-1)/2
個分類器。對于 MNIST 問題,需要訓練 45 個二分類器!當你想對一張圖檔進行分類,你必須将這張圖檔跑在全部45個二分類器上。然後看哪個類勝出。OvO 政策的主要有點是:每個分類器隻需要在訓練集的部分資料上面進行訓練。這部分資料是它所需要區分的那兩個類對應的資料。
一些算法(比如 SVM 分類器)在訓練集的大小上很難擴充,是以對于這些算法,OvO 是比較好的,因為它可以在小的資料集上面可以更多地訓練,較之于巨大的資料集而言。但是,對于大部分的二分類器來說,OvA 是更好的選擇。
Scikit-Learn 可以探測出你想使用一個二分類器去完成多分類的任務,它會自動地執行 OvA(除了 SVM 分類器,它使用 OvO)。讓我們試一下
SGDClassifier
.
>>> sgd_clf.fit(X_train, y_train) # y_train, not y_train_5
>>> sgd_clf.predict([some_digit])
array([ 5.])
很容易。上面的代碼在訓練集上訓練了一個
SGDClassifier
。這個分類器處理原始的目标class,從 0 到 9(
y_train
),而不是僅僅探測是否為 5 (
y_train_5
)。然後它做出一個判斷(在這個案例下隻有一個正确的數字)。在幕後,Scikit-Learn 實際上訓練了 10 個二分類器,每個分類器都産到一張圖檔的決策數值,選擇數值最高的那個類。
為了證明這是真實的,你可以調用
decision_function()
方法。不是傳回每個樣例的一個數值,而是傳回 10 個數值,一個數值對應于一個類。
>>> some_digit_scores = sgd_clf.decision_function([some_digit])
>>> some_digit_scores
array([[-311402.62954431, -363517.28355739, -446449.5306454 ,
-183226.61023518, -414337.15339485, 161855.74572176,
-452576.39616343, -471957.14962573, -518542.33997148,
-536774.63961222]])
最高數值是對應于類别 5 :
>>> np.argmax(some_digit_scores)
5
>>> sgd_clf.classes_
array([ 0., 1., 2., 3., 4., 5., 6., 7., 8., 9.])
>>> sgd_clf.classes_[5]
5.0
一個分類器被訓練好了之後,它會儲存目标類别清單到它的屬性中去,按照值排序。在本例子當中,在
classes_
數組當中的每個類的索引友善地比對了類本身,比如,索引為 5 的類恰好是類别 5 本身。但通常不會這麼幸運。
classes_
如果你想強制 Scikit-Learn 使用 OvO 政策或者 OvA 政策,你可以使用
OneVsOneClassifier
類或者
OneVsRestClassifier
類。建立一個樣例,傳遞一個二分類器給它的構造函數。舉例子,下面的代碼會建立一個多類分類器,使用 OvO 政策,基于
SGDClassifier
。
>>> from sklearn.multiclass import OneVsOneClassifier
>>> ovo_clf = OneVsOneClassifier(SGDClassifier(random_state=42))
>>> ovo_clf.fit(X_train, y_train)
>>> ovo_clf.predict([some_digit])
array([ 5.])
>>> len(ovo_clf.estimators_)
45
訓練一個
RandomForestClassifier
同樣簡單:
>>> forest_clf.fit(X_train, y_train)
>>> forest_clf.predict([some_digit])
array([ 5.])
這次 Scikit-Learn 沒有必要去運作 OvO 或者 OvA,因為随機森林分類器能夠直接将一個樣例分到多個類别。你可以調用
predict_proba()
,得到樣例對應的類别的機率值的清單:
>>> forest_clf.predict_proba([some_digit])
array([[ 0.1, 0. , 0. , 0.1, 0. , 0.8, 0. , 0. , 0. , 0. ]])
你可以看到這個分類器相當确信它的預測:在數組的索引 5 上的 0.8,意味着這個模型以 80% 的機率估算這張圖檔代表數字 5。它也認為這個圖檔可能是數字 0 或者數字 3,分别都是 10% 的幾率。
現在當然你想評估這些分類器。像平常一樣,你想使用交叉驗證。讓我們用
cross_val_score()
來評估
SGDClassifier
的精度。
>>> cross_val_score(sgd_clf, X_train, y_train, cv=3, scoring="accuracy")
array([ 0.84063187, 0.84899245, 0.86652998])
在所有測試折(test fold)上,它有 84% 的精度。如果你是用一個随機的分類器,你将會得到 10% 的正确率。是以這不是一個壞的分數,但是你可以做的更好。舉例子,簡單将輸入正則化,将會提高精度到 90% 以上。
>>> from sklearn.preprocessing import StandardScaler
>>> scaler = StandardScaler()
>>> X_train_scaled = scaler.fit_transform(X_train.astype(np.float64))
>>> cross_val_score(sgd_clf, X_train_scaled, y_train, cv=3, scoring="accuracy")
array([ 0.91011798, 0.90874544, 0.906636 ])
誤差分析
當然,如果這是一個實際的項目,你會在你的機器學習項目當中,跟随以下步驟(見附錄 B):探索準備資料的候選方案,嘗試多種模型,把最好的幾個模型列為入圍名單,用
GridSearchCV
調試超參數,盡可能地自動化,像你前面的章節做的那樣。在這裡,我們假設你已經找到一個不錯的模型,你試圖找到方法去改善它。一個方式是分析模型産生的誤差的類型。
首先,你可以檢查混淆矩陣。你需要使用
cross_val_predict()
做出預測,然後調用
confusion_matrix()
函數,像你早前做的那樣。
>>> y_train_pred = cross_val_predict(sgd_clf, X_train_scaled, y_train, cv=3)
>>> conf_mx = confusion_matrix(y_train, y_train_pred)
>>> conf_mx
array([[5725, 3, 24, 9, 10, 49, 50, 10, 39, 4],
[ 2, 6493, 43, 25, 7, 40, 5, 10, 109, 8],
[ 51, 41, 5321, 104, 89, 26, 87, 60, 166, 13],
[ 47, 46, 141, 5342, 1, 231, 40, 50, 141, 92],
[ 19, 29, 41, 10, 5366, 9, 56, 37, 86, 189],
[ 73, 45, 36, 193, 64, 4582, 111, 30, 193, 94],
[ 29, 34, 44, 2, 42, 85, 5627, 10, 45, 0],
[ 25, 24, 74, 32, 54, 12, 6, 5787, 15, 236],
[ 52, 161, 73, 156, 10, 163, 61, 25, 5027, 123],
[ 43, 35, 26, 92, 178, 28, 2, 223, 82, 5240]])
這裡是一對數字。使用 Matplotlib 的
matshow()
函數,将混淆矩陣以圖像的方式呈現,将會更加友善。
plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()
這個混淆矩陣看起來相當好,因為大多數的圖檔在主對角線上。在主對角線上意味着被分類正确。數字 5 對應的格子看起來比其他數字要暗淡許多。這可能是資料集當中數字 5 的圖檔比較少,又或者是分類器對于數字 5 的表現不如其他數字那麼好。你可以驗證兩種情況。
讓我們關注僅包含誤差資料的圖像呈現。首先你需要将混淆矩陣的每一個值除以相應類别的圖檔的總數目。這樣子,你可以比較錯誤率,而不是絕對的錯誤數(這對大的類别不公平)。
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums
現在讓我們用 0 來填充對角線。這樣子就隻保留了被錯誤分類的資料。讓我們畫出這個結果。
np.fill_diagonal(norm_conf_mx, 0)
plt.matshow(norm_conf_mx, cmap=plt.cm.gray)
plt.show()
現在你可以清楚看出分類器制造出來的各類誤差。記住:行代表實際類别,列代表預測的類别。第 8、9 列相當亮,這告訴你許多圖檔被誤分成數字 8 或者數字 9。相似的,第 8、9 行也相當亮,告訴你數字 8、數字 9 經常被誤以為是其他數字。相反,一些行相當黑,比如第一行:這意味着大部分的數字 1 被正确分類(一些被誤分類為數字 8 )。留意到誤差圖不是嚴格對稱的。舉例子,比起将數字 8 誤分類為數字 5 的數量,有更多的數字 5 被誤分類為數字 8。
分析混淆矩陣通常可以給你提供深刻的見解去改善你的分類器。回顧這幅圖,看樣子你應該努力改善分類器在數字 8 和數字 9 上的表現,和糾正 3/5 的混淆。比如,你可以嘗試去收集更多的資料,或者你可以構造新的、有助于分類器的特征。舉例子,寫一個算法去數閉合的環(比如,數字 8 有兩個環,數字 6 有一個, 5 沒有)。又或者你可以預處理圖檔(比如,使用 Scikit-Learn,Pillow, OpenCV)去構造一個模式,比如閉合的環。
分析獨特的誤差,是獲得關于你的分類器是如何工作及其為什麼失敗的洞見的一個好途徑。但是這相對難和耗時。舉例子,我們可以畫出數字 3 和 5 的例子
cl_a, cl_b = 3, 5
X_aa = X_train[(y_train == cl_a) & (y_train_pred == cl_a)]
X_ab = X_train[(y_train == cl_a) & (y_train_pred == cl_b)]
X_ba = X_train[(y_train == cl_b) & (y_train_pred == cl_a)]
X_bb = X_train[(y_train == cl_b) & (y_train_pred == cl_b)]
plt.figure(figsize=(8,8))
plt.subplot(221); plot_digits(X_aa[:25], ../images_per_row=5)
plt.subplot(222); plot_digits(X_ab[:25], ../images_per_row=5)
plt.subplot(223); plot_digits(X_ba[:25], ../images_per_row=5)
plt.subplot(224); plot_digits(X_bb[:25], ../images_per_row=5)
plt.show()
左邊兩個
5*5
的塊将數字識别為 3,右邊的将數字識别為 5。一些被分類器錯誤分類的數字(比如左下角和右上角的塊)是書寫地相當差,甚至讓人類分類都會覺得很困難(比如第 8 行第 1 列的數字 5,看起來非常像數字 3 )。但是,大部分被誤分類的數字,在我們看來都是顯而易見的錯誤。很難明白為什麼分類器會分錯。原因是我們使用的簡單的
SGDClassifier
,這是一個線性模型。它所做的全部工作就是配置設定一個類權重給每一個像素,然後當它看到一張新的圖檔,它就将權重的像素強度相加,每個類得到一個新的值。是以,因為 3 和 5 隻有一小部分的像素有差異,這個模型很容易混淆它們。
3 和 5 之間的主要差異是連接配接頂部的線和底部的線的細線的位置。如果你畫一個 3,連接配接處稍微向左偏移,分類器很可能将它分類成 5。反之亦然。換一個說法,這個分類器對于圖檔的位移和旋轉相當敏感。是以,減輕 3/5 混淆的一個方法是對圖檔進行預處理,確定它們都很好地中心化和不過度旋轉。這同樣很可能幫助減輕其他類型的錯誤。
多标簽分類
到目前為止,所有的樣例都總是被配置設定到僅一個類。有些情況下,你也許想讓你的分類器給一個樣例輸出多個類别。比如說,思考一個人臉識别器。如果對于同一張圖檔,它識别出幾個人,它應該做什麼?當然它應該給每一個它識别出的人貼上一個标簽。比方說,這個分類器被訓練成識别三個人臉,Alice,Bob,Charlie;然後當它被輸入一張含有 Alice 和 Bob 的圖檔,它應該輸出
[1, 0, 1]
(意思是:Alice 是,Bob 不是,Charlie 是)。這種輸出多個二值标簽的分類系統被叫做多标簽分類系統。
目前我們不打算深入臉部識别。我們可以先看一個簡單點的例子,僅僅是為了闡明的目的。
from sklearn.neighbors import KNeighborsClassifier
y_train_large = (y_train >= 7)
y_train_odd = (y_train % 2 == 1)
y_multilabel = np.c_[y_train_large, y_train_odd]
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train, y_multilabel)
這段代碼創造了一個
y_multilabel
數組,裡面包含兩個目标标簽。第一個标簽指出這個數字是否為大數字(7,8 或者 9),第二個标簽指出這個數字是否是奇數。接下來幾行代碼會建立一個
KNeighborsClassifier
樣例(它支援多标簽分類,但不是所有分類器都可以),然後我們使用多目标數組來訓練它。現在你可以生成一個預測,然後它輸出兩個标簽:
>>> knn_clf.predict([some_digit])
array([[False, True]], dtype=bool)
它工作正确。數字 5 不是大數(
False
),同時是一個奇數(
True
)。
有許多方法去評估一個多标簽分類器,和選擇正确的量度标準,這取決于你的項目。舉個例子,一個方法是對每個個體标簽去量度 F1 值(或者前面讨論過的其他任意的二分類器的量度标準),然後計算平均值。下面的代碼計算全部标簽的平均 F1 值:
>>> y_train_knn_pred = cross_val_predict(knn_clf, X_train, y_train, cv=3)
>>> f1_score(y_train, y_train_knn_pred, average="macro")
0.96845540180280221
這裡假設所有标簽有着同等的重要性,但可能不是這樣。特别是,如果你的 Alice 的照片比 Bob 或者 Charlie 更多的時候,也許你想讓分類器在 Alice 的照片上具有更大的權重。一個簡單的選項是:給每一個标簽的權重等于它的支援度(比如,那個标簽的樣例的數目)。為了做到這點,簡單地在上面代碼中設定
average="weighted"
多輸出分類
我們即将讨論的最後一種分類任務被叫做“多輸出-多類分類”(或者簡稱為多輸出分類)。它是多标簽分類的簡單泛化,在這裡每一個标簽可以是多類别的(比如說,它可以有多于兩個可能值)。
為了說明這點,我們建立一個系統,它可以去除圖檔當中的噪音。它将一張混有噪音的圖檔作為輸入,期待它輸出一張幹淨的數字圖檔,用一個像素強度的數組表示,就像 MNIST 圖檔那樣。注意到這個分類器的輸出是多标簽的(一個像素一個标簽)和每個标簽可以有多個值(像素強度取值範圍從 0 到 255)。是以它是一個多輸出分類系統的例子。
分類與回歸之間的界限是模糊的,比如這個例子。按理說,預測一個像素的強度更類似于一個回歸任務,而不是一個分類任務。而且,多輸出系統不限于分類任務。你甚至可以讓你一個系統給每一個樣例都輸出多個标簽,包括類标簽和值标簽。
讓我們從 MNIST 的圖檔建立訓練集和測試集開始,然後給圖檔的像素強度添加噪聲,這裡是用 NumPy 的
randint()
函數。目标圖像是原始圖像。
noise = rnd.randint(0, 100, (len(X_train), 784))
noise = rnd.randint(0, 100, (len(X_test), 784))
X_train_mod = X_train + noise
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test
讓我們看一下測試集當中的一張圖檔(是的,我們在窺探測試集,是以你應該馬上鄒眉):
左邊的加噪聲的輸入圖檔。右邊是幹淨的目标圖檔。現在我們訓練分類器,讓它清潔這張圖檔:
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)
看起來足夠接近目标圖檔。現在總結我們的分類之旅。希望你現在應該知道如何選擇好的量度标準,挑選出合适的準确率/召回率的折衷方案,比較分類器,更概括地說,就是為不同的任務建立起好的分類系統。
練習
- 嘗試在 MNIST 資料集上建立一個分類器,使它在測試集上的精度超過 97%。提示:
非常适合這個任務。你隻需要找出一個好的超參數值(試一下對權重和超參數KNeighborsClassifier
進行網格搜尋)。n_neighbors
- 寫一個函數可以是 MNIST 中的圖像任意方向移動(上下左右)一個像素。然後,對訓練集上的每張圖檔,複制四個移動後的副本(每個方向一個副本),把它們加到訓練集當中去。最後在擴充後的訓練集上訓練你最好的模型,并且在測試集上測量它的精度。你應該會觀察到你的模型會有更好的表現。這種人工擴大訓練集的方法叫做資料增強,或者訓練集擴張。
- 拿 Titanic 資料集去搗鼓一番。開始這個項目有一個很棒的平台:Kaggle!
- 建立一個垃圾郵件分類器(這是一個更有挑戰性的練習):
- 下載下傳垃圾郵件和非垃圾郵件的樣例資料。位址是 Apache SpamAssassin 的公共資料集
- 解壓這些資料集,并且熟悉它的資料格式。
- 将資料集分成訓練集和測試集
- 寫一個資料準備的流水線,将每一封郵件轉換為特征向量。你的流水線應該将一封郵件轉換為一個稀疏向量,對于所有可能的詞,這個向量标志哪個詞出現了,哪個詞沒有出現。舉例子,如果所有郵件隻包含了
這四個詞,那麼一封郵件(内容是:"Hello","How","are", "you"
)将會被轉換為向量"Hello you Hello Hello you"
(意思是:[1, 0, 0, 1]
出現,"Hello"
不出現,"How"
"are"
出現),或者"you"
,如果你想數出每個單詞出現的次數。[3, 0, 0, 2]
- 你也許想給你的流水線增加超參數,控制是否剝過郵件頭、将郵件轉換為小寫、去除标點符号、将所有 URL 替換成
,将所有數字替換成"URL"
,或者甚至提取詞幹(比如,截斷詞尾。有現成的 Python 庫可以做到這點)。"NUMBER"
- 然後 嘗試幾個不同的分類器,看看你可否建立一個很棒的垃圾郵件分類器,同時有着高召回率和高準确率。