如果你想知道一個複雜的問題的答案,參考幾千人的意見的結果可能比專家的結果還要好,這是羣體的智慧。同樣,如果你集成許多個預測器(分類或者回歸)的結果,結果可能比非常優秀單個的預測器還要好。這就是集成學習(Ensemble Learning)

我們會討論一些最著名的集成學習方法

投票分類

假設你有幾個準確率80%的分類器

做預測的一個方法是讓這些分類器投票,選取最多的結果。這稱為硬投票分類。喫驚的是,投票的結果要比其中最好的還要好一些。

事實上,就算每個分類器都很弱(只比瞎猜好一點點),只要分類器足夠多並且分類器之間有差異性,都可以得到不錯的結果

假設你有一個正面概率為51%的硬幣,做扔硬幣的實驗,你會發現在實驗 1000 次後,正面概率為 51% 的人比例為 75%。,試驗了 10000 次,比例會達到 97%)。這是因為大數定律:在試驗不變的條件下,重複試驗多次,隨機事件的頻率近似於它的概率。偶然中包含著某種必然。

同理,如果每個模型準確率在51%,1000個模型集成起來就有75%的準確率。但事實上,這要求所有模型之間是相互完全獨立的。錯誤完全不相關,如果他們犯的錯誤一樣,集成起來就毫無幫助,事實上,儘管不可能做到錯誤完全不相關,但是,我們希望不同的模型預測的結果儘可能存在著較大的差異性,這樣模型集成的效果也會變好。

from sklearn.ensemble import RandomForestClassifier
from sklearn.ensemble import VotingClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC
log_clf = LogisticRegression()
rnd_clf = RandomForestClassifier()
svm_clf = SVC()
voting_clf = VotingClassifier(estimators=[(lr, log_clf), (rf, rnd_clf),
(svc, svm_clf)],voting=hard)
voting_clf.fit(X_train, y_train)

>>> from sklearn.metrics import accuracy_score
>>> for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
>>> clf.fit(X_train, y_train)
>>> y_pred = clf.predict(X_test)
>>> print(clf.__class__.__name__, accuracy_score(y_test, y_pred))
LogisticRegression 0.864
RandomForestClassifier 0.872
SVC 0.888
VotingClassifier 0.896
#結果證實了我們的觀點,集成起來好於任何一個模型

和硬方法對應的還有軟方法(voting=soft),軟方法是根據predict_proba()方法的概率加權投票,概率大的權值會高一些,這樣一般比硬方法要好一些,但是,不是所有的類都有predict_proba()方法,比如SVC,標準的SVC是不能提供概率的,可以把probability hyperparameter設置為True來提供,這樣的原理是用交叉驗證的方法獲得probability,副作用是也會減慢速度。這樣上面的例子可以達到91%的準確值。

Bagging和Pasting

之前介紹的是用不同的模型在相同的數據集上的結果做投票,那麼,另一種方法就是,每次在不同的數據子集上運行相同的模型,最後做投票。根據採樣的不同,有放回採樣被稱為裝袋(Bagging,是bootstrap aggregating的縮寫)。無放回採樣稱為粘貼(pasting)。

如圖,Bagging有個顯而易見的優點,不同的袋可以分別在不同的CPU或者伺服器上進行,並行化是一個bagging這麼流行的重要原因。

假設每個模型之間相互獨立,每個模型的方差為 sigma^2 ,總體的Var(frac1nsum_{i=1}^n x_i)=frac{sigma^2} n 。因此,Bagging的集成模型偏差和單獨的模型差不多,但是方差可以降低很多

Bootstrap在每個子集裏引入了更多的多樣性,儘管偏差會比Pasting高一點,但是更大程度減少了方差,總體來看,一般時候Bagging會好一些,但是如果時間充裕,你可以都試一下在驗證集上驗證它們。

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,
max_samples=100, bootstrap=True, n_jobs=-1)
#n_jobs是並行的數量,n_jobs=-1,會用全部空閑的核
#bootstrap為False則是Pasting,True的話是Bagging

bag_clf.fit(X_train, y_train)
y_pred = bag_clf.predict(X_test)

一般的話,加權會採用硬分類的方法,如果基分類器可以預測類別概率(例如它擁有predict_proba()方法),那麼BaggingClassifier會自動的運行軟投票

從圖中可以看出,bagging減少了方差,右面看起來泛化會好一點

out-of-bag估計

