「如果你沒有什麼好的思路的話,那麼就模型融合吧!」

『我愛機器學習』集成學習(一)模型融合與Bagging - 細語呢喃?

www.hrwhisper.me圖標蹭蹭不進去:Kaggle機器學習之模型融合(stacking)心得?

zhuanlan.zhihu.com
圖標

Stacking利器(mlxtend庫):

https://blog.csdn.net/weixin_38569817/article/details/80534785?

blog.csdn.net

在KDD CUP、Kaggle、天池等數據挖掘比賽中,常常用到集成學習。使用了集成學習後,模型的效果往往有很大的進步。

本文將介紹常見的集成學習方法,包括但不限於:

  • 集成學習為什麼有效
  • Voting
  • Linear Blending
  • Stacking
  • Bagging
  • 隨機森林

集成學習

如果硬要把集成學習進一步分類,可以分為兩類,一種是把強分類器進行強強聯合,使得融合後的模型效果更強,稱為模型融合。另一種是將弱分類器通過學習演算法集成起來變為很強的分類器,稱為機器學習元演算法

這裡我們把用來進行融合的學習器稱為個體學習器

模型融合的代表有:投票法(Voting)、線性混合(Linear Blending)、Stacking。

而機器學習元演算法又可以根據個體學習器之間是否存在依賴關係分為兩類,稱為Bagging和Boosting:

  • Bagging: 個體學習器不存在依賴關係,可同時對樣本隨機採樣並行化生成個體學習器。代表作為隨機森林(Random Forest)
  • Boosting: 個體學習器存在依賴關係,基於前面模型的訓練結果誤差生成新的模型,必須串列化生成。代表的演算法有:Adaboost、GBDT、XGBoost

zach96:Boosting方法-GBDT,XGBoost,LightGBM?

zhuanlan.zhihu.com圖標

模型融合

上面提到,模型融合是把強分類器進行強強聯合,變得更強。

在進行模型融合的時候,也不是說隨意的融合就能達到好的效果。進行融合時,所需的集成個體(就是用來集成的模型)應該好而不同。好指的是個體學習器的性能要好,不同指的是個體模型的類別不同。

這裡舉個西瓜書的例子,在介紹例子之前,首先提前介紹簡單投票法,以分類問題為例,就是每個分類器對樣例進行投票,哪個類別得到的票數最多的就是融合後模型的結果。

在上面的例子中,採用的就是簡單的投票法。中間的圖b各個模型輸出都一樣,因此沒有什麼效果。第三個圖c每個分類器的精度只有33%,融合後反而更糟。也就是說,想要模型融合有效果,個體學習器要有一定的準確率,並且要有多樣性,學習器之間具有差異,即」好而不同「。

如何做到好而不同呢?可以由下面幾個方面:

  • 針對輸入數據:使用採樣的方法得到不同的樣本(比如bagging方法採用自助法進行抽樣)
  • 針對特徵:對特徵進行抽樣
  • 針對演算法本身:
    • 個體學習器 h_t 來自不同的模型集合
    • 個體學習器 h_t 來自於同一個模型集合的不同超參數,例如學習率η不同
    • 演算法本身具有隨機性,例如用不同的隨機種子來得到不同的模型
  • 針對輸出:對輸出表示進行操縱以增強多樣性
    • 如將多分類轉化為多個二分類任務來訓練單模型
    • 將分類輸出轉化為回歸輸出等

投票和平均 Voting and Average

分類

對於分類任務來說,可以使用投票的方法:

  • 簡單投票法: H({f x}) =c_{underset{x}{mathrm{argmin}}}sum_{i=1}^Th_i^j({f x})
    • 即各個分類器輸出其預測的類別,取最高票對應的類別作為結果。若有多個類別都是最高票,那麼隨機選取一個。
  • 加權投票法: H({f x}) =c_{underset{x}{mathrm{argmin}}}sum_{i=1}^Talpha_icdot h_i^j({f x})
    • 和上面的簡單投票法類似,不過多了權重αi,這樣可以區分分類器的重要程度,通常 alpha_i ge 0;hspace{1ex} sum_{i=1}^Talpha_i = 1

此外,個體學習器可能產生不同的 h_i^j({f x}) 的值,比如類標記和類概率。

  • 類標記 h_i^j({f x}) in {0, 1} ,若hi將樣本x預測為類別 c_j 取值為1,否則為0。使用類標記的投票亦稱「硬投票」。(其實就是多分類的輸出),使用類標記的稱為硬投票
  • 類概率 h_i^j({f x}) in [0, 1] ,即輸出類別為 c_j 的概率。使用類概率的投票稱為軟投票對應sklearn中的VotingClassifier中voting參數設為soft。

