1. 程式人生 > >【機器學習】CART分類決策樹+程式碼實現

【機器學習】CART分類決策樹+程式碼實現

1. 基礎知識

CART作為二叉決策樹,既可以分類,也可以迴歸。

分類時:基尼指數最小化。

迴歸時:平方誤差最小化。

資料型別:標值型,連續型。連續型分類時採取“二分法”, 取中間值進行左右子樹的劃分。

2. CART分類樹

特徵A有N個取值,將每個取值作為分界點,將資料D分為兩類,然後計算基尼指數Gini(D,A), 選擇基尼指數小的特徵A的取值。然後對於每個特徵在計算基尼指數,最後得到最佳的特徵的最佳取值作為分支點。

基尼指數表示資料D的不純度,基尼指數越小不純度越小。

\\Gini(D) = 1- \sum_{k=1}^{K}(\frac{|D^k|}{|D|})^2 \\Gini(D,A) = \frac{|D_1|}{|D|}Gini(D_1) + \frac{|D_2|}{|D|}Gini(D_2)

3. CART迴歸樹

切分資料時依據的誤差函式:總方差最小化。

計算屬於該節點的所有樣本的y的均值\mu

, 接著計算總方差,N為屬於該節點的樣本數目:

\sigma = \sum_{i=1}^{N} \sqrt{(y_i - \mu)^2}

特徵A的某個取值val將資料集分成兩個資料集,那麼分支後的誤差為:

\sigma_A = \sigma_{A}^1 + \sigma_{A }^2 = \sum_{i=1}^{N} \sqrt{(y_i^1 - \mu^1)^2} + \sum_{i=1}^{N} \sqrt{(y_i^2 - \mu^2)^2}

每次選擇用於分支的特徵及其對應的值時:遍歷所有的特徵,遍歷每個特徵所有的取值,找出使得誤差最小的特徵及其取值。

4.模型樹

普通迴歸樹:葉子節點上是屬於該節點的樣本的y值的均值;

模型樹:葉子節點上是線性模型。

引入模型樹的原因:有些樣本資料可能用線性模型描述y值比用數值要方便,比如下圖,用兩個線性函式描述這些資料點更為合適。

5. 構建樹的停止條件

誤差閾值:誤差較於分割前的資料集下降不多;

資料集大小閾值:分割出的資料集所包含的資料過少;

分割後的資料集屬於同一類(分類),所有值相等(迴歸);

6.樹的剪枝

預剪枝:邊生成樹邊剪枝,兩種方法:其一是設定一些閾值(比如容許誤差降低的最小值,葉子節點中含有樣本數的最小值),其二利用驗證集,計算該節點分支前後誤差的變化。缺點:其一對資料的數量級較為敏感,可能需要隨時改變閾值大小,其二產生欠擬合。

後剪枝:先用訓練集生成儘可能大的樹,然後利用驗證集(測試集)在這棵樹上進行剪枝。

注意:迴歸樹中,生成樹時,計算誤差用的均值是訓練集中屬於該個分組的y的均值,但是用測試集剪枝時,均值仍舊是之前的訓練集上的均值。

7. 程式碼實現

參考:《機器學習實戰》

原始碼地址以及資料:

https://github.com/JieruZhang/MachineLearninginAction_src

from numpy import *

#載入資料集
def loadData(filename):
    dataMat = []
    fr = open(filename)
    for line in fr.readlines():
        line = line.strip().split('\t')
        for i in range(len(line)):
            line[i] = float(line[i])
        dataMat.append(line)
    return dataMat

#切分資料集,對於特徵屬性feature,以value作為中點,小於value作為資料集1,大於value作為資料集2
def binSplitData(data, feature, value):
    #nonzero,當使用布林陣列直接作為下標物件或者元組下標物件中有布林陣列時,
    #都相當於用nonzero()將布林陣列轉換成一組整數陣列,然後使用整數陣列進行下標運算。
    mat1 = data[nonzero(data[:,feature] > value)[0],:]
    mat2 = data[nonzero(data[:,feature] <= value)[0],:]
    return mat1, mat2

#找到資料切分的最佳位置,遍歷所有特徵及其可能取值找到使誤差最小化的切分閾值
#生成葉子節點,即計算屬於該葉子的所有資料的label的均值(迴歸樹使用總方差)
def regLeaf(data):
    return mean(data[:,-1])

