1. 程式人生 > >機器學習系統模型調優實戰--所有調優技術都附相應的scikit-learn實現

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

引言

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

介紹資料集

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

下面的連結有資料集更詳細的介紹,有興趣的朋友可以看看。

初識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曲線