PS:使用類概率進行結合往往比直接基於類標記的效果好,即使分類器估計出的概率值一般都不太準確。

回歸

對於回歸任務來說,採用的為平均法(Average)

  • 簡單平均: H({f x}) =frac{1}{T} sum_{i=1}^Th_i({f x})
  • 加權平均: H({f x}) =frac{1}{T} sum_{i=1}^Talpha_i cdot h_i({f x}) ;   alpha_i ge 0;   sum_{i=1}^Talpha_i = 1

首先我們回顧兩個概念:

https://blog.csdn.net/liucheng17/article/details/8507281?

blog.csdn.net

holdout交叉驗證是兩個常用的評估分類器預測準確率的技術,它們均是在給定數據集中隨機取樣劃分數據。 holdout:將所給定的數據集隨機劃分成兩個獨立部分:一個作為訓練數據集,而另一個作為測試數據集,通常訓練數據集包含初始數據集中的三分之二的數據,而其餘的三分之一則作為測試數據集的內容。利用訓練集數據學習獲得一個分類器,然後使用測試數據集對該分類器預測準確率進行評估,由於僅使用初始數據集中的一部分進行學習,因此對所得分類器預測準確性的估計應該是悲觀的估計。 隨機取樣是holdout方法的一種變化,在隨機取樣方法中,重複利用holdout方法進行預測準確率估計k次,最後對這k次所獲得的預測準確率求平均,以便獲得最終的預測準確率。 k-交叉驗證:將初始數據集隨機分為k個互不相交的子集,S1,S2,...,Sk,每個子集大小基本相同。學習和測試分別進行k次,在第i次循環中,子集Si作為測試集,其他子集則合併到一起構成一個大訓練數據集並通過學習獲得相應的分類器,也就是第一次循環,使用S2....Sk作為訓練數據集,S1作為測試數據集;而在第二次循環時,使用S1,S3,...,Sk作為訓練數據集,S2作為測試數據集;如此下去等等。而對整個初始數據所得分類器的準確率估計則可用k次循環中所獲得的正確分類數目之和除以初始數據集的大小來獲得。在分層交叉驗證中,將所劃分的子集層次化以確保每個子集中的各類別分布與初始數據集中的類別分布基本相同。

Stacking

Stacking相比Linear Blending來說,更加強大,然而也更容易過擬合。

Stacking做法和Linear Blending類似,首先從數據集中訓練出初級學習器,然後」生成「一個新的數據集用於訓練次級學習器。為了防止過擬合,採用K折交叉驗證法求解。

一個直觀的圖如下:

假設採用5折交叉驗證,每個模型都要做滿5次訓練和預測,對於每一次:

  • 從80%的數據訓練得到一個模型ht,然後預測訓練集剩下的那20%,同時也要預測測試集。
  • 每次有20%的訓練數據被預測,5次後正好每個訓練樣本都被預測過了。
  • 每次都要預測測試集,因此最後測試集被預測5次,最終結果取5次的平均。

stacking例子:

對於每一輪的 5-fold,Model 1都要做滿5次的訓練和預測。

Titanic 栗子:Train Data有890行。(請對應圖中的上層部分)每1次的fold,都會生成 713行 小train, 178行 小test。我們用Model 1來訓練 713行的小train,然後預測 178行 小test。預測的結果是長度為 178 的預測值。這樣的動作走5次! 長度為178 的預測值 X 5 = 890 預測值,剛好和Train data長度吻合。這個890預測值是Model 1產生的,我們先存著,因為,一會讓它將是第二層模型的訓練來源。重點:這一步產生的預測值我們可以轉成 890 X 1 (890 行,1列),記作 P1 (大寫P)接著說 Test Data 有 418 行。(請對應圖中的下層部分,對對對,綠綠的那些框框)每1次的fold,713行 小train訓練出來的Model 1要去預測我們全部的Test Data(全部!因為Test Data沒有加入5-fold,所以每次都是全部!)。此時,Model 1的預測結果是長度為418的預測值。

這樣的動作走5次!我們可以得到一個 5 X 418 的預測值矩陣。然後我們根據行來就平均值,最後得到一個 1 X 418 的平均預測值。

