# 三、分類
> 譯者:[@時間魔術師](https://github.com/hewind1992)
>
> 校對者:[@Lisanaaa](https://github.com/Lisanaaa)、[@飛龍](https://github.com/wizardforcel)、[@ZTFrom1994](https://github.com/ZTFrom1994)、[@XinQiu](https://github.com/xinqiu)、[@tabeworks](https://github.com/tabeworks)、[@JasonLee](https://github.com/lxlhappylife)、[@howie.hu](https://github.com/howie6879)
在第一章我們提到過最常用的監督學習任務是回歸(用于預測某個值)和分類(預測某個類別)。在第二章我們探索了一個回歸任務:預測房價。我們使用了多種算法,諸如線性回歸,決策樹,和隨機森林(這個將會在后面的章節更詳細地討論)。現在我們將我們的注意力轉到分類任務上。
## MNIST
在本章當中,我們將會使用 MNIST 這個數據集,它有著 70000 張規格較小的手寫數字圖片,由美國的高中生和美國人口調查局的職員手寫而成。這相當于機器學習當中的“Hello World”,人們無論什么時候提出一個新的分類算法,都想知道該算法在這個數據集上的表現如何。機器學習的初學者遲早也會處理 MNIST 這個數據集。
Scikit-Learn 提供了許多輔助函數,以便于下載流行的數據集。MNIST 是其中一個。下面的代碼獲取 MNIST
```python
>>> 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`鍵存放一個標簽數組
讓我們看一下這些數組
```python
>>> 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 數據集的圖片。當你處理更加復雜的分類任務的時候,它會讓你更有感覺。

先等一下!你總是應該先創建測試集,并且在驗證數據之前先把測試集晾到一邊。MNIST 數據集已經事先被分成了一個訓練集(前 60000 張圖片)和一個測試集(最后 10000 張圖片)
```python
X_train, X_test, y_train, y_test = X[:60000], X[60000:], y[:60000], y[60000:]
```
讓我們打亂訓練集。這可以保證交叉驗證的每一折都是相似(你不會期待某一折缺少某類數字)。而且,一些學習算法對訓練樣例的順序敏感,當它們在一行當中得到許多相似的樣例,這些算法將會表現得非常差。打亂數據集將保證這種情況不會發生。
```python
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”。讓我們為這個分類任務創建目標向量:
```python
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`和在整個數據集上訓練它。
```python
from sklearn.linear_model import SGDClassifier
sgd_clf = SGDClassifier(random_state=42)
sgd_clf.fit(X_train, y_train_5)
```
> `SGDClassifier`依賴于訓練集的隨機程度(所以被命名為 stochastic,隨機之義)。如果你想重現結果,你應該固定參數`random_state`
現在你可以用它來查出數字 5 的圖片。
```python
>>> sgd_clf.predict([some_digit])
array([ True], dtype=bool)
```
分類器猜測這個數字代表 5(`True`)。看起來在這個例子當中,它猜對了。現在讓我們評估這個模型的性能。
## 對性能的評估
評估一個分類器,通常比評估一個回歸器更加玄學。所以我們將會花大量的篇幅在這個話題上。有許多量度性能的方法,所以拿來一杯咖啡和準備學習許多新概念和首字母縮略詞吧。
### 使用交叉驗證測量準確性
評估一個模型的好方法是使用交叉驗證,就像第二章所做的那樣。
> <center>**實現交叉驗證**</center>
在交叉驗證過程中,有時候你會需要更多的控制權,相較于函數`cross_val_score()`或者其他相似函數所提供的功能。這種情況下,你可以實現你自己版本的交叉驗證。事實上它相當簡單。以下代碼粗略地做了和`cross_val_score()`相同的事情,并且輸出相同的結果。
```python
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
```
> `StratifiedKFold`類實現了分層采樣(詳見第二章的解釋),生成的折(fold)包含了各類相應比例的樣例。在每一次迭代,上述代碼生成分類器的一個克隆版本,在訓練折(training folds)的克隆版本上進行訓練,在測試折(test folds)上進行預測。然后它計算出被正確預測的數目和輸出正確預測的比例。
讓我們使用`cross_val_score()`函數來評估`SGDClassifier`模型,同時使用 K 折交叉驗證,此處讓`k=3`。記住:K 折交叉驗證意味著把訓練集分成 K 折(此處 3 折),然后使用一個模型對其中一折進行預測,對其他折進行訓練。
```python
>>> 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”這個類上的表現。
```python
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)
```
你能猜到這個模型的精度嗎?揭曉謎底:
```python
>>> 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()`函數
```python
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`)給它。
```python
>>> 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)。一個完美的分類器將只有真反例和真正例,所以混淆矩陣的非零值僅在其主對角線(左上至右下)。
```python
>>> 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 將對你有幫助

### 準確率與召回率
Scikit-Learn 提供了一些函數去計算分類器的指標,包括準確率和召回率。
```python
>>> 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()`
```python
>>> from sklearn.metrics import f1_score
>>> f1_score(y_train_5, y_train_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%。相反,降低閾值可提高召回率、降低準確率。

Scikit-Learn 不讓你直接設置閾值,但是它給你提供了設置決策分數的方法,這個決策分數可以用來產生預測。它不是調用分類器的`predict()`方法,而是調用`decision_function()`方法。這個方法返回每一個樣例的分數值,然后基于這個分數值,使用你想要的任何閾值做出預測。
```python
>>> 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`)。讓我們提高這個閾值:
```python
>>> 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()`得到每一個樣例的分數值,但是這一次指定返回一個決策分數,而不是預測值。
```python
y_scores = cross_val_predict(sgd_clf, X_train, y_train_5, cv=3,
method="decision_function")
```
現在有了這些分數值。對于任何可能的閾值,使用`precision_recall_curve()`,你都可以計算準確率和召回率:
```python
from sklearn.metrics import precision_recall_curve
precisions, recalls, thresholds = precision_recall_curve(y_train_5, y_scores)
```
最后,你可以使用 Matplotlib 畫出準確率和召回率(圖 3-4),這里把準確率和召回率當作是閾值的一個函數。
```python
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-3,留意當你從中間箭頭開始然后向右移動一個數字會發生什么: 準確率會由 4/5(80%)降到 3/4(75%)。另一方面,當閾值提高時候,召回率只會降低。這也就說明了為什么召回率的曲線更加平滑。
現在你可以選擇適合你任務的最佳閾值。另一個選出好的準確率/召回率折衷的方法是直接畫出準確率對召回率的曲線,如圖 3-5 所示。

可以看到,在召回率在 80% 左右的時候,準確率急劇下降。你可能會想選擇在急劇下降之前選擇出一個準確率/召回率折衷點。比如說,在召回率 60% 左右的點。當然,這取決于你的項目需求。
我們假設你決定達到 90% 的準確率。你查閱第一幅圖(放大一些),在 70000 附近找到一個閾值。為了作出預測(目前為止只在訓練集上預測),你可以運行以下代碼,而不是運行分類器的`predict()`方法。
```python
y_train_pred_90 = (y_scores > 70000)
```
讓我們檢查這些預測的準確率和召回率:
```python
>>> 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()`函數:
```python
from sklearn.metrics import roc_curve
fpr, tpr, thresholds = roc_curve(y_train_5, y_scores)
```
然后你可以使用 matplotlib,畫出 FPR 對 TPR 的曲線。下面的代碼生成圖 3-6.
```python
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()
```

這里同樣存在折衷的問題:召回率(TPR)越高,分類器就會產生越多的假正例(FPR)。圖中的點線是一個完全隨機的分類器生成的 ROC 曲線;一個好的分類器的 ROC 曲線應該盡可能遠離這條線(即向左上角方向靠攏)。
一個比較分類器之間優劣的方法是:測量ROC曲線下的面積(AUC)。一個完美的分類器的 ROC AUC 等于 1,而一個純隨機分類器的 ROC AUC 等于 0.5。Scikit-Learn 提供了一個函數來計算 ROC AUC:
```python
>>> 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。
```python
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 曲線,你需要的是樣例的分數,而不是概率。一個簡單的解決方法是使用正例的概率當作樣例的分數。
```python
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。
```python
plt.plot(fpr, tpr, "b:", label="SGD")
plot_roc_curve(fpr_forest, tpr_forest, "Random Forest")
plt.legend(loc="bottom right")
plt.show()
```

