機器學習系統模型調優實戰--所有調優技術都附相應的scikit-learn實現

引言

如果你對機器學習算法已經很熟悉了,但是有時候你的模型並沒有很好的預測效果或者你想要追求更好地模型性能。那麼這篇文章會告訴你一些最實用的技術診斷你的模型出了什麼樣的問題,並用什麼的方法來解決出現的問題,並通過一些有效的方法可以讓你的模型具有更好地性能。

介紹數據集

這個數據集有569個樣本,它的前兩列爲唯一的ID號和診斷結果 (M = malignant, B = benign) ,它的3->32列爲實數值特徵,我不是醫學專家,我不太明白具體特徵的是什麼意思,都是關於細胞的,但是,機器學習的偉大之處就在於這點,即使我們不是一個這方面的專家,我們依然可以讀懂這些數據,掌握數據中的模式,從而我們也可以像一個專家一樣做出預測。

下面的鏈接有數據集更詳細的介紹,有興趣的朋友可以看看。

http://archive.ics.uci.edu/ml/datasets/Breast+Cancer+Wisconsin+(Diagnostic)

初識pipeline

在訓練機器學習算法的過程中,我們用到了不同的數據預處理技術,比如:標準化,PCA等。在scikit-learn中,我們可以用pipeline這個非常方便的類幫我們簡化這些過程。這個類幫我們用任意次的轉換步驟來擬合模型並預測新的數據。下面,我用具體代碼來演示這個類的好處:

import pandas as pd
df = pd.read_csv('http://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.data', header=None) # 讀取數據集

from sklearn.preprocessing import LabelEncoder
X = df.loc[:, 2:].values # 抽取訓練集特徵
y = df.loc[:, 1].values # 抽取訓練集標籤
le = LabelEncoder()
y = le.fit_transform(y) # 把字符串標籤轉換爲整數,惡性-1,良性-0

from sklearn.cross_validation import train_test_split
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=1) # 拆分成訓練集(80%)和測試集(20%)

# 下面,我要用邏輯迴歸擬合模型,並用標準化和PCA(30維->2維)對數據預處理,用Pipeline類把這些過程鏈接在一起
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline

# 用StandardScaler和PCA作爲轉換器,LogisticRegression作爲評估器
estimators = [('scl', StandardScaler()), ('pca', PCA(n_components=2)), ('clf', LogisticRegression(random_state=1))]
# Pipeline類接收一個包含元組的列表作爲參數,每個元組的第一個值爲任意的字符串標識符,比如:我們可以通過pipe_lr.named_steps['pca']來訪問PCA組件;第二個值爲scikit-learn的轉換器或評估器
pipe_lr = Pipeline(estimators)
pipe_lr.fit(X_train, y_train)
print(pipe_lr.score(X_test, y_test))

當我們調用Pipeline的fit方法時,StandardScaler調用fit和transform方法來將數據標準化,接着標準化後的數據被傳到PCA中,PCA也執行fit和transform方法對標準化後的數據進行降維,最後把降維的數據用邏輯迴歸模型擬合,當然了,這只是我的一個演示,你可以把任意多的轉換步驟放到Pipeline中。而且,它的另一個優點是,當我們評估測試集的時候,它也會用上面轉換過程保留的參數來轉換測試集。這一切是不是很Cool?Pipeline就像一個工廠流水線一樣,把所有的步驟鏈接到了一起,具體的細節完全不用我們自己操心。整個流程可以用下面的圖形概括。

scikit-learn的Pipeline流程示意圖

用holdout和k-fold交叉驗證評估模型性能

holdout交叉驗證

在我們上面的那個例子中,我們只把數據集分成了訓練集和測試集。然而在實際的機器學習應用中,我們會選擇最優的模型和最優的學習參數來提高我們算法對未見到的數據預測的性能。然而,如果我們在模型選擇或調節參數的過程中,一遍又一遍地用我們的測試集,那麼它也變成了我們測試集的一部分了,很有可能這樣的參數和模型只適應當前的測試集,而對沒有見過的數據集它的性能很不好,也就是發生了過擬合現象。

因此,一個更好地方式是把我們的數據集分成三個部分:把上面例子中的訓練集分成訓練集和交叉驗證集,測試集。現在,我們構建機器學習的系統步驟應該是:

  1. 選擇模型和參數,用訓練集去擬合
  2. 把擬合後的模型和參數應用到交叉驗證集來評估其性能
  3. 不斷地重複1,2兩個過程,直到挑選出我們滿意的模型和參數
  4. 用測試集去評估步驟3選出的模型,看它在未見過的數據上的性能

但是,holdout交叉驗證有個缺點是:樣本不同的訓練集和交叉驗證集會產生不同的性能評估。下面,讓我們介紹k-fold交叉驗證來解決這個問題。

