前面我們了解了決策樹和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連續值的生成以及剪枝的實驗。
