1. 程式人生 > 其它 >GBDT-梯度提升決策樹

GBDT-梯度提升決策樹

前面介紹了決策樹和整合演算法的相關知識,本章介紹的GBDT(Gradient Boosting Decision Tree)是這兩個知識點的融合,GBDT所採用的樹模型是CART迴歸樹,將回歸樹改造後,GBDT不僅用於迴歸也可用於分類,GBDT與SVM支援向量機被認為是泛化能力較強的模型。名稱中的提升(Boosting)說明該演算法是一種整合演算法,與AdaBoosting不同的是:GBDT整合的物件必須是CART樹,而AdaBoosting整合目標是弱分類器;兩者迭代的方式也有區別,AdaBoosting利用上一輪錯誤率來更新下一輪分類器的權重、從而實現最終識別能力的提升,而GBDT使用梯度來實現模型的提升,演算法中利用殘差來擬合梯度,通過不斷減小殘差實現梯度的下降。

一、GBDT實現迴歸

1.1殘差的概念

首先來了解殘差概念,假設有一組資料分別是:5、6、7,取平均數6為每個資料的預測值,則三個資料殘差分別為5-6、6-6、7-6:-1,0,1,殘差可以定義為真實值與預測值之間的差值。GBDT實現迴歸功能時,通常取平均值作為初始值或稱之為第0輪預測值,利用0輪預測值可得到每個資料的殘差,將殘差作為目標值訓練CART決策樹,根據設定CART樹層數閾值、分裂數目等規則,新生成CART樹葉節點中可能為一個含有初始殘差的集合,也有可能只有一個殘差元素;得到CART樹後將得到一組新的殘差值,將新的一組殘差值輸入下一輪再生成CART樹,依次類推,最後殘差值會越來越小,代表著GBDT模型逐步擬合真實的資料從而實現迴歸。下面通過一個示例來說明GBDT實現迴歸的過程,樣本有年齡、體重兩個屬性,需要通過這兩個屬性預測目標值身高:

初始時取身高平均值1.475作為預測值,並得到初始殘差資料:

將殘差作為目標資料訓練CART迴歸樹:

CART樹實現迴歸原理在CART樹分類、迴歸、剪枝實現一章中已經介紹過,假設以年齡小於7歲作為分裂點,小於7歲資料只有年齡為5歲的樣本,其平方誤差為(-0.375-(-0.375))^2=0,將其定義為左結點誤差SEl,即SEl=0;而大於等於7歲的樣本殘差平均值為(-0.175+0.225+ 0.325)/3=0.125,可得到右結點誤差SEr:

SEr=(-0.175-0.125)^2+(0.225-0.125)^2+( 0.325-0.125)^2=0.140

依次計算,取SEr+SEl值最小的點作為屬性分裂結點,得到下面的表:

可以看出選擇年齡為21或體重為60作為屬性分裂點,其平方誤差最小,不妨選擇年齡為21作為分裂點,這樣就得到第一棵CART樹:

可以看出這顆CART樹葉結點含有多個殘差元素,以左結點為例含有編號為0,1兩個資料,如果不繼續分裂,那麼通常取殘差的平均值作為該葉結點的'得分':

scoreleft=(-0.375-0.175)/2=-0.275

這時編號0的新的殘差為:1.1-(1.475-0.275)=-0.100,式中的1.475-0.275是截止到目前的預測值,可以看出:-0.100比起先前的殘差-0.375誤差縮小了很多(比較絕對值),同樣編號為1資料殘差為0.1,比起初始殘差-0.175誤差也縮小了。這顆CART樹深度是2,如果設定CART樹的深度為3,那麼可以對上面的樹繼續展開,以左結點0,1為例,繼續計算各個屬性區間的平方誤差可得到下表:

同樣得到右結點平方誤差表:

選擇平方誤差最小屬性值作為分裂結點,可得到三層結構的CART樹:

利用CART樹得到一組新的殘差,接下來將新的殘差值作為目標值訓練第二棵CART樹,具體要迭代到第幾棵樹終止計算,可以通過設定樹的數量,而最終的預測值是把初始預測值與所有樹葉節點的殘差相加。需要強調的是,當CART樹葉結點包含多個元素時,需要對葉節點打分,就回歸問題而言,取平均值就可以了,GBDT實現分類甚至跟高階的xgboost模型都有打分這個環節,根據問題的不同打分的方式也有區別。

1.2公式推導

上面的例子中利用了殘差,到目前為止並未出現梯度的概念,之前說過了,GBDT利用殘差來代替梯度,接下來通過推導來了解迴歸問題中殘差、梯度、CART樹三者的關係,GBDT迴歸函式可用下面的公式表示:

公式中m代表迭代輪數,m>=1,代表前m-1輪迴歸函式,而代表m輪需要求解的CART迴歸樹,特殊的,當m=1時,就回歸問題而言,F0(x)是目標值的平均數,在程式碼實現中稱之為初始值。、、其實都是分段函式,一個編號為i的樣本資料,①式可寫成:

(1.1)

解決迴歸問題常用平方差函式作為損失函式,假設已經得到m-1輪迴歸函式,則有損失函式:

(1.2)

將視為一個變數,對求導可得(1.2)的梯度:

(1.3)

上式中即為之前介紹的殘差,m-1輪、第i個數據的殘差可用表示,另外要使得(1.2)式損失函式值變小,需要沿當前梯度的負方向前進,這樣就可以得到的表示式:

(1.4)

上式中λ稱為步長係數,在程式碼實現中也稱為學習率,(1.4)進一步可寫成:

(1.5)

由(1.1)和(1.5)式可知,每輪求解的CART樹與損失函式梯度、殘差,三者間有著對應關係:

(1.6)

當損失函式(1.2)的梯度為0時即到達了極值點,也就是損失函式獲得了最小值,梯度等於0時殘差也必然接近0,在GBDT演算法中沒有梯度的計算,原因是梯度與殘差是協同、等效的,通過降低殘差可以實現降低梯度,而殘差的降低又是通過CART樹實現的,所以說,迭代生成CART樹的過程本質是梯度減小的過程,是一個二次函式優化的過程。

1.3 GBDT的python實現

接下來用sklearn自帶的GBDT演算法GradientBoostingRegressor分析波士頓房價資料集,該資料集有十幾個屬性,目標值是房價,屬性定義分別為:

實現程式碼如下:

fromsklearnimportdatasets
importnumpyasnp
fromsklearn.ensembleimportGradientBoostingRegressor
fromsklearn.utilsimportshuffle
fromsklearn.metricsimportmean_squared_error
defloaddata(splitset=0.9):
boston=datasets.load_boston()
X,y=shuffle(boston.data,boston.target,random_state=13)
X=X.astype(np.float32)
offset=int(X.shape[0]*splitset)
X_train,y_train=X[:offset],y[:offset]
X_test,y_test=X[offset:],y[offset:]
returnX_train,y_train,X_test,y_test

defsklearnGBDT():
X_train,y_train,X_test,y_test=loaddata()
gbdt=GradientBoostingRegressor(
loss='ls'
,learning_rate=0.01
,n_estimators=500
,subsample=1
,min_samples_split=2
,min_samples_leaf=1
,max_depth=10
,init=None
,random_state=None
,max_features='log2'
,alpha=0.9
,verbose=0
,max_leaf_nodes=None
,warm_start=False
)
gbdt.fit(X_train,y_train)
pred=gbdt.predict(X_test)
total_err=0
foriinrange(pred.shape[0]):
y=y_test[i]
print('predict=%0.2freal=%0.2f'%(pred[i],y))
total_err+=(pred[i]-y)**2

score1=gbdt.score(X_train,y_train)
score2=gbdt.score(X_test,y_test)
print('訓練集得分=%0.2f'%(score1))
print('測試集得分=%0.2f'%(score2))
mse=mean_squared_error(y_test,pred)
print('total_err=%0.2fmse=%0.2f'%(total_err/pred.shape[0],mse))


if__name__=="__main__":
sklearnGBDT()

得到模型結果如下:

GradientBoostingRegressor建構函式中有幾個引數說明一下:

learning_rate:為學習率,對應公式(1.6)中的引數λ,在優化演算法中也成為步長係數。

n_estimators:代表一共需要迭代生成CART樹的數量,數量越多擬合能力越強,但訓練的時間也會相應的增加。

min_samples_split:是結點分裂閾值,當大於min_samples_split時CART樹繼續分裂結點,本例中min_samples_split=2說明最後CART樹葉結點中都只有一個元素。

max_depth:是每棵樹的深度,當深度小於max_depth時繼續分裂結點。

max_features:主要是當屬性較多時,訓練耗時較長,max_features含義是如何選擇部分屬性生成決策樹。

根據上述推導,下面是一個實現的GBDT的python程式碼,利用的仍然是波士頓房價資料集。

=regression_tree.py= CART迴歸樹程式碼:

#-*-coding:utf-8-*-

fromcopyimportcopy
importnumpyasnp
fromnumpyimportndarray

classNode:
"""Nodeclasstobuildtreeleaves.
Attributes:
avg{float}--predictionoflabel.(default:{None})
left{Node}--Leftchildnode.
right{Node}--Rightchildnode.
feature{int}--Columnindex.
split{int}--Splitpoint.
mse{float}--Meansquareerror.
"""

attr_names=("avg","left","right","feature","split","mse")

def__init__(self,avg=None,left=None,right=None,feature=None,split=None,mse=None):
self.avg=avg
self.left=left
self.right=right
self.feature=feature
self.split=split
self.mse=mse

def__str__(self):
ret=[]
forattr_nameinself.attr_names:
attr=getattr(self,attr_name)
#DescribetheattributeofNode.
ifattrisNone:
continue
ifisinstance(attr,Node):
des="%s:Nodeobject."%attr_name
else:
des="%s:%s"%(attr_name,attr)
ret.append(des)

return"\n".join(ret)+"\n"

defcopy(self,node):
"""CopytheattributesofanotherNode.
Arguments:
node{Node}
"""

forattr_nameinself.attr_names:
attr=getattr(node,attr_name)
setattr(self,attr_name,attr)


classRegressionTree:
"""RegressionTreeclass.
Attributes:
root{Node}--RootnodeofRegressionTree.
depth{int}--DepthofRegressionTree.
_rules{list}--Rulesofallthetreenodes.
"""

def__init__(self):
self.root=Node()
self.depth=1
self._rules=None

def__str__(self):
ret=[]
fori,ruleinenumerate(self._rules):
literals,avg=rule

ret.append("Rule%d:"%i+'|'.join(
literals)+'=>y_hat%.4f'%avg)
return"\n".join(ret)

@staticmethod
def_expr2literal(expr:list)->str:
"""Auxiliaryfunctionofget_rules.
Arguments:
expr{list}--1Dlistlike[Feature,op,split].
Returns:
str
"""

feature,operation,split=expr
operation=">="ifoperation==1else"<"
return"Feature%d%s%.4f"%(feature,operation,split)

defget_rules(self):
"""Gettherulesofallthetreenodes.
Expr:1Dlistlike[Feature,op,split].
Rule:2Dlistlike[[Feature,op,split],label].
Op:-1meanslessthan,1meansequalormorethan.
"""

#Breadth-FirstSearch.
que=[[self.root,[]]]
self._rules=[]

whileque:
node,exprs=que.pop(0)

#Generatearulewhenthecurrentnodeisleafnode.
ifnot(node.leftornode.right):
#Convertexpressiontotext.
literals=list(map(self._expr2literal,exprs))
self._rules.append([literals,node.avg])

#Expandwhenthecurrentnodehasleftchild.
ifnode.left:
rule_left=copy(exprs)
rule_left.append([node.feature,-1,node.split])
que.append([node.left,rule_left])

#Expandwhenthecurrentnodehasrightchild.
ifnode.right:
rule_right=copy(exprs)
rule_right.append([node.feature,1,node.split])
que.append([node.right,rule_right])

@staticmethod
def_get_split_mse(col:ndarray,label:ndarray,split:float)->Node:
"""Calculatethemseoflabelwhencolissplittedintotwopieces.
MSEasLossfuction:
y_hat=Sum(y_i)/n,i<-[1,n]
Loss(y_hat,y)=Sum((y_hat-y_i)^2),i<-[1,n]
--------------------------------------------------------------------
Arguments:
col{ndarray}--Afeatureoftrainingdata.
label{ndarray}--Targetvalues.
split{float}--Splitpointofcolumn.
Returns:
Node--MSEoflabelandaverageofsplittedx
"""

#Splitlabel.
label_left=label[col<split]
label_right=label[col>=split]

#Calculatethemeansoflabel.
avg_left=label_left.mean()
avg_right=label_right.mean()

#Calculatethemseoflabel.
mse=(((label_left-avg_left)**2).sum()+
((label_right-avg_right)**2).sum())/len(label)

#Createnodestostoreresult.
node=Node(split=split,mse=mse)
node.left=Node(avg_left)
node.right=Node(avg_right)

returnnode

def_choose_split(self,col:ndarray,label:ndarray)->Node:
"""Iterateeachxiandsplitx,yintotwopieces,
andthebestsplitpointisthexiwhenwegetminimummse.
Arguments:
col{ndarray}--Afeatureoftrainingdata.
label{ndarray}--Targetvalues.
Returns:
Node--Thebestchoiceofmse,splitpointandaverage.
"""

#Featurecannotbesplittedifthere'sonlyoneuniqueelement.
node=Node()
unique=set(col)
iflen(unique)==1:
returnnode

#Incaseofemptysplit.
unique.remove(min(unique))

#Getsplitpointwhichhasminmse.
ite=map(lambdax:self._get_split_mse(col,label,x),unique)
node=min(ite,key=lambdax:x.mse)

returnnode

def_choose_feature(self,data:ndarray,label:ndarray)->Node:
"""Choosethefeaturewhichhasminimummse.
Arguments:
data{ndarray}--Trainingdata.
label{ndarray}--Targetvalues.
Returns:
Node--featurenumber,splitpoint,average.
"""

#Comparethemseofeachfeatureandchoosebestone.
_ite=map(lambdax:(self._choose_split(data[:,x],label),x),
range(data.shape[1]))
ite=filter(lambdax:x[0].splitisnotNone,_ite)

#ReturnNoneifnofeaturecanbesplitted.
node,feature=min(
ite,key=lambdax:x[0].mse,default=(Node(),None))
node.feature=feature

returnnode

deffit(self,data:ndarray,label:ndarray,max_depth=5,min_samples_split=2):
"""Buildaregressiondecisiontree.
Note:
Atleastthere'sonecolumnindatahasmorethan2uniqueelements,
andlabelcannotbeallthesamevalue.
Arguments:
data{ndarray}--Trainingdata.
label{ndarray}--Targetvalues.
KeywordArguments:
max_depth{int}--Themaximumdepthofthetree.(default:{5})
min_samples_split{int}--Theminimumnumberofsamplesrequired
tosplitaninternalnode.(default:{2})
"""

#Initializewithdepth,node,indexes.
self.root.avg=label.mean()
que=[(self.depth+1,self.root,data,label)]

#Breadth-FirstSearch.
whileque:
depth,node,_data,_label=que.pop(0)

#Terminateloopiftreedepthismorethanmax_depth.
ifdepth>max_depth:
depth-=1
break

#Stopsplitwhennumberofnodesamplesislessthan
#min_samples_splitorNodeis100%pure.
iflen(_label)<min_samples_splitorall(_label==label[0]):
continue

#Stopsplitifnofeaturehasmorethan2uniqueelements.
_node=self._choose_feature(_data,_label)
if_node.splitisNone:
continue

#Copytheattributesof_nodetonode.
node.copy(_node)

#Putchildrenofcurrentnodeinque.
idx_left=(_data[:,node.feature]<node.split)
idx_right=(_data[:,node.feature]>=node.split)
que.append(
(depth+1,node.left,_data[idx_left],_label[idx_left]))
que.append(
(depth+1,node.right,_data[idx_right],_label[idx_right]))

#Updatetreedepthandrules.
self.depth=depth
self.get_rules()

defpredict_one(self,row:ndarray)->float:
"""Auxiliaryfunctionofpredict.
Arguments:
row{ndarray}--Asampleoftestingdata.
Returns:
float--Predictionoflabel.
"""

node=self.root
whilenode.leftandnode.right:
ifrow[node.feature]<node.split:
node=node.left
else:
node=node.right

returnnode.avg

defpredict(self,data:ndarray)->ndarray:
"""Getthepredictionoflabel.
Arguments:
data{ndarray}--Testingdata.
Returns:
ndarray--Predictionoflabel.
"""

returnnp.apply_along_axis(self.predict_one,1,data)

=gbdt_base.py=gbdt基類:

#-*-coding:utf-8-*-
fromtypingimportDict,List

importnumpyasnp
fromnumpyimportndarray
fromnumpy.randomimportchoice
fromregression_treeimportNode,RegressionTree
classGradientBoostingBase:
"""GBDTbaseclass.
http://statweb.stanford.edu/~jhf/ftp/stobst.pdf
Attributes:
trees{list}:AlistofRegressionTreeobjects.
lr{float}:Learningrate.
init_val{float}:Initialvaluetopredict.
"""

def__init__(self):
self.trees=None
self.learning_rate=None
self.init_val=None

def_get_init_val(self,label:ndarray):
"""Calculatetheinitialpredictionofy.
Arguments:
label{ndarray}--Targetvalues.
Raises:
NotImplementedError
"""

raiseNotImplementedError


@staticmethod
def_match_node(row:ndarray,tree:RegressionTree)->Node:
"""Findtheleafnodethatthesamplebelongsto.
Arguments:
row{ndarray}--Sampleoftrainingdata.
tree{RegressionTree}
Returns:
Node
"""

node=tree.root
whilenode.leftandnode.right:
ifrow[node.feature]<node.split:
node=node.left
else:
node=node.right
returnnode

@staticmethod
def_get_leaves(tree:RegressionTree)->List[Node]:
"""Getsallleafnodesofaregressiontree.
Arguments:
tree{RegressionTree}
Returns:
List[Node]--AlistofRegressionTreeobjects.
"""

nodes=[]
que=[tree.root]
whileque:
node=que.pop(0)
ifnode.leftisNoneornode.rightisNone:
nodes.append(node)
continue

que.append(node.left)
que.append(node.right)

returnnodes

def_divide_regions(self,tree:RegressionTree,nodes:List[Node],
data:ndarray)->Dict[Node,List[int]]:
"""Divideindexesofthesamplesintocorrespondingleafnodes
oftheregressiontree.
Arguments:
tree{RegressionTree}
nodes{List[Node]}--AlistofNodeobjects.
data{ndarray}--Trainingdata.
Returns:
Dict[Node,List[int]]--e.g.{node1:[1,3,5],node2:[2,4,6]...}
"""

regions={node:[]fornodeinnodes}#type:Dict[Node,List[int]]
fori,rowinenumerate(data):
node=self._match_node(row,tree)
regions[node].append(i)

returnregions

@staticmethod
def_get_residuals(label:ndarray,prediction:ndarray)->ndarray:
"""Updateresidualsforeachiteration.
Arguments:
label{ndarray}--Targetvalues.
prediction{ndarray}--Predictionoflabel.
Returns:
ndarray--residuals
"""

returnlabel-prediction

def_update_score(self,tree:RegressionTree,data:ndarray,prediction:ndarray,
residuals:ndarray):
"""updatethescoreofregressiontreeleafnode.
Arguments:
tree{RegressionTree}
data{ndarray}--Trainingdata.
prediction{ndarray}--Predictionoflabel.
residuals{ndarray}
Raises:
NotImplementedError
"""

raiseNotImplementedError

deffit(self,data:ndarray,label:ndarray,n_estimators:int,learning_rate:float,
max_depth:int,min_samples_split:int,subsample=None):
"""Buildagradientboostdecisiontree.
Arguments:
data{ndarray}--Trainingdata.
label{ndarray}--Targetvalues.
n_estimators{int}--numberoftrees.
learning_rate{float}--Learningrate.
max_depth{int}--Themaximumdepthofthetree.
min_samples_split{int}--Theminimumnumberofsamplesrequired
tosplitaninternalnode.
KeywordArguments:
subsample{float}--Subsampleratewithoutreplacement.
(default:{None})
"""

#Calculatetheinitialpredictionofy.
self.init_val=self._get_init_val(label)
#Initializeprediction.
n_rows=len(label)
prediction=np.full(label.shape,self.init_val)
#Initializetheresiduals.
residuals=self._get_residuals(label,prediction)

#TrainRegressionTrees
self.trees=[]
self.learning_rate=learning_rate
idx=range(n_rows)
data_sub=data[idx]
for_inrange(n_estimators):
residuals_sub=residuals[idx]
prediction_sub=prediction[idx]
#TrainaRegressionTreebysub-sampleofX,residuals
tree=RegressionTree()
tree.fit(data_sub,residuals_sub,max_depth,min_samples_split)
#Updatescoresoftreeleafnodes
#self._update_score(tree,data_sub,prediction_sub,residuals_sub)
#Updateprediction
prediction=prediction+learning_rate*tree.predict(data)
#Updateresiduals
residuals=self._get_residuals(label,prediction)
self.trees.append(tree)

defpredict_one(self,row:ndarray)->float:
"""Auxiliaryfunctionofpredict.
Arguments:
row{ndarray}--Asampleoftrainingdata.
Returns:
float--Predictionoflabel.
"""

#Sumpredictionwithresidualsofeachtree.
residual=np.sum([self.learning_rate*tree.predict_one(row)
fortreeinself.trees])

returnself.init_val+residual
pass

=gbdt_regressor.py=gbdt實現程式碼:

#-*-coding:utf-8-*-

importnumpyasnp
fromnumpyimportndarray
fromgbdt_baseimportGradientBoostingBase

classEXGradientBoostingRegressor(GradientBoostingBase):
"""GradientBoostingRegressor"""

def_get_init_val(self,label:ndarray):
"""Calculatetheinitialpredictionofy
SetMSEaslossfunction,yi<-y,andcisaconstant:
L=MSE(y,c)=Sum((yi-c)^2)/n
Getderivativeofc:
dL/dc=Sum(-2*(yi-c))/n
dL/dc=-2*(Sum(yi)/n-Sum(c)/n)
dL/dc=-2*(Mean(yi)-c)
Letderivativeequalstozero,thenwegetinitialconstantvalue
tominimizeMSE:
-2*(Mean(yi)-c)=0
c=Mean(yi)
----------------------------------------------------------------------------------------
Arguments:
label{ndarray}--Targetvalues.
Returns:
float
"""

returnlabel.mean()

def_update_score(self,tree,data:ndarray,prediction:ndarray,residuals:ndarray):
"""updatethescoreofregressiontreeleafnode
Fm(xi)=Fm-1(xi)+fm(xi)
LossFunction:
Loss(yi,Fm(xi))=Sum((yi-Fm(xi))^2)/n
Taylor1st:
f(x+x_delta)=f(x)+f'(x)*x_delta
f(x)=g'(x)
g'(x+x_delta)=g'(x)+g"(x)*x_delta
1stderivative:
Loss'(yi,Fm(xi))=-2*Sum(yi-Fm(xi))/n
2ndderivative:
Loss"(yi,Fm(xi))=2
So,
Loss'(yi,Fm(xi))=Loss'(yi,Fm-1(xi)+fm(xi))
=Loss'(yi,Fm-1(xi))+Loss"(yi,Fm-1(xi))*fm(xi)=0
fm(xi)=-Loss'(yi,Fm-1(xi))/Loss"(yi,Fm-1(xi))
fm(xi)=2*Sum(yi-Fm-1(xi)/n/2
fm(xi)=Sum(yi-Fm-1(xi))/n
fm(xi)=Mean(yi-Fm-1(xi))
----------------------------------------------------------------------------------------
Arguments:
tree{RegressionTree}
data{ndarray}--Trainingdata.
prediction{ndarray}--Predictionoflabel.
residuals{ndarray}
"""

pass

defpredict(self,data:ndarray)->ndarray:
"""Getthepredictionoflabel.
Arguments:
data{ndarray}--Trainingdata.
Returns:
ndarray--Predictionoflabel.
"""

returnnp.apply_along_axis(self.predict_one,axis=1,arr=data)

=run.py=測試程式碼:

fromsklearnimportdatasets
importnumpyasnp
fromsklearn.utilsimportshuffle
fromgbdt_regressorimportEXGradientBoostingRegressor
defloaddata(splitset=0.9):
boston=datasets.load_boston()
X,y=shuffle(boston.data,boston.target,random_state=13)
X=X.astype(np.float32)
offset=int(X.shape[0]*splitset)
X_train,y_train=X[:offset],y[:offset]
X_test,y_test=X[offset:],y[offset:]
returnX_train,y_train,X_test,y_test

defmygbdt():
X_train,y_train,X_test,y_test=loaddata()
gbdt=EXGradientBoostingRegressor()
gbdt.fit(data=X_train,label=y_train,n_estimators=500,
learning_rate=0.01,max_depth=5,min_samples_split=2)
y_=gbdt.predict(X_test)
total_err=0
foriinrange(y_.shape[0]):
print('predict=%0.2freal=%0.2f'%(y_[i],y_test[i]))
total_err+=(y_[i]-y_test[i])**2
print('total_err=%0.2f'%(total_err/y_test.shape[0]))

if__name__=="__main__":
mygbdt()
print('-'*100)

二、GBDT實現分類

2.1 GBDT實現二分類原理

從GBDT實現迴歸可以看出,CART樹的葉子節點值(得分)是一個實數,GBDT實現迴歸時,得分值取平均值即可,而分類問題常用交叉熵作為損失函式,這就需要把實數變換為概率,邏輯迴歸一章中曾介紹過,利用Sigmod函式可把實數變為概率形式:

Sigmod函式的導數為:

與實現迴歸時一樣,設第m輪CART分類樹:

編號i的資料概率為:

對於二分類問題,編號i的實際概率yi等於0或1,設有n個數據,以交叉熵作為損失函式有:

(2.1)

對於一個有二階導數函式,可先展開為一階泰勒級數形式:

兩邊求導的得:

將(2.1)式中的Fm-1(x)看成上式中的x,T(x,θ)看成上式中的△x,則損失函式一階導數可表示為:

(2.2)

接下來通過求導法則求出:

(2.2.1)

與迴歸問題一樣,(2.2.1)中出現分類問題的殘差:yi-pi,殘差與一階導數即梯度也存在著協同、對應的關係,接下來求二階導數:

(2.2.2)

當(2.2)式等於0時,損失函式取得最小值,將(2.2.1)和(2.2.2)帶入(2.2)有:

這樣就得到CART分類樹的表達形式,在程式碼實現中用下面的公式對葉節點打分:

(2.3)

(2.3)式的分子可以寫成殘差形式:

通過以上推導再來梳理一下GBDT分類演算法中殘差、梯度、決策樹三者之間的關係,由(2.2.1)式可以看出殘差與梯度是等效的,降低殘差即可實現降低梯度,反之亦然;而通過求導損失函式並取0時得到的分類樹的函式形式,當每輪的CART樹滿足該形式時可有效的降低梯度以及殘差,GBDT實現分類或迴歸都是利用三者之間的等效協同性,歸納一下:GBDT本質與其他優化問題一樣,依然是降低梯度,而梯度的表現形式是殘差,通過迭代CART樹不斷縮小殘差範圍,間接的降低梯度直至到極值點。

利用公式(2.2.1)可以得到分類樹的初始值:F0(x),迴歸問題中初始值取目標值的平均數作為初始值,在分類演算法中,目標值是0或1且使用交叉熵函式作為損失函式,這裡需要稍微處理下:與迴歸問題一樣,分類時初始值希望用一個值來作為預測值,不妨設用一個待求引數概率p表示,p帶入(2.2.1)有:

餘下文章請轉至連結 :GBDT-梯度提升決策樹