k-fold交叉驗證

在holdout交叉驗證中,我們把訓練集拆分成訓練集和交叉驗證集。在k-fold交叉驗證中,我們把訓練集拆分成k份,k-1份用作訓練,1份用作測試。然後,我們把這k份中的每份都用來測試剩下的份用作訓練,因此,我們會得到k個模型和性能的評估,最後求出性能的平均值。下面,我假設k=10,看下圖:

k-fold交叉驗證

上圖中,我把訓練集分成10份,在10次迭代中,9份被用作訓練,1份被用作測試,最後我們求出平均性能。注意:在每次迭代中,我們並沒有重新劃分訓練集,我們只是最初分成10份,接着在每次迭代中,用這10份中的每一份做測試剩下的9份用作訓練。

對於大多數的應用k=10是個合理的選擇。然而,如果我們的訓練集相對較小,我們可以增加k值,因此,在每次迭代中我們將有更多的訓練集來擬合模型,結果是我們對泛化的性能有更低地偏差。另一方面,如果我們有更大地數據集,我們可以選擇一個更小地值k,即使k值變小了,我們依然可以得到一個對模型性能的準確評估,與此同時,我們還減少了重新擬合模型的計算代價。

在scikit-learn的實現中,它對我們的k-fold交叉驗證做了一個小小的改進,它在每個份的訓練集中都有相同的類別比例,請看如下代碼:

from sklearn.cross_validation import StratifiedKFold
import numpy as np

scores = []
kfold = StratifiedKFold(y=y_train, n_folds=10, random_state=1) # n_folds參數設置爲10份
for train_index, test_index in kfold:
    pipe_lr.fit(X_train[train_index], y_train[train_index])
    score = pipe_lr.score(X_train[test_index], y_train[test_index])
    scores.append(score)
    print('類別分佈: %s, 準確度: %.3f' % (np.bincount(y_train[train_index]), score))

np.mean(scores) # 求出評估的平均值,0.94956521739130439

# 輸出如下:
類別分佈: [257 153], 準確度: 0.891
類別分佈: [257 153], 準確度: 0.978
類別分佈: [257 153], 準確度: 0.978
類別分佈: [257 153], 準確度: 0.913
類別分佈: [257 153], 準確度: 0.935
類別分佈: [257 153], 準確度: 0.978
類別分佈: [257 153], 準確度: 0.933
類別分佈: [257 153], 準確度: 0.956
類別分佈: [257 153], 準確度: 0.978
類別分佈: [257 153], 準確度: 0.956

上面,我們自己寫for循環去擬合每個訓練集。scikit-learn有一種更有效地方式幫我們實現了上述的方法:

from sklearn.cross_validation import cross_val_score

scores = cross_val_score(estimator=pipe_lr, X=X_train, y=y_train, cv=10, n_jobs=1)
print(scores)

# 輸出如下:
[ 0.89130435  0.97826087  0.97826087  0.91304348  0.93478261  0.97777778
  0.93333333  0.95555556  0.97777778  0.95555556]

上面的n_jobs參數可以指定我們機器上的多個CPU來評估我們每份不同的訓練集,這是一個非常有用的參數。

學習曲線和驗證曲線

學習曲線

通過繪製模型訓練和驗證準確性關於訓練集大小的函數,我們能很容易地診斷出模型是高方差還是高偏差。

學習曲線

左上角的那個圖像是高偏差,這個模型的訓練集準確性和交叉驗證集準確性都很低,這表明它欠擬合數據。解決高偏差問題通常要增加模型的參數,比如,構建更多的樣本特徵或減小正則化的程度。

右上角的那個圖像是高方差,這個模型的訓練集準確性和交叉驗證集準確性之間有個很大的缺口,這表明模型很好地擬合了訓練集,但是對未見過的數據效果很差。對於過擬合問題,我們可以收集更多地數據或降低模型的複雜度。有一點我們應該注意,如果訓練集有很多的噪音或模型已經接近最優性能了,我們收集在多的數據也於事無補。

下面,我們用具體的代碼來繪製學習曲線。

from sklearn.learning_curve import learning_curve

pipe_lr = Pipeline([('scl', StandardScaler()), ('clf', LogisticRegression(penalty='l2', random_state=0))])
# train_sizes參數指定用於生成學習曲線的訓練集數量,如果是分數指的是相對數量,整數指的是絕對數量
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()

學習曲線

從上圖我們可以看出,模型在交叉訓練集上表現地很好。但是,它是有點過擬合的,因爲在兩個曲線之間有一點明顯地間隔。

驗證曲線

學習曲線是訓練集數量與準確性之間的函數。而驗證曲線是不同的模型參數與準確性之間的函數。具體代碼如下:

