Boosting
Boosting(原先稱為hypothesis boosting),指的是能夠將多個弱學習器結合在一起的任何集成方法。對於大部分boosting方法來說,它們常規的做法是:按順序訓練模型,每個模型都會嘗試修正它的前一個模型。Booting 方法有很多種,不過到現在為止最熱門的是AdaBoost(Adaptive Boosting的簡稱)和Gradient Boosting。我們首先看一下AdaBoost。
AdaBoost
其中一個讓一個新模型修正它前一個模型的方法是:放更多的精力在前任模型欠擬合的訓練數據實例上。結果就是接下來的模型們會放越來越多的精力在那些難處理的條目上。這個就是AdaBoost所用的技巧。
例如,在訓練一個AdaBoost 分類器時,算法首先訓練一個基分類器(例如決策樹),然后使用它在訓練集上做預測。算法然后增加那些分類錯誤的訓練數據條目的相對權重。接着它開始訓練第二個分類器,使用更新后的權重值,並在訓練結束后再次在訓練集上做預測,更新數據實例的權重,諸如此類。如下圖所示:
下圖展示的是5個連續的模型 ,使用的訓練集是moons 數據集(在這個例子中,每個模型都是一個高度正則后的SVM分類器,使用RBF核)。第一個分類器有很多實例都分類錯誤,所以這些實例的權重得到增強。第二個分類器因此在這些實例上獲得了更好的表現;依次類推。右邊的圖是同樣一個5連續的模型,除了它的learning_rate 進行了減半(也就是說,被錯誤分類的實例的權重在每輪的增強都減半)。我們可以看到,這個順序學習技術與梯度下降有些類似,不過梯度下降調整的是單個模型的參數,以最小化損失函數;而AdaBoost是在集成中增加模型,逐漸的讓集成更優秀。
對於連續學習技術來說,有一個非常重要的缺點:它無法並行(或者僅能部分並行),因為每個模型只能在前一個模型訓練完並評估后才能開始訓練。所以導致它的擴展不如bagging或pasting好。
我們進一步看一下AdaBoost算法,每個實例的權重w(i) 在初始設置為 1/m。在第一個模型訓練后,會在訓練集上計算它的帶權錯誤率,如下所示:
然后使用以下公式計算出模型權重αj :
這里η是超參數學習率(默認為1)。模型的准確度越高,則它的權重也會更高。如果它僅是一個隨機猜測的模型,則它的權重會接近於0。不過,如果它的預測錯誤率非常高的話(例如比隨機猜的正確率還要低),則它的權重會是負值。
然后,AdaBoost算法會更新實例的權重,使用以下公式,增加錯誤分類實例的權重:
然后所有的實例權重會被標准化(也就是說除以∑w(i))
最后,一個新的模型會使用權重更新后的訓練集進行訓練,並且整個過程迭代(新模型的權重被計算,實例權重更新,然后下一個模型開始訓練,依次類推)。在達到了指定的模型數量,或是一個非常完美的模型被發現后,算法終止。
在做預測時,AdaBoost會計算所有模型的預測,然后使用模型的權重αj給預測結果加權。最終輸出的預測類是獲取最多帶權票數的那一類,如下公式所示:
Sk-learn使用了AdaBoost的一個多類別分類版本,稱為SAMME(Stagewise Additive Modeling using a Multiclass Exponential loss function)。如果目標類別僅有2類,則SAMME等同於AdaBoost。如果模型可以預測類別的概率(例如它們有predict_proba() 方法),則sk-learn可以使用一個SAMME的變種,稱為SAMME.R(這里R代表Real),它基於的是預測類別的概率,而不是直接預測類別,一般這種表現會更好。
下面的代碼訓練一個AdaBoost分類器,基於的是200個決策樁(Decision Stumps,也就是只有一層的決策樹),使用sk-learn的AdaBoostClassifier 類(同理,也有AdaBoostRegressor類)。決策樁就是單決策節點帶兩個葉子節點,它是AdaBoostClassifier類默認的基模型:
from sklearn.ensemble import AdaBoostClassifier ada_clf = AdaBoostClassifier( DecisionTreeClassifier(max_depth=1), n_estimators=200, algorithm="SAMME.R", learning_rate=0.5) ada_clf.fit(X_train, y_train)
如果AdaBoost 集成在訓練集上過擬合,則我們可以嘗試減少模型的數量,或是對基模型使用更強的正則。
Gradient Boosting
另一個非常人們的boosting 算法是Gradient Boosting。與AdaBoost 類似,Gradient Boosting也是將模型按順序增加到集成中,每個模型都會修正它的前一個模型。但是,與AdaBoost在每輪調整實例的權重不一樣,Gradient Boosting方法會嘗試將新的模型與前一個模型的殘差(residual errors)進行擬合。
我們先看一個簡單的回歸例子,使用決策樹作為基模型,這個稱謂Gradient Tree Boosting,或Gradient Boosted Regression Trees(GBRT)。首先,我們用一個DecistionTreeRegressor擬合訓練數據(例如,帶噪聲的二次方訓練集):
from sklearn.tree import DecisionTreeRegressor tree_reg1 = DecisionTreeRegressor(max_depth=2) tree_reg1.fit(X, y)
然后,我們訓練第二個DecisionTreeRegressor,基於第一個模型產生的殘差進行訓練:
y2 = y - tree_reg1.predict(X) tree_reg2 = DecisionTreeRegressor(max_depth=2) tree_reg2.fit(X, y2)
接下來訓練第三個回歸器,基於第二個模型產生的殘差進行訓練:
y3 = y2 - tree_reg2.predict(X) tree_reg3 = DecisionTreeRegressor(max_depth=2) tree_reg3.fit(X, y3)
現在我們有了一個集成,包含3棵樹。它在對一個新實例做預測時,會簡單地將所有樹的輸出進行相加:
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))
下圖中的左圖展示的是這3棵樹的預測,右圖代表的是集成器的預測。在第一行,集成僅有一棵樹,所以它的預測與左邊的樹的預測完全一樣。在第二行,一顆新樹基於第一棵樹的殘差進行了訓練。在第二行右邊,我們可以看到集成器的預測等同於前兩顆樹的和。類似,第三行訓練的第三棵樹基於的是第二顆樹的殘差進行訓練。我們可以看到集成器的預測表現隨着加入的數越多而表現的越好。
另外一個跟簡單的訓練GBRT集成器的方法是使用sk-learn的GradientBoostingRegressor類。它與RandomForestRegressor類類似,有超參數用於控制如何構造決策樹(例如max_depth, min_samples_leaf),以及超參數控制集成器的訓練,例如樹的個數(n_estimators)。下面的代碼訓練了與上述模型同樣的一個集成器:
from sklearn.ensemble import GradientBoostingRegressor gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=3, learning_rate=1.0) gbrt.fit(X, y)
learning_rate 超參數控制的是每棵樹的貢獻。如果將它設置一個很小的值,例如0.1,則我們的集成器需要更多的樹來擬合訓練集,不過預測的泛化性能一般會更好。這是一個正則技巧,稱為收縮(shrinkage)。下圖展示的是兩個使用了低學習率的GBRT集成器,左邊的集成器沒有足夠的樹來擬合數據,而右邊的集成器包含的樹太多,導致了在訓練集上的過擬合。
為了找到最佳數目的樹,我們可以使用early stopping。一個簡單的實現辦法是使用staged_predict() 方法:它返回一個迭代器,包含的是集成器在每個階段訓練(例如使用一棵樹,兩棵樹,等等)后做的預測。下面的代碼訓練了一個GBRT集成器,使用120棵樹,然后在每個訓練階段衡量它的交叉錯誤(validation error),以找到最優數目的樹,並最終訓練另一個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) gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120) gbrt.fit(X_train, y_train) errors = [mean_squared_error(y_val, y_pred) for y_pred in gbrt.staged_predict(X_val)] bst_n_estimators = np.argmin(errors) + 1 gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators) gbrt_best.fit(X_train, y_train)
交叉錯誤(validation errors)如下圖左圖所示,最佳模型的預測結果如右圖所示:
上面實現early stopping 的辦法是先訓練大量數目的數模型,然后再回看去找最優。不過我們也有其他的方式實現真正意義上的提前結束(early stopping)。我們可以通過設置warm_start=True 參數,這樣sk-learn會在調用fit() 方法后保留已存在的樹,允許增量訓練。下面的代碼會在5輪迭代后交叉錯誤仍未提升時停止訓練:
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True) 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: error_going_up = 0 else: error_going_up += 1 if error_going_up == 5: break # early stopping
GradientBoostingRegresoor類也支持一個subsample的超參數,它可以指定使用訓練集的多少比例去訓練每顆樹。例如,如果設置subsample=0.25,則每棵樹會在25%的訓練實例上進行訓練,選擇的方式是隨機選擇。同樣,這里也是為了得到更低的variance而犧牲了低bias(bias會更高,更欠擬合)。它也可以極大的提升訓練的速度,稱為隨機梯度增強(Stochastic Gradient Boosting)。
其實我們也可以為Gradient Boosting 指定其他損失函數,這個由loss 超參數控制。具體可以參考sk-learn 的文檔。
這里有必要提示的是,Gradient Boosting 其中的一個優化后的實現是XGBoost,非常熱門的一個python庫。XGBoost在ML比賽中是非常常用的一個組件,並且它的使用方法也類似於sk-learn:
import xgboost xgb_reg = xgboost.XGBRegressor() xgb_reg.fit(X_train, y_train) y_pred = xgb_reg.predict(X_val) XGBoost也提供了一些非常好的功能,例如自動控制提前結束(early stopping): xgb_reg.fit(X_train, y_train, eval_set=[(X_val, y_val)], early_stopping_rounds=2) y_pred = xgb_reg.predict(X_val)
請務必要嘗試XGBoost!
Stacking
最后要提的一個集成方法是stacking(stacked generalization的簡稱),它基於的原理很簡單:與其使用一個簡單的方法(例如硬投票)來聚合集成器中所有模型的預測結果,為什么不直接訓練一個模型來執行這個聚合呢?下圖展示的就是這樣一個集成器,它在一個新的實例上執行一個回歸任務。最底層的3個模型都預測了一個不同的值(3.1,2.7和2.9),然后最后的一個模型(稱為 blender,攪拌器,或是meta learner,元學習器)接收這些預測值並作出最終的預測。
在訓練一個blender時,一個常見的辦法是使用 hold-out 集,它的工作原理為:首先,訓練集被分類成2個子集。第一個子集用於在第一層訓練模型,如下圖所示:
然后,第一層的模型會在第二個子集(held-out 集)上做預測(如下圖所示)。這樣可以確保預測值是干凈、不受影響的,因為模型在訓練過程中不會看到這些實例。對於每個在hold-out集中的實例,它都有3個預測值。我們可以使用這些預測值創建一個新的訓練集作為輸入特征(會讓這個新訓練集變為3維),並保留目標值(target或label)。blender在這個新訓練集上進行訓練,所以它會根據第一層的預測,學習如何預測目標值。
實際上也可以通過這種方式訓練多個不同的blenders(例如,一個使用線性回歸,另一個使用隨機森林回歸),來得到一整個blenders層。辦法是將訓練集分割為3個子集(第一個用於訓練第一層,第二個用於創建新訓練集給第二層模型訓練(使用第一層的預測值作為第二層的訓練數據),然后第3個子集用於給第3層模型訓練創建訓練數據(使用第二層的預測數據作為訓練數據)。在這個過程完成后,在做預測時,新實例會按順序經過每一層后,輸出預測值,如下圖所示:
可惜的是,sk-learn並未直接支持stacking,不過實現它也不是特別困難。或者我們也可以使用開源的實現例如DESlib。