The Optimization of the Adaboost and Gradient Boosted Decision Tree

The Optimization of the Adaboost

1.對於Adaboost error function的推導

再回到我們上篇文章講到的Adaboost算法,我們要從Adaboost算法推導出GBDT。首先回顧一下上篇文章的Adaboost,主要思想就是把弱分類器集中起來得到一個強的分類器。首先第一次建造樹的時候每一個樣本的權值都是一樣的,之後的每一次訓練只要有錯誤,那麼這個錯誤就會被放大,而正確的權值就會被縮小,之後會得到每一個模型的α,根據每一個樹的α把結果結合起來就得到需要的結果。

10624272-852c5746735815f0.png

在Adaboost裏面,Ein表達式:
10624272-66b23acac2234e82.png

每一個錯誤的點乘權值相加求平均,我們想把這一個特徵結合到decision tree裏面,那麼就需要我們在決策樹的每一個分支下面加上權值,這樣很麻煩。爲了保持決策樹的封閉性和穩定性,我們不改變結構,只是改變數據的結果,餵給的數據乘上權值,其他的不改變,把決策樹當成是一個黑盒子。
權值u其實就是這個數據用boostrap抽樣抽到的概率,可以做一個帶權抽樣,也就是帶了u的sample,這樣抽出來的數據沒一個樣本和u的比例應該是差不多一致的,所以帶權sampling也叫boostrap的反操作。這種方法就是對數據本身進行改變而對於算法本身的數據結構不變。
10624272-7e3876db9e4e66d0.png

10624272-0b31edf509777fce.png

上面的步驟就是得到gt,接下來就是求α了,α就是每一個model的重要性,上節課我們是講到過怎麼求的,先得到錯誤率ξ,然後:
10624272-c90fe9ec1ffe375b.png

這個東西很重要,之後我們會對這公式做推導。

①爲什麼Adaboost要弱類型的分類器?
分類器講道理,應該是越強越好的,但是Adaboost相反,他只要弱的,強的不行。來看一下α,如果你的分類器很強,ξ基本就是equal 0了,這樣的α是無窮大的,其他的分類器就沒有意義了,這樣就又回到了單分類器,而單分類器是做不到aggregation model的好處的,祥見aggregation model這篇文章。

10624272-a7c46938ed3ace36.png

我們把這種稱爲autocracy,獨裁。針對這兩個原因,我們可以做剪枝,限制樹的大小等等的操作,比如只使用一部分樣本,這在sampling的操作中已經起到這類作用,因爲必然有些樣本沒有被採樣到。
10624272-41f9cda4a3167df3.png

所以,綜上原因,Adaboost常用的模型就是decision stump,一層的決策樹。
10624272-c87b542c40b61f4a.png

10624272-04af26ed55e573bf.png

事實上,如果樹高爲1時,通常較難遇到ξ=0的情況,且一般不採用sampling的操作,而是直接將權重u代入到算法中。這是因爲此時的AdaBoost-DTree就相當於是AdaBoost-Stump,而AdaBoost-Stump就是直接使用u來優化模型的。前面的算法實現也是基於這些理論實現的算法。

回到正題,繼續從optimization的角度討論Adaboost,Adaboost權重計算如下:

10624272-848666cd16af45bf.png

把上面兩張形式結合一下,得到下面綜合形式。第一層的u是1/N,每一次遞推要乘上
10624272-edc8225ba9e0d040.png

於是,u(t+1):
10624272-8d82b41e684f0c7f.png

而在這裏面
10624272-66b2bccd3a6d7cad.png
稱爲是voting score,也就是集合投票的分數,而u(t+1)是和voting score成正比的:
10624272-091a3539f6be95c5.png

仔細看一下,voting score是由許多的g(x)通過不同的α進行線性組合而成的,換一個角度,把g(x)看做是特徵轉換的φ(x),α就是權重w,這樣一來再和SVM對比一下:
10624272-397decae570354fa.png