重點:這一步產生的預測值我們可以轉成 418 X 1 (418行,1列),記作 p1 (小寫p)走到這裡,你的第一層的Model 1完成了它的使命。第一層還會有其他Model的,比如Model 2,同樣的走一遍, 我們有可以得到 890 X 1 (P2) 和 418 X 1 (p2) 列預測值。這樣吧,假設你第一層有3個模型,這樣你就會得到:來自5-fold的預測值矩陣 890 X 3,(P1,P2, P3) 和 來自Test Data預測值矩陣 418 X 3, (p1, p2, p3)。-----------------------------------------到第二層了..................來自5-fold的預測值矩陣 890 X 3 作為你的Train Data,訓練第二層的模型來自Test Data預測值矩陣 418 X 3 就是你的Test Data,用訓練好的模型來預測他們吧。

回歸問題,代碼如下(get_oof就是上圖的過程):

_N_FOLDS = 5 # 採用5折交叉驗證
kf = KFold(n_splits=_N_FOLDS, random_state=42) # sklearn的交叉驗證模塊,用於劃分數據

def get_oof(clf, X_train, y_train, X_test):
# X_train: 1000 * 10
# y_train: 1 * 1000
# X_test : 500 * 10
oof_train = np.zeros((X_train.shape[0], 1)) # 1000 * 1 Stacking後訓練數據的輸出
oof_test_skf = np.empty((_N_FOLDS, X_test.shape[0], 1)) # 5 * 500 * 1,oof_test_skf[i]代表第i折交叉驗證產生的模型對測試集預測結果

for i, (train_index, test_index) in enumerate(kf.split(X_train)): # 交叉驗證劃分此時的訓練集和驗證集
kf_X_train = X_train[train_index] # 800 * 10 訓練集
kf_y_train = y_train[train_index] # 1 * 800 訓練集對應的輸出
kf_X_val = X_train[test_index] # 200 * 10 驗證集

clf.fit(kf_X_train, kf_y_train) # 當前模型進行訓練

oof_train[test_index] = clf.predict(kf_X_val).reshape(-1, 1) # 對當前驗證集進行預測, 200 * 1
oof_test_skf[i, :] = clf.predict(X_test).reshape(-1, 1) # 對測試集預測 oof_test_skf[i, :] : 500 * 1

oof_test = oof_test_skf.mean(axis=0) # 對每一則交叉驗證的結果取平均
return oof_train, oof_test # 返回當前分類器對訓練集和測試集的預測結果

# 將數據換成你的數據
X_train = np.random.random((1000, 10)) # 1000 * 10
y_train = np.random.random_integers(0, 1, (1000,)) # 1000
X_test = np.random.random((500, 10)) # 500 * 10

# 將你的每個分類器都調用get_oof函數,並把它們的結果合併,就得到了新的訓練和測試數據new_train,new_test
new_train, new_test = [], []
for clf in [LinearRegression(), RandomForestRegressor()]:
oof_train, oof_test = get_oof(clf, X_train, y_train, X_test)
new_train.append(oof_train)
new_test.append(oof_test)

new_train = np.concatenate(new_train, axis=1)
new_test = np.concatenate(new_test, axis=1)

# 用新的訓練數據new_train作為新的模型的輸入,stacking第二層
clf = RandomForestRegressor()
clf.fit(new_train, y_train)
clf.predict(new_test)

如果是分類問題,我們對測試集的結果就不能像回歸問題一樣直接取平均,而是分類器輸出所有類別的概率,最後取平均。每個分類器都貢獻了_N_CLASS(類別數)的維度。

修改get_oof函數如下即可:

_N_CLASS = 2
def get_oof(clf, X_train, y_train, X_test):
# X_train: 1000 * 10
# y_train: 1 * 1000
# X_test : 500 * 10
oof_train = np.zeros((X_train.shape[0], _N_CLASS)) # 1000 * _N_CLASS
oof_test = np.empty((X_test.shape[0], _N_CLASS)) # 500 * _N_CLASS

for i, (train_index, test_index) in enumerate(kf.split(X_train)):
kf_X_train = X_train[train_index] # 800 * 10 交叉驗證劃分此時的訓練集和驗證集
kf_y_train = y_train[train_index] # 1 * 800
kf_X_test = X_train[test_index] # 200 * 10 驗證集

clf.fit(kf_X_train, kf_y_train) # 當前模型進行訓練

oof_train[test_index] = clf.predict_proba(kf_X_test) # 當前驗證集進行概率預測, 200 * _N_CLASS
oof_test += clf.predict_proba(X_test) # 對測試集概率預測 oof_test_skf[i, :] , 500 * _N_CLASS

oof_test /= _N_FOLDS # 對每一則交叉驗證的結果取平均
return oof_train, oof_test # 返回當前分類器對訓練集和測試集的預測結果