每個子集抽取1/n,抽取n次,那麼lim_{n 
ightarrow infty}{(1-frac1n)^n}=frac1e ,約有37%的樣本沒有被任何的模型當作過訓練樣本,這一部分稱為out-of-bag,因為模型沒有見過這部分樣本,所以可以拿來當驗證集合,而不需要再劃分驗證集或者交叉驗證了。

BaggingClassifier來自動評估時設置oob_score=True來自動評估。接下來的代碼展示了這個操作。評估結果通過變數oob_score_來顯示:

當基決策器有predict_proba()時,也可以通過oob_decision_function_變數來展示類別概率

bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500,bootstrap=True, n_jobs=-1, oob_score=True)
bag_clf.fit(X_train, y_train)
>>> bag_clf.oob_score_
0.93066666666666664

>>> from sklearn.metrics import accuracy_score
>>> y_pred = bag_clf.predict(X_test)
>>> accuracy_score(y_test, y_pred)
0.93600000000000005 #可以看出OOB結果和測試集非常接近的

>>> bag_clf.oob_decision_function_
array([[ 0., 1.], [ 0.60588235, 0.39411765],[ 1., 0. ],
... [ 1. , 0. ],[ 0., 1.],[ 0.48958333, 0.51041667]])

隨機子空間

除了支持對樣本採樣,BaggingClassifier也支持採樣特徵。它被兩個超參數max_featuresbootstrap_features控制。可以選一部分特徵而不是全部特徵,在高維空間尤其有效。原理上,和採樣樣本類似,增加了模型之間的多樣性,都是以高偏差換小方差。

隨機森林

隨機森林是決策樹的一種集成,通常是通過 bagging 方法(有時是 pasting 方法)進行訓練

from sklearn.ensemble import RandomForestClassifier
rnd_clf = RandomForestClassifier(n_estimators=500, max_leaf_nodes=16, n_jobs=-1)
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)

RandomForestClassifier使用DecisionTreeClassifier的超參數(決定每個樹怎麼生長),和BaggingClassifier的超參數(控制集成本身)

和Bagging相比,因為每一次樹分裂時候選取特徵是在一個特徵的隨機子集而不是全部的特徵上,所有進一步加大了模型之間的差異性,得到更好的模型

極端決策樹

相比於尋找一個最好的閾值,另一個方法是找一個隨機的閾值,這能夠進一步的增大模型之間的差異性,並且尋找閾值是最耗時間的一步,因此可以大幅提升模型的效率。這種極端隨機的樹被簡稱為Extremely Randomized Trees(極端隨機樹),或者更簡單的稱為Extra-Tree

你可以使用 sklearn 的ExtraTreesClassifier來創建一個Extra-Tree分類器。他的 API 跟RandomForestClassifier是相同的,相似的,ExtraTreesRegressorRandomForestRegressor也是相同的 API。

通常很難判斷普通隨機森林和這種極端隨機樹哪個更好,可以在交叉驗證集上比較他們。

特徵重要程度

對於一棵決策樹,重要的特徵會出現在更靠近根部的位置,而不重要的特徵會經常出現在靠近葉子的位置。因此我們可以通過計算一個特徵在森林的全部樹中出現的平均深度來預測特徵的重要性。sklearn 在訓練後會自動計算每個特徵的重要度。你可以feature_importances_變數來查看結果

>>> from sklearn.datasets import load_iris
>>> iris = load_iris()
>>> rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1)
>>> rnd_clf.fit(iris["data"], iris["target"])
>>> for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_):
>>> print(name, score)
sepal length (cm) 0.112492250999
sepal width (cm) 0.0231192882825
petal length (cm) 0.441030464364
petal width (cm) 0.423357996355

可以知道,花瓣的長度和寬度是更重要的特徵

如果在MINIST數據集上訓練隨機森林,也可以得到每個像素的重要程度,如圖:

隨機森林可以非常方便快速得了解哪些特徵實際上是重要的,特別是你需要進行特徵選擇的時候。

Boosting

提升(Boosting)指的是可以將幾個弱學習者組合成強學習者的集成方法。大多數情況下,按順序去訓練分類器,每一個都要嘗試修正前面的分類。最著名提升包括Adaboost(適應性提升,是Adaptive Boosting的簡稱) 和Gradient Boosting(梯度提升)。

Adaboost

Adaboost每次分類都會更關注那些之前分類存在著問題,這樣會更關注那些難以處理正確的樣本,如圖,每輪訓練之後,對於那些和預測值差距比較大的樣本在下一輪會被格外關註:

