<EYD與機器學習>三:Classifcation

各位知乎兒大家好,這是<EYD與機器學習>專欄的第三篇文章,這篇文章以《Hands-on Machine Learning with Scikit-Learn and TensorFlow》(後面簡稱為HMLST)第三章的內容為主線,其間會加入我們成員的一些經驗和思考與大家分享。

這一章的主題是分類,但並不是介紹各種高端的分類演算法,而是將主要工作放在了分類演算法的性能度量、誤差分析和各種分類情況的區別上,在後續的章節會為大家介紹具體的演算法。

第三章:Classifcation

3.1 MNIST

在這一章我們要用到的數據集是MNIST,也就是Yann LeCun構建的著名的手寫數字數據集。數據集共有70000張圖片,其中訓練集60000張,測試集10000張,圖片的大小為28*28=784個像素點,每張圖片都有標記。

我們首先要導入MNIST數據集,scikit-learning 為我們提供了很大的便利:

>>> 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.])}

通過scikit-learn導入的數據集通常會有一個類似於字典的結構:

DESCR:用來描述整個數據集

data:一個陣列,每一行代表一個樣本,而列代表各個特徵值

target:一個向量,裡邊是所有樣本的標記

>>> X, y = mnist["data"], mnist["target"]

>>> X.shape

(70000, 784)

>>> y.shape

(70000,)

MNIST數據集的部分圖片(HMLST figure 3-1)

在導入數據集之後我們開始劃分訓練集和測試集,因為MNIST的圖片是按標記有序排列的,所以需要打亂:

>>>import numpy as np

>>>X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]

>>>shuffle_index = np.random.permutation(60000)

>>>X_train, y_train = X_train[shuffle_index], y_train[shuffle_index]

3.2 訓練二分類分類器

從簡單的情況入手,我們先構建一個二分類的分類器:判斷當前數字是不是「5」。先對之前劃分的訓練集和測試集進行處理,將標記為「5」的樣本標記改為TRUE,「非5」的數據標記改為FALSE:

>>>y_train_5 = (y_train == 5) # True for all 5s, False for all other digits.

>>>y_test_5 = (y_test == 5)

數據處理好之後我們就可以挑選一個演算法了,HMLST的作者使用的是Scikit-Learn 的隨機梯度下降分類器(SGDClassifier ),這個分類器比較簡單,我們會在後面詳細介紹,它的在處理大量數據時效率很高,因為它每次只用一個樣本訓練,使得它在「online learning」的情況下同樣適用:

>>>from sklearn.linear_model import SGDClassifier

>>>sgd_clf = SGDClassifier(random_state=42)

>>>sgd_clf.fit(X_train, y_train_5)

從測試集中挑選出一個標記為「5」的數據(命名為some_digit )對sgd_clf進行一次簡單的驗證:

>>> sgd_clf.predict([some_digit])

array([ True], dtype=bool)

可見,這個簡單的演算法是可以在測試集上運行的,雖然只有一個測試樣本,下面我們深入的分析一下演算法的性能。

3.3性能指標

在第二章里,我們通過「加州房價預測」這個小項目向大家演示了如何評價一個多元回歸模型的性能。在這一章我們來具體闡述分類演算法的性能度量,這些度量和之前回歸模型的性能指標有很大的差異。

3.3.1使用交叉驗證來計算精度

在第一章的時候講過,在訓練模型時進行交叉驗證是十分必要的,scikit-learn為我們提供了方便的交叉驗證函數cross_val_score() :

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

在上面的cross_val_score參數里cv=3設置了我們是進行「3-fold 」的交叉驗證,計算的每次驗證的準確率。我們發現準確率很高,基本上都在95%以上,難道一個簡單的SGDClassifier就可以解決這個問題了嗎?現在我們用一個更加「愚蠢」的演算法來驗證這個問題是不是真的如此簡單:

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

上面定義的分類器荒唐到認為所有的樣本都不是「5」,那麼用這個分類器來解決之前的問題會得到多高的精度呢?

>>> 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%以上的精度,如此一來我們應該意識到問題所在了。並不是SGDClassifier的能力強,而是我們的數據有問題,MNIST本來有0~9十類手寫數字,我們只判斷數據是不是「5」,那麼標記為「5」的數據量和「非5」的比例就是1:9,所以即便是全部猜成是「非5」也可以得到90%的準確率。從這個小例子我們可以看出當樣本極其不均衡時,僅僅考慮精度是無法衡量一個分類器的好壞的。當然,我們在實際操作的時候不會使用如此不均衡的數據集,也不會使用如此簡單的演算法解決複雜的問題,但是上述情況的出現還是提醒了我們其他性能度量的重要性。

