1. 程式人生 > >前向分步演算法與提升樹模型

前向分步演算法與提升樹模型

本篇部落格主要來說明前向分步演算法以及通過前向分步演算法構造的提升樹模型。

首先,我們假設某一模型公式具有如下形式:

f(x)=\sum_{m=1}^{M}\beta_mb(x;\gamma_m)

其中我們稱b(x;\gamma_m)為基函式,\beta_m為此基函式的係數,而\gamma_m為基函式的相關引數,我們稱這樣的模型f(x)為加法模型;我們通過使用這樣一個模型來進行迴歸於分類任務。

然而,這樣一個模型改如何來構造呢?我們採用一種叫做前向分步演算法的方法來構造這麼一個模型。

首先我們給定損失函式如下所示:

L(y, f(x))

那我們的任務就是優化的損失函式如下所示:

\underset{\beta_m,\gamma_m}{min}\sum_{i=1}^{N}L(y_i, \sum_{m=1}^{M}\beta_mb(x_i;\gamma_m))

然而這是一個複雜的優化問題,對其直接求解往往過於複雜,因此這裡採用前向分步演算法從前向後逐個求出基函式b(x;\gamma_m)及其係數\beta_m,也就是說我們每次只需優化如下損失函式即可求出一組\beta

b(x;\gamma)

\underset{\beta,\gamma}{min}\sum_{i=1}^{N}L(y_i, \beta b(x_i;\gamma))

李航的統計學習方法書中寫到這裡用y_i,但是個人認為這裡的y_i並非是樣本真實標記,其只代表一個數值,表明了每次只需優化有這這種形式的損失函式,到後面可以看出,這個y_i實際上代表殘差,即樣本真實值和當前模型預測值之差。

接下來給出前向分步演算法的具體操作步驟:

假設存在訓練資料集T=\left\{(x_1, y_1), (x_2, y_2), ..., (x_N, y_N) \right \}x_i\in \chi \subseteq R^ny_i\in \left\{+1, -1\right\};損失函式為L(y, f(x));並且我們還知道基函式集\left\{b(x, \gamma)\right\},我們不知道每一個基函式的引數,我們需要通過前向分步演算法將引數求解出來

第一步:初始化模型f_0(x)=0

第二步:對於m=1, 2, ..., M:

  • 2.1 極小化如下損失:(\beta_m, \gamma_m)=arg\;\underset{\beta_m,\gamma_m}{min}\sum_{i=1}^{N}L(y_i, f_{m-1}(x_i) + \beta_m b(x_i;\gamma_m)),其中f_{m-1}(x_i)=\sum_{j=1}^{m-1}\beta_jb(x_i;\gamma_j)為當前模型對於樣本點x_i的預測值,我們記做\widehat{y}_{m-1}^i,也就是對(\beta_m, \gamma_m)=arg\;\underset{\beta_m,\gamma_m}{min}\sum_{i=1}^{N}L(y_i, \widehat{y}_{m-1}^i + \beta_m b(x_i;\gamma_m))進行優化;實際上我們是要讓\widehat{y}_{m-1}^i + \beta_m b(x_i;\gamma_m)逼近真實值y_i,即\widehat{y}_{m-1}^i + \beta_m b(x_i;\gamma_m)\rightarrow y_i,而\widehat{y}_{m-1}^i為已知量,因此將這個逼近問題轉化為\beta_m b(x_i;\gamma_m)\rightarrow y_i-\widehat{y}_{m-1}^i
    ,所以\beta_m b(x_i;\gamma_m)是對當前模型預測殘差的一個擬合
  • 2.2 更新當前模型:f_{m}(x)=f_{m-1}(x)+\beta_mb(x;\gamma_m)
  • 迴圈執行2.1和2.2知道構造完所有的M個基函式

第三步:構造最終分類器:f(x)=f_M(x)=\sum_{m=1}^{M}\beta_mb(x;\gamma_m)

以上便是前向分步演算法的具體步驟及公式

前向分步演算法中的損失函式如果是針對分類問題我們往往採取指數損失L(y, f(x))=exp(-yf(x));而如果是迴歸問題,我們通常採用平方誤差損失L(x, f(x))=(y-f(x))^2;其實Adaboost演算法得到的模型實質上與前向分步演算法採用指數損失得到的加法模型等價。

下面我們介紹提升樹模型:

以決策樹為基函式,進行前向分步演算法得到的模型就是提升樹模型,這種以決策樹為基函式的提升方法成為提升樹(boosting tree)。如果我們最終要構造的時分類模型,我們採用二叉分類樹作為基函式;如果要構造迴歸模型,我們採用二叉迴歸樹作為基函式。

提升樹模型如果用來解決分類問題,我們只需將前向分佈演算法中的損失函式用指數損失,基函式用二叉分類樹即可得到提升樹模型;實質上我們在Adaboost演算法中將基分類器限制為二叉分類樹也可以得到提升樹模型,與前者是等價的,因此提升樹演算法是Adaboost演算法的一種特殊情況。