從下圖的決策邊界的變化可以看出,第一個分類器誤分類了很多實例,所以它們的權重被提升了。第二個分類器因此對這些誤分類的實例分類效果更好,以此類推。另外,超參數learning_rate可以控制每次改變的大小,和梯度下降非常類似

當所有的分類器訓練完成之後,會按他們的準確率加權

讓我們詳細瞭解下Adaboost的過程

最開始,每個樣本wi的權重都是 frac1m ,然後會算出一個權重加權的誤差率

rj是第j輪模型的誤差率,那麼可以由此推算一個第j個分類器的權重,如果rj接近瞎猜(0.5),那麼就會權值接近於0,如果接近於猜的全對,這個分類器在所有分類器裏的權重將會非常高。η是超參數學習率(默認為 1)。分類器準確率越高,它的權重就越高。

然後的任務是更新每個樣本的權重,對於i=1, 2, ..., m

然後歸一化,比如除以wi的和,來保證總和為1。

整個過程被重複(新的分類器權重被計算,實例的權重被更新,隨後另一個分類器被訓練,以此類推)。當規定的分類器數量達到或者最好的分類器被找到後演算法就會停止。

sklearn 通常使用 Adaboost 的多分類版本SAMME(這就代表了分段加建模使用多類指數損失函數)。如果只有兩類別,那麼SAMME是與 Adaboost 相同的。如果分類器可以預測類別概率(例如如果它們有predict_proba()),如果 sklearn 可以使用SAMME叫做SAMME.R的變數(R 代表「REAL」),這種依賴於類別概率的通常比依賴於分類器的更好。

>>>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 集成過擬合了訓練集,你可以嘗試減少基分類器的數量或者對基分類器使用更強的正則化。

梯度提升

另一種著名的Boosting方法是梯度提升,和Adaboost的區別是,它不再每一次迭代都更改實例的權重,這個方法是去使用新的分類器去擬合前面分類器預測的殘差

這相當於,有好幾個決策樹,每次學習上一個分類器和正確值的差

>>>from sklearn.tree import DecisionTreeRegressor
>>>tree_reg1 = DecisionTreeRegressor(max_depth=2)
>>>tree_reg1.fit(X, y)

>>>y2 = y - tree_reg1.predict(X)
>>>tree_reg2 = DecisionTreeRegressor(max_depth=2)
>>>tree_reg2.fit(X, y2)

>>>y3 = y2 - tree_reg1.predict(X)
>>>tree_reg3 = DecisionTreeRegressor(max_depth=2)
>>>tree_reg3.fit(X, y3)

>>>y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3))

我們可以使用 sklean 中的GradientBoostingRegressor來訓練 GBRT 集成。與RandomForestClassifier相似,它也有超參數去控制決策樹的生長(例如max_depthmin_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:每次走一小步逐漸逼近結果的效果,要比每次邁一大步很快逼近結果的方式更容易避免過擬合

為了確定最優的樹的個數,可以採取Early-stopping的方法,最簡單使用這個技術的方法就是使用staged_predict():它在訓練的每個階段(用一棵樹,兩棵樹等)返回一個迭代器。

>>>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)
>>>gbrt_best = GradientBoostingRegressor(max_depth=2,n_estimators=bst_n_estimators)
>>>gbrt_best.fit(X_train, y_train)

結果如下:

GradientBoostingRegressor也支持指定用於訓練每棵樹的訓練實例比例的超參數subsample。例如如果subsample=0.25,那麼每個樹都會在 25% 隨機選擇的訓練實例上訓練。你現在也能猜出來,這也是個高偏差換低方差的作用。它同樣也加速了訓練。這個技術叫做隨機梯度提升

Stacking

最後一種我們討論的集成方法是Stacking,這個演算法基於一個簡單的想法:不使用函數(如硬投票)來聚合集合中所有分類器的預測,我們為什麼不訓練一個模型來執行這個聚合?如圖:

其中上層模型又叫blender或者meta learner

為了訓練這些模型,可以採用held-out集合的方法,即:把訓練集劃分為幾個子集,每一層模型用一個子集來訓練

然而不幸的是,sklearn 並不直接支持 stacking ,但是你自己組建是很容易的。或者你也可以使用開源的項目例如brew(網址為github.com/viisar/brew


推薦閱讀:
相關文章