上面的代碼只做了兩層,你想的話還可以在加幾層,因此這個方法叫做stacking,堆疊。。

線性混合 Linear Blending

前面提到過加權平均法,每個個體學習器的權重不再相等,看起來就像是對每個個體學習器做一個線性組合,這也是線性混合法名字的由來。那麼最優的權重是什麼呢?一個直接的想法就是最好的αi使得error最小,即對應了優化問題:

min_{alpha_tge0} frac{1}{M}sum_{i=1}^Mleft(y_i – sum_{t=1}^T alpha_th_t({f x}_i)
ight)^2

這裡有T個個體學習器,每個學習器用 h_t 表示,而αt就是對應的權重。

這裡我們首先用訓練數據訓練出所有的h,然後再做線性回歸求出 alpha_t 。注意到這裡要求 alpha_t ge 0 ,來個拉格朗日函數?其實不用,通常我們可以忽略這個條件。以二分類為例如果αi小於0,相當於把模型反過來用。(假如給你個錯誤率99%的模型,你反過來用正確率不就99%了么!)

如何得到 h_t 呢?這裡我們將個體學習器稱為初級學習器,用於結合的學習器稱為次級學習器首先從數據集中訓練出初級學習器,然後」生成「一個新的數據集用於訓練次級學習器。注意為了防止過擬合,我們需要在訓練集上做訓練得到初級學習器ht,而在驗證集上比較不同α的好壞。最終模型則在所有的數據上進行訓練(數據量多可能使得模型效果更好)

步驟如下:

  1. 訓練集Dtrain中訓練得到 h_1^-,h_2^-,cdots,h_t^- ,並對驗證集 D_{val} 中的數據 ({f x_i},y_i) 做轉換為新的數據集 (Phi^-({f x_i}),y_i) ,其中 Phi^-({f x_i}) = (h_1^-({f x_i}),h_2^-({f x_i}),cdots,h_t^-({f x_i}))
  2. 用線性回歸求解 alpha = Linleft({(z_i, y_i)}
ight)
  3. 最後,用所有的數據D求解得到 h_1,h_2,cdots,h_t ,組成特徵變換向量 Phi({f x}) = (h_1({f x}),h_2({f x}),cdots,h_t({f x}))
  4. 對於新數據x, f({f x}) = frac{1}{T}sum_{t=1}^Talpha_th_t({f x})

Blending與Stacking大致相同,只是Blending的主要區別在於訓練集不是通過K-Fold的CV策略來獲得預測值從而生成第二階段模型的特徵,而是建立一個Holdout集,例如10%的訓練數據,第二階段的stacker模型就基於第一階段模型對這10%訓練數據的預測值進行擬合。說白了,就是把Stacking流程中的K-Fold CV 改成 HoldOut CV。

Blending的優點在於:

  1. 比stacking簡單(因為不用進行k次的交叉驗證來獲得stacker feature)
  2. 避開了一個信息泄露問題:generlizers和stacker使用了不一樣的數據集
  3. 在團隊建模過程中,不需要給隊友分享自己的隨機種子

而缺點在於:

  1. 使用了很少的數據
  2. blender可能會過擬合(其實大概率是第一點導致的)
  3. stacking使用多次的CV會比較穩健

mlxtend庫

一.StackingClassifier

https://blog.csdn.net/weixin_38569817/article/details/80534785#?

blog.csdn.net

https://blog.csdn.net/shincling/article/details/50637528?

blog.csdn.net

下面我們介紹一款功能強大的stacking利器,mlxtend庫,它可以很快地完成對sklearn模型地stacking。

主要有以下幾種使用方法吧:

I. 最基本的使用方法,即使用前面分類器產生的特徵輸出作為最後總的meta-classifier的輸入數據

from sklearn import datasets

iris = datasets.load_iris()
X, y = iris.data[:, 1:3], iris.target

from sklearn import model_selection
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from mlxtend.classifier import StackingClassifier
import numpy as np

clf1 = KNeighborsClassifier(n_neighbors=1)
clf2 = RandomForestClassifier(random_state=1)
clf3 = GaussianNB()
lr = LogisticRegression()
sclf = StackingClassifier(classifiers=[clf1, clf2, clf3],
meta_classifier=lr)

print(3-fold cross validation:
)

for clf, label in zip([clf1, clf2, clf3, sclf],
[KNN,
Random Forest,
Naive Bayes,
StackingClassifier]):

scores = model_selection.cross_val_score(clf, X, y,
cv=3, scoring=accuracy)
print("Accuracy: %0.2f (+/- %0.2f) [%s]"
% (scores.mean(), scores.std(), label))