3.3.2 混淆矩陣

一般來說,對於一個分類任務而言,混淆矩陣(Confusion Matrix )更能體現它的性能。混淆矩陣的思想很簡單,就是統計某一類數據被劃分為本類以及其他類的次數。

混淆矩陣(圖片來自維基百科)

scikit learn為我們提供了計算混淆矩陣的函數confusion_matrix ,但是在使用之前要先得到模型的預測結果,我們現在不能使用測試集,所以先用交叉驗證集來預測:

>>>from sklearn.model_selection import cross_val_predict

>>>y_train_pred = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3)

接下來就可以計算混淆矩陣了:

>>> from sklearn.metrics import confusion_matrix

>>> confusion_matrix(y_train_5, y_train_pred)

array([[53272, 1307],

[ 1077, 4344]])

第一行顯示,「非5」的數據被正確分類的數量為53272,這部分樣本被稱為『true negatives 』;「非5」的數據被誤分為『5』的數量為1307,這部分樣本被稱為『false positives』。第二行顯示,標記為「5」的數據被正確分類的數量為4344,這部分樣本被稱為『true positives 』;標記為「5」的數據被誤分為『非5』的數量為1077,這部分樣本被稱為『false negatives 』。因此,一個混淆矩陣有如下結構:

混淆矩陣結構(圖片來自《機器學習》)

混淆矩陣體現了很多的信息,但是我們往往更加喜歡那些簡潔明了的性能度量,這裡我們關注查准率(Precision)和查全率(Recall)。

查准率(Precision): precision = frac{TP}{TP+FP}

查全率(Recall): recall = frac{TP}{TP+FN}

可見,查准率的含義就是在所有預測為某一類的樣本中真正屬於該類的樣本的比例,也就是某一類預測結果的準確率。而查全率的含義則為某一類樣本中真正被預測為該類的樣本的比例,也就是一類數據被分類正確的比例。

查准率和查全率是一對相互矛盾的度量,以上面的分類任務為例:當我們把所有的樣本都預測為「5」時,查全率為100%,而此時查准率卻很低;當我們只把少量最有可能為「5」的樣本預測為「5」時,查准率很高,但是查全率很低。這兩個性能度量在不同的情況下會各有偏重。

3.3.3 計算查准率和查全率

Scikit learn為我們提供了很方便的計算函數precision_score ()和recall_score (),我們只需要先得到預測結果和真實標記:

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

從這兩個性能指標來看,我們的演算法的表現並不能算是很好。為了更方便的衡量演算法的表現,我們引入F1度量(F1 score),它是對查准率和查全率的調和平均:

F_{1} = frac{2}{frac{1}{precision}+frac{1}{recall}} = frac{TP}{TP+frac{FN+FP}{2}}

上面的公式是標準的F1 score,我們還可以在查准率和查全率的前面添加權重來實現我們更加偏重哪個度量。現在我們的演算法只需要能得到很高的F1 score,則就同時得到了很高的查准率和查全率。F1 score的計算也很簡單:

>>> from sklearn.metrics import f1_score

>>> f1_score(y_train_5, y_pred)

0.78468208092485547

3.3.5 查准率和查全率的均衡

在討論兩者的均衡之前我們先來弄清楚SGDClassifier() 是如何來決定一個樣本時正例(5)還是反例(非5)的。對每一個樣本,分類器首先通過decision function 來計算出一個分值,然後將這個分值與一個閾值進行比較,大於閾值就被分為正例,否則為反例。如下圖(HMLST figure 3-3)所示,圖中有12個樣本,並且按照決定函數計算的分值排序,假如我們將閾值設置在中間位置(中間的箭頭),那麼高於閾值的有5個樣本,全被分為類別「5」,但是5個贗本中有一個樣本的真實標記是「6」,那麼可以得到這個閾值下演算法的查准率為4/5=80%;同理,下圖中有6個樣本的真實標記為「5」,但是閾值在中間位置時只有4個樣本被分為了 「5」 ,所以查全率為4/6=67%。當把閾值降低(左側的箭頭),我們發現查全率會提高,而查准率相應的降低了。