對比上面其實是很像的,乘上的y就是表名是在正確的一側還是錯誤的一側,所以,這裏的voting score也可以看做是沒有正規化的距離。也就是沒有除|w|的距離,所以從效果上看,voting score是要越大越好。再來看之前的乘上y的表達式,我們要做的就是要使得分類正確,所以y的符號和voting score符號相同的數量會越來越多,這樣也就保證了這個voting score是在不斷增大,距離不斷增大。這種距離要求大的在SVM裏面我們叫正則化——regularization,同樣,這樣就證明了①Adaboost的regularization。剛剛的g(x)看做是φ其實就是feature transform,所以也證明了②Adaboost的feature transform。這兩個特性一個是踩剎車一個是加速,效果固然是比單分類器好的。
10624272-8d82b41e684f0c7f.png

所以根據上面的公式,可以得到u是在不斷減小的:
10624272-805d3ee4001113e8.png

所以我們的目標就是要求經過了(t+1)輪的迭代之後,u的數據集合要越來越小。有如下:
10624272-e486e1f644003a71.png

②Adaboost的error
上式中我們把voting score看做是s。對於0/1error,如果ys >= 0,證明是分類正確的,不懲罰err0/1 = 0,;如果ys < 0,證明分類錯誤,懲罰err0/1 = 1。對於Adaboost,我們可以用

10624272-d711b058e10f86a2.png

來表示,可能有些同學覺得這裏不太對,這裏明明是u的表達式爲什麼可以作爲錯誤的衡量?這個表達式裏面包含了y和s,正好可以用來表達Adaboost的error,是就使用了。
10624272-8631c494abbef1bc.png

正好也是減小的,和我們需要的err是一樣的,要注意這裏Adaboost我們是反推,已知到效果和方法反推表達式,使用要用上一切可以使用的。
和error0/1對比:
10624272-a631633e11f5a8fa.png

10624272-a80cab580ebafcf1.png

所以,Adaboost的error function是可以代替error0/1的,並且效果更好。

2.對於Adaboost error function的一些簡單處理

③首先要了解一些泰勒展開:
在一般的函數情況下,是有:

10624272-c9523e5bfeb79da6.png

①:
10624272-360a6d482b2168ed.png

②:
10624272-f966f0fb83d9b860.png

①②式子代入上面:
10624272-d8865675acc17410.png
10624272-001912e89e2a8cbb.png

這就是一階Taylor expansion。待會要利用他們來求解。

用Taylor expansion處理一下Adaboost error function:

10624272-e79d545cdae2b057.png

這是Gradient descent的公式,我們類比一下error function:
10624272-f7f1c185d27ab832.png

這裏是使用exp(x) 的Taylor一階展開,這裏的方向是h(x),而Gradient descent是w纔是方向,w是某一個點的方向,而h(x)也可以代入某一個點得到一個value,這個value就是方向,所以基本上和梯度下降是一致的:
10624272-d818384007f62c85.png
以上的對於error function的分解是在x = 0的點進行Taylor展開,可以看到前面的一項是常數,後面的一項纔是變量,使用我們需要優化的就是減小後面一項的值。所以我們先要找到一個最合適的h(x)來優化這個函數,n先忽略:
10624272-c2a618c25b24e397.png

對於y和h(x)我們均限定是{1,-1},對這個優化項做一些平移:
10624272-d6bfacdec6b3fb85.png

所以要優化的項最後又轉變成了Ein,我們的演算法一直都是在做減小Ein這件事,所以Adaboost的base algorithm——decision stump就是做的這件事。h(x)就可以解決了。
10624272-70b62671c722712d.png

解決了h(x)的問題接下來就是η的問題了:
10624272-65fd3e88b043ebb9.png

10624272-5c8fec73f6b5be73.png

所以最後的公式如上圖所示。
上式中有幾個情況可以考慮:
10624272-fb0cb1be993dce14.png

經過推導:
10624272-21bc1da660bb86ef.png