II. 另一種使用第一層基本分類器產生的類別概率值作為meta-classfier的輸入,這種情況下需要將StackingClassifier的參數設置為 use_probas=True。如果將參數設置為 average_probas=True,那麼這些基分類器對每一個類別產生的概率值會被平均,否則會拼接。

例如有兩個基分類器產生的概率輸出為:
classifier 1: [0.2, 0.5, 0.3]
classifier 2: [0.3, 0.4, 0.4]
1) average = True :
產生的meta-feature 為:[0.25, 0.45, 0.35]
2) average = False:
產生的meta-feature為[0.2, 0.5, 0.3, 0.3, 0.4, 0.4]

from sklearn import datasets

iris = datasets.load_iris()
X, y = iris.data[:, 1:3], iris.target

from sklearn import model_selection
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.ensemble import RandomForestClassifier
from mlxtend.classifier import StackingClassifier
import numpy as np

clf1 = KNeighborsClassifier(n_neighbors=1)
clf2 = RandomForestClassifier(random_state=1)
clf3 = GaussianNB()
lr = LogisticRegression()
sclf = StackingClassifier(classifiers=[clf1, clf2, clf3],
use_probas=True,
average_probas=False,
meta_classifier=lr)

print(3-fold cross validation:
)

for clf, label in zip([clf1, clf2, clf3, sclf],
[KNN,
Random Forest,
Naive Bayes,
StackingClassifier]):

scores = model_selection.cross_val_score(clf, X, y,
cv=3, scoring=accuracy)
print("Accuracy: %0.2f (+/- %0.2f) [%s]"
% (scores.mean(), scores.std(), label))

III. 另外一種方法是對訓練基中的特徵維度進行操作的,這次不是給每一個基分類器全部的特徵,而是給不同的基分類器分不同的特徵,即比如基分類器1訓練前半部分特徵,基分類器2訓練後半部分特徵(可以通過sklearn 的pipelines 實現)。最終通過StackingClassifier組合起來。

from sklearn.datasets import load_iris
from mlxtend.classifier import StackingClassifier
from mlxtend.feature_selection import ColumnSelector
from sklearn.pipeline import make_pipeline
from sklearn.linear_model import LogisticRegression

iris = load_iris()
X = iris.data
y = iris.target

pipe1 = make_pipeline(ColumnSelector(cols=(0, 2)),
LogisticRegression())
pipe2 = make_pipeline(ColumnSelector(cols=(1, 2, 3)),
LogisticRegression())

sclf = StackingClassifier(classifiers=[pipe1, pipe2],
meta_classifier=LogisticRegression())

sclf.fit(X, y)

StackingClassifier 使用API及參數解析

StackingClassifier(classifiers, meta_classifier, use_probas=False, average_probas=False, verbose=0, use_features_in_secondary=False)

參數:

classifiers : 基分類器,數組形式,[cl1, cl2, cl3]. 每個基分類器的屬性被存儲在類屬性 self.clfs_.
meta_classifier : 目標分類器,即將前面分類器合起來的分類器
use_probas : bool (default: False) ,如果設置為True 那麼目標分類器的輸入就是前面分類輸出的類別概率值而不是類別標籤
average_probas : bool (default: False),用來設置上一個參數當使用概率值輸出的時候是否使用平均值。
verbose : int, optional (default=0)。用來控制使用過程中的日誌輸出,當 verbose = 0時,什麼也不輸出, verbose = 1,輸出回歸器的序號和名字。verbose = 2,輸出詳細的參數信息。verbose > 2, 自動將verbose設置為小於2的verbose -2.
use_features_in_secondary : bool (default: False). 如果設置為True,那麼最終的目標分類器就被基分類器產生的數據和最初的數據集同時訓練。如果設置為False,最終的分類器只會使用基分類器產生的數據訓練。

屬性:
clfs_ : 每個基分類器的屬性,list, shape [n_classifiers]
meta_clf_ : 最終目標分類器的屬性
方法:

fit(X, y)
fit_transform(X, y=None, fit_params)
get_params(deep=True),如果是使用sklearn的GridSearch方法,那麼返回分類器的各項參數。
predict(X)
predict_proba(X)
score(X, y, sample_weight=None) 對於給定數據集和給定label,返回評價accuracy
set_params(params),設置分類器的參數,params的設置方法和sklearn的格式一樣

二.StackingRegressor

見:

【譯】MLXTEND之StackingRegressor?

www.jianshu.com圖標
推薦閱讀:

相关文章