而提升樹模型如果用來解決迴歸問題,我們需要將前向分佈演算法中的損失函式設定為平方誤差損失,基函式使用二叉迴歸樹即可得到解決迴歸問題的提升樹模型。

因為解決分類問題就是Adaboost演算法,只不過基分類器採用了二叉分類樹,Adaboost前面已經介紹過了,所以這裡就不介紹了,這裡我們只介紹用於解決迴歸問題的提升樹模型,這裡還是採用前向分步演算法:

第一步:確定初始提升樹:f_0(x)=0

第二步:確定第m顆二叉迴歸樹T(x;\Theta_m),其中\Theta_m表示這棵樹的引數:

首先可以確定的是,通過前向分步演算法我們可以通過解決這樣一個優化問題\Theta_m=arg\;\underset{\Theta_m}{min}\sum_{i=1}^{N}L(y_i, f_{m-1}(x_i)+T(x_i;\Theta_m))來求出第m顆樹。但我們進行進一步分析,由於我們這裡採用的是平方誤差損失,故L(y_i, f_{m-1}(x_i)+T(x_i;\Theta_m))=(y_i- f_{m-1}(x_i)-T(x_i;\Theta_m))^2,而y_i- f_{m-1}(x_i)實際上就是當前模型f_{m-1}(x)對於第i個樣本預測值與真實值的殘差,我們記作r_{m-1}^i=y_i- f_{m-1}(x_i),因此損失函式便成為了L(y_i, f_{m-1}(x_i)+T(x_i;\Theta_m))=(r_{m-1}^i-T(x_i;\Theta_m))^2,可以看出實際上T(x;\Theta_m)是用來擬合當前模型的殘差r_{m-1}^i=y_i- f_{m-1}(x_i)的。

所以第m顆樹的求取我們可以通過:

  • 2.1 計算殘差r_{m-1}^i=y_i- f_{m-1}(x_i)
  • 2.2 優化:\Theta_m=arg\;\underset{\Theta_m}{min}\sum_{i=1}^{N}L(r_{m-1}^i, T(x_i;\Theta_m))=arg\;\underset{\Theta_m}{min}\sum_{i=1}^{N}(r_{m-1}^i- T(x_i;\Theta_m))^2
  • 得到第m顆樹T(x;\Theta_m),更新f_m(x)=f_{m-1}(x)+T(x;\Theta_m)

第三步:得到迴歸問題的提升樹模型:f_M(x)=\sum_{m=1}^{M}T(x;\Theta_m)

到這裡提升樹以及前向分步演算法頁介紹完畢了,下面附上提升樹相關程式,資料為波士頓房價資料:

from sklearn.preprocessing import StandardScaler
import pandas as pd
import numpy as np
from numpy import random as rd
from sklearn.datasets import load_boston
from copy import copy
np.set_printoptions(linewidth=1000, suppress=True)


class Foward(object):
    
    def __init__(self, n):
        # self.base_funcs用於存放基函式
        self.base_funcs = []
        x = load_boston()["data"]
        y = load_boston()["target"]
        self.x_train, self.x_test, self.y_train, self.y_test = train_test_split(x, y, test_size=0.3, random_state=3)
        stand_transfer = StandardScaler()
        self.x_train = stand_transfer.fit_transform(self.x_train)
        self.x_test = stand_transfer.transform(self.x_test)
        self.n = n
        # 假設初始殘差為self.residual與訓練標記相同
        self.residual = copy(self.y_train)
    
    def train(self):
        # acum_residual用於累加預測的殘差
        acum_residual = np.zeros(self.y_train.shape)
        for i in range(self.n):
            tree = DecisionTreeRegressor()
            param = {"max_depth": np.arange(1, 5), "min_samples_leaf": np.arange(1, 5), "min_samples_split": np.arange(2, 5)}
            clsf = GridSearchCV(estimator=tree, param_grid=param, cv=10)
            clsf.fit(self.x_train, self.residual)
            residual_pred = clsf.predict(self.x_train)
            self.base_funcs.append(clsf.best_estimator_)
            acum_residual += residual_pred
            # self.residual為真實殘差,計算方式為y的真實值減去預測殘差的累加就是真實殘差,下一次再用base_func擬合這個殘差
            self.residual = copy(self.y_train - acum_residual)
    
    def test(self):
        y_test_predict = np.zeros(self.y_test.shape)
        for model in self.base_funcs:
            y_test_predict += model.predict(self.x_test)
        print("===================前向分步演算法================")
        print("測試集真實值:", self.y_test)
        print("測試集預測值:", y_test_predict)
        print("測試集均方誤差為:", mean_squared_error(y_pred=y_test_predict, y_true=self.y_test))
            
    def run(self):
        self.train()
        self.test()
        
        
def main():
    f = Foward(30)
    f.run()

    
if __name__ == "__main__":
    main()