文章目錄
-
第3章 分類
1 MINIST
2 訓練一個二元分類器
3 性能考覈
3.1 使用交叉驗證測量精度
3.2 混淆矩陣
3.3 精度和召回率
3.4 精度 / 召回率權衡
3.5 ROC曲線及【召回率的本質】
4 多類別分類器
5 錯誤分析
6 多標籤分類
7 多輸出分類
8 練習
1. MINIST
這是一組由美國高中生和人口調查局員工手寫的70000個數字的圖片。每張圖像都用其代表的數字標記。被稱爲機器學習領域的“Hello world ”. sklearn提供了方便的獲取 MNIST數據集(github) 的方式.
【 祖傳祕製】
# To support both python 2 and python 3
from __future__ import division, print_function, unicode_literals
# Common imports
import numpy as np
import pandas as pd
import os
# to make this notebook's output stable across runs
np.random.seed(42)
# To plot pretty figures
%matplotlib inline
import matplotlib as mpl
import matplotlib.pyplot as plt
mpl.rc('axes', labelsize=14)
mpl.rc('xtick', labelsize=12)
mpl.rc('ytick', labelsize=12)
#多個輸出就展示多行
%config ZMQInteractiveShell.ast_node_interactivity="all"
# 忽略無用警告(see SciPy issue #5998)
import warnings
warnings.filterwarnings(action="ignore", message="^internal gelsd")
# Where to save the figures ------ 可修改 start--------
PROJECT_ROOT_DIR = "."
CHAPTER_ID = "classification"
def save_fig(fig_id, tight_layout=True):
path = os.path.join(PROJECT_ROOT_DIR, "images", CHAPTER_ID, fig_id + ".png")
print("Saving figure", fig_id)
if tight_layout:
plt.tight_layout()
plt.savefig(path, format='png', dpi=300)
# Where to save the figures ------ 可修改 end --------
MNIST官方數據集不能在線獲取,直接手動下載到本地然後讀取:
from scipy.io import loadmat
dataPath = "D:\\0文檔\\00機器學習\\0 ML6 _20181022\\Data\\mnist-original.mat"
mnist = loadmat(dataPath)
mnist
查看數據,需要行列轉置
X,y = mnist["data"],mnist["label"]
X.shape
y.shape
X = X.T
y = y.T
X.shape
y.shape
共有70000張圖片樣本,784個特徵,即28*28 = 784 個像素點的強度特徵。將其重新形成一個28×28數組,用matplotlib的imshow就可以顯示出來。
some_digit = X[36000]
some_digit_image = some_digit.reshape(28,28)
plt.imshow(some_digit_image,cmap=mpl.cm.binary,interpolation="nearest")
貌似是5,y[36000],確實是5.
MNIST已經分好了訓練集和測試集,所以直接獲取
#創建訓練集和測試集(MNIST已經建好前60000條樣本爲訓練集)
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]
2. 訓練一個二元分類器
做一個判斷是不是數字5的二元分類器。label改爲True和False:
y_train_5 = (y_train == 5)
y_test_5 = (y_test == 5)
選擇隨機梯度下降(SGD)分類器
from sklearn.linear_model import SGDClassifier #隨機梯度下降分類器
sgd = SGDClassifier(random_state=42)
sgd.fit(X_train,y_train_5)
sgd.predict([some_digit]) #預測之前的數字5
3. 性能考覈
評估分類器比評估迴歸器要困難得多!
3.1 使用交叉驗證測量精度
如果希望自己可控性強一點,可以自行實施交叉驗證。
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_sgd = clone(sgd) #copy 已訓練好的SGD二元分類器
X_train_folds = X_train[train_index] #當前fold的訓練集
y_train_folds = (y_train_5[train_index])#當前fold的訓練集 label
X_test_fold = X_train[test_index] #當前訓練集切分出來的驗證集
y_test_fold = (y_train_5[test_index]) #當前訓練集切分出來的驗證集 label
clone_sgd.fit(X_train_folds,y_train_folds)#重新用當前fold的訓練集訓練分類器
y_pred = clone_sgd.predict(X_test_fold) #預測驗證集
#注意:y_test_fold是一個列表套列表的結構,直接按原文做n_correct是一個列表,所以列表推導式處理了下。
n_correct = sum(y_pred == [x[0] for x in y_test_fold]) #預測正確的樣本數量
print("預測準確率:",n_correct/(len(y_pred))) #
輸出結果跟sk自帶的cross_val_score一致。
from sklearn.model_selection import cross_val_score
cross_val_score(sgd, X_train, y_train_5, cv=3, scoring="accuracy")
但是準確率不能成爲分類器的首要性能指標。舉個例子:有100個蘋果,95個是紅蘋果,5個是綠蘋果,分類器預測一個蘋果爲紅蘋果,準確率將達到95%,但是,綠色蘋果的準確率確爲5%,所以驗證拉低了模型的泛化能力,肯定是不行的。 原因就是正負樣本的分佈不均衡
。
3.2 混淆矩陣
評估分類器性能更好的方法是混淆矩陣。理解它的關鍵在於弄清楚行列代表的含義:
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import confusion_matrix
y_train_pred = cross_val_predict(sgd,X_train,y_train_5,cv=3)
confusion_matrix(y_train_5,y_train_pred)
最理想的狀態就是對角線上的數值都爲0。
3.3 精度和召回率
在學習之前一定先搞清楚概念,掃盲
開始:
P : positive 正例,N: negative 負例
注意:
當類別不均衡
時,要將少數的類歸爲正類。
假設關注的類爲正類(正例)
:
TP —— 將正類預測爲正類的數量;
FN —— 將正類預測爲負類的數量;
FP —— 將負類預測爲正類的數量;
TN —— 將負類預測爲負類的數量;
精度(Precision)
:P
= TP / TP +FP 或 P = TN/TN+FN,描述的是某一類樣本。也可以用負例作爲研究指標,
召回率(Recall)
: R
= TP / TP +FN ,描述的是某一類樣本。
準確率(accuracy)
: (TP + TN )/( TP + FP + TN + FN),描述的是整體。
F1-score
= 2 / (1/P + 1/R)= 2 * P * R / P + R ,F1-Score是精度和召回率的諧波平均值
,諧波平均值會給予較低的值更高的權重。所以,當精度和召回率都高時,才能得到較高的F1分數(記住結論
)。
from sklearn.metrics import precision_score,recall_score,f1_score
precision_score(y_train_5,y_train_pred)
recall_score(y_train_5,y_train_pred)
f1_score(y_train_5,y_train_pred)
3.4 精度 / 召回率權衡
主要有幾種權衡方式
:精度召回率和閾值關係圖、PR圖(精度和召回率關係)、ROC曲線或AUC。
閾值跟精度成正比,跟召回率成反比。
① 提高閾值:精度提升,召回率降低;
② 降低閾值:精度降低,召回率提升;
sk不允許直接設置閾值,但是可以訪問模型用於預測的決策分數
。decision_function() 用於返回每個實例的分數
,然後預測
: 將實例分數分別與閾值進行比較
,大於閾值爲正類,小於爲負類:
y_scores = sgd.decision_function([some_digit])
y_scores
threshold = 0 # SGD分類器的閾值爲 0
y_some_digit_pred = (y_scores > threshold)
y_some_digit_pred
如果提高閾值,則[some_digit]預測爲False了,說明提高了閾值,降低了召回率(一提召回率就得聯想到真正類率
,正類少了真正類率不就低了,召回率不就降低了)。根據決策分數 和 label,求出精度,召回率,閾值:
y_scores = cross_val_predict(sgd,X_train,y_train_5,cv=3,method="decision_function")
y_scores
#根據決策分數 和 label,求出精度,召回率,閾值
from sklearn.metrics import precision_recall_curve
precisions,recalls,thresholds = precision_recall_curve(y_train_5,y_scores)
precisions,recalls,thresholds
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)
3.5 ROC曲線及召回率的本質
ROC,叫做受試者工作特徵曲線(簡稱ROC),它繪製是的真正類率TPR和假正類率FPR。真負類率TNR(就是粉色部分佔N的比例)叫特異度,所以FPR = 1 - 特異度。ROC繪製的是靈敏度和 (1-特異度) 的關係。
上面說了一大堆了,是不是看着有點暈,再來張通俗點的圖
:
通過上圖和精度召回率的公式,可以看出很多問題哦,比如精度超高,但是召回率超低,你能想到是啥情況麼,自己嘗試畫一下。
如何繪製ROC?
① 計算決策分數y_scores (利用decision_function
或predict_proba
,獲取分類器的分數,在sk中就這倆方法):
y_scores = cross_val_predict(sgd,X_train,y_train_5,cv=3,method="decision_function")
② 計算fpr,tpr (利用roc_curve),繪製圖:
from sklearn.metrics import roc_curve
fpr,tpr,thresholds = roc_curve(y_train_5,y_scores)
def plot_roc_curve(fpr,tpr):
plt.plot(fpr,tpr,linewidth=2.5)
plt.plot([0,1],[0,1],'k--') #畫出對角虛線
plt.axis([0,1,0,1])#設置x軸,y軸的最大最小值
plt.xlabel("FPR")
plt.ylabel("TPR")
plot_roc_curve(fpr,tpr)
③ AUC怎麼計算? AUC,area under curve,即曲線下的面積,其實就是ROC曲線下的面積。
# 計算AUC
from sklearn.metrics import roc_auc_score
roc_auc_score(y_train_5,y_scores)
注意:
roc_curve()求出的值是所有示例的tpr,fpr,而roc_auc_score求出的值只有一個,即面積。
問題
:PR曲線和ROC曲線如何選擇?PR曲線舉例:
選擇的經驗法則
:當正類非常少(或者更關注假正類)時,應該選擇PR曲線。反之,則選擇ROC曲線。
最後,我們再訓練一個RF分類器,與SGD分類器進行比較。
# 訓練RF分類器,與SGD進行ROC的比較
from sklearn.ensemble import RandomForestClassifier
rf = RandomForestClassifier(random_state=42)
y_proba_rf = cross_val_predict(rf,X_train,y_train_5,cv=3,method="predict_proba")
y_proba_rf #格式爲:每個給定實例屬於某個給定類別的概率
y_scores_rf = y_proba_rf[:,1] # 獲取爲正類概率的列表
y_scores_rf
fpr_rf,tpr_rf,thresholds_rf = roc_curve(y_train_5,y_scores_rf)
plt.plot(fpr,tpr,"b:",label="SGD")
plot_roc_curve(fpr_rf,tpr_rf,"Random Forest")
plt.legend(loc="bottom right")
看着ROC,RF要比SGD好很多。我們再看下AUC、精度和召回率,都還不錯。
#訓練RF
y_train_pred_rf = cross_val_predict(rf,X_train,y_train_5,cv=3)
roc_auc_score(y_train_5,y_scores_rf)
precision_score(y_train_5,y_train_pred_rf)
recall_score(y_train_5,y_train_pred_rf)
4. 多類別分類器
有些算法可以直接處理多個類別,比如隨機森林分類器或樸素貝葉斯分類器。但是,有的就不行,比如SVM。
主要有2種實現策略(0~9,10個類別爲例):
① OvA one vs all 一對多
策略:訓練10個二元分類器,每個數字一個(比如0-檢測器,1檢測器,等等); 然後獲取每個分類器的分數,哪個分數最高就選哪個類別。OvA稱爲一對多可能有點繞,其實他就是一 對 其他
。
② OvO one vs one 一對一
策略:爲每一對數字訓練一個二元分類器,比如0和1分類器,0和2分類器,1和3分類器,等等。如果存在N個類別,則就有N * (N-1) ÷ 2個分類器。
對大多數二元分類器來說,OvA是最好的選擇,數據規模大了表現糟糕的算法比如SVM有限選OVO。
sgd.fit(X_train,y_train) #沒有區分 是否爲5 實際sk生成了10個二元分類器
sgd.predict([some_digit])
some_digit_scores = sgd.decision_function([some_digit])
some_digit_scores
maxScore = np.argmax(some_digit_scores) #最高分
sgd.classes_ #所有類別
sgd.classes_[maxScore] #最高分對應的具體類別
有10個分數,說明sk自動生成了10個二元分類器。
如何制定sk使用一對一還是一對多策略呢?則生成一個二元分類器再傳入一對一或一對多分類器的構造函數裏即可。
from sklearn.multiclass import OneVsOneClassifier #一對一
ovo_clf = OneVsOneClassifier(SGDClassifier(random_state=42))
ovo_clf.fit(X_train,y_train)
ovo_clf.predict([some_digit])
len(ovo_clf.estimators_) #分類評估器數量
隨機森林實現多分類
# 隨機森林實現多分類
rf_clf = rf.fit(X_train,y_train)
rf_clf.predict([some_digit])
rf_clf.predict_proba([some_digit]) # 將每個實例分類爲每個類別的概率
#第5個指數0.8,說明圖片是5的概率爲80%
cross_val_score(sgd,X_train,y_train,cv=3,scoring="accuracy")
目前準確率超過了80%,做簡單的標準化之後,準確率能達到90%:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_train.astype(np.float64))
cross_val_score(sgd,X_scaled,y_train,cv=3,scoring="accuracy")
5. 錯誤分析
假設已經找到了一個有潛力的模型,想進一步改進這個單模,方法之一就是分析其錯誤類型。
① 查看混淆矩陣並繪圖。
y_train_pred = cross_val_predict(sgd,X_scaled,y_train,cv=3)
conf_mx = confusion_matrix(y_train,y_train_pred)
conf_mx
plt.matshow(conf_mx,cmap=plt.cm.Blues_r)
#怎麼看? 每行代表實際類別,而每列代表預測類別
使用錯誤率而不是絕對值,因爲絕對值對數量較多的類別不公平:
sum_rows = conf_mx.sum(axis=1,keepdims=True) #獲取每個類別的圖片數量
norm_conf_mx = conf_mx / sum_rows # 求錯誤率
np.fill_diagonal(norm_conf_mx,0) #用0填充對角線
plt.matshow(norm_conf_mx,cmap=plt.cm.Blues_r)
② 上圖已經找到錯誤了,那麼如何去分析和解決錯誤呢?
造成數字混淆的原因很明確,就是兩個數字圖片很像,特徵也類似。解決方式無非就兩種:一是增加訓練樣本;二是增加新特徵,比如,對圖片進行預處理(比如使用sklearn-image、pillow或OpenCV)、增加閉環等新特徵。
6. 多標籤分類
單標籤分類:每個實例都會被分到一個類別裏面。
多標籤分類:有時候我們想把每個實例分到多個類別裏面。比如,一張圖片裏有張三、李四、王五,那它應該輸出[1,0,1]
表示是張三
,不是李四
,是王五
。這種輸出多個二元分類標籤的分類系統稱爲多標籤分類
。
from sklearn.neighbors import KNeighborsClassifier #不是 所有的分類器都支持 多標籤分類
y_train_large = (y_train >= 7) #label大於7的
y_train_odd = (y_train % 2 == 1)#label爲奇數的
y_mul = np.c_[y_train_large,y_train_odd] #合併
y_train_large.shape
y_mul.shape
knn_clf = KNeighborsClassifier()
knn_clf.fit(X_train,y_mul)
knn_clf.predict([some_digit])
結果分類正確:5小於7,5是奇數,所以輸出爲[False,True] 。注意:
並不是 所有的分類器都支持 多標籤分類。
評估多標籤分類器也可以額用之前的評估方式,比如F1分數。
from sklearn.model_selection import cross_val_predict
from sklearn.metrics import f1_score
#KNN交叉驗證跑了半個小時。。。
y_train_knn_pred = cross_val_predict(knn_clf,X_train,y_train,cv=3,n_jobs=-1)
f1_score(y_train,y_train_knn_pred,average="macro")
KNN交叉驗證跑了半個小時。。。說明計算量很大,時間複雜度高。KNN算法缺點:
1. 計算量大,尤其是特徵數非常多的時候
2. 樣本不平衡的時候,對稀有類別的預測準確率低
3. KD樹,球樹之類的模型建立需要大量的內存
4. 是慵懶散學習方法,基本上不學習,導致預測時速度比起邏輯迴歸之類的算法慢
5. 相比決策樹模型,KNN模型的可解釋性不強
7. 多輸出分類
最後一種分類任務叫多輸出 - 多類別分類(多輸出多分類),類似多對多。。咋有這麼變態的分類。。
def plot_digit(data):
image = data.reshape(28, 28)
plt.imshow(image, cmap = mpl.cm.binary,
interpolation="nearest")
plt.axis("off")
noise = np.random.randint(0, 100, (len(X_train), 784))
X_train_mod = X_train + noise
noise = np.random.randint(0, 100, (len(X_test), 784))
X_test_mod = X_test + noise
y_train_mod = X_train
y_test_mod = X_test
some_index = 5500
plt.subplot(121); plot_digit(X_test_mod[some_index])
plt.subplot(122); plot_digit(y_test_mod[some_index])
# save_fig("noisy_digit_example_plot")
knn_clf.fit(X_train_mod, y_train_mod)
clean_digit = knn_clf.predict([X_test_mod[some_index]])
plot_digit(clean_digit)