選自Dataquest
作者:Sebastian Flennerhag
集成方法可將多種機器學習模型的預測結果結合在一起,獲得單個模型無法匹敵的精確結果,它已成爲幾乎所有 Kaggle 競賽冠軍的必選方案。那麼,我們該如何使用 Python 集成各類模型呢?本文作者,曼徹斯特大學計算機科學與社會統計學院的在讀博士 Sebastian Flennerhag 對此進行了一番簡述。
在 Python 中高效堆疊模型
集成(ensemble)正在迅速成爲應用機器學習最熱門和流行的方法。目前,幾乎每一個 Kaggle 冠軍的解決方案都使用了集成,很多數據科學 pipeline 也使用集成。
簡單來說,集成把不同模型的預測結果結合起來,生成最終預測,集成的模型越多,效果就越好。另外,由於集成結合了不同的基線預測,它們的性能至少等同於最優的基線模型。集成使得我們幾乎免費就獲得了性能提升!
集成圖示。輸入數組 X 通過兩個預處理 pipeline 輸入至多個基學習器 f(i)。集成將所有的基學習器的預測結果結合起來,導出最終的預測數組 P。(圖片來源:http://ml-ensemble.com/)
本文介紹集成的基礎知識:定義及工作原理,並提供構建基礎集成的實踐教程。閱讀本文,你將:
理解集成的基礎知識;
瞭解如何編寫集成;
理解集成的主要優缺點。
預測共和黨和民主黨的捐款
在本文中,我們將使用美國政治捐款數據集來解釋集成的工作原理。該數據集最初由 FiveThirtyEight 的 Ben Wieder 製作,他查閱美國政府的政治捐款記錄,發現科學家向政治家們捐款時,通常會選擇民主黨。
該論斷基於向共和黨和民主黨捐款數額的比例。但是,我們還可以從中看出更多事情:比如,哪個學科最可能捐款給共和黨,哪個州最可能捐款給民主黨。我們將更進一步,預測捐款更可能的流向。
此處使用的數據經過稍微修改(https://www.dataquest.io/blog/large_files/input.csv)。我們刪除了捐款給共和黨和民主黨之外的其他黨派的捐款信息,以使過程更加清晰,並且還去除了重複信息和不太有趣的特徵。數據腳本地址:https://www.dataquest.io/blog/large_files/gen_data.py。數據如下:
importnumpy asnp
importpandas aspd
importmatplotlib.pyplot asplt
%matplotlib inline
### Import data
# Always good to set a seed for reproducibility
SEED = 222
np.random.seed(SEED)
df = pd.read_csv('input.csv')
### Training and test set
fromsklearn.model_selection importtrain_test_split
fromsklearn.metrics importroc_auc_score
defget_train_test(test_size=0.95):
"""Split Data into train and test sets."""
y = 1* (df.cand_pty_affiliation == "REP")
X = df.drop(["cand_pty_affiliation"], axis=1)
X = pd.get_dummies(X, sparse=True)
X.drop(X.columns[X.std() == 0], axis=1, inplace=True)
returntrain_test_split(X, y, test_size=test_size, random_state=SEED)
xtrain, xtest, ytrain, ytest = get_train_test()
# A look at the data
print("nExample data:")
df.head()
df.cand_pty_affiliation.value_counts(normalize=True).plot(
kind="bar", title="Share of No. donations")
plt.show()
上圖是 Ben 的論斷的數據依據。確實,75% 的捐款是給民主黨的。我們來看一下可以使用的特徵,我們具備捐款人、交易詳情和捐款接受者的數據信息:
我們使用 ROC-AUC 來評估模型性能。如果你之前沒用過該指標,隨機猜測可以是 0.5 分,完美的召回率和精確率是 1.0。
什麼是集成?
想象一下你在玩常識問答遊戲。一個人玩時,可能會有一些題你完全不瞭解。如果我們想獲得高分,就需要組建一個團隊來覆蓋所有相關主題。這就是集成的基本概念:結合多個模型的預測,對特異性誤差取平均,從而獲得更好的整體預測結果。
一個重要問題是如何結合預測。以常識問答遊戲爲例,我們很容易想象到團隊成員可能會使用多數投票的方式確定選擇哪個答案。機器學習的分類問題也是一樣:作出最常見的類別標籤預測相當於多數投票規則。但是也有很多其他方式可以結合預測,通常我們會使用一個模型來學習如何最好地結合預測結果。
基礎集成的結構。輸入輸送至一系列模型中,元學習器將多個模型的預測結果結合起來。(圖片來源:http://flennerhag.com/2017-04-18-introduction-to-ensembles/)
通過決策樹理解集成
我們用一個簡單的可解釋性模型來解釋集成:使用 if-then 規則的決策樹。決策樹越深,可以捕捉的模式就越複雜,不過也更有可能出現過擬合。因此,我們需要另一種方式來構建決策樹的複雜模型,而不同決策樹的集成就是這樣一種方式。
我麼使用下列輔助函數來可視化決策樹:
importpydotplus # you can install pydotplus with: pip install pydotplus
fromIPython.display importImage
fromsklearn.metrics importroc_auc_score
fromsklearn.tree importDecisionTreeClassifier, export_graphviz
defprint_graph(clf, feature_names):
"""Print decision tree."""
graph = export_graphviz(
clf,
label="root",
proportion=True,
impurity=False,
out_file=None,
feature_names=feature_names,
class_names={0: "D", 1: "R"},
filled=True,
rounded=True
)
graph = pydotplus.graph_from_dot_data(graph)
returnImage(graph.create_png())
在訓練數據上用決策樹擬合一個節點(決策規則),查看它在測試集上的性能:
t1 = DecisionTreeClassifier(max_depth=1, random_state=SEED)
t1.fit(xtrain, ytrain)
p = t1.predict_proba(xtest)[:, 1]
print("Decision tree ROC-AUC score: %.3f"% roc_auc_score(ytest, p))
print_graph(t1, xtrain.columns)
決策樹的 ROC-AUC 得分:0.672
每個葉節點記錄它們在訓練樣本中的比例、類別分佈和類別標籤預測。我們的決策樹根據捐款金額是否超過 101.5 進行預測:它竟然作出了同樣的預測!鑑於 75% 的捐款都給了民主黨,這個結果並不令人驚訝。但是這沒有充分利用我們已有的數據,下面我們使用三層決策規則,看看會得到什麼:
t2 = DecisionTreeClassifier(max_depth=3, random_state=SEED)
t2.fit(xtrain, ytrain)
p = t2.predict_proba(xtest)[:, 1]
print("Decision tree ROC-AUC score: %.3f"% roc_auc_score(ytest, p))
print_graph(t2, xtrain.columns)
決策樹的 ROC-AUC 得分:0.751
該模型並不比簡單的決策樹好太多:預測到的共和黨捐款金額比例只有 5%,遠遠低於 25%。仔細觀察會發現該決策樹使用了很多不確定的分割規則(splitting rule)。觀察結果中高達 47.3% 的結果在最左邊的葉節點中,而 35.9% 在右二的葉節點中。因此大量葉節點沒有關聯。使該模型更深只會導致過擬合。
在深度固定的情況下,決策樹可以通過增加「寬度」的方式來增加複雜度,即創建多個決策樹,並將其連接起來。也就是決策樹的集成。想了解這個集成模型爲什麼會起作用,先要考慮我們如何讓決策樹探索出比上層樹更多的其他模式。最簡單的解決方案就是刪除樹中較早出現的特徵。假如我們刪除了轉賬金額特徵(transaction_amt),樹的根節點,則新的決策樹如下:
drop = ["transaction_amt"]
xtrain_slim = xtrain.drop(drop, 1)
xtest_slim = xtest.drop(drop, 1)
t3 = DecisionTreeClassifier(max_depth=3, random_state=SEED)
t3.fit(xtrain_slim, ytrain)
p = t3.predict_proba(xtest_slim)[:, 1]
print("Decision tree ROC-AUC score: %.3f"% roc_auc_score(ytest, p))
print_graph(t3, xtrain_slim.columns)
決策樹的 ROC-AUC 得分:0.740
ROC-AUC 得分與上樹得分類似,但是共和黨捐款比例增加至 7.3%。還是很低,但比之前稍高一些。重要的是,與第一個樹相反(第一個樹大部分規則與轉賬本身相關),這棵樹更專注於候選人的居住地。現在我們有兩個模型,二者預測能力相近,但基於不同的規則運行。因此,它們可能出現不同的預測誤差,我們可以使用集成方法取其平均數。
爲什麼平均預測有作用
假如我們要基於兩個觀察結果生成預測。第一個觀察結果的真正標籤爲共和黨,第二個是民主黨。在該示例中,假設模型 1 預測結果是民主黨,模型 2 預測結果是共和黨,如下表所示:
如果我們使用標準的 50% 分割規則(50% cutoff rule)進行類別預測,每個決策樹的預測結果都是一個正確一個錯誤。我們對模型的類別概率取平均來創建一個集成。在該示例中,模型 2 對觀察結果 1 的預測是確定的,而模型 1 相對來說不那麼確定。集成對二者的預測進行衡量,然後支持模型 2,正確地預測了共和黨。至於第二個觀察結果,局面扭轉過來,集成正確地預測到民主黨:
如果集成包含兩個以上決策樹,則它根據多數原則進行預測。因此,集成對分類器預測結果取平均又叫作多數投票分類器(majority voting classifier)。當集成基於概率取平均時,我們稱其爲軟投票,而對類別標籤預測結果取平均被成爲硬投票。
當然,集成不是萬能的。你可能注意到上述示例中,取平均有效的前提是預測誤差必須不相關。如果兩個模型都作出了錯誤的預測,則集成無法作出進行修正。此外,在軟投票機制中,如果一個模型作出了錯誤的預測,但概率值較高,則集成可能會作出錯誤的判斷。通常,集成無法使每個預測都正確,但是預計其性能優於底層模型。
森林是樹的集成
回到我們的預測問題,看看我們是否可以用兩個決策樹構建一個集成。首先檢查誤差關聯性:高度關聯的誤差會造成差的集成。
p1 = t2.predict_proba(xtest)[:, 1]
p2 = t3.predict_proba(xtest_slim)[:, 1]
pd.DataFrame({"full_data": p1,
"red_data": p2}).corr()
有一些關聯性,但不過分:預測方差仍有很大的利用空間。爲了構建該集成,我們簡單地平均了兩個模型的預測。
p1 = t2.predict_proba(xtest)[:, 1]
p2 = t3.predict_proba(xtest_slim)[:, 1]
p = np.mean([p1, p2], axis=0)
print("Average of decision tree ROC-AUC score: %.3f"% roc_auc_score(ytest, p))
決策樹的 ROC-AUC 平均分值:0.783
確實,集成步驟導致分值增加。但是如果我們有更多不同的樹,我們甚至可以得到更大的分值。在設計決策樹時,我們應該去除哪些特徵?
一個快速有效的實踐方法是隨機地選擇一個特徵子集,在每個 draw 上擬合一個決策樹並平均其預測。這一過程被稱爲自舉平均(bootstrapped averaging,通常縮寫爲 bagging),它應用於決策樹所產生的模型是隨機森林。讓我們看看隨機森林能爲我們做什麼。我們使用 Scikit-learn 實現構建了 10 個決策樹的集成,每一個擬合包含 3 個特徵的子集。
fromsklearn.ensemble importRandomForestClassifier
rf = RandomForestClassifier(
n_estimators=10,
max_features=3,
random_state=SEED
)
rf.fit(xtrain, ytrain)
p = rf.predict_proba(xtest)[:, 1]
print("Average of decision tree ROC-AUC score: %.3f"% roc_auc_score(ytest, p))
決策樹的 ROC-AUC 平均分值:0.844
隨機森林極大改進了我們之前的模型。但是隻使用決策樹可以做的事情比較有限。是時候擴展我們的視野了。
作爲平均預測的集成
目前爲止,我們看到了集成的兩個重要方面:
1. 預測誤差的關聯性越低,效果越好
2. 模型越多,效果越好
出於這一原因,儘可能使用不同模型不失爲一個好方法(只要它們表現良好)。目前爲止我們一直在依賴簡單的平均,但是稍後我們將瞭解如何使用更復雜的結合。爲了記錄進程,我們把集成公式化爲如下:
涵蓋的模型沒有限制:決策樹、線性模型、核模型、非參數模型、神經網絡,或者甚至其他集成!記住我們包含的模型越多,集成的速度就會越慢。
爲了構建不同模型的集成,我們首先在數據集上對一組 Scikit-learn 分類器進行基準測試。爲了避免代碼重複,我們使用下面的輔助函數:
# A host of Scikit-learn models
fromsklearn.svm importSVC, LinearSVC
fromsklearn.naive_bayes importGaussianNB
fromsklearn.ensemble importRandomForestClassifier, GradientBoostingClassifier
fromsklearn.linear_model importLogisticRegression
fromsklearn.neighbors importKNeighborsClassifier
fromsklearn.neural_network importMLPClassifier
fromsklearn.kernel_approximation importNystroem
fromsklearn.kernel_approximation importRBFSampler
fromsklearn.pipeline importmake_pipeline
defget_models():
"""Generate a library of base learners."""
nb = GaussianNB()
svc = SVC(C=100, probability=True)
knn = KNeighborsClassifier(n_neighbors=3)
lr = LogisticRegression(C=100, random_state=SEED)
nn = MLPClassifier((80, 10), early_stopping=False, random_state=SEED)
gb = GradientBoostingClassifier(n_estimators=100, random_state=SEED)
rf = RandomForestClassifier(n_estimators=10, max_features=3, random_state=SEED)
models = {'svm': svc,
'knn': knn,
'naive bayes': nb,
'mlp-nn': nn,
'random forest': rf,
'gbm': gb,
'logistic': lr,
}
returnmodels
deftrain_predict(model_list):
"""Fit models in list on training set and return preds"""
P = np.zeros((ytest.shape[0], len(model_list)))
P = pd.DataFrame(P)
print("Fitting models.")
cols = list()
fori, (name, m) inenumerate(models.items()):
print("%s..."% name, end=" ", flush=False)
m.fit(xtrain, ytrain)
P.iloc[:, i] = m.predict_proba(xtest)[:, 1]
cols.append(name)
print("done")
P.columns = cols
print("Done.n")
returnP
defscore_models(P, y):
"""Score model in prediction DF"""
print("Scoring models.")
form inP.columns:
score = roc_auc_score(y, P.loc[:, m])
print("%-26s: %.3f"% (m, score))
print("Done.n")
我們現在正準備創建一個預測矩陣 P,其中每個特徵對應於由給定模型做出的預測,並根據測試集爲每個模型評分:
models = get_models()
P = train_predict(models)
score_models(P, ytest)
這是我們的基線。梯度提升機(Gradient Boosting Machine/GBM)效果最好,其次是簡單的 logistic 迴歸。對於我們的集成策略來說,預測誤差必須是相對不關聯的。
# You need ML-Ensemble for this figure: you can install it with: pip install mlens
frommlens.visualization importcorrmat
corrmat(P.corr(), inflate=False)
plt.show()
誤差明顯關聯,這對於表現良好的模型是可以預期的,因爲它是典型的異常值,很難糾正。然而,大多數關聯性在 50-80%的範圍內,所以還有很大的改進餘地。事實上,如果我們從類別預測的角度看誤差關聯性,事情看起來會更有希望:
corrmat(P.apply(lambdapred: 1*(pred >= 0.5) - ytest.values).corr(), inflate=False)
plt.show()
爲了創建集成,我們繼續並進行平均預測,正如我們所期望的,集成的性能要好於基線。平均化是一個簡單的過程,如果我們存儲模型預測,我們可以從一個簡單的集成開始,並在訓練新模型時隨時增加其大小。
print("Ensemble ROC-AUC score: %.3f"% roc_auc_score(ytest, P.mean(axis=1)))
集成的 ROC-AUC 分值:0.884
可視化集成的工作過程
我們已經對集成的誤差關聯機制有所瞭解。這意味着集成通過平均謬誤可以平滑決策邊界。決策邊界向我們表明評估器如何將特徵空間分割成鄰域,其中所有的觀察結果被預測爲具有相同的分類標籤。通過平均基學習器決策邊界,集成被賦予更平滑的邊界,泛化也更自然。
下圖展示了這一點。這裏的實例是鳶尾花數據集,其中評估者試圖對三種花進行分類。基學習器在其邊界都有一些不良的特性,但是這個集成有一個相對平滑的決策邊界,與觀察結果一致。令人驚訝的是,集成既增加了模型的複雜度,也起到了正則化項的作用!
三個模型及其集成的決策邊界示例
當任務是分類時,另一種理解集成的方式是檢查 ROC 曲線(Receiver Operator Curve),它向我們展示了評估者如何進行精確率和召回率之間的權衡。通常,不同的基學習器做出不同的權衡:一些通過犧牲召回率實現更高的精確率,另一些則相反。
另一方面,對於每個訓練點,非線性元學習器可以調整其依賴的模型。這意味其可以極大地減少不必要的犧牲,並在增加召回率的同時保持高精確率(反之亦然)。下圖中,集成在精確率上做了一個更小的犧牲,以增加召回率。
fromsklearn.metrics importroc_curve
defplot_roc_curve(ytest, P_base_learners, P_ensemble, labels, ens_label):
"""Plot the roc curve for base learners and ensemble."""
plt.figure(figsize=(10, 8))
plt.plot([0, 1], [0, 1], 'k--')
cm = [plt.cm.rainbow(i)
fori innp.linspace(0, 1.0, P_base_learners.shape[1] + 1)]
fori inrange(P_base_learners.shape[1]):
p = P_base_learners[:, i]
fpr, tpr, _ = roc_curve(ytest, p)
plt.plot(fpr, tpr, label=labels[i], c=cm[i + 1])
fpr, tpr, _ = roc_curve(ytest, P_ensemble)
plt.plot(fpr, tpr, label=ens_label, c=cm[0])
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC curve')
plt.legend(frameon=False)
plt.show()
plot_roc_curve(ytest, P.values, P.mean(axis=1), list(P.columns), "ensemble")
超越簡單平均值的集成
但是在預測誤差變動一定的情況下,你不會期望更多的提升嗎?一些模型表現要比其他模型相對糟糕,但是其影響一樣大。這對不平衡數據集來說是毀滅性的:如果一個模型做出極端預測(即接近於 0 或 1),則通過軟投票召回,因爲其對預測平均值有極大的影響。
對我們來說,一個重要的因素是模型是否可以捕捉到共和黨所收捐款的全部比例。一個簡單的檢查表明所有模型對共和黨捐款比例的預測都過低,其中一些相對更糟。
p = P.apply(lambdax: 1*(x >= 0.5).value_counts(normalize=True))
p.index = ["DEM", "REP"]
p.loc["REP", :].sort_values().plot(kind="bar")
plt.axhline(0.25, color="k", linewidth=0.5)
plt.text(0., 0.23, "True share republicans")
plt.show()
我們嘗試通過去除最糟糕的來提升集成,比如說多層感知機(MLP):
include = [c forc inP.columns ifc notin["mlp-nn"]]
print("Truncated ensemble ROC-AUC score: %.3f"% roc_auc_score(ytest, P.loc[:, include].mean(axis=1)))
截斷集成的 ROC-AUC 分值:0.883
實際上不算提升:我們需要一個更聰明的方式來排定模型之間的優先順序。很明顯,從一個集成中刪除模型是相當猛烈的,因爲有可能刪除帶有重要信息的模型。我們真正想要的是學習平均預測時使用的一組合理的權重。這把集成變成了一個需要訓練的參數化模型。
學習結合預測
學習加權平均值意味着對於每個模型 f_i 都有一個權重參數 ω_i∈(0,1) 將權重分配給該模型的預測。加權平均值需要所有的權重總和爲 1。現在,集成的定義如下:
這與之前的定義有一些小的改變,但是很有趣的一點是:一旦模型生成預測 p_i=f_i(x),則學習權重就相當於基於預測來擬合線性迴歸:
權重有一些約束項。然後,我們不用僅擬合線性模型了。假設我們擬合最近鄰模型。集成會基於給定觀察結果的最近鄰取局部平均值,這樣集成就可以適應模型性能隨着輸入變化而產生的改變。
實現集成
要構建這種類型的集成,我們需要:
1. 用於生成預測的基學習器庫;
2. 學習如何最佳結合預測結果的元學習器;
3. 在基學習器和元學習器之間分割訓練數據的方法。
基學習器採用原始輸入,生成一系列預測。如果我們的原始數據集是形態爲 (n_samples, n_features) 的矩陣 X,那麼基學習器庫輸出新的預測矩陣 P_base,形態爲 (n_samples, n_base_learners),其中每一列代表一個基學習器的預測結果。元學習器在 P_base 上訓練。
這意味着恰當地處理訓練集 X 至關重要。尤其是,如果我們在 X 上訓練基學習器,用它們預測 X,則元學習器將在基學習器的訓練誤差上訓練,但在測試時元學習器將面對基學習器的測試誤差。
我們需要一種策略來生成反映測試誤差的預測矩陣 P。最簡單的策略是將完整的數據集 X 分割成兩部分:在其中一半上訓練基學習器,然後讓它們預測另一半,然後將預測輸入元學習器。這種方法又簡單又快,不過會丟失一些數據。對於中小型規模的數據集,信息丟失可能會比較嚴重,導致基學習器和元學習器性能不好。
爲了保證覆蓋完整的數據集,我們可以使用交叉驗證法。有很多方式可以執行交叉驗證,在那之前,我們先來一步一步地實現集成。
第一步:定義基學習器的庫
它們是處理輸入數據並生成預測的模型,可以是線性迴歸,也可以是神經網絡,甚至可以是另一個集成。和往常一樣,多樣性是強大的!唯一需要注意的是,我們加入越多的模型,集成運行的速度就會越慢。在這裏,我會使用此前的模型集合:
base_learners =get_models()
第二步:定義一個元學習器
應該使用哪個元學習器,人們並沒有統一看法,但目前流行的選擇是線性模型、基於核的模型(支持向量機和 KNN 算法)以及基於決策樹的模型。你也可以使用另一個集成作爲「元學習器」:在這種特殊情況下,你最終會得到一個兩層的集成,這有點類似於前饋神經網絡。
在這裏,我們會使用一個梯度提升機。爲了確保 GBM 能夠探索局部特徵,我們需要限定每 1000 個決策樹在 4 個基學習器的隨機子集和 50% 的輸入數據上進行訓練。這樣,GBM 就會表達每個基學習器在不同近鄰輸入空間上的預測內容。
meta_learner =GradientBoostingClassifier(
n_estimators=1000,
loss="exponential",
max_features=4,
max_depth=3,
subsample=0.5,
learning_rate=0.005,
random_state=SEED
)
第三步:定義步驟,生成訓練和測試集
爲簡單起見,我們將完整訓練集分爲基學習器的訓練集和預測集。這種方法有時被稱爲「混合(Blending)」。不過,不同社區之間的術語是不同的,所以知道集成使用了哪種類型的交叉驗證有時並不容易。
xtrain_base,xpred_base,ytrain_base,ypred_base =train_test_split(
xtrain,ytrain,test_size=0.5,random_state=SEED)
我們現在有一個爲基學習器準備的訓練集(X_train_base,y_train_base)和一個預測集(X_pred_base,y_pred_base),準備好爲元學習器生成預測矩陣了。
第四步:在訓練集上訓練基學習器
爲在訓練數據上訓練基學習器,我們照常運行:
deftrain_base_learners(base_learners,inp,out,verbose=True):
"""Train all base learners in the library."""
ifverbose:print("Fitting models.")
fori,(name,m)inenumerate(base_learners.items()):
ifverbose:print("%s..."%name,end=" ",flush=False)
m.fit(inp,out)
ifverbose:print("done")
爲訓練基學習器,我們需要執行:
train_base_learners(base_learners,xtrain_base,ytrain_base)
第五步:生成基學習器預測
基學習器擬合後,我們現在可以生成一系列預測用於訓練元學習器。注意,我們生成的基於觀測值的預測並不會用於基學習器的訓練,對於每個觀測:
在基學習器預測集中,我們生成了基學習器預測結果的集合:
如果你實現自己的集成,請特別注意如何索引預測矩陣的行和列——將數據分成兩個部分並不難,但對於後來的交叉驗證就很有挑戰性了。
defpredict_base_learners(pred_base_learners,inp,verbose=True):
"""Generate a prediction matrix."""
P =np.zeros((inp.shape[0],len(pred_base_learners)))
ifverbose:print("Generating base learner predictions.")
fori,(name,m)inenumerate(pred_base_learners.items()):
ifverbose:print("%s..."%name,end=" ",flush=False)
p =m.predict_proba(inp)
# With two classes, need only predictions for one class
P[:,i]=p[:,1]
ifverbose:print("done")
returnP
爲生成預測,我們需要執行:
P_base =predict_base_learners(base_learners,xpred_base)
第六步:訓練元學習器
預測矩陣 P_base 反映了測試時間的性能,可被用於訓練元學習器:
meta_learner.fit(P_base,ypred_base)
就是這樣!我們現在有了完全訓練的集成,可用來預測新數據了。爲生成觀測值 x(j) 的預測,我們先要將其輸入基學習器。它們輸出一系列預測
我們再將其輸入元學習器。元學習器會給我們集成的最終預測
現在我們對於集成學習有了一個明確的認識,是時候看看它能對政治捐款數據集做出怎樣的預測了:
defensemble_predict(base_learners,meta_learner,inp,verbose=True):
"""Generate predictions from the ensemble."""
P_pred =predict_base_learners(base_learners,inp,verbose=verbose)
returnP_pred,meta_learner.predict_proba(P_pred)[:,1]
爲生成預測,我們需要執行:
P_pred,p =ensemble_predict(base_learners,meta_learner,xtest)
print("nEnsemble ROC-AUC score: %.3f"%roc_auc_score(ytest,p))
集成的 ROC-AUC 分數:0.881
正如我們所料,集成擊敗了此前基準的最佳估計,但它仍然無法擊敗簡單的平均集成。這是因爲我們只對一半的數據進行基學習器和元學習器的訓練,所以大量的信息丟失了。爲了防止這點,我們需要使用交叉驗證策略。
利用交叉驗證訓練
在交叉驗證訓練基學習器時,每個基學習器的備份都進行了 K-1 fold 的擬合,並進行了剩餘 fold 的預測。這一過程不斷重複,直到每個 fold 都被預測。我們指定的 fold 越多,每次訓練過程中的數據就越少。這使得交叉驗證的預測在測試期間噪聲更小,性能更好。但這顯著增加了訓練時間。通過交叉驗證擬合一個集成經常被稱爲堆疊(stacking),而集成本身會被稱爲超級學習器(Super Learner)。
爲理解交叉驗證是如何運作的,我們可以把它想象爲之前集成的一個外循環(outer loop)。外循環在個格不同的測試 fold 上迭代,而其餘的數據用於訓練;內循環訓練基學習器併產生預測數據。這是一個簡單的堆疊實現:
fromsklearn.base importclone
defstacking(base_learners,meta_learner,X,y,generator):
"""Simple training routine for stacking."""
# Train final base learners for test time
print("Fitting final base learners...",end="")
train_base_learners(base_learners,X,y,verbose=False)
print("done")
# Generate predictions for training meta learners
# Outer loop:
print("Generating cross-validated predictions...")
cv_preds,cv_y =[],[]
fori,(train_idx,test_idx)inenumerate(generator.split(X)):
fold_xtrain,fold_ytrain =X[train_idx,:],y[train_idx]
fold_xtest,fold_ytest =X[test_idx,:],y[test_idx]
# Inner loop: step 4 and 5
fold_base_learners ={name:clone(model)
forname,model inbase_learners.items()}
train_base_learners(
fold_base_learners,fold_xtrain,fold_ytrain,verbose=False)
fold_P_base =predict_base_learners(
fold_base_learners,fold_xtest,verbose=False)
cv_preds.append(fold_P_base)
cv_y.append(fold_ytest)
print("Fold %i done"%(i +1))
print("CV-predictions done")
# Be careful to get rows in the right order
cv_preds =np.vstack(cv_preds)
cv_y =np.hstack(cv_y)
# Train meta learner
print("Fitting meta learner...",end="")
meta_learner.fit(cv_preds,cv_y)
print("done")
returnbase_learners,meta_learner
讓我們來看看這裏涉及的步驟。首先,我們在所有數據上擬合基學習器:這與之前的混合集成相反,基學習器在測試時間內訓練了所有數據。隨後我們遍歷所有 fold,隨後遍歷所有基學習器生成交叉驗證預測。這些預測堆疊在一起構成了元學習器的訓練集——它也訓練了所有數據。
混合和堆疊的基本區別在於,堆疊允許基學習器和元學習器在全部數據集上進行訓練。使用雙重交叉驗證,我們可以測量這種情況下的差異:
fromsklearn.model_selection importKFold
# Train with stacking
cv_base_learners,cv_meta_learner =stacking(
get_models(),clone(meta_learner),xtrain.values,ytrain.values,KFold(2))
P_pred,p =ensemble_predict(cv_base_learners,cv_meta_learner,xtest,verbose=False)
print("nEnsemble ROC-AUC score: %.3f"%roc_auc_score(ytest,p))
集成 的 ROC-AUC 分數:0.889
堆疊帶來了可觀的性能提升:事實上,它得到了目前的最佳分數。對於中小型數據集來說,這種成績是非常典型的,其中混合的影響比重很大。隨着數據集體量的增大,混合與堆疊的表現會逐漸趨同。
堆疊也有自己的缺點,特別是速度。通常,在交叉驗證的情況下,我們需要知道這些問題:
1. 計算複雜度
2. 結構複雜度(信息泄露的風險)
3. 內存用量
理解它們對於高效使用集成方法來說非常重要,讓我們一一道來。
1. 計算複雜度
假設我們需要使用 10 折堆疊。這需要在 90% 的數據上訓練所有基學習器 10 次,然後在所有數據上再訓練一次。若有 4 個基學習器,集成需要花費的時間大約是最佳基學習器的 40 倍。
但每個 cv-fit 都是獨立的,所以我們不需要依次擬合模型。如果我們能夠平行擬合所有 fold,集成就只會比最佳基學習器慢 4 倍——這是一個巨大的提升。集成是並行化的最佳受益者,能夠充分利用這一機制對它來說至關重要。爲所有模型擬合所有 fold,集成的時間懲罰就可以忽略不計了。爲了介紹這一點,下圖是 ML-Ensemble 上的一個基準,它展示了 4 個線程上依次或並行堆疊或混合擬合所花費的時間。
即使有了這種程度的並行性,我們也可以減少大量計算時間。然而,並行化與一系列潛在的棘手問題有關,如競態條件、鎖死和內存爆炸。
2. 結構複雜度
當我們決定在元學習器上使用整個訓練集時,我們必須關注「信息泄露」問題。當錯誤地預測訓練期間使用的樣本時,就會出現這種現象,例如混合了不同的 fold,或使用了錯誤的訓練子集。當元學習器在訓練集上出現信息泄露時,預測錯誤就會產生:garbage in、garbage out。發現這樣的 bug 是非常困難的。
3. 內存用量
並行化的最後一個問題,特別是在 Python 中多任務處理時經常會碰到的問題。在這種情況下,每個子進程都有自己的內存,同時需要複製父進程中所有的數據。因此,一個未做優化的實現會複製程序的所有數據,佔用大量內存,浪費數據序列化的時間。爲阻止這種情況,我們需要共享數據存儲器,而這反過來又容易導致數據損壞。
結果:使用工具包
這一問題的結果是需要使用一個經過測試、且爲你的機器學習方法構建的軟件包。事實上,一旦你用上了集成工具包,構建集成就會變得非常簡單:你需要做的僅僅是選定基學習器、元學習器,以及訓練集成的方法。
幸運的是,今天每個流行的編程語言裏都有很多可用的工具包——雖然它們有着不同的風格。在本文的末尾我會列舉其中的一些。現在,讓我們選用其中的一個,看看集成方法是如何處理政治捐款數據集的。在這裏,我們使用 ML-Ensemble 來構建我們之前提到的廣義集合,但現在使用 10 折交叉驗證。
frommlens.ensemble importSuperLearner
# Instantiate the ensemble with 10 folds
sl =SuperLearner(
folds=10,
random_state=SEED,
verbose=2,
backend="multiprocessing"
)
# Add the base learners and the meta learner
sl.add(list(base_learners.values()),proba=True)
sl.add_meta(meta_learner,proba=True)
# Train the ensemble
sl.fit(xtrain,ytrain)
# Predict the test set
p_sl =sl.predict_proba(xtest)
print("nSuper Learner ROC-AUC score: %.3f"%roc_auc_score(ytest,p_sl[:,1]))
Fitting2layers
Processinglayer-1done |00:02:03
Processinglayer-2done |00:00:03
Fitcomplete |00:02:08
Predicting2layers
Processinglayer-1done |00:00:50
Processinglayer-2done |00:00:02
Predictcomplete |00:00:54
SuperLearnerROC-AUC score:0.890
就是這麼簡單!
觀察超級學習器簡單平均集合的 ROC 曲線,其中展示了超級學習器如何利用全部數據僅犧牲少量召回率即可獲得給定精確率。
plot_roc_curve(ytest,p.reshape(-1,1),P.mean(axis=1),["Simple average"],"Super Learner")
繼續進發
除了本文介紹的幾種集成外,目前還有很多其他的集成方式,不過它們的基礎都是一樣的:一個基學習器庫、一個元學習器,以及一個訓練程序。通過調整這些組件的配合,我們可以設計出各種特定的集合形式。有關更高級集合的內容請參閱這篇文章:https://mlwave.com/kaggle-ensembling-guide/。
當我們談到軟件時,每個人都有自己的喜好。隨着集成方法的流行,集成工具包的數量也越來越多。實際上集成方法是先在統計學社區中流行起來的,所以 R 語言中有很多爲此設計的庫。近年來,Python 和其他語言中也出現了更多相關工具。每個工具包都能滿足不同的需求,處於不同的成熟階段,所以我建議大家選用前先瀏覽一番再做決策。
下表列出了其中一些工具:
原文鏈接:https://www.dataquest.io/blog/introduction-to-ensembles/