決定函數的閾值與查准率和查全率的均衡(HMLST figure 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() 函數是相同的。現在我們將閾值提高:

>>> threshold = 200000

>>> y_some_digit_pred = (y_scores > threshold)

>>> y_some_digit_pred

array([False], dtype=bool)

我們發現在一個極高的閾值下,這個樣本不再被分類為「5」。

那麼我們如何選擇合適的閾值呢?首先我們計算出所有樣本decision_function的結果:

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

之後利用得到的「precisions, recalls, thresholds」畫出閾值與查准率和查全率的關係:

查准率和查全率隨閾值變化(HMLST figure 3-4)

這樣我們可以通過圖中的兩條曲線來選擇合適的閾值從而得到我們想要的查准率和查全率。不過圖中是兩條曲線,我們可以繪製「查准率-查全率」曲線來觀察兩者的關係:

查准率-查全率曲線(HMLST figure 3-5)

3.3.6 ROC曲線

ROC曲線全稱為「受試者工作特徵(Receiver Operating Characteristic) )」曲線,與查准率-查全率曲線類似,但是ROC繪製的是TPR(true positive rate,查全率的別稱 )和FPR (false positive rate )的關係,兩者的公式如下:

TPR= frac{TP}{TP+FN}

FPR = frac{FP}{TN+FP}

ROC的繪製也比較簡單,只需計算TPR、FPR和閾值即可:

>>>from sklearn.metrics import roc_curve

>>>fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)

ROC曲線(HMLST figure 3-6)

利用ROC曲線的方法之一是通過計算AUC(area under the curve )來來比較演算法性能,AUC也就是ROC曲線包圍區域的面積。圖中的虛線表示的是隨機猜測的ROC曲線,當ROC曲線越貼近左上角時AUC越大,演算法的性能越好。AUC的計算也很簡單:

>>> from sklearn.metrics import roc_auc_score

>>> roc_auc_score(y_train_5, y_scores)

0.97061072797174941

當一個演算法的ROC曲線可以把另一個演算法的ROC曲線包圍時,則前者肯定優於後置。我們在同樣的數據集上比較SGDClassifier 和RandomForestClassifier 的ROC曲線:

比較不同分類器的ROC曲線(HMLST figure 3-6)

RandomForestClassifier 比SGDClassifier 的能力更強,所以前者的ROC曲線包圍了後者。

3.4 多類別分類器

上述的例子都是二分類問題,但是在實際任務中很多都是多分類問題,樣本的類別有多個。某些演算法,例如隨機森林和貝葉斯分類器本身就可以解決多分類問題,然而某些演算法例如SVM和線性分類器都是嚴格的二分類演算法,所以我們需要使用一些策略來使他們能處理多分類問題。

一種方法是OvA :one-versus-all (也稱為one-versus-the-rest ),假設我們想要識別0~9十種手寫數字,那麼我們可以建立十個分類器,分別判斷輸入的樣本是不是0,1,2,...,9,並且取這十個分類器結果中概率最大的預測為最終結果。

另一種方法是OvO:one-versus-one, 建立多個二分類器,但是每個分類器只判斷是兩個類別中的哪一個,例如分類器1判斷是「0」還是「1」,分類器2判斷是「0」還是「2」...,

假如樣本有N個類別,那麼需要建立N × (N – 1) / 2 個分類器。

上面兩者各有優缺點,OvA建立的分類器少,但是每個分類器使用的訓練數據多,OVO建立的分類器多,但是每個分類器使用的數據少,具體使用哪種策略要根據自己的實際任務而定。Scikit learn提供了便利的函數來實現OvA和OvO方法,以OVO為例:

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

3.5 誤差分析

假如我們針對一個多分類(以0~9的手寫數字識別為例)問題已經確定了使用某種演算法,那麼我們接下來要做的就是通過調參來提升演算法性能。其中很重要的一步就是進行誤差分析,觀察演算法出現了哪些錯誤。

首先,我們觀察混淆矩陣:

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

矩陣中有很多數字,不能很直接的看出效果,那我們通過繪製混淆矩陣的圖來進行一個直觀的觀察:

>>>plt.matshow(conf_mx, cmap=plt.cm.gray)

>>>plt.show()

可以看出對角線的色塊最亮,也就是說大部分數據都被分類正確了,但是發現數字「5」對應的色塊略暗一些,這可能表示演算法在數字「5」上的表現不是很好。現在我們來關注各個類別的誤差:

>>>row_sums = conf_mx.sum(axis=1, keepdims=True)

>>>norm_conf_mx = conf_mx / row_sums

>>>np.fill_diagonal(norm_conf_mx, 0)

