文章目錄
相關文章:
監督學習 | 集成學習之Bagging、隨機森林及Sklearn實現
Boosting
提升法
(Boosting,最初被稱爲假設提升法)是指可以將幾個弱學習器結合成一個強學習器的任意集成方法。大多數提升法的總體思路是循環訓練預測器,每一次都對其前序做出一些改正。可用的提升法有很多,但目前最流行的方法是AdaBoost
(Adaptive Boosting,自適應提升法)和梯度提升
。
1. AdaBoost
新預測器對其前序進行糾正的方法之一,就是更多地關注前序擬合不足的訓練實例。從而使新的預測器不斷地越來越專注於難纏的問題,這就是 AdaBoost
使用的技術。
例如要構建一個 AdaBoost 分類器,首先需要訓練一個基礎分類器(比如決策樹),用它對訓練集進行預測,然後對錯誤分類的訓練實例增加其相對權重。接着,使用這個新的權重對第二個分類器進行訓練,然後再次對訓練集進行預測,繼續更新權重,並不斷循環前進。[1]
1.1 AdaBoost 原理
AdaBoost
有多種推導方式,比較容易理解的是基於“加線性模型”(additive model),即基學習器 的線性組合:
來最小化指數損失函數
(exponential loss function):
若 能令指數損失函數最小化,則考慮公式 (2) 對 的偏導:
令公式 (3) 爲零可解得:
因此有:
這意味着 達到了貝葉斯最優錯誤率。換言之,若指數損失函數
最小化,則分類錯誤率也將最小化;這說明指數損失函數是分類問題原本0/1 損失函數
的一致替代損失函數。由於這個替代函數是連續可微的,因此我們用它代替 0/1 損失函數作爲優化目標。
在 AdaBoost 算法中,第一個基分類器 是通過直接將基學習器用於初始數據分佈而得;此後迭代地生成 和 ,當基分類器 基於分佈 產生後,該基分類器的權重 應使得 最小化指數損失函數:
其中 。考慮指數損失函數的導數:
令公式 (7) 爲零得:
即 AdaBoost 算法第六行中的分類器權重更新公式
。
AdaBoost 算法在活得 之後的樣本分佈將進行調整,使下一輪的基學習器 能糾正 的一些錯誤。理想的 能糾正 的全部錯誤,即最小化
注意到 ,式 (9) 可以使用 的泰勒展開式近似爲:
於是,理想的基學習器
注意到 是一個常數。令 表示一個分佈:
則根據數學期望的定義,這等價於令
由 ,有
由此可見,理想的 將在分佈 下最小化分類誤差,因此,弱分類器將給予分佈 來訓練,且針對 的分類誤差應小於 0.5 。這在一定程度上類似“殘差逼近”的思想。考慮到 和 的關係,有
這恰是 AdaBoost 算法第 7 行的樣本分佈更新公式
。
Boosting 算法要求基學習器能對特定的數據分佈進行學習,這可通過“重賦權法
”( re-weighting)實施,即在訓練過程的每一輪中,根據樣本分佈爲每個訓練樣本重新賦予一個權重。
對無法接受帶權樣本的基學習算法,則可通過“重採樣法
”( re-sampling)來處理,即在每一輪學習中,根據樣本分佈對訓練集重新進行採樣,再用重採樣而得的樣本集對基學習器進行訓練。
一般而言,這兩種做法沒有顯著的優劣差別,需注意的是,Boostig 算法在訓練的每一輪都要檢查當前生成的基學習器是否滿足基本條件(圖 1 第 5 行,檢査當前基分類器是否是比隨機猜測好),一旦條件不滿足,則當前基學習器即被拋棄,且學習過程停止。在此種情形下,初始設置的學習輪數也許還遠未達到,可能導致最終集成中只包含很少的基學習器而性能不佳。若採用“重採樣法”,則可獲得“重啓動”機會以避免訓練過程過早停止,即在拋棄不滿足條件的當前基學習器之後,可根據當前分佈重新對訓練樣本進行採樣,再基於新的採樣結果重新訓練出基學習器,從而使得學習過程可以持續到預設的 T 輪完成。
從偏差一方差分解的角度看, Boosting 主要關注降低偏差,因此 Boosting 能基於泛化性能相當弱的學習器構建出很強的集成。[2]
1.2 Python 實現
下面使用 5 個連續的 SVM (RBF 核、高度正則化)分類器對衛星數據進行預測。
# 導入數據
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.datasets import make_moons
X, y = make_moons(n_samples=500, noise=0.30, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
from matplotlib.colors import ListedColormap
def plot_decision_boundary(clf, X, y, axes=[-1.5, 2.5, -1, 1.5], alpha=0.5, contour=True):
x1s = np.linspace(axes[0], axes[1], 100)
x2s = np.linspace(axes[2], axes[3], 100)
x1, x2 = np.meshgrid(x1s, x2s)
X_new = np.c_[x1.ravel(), x2.ravel()]
y_pred = clf.predict(X_new).reshape(x1.shape)
custom_cmap = ListedColormap(['#fafab0','#9898ff','#a0faa0'])
plt.contourf(x1, x2, y_pred, alpha=0.3, cmap=custom_cmap)
if contour:
custom_cmap2 = ListedColormap(['#7d7d58','#4c4c7f','#507d50'])
plt.contour(x1, x2, y_pred, cmap=custom_cmap2, alpha=0.8)
plt.plot(X[:, 0][y==0], X[:, 1][y==0], "yo", alpha=alpha)
plt.plot(X[:, 0][y==1], X[:, 1][y==1], "bs", alpha=alpha)
plt.axis(axes)
plt.xlabel(r"$x_1$", fontsize=18)
plt.ylabel(r"$x_2$", fontsize=18, rotation=0)
m = len(X_train)
plt.figure(figsize=(11, 4))
for subplot, learning_rate in ((121, 1), (122, 0.5)):
sample_weights = np.ones(m)
plt.subplot(subplot)
for i in range(5):
svm_clf = SVC(kernel="rbf", C=0.05, gamma="auto", random_state=42)
svm_clf.fit(X_train, y_train, sample_weight=sample_weights)
y_pred = svm_clf.predict(X_train)
sample_weights[y_pred != y_train] *= (1 + learning_rate) # 對錯誤分類的點增加權重
plot_decision_boundary(svm_clf, X, y, alpha=0.2)
plt.title("learning_rate = {}".format(learning_rate), fontsize=16)
if subplot == 121:
plt.text(-0.7, -0.65, "1", fontsize=14)
plt.text(-0.6, -0.10, "2", fontsize=14)
plt.text(-0.5, 0.10, "3", fontsize=14)
plt.text(-0.4, 0.55, "4", fontsize=14)
plt.text(-0.3, 0.90, "5", fontsize=14)
plt.show()
第一個分類器產生了許多錯誤實例,所以這些實例的權重得到提升。因此第二個分類器在這些實例上的表現有所提升,然後第三個、第四個…右圖繪製的是相同預測器序列,唯一的差別在於學習了減半(即每次迭代僅提升一般錯誤分類的實例的權重)。
1.3 Sklearn 實現
from sklearn.ensemble import AdaBoostClassifier
AdaBoostClassifier(base_estimator=None, n_estimators=50, learning_rate=1.0, algorithm=’SAMME.R’, random_state=None)
from sklearn.ensemble import AdaBoostRegressor
AdaBoostRegressor(base_estimator=None, n_estimators=50, learning_rate=1.0, loss=’linear’, random_state=None)
如果 AdaBoost 集成過擬合,可以減少估算器的數量,或是提高基礎估算器的正則化程度。
以單層決策樹作爲基學習器創建 AdaBoost 分類器:
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import AdaBoostClassifier
ada_clf = AdaBoostClassifier(
DecisionTreeClassifier(max_depth=1), n_estimators=200,
algorithm="SAMME.R", learning_rate=0.5, random_state=42)
ada_clf.fit(X_train, y_train)
AdaBoostClassifier(algorithm='SAMME.R',
base_estimator=DecisionTreeClassifier(class_weight=None,
criterion='gini',
max_depth=1,
max_features=None,
max_leaf_nodes=None,
min_impurity_decrease=0.0,
min_impurity_split=None,
min_samples_leaf=1,
min_samples_split=2,
min_weight_fraction_leaf=0.0,
presort=False,
random_state=None,
splitter='best'),
learning_rate=0.5, n_estimators=200, random_state=42)
plot_decision_boundary(ada_clf, X, y)
2. 梯度提升
另一個非常受歡迎的提升方法就是梯度提升
(Grandient Boosting)。跟 AdaBoost 一樣,梯度提升也是逐步在集成中添加預測器,每一個都對其前序做出改正。不同之處在於,它不是像 AdaBoost 那樣在每個迭代中調整實例權重,而是讓新的預測器針對前一個預測器的殘差進行擬合。
2.1 梯度提升迴歸樹(GBRT)
使用決策樹作爲基學習器來進行迴歸,這被稱爲梯度樹提升
或是梯度提升迴歸樹
(GBRT)。
2.1.1 Python 實現
np.random.seed(42)
X = np.random.rand(100, 1) - 0.5
y = 3*X[:, 0]**2 + 0.05 * np.random.randn(100) # 帶噪聲的二次訓練集
# 首先,在訓練集上擬合一個 DecisionTreeRegressor
from sklearn.tree import DecisionTreeRegressor
tree_reg1 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg1.fit(X, y)
DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=42, splitter='best')
# 現在,針對第一個預測器的殘差,訓練第二個 DecisionTreeRegressor
y2 = y - tree_reg1.predict(X)
tree_reg2 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg2.fit(X, y2)
DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=42, splitter='best')
# 然後,針對第二個預測器的殘差,訓練第三個 DecisionTreeRegressor
y3 = y2 - tree_reg2.predict(X)
tree_reg3 = DecisionTreeRegressor(max_depth=2, random_state=42)
tree_reg3.fit(X, y3)
DecisionTreeRegressor(criterion='mse', max_depth=2, max_features=None,
max_leaf_nodes=None, min_impurity_decrease=0.0,
min_impurity_split=None, min_samples_leaf=1,
min_samples_split=2, min_weight_fraction_leaf=0.0,
presort=False, random_state=42, splitter='best')
X_new = np.array([[0.8]])
# 現在,我們有了一個包含三棵樹的集成。
# 它將所有樹的預測相加,從而對新實例進行預測
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
y_pred
array([0.75026781])
def plot_predictions(regressors, X, y, axes, label=None, style="r-", data_style="b.", data_label=None):
x1 = np.linspace(axes[0], axes[1], 500)
y_pred = sum(regressor.predict(x1.reshape(-1, 1)) for regressor in regressors)
plt.plot(X[:, 0], y, data_style, label=data_label)
plt.plot(x1, y_pred, style, linewidth=2, label=label)
if label or data_label:
plt.legend(loc="upper center", fontsize=16)
plt.axis(axes)
plt.figure(figsize=(11,11))
plt.subplot(321)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h_1(x_1)$", style="g-", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Residuals and tree predictions", fontsize=16)
plt.subplot(322)
plot_predictions([tree_reg1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1)$", data_label="Training set")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.title("Ensemble predictions", fontsize=16)
plt.subplot(323)
plot_predictions([tree_reg2], X, y2, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_2(x_1)$", style="g-", data_style="k+", data_label="Residuals")
plt.ylabel("$y - h_1(x_1)$", fontsize=16)
plt.subplot(324)
plot_predictions([tree_reg1, tree_reg2], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1)$")
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.subplot(325)
plot_predictions([tree_reg3], X, y3, axes=[-0.5, 0.5, -0.5, 0.5], label="$h_3(x_1)$", style="g-", data_style="k+")
plt.ylabel("$y - h_1(x_1) - h_2(x_1)$", fontsize=16)
plt.xlabel("$x_1$", fontsize=16)
plt.subplot(326)
plot_predictions([tree_reg1, tree_reg2, tree_reg3], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="$h(x_1) = h_1(x_1) + h_2(x_1) + h_3(x_1)$")
plt.xlabel("$x_1$", fontsize=16)
plt.ylabel("$y$", fontsize=16, rotation=0)
plt.show()
左側便是這三棵樹單獨的預測,右側表示結成的預測。隨着集成的數量增加,預測的效果也逐漸變好。
2.1.2 Sklearn 實現
from sklearn.ensemble import GradientBoostingRegressor
GradientBoostingRegressor(loss=’ls’, learning_rate=0.1, n_estimators=100, subsample=1.0, criterion=’friedman_mse’, min_samples_split=2, min_samples_leaf=1, min_weight_fraction_leaf=0.0, max_depth=3, min_impurity_decrease=0.0, min_impurity_split=None, init=None, random_state=None, max_features=None, alpha=0.9, verbose=0, max_leaf_nodes=None, warm_start=False, presort=’auto’, validation_fraction=0.1, n_iter_no_change=None, tol=0.0001)
參數設置:
warm_start=True:早期停止法(2.1.3)
subsample:用於訓練每棵樹的實例的比例(隨機梯度提升,2.1.4)
與 RandomForestRegressor 類似,它具有控制決策樹生長的超參數(max_depth、min_samples_leaf),以及控制集成訓練的超參數(n_estimators、learning_rate)
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0, random_state=42)
gbrt.fit(X, y)
GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
learning_rate=1.0, loss='ls', max_depth=2,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=3,
n_iter_no_change=None, presort='auto',
random_state=42, subsample=1.0, tol=0.0001,
validation_fraction=0.1, verbose=0, warm_start=False)
gbrt_slow = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow.fit(X, y)
GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
learning_rate=0.1, loss='ls', max_depth=2,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=200,
n_iter_no_change=None, presort='auto',
random_state=42, subsample=1.0, tol=0.0001,
validation_fraction=0.1, verbose=0, warm_start=False)
plt.figure(figsize=(11,4))
plt.subplot(121)
plot_predictions([gbrt], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="Ensemble predictions")
plt.title("learning_rate={}, n_estimators={}".format(gbrt.learning_rate, gbrt.n_estimators), fontsize=14)
plt.subplot(122)
plot_predictions([gbrt_slow], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow.learning_rate, gbrt_slow.n_estimators), fontsize=14)
plt.show()
2.1.3 早期停止法
我們先來看一個例子,與上面一樣,只是調整了學習率和迭代次數:
from sklearn.ensemble import GradientBoostingRegressor
gbrt_slow1 = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=0.1, random_state=42)
gbrt_slow1.fit(X, y)
gbrt_slow2 = GradientBoostingRegressor(max_depth=2, n_estimators=200, learning_rate=0.1, random_state=42)
gbrt_slow2.fit(X, y)
plt.figure(figsize=(11,4))
plt.subplot(121)
plot_predictions([gbrt_slow1], X, y, axes=[-0.5, 0.5, -0.1, 0.8], label="Ensemble predictions")
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow1.learning_rate, gbrt_slow1.n_estimators), fontsize=14)
plt.subplot(122)
plot_predictions([gbrt_slow2], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("learning_rate={}, n_estimators={}".format(gbrt_slow2.learning_rate, gbrt_slow2.n_estimators), fontsize=14)
plt.show()
超參數 learning_rate
對每棵樹的貢獻進行縮放。如果將其設爲低值,比如 0.1 ,則需要更多的樹來擬合訓練集,但是預測的泛化效果通常更好。這是一種被稱爲收縮
的正則化技術。如上圖所示,圖6 看是了用低學習率尋來呢的兩個 GBRT 集成:左側擬合訓練集的樹數量不足,而右側擬合訓練集的樹數量過多從而導致過度擬合。
要找到樹的最佳數量,可以使用早期停止法
(在驗證集誤差達到最小時停止訓練)。簡單的實現方法就是使用 staged_predict()
方法:它在訓練的每個階段(一棵樹時,兩棵樹時,等等)都對集成的預測返回一個迭代器。
以下代碼訓練了一個擁有 120 顆樹的 GBRT 集成,然後測量每個訓練階段的驗證誤差,從而找到樹的最優數量,最後使用最優樹數訓練了一個 GBRT 集成:
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X_train, X_val, y_train, y_val = train_test_split(X, y, random_state=49)
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120, random_state=42)
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred)
for y_pred in gbrt.staged_predict(X_val)] # staged_predict()
bst_n_estimators = np.argmin(errors) + 1
gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators, random_state=42)
gbrt_best.fit(X_train, y_train)
GradientBoostingRegressor(alpha=0.9, criterion='friedman_mse', init=None,
learning_rate=0.1, loss='ls', max_depth=2,
max_features=None, max_leaf_nodes=None,
min_impurity_decrease=0.0, min_impurity_split=None,
min_samples_leaf=1, min_samples_split=2,
min_weight_fraction_leaf=0.0, n_estimators=56,
n_iter_no_change=None, presort='auto',
random_state=42, subsample=1.0, tol=0.0001,
validation_fraction=0.1, verbose=0, warm_start=False)
min_error = np.min(errors)
plt.figure(figsize=(11, 4))
plt.subplot(121)
plt.plot(errors, "b.-")
plt.plot([bst_n_estimators, bst_n_estimators], [0, min_error], "k--")
plt.plot([0, 120], [min_error, min_error], "k--")
plt.plot(bst_n_estimators, min_error, "ko")
plt.text(bst_n_estimators, min_error*1.2, "Minimum", ha="center", fontsize=14)
plt.axis([0, 120, 0, 0.01])
plt.xlabel("Number of trees")
plt.title("Validation error", fontsize=14)
plt.subplot(122)
plot_predictions([gbrt_best], X, y, axes=[-0.5, 0.5, -0.1, 0.8])
plt.title("Best model (%d trees)" % bst_n_estimators, fontsize=14)
plt.show()
事實上,要實現早期停止法,不一定需要先訓練大量的樹,然後再回頭找最優的數量。在 Sklearn 中,通過設置 warm_start=True
,當 fit() 方法被調用時,Sklearn 會保留現有的樹,從而允許增量訓練。
以下代碼會在驗證誤差連續 5 次迭代未改善時,直接停止訓練:
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True, random_state=42)
min_val_error = float("inf")
error_going_up = 0
for n_estimators in range(1, 120):
gbrt.n_estimators = n_estimators
gbrt.fit(X_train, y_train)
y_pred = gbrt.predict(X_val)
val_error = mean_squared_error(y_val, y_pred)
if val_error < min_val_error:
min_val_error = val_error
error_going_up = 0
else:
error_going_up += 1
if error_going_up == 5:
break # early stopping
print(gbrt.n_estimators)
61
print("Minimum validation MSE:", min_val_error)
Minimum validation MSE: 0.002712853325235463
2.1.4 隨機梯度提升
GradientBoostingRegressor 類還可以支持超參數 subsample
,用於制定訓練每棵樹的實例的比例,數據通過隨機抽取,擇業時用更高的偏差換取了更低的偏差。[3]
參考資料
[1] Aurelien Geron, 王靜源, 賈瑋, 邊蕤, 邱俊濤. 機器學習實戰:基於 Scikit-Learn 和 TensorFlow[M]. 北京: 機械工業出版社, 2018: 174-175.
[2] 周志華. 機器學習[M]. 北京: 清華大學出版社, 2016: 173-177.
[3] Aurelien Geron, 王靜源, 賈瑋, 邊蕤, 邱俊濤. 機器學習實戰:基於 Scikit-Learn 和 TensorFlow[M]. 北京: 機械工業出版社, 2018: 175-180.