一、分類樹構建存在的問題
1. 切分過於迅速
假定當前葉子節點選擇特征A來分割數據,那么數據A將不再后續的葉子節點中起作用,這樣就會造成切分過於迅速
2. 不能處理連續數據
想要處理連續型數據,必須先將連續性數據轉化成離散型數據。CART就是特別有名的利用二分法來處理連續性變量的樹形算法。將度量方法稍作修改,就可以實現回歸樹。
二、混亂度
分類樹中我們用信息熵和信息增益來決定最優划分屬性,實際上信息增益的意義就是,選擇這個划分屬性能夠達相對最好的分類效果,也就是給定節點時區計算數據的混亂度。
對於連續型數據來說,混亂度的衡量方法很簡單,計算所有數據的均值,統計出所有數據到均值點的差值,為了將正負同等看待,一般使用平方值或者是絕對值來代替。
三、偽代碼
for 遍歷 特征
for 遍歷 特征值(我們可以選擇連續兩個數的平均值)
將數據切分成兩部分
計算切分后的誤差(混亂度)
if 判斷 誤差和歷史最小誤差比較
該誤差較小 存儲特征和特征值並更新歷史最小誤差
if 判斷 誤差減少較少 or 數據可供分割項較少(當誤差設定為0,可供分割項選1的時候,可以生成最大樹)
結束回歸樹構建
結束所有遍歷,返回最佳特征和特征值(切分目標特征和閾值)
四、數值划分代碼
#-*-coding:utf-8-*- ## 引入numpy包 from numpy import * ## 加載數據 def loadDataSet(fileName): # 按照數據應該是3 numFeat=len(open(fileName).readline().split('\t')) dataMat=[] fr=open(fileName) # for line in fr.readlines(): # curLine=line.strip().split('\t') # # map(float,curLine)用來將每行的內容保存成一組浮點數,map形式 # fltLine=map(float,curLine) # dataMat.append(fltLine) # return dataMat for line in fr.readlines(): lineArr=[] curLine=line.strip().split('\t') for i in range(numFeat): lineArr.append(float(curLine[i])) dataMat.append(lineArr) return dataMat ## 通過數組過濾的方式來集合且分為兩個子集合 參數:數據集合,待切分的特征,該特征的某個值 def binSplitDataSet(dataSet,feature,value): # 大於小於得到的是一個布爾值,nonzero是根據布爾值得到相應的坐標,[0]指的是橫坐標,仔細想想就知道了 mat0=dataSet[nonzero(dataSet[:,feature]>value)[0],:] mat1=dataSet[nonzero(dataSet[:,feature]<=value)[0],:] return mat0,mat1 ## 均值函數和方差函數mean&var # 葉子節點的確定 其實簡單來說就是求均值嘛 def regLeaf(dataSet): return mean(dataSet[:,-1]) # 計算目標變量的平方誤差,因為這里用到的是總誤差,所以乘上了樣本個數 def regErr(dataSet): return var(dataSet[:,-1])*shape(dataSet)[0] ## 用最佳方式分割數據集,並生成相應的葉子節點 # 第一個返回值為None的時候不會再進行切分 def chooseBestSplit(dataSet,leafType=regLeaf,errType=regErr,ops=(0,4)): # 用戶指定 控制函數停止時機 (容許的誤差下降值,切分的最少樣本數) tolS=ops[0] tolN=ops[1] # 如果目標變量是相同值,則返回變量 if len(set(dataSet[:,-1].T.tolist()[0]))==1: print ("aaa") print (len(set(dataSet[:,-1].T.tolist()[0]))) return None,leafType(dataSet) m,n=shape(dataSet) S=errType(dataSet) bestS=inf bestIndex=0 bestValue=0 # 針對每個特征 for featIndex in range(n-1): # 這里必須要這樣 不要問為什么 for splitVal in set(dataSet[:,featIndex].T.tolist()[0]): # 調用函數binSplitDataSet 將數據按照上一層結果進行簡單分類 mat0,mat1=binSplitDataSet(dataSet,featIndex,splitVal) if (shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN): continue newS=errType(mat0)+errType(mat1) if newS<bestS: bestIndex=featIndex bestValue=splitVal bestS=newS # 根據運行結果找到合適的葉子節點 feat & val # 當S和bestS之間差距滿足最小下降值 則不再進行切分 if (S-bestS)<tolS: print ("bbb") print (S,bestS,tolS) return None,leafType(dataSet) # 當兩邊的切分樣本 有一方小於4個 則不再進行切分 if(shape(mat0)[0]<tolN) or (shape(mat1)[0]<tolN): print ("ccc") print (shape(mat0)[0]) print (shape(mat1)[0]) print (tolN) return None,leafType(dataSet) return bestIndex,bestValue ## 創建樹形結構 參數(數據集,創建葉子節點的函數,計算誤差的函數,(用戶定義)樹構建所需元祖) def createTree(dataSet,leafType=regLeaf,errType=regErr,ops=(0,4)): feat,val=chooseBestSplit(dataSet,leafType,errType,ops) # 直到無法區分為止 if feat==None: return val # 定義返回值--二叉樹 retTree={} # 定義分支節點的屬性和值 retTree['spInd']=feat retTree['spVal']=val # 用binSplitDataSet將數據分割成兩個部分 lSet,rSet=binSplitDataSet(dataSet,feat,val) # 遞歸方法,重復創建二叉樹,直到無法區分位置 retTree['left']=createTree(lSet,leafType,errType,ops) retTree['right']=createTree(rSet,leafType,errType,ops) return retTree
五、剪枝
為了解決過多的復雜度而帶來的過擬合問題,預剪枝和后剪枝的存在可以極大地避免決策樹陷入過擬合。
離散數據(連續數據)的預剪枝和后剪枝和標稱型數據的剪枝處理不太一樣,因為標稱型的數據分類絕對會到達分支,並且存在錯誤節點。但是由於離散數據在某個節點的某個特征分割之后,還可以在這個特征進一步分割,導致永遠的划分都是正確的,永遠都不會停止,所以它的預剪枝和后剪枝並不一樣。
離散數據的預剪枝可以采用終止條件,這個終止條件可以是判斷誤差、也可以是判斷當前節點達到什么程度,都可以。但是值得注意的是,當當前所有結點都是同一類數據的時候,直接跳出就成,沒必要進行比較喲。
因為每次划分都會比原先的准確率更高,所以后剪枝通常伴隨着訓練集和測試集來進行,通過測試機將訓練集訓練出來的足夠大的樹進行剪枝。從上置下的尋找葉節點(但實際上,程序運行時從下向上進行合並),如果合並后會降低誤差,那么就將葉節點進行合並。
六、后剪枝代碼(相對多出的代碼)
## 利用字典屬性來判斷這個有沒有子節點 def isTree(obj): # 是的話,就返回true,表明這個是個有葉子節點的節點 return (type(obj).__name__=='dict') ## 遞歸方式獲取當前值 def getMean(tree): if isTree(tree['left']): tree['left']=getMean(tree['left']) if isTree(tree['right']): tree['right']=getMean(tree['right']) return (tree['left']+tree['right'])/2.0 ## 剪枝處理 def prune(tree, testData): # 如果test的目標值為0,那么根據數結構,獲取左右數據 if shape(testData)[0]==0: return getMean(tree) # 如果左右子樹存在的話,那么就將測試集合進行拆分喲 if (isTree(tree['right']) or isTree(tree['left'])): # 進行分割數據 lSet,rSet=binSplitDataSet(testData,tree['spInd'],tree['spVal']) # 左、右子樹存在,就進行繼續剪枝驗證 if isTree(tree['left']): # 更新樹 tree['left']=prune(tree['left'],lSet) if isTree(tree['right']): # 更新樹 tree['right']=prune(tree['right'],rSet) # 找到可以合並的點,即左右都存在的時刻,如果拆分后的比拆分前的准確率還要低,那么合並 if not isTree(tree['left']) and not isTree(tree['right']): # 拆分數據,然后進行對比 lSet,rSet=binSplitDataSet(testData,tree['spInd'],tree['spVal']) # 合並后的節點存儲值 treeMean=(tree['left']+tree['right'])/2.0 # 重新生成合並前的和合並后的誤差值,進行判斷 errorNoMerge=sum(power(lSet[:,-1]-tree['left'],2))+sum(power(rSet[:,-1]-tree['right'],2)) errorMerge=sum(power(testData[:,-1]-treeMean,2)) # if errorMerge<errorNoMerge: print "merging" return treeMean else: return tree else: return tree
七、模型樹
回歸樹就是之前提到的用節點來進行划分,模型樹是用線性函數來進行划分。回歸樹的葉節點是節點數據標簽值的平均值,而模型樹的節點數據是一個線性模型(可用最簡單的最小二乘法來構建線性模型)。
把一個葉節點定義為線性函數能夠更容易的將樹型結構縮減,其解釋性也是由於回歸樹的特點之一。
回歸樹種用的是均值記錄葉子節點信息,方差和記錄誤差。在模型樹種使用不一樣的概念。
## 線性回歸解決器
def linearSolve(dataSet): m,n=shape(dataSet) # 初始化矩陣,用於簡單的線性回歸
X=mat(ones((m,n))) Y=mat(ones((m,1))) X[:,1:n]=dataSet[:,0:n-1] Y=dataSet[:,-1] xTx=X.T*X # 矩陣的逆不存在會造成異常
if linalg.det(xTx)==0.0: raise NameError("abc") ws=xTx.I*(X.T*Y) return ws,X,Y def modelLeaf(dataSet): ws,X,Y=linearSolve(dataSet) return ws def modelErr(dataSet): ws,X,Y=linearSolve(dataSet) yHat=X*ws return sum(power(Y-yHat,2))
哇,可是線性回歸又是怎么回事呢,別急,代碼中給到的,一會兒會整理。
補充:線性回歸(當然會單列專題來說這個事情)
利用線性模型可以嘗試去搞一個線性組合來進行預測,函數形式如下:

為了讓線性模型盡可能的完善,我們需要盡可能的減少均方誤差:

在線性回歸中,最小二乘法就是嘗試去找一條直線,讓所有的樣本到這個直線上的歐式距離之和最小,也就是上述公式,為了讓其得到最小值,我們通常需要對式子對未知數w向量和求導並設置為0可以進行求解。
為了方便討論我們通常將w向量和b放在一起進行討論,設定一個全新的矩陣D,D的前n-1列恆定為屬性值,最后一列設定初始值為1,實際上就是wx+b的形式,因為b的參數是1嘛,因此得到的誤差函數為:

求導並另其為0,得到線性回歸公式:

這個也就是上面公式的由來,嘖嘖