from sklearn.learning_curve 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.legend(loc='lower right')
plt.xlabel('Parameter C')
plt.ylabel('Accuracy')
plt.ylim([0.8, 1.0])
plt.show()

驗證曲線

從上圖我們可以看到隨着參數C的增大,模型有點過擬合數據,因爲C越大,就意味着正則化的強度越小。然而,對於小的參數C來說,正則化的強度很大,模型有點欠擬合。我感覺當C在0.1左右是最好的。

通過網格搜索(grid search)微調機器學習模型

在機器學習應用中,我們有兩種類型的參數:一個是從訓練集中學得的參數,例如邏輯迴歸的權重;另一個是爲了使學習算法達到最優化可調節的參數,例如邏輯迴歸中的正則化參數或決策樹中的深度參數。這種可調節的參數稱爲超參數(hyperparameters)。

上面我們用驗證曲線調節超參數中的一個參數來優化模型。現在,我們要用網格搜索這個更加強大的超參數優化工具來找到超參數值的最優組合從而進一步改善模型的性能。

網格搜索的思路其實很簡單,就是列舉出所有你想要調節的參數,然後窮舉出所有參數組合,最後得出一個使模型性能最好的參數組合。下面,讓我們來調節SVM分類器的C,kernel,gamma參數,代碼如下:

from sklearn.grid_search 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_)

clf = gs.best_estimator_
clf.fit(X_train, y_train)
print('Test accuracy: %.3f' % clf.score(X_test, y_test))

性能指標之 precision, recall, 和F1-score

度量模型的性能指標不僅僅是它的準確性,還有 precision, recall, 和F1-score。下圖的四個方塊中分別計算了對應情況的數量,我們可以把它看作是一個2 × 2的矩陣。

precision, recall, 和F1-score

scikit-learn可以很容易地得出上面的矩陣形式。代碼如下:

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

precision, recall

假設類別1(惡性)爲positive類別,那麼我們的模型正確地分類屬於類別0的71個樣本(true negatives) 和屬於類別1的40個樣本(true positives)。然而,我們的模型也錯誤地分類原本屬於類別1而預測爲0的2個樣本(false negatives)和原本屬於類別0而預測爲1的1個樣本(false positive)。接下來,我們用這個信息來計算不同的誤差評價標準。

在類別很不平衡的機器學習系統中,我們通常用precision(PRE)和recall(REC)來度量模型的性能,下面我給出它們的公式:

PRE=TPTP+FPREC=TPTP+FN

在實際中,我們通常結合兩者,組成F1-score:

F1=2PRE×RECPRE+REC

上面3種測量手段,在scikit-learn中都已經實現:

from sklearn.metrics import precision_score, recall_score, f1_score
precision_score(y_true=y_test, y_pred=y_pred)
recall_score(y_true=y_test, y_pred=y_pred)
f1_score(y_true=y_test, y_pred=y_pred)

ROC

在介紹ROC曲線前,我先給出true positive rate(TPR)和false positive rate(FPR)的定義:

TPR=TPFN+TPFPR=FPTN+FP

ROC是一種選擇分類模型的工具,它是基於true positive rate(TPR)和false positive rate(FPR)的性能來做出選擇的。我們通過移動分類器的決策闕值來計算TPR和FPR。ROC圖像上的對角線可以看作是隨機猜測的結果,如果分類模型在對角線的下面則證明它的性能比隨機猜測的結果還要糟糕。基於ROC曲線,我們可以計算出描述分類模型性能的AUC(area under the curve)。在ROC曲線中,左下角的點所對應的是將所有樣例判爲反例的情況,而右上角的點對應的則是將所有樣例判爲正例的情況。

下面的代碼繪製了ROC曲線。

from sklearn.metrics import roc_curve, auc
from scipy import interp

X_train2 = X_train[:, [4, 14]]
cv = StratifiedKFold(y_train, n_folds=3, random_state=1)
fig = plt.figure()

mean_tpr = 0.0
mean_fpr = np.linspace(0, 1, 100)
all_tpr = []

# plot每個fold的ROC曲線,這裏fold的數量爲3,被StratifiedKFold指定
for i, (train, test) in enumerate(cv):
    # 返回預測的每個類別(這裏爲0或1)的概率
    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, linewidth=1, label='ROC fold %d (area = %0.2f)' % (i+1, roc_auc))


# plot random guessing line
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)
# plot perfect performance line
plt.plot([0, 0, 1], [0, 1, 1], lw=2, linestyle=':', color='black', label='perfect performance')
# 設置x,y座標範圍
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.show()

ROC曲線

上面代碼中涉及到roc_curve類,詳情請參考官方文檔:http://scikit-learn.org/stable/modules/generated/sklearn.metrics.roc_curve.html

發佈了106 篇原創文章 · 獲贊 213 · 訪問量 59萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章