#誤差計算函式:總方差
def regErr(data):
    return var(data[:,-1]) * shape(data)[0]

#最佳切分查詢函式
def chooseBestSplit(data, leafType=regLeaf, errType=regErr, ops=(1,4)):
    #容許的誤差下降值
    tolS = ops[0]
    #切分的最少樣本數
    tolN = ops[1]
    #如果資料的y值都相等,即屬於一個label,則說明已經不用再分了,則返回葉子節點並退出
    if len(set(data[:,-1].T.tolist()[0])) == 1:
        return None, leafType(data)
    #否則,繼續分
    m,n = shape(data)
    #原資料集的誤差
    s = regErr(data)
    #最佳誤差(先設為極大值),最佳誤差對應的特徵的index,和對應的使用的切分值
    best_s =  inf
    best_index = 0
    best_val = 0
    for feat_index in range(n-1):
        for val in set(data[:,feat_index].T.A.tolist()[0]):
            #根據特徵feat_index和其對應的劃分取值val將資料集分開
            mat1, mat2 = binSplitData(data, feat_index, val)
            #若某一個數據集大小小於tolN,則停止該輪迴圈
            if (shape(mat1)[0] < tolN) or (shape(mat2)[0] < tolN):
                continue
            new_s = errType(mat1) + errType(mat2)
            if new_s < best_s:
                best_s = new_s
                best_index = feat_index
                best_val = val
    #如果最佳的誤差相較於總誤差下降的不多,則停止分支,返回葉節點
    if (s-best_s) < tolS:
        return None, leafType(data)
    #如果劃分出來的兩個資料集,存在大小小於tolN的,也停止分支,返回葉節點
    mat1, mat2 = binSplitData(data, best_index, best_val)
    if (shape(mat1)[0] < tolN) or (shape(mat2)[0] < tolN):
        return None, leafType(data)
    #否則,繼續分支,返回最佳的特徵和其選取的值
    return best_index, best_val

#建立迴歸樹
def createTree(data, leafType=regLeaf, errType=regErr, ops = (1,4)):
    #找到最佳的劃分特徵以及其對應的值
    feat, val = chooseBestSplit(data, leafType, errType, ops)
    #若達到停止條件,feat為None並返回數值(迴歸樹)或線性方程(模型樹)
    if feat is None:
        return val
    #若未達到停止條件,則根據feat和對應的val將資料集分開,然後左右孩子遞迴地建立迴歸樹
    #tree 儲存了當前根節點劃分的特徵以及其對應的劃分值,另外,左右孩子也作為字典儲存
    rgtree = {}
    rgtree['spInd'] = feat
    rgtree['spVal'] = val
    lset, rset = binSplitData(data,feat,val)
    rgtree['left'] = createTree(lset,leafType, errType, ops)
    rgtree['right'] = createTree(rset,leafType, errType, ops)
    return rgtree

#判斷是否為樹
def isTree(obj):
    return (type(obj).__name__ == 'dict')

#遞迴函式,找到葉節點平均值,塌陷處理
def getMean(tree):
    if isTree(tree['right']):
        tree['right'] = getMean(tree['right'])
    if isTree(tree['left']):
        tree['left'] = getMean(tree['left'])
    return (tree[left] + tree[right])/2.0

#剪枝
def prune(tree, testData):
    #如果測試資料為空,則直接對原樹進行塌陷處理
    if shape(testData)[0] == 0:
        return getMean(tree)
    #如果當前節點不是葉子節點的父節點,將test資料分支,然後遞迴地對左子樹和右子樹剪枝
    if isTree(tree['left']) or isTree(tree['right']):
        lSet, rSet = binSplitData(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 = binSplitData(testData, tree['spInd'], tree['spVal'])
        #不剪枝
        errorNoMerge = sum(power(lSet[:,-1]-tree['left'],2)) + sum(power(rSet[:,-1]-tree['right'],2))
        #剪枝
        treeMean = (tree['left'] + tree['right'])/2.0
        errorMerge = sum(power(testData[:,-1]-treeMean,2))
        if errorMerge < errorNoMerge:
            print('merging')
            return treeMean
        else:
            return tree
    else:
        return tree