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-梯度提升決策樹