10624272-69700a643938189c.png

求η最小值自然就是求導了:
10624272-3ab88b5f6737ade4.png

就得到:
10624272-e9850a420c2df85f.png

而η = α,這樣就推導出了α的表達式了。
Adaboost實際上就是在尋找最塊下降方向和最快下降步長的過程中優化,而α相當於最大的步長,h(x)相當於最快的方向。所以,Adaboost就是在Gradient descent上尋找最快的方向和最快的步長。

Gradient Boosted Decision Tree

推導完了Adaboost,我們接着推導Gradient Boosted Decision Tree,其實看名字就知道只不過是error function不太一樣而已。前面Adaboost的推導總的可以概括爲:

10624272-5b3e98e01725b24e.png

這種exp(-ys)function是Adaboost專有的,我們能不能換成其他的?比如logistics或者linear regression的。
10624272-da2d376265b48494.png

使用Gradient descent的就是這種形式,雖然形式變了,但是最終的結果都是求解最快的方向和最長的步長。
10624272-dd0f7df75592dd1e.png

10624272-31ce06e367b1a11e.png

這裏使用均方差替代error。使用一階泰勒展開:
10624272-b61f2d732b2d2104.png

constant我們不需要管,我們只需要關心最後的一項。使得這一項最小,那只需要h(x)和2(s - y)互爲相反數,並且h(x)很大很大就好了,h(x)不做限制,很明顯這樣是求不出來的。有一個簡單的做法,收了regularization的啓發,我們可以在後面加上懲罰項,使用L2範式,L2範式會使他們很小但不會爲0,但是L1範式會使得他們集中到邊角上,係數矩陣,使用這裏使用L2範式,另一方面,也是對於化簡的方便做了準備。
10624272-feda1b65abf61d52.png

所以,優化的目標:
10624272-4d9cf15805168451.png

y-s我們稱爲殘差,我們要做的就是使得h(x)和(y - s)接近,也就是做一個擬合的過程,也就是做regression。
10624272-d90c1097cb35af89.png

10624272-f8f155085aaa6a49.png

擬合出來得到的h(x)就是我們要的gt(x)了。之後就是求η了。
③:
10624272-df26cd4aa152a1d2.png

得到了g(x)之後就是求η步長了。注意上面的③纔是我們要求的公式,
10624272-c56baf185ae7cd80.png
這一個知識爲了化簡方便的。
10624272-fd9fffa628f16910.png

10624272-2365062725ba2d04.png

總結一下,以上就是GBDT的流程了。值得注意的是,sn的初始值一般均設爲0,即s1=s2=⋯=sN=0。每輪迭代中,方向函數gt通過C&RT算法做regression,進行求解;步進長度η通過簡單的單參數線性迴歸進行求解;然後每輪更新sn的值,即sn←sn+αtgt(xn)。T輪迭代結束後,最終得到
10624272-7e298f6da1fdbf79.png
。值得一提的是,本節課第一部分介紹的AdaBoost-DTree是解決binary classification問題,而此處介紹的GBDT是解決regression問題。二者具有一定的相似性,可以說GBDT就是AdaBoost-DTree的regression版本。

Summary of Aggregation Models

到這裏,aggregation model基本就完成了。主要有三個方面:
①uniform:把g(x)平均結合。
②non-uniform:把g(x)線性組合。
③conditional:根據不同條件做非線性組合。

uniform採用投票、求平均的形式更注重穩定性;而non-uniform和conditional追求的更復雜準確的模型,但存在過擬合的危險。

10624272-60b903221e94f19b.png

剛剛所討論的model都是建立在g(x)已知的情況下,如果g(x)不知道,我們就可以使用一下方法:
①Bagging:通過boostrap方法訓練模型平均結合結果。
②Adaboost:通過boostrap方法訓練模型進行線性組合。
③Decision Tree:數據分割得到不同的g(x)進行線性組合。

10624272-a866f3c0f29d0374.png

除了以上的方法,我們還可以把Bagging和Decision Tree結合起來稱爲random forest,Adaboost和decision tree結合起來就是Adaboost-stump,Gradient Boosted和Adaboost結合起來就是GBDT了。
10624272-6eeeb01adfd801c5.png

Aggregation的核心是將所有的gt結合起來,融合到一起,也就是集體智慧的思想。這種做法能夠得到好的G的原因,是因爲aggregation具有兩個方面的優點:cure underfitting和cure overfitting。
①aggregation models有助於防止欠擬合(underfitting)。它把所有比較弱的gt結合起來,利用集體智慧來獲得比較好的模型G。aggregation就相當於是feature transform,來獲得複雜的學習模型。
②aggregation models有助於防止過擬合(overfitting)。它把所有gt進行組合,容易得到一個比較中庸的模型,類似於SVM的large margin一樣的效果,避免了一些過擬合的情況發生。從這個角度來說,aggregation起到了regularization的效果。

由於aggregation具有這兩個方面的優點,所以在實際應用中aggregation models都有很好的表現。

代碼實現

主要的做法就是用{(x, (y - s))}做擬合就好了。
由於之前寫的決策樹結構設計的不太好,使用起來不方便於是重新寫了一個,這裏的CART樹是用方差來衡量impurity的。

def loadDataSet(filename):
    '''
    load dataSet
    :param filename: the filename which you need to open
    :return: dataset in file
    '''
    dataMat = pd.read_csv(filename)
    for i in range(np.shape(dataMat)[0]):
        if dataMat.iloc[i, 2] == 0:
            dataMat.iloc[i, 2] = -1
    return dataMat
    pass

def split_data(data_array, col, value):
    '''split the data according to the feature'''
    array_1 = data_array.loc[data_array.iloc[:, col] >= value, :]
    array_2 = data_array.loc[data_array.iloc[:, col] < value, :]
    return array_1, array_2
    pass

def getErr(data_array):
    '''calculate the var '''
    return np.var(data_array.iloc[:, -1]) * data_array.shape[0]
    pass

def regLeaf(data_array):
    return np.mean(data_array.iloc[:, -1])

加載數據,分割數據,計算方差,計算葉子平均,其實就是計算擬合的類別了。

def get_best_split(data_array, ops = (1, 4)):
    '''the best point to split data'''
    tols = ops[0]
    toln = ops[1]
    if len(set(data_array.iloc[:, -1])) == 1:
        return None, regLeaf(data_array)
    m, n = data_array.shape
    best_S = np.inf
    best_col = 0
    best_value = 0
    S = getErr(data_array)
    for col in range(n - 1):
        values = set(data_array.iloc[:, col])
        for value in values:
            array_1, array_2 = split_data(data_array, col, value)
            if (array_1.shape[0] < toln) or (array_2.shape[0] < toln):
                continue
            totalError = getErr(array_1) + getErr(array_2)
            if totalError< best_S:
                best_col = col
                best_value = value
                best_S = totalError
    if (S - best_S) < tols:
        return None, regLeaf(data_array)
    array_1, array_2 = split_data(data_array, best_col, best_value)
    if (array_1.shape[0] < toln) or (array_2.shape[0] < toln):
        return None, regLeaf(data_array)

    return best_col, best_value

得到最好的分類,這裏相比之前的決策樹加了一些條件限制,葉子數量不能少於4,和之前的一樣,計算方差對比看看哪個小。

class node:
    '''tree node'''
    def __init__(self, col=-1, value=None, results=None, gb=None, lb=None):
        self.col = col
        self.value = value
        self.results = results
        self.gb = gb
        self.lb = lb
        pass

葉子節點,col列,val劃分的值,results結果,gb右子樹,lb左子樹。

def buildTree(data_array, ops = (1, 4)):
    col, val = get_best_split(data_array, ops)
    if col == None:
        return node(results=val)
    else:
        array_1, array_2 = split_data(data_array, col, val)
        greater_branch = buildTree(array_1, ops)
        less_branch = buildTree(array_2, ops)
        return node(col=col, value=val, gb=greater_branch, lb=less_branch)
    pass

建立一棵樹。


def treeCast(tree, inData):
    '''get the classification'''
    if tree.results != None:
        return tree.results
    if inData.iloc[tree.col] > tree.value:
        return treeCast(tree.gb, inData)
    else:
        return treeCast(tree.lb, inData)
    pass

def createForeCast(tree, testData):
    m = len(testData)
    yHat = np.mat(np.zeros((m, 1)))
    for i in range(m):
        yHat[i, 0] = treeCast(tree, testData.iloc[i])
    return yHat

創建分類。

def GBDT_model(data_array, num_iter, ops = (1, 4)):
    m, n = data_array.shape
    x = data_array.iloc[:, 0:-1]
    y = data_array.iloc[:, -1]
    y = np.mat(y).T
    list_trees = []
    yHat = None
    for i in range(num_iter):
        print('the ', i, ' tree')
        if i == 0:
            tree = buildTree(data_array, ops)
            list_trees.append(tree)
            yHat = createForeCast(tree, x)
        else:
            r = y - yHat
            data_array = np.hstack((x, r))
            data_array = pd.DataFrame(data_array)
            tree = buildTree(data_array, ops)
            list_trees.append(tree)
            rHat = createForeCast(tree, x)
            yHat = yHat + rHat
    return list_trees, yHat

這裏只是使用了迴歸問題的迴歸樹,x和(y - s)做擬合之後加入預測集即可。
接下來就是畫圖了:

def getwidth(tree):
    if tree.gb == None and tree.lb == None: return 1
    return getwidth(tree.gb) + getwidth(tree.lb)


def getdepth(tree):
    if tree.gb == None and tree.lb == None: return 0
    return max(getdepth(tree.gb), getdepth(tree.lb)) + 1


def drawtree(tree, jpeg='tree.jpg'):
    w = getwidth(tree) * 100
    h = getdepth(tree) * 100 + 120

    img = Image.new('RGB', (w, h), (255, 255, 255))
    draw = ImageDraw.Draw(img)

    drawnode(draw, tree, w / 2, 20)
    img.save(jpeg, 'JPEG')


def drawnode(draw, tree, x, y):
    if tree.results == None:
        # Get the width of each branch
        w1 = getwidth(tree.lb) * 100
        w2 = getwidth(tree.gb) * 100

        # Determine the total space required by this node
        left = x - (w1 + w2) / 2
        right = x + (w1 + w2) / 2

        # Draw the condition string
        draw.text((x - 20, y - 10), str(tree.col) + ':' + str(tree.value), (0, 0, 0))

        # Draw links to the branches
        draw.line((x, y, left + w1 / 2, y + 100), fill=(255, 0, 0))
        draw.line((x, y, right - w2 / 2, y + 100), fill=(255, 0, 0))

        # Draw the branch nodes
        drawnode(draw, tree.lb, left + w1 / 2, y + 100)
        drawnode(draw, tree.gb, right - w2 / 2, y + 100)
    else:
        txt = str(tree.results)
        draw.text((x - 20, y), txt, (0, 0, 0))

之後就是運行主函數了:

if __name__ == '__main__':
    data = loadDataSet('../Data/LogiReg_data.txt')
    tree = buildTree(data)
    drawtree(tree, jpeg='treeview_cart.jpg')
    gbdt_results, y = GBDT_model(data, 10)
    print(y)
    for i in range(len(y)):
        if y[i] > 0:
            print('1')
        elif y[i] < 0:
            print('0')
10624272-21b040afef2b98b1.png

效果:


10624272-c51ee23929e69689.png

效果貌似還是可以的。aggregation model就到此爲止了,幾乎所有常用模型都講完了。

最後符上GitHub所有代碼:
https://github.com/GreenArrow2017/MachineLearning/tree/master/MachineLearning/GBDT

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章