如你所見,`RandomForestClassifier`的 ROC 曲線比`SGDClassifier`的好得多:它更靠近左上角。所以,它的 ROC AUC 也會更大。
```python
>>> 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`.
```python
>>> 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 個數值,一個數值對應于一個類。
```python
>>> 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 :
```python
>>> 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_` 中去,按照值排序。在本例子當中,在`classes_` 數組當中的每個類的索引方便地匹配了類本身,比如,索引為 5 的類恰好是類別 5 本身。但通常不會這么幸運。
如果你想強制 Scikit-Learn 使用 OvO 策略或者 OvA 策略,你可以使用`OneVsOneClassifier`類或者`OneVsRestClassifier`類。創建一個樣例,傳遞一個二分類器給它的構造函數。舉例子,下面的代碼會創建一個多類分類器,使用 OvO 策略,基于`SGDClassifier`。
```python
>>> 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`同樣簡單:
```python
>>> forest_clf.fit(X_train, y_train)
>>> forest_clf.predict([some_digit])
array([ 5.])
```
這次 Scikit-Learn 沒有必要去運行 OvO 或者 OvA,因為隨機森林分類器能夠直接將一個樣例分到多個類別。你可以調用`predict_proba()`,得到樣例對應的類別的概率值的列表:
```python
>>> 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`的精度。
```python
>>> 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% 以上。
```python
>>> 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()`函數,像你早前做的那樣。
```python
>>> 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()`函數,將混淆矩陣以圖像的方式呈現,將會更加方便。
```python
plt.matshow(conf_mx, cmap=plt.cm.gray)
plt.show()
```

這個混淆矩陣看起來相當好,因為大多數的圖片在主對角線上。在主對角線上意味著被分類正確。數字 5 對應的格子看起來比其他數字要暗淡許多。這可能是數據集當中數字 5 的圖片比較少,又或者是分類器對于數字 5 的表現不如其他數字那么好。你可以驗證兩種情況。
讓我們關注僅包含誤差數據的圖像呈現。首先你需要將混淆矩陣的每一個值除以相應類別的圖片的總數目。這樣子,你可以比較錯誤率,而不是絕對的錯誤數(這對大的類別不公平)。
```python
row_sums = conf_mx.sum(axis=1, keepdims=True)
norm_conf_mx = conf_mx / row_sums
```
現在讓我們用 0 來填充對角線。這樣子就只保留了被錯誤分類的數據。讓我們畫出這個結果。
```python
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 的例子
```python
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 是)。這種輸出多個二值標簽的分類系統被叫做多標簽分類系統。
目前我們不打算深入臉部識別。我們可以先看一個簡單點的例子,僅僅是為了闡明的目的。
```python
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`樣例(它支持多標簽分類,但不是所有分類器都可以),然后我們使用多目標數組來訓練它。現在你可以生成一個預測,然后它輸出兩個標簽:
```python
>>> knn_clf.predict([some_digit])
array([[False, True]], dtype=bool)
```
它工作正確。數字 5 不是大數(`False`),同時是一個奇數(`True`)。
有許多方法去評估一個多標簽分類器,和選擇正確的量度標準,這取決于你的項目。舉個例子,一個方法是對每個個體標簽去量度 F1 值(或者前面討論過的其他任意的二分類器的量度標準),然后計算平均值。下面的代碼計算全部標簽的平均 F1 值:
```python
>>> 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()`函數。目標圖像是原始圖像。
```python
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
```
讓我們看一下測試集當中的一張圖片(是的,我們在窺探測試集,所以你應該馬上鄒眉):

左邊的加噪聲的輸入圖片。右邊是干凈的目標圖片。現在我們訓練分類器,讓它清潔這張圖片:
```python
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)
```

看起來足夠接近目標圖片。現在總結我們的分類之旅。希望你現在應該知道如何選擇好的量度標準,挑選出合適的準確率/召回率的折衷方案,比較分類器,更概括地說,就是為不同的任務建立起好的分類系統。
## 練習
1. 嘗試在 MNIST 數據集上建立一個分類器,使它在測試集上的精度超過 97%。提示:`KNeighborsClassifier`非常適合這個任務。你只需要找出一個好的超參數值(試一下對權重和超參數`n_neighbors`進行網格搜索)。
2. 寫一個函數可以是 MNIST 中的圖像任意方向移動(上下左右)一個像素。然后,對訓練集上的每張圖片,復制四個移動后的副本(每個方向一個副本),把它們加到訓練集當中去。最后在擴展后的訓練集上訓練你最好的模型,并且在測試集上測量它的精度。你應該會觀察到你的模型會有更好的表現。這種人工擴大訓練集的方法叫做數據增強,或者訓練集擴張。
3. 拿 Titanic 數據集去搗鼓一番。開始這個項目有一個很棒的平臺:Kaggle!
4. 建立一個垃圾郵件分類器(這是一個更有挑戰性的練習):
- 下載垃圾郵件和非垃圾郵件的樣例數據。地址是[Apache SpamAssassin 的公共數據集](https://spamassassin.apache.org/publiccorpus/)
- 解壓這些數據集,并且熟悉它的數據格式。
- 將數據集分成訓練集和測試集
- 寫一個數據準備的流水線,將每一封郵件轉換為特征向量。你的流水線應該將一封郵件轉換為一個稀疏向量,對于所有可能的詞,這個向量標志哪個詞出現了,哪個詞沒有出現。舉例子,如果所有郵件只包含了`"Hello","How","are", "you"`這四個詞,那么一封郵件(內容是:`"Hello you Hello Hello you"`)將會被轉換為向量`[1, 0, 0, 1]`(意思是:`"Hello"`出現,`"How"`不出現,`"are"`不出現,`"you"`出現),或者`[3, 0, 0, 2]`,如果你想數出每個單詞出現的次數。
- 你也許想給你的流水線增加超參數,控制是否剝過郵件頭、將郵件轉換為小寫、去除標點符號、將所有 URL 替換成`"URL"`,將所有數字替換成`"NUMBER"`,或者甚至提取詞干(比如,截斷詞尾。有現成的 Python 庫可以做到這點)。
- 然后 嘗試幾個不同的分類器,看看你可否建立一個很棒的垃圾郵件分類器,同時有著高召回率和高準確率。