>>>plt.matshow(norm_conf_mx, cmap=plt.cm.gray)

>>>plt.show()

矩陣對角線的元素填充為零,每一行都代表真實的標記,每一列代表預測的標記。每一個小方格的數值為列值比行值,也就是某一類數據被預測為各個類的比例,對角線的值強製為0,值越大色塊顏色越淺(亮度高)。我們可以看出大多數色塊顏色很深,說明錯誤率比較低,但是坐標為(3,5)和(5,3)的兩個色塊已經坐標為(7,9)和(9,7)的兩個色塊顏色很淺。這說明數字 「3」 和「5」互相誤分類的概率比較大,數字「7」和「9」互相誤分類的概率也比較大。我們來看一下數字「3」 和「5」中被正確分類和錯誤分類的部分樣本:

數字「3」 和「5」部分樣本分類情況

在上圖中,上面的兩個數字矩陣是真實標記為「3」的樣本,但是左上角的矩陣被預測為「3」,右上角的矩陣被預測為「5」;同樣下面的兩個數字矩陣是真實標記為「5」的樣本,但是左下角的矩陣被預測為「3」,右下角的矩陣被預測為「5」。為什麼這些樣本會被分類錯誤呢?而且為什麼數字「3」 和「5」容易混淆呢?要想明白這個問題首先要了解我們的分類器SGDClassifier ,它是一個簡單的線性模型,它所做的就是為圖片的每一個像素點分配一個權重,然後再對每個像素點加權求和,每一類都會得到一個分布在一個大致範圍的加權和,然後通過比較加權和與閾值,來判斷樣本的類別。這樣我們不難想到數字「3」 和「5」為什麼容易混淆了,看下圖:

這不是手寫的3和5,但是即便是計算機數字,兩者的差別也僅僅在紅色標記的部分,所以兩者在像素的位置和個數上差別其實並不大,尤其在像素的個數上。從 (數字「3」 和「5」部分樣本分類情況)圖中可以看出,大部分被分類錯誤的樣本都是因為紅色標記的這一「豎」沒有寫好,太短或者比較歪。這種誤差的出現就是我們要考慮的,因為這體現了演算法的特點和不足,假如我們同原來完全不同的卷積網路來做分類,那麼數字「3」 和「5」就很難混淆,因為兩者對於卷積網路而言差別很大。

3.6 多標記分類器

上面所闡述的無論是二分類器還是多類別分類器,對於每一個樣本,它們都只會輸出一個標記,但是有些情況下,我們需要讓分類器對一個樣本同時輸出多個標記。例如我們想判斷一個數字是否大於7並且同時判斷它是為奇數,那麼每個樣本就有了兩個標記(大於7,奇數),我們用這個例子來簡單的說明多標記分類器的應用:

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

>>> knn_clf.predict([some_digit])

array([[False, True]], dtype=bool)

得到了正確的結果:不大於7,是奇數

3.7 多輸出分類器

多輸出分類器是對多標記分類器的一個推廣,每一個標記都可以是多個類別。作者在書中舉了一個例子,移除數字圖像中的雜訊:對於每一個樣本都有多個標記(每個像素點都有一個標記),每個標記都有多個值(0~255),下面是實常式序,首先添加雜訊:

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

我們得到了一個不錯的結果,圖片比較清晰而且沒有雜訊。

3.8 總結

在這一章,我們討論了各種分類任務,並且針對「分類」這一類任務專門討論了性能度量和誤差分析。這些是比較基礎的知識,但是筆者認為還是很重要的。在進行誤差分析時,首先需要了解演算法的工作原理,這樣在出現比較大的誤差的時候我們才能分析出問題所在,對於傳統的機器學習演算法,原理已經比較完備了,我們不應該忽視。

在後續的章節我們會和大家分享模型訓練方面的知識,本次文章中出現了什麼問題以及大家對我們的工作有什麼建議歡迎提出。

——Double_D 編輯

參考文獻:

[1] 周志華. 機器學習[M]. Qing hua da xue chu ban she, 2016.

完整代碼:

ageron/handson-ml?

github.com圖標
推薦閱讀:

好書推薦:《深度學習輕鬆學:核心演算法與視覺實踐》
EdX-Columbia機器學習課第1講筆記:概論與最大似然
Hulu機器學習問題與解答系列 | 十九:主題模型
是什麼技術讓我們變成透明人?
機器學習基石筆記11:邏輯斯蒂(Logistic)回歸 下

TAG:機器學習 | 深度學習DeepLearning | 數據挖掘 |