Python之ML–模型評估與參數調優
主要知識點如下:
- 模型性能的無偏估計
- 處理機器學習算法常見問題
- 機器學習模型調優
- 使用不同的性能指標評估預測模型
一.基於流水線的工作流
本節使用scikit-learn中的Pipline類.它使得我們可以擬合出包含任意多個處理步驟的模型,並將模型用於新數據的預
1.威斯康星乳腺癌數據集
威斯康星乳腺癌(Breast Cancer Wisconsin)數據集進行講解,此數據集共包含了569個惡性或者良性腫瘤樣本.數據集的前兩列分別存儲了樣本唯一的ID以及對樣本的診斷結果(M代表惡性,B代表良性).數據集的3-32列包含了30個從細胞核照片中提取,用實數值標識的特征,它們可以用於構建判定模型,對腫瘤是良性還是惡性做出預測
使用pandas從UCI網站直接讀取數據集
import pandas as pd
df=pd.read_csv('https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data',header=None)
print('rows, columns:', df.shape)
df.head()
rows, columns: (569, 32)
0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | … | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 842302 | M | 17.99 | 10.38 | 122.80 | 1001.0 | 0.11840 | 0.27760 | 0.3001 | 0.14710 | … | 25.38 | 17.33 | 184.60 | 2019.0 | 0.1622 | 0.6656 | 0.7119 | 0.2654 | 0.4601 | 0.11890 |
1 | 842517 | M | 20.57 | 17.77 | 132.90 | 1326.0 | 0.08474 | 0.07864 | 0.0869 | 0.07017 | … | 24.99 | 23.41 | 158.80 | 1956.0 | 0.1238 | 0.1866 | 0.2416 | 0.1860 | 0.2750 | 0.08902 |
2 | 84300903 | M | 19.69 | 21.25 | 130.00 | 1203.0 | 0.10960 | 0.15990 | 0.1974 | 0.12790 | … | 23.57 | 25.53 | 152.50 | 1709.0 | 0.1444 | 0.4245 | 0.4504 | 0.2430 | 0.3613 | 0.08758 |
3 | 84348301 | M | 11.42 | 20.38 | 77.58 | 386.1 | 0.14250 | 0.28390 | 0.2414 | 0.10520 | … | 14.91 | 26.50 | 98.87 | 567.7 | 0.2098 | 0.8663 | 0.6869 | 0.2575 | 0.6638 | 0.17300 |
4 | 84358402 | M | 20.29 | 14.34 | 135.10 | 1297.0 | 0.10030 | 0.13280 | 0.1980 | 0.10430 | … | 22.54 | 16.67 | 152.20 | 1575.0 | 0.1374 | 0.2050 | 0.4000 | 0.1625 | 0.2364 | 0.07678 |
5 rows × 32 columns
接下來,將數據集的30個特征的賦值給一個Numpy的數組對象X.使用scikit-learn中的LabelEncoder類,我們可以將類標從原始的字符串表示(M或者B)轉換為整數
from sklearn.preprocessing import LabelEncoder
X=df.loc[:,2:].values
y=df.loc[:,1].values
le=LabelEncoder()
y=le.fit_transform(y)
轉換后的類標(診斷結果)存儲在一個數組y中,此時惡性腫瘤和良性腫瘤分別被標識為類1和類0,我們通過LabelEncoder的transform方法來顯示虛擬類標(0和1)
le.transform(['M','B'])
array([1, 0], dtype=int64)
from sklearn.model_selection import train_test_split
X_train,X_test,y_train,y_test=train_test_split(X,y,test_size=0.2,random_state=1)
2.在流水線中集成數據轉換及評估操作
出於性能優化的目的,許多學習算法要求將不同特征的值縮放到相同的范圍.我們在使用邏輯斯諦回歸模型等線性分類器分析威斯康星乳腺癌數據集之前,需要對其特征列做標准化處理.我們無需在訓練數據集和測試數據集上分別進行模擬擬合,數據轉換,而是通過流水線將StandardScaler,PCA以及LogisticRegression對象串聯起來:
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
pipe_lr=Pipeline([('scl',StandardScaler()),('pca',PCA(n_components=2)),('clf',LogisticRegression(random_state=1))])
pipe_lr.fit(X_train,y_train)
print('Test Accuracy:%.3f'%pipe_lr.score(X_test,y_test))
Test Accuracy:0.947
Pipeline對象采用元組的序列作為輸入,其中每個元組中的第一個值為一個字符串,它可以是任意的標識符,我們通過它來訪問流水線中的元素,而元組的第二個值則為scikit-learn中的一個轉換器或者評估器
流水線中包含了scikit-learn中用於數據預處理的類,最后還包括一個評估器.在前面的示例代碼中,流水線中有兩個預處理環節,分別是用於數據縮放和轉換的StandardScaler及PCA,最后還有一個作為評估器的邏輯斯諦回歸分類器
from IPython.display import Image
二.使用k折交叉驗證評估模型性能
如果一個模型過於簡單,將會面臨欠擬合(高偏差)的問題,而模型基於訓練數據構造得過於復雜,則會導致過擬合(高方差)問題.交叉驗證技術:holdout交叉驗證(holdout cross-validation)和k折交叉驗證(k-fold cross-validation)
1.holdout方法
通過holdout方法,我們將最初的數據集划分為訓練數據集和測試數據集:前者用於模型的訓練,而后者則用於性能的評估.然而,為了進一步提高模型在預測未知數據上的性能,我們還要對不同參數設置進行調優和比較.該過程稱為模型選擇(model selection),指的是針對給定問題我們調整參數以尋求最優值(也稱為超參,hyperparameter)的過程
使用holdout進行模型選擇更好的方法是將數據划分為三個部分:訓練數據集,驗證數據集和測試數據集
holdout方法的一個缺點在於:模型性能的評估對訓練數據集划分為訓練及驗證子集的方法是敏感的:評價的結果會隨樣本的不同而發生變化
2.k折交叉驗證
在k折交叉驗證中,我們不重復地隨機將訓練數據集划分為k個,其中k-1個用於模型的訓練,剩余的1個用於測試.重復此過程k次,我們就得到了k個模型及對模型性能的評價
下面通過scikit-learn中的StratifiedKFold迭代器來演示
import numpy as np
from sklearn.model_selection import StratifiedKFold
kfold=StratifiedKFold(n_splits=10,random_state=1).split(X_train,y_train)
scores=[]
for k,(train,test) in enumerate(kfold):
pipe_lr.fit(X_train[train],y_train[train])
score=pipe_lr.score(X_train[test],y_train[test])
scores.append(score)
print('Fold:%s,Class dist:%s,Acc:%.3f'%(k+1,np.bincount(y_train[train]),score))
Fold:1,Class dist:[256 153],Acc:0.891
Fold:2,Class dist:[256 153],Acc:0.978
Fold:3,Class dist:[256 153],Acc:0.978
Fold:4,Class dist:[256 153],Acc:0.913
Fold:5,Class dist:[256 153],Acc:0.935
Fold:6,Class dist:[257 153],Acc:0.978
Fold:7,Class dist:[257 153],Acc:0.933
Fold:8,Class dist:[257 153],Acc:0.956
Fold:9,Class dist:[257 153],Acc:0.978
Fold:10,Class dist:[257 153],Acc:0.956
print('CV accuracy:%.3f+/-%.3f'%(np.mean(scores),np.std(scores)))
CV accuracy:0.950+/-0.029
首先,我們用訓練集中的類標y_train來初始化sklearn.model_selection模塊下的StratifiedKfold迭代器,並通過n_folds參數來設置塊的數量.當我們使用kfold迭代器在k個塊中進行循環時,使用train中返回的索引去擬合所構建的邏輯斯諦回歸流水線,通過pile_lr流水線,我們可以保證每次迭代中樣本都得到適當的縮放(如標准化).然后使用test索引計算模型的准確率,將其存儲在score列表中,用於計算平均准確率以及性能評估標准差
使用分層k折交叉驗證對模型進行評估
from sklearn.model_selection import cross_val_score
scores=cross_val_score(estimator=pipe_lr,X=X_train,y=y_train,cv=10,n_jobs=-1)
print('CV accuracy scores:%s'%scores)
CV accuracy scores:[0.89130435 0.97826087 0.97826087 0.91304348 0.93478261 0.97777778
0.93333333 0.95555556 0.97777778 0.95555556]
print('CV accuracy:%.3f+/-%.3f'%(np.mean(scores),np.std(scores)))
CV accuracy:0.950+/-0.029
cross_val_score方法具備一個較為有用的特點,它可以將不同分塊的性能評估分布到多個CPU上進行處理
三.通過學習及驗證曲線來調試算法
兩個提高學習算法性能的簡單但功能強大的判定工具:學習曲線(learning curve)與驗證曲線(validation curve)
1.使用學習曲線判定偏差和方差問題
我們先通過下圖來討論一個模型常見的兩個問題:
左上圖圖像顯示的是一個高偏差模型.此模型的訓練准確率和交叉驗證准確率都很低,這表明此模型未能很好地擬合數據.解決此問題的常用方法是增加模型中的參數的數量,例如收集或構建額外特征,或者降低類似於SVM和邏輯斯諦回歸器等模型的正則化程度.右上圖圖像中的模型面臨高方差的問題,表明訓練准確度與交叉驗證准確度之間有很大差距,針對此類過擬合問題,我們可以收集更多的訓練數據或者降低模型的復雜度,如增加正則化的參數
看一下如何使用scikit-learn中的學習曲線函數評估模型
import matplotlib.pyplot as plt
from sklearn.model_selection import learning_curve
pipe_lr=Pipeline([
('scl',StandardScaler()),
('clf',LogisticRegression(penalty='l1',random_state=0))
])
train_sizes,train_scores,test_scores=learning_curve(estimator=pipe_lr,X=X_train,y=y_train,train_sizes=np.linspace(0.1,1.0,10),cv=10,n_jobs=-1)
train_mean=np.mean(train_scores,axis=1)
train_std=np.std(train_scores,axis=1)
test_mean=np.mean(test_scores,axis=1)
test_std=np.std(test_scores,axis=1)
plt.plot(train_sizes,train_mean,color='blue',marker='o',markersize=5,label='training accuracy')
plt.fill_between(train_sizes,train_mean+train_std,train_mean-train_std,alpha=0.15,color='blue')
plt.plot(train_sizes,test_mean,color='green',linestyle='--',marker='s',markersize=5,label='validation accuracy')
plt.fill_between(train_sizes,test_mean+test_std,test_mean-test_std,alpha=0.15,color='green')
plt.grid()
plt.xlabel('Number of training samples')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8,1.0])
plt.show()
通過learning_curve函數的train_size參數,我們可以控制用於生成學習曲線的樣本的絕對或相對數量.通過設置train_sizes=np.linspace(0.1,1.0,10)來使用訓練數據集上等距間隔的10個樣本.默認情況下,learning_curve函數使用分層k折交叉驗證來計算交叉驗證的准確性,通過cv參數將k的值設置為10.在繪制圖像時,我們通過fill_between函數加入了平均准確率標准差的信息,用以表示評價結果的方差
2.通過驗證曲線來判定過擬合與欠擬合
邏輯斯諦回歸模型中的正則化參數C.使用scikit-learn來繪制驗證曲線
from sklearn.model_selection import validation_curve
param_range=[0.001,0.01,0.1,1.0,10.0,100.0]
train_scores,test_scores=validation_curve(
estimator=pipe_lr,
X=X_train,
y=y_train,
param_name='clf__C',
param_range=param_range,
cv=10
)
train_mean=np.mean(train_scores,axis=1)
train_std=np.std(train_scores,axis=1)
test_mean=np.mean(test_scores,axis=1)
test_std=np.std(test_scores,axis=1)
plt.plot(param_range,train_mean,color='blue',marker='o',markersize=5,label='training accuracy')
plt.fill_between(param_range,train_mean+train_std,train_mean-train_std,alpha=0.15,color='blue')
plt.plot(param_range,test_mean,color='green',linestyle='--',marker='s',markersize=5,label='validation accuracy')
plt.fill_between(param_range,test_mean+test_std,test_mean-test_std,alpha=0.15,color='green')
plt.grid()
plt.xscale('log')
plt.xlabel('Parameter C')
plt.ylabel('Accuracy')
plt.legend(loc='lower right')
plt.ylim([0.8,1.0])
plt.show()
雖然不同C值之間准確率的差異非常小,但我們可以看到,如果加大正則化強度(較小的C值),會導致模型輕微的欠擬合;如果增加C的值,這意味着降低正則化的強度,因此模型會趨於過擬合.最優點在C=0.1
四.使用網格搜索調優機器學習模型
在機器學習中,有兩類參數:通過訓練數據學習得到的參數,如邏輯斯諦回歸中的回歸系數;以及學習算法中需要單獨進行優化的參數.后者即為調優參數,也稱為超參,對模型來說,就如邏輯斯諦回歸中的正則化系數,或者決策樹中的深度參數
接下來學習一種功能強大的超參數優化技巧:網格搜索(grid search),它通過尋找最優的超參值的組合以進一步提高模型的性能
1.使用網格搜索調優超參
網格搜索法非常簡單,它通過對我們指定的不同超參列表進行暴力窮舉搜索,並計算評估每個組合對模型性能的影響,以獲得參數的最優組合
from sklearn.model_selection import GridSearchCV
from sklearn.svm import SVC
pipe_svc=Pipeline([('scl',StandardScaler()),('clf',SVC(random_state=1))])
param_range=[0.0001,0.001,0.01,0.1,1.0,10.0,100.0,1000.0]
param_grid=[{'clf__C':param_range,'clf__kernel':['linear']},
{'clf__C':param_range,'clf__gamma':param_range,'clf__kernel':['rbf']}]
gs=GridSearchCV(estimator=pipe_svc,param_grid=param_grid,scoring='accuracy',cv=10,n_jobs=-1)
gs=gs.fit(X_train,y_train)
print(gs.best_score_)
print(gs.best_params_)
0.978021978021978
{'clf__C': 0.1, 'clf__kernel': 'linear'}
我們將GridSearchCV的param_grid參數以字典的方式定義為待調優的參數,通過best_score屬性得到最優模型的性能評分,具體參數信息可通過best_params_屬性獲得
最后,我們將使用獨立的測試數據集,通過GridSearchCV對象的best_estimator_屬性對最優模型進行性能評估
clf=gs.best_estimator_
clf.fit(X_train,y_train)
print('Test accuracy:%.3f'%clf.score(X_test,y_test))
Test accuracy:0.965
2.通過嵌套交叉驗證選擇算法
在嵌套交叉驗證的外圍循環中,我們將數據划分為訓練塊及測試塊;而在用於模型選擇的內部循環中,我們則基於這些訓練塊使用k折交叉驗證.在完成模型的選擇后,測試模塊用於模型性能的評估
下圖通過5個外圍模塊及2個內部模塊解釋嵌套交叉驗證的概念
通過如下方式使用嵌套交叉驗證
gs=GridSearchCV(estimator=pipe_svc,param_grid=param_grid,scoring='accuracy',cv=10,n_jobs=-1)
scores=cross_val_score(gs,X,y,scoring='accuracy',cv=5)
print('CV accuracy:%.3f+/-%.3f'%(np.mean(scores),np.std(scores)))
CV accuracy:0.972+/-0.012
我們使用嵌套交叉驗證方法比較SVM模型與簡單的決策樹分類器;為了簡單起見,我們只調優樹的深度參數
from sklearn.tree import DecisionTreeClassifier
gs=GridSearchCV(
estimator=DecisionTreeClassifier(random_state=0),
param_grid=[{'max_depth':[1,2,3,4,5,6,7,None]}],
scoring='accuracy',
cv=5
)
scores=cross_val_score(gs,X_train,y_train,scoring='accuracy',cv=5)
print('CV accuracy:%.3f+/-%.3f'%(np.mean(scores),np.std(scores)))
CV accuracy:0.908+/-0.045
在此可見:嵌套交叉驗證對SVM模型性能的評分(97.8%)遠高於決策樹的(90.8%).由此,可以預期:SVM是用於對此數據集未知數據進行分類的一個好的選擇
五.不同的性能評價指標
准確率(precision),召回率(recall)以及F1分數(F1-score)
1.讀取混淆矩陣
先繪制一個所謂的混淆矩陣(confusion matrix):即展示學習算法性能的一種矩陣.混淆矩陣是一個簡單的方陣,用於展示一個分類器預測結果–真正(true positive),真負(true negative),假正(false positive)及假負(false negative)–的數量
雖然這指標的數據可以通過人工比較真實類標與預測類標來獲得,不過scikit-learn提供了一個方便使用的confusion_matrix函數,其使用方法如下:
from sklearn.metrics import confusion_matrix
pipe_svc.fit(X_train,y_train)
y_pred=pipe_svc.predict(X_test)
confmat=confusion_matrix(y_true=y_test,y_pred=y_pred)
print(confmat)
[[71 1]
[ 2 40]]
使用matplotlib中的matshow函數將它們表示出上圖所示的混淆矩陣形式
fig,ax=plt.subplots(figsize=(2.5,2.5))
ax.matshow(confmat,cmap=plt.cm.Blues,alpha=0.3)
for i in range(confmat.shape[0]):
for j in range(confmat.shape[1]):
ax.text(x=j,y=i,s=confmat[i,j],va='center',ha='center')
plt.xlabel('predicted label')
plt.ylabel('true label')
plt.show()
在本例中,假定類別1(惡性)為正類,模型正確地預測了71個屬於類別0的樣本(真負),以及40個屬於類別1的樣本(真正).不過我們的模型也錯誤地將兩個屬於類別0的樣本划分到了類別1(假負),另外還將一個惡性腫瘤誤判為良性的(假正)
2.優化分類模型的准確率和召回率
預測誤差(error,ERR)和准確率(accuracy,ACC)都提供了誤分類樣本數量的相關信息.誤差可以理解為預測錯誤樣本的數量與所有被預測樣本數量的比值,而准確率計算方法則是正確預測樣本的數量與所有被預測樣本數量的比值:
預測准確率也可以通過誤差直接計算:
對於類別數量不均衡的分類問題來說,真正率(TPR)與假正率(FPR)是非常有用的性能指標:
准確率(precision,PRE)和召回率(recall,REC)是與真正率,真負率相關的性能評價指標,實際上,召回率與真正率含義相同:
所有這些評分指標均以在scikit-learn中實現,可以從sklearn.metric模塊中導入使用
from sklearn.metrics import precision_score
from sklearn.metrics import recall_score,f1_score
print('Precision:%.3f'%precision_score(y_true=y_test,y_pred=y_pred))
print('Recall:%.3f'%recall_score(y_true=y_test,y_pred=y_pred))
print('F1:%.3f'%f1_score(y_true=y_test,y_pred=y_pred))
Precision:0.976
Recall:0.952
F1:0.964
3.繪制ROC曲線
受試者工作特征曲線(receiver operator characteristic,ROC)是基於模型假正率和真正率等性能指標進行分類模型選擇的有用工具,假正率和真正率可以通過移動分類器的分類閾值來計算.基於ROC曲線,我們就可以計算所謂的ROC線下區域(area under the curve,AUC),用來刻畫分類模型的性能
我們將StratifiedKFold驗證器中的分塊數量減少為3
from sklearn.metrics import roc_curve, auc
from scipy import interp
pipe_lr = Pipeline([('scl', StandardScaler()),
('pca', PCA(n_components=2)),
('clf', LogisticRegression(penalty='l2',
random_state=0,
C=100.0))])
X_train2 = X_train[:, [4, 14]]
cv = list(StratifiedKFold(n_splits=3,random_state=1).split(X_train, y_train))
fig = plt.figure(figsize=(7, 5))
mean_tpr = 0.0
mean_fpr = np.linspace(0, 1, 100)
all_tpr = []
for i, (train, test) in enumerate(cv):
probas = pipe_lr.fit(X_train2[train],
y_train[train]).predict_proba(X_train2[test])
fpr, tpr, thresholds = roc_curve(y_train[test],
probas[:, 1],
pos_label=1)
mean_tpr += interp(mean_fpr, fpr, tpr)
mean_tpr[0] = 0.0
roc_auc = auc(fpr, tpr)
plt.plot(fpr,
tpr,
lw=1,
label='ROC fold %d (area = %0.2f)'
% (i+1, roc_auc))
plt.plot([0, 1],
[0, 1],
linestyle='--',
color=(0.6, 0.6, 0.6),
label='random guessing')
mean_tpr /= len(cv)
mean_tpr[-1] = 1.0
mean_auc = auc(mean_fpr, mean_tpr)
plt.plot(mean_fpr, mean_tpr, 'k--',
label='mean ROC (area = %0.2f)' % mean_auc, lw=2)
plt.plot([0, 0, 1],
[0, 1, 1],
lw=2,
linestyle=':',
color='black',
label='perfect performance')
plt.xlim([-0.05, 1.05])
plt.ylim([-0.05, 1.05])
plt.xlabel('false positive rate')
plt.ylabel('true positive rate')
plt.title('Receiver Operator Characteristic')
plt.legend(loc="lower right")
plt.tight_layout()
# plt.savefig('./figures/roc.png', dpi=300)
plt.show()
分類器在只有兩個特征的訓練集上完成擬合后,使用如下代碼計算分類器在單獨測試集上的ROC AUC得分
pipe_lr = pipe_lr.fit(X_train2, y_train)
y_labels = pipe_lr.predict(X_test[:, [4, 14]])
y_probas = pipe_lr.predict_proba(X_test[:, [4, 14]])[:, 1]
from sklearn.metrics import roc_auc_score, accuracy_score
print('ROC AUC: %.3f' % roc_auc_score(y_true=y_test, y_score=y_probas))
print('Accuracy: %.3f' % accuracy_score(y_true=y_test, y_pred=y_labels))
ROC AUC: 0.752
Accuracy: 0.711