CART(分類回歸樹)原理和實現


前面我們了解了決策樹和adaboost的決策樹墩的原理和實現,在adaboost我們看到,用簡單的決策樹墩的效果也很不錯,但是對於更多特征的樣本來說,可能需要很多數量的決策樹墩

或許我們可以考慮使用更加高級的弱分類器,下面我們看下CART(Classification And Regression Tree)的原理和實現吧

CART也是決策樹的一種,不過是滿二叉樹,CART可以是強分類器,就跟決策樹一樣,但是我們可以指定CART的深度,使之成為比較弱的分類器

CART生成的過程和決策樹類似,也是采用遞歸划分的,不過也存在很多的不同之處

數據集:第一列為樣本名稱,最后一列為類別,中間為特征

human    constant    hair    true    false    false    false    true    false    mammal
python    cold_blood    scale    false    true    false    false    false    true    reptile
salmon    cold_blood    scale    false    true    false    true    false    false    fish
whale    constant    hair    true    false    false    true    false    false    mammal
frog    cold_blood    none    false    true    false    sometime    true    true    amphibious
lizard    cold_blood    scale    false    true    false    false    true    false    reptile
bat    constant    hair    true    false    true    false    true    false    mammal
cat    constant    skin    true    false    false    false    true    false    mammal
shark    cold_blood    scale    true    false    false    true    false    false    fish
turtle    cold_blood    scale    false    true    false    sometime    true    false    reptile
pig    constant    bristle    true    false    false    false    true    true    mammal
eel    cold_blood    scale    false    true    false    true    false    false    fish
salamander    cold_blood    none    false    true    false    sometime    true    true    amphibious

 

特征名稱如下

["temperature","cover","viviparity","egg","fly","water","leg","hibernate"]

 

1:數據集划分評分

CART使用gini系數來衡量數據集的划分效果而不是香農熵(借用下面的一張圖)

def calGini(dataSet):
    numEntries = len(dataSet)
    labelCounts={}
    for featVec in dataSet: 
        currentLabel = featVec[-1]
        if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    gini=1
    for label in labelCounts.keys():
        prop=float(labelCounts[label])/numEntries
        gini -=prop*prop
    return gini

 

2:數據集划分

決策樹是遍歷每一個特征的特征值,每個特征值得到一個划分,然后計算每個特征的信息增益從而找到最優的特征;

CART每一個分支都是二分的,當特征值大於兩個的時候,需要考慮特征值的組合得到兩個“超級特征值”作為CART的分支;當然我們也可以偷懶,每次只取多個特征值的一個,挑出最優的一個和剩下的分別作為一個分支,但無疑這得到的cart不是最優的

# 傳入的是一個特征值的列表,返回特征值二分的結果
def featuresplit(features):
    count = len(features)#特征值的個數
    if count < 2:
        print "please check sample's features,only one feature value"
        return -1
    # 由於需要返回二分結果,所以每個分支至少需要一個特征值,所以要從所有的特征組合中選取1個以上的組合
    # itertools的combinations 函數可以返回一個列表選多少個元素的組合結果,例如combinations(list,2)返回的列表元素選2個的組合
    # 我們需要選擇1-(count-1)的組合
    featureIndex = range(count)
    featureIndex.pop(0) 
    combinationsList = []    
    resList=[]
    # 遍歷所有的組合
    for i in featureIndex:
        temp_combination = list(combinations(features, len(features[0:i])))
        combinationsList.extend(temp_combination)
        combiLen = len(combinationsList)
    # 每次組合的順序都是一致的,並且也是對稱的,所以我們取首尾組合集合
    # zip函數提供了兩個列表對應位置組合的功能
    resList = zip(combinationsList[0:combiLen/2], combinationsList[combiLen-1:combiLen/2-1:-1])
    return resList

 

得到特征的划分結果之后,我們使用二分后的特征值划分數據集

def splitDataSet(dataSet, axis, values):
    retDataSet = []
    for featVec in dataSet:
        for value in values:
            if featVec[axis] == value:
                reducedFeatVec = featVec[:axis]     #剔除樣本集
                reducedFeatVec.extend(featVec[axis+1:])
                retDataSet.append(reducedFeatVec)
    return retDataSet    

遍歷每個特征的每個二分特征值,得到最好的特征以及二分特征值

# 返回最好的特征以及二分特征值
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #
    bestGiniGain = 1.0; bestFeature = -1;bestBinarySplit=()
    for i in range(numFeatures):        #遍歷特征
        featList = [example[i] for example in dataSet]#得到特征列
        uniqueVals = list(set(featList))       #從特征列獲取該特征的特征值的set集合
        # 三個特征值的二分結果:
        # [(('young',), ('old', 'middle')), (('old',), ('young', 'middle')), (('middle',), ('young', 'old'))]
        for split in featuresplit(uniqueVals):
            GiniGain = 0.0
            if len(split)==1:
                continue
            (left,right)=split
            # 對於每一個可能的二分結果計算gini增益
            # 左增益
            left_subDataSet = splitDataSet(dataSet, i, left)
            left_prob = len(left_subDataSet)/float(len(dataSet))
            GiniGain += left_prob * calGini(left_subDataSet)
            # 右增益
            right_subDataSet = splitDataSet(dataSet, i, right)
            right_prob = len(right_subDataSet)/float(len(dataSet))
            GiniGain += right_prob * calGini(right_subDataSet)
            if (GiniGain <= bestGiniGain):       #比較是否是最好的結果
                bestGiniGain = GiniGain         #記錄最好的結果和最好的特征
                bestFeature = i
                bestBinarySplit=(left,right)
    return bestFeature,bestBinarySplit    

 所有特征用完時多數表決程序

def majorityCnt(classList):
    classCount={}
    for vote in classList:
        if vote not in classCount.keys(): classCount[vote] = 0
        classCount[vote] += 1
    sortedClassCount = sorted(classCount.iteritems(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

現在來生成cart吧

 

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet]
    # print dataSet
    if classList.count(classList[0]) == len(classList): 
        return classList[0]#所有的類別都一樣,就不用再划分了
    if len(dataSet) == 1: #如果沒有繼續可以划分的特征,就多數表決決定分支的類別
        # print "here"
        return majorityCnt(classList)
    bestFeat,bestBinarySplit = chooseBestFeatureToSplit(dataSet)
    # print bestFeat,bestBinarySplit,labels
    bestFeatLabel = labels[bestFeat]
    if bestFeat==-1:
        return majorityCnt(classList)
    myTree = {bestFeatLabel:{}}
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = list(set(featValues))
    for value in bestBinarySplit:
        subLabels = labels[:]       # #拷貝防止其他地方修改
        if len(value)<2:
            del(subLabels[bestFeat])
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree 

 

看下效果,左邊是cart,右邊是決策樹,(根節點用cover和temperature是一樣的,為了對比決策樹,此時我選了cover),第三個圖是temperature作為根節點的cart

上面的代碼是不考慮特征繼續使用的,也就是每個特征只使用一次;但是我們發現有些有些分支里面特征值個數多余兩個的,其實我們應該讓這些特征繼續參與下一次的划分

可以發現,temperature作為根節點的cart沒有變化,而cover作為根節點的cart深度變淺了,並且cover特征出現了兩次(或者說效果變好了)

 下面是有變化的代碼

特征值多余兩個的分支保留特征值

def splitDataSet(dataSet, axis, values):
    retDataSet = []
    if len(values) < 2:
        for featVec in dataSet:
            if featVec[axis] == values[0]:#如果特征值只有一個,不抽取當選特征
                reducedFeatVec = featVec[:axis]     
                reducedFeatVec.extend(featVec[axis+1:])
                retDataSet.append(reducedFeatVec)
    else:
        for featVec in dataSet:
            for value in values:
                if featVec[axis] == value:#如果特征值多於一個,選取當前特征
                    retDataSet.append(featVec)

    return retDataSet

 

createTree函數for循環判斷是否需要移除當前最優特征

    for value in bestBinarySplit:
        if len(value)<2:
            del(labels[bestFeat])
        subLabels = labels[:]       #拷貝防止其他地方修改
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)

 

這樣我們就生成了一個cart,但是這個數據集沒有出現明顯的過擬合的情景,我們換一下數據集看看

sunny    hot    high    FALSE    no
sunny    hot    high    TRUE    no
overcast    hot    high    FALSE    yes
rainy    mild    high    FALSE    yes
rainy    cool    normal    FALSE    yes
rainy    cool    normal    TRUE    no
overcast    cool    normal    TRUE    yes
sunny    mild    high    FALSE    no
sunny    cool    normal    FALSE    yes
rainy    mild    normal    FALSE    yes
sunny    mild    normal    TRUE    yes
overcast    mild    high    TRUE    yes
overcast    hot    normal    FALSE    yes
rainy    mild    high    TRUE    no

 

 特征名稱:"Outlook" , "Temperature" , "Humidity" , "Wind"

生成的cart比價合理,這是因為數據比較合理,我們添加一條臟數據看看cart會變成怎么樣(右圖),可以看到cart為了擬合我新加的這條臟數據,

樹深度增加1,葉子節點增加3,不過另一方面也是因為樣本數少的原因,一個噪聲樣本就產生了如此大的印象

overcast    mild    normal    FALSE    no

下一篇博客我們繼續討論cart連續值的生成以及剪枝的實驗。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM