The Optimization of the Adaboost
1.對於Adaboost error function的推導
再回到我們上篇文章講到的Adaboost算法,我們要從Adaboost算法推導出GBDT。首先回顧一下上篇文章的Adaboost,主要思想就是把弱分類器集中起來得到一個強的分類器。首先第一次建造樹的時候每一個樣本的權值都是一樣的,之後的每一次訓練只要有錯誤,那麼這個錯誤就會被放大,而正確的權值就會被縮小,之後會得到每一個模型的α,根據每一個樹的α把結果結合起來就得到需要的結果。
在Adaboost裏面,Ein表達式:
每一個錯誤的點乘權值相加求平均,我們想把這一個特徵結合到decision tree裏面,那麼就需要我們在決策樹的每一個分支下面加上權值,這樣很麻煩。爲了保持決策樹的封閉性和穩定性,我們不改變結構,只是改變數據的結果,餵給的數據乘上權值,其他的不改變,把決策樹當成是一個黑盒子。
權值u其實就是這個數據用boostrap抽樣抽到的概率,可以做一個帶權抽樣,也就是帶了u的sample,這樣抽出來的數據沒一個樣本和u的比例應該是差不多一致的,所以帶權sampling也叫boostrap的反操作。這種方法就是對數據本身進行改變而對於算法本身的數據結構不變。
上面的步驟就是得到gt,接下來就是求α了,α就是每一個model的重要性,上節課我們是講到過怎麼求的,先得到錯誤率ξ,然後:
這個東西很重要,之後我們會對這公式做推導。
①爲什麼Adaboost要弱類型的分類器?
分類器講道理,應該是越強越好的,但是Adaboost相反,他只要弱的,強的不行。來看一下α,如果你的分類器很強,ξ基本就是equal 0了,這樣的α是無窮大的,其他的分類器就沒有意義了,這樣就又回到了單分類器,而單分類器是做不到aggregation model的好處的,祥見aggregation model這篇文章。
我們把這種稱爲autocracy,獨裁。針對這兩個原因,我們可以做剪枝,限制樹的大小等等的操作,比如只使用一部分樣本,這在sampling的操作中已經起到這類作用,因爲必然有些樣本沒有被採樣到。
所以,綜上原因,Adaboost常用的模型就是decision stump,一層的決策樹。
事實上,如果樹高爲1時,通常較難遇到ξ=0的情況,且一般不採用sampling的操作,而是直接將權重u代入到算法中。這是因爲此時的AdaBoost-DTree就相當於是AdaBoost-Stump,而AdaBoost-Stump就是直接使用u來優化模型的。前面的算法實現也是基於這些理論實現的算法。
回到正題,繼續從optimization的角度討論Adaboost,Adaboost權重計算如下:
把上面兩張形式結合一下,得到下面綜合形式。第一層的u是1/N,每一次遞推要乘上
於是,u(t+1):
而在這裏面
仔細看一下,voting score是由許多的g(x)通過不同的α進行線性組合而成的,換一個角度,把g(x)看做是特徵轉換的φ(x),α就是權重w,這樣一來再和SVM對比一下:
對比上面其實是很像的,乘上的y就是表名是在正確的一側還是錯誤的一側,所以,這裏的voting score也可以看做是沒有正規化的距離。也就是沒有除|w|的距離,所以從效果上看,voting score是要越大越好。再來看之前的乘上y的表達式,我們要做的就是要使得分類正確,所以y的符號和voting score符號相同的數量會越來越多,這樣也就保證了這個voting score是在不斷增大,距離不斷增大。這種距離要求大的在SVM裏面我們叫正則化——regularization,同樣,這樣就證明了①Adaboost的regularization。剛剛的g(x)看做是φ其實就是feature transform,所以也證明了②Adaboost的feature transform。這兩個特性一個是踩剎車一個是加速,效果固然是比單分類器好的。
所以根據上面的公式,可以得到u是在不斷減小的:
所以我們的目標就是要求經過了(t+1)輪的迭代之後,u的數據集合要越來越小。有如下:
②Adaboost的error
上式中我們把voting score看做是s。對於0/1error,如果ys >= 0,證明是分類正確的,不懲罰err0/1 = 0,;如果ys < 0,證明分類錯誤,懲罰err0/1 = 1。對於Adaboost,我們可以用
來表示,可能有些同學覺得這裏不太對,這裏明明是u的表達式爲什麼可以作爲錯誤的衡量?這個表達式裏面包含了y和s,正好可以用來表達Adaboost的error,是就使用了。
正好也是減小的,和我們需要的err是一樣的,要注意這裏Adaboost我們是反推,已知到效果和方法反推表達式,使用要用上一切可以使用的。
和error0/1對比:
所以,Adaboost的error function是可以代替error0/1的,並且效果更好。
2.對於Adaboost error function的一些簡單處理
③首先要了解一些泰勒展開:
在一般的函數情況下,是有:
①:
②:
①②式子代入上面:
這就是一階Taylor expansion。待會要利用他們來求解。
用Taylor expansion處理一下Adaboost error function:
這是Gradient descent的公式,我們類比一下error function:
這裏是使用exp(x) 的Taylor一階展開,這裏的方向是h(x),而Gradient descent是w纔是方向,w是某一個點的方向,而h(x)也可以代入某一個點得到一個value,這個value就是方向,所以基本上和梯度下降是一致的:
對於y和h(x)我們均限定是{1,-1},對這個優化項做一些平移:
所以要優化的項最後又轉變成了Ein,我們的演算法一直都是在做減小Ein這件事,所以Adaboost的base algorithm——decision stump就是做的這件事。h(x)就可以解決了。
解決了h(x)的問題接下來就是η的問題了:
所以最後的公式如上圖所示。
上式中有幾個情況可以考慮:
經過推導:
求η最小值自然就是求導了:
就得到:
而η = α,這樣就推導出了α的表達式了。
Adaboost實際上就是在尋找最塊下降方向和最快下降步長的過程中優化,而α相當於最大的步長,h(x)相當於最快的方向。所以,Adaboost就是在Gradient descent上尋找最快的方向和最快的步長。
Gradient Boosted Decision Tree
推導完了Adaboost,我們接着推導Gradient Boosted Decision Tree,其實看名字就知道只不過是error function不太一樣而已。前面Adaboost的推導總的可以概括爲:
這種exp(-ys)function是Adaboost專有的,我們能不能換成其他的?比如logistics或者linear regression的。
使用Gradient descent的就是這種形式,雖然形式變了,但是最終的結果都是求解最快的方向和最長的步長。
這裏使用均方差替代error。使用一階泰勒展開:
constant我們不需要管,我們只需要關心最後的一項。使得這一項最小,那只需要h(x)和2(s - y)互爲相反數,並且h(x)很大很大就好了,h(x)不做限制,很明顯這樣是求不出來的。有一個簡單的做法,收了regularization的啓發,我們可以在後面加上懲罰項,使用L2範式,L2範式會使他們很小但不會爲0,但是L1範式會使得他們集中到邊角上,係數矩陣,使用這裏使用L2範式,另一方面,也是對於化簡的方便做了準備。
所以,優化的目標:
y-s我們稱爲殘差,我們要做的就是使得h(x)和(y - s)接近,也就是做一個擬合的過程,也就是做regression。
擬合出來得到的h(x)就是我們要的gt(x)了。之後就是求η了。
③:
得到了g(x)之後就是求η步長了。注意上面的③纔是我們要求的公式,
總結一下,以上就是GBDT的流程了。值得注意的是,sn的初始值一般均設爲0,即s1=s2=⋯=sN=0。每輪迭代中,方向函數gt通過C&RT算法做regression,進行求解;步進長度η通過簡單的單參數線性迴歸進行求解;然後每輪更新sn的值,即sn←sn+αtgt(xn)。T輪迭代結束後,最終得到
Summary of Aggregation Models
到這裏,aggregation model基本就完成了。主要有三個方面:
①uniform:把g(x)平均結合。
②non-uniform:把g(x)線性組合。
③conditional:根據不同條件做非線性組合。
uniform採用投票、求平均的形式更注重穩定性;而non-uniform和conditional追求的更復雜準確的模型,但存在過擬合的危險。
剛剛所討論的model都是建立在g(x)已知的情況下,如果g(x)不知道,我們就可以使用一下方法:
①Bagging:通過boostrap方法訓練模型平均結合結果。
②Adaboost:通過boostrap方法訓練模型進行線性組合。
③Decision Tree:數據分割得到不同的g(x)進行線性組合。
除了以上的方法,我們還可以把Bagging和Decision Tree結合起來稱爲random forest,Adaboost和decision tree結合起來就是Adaboost-stump,Gradient Boosted和Adaboost結合起來就是GBDT了。
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')
效果:
效果貌似還是可以的。aggregation model就到此爲止了,幾乎所有常用模型都講完了。
最後符上GitHub所有代碼:
https://github.com/GreenArrow2017/MachineLearning/tree/master/MachineLearning/GBDT