姓名:唐來賓? 學號:17101223417
轉載http://mp.weixin.qq.com/s/-CSSh2ptYXmNXzYYjCkYbQ
【嵌牛導讀】本文主要介紹決策樹用于回歸問題的相關算法實現,其中包括回歸樹(regression tree)和模型樹(model tree)的實現,并介紹了預剪枝(preprune)和后剪枝(postprune)的防止樹過擬合的技術以及實現。最后對回歸樹和標準線性回歸進行了對比。
【嵌牛鼻子】樹回歸 決策
【嵌牛提問】如何優化樹回歸?
【嵌牛正文】前言
最近由于開始要把精力集中在課題的應用上面了,這篇總結之后算法原理的學習先告一段落。本文主要介紹決策樹用于回歸問題的相關算法實現,其中包括回歸樹(regression tree)和模型樹(model tree)的實現,并介紹了預剪枝(preprune)和后剪枝(postprune)的防止樹過擬合的技術以及實現。最后對回歸樹和標準線性回歸進行了對比。
正文
在之前的文章中我總結了通過使用構建決策樹來進行類型預測。直觀來看樹結構最容易對分類問題進行處理,通過遞歸我們在數據中選取最佳分割特征對訓練數據進行分割并進行樹分裂最終到達觸底條件獲得訓練出來決策樹,可以通過可視化的方式直觀的查看訓練模型并對數據進行分類。
通常決策樹樹分裂選擇特征的方法有ID3, C4.5算法, C5.0算法和CART樹。在《機器學習算法實踐-決策樹(Decision Tree)》中對ID3以及C4.5算法進行了介紹并使用ID3算法處理了分類問題。本文主要使用決策樹解決回歸問題,使用CART(Classification And Regression Trees)算法。
CART
CART是一種二分遞歸分割的技術,分割方法采用基于最小距離的基尼指數估計函數,將當前的樣本集分為兩個子樣本集,使得生成的的每個非葉子節點都有兩個分支。因此,CART算法生成的決策樹是結構簡潔的二叉樹。
分類樹是針對目標變量是離散型變量,通過二叉樹將數據進行分割成離散類的方法。而回歸樹則是針對目標變量是連續性的變量,通過選取最優分割特征的某個值,然后數據根據大于或者小于這個值進行劃分進行樹分裂最終生成回歸樹。
特征和最佳分割點的選取
在使用決策樹解決回歸問題中我們需要不斷的選取某一特征的一個值作為分割點來生成子樹。選取的標準就是使得被分割的兩部分數據能有最好的純度。
對于離散型數據我們可以通過計算分割兩部分數據的基尼不純度的變化來判定最有分割點;
對于連續性變量我們通過計算最小平方殘差,也就是選擇使得分割后數據方差變得最小的特征和分割點。直觀的理解就是使得分割的兩部分數據能夠有最相近的值。
樹分裂的終止條件
有了選取分割特征和最佳分割點的方法,樹便可以依此進行分裂,但是分裂的終止條件是什么呢?
節點中所有目標變量的值相同, 既然都已經是相同的值了自然沒有必要在分裂了,直接返回這個值就好了.
樹的深度達到了預先指定的最大值
不純度的減小量小于預先定好的閾值,也就是之進一步的分割數據并不能更好的降低數據的不純度的時候就可以停止樹分裂了。
節點的數據量小于預先定好的閾值
回歸樹的Python實現
本部分使用Python實現簡單的回歸樹,并對給定的數據進行回歸并可視化回歸曲線和樹結構。完整代碼詳見: https://github.com/PytLab/MLBox/tree/master/classification_and_regression_trees
首先是加載數據的部分,這里的所有測試數據我均使用的《Machine Learning in Action》中的數據,格式比較規整加載方式也比較一致, 這里由于做樹回歸,自變量和因變量都放在同一個二維數組中:
defload_data(filename):
''' 加載文本文件中的數據.
'''
dataset=[]
withopen(filename,'r')asf:
forlineinf:
line_data=[float(data)fordatainline.split()]
dataset.append(line_data)
returndataset
樹回歸中再找到分割特征和分割值之后需要將數據進行劃分以便構建子樹或者葉子節點:
defsplit_dataset(dataset,feat_idx,value):
''' 根據給定的特征編號和特征值對數據集進行分割
'''
ldata,rdata=[],[]
fordataindataset:
ifdata[feat_idx]
ldata.append(data)
else:
rdata.append(data)
returnldata,rdata
然后就是重要的選取最佳分割特征和分割值了,這里我們通過找到使得分割后的方差最小的分割點最為最佳分割點:
defchoose_best_feature(dataset,fleaf,ferr,opt):
''' 選取最佳分割特征和特征值
dataset: 待劃分的數據集
fleaf: 創建葉子節點的函數
ferr: 計算數據誤差的函數
opt: 回歸樹參數.
err_tolerance: 最小誤差下降值;
n_tolerance: 數據切分最小樣本數
'''
dataset=np.array(dataset)
m,n=dataset.shape
err_tolerance,n_tolerance=opt['err_tolerance'],opt['n_tolerance']
err=ferr(dataset)
best_feat_idx,best_feat_val,best_err=0,0,float('inf')
# 遍歷所有特征
forfeat_idxinrange(n-1):
values=dataset[:,feat_idx]
# 遍歷所有特征值
forvalinvalues:
# 按照當前特征和特征值分割數據
ldata,rdata=split_dataset(dataset.tolist(),feat_idx,val)
iflen(ldata)
# 如果切分的樣本量太小
continue
# 計算誤差
new_err=ferr(ldata)+ferr(rdata)
ifnew_err
best_feat_idx=feat_idx
best_feat_val=val
best_err=new_err
# 如果誤差變化并不大歸為一類
ifabs(err-best_err)
returnNone,fleaf(dataset)
# 檢查分割樣本量是不是太小
ldata,rdata=split_dataset(dataset.tolist(),best_feat_idx,best_feat_val)
iflen(ldata)
returnNone,fleaf(dataset)
returnbest_feat_idx,best_feat_val
其中,停止選取的條件有兩個: 一個是當分割的子數據集的大小小于一定值;一個是當選取的最佳分割點分割的數據的方差減小量小于一定的值。
fleaf是創建葉子節點的函數引用,不同的樹結構此函數也是不同的,例如本部分的回歸樹,創建葉子節點就是根據分割后的數據集平均值,而對于模型樹來說,此函數返回值是根據數據集得到的回歸系數。ferr是計算數據集不純度的函數,不同的樹模型該函數也會不同,對于回歸樹,此函數計算數據集的方差來判定數據集的純度,而對于模型樹來說我們需要計算線性模型擬合程度也就是線性模型的殘差平方和。
然后就是最主要的回歸樹的生成函數了,樹結構肯定需要通過遞歸創建的,選不出新的分割點的時候就觸底:
defcreate_tree(dataset,fleaf,ferr,opt=None):
''' 遞歸創建樹結構
dataset: 待劃分的數據集
fleaf: 創建葉子節點的函數
ferr: 計算數據誤差的函數
opt: 回歸樹參數.
err_tolerance: 最小誤差下降值;
n_tolerance: 數據切分最小樣本數
'''
ifoptisNone:
opt={'err_tolerance':1,'n_tolerance':4}
# 選擇最優化分特征和特征值
feat_idx,value=choose_best_feature(dataset,fleaf,ferr,opt)
# 觸底條件
iffeat_idxisNone:
returnvalue
# 創建回歸樹
tree={'feat_idx':feat_idx,'feat_val':value}
# 遞歸創建左子樹和右子樹
ldata,rdata=split_dataset(dataset,feat_idx,value)
ltree=create_tree(ldata,fleaf,ferr,opt)
rtree=create_tree(rdata,fleaf,ferr,opt)
tree['left']=ltree
tree['right']=rtree
returntree
使用回歸樹對數據進行回歸
這里使用了現成的分段數據作為訓練數據生成回歸樹,本文所有使用的數據詳見: https://github.com/PytLab/MLBox/tree/master/classification_and_regression_trees
可視化數據點
dataset=load_data('ex0.txt')
dataset=np.array(dataset)
# 繪制散點
plt.scatter(dataset[:,0],dataset[:,1])
創建回歸樹并可視化
看到這種分段的數據,回歸樹擬合它可是最合適不過了,我們創建回歸樹:
tree=create_tree(dataset,fleaf,ferr,opt={'n_tolerance':4,
'err_tolerance':1})
通過Python字典表示的回歸樹結構:
{'feat_idx':0,
'feat_val':0.40015800000000001,
'left':{'feat_idx':0,
'feat_val':0.20819699999999999,
'left': -0.023838155555555553,
'right':1.0289583666666666},
'right':{'feat_idx':0,
'feat_val':0.609483,
'left':1.980035071428571,
'right':{'feat_idx':0,
'feat_val':0.81674199999999997,
'left':2.9836209534883724,
'right':3.9871631999999999}}}
這里我還是使用Graphviz來可視化回歸樹,類似之前決策樹做分類的文章中的dotify函數,這里稍微修改下葉子節點的label,我們便可以遞歸得到決策樹對應的dot文件, dotify函數的實現見:https://github.com/PytLab/MLBox/blob/master/classification_and_regression_trees/regression_tree.py#L159
然后獲取樹結構圖:
datafile='ex0.txt'
dotfile='{}.dot'.format(datafile.split('.')[0])
withopen(dotfile,'w')asf:
content=dotify(tree)
f.write(content)
生成回歸樹圖片:
dot -Tpng ex0.dot -o ex0_tree.png
其中節點上數字代表:特征編號: 特征分割值
繪制回歸樹回歸曲線
有了回歸樹,我們便可以繪制回歸樹回歸曲線,看看它對于分段數據是否能有較好的回歸效果:
# 繪制回歸曲線
x=np.linspace(0,1,50)
y=[tree_predict([i],tree)foriinx]
plt.plot(x,y,c='r')
plt.show()
樹剪枝
在介紹樹剪枝之前先使用上一部分的代碼對兩組類似的數據進行回歸,可視化后的數據以及回歸曲線如下(數據文件左&數據文件右):
左右兩邊的數據的分布基本相同但是使用相同的參數得到的回歸樹卻完全不同左邊的回歸樹只有兩個分支,而右邊的分支則有很多,甚至有時候會為所有的數據點得到一個分支,這樣回歸樹將會非常的龐大, 如下是可視化得到的兩個回歸樹:
如果一棵樹的節點過多則表明該模型可能對數據進行了“過擬合”。那么我們需要降低決策樹的復雜度來避免過擬合,此過程就是剪枝。剪枝技術又分為預剪枝和后剪枝。
預剪枝
預剪枝是在生成決策樹之前通過改變參數然后在樹生成的過程中進行的。比如在上文中我們創建回歸樹的函數中有個opt參數,其中包含n_tolerance和err_tolerance,他們可以控制何時停止樹的分裂,當增大葉子節點的最小數據量以及增大誤差容忍度,樹的分裂也會越提前的終止。當我們把誤差變化容忍度增加到2000的時候得到的回歸樹以及回歸曲線可視化如下:
后剪枝
預剪枝技術需要用于預先指定參數,但是后剪枝技術則是通過測試數據來自動進行剪枝不需要用戶干預因此是一種更理想的剪枝技術,但是我們需要寫剪枝函數來處理。
后剪枝的大致思想就是我們針對一顆子樹,嘗試將其左右子樹(節點)合并,通過測試數據計算合并前后的方差,如果合并后的方差比合并前的小,這說明可以合并此子樹。
對樹進行塌陷處理: 我們對一棵樹進行塌陷處理,就是遞歸將這棵樹進行合并返回這棵樹的平均值。
defcollapse(tree):
''' 對一棵樹進行塌陷處理, 得到給定樹結構的平均值
'''
ifnot_tree(tree):
returntree
ltree,rtree=tree['left'],tree['right']
return(collapse(ltree)+collapse(rtree))/2
后剪枝的Python實現:
defpostprune(tree,test_data):
''' 根據測試數據對樹結構進行后剪枝
'''
ifnot_tree(tree):
returntree
# 若沒有測試數據則直接返回樹平均值
ifnottest_data:
returncollapse(tree)
ltree,rtree=tree['left'],tree['right']
ifnot_tree(ltree)andnot_tree(rtree):
# 分割數據用于測試
ldata,rdata=split_dataset(test_data,tree['feat_idx'],tree['feat_val'])
# 分別計算合并前和合并后的測試數據誤差
err_no_merge=(np.sum((np.array(ldata)-ltree)**2)+
np.sum((np.array(rdata)-rtree)**2))
err_merge=np.sum((np.array(test_data)-(ltree+rtree)/2)**2)
iferr_merge
print('merged')
return(ltree+rtree)/2
else:
returntree
tree['left']=postprune(tree['left'],test_data)
tree['right']=postprune(tree['right'],test_data)
returntree
我們看一下不對剛才的樹進行預剪枝而是使用測試數據進行后剪枝的效果:
data_test=load_data('ex2test.txt')
pruned_tree=postprune(tree,data_test)
merged
merged
merged
merged
merged
merged
merged
merged
通過輸出可以看到總共進行了8次剪枝操作,通過把剪枝前和剪枝后的樹可視化對比下看看:
樹的規模的確是減小了。
模型樹
上一部分葉子節點上放的是分割后數據的平均值并以他作為滿足條件的樣本的預測值,本部分我們將在葉子節點上放一個線性模型來做預測。也就是指我們的樹是由多個線性模型組成的,顯然會比強行用平均值來建模更有優勢。
模型樹使用多個線性函數來做回歸比用多個平均值組成一棵大樹的模型更有可解釋性
而且線性模型的使用可以使樹的規模減小,畢竟平均值的覆蓋范圍只是局部的,而線性模型可以覆蓋所有具有線性關系的數據。
模型樹也具有更高的預測準確度
創建模型樹
模型樹和回歸樹的思想是完全一致的,只是在生成葉子節點的方法以及計算數據誤差(不純度)的方式不同。在模型樹里針對一個葉子節點我們需要使用分割到的數據進行線性回歸得到線性回歸系數而不是簡單的計算數據的平均值。不純度的計算也不是簡單的計算數據的方差,而是計算線性模型的殘差平方和。
為了能為葉子節點計算線性模型,我們還需要實現一個標準線性回歸函數linear_regression, 相應模型樹的ferr和fleaf的Python實現
deflinear_regression(dataset):
''' 獲取標準線性回歸系數
'''
dataset=np.matrix(dataset)
# 分割數據并添加常數列
X_ori,y=dataset[:,:-1],dataset[:,-1]
X_ori,y=np.matrix(X_ori),np.matrix(y)
m,n=X_ori.shape
X=np.matrix(np.ones((m,n+1)))
X[:,1:]=X_ori
# 回歸系數
w=(X.T*X).I*X.T*y
returnw,X,y
deffleaf(dataset):
''' 計算給定數據集的線性回歸系數
'''
w,_,_=linear_regression(dataset)
returnw
defferr(dataset):
''' 對給定數據集進行回歸并計算誤差
'''
w,X,y=linear_regression(dataset)
y_prime=X*w
returnnp.var(y_prime-y)
在分段線性數據上應用模型樹
本部分使用了事先準備好的分段線性數據來構建模型樹,數據點可視化如下:
現在我們使用這些數據構建一個模型樹:
tree=create_tree(dataset,fleaf,ferr,opt={'err_tolerance':0.1,'n_tolerance':4})
tree
得到的樹結構:
{'feat_idx':0,
'feat_val':0.30440099999999998,
'left':matrix([[3.46877936],
[1.18521743]]),
'right':matrix([[1.69855694e-03],
[1.19647739e+01]])}
可視化:
繪制回歸曲線:
可以通過模型樹看到對于此數據只需要兩個分支,數的深度也只有2層。
當x<0.304的時候,使用線性模型y=3.47+1.19x來回歸
當x>0.304的時候,使用線性模型y=0.0017+1.20x來回歸
回歸樹與線性回歸的對比
本部分我們使用標準線性回歸和回歸樹分別對同一組數據進行回歸,并使用同一組測試數據計算相關系數(Correlation Coefficient)對兩種模型的回歸效果進行對比。
數據我還是使用《Machinie Learning in Action》中的現成數據,數據可視化如下:
現在我們分別使用標準線性回歸和回歸樹對該數據進行回歸,并計算模型預測值和測試樣本的相關系數R2R2(完整代碼見:https://github.com/PytLab/MLBox/blob/master/classification_and_regression_trees/compare.py)
相關系數計算:
defget_corrcoef(X,Y):
# X Y 的協方差
cov=np.mean(X*Y)-np.mean(X)*np.mean(Y)
returncov/(np.var(X)*np.var(Y))**0.5
獲得的相關系數:
linear regression correlationcoefficient:0.9434684235674773
regression tree correlationcoefficient:0.9780307932704089
繪制線性回歸和樹回歸的回歸曲線(黃色會樹回歸曲線,紅色會線性回歸):
可見樹回歸方法在預測復雜數據的時候會比簡單的線性模型更有效。
總結
本文對決策樹用于連續數值的回歸預測進行了介紹,并實現了回歸樹, 剪枝和模型樹以及相應的樹結構輸出可視化等。對于模型樹也給予了相應的Python實現并針對分段線性數據進行了回歸測試。最后并對回歸樹模型和簡單的標準線性回歸模型進行了對比。
參考
《Machine Learning in Action》
CART分類與回歸樹的原理與實現