機器學習筆記(5)——C4.5決策樹中的連續值處理和Python實現
在ID3決策樹演算法中,我們實現了基於離散屬性的決策樹構造。C4.5決策樹在劃分屬性選擇、連續值、缺失值、剪枝等幾方面做了改進,內容較多,今天我們專門討論連續值的處理和Python實現。
1. 連續屬性離散化
C4.5演算法中策略是採用二分法將連續屬性離散化處理:假定樣本集D的連續屬性有n個不同的取值,對這些值從小到大排序,得到屬性值的集合。把區間的中位點作為候選劃分點,於是得到包含n-1個元素的劃分點集合
基於每個劃分點t,可將樣本集D分為子集和,其中中包含屬性上不大於t的樣本,包含屬性上大於t的樣本。
對於每個劃分點t,按如下公式計算其資訊增益值,然後選擇使資訊增益值最大的劃分點進行樣本集合的劃分。
在ID3演算法中的西瓜資料集中增加兩個連續屬性“密度”和“含糖率”,下面我們計算屬性“密度”的資訊增益。
從資料可以看出,17個樣本的密度屬性值均不同,因此該屬性的候選劃分點集合由16個值組成:
所有劃分點的資訊增益均可按上述方法計算得出,最優劃分點為0.381,對應的資訊增益為0.263。我們分別按照離散值和連續值的資訊增益計算方法,計算出每個屬性的資訊增益,從而選擇最優劃分屬性,構造決策樹。
需要注意的是:當前節點劃分屬性為連續屬性,該屬性還可作為其後代節點的劃分屬性。
2. Python實現
在ID3決策樹演算法的基礎上,我們需要新增或修改一些方法,以便可以處理連續值。
- 新增一個劃分資料集的方法
# 劃分資料集, axis:按第幾個特徵劃分, value:劃分特徵的值, LorR: value值左側(小於)或右側(大於)的資料集 def splitDataSet_c(dataSet, axis, value, LorR='L'): retDataSet = [] featVec = [] if LorR == 'L': for featVec in dataSet: if float(featVec[axis]) < value: retDataSet.append(featVec) else: for featVec in dataSet: if float(featVec[axis]) > value: retDataSet.append(featVec) return retDataSet
- 修改原來的最優劃分屬性選擇方法,在其中增加連續屬性的分支
# 選擇最好的資料集劃分方式
def chooseBestFeatureToSplit_c(dataSet, labelProperty):
numFeatures = len(labelProperty) # 特徵數
baseEntropy = calcShannonEnt(dataSet) # 計算根節點的資訊熵
bestInfoGain = 0.0
bestFeature = -1
bestPartValue = None # 連續的特徵值,最佳劃分值
for i in range(numFeatures): # 對每個特徵迴圈
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) # 該特徵包含的所有值
newEntropy = 0.0
bestPartValuei = None
if labelProperty[i] == 0: # 對離散的特徵
for value in uniqueVals: # 對每個特徵值,劃分資料集, 計算各子集的資訊熵
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet) / float(len(dataSet))
newEntropy += prob * calcShannonEnt(subDataSet)
else: # 對連續的特徵
sortedUniqueVals = list(uniqueVals) # 對特徵值排序
sortedUniqueVals.sort()
listPartition = []
minEntropy = inf
for j in range(len(sortedUniqueVals) - 1): # 計算劃分點
partValue = (float(sortedUniqueVals[j]) + float(
sortedUniqueVals[j + 1])) / 2
# 對每個劃分點,計算資訊熵
dataSetLeft = splitDataSet_c(dataSet, i, partValue, 'L')
dataSetRight = splitDataSet_c(dataSet, i, partValue, 'R')
probLeft = len(dataSetLeft) / float(len(dataSet))
probRight = len(dataSetRight) / float(len(dataSet))
Entropy = probLeft * calcShannonEnt(
dataSetLeft) + probRight * calcShannonEnt(dataSetRight)
if Entropy < minEntropy: # 取最小的資訊熵
minEntropy = Entropy
bestPartValuei = partValue
newEntropy = minEntropy
infoGain = baseEntropy - newEntropy # 計算資訊增益
if infoGain > bestInfoGain: # 取最大的資訊增益對應的特徵
bestInfoGain = infoGain
bestFeature = i
bestPartValue = bestPartValuei
return bestFeature, bestPartValue
- 修改原來的決策樹構建方法
# 建立樹, 樣本集 特徵 特徵屬性(0 離散, 1 連續)
def createTree_c(dataSet, labels, labelProperty):
# print dataSet, labels, labelProperty
classList = [example[-1] for example in dataSet] # 類別向量
if classList.count(classList[0]) == len(classList): # 如果只有一個類別,返回
return classList[0]
if len(dataSet[0]) == 1: # 如果所有特徵都被遍歷完了,返回出現次數最多的類別
return majorityCnt(classList)
bestFeat, bestPartValue = chooseBestFeatureToSplit_c(dataSet,
labelProperty) # 最優分類特徵的索引
if bestFeat == -1: # 如果無法選出最優分類特徵,返回出現次數最多的類別
return majorityCnt(classList)
if labelProperty[bestFeat] == 0: # 對離散的特徵
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}}
labelsNew = copy.copy(labels)
labelPropertyNew = copy.copy(labelProperty)
del (labelsNew[bestFeat]) # 已經選擇的特徵不再參與分類
del (labelPropertyNew[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueValue = set(featValues) # 該特徵包含的所有值
for value in uniqueValue: # 對每個特徵值,遞迴構建樹
subLabels = labelsNew[:]
subLabelProperty = labelPropertyNew[:]
myTree[bestFeatLabel][value] = createTree_c(
splitDataSet(dataSet, bestFeat, value), subLabels,
subLabelProperty)
else: # 對連續的特徵,不刪除該特徵,分別構建左子樹和右子樹
bestFeatLabel = labels[bestFeat] + '<' + str(bestPartValue)
myTree = {bestFeatLabel: {}}
subLabels = labels[:]
subLabelProperty = labelProperty[:]
# 構建左子樹
valueLeft = '是'
myTree[bestFeatLabel][valueLeft] = createTree_c(
splitDataSet_c(dataSet, bestFeat, bestPartValue, 'L'), subLabels,
subLabelProperty)
# 構建右子樹
valueRight = '否'
myTree[bestFeatLabel][valueRight] = createTree_c(
splitDataSet_c(dataSet, bestFeat, bestPartValue, 'R'), subLabels,
subLabelProperty)
return myTree
- 修改原來的測試方法
# 測試演算法
def classify_c(inputTree, featLabels, featLabelProperties, testVec):
firstStr = inputTree.keys()[0] # 根節點
firstLabel = firstStr
lessIndex = str(firstStr).find('<')
if lessIndex > -1: # 如果是連續型的特徵
firstLabel = str(firstStr)[:lessIndex]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstLabel) # 跟節點對應的特徵
classLabel = None
for key in secondDict.keys(): # 對每個分支迴圈
if featLabelProperties[featIndex] == 0: # 離散的特徵
if testVec[featIndex] == key: # 測試樣本進入某個分支
if type(secondDict[key]).__name__ == 'dict': # 該分支不是葉子節點,遞迴
classLabel = classify_c(secondDict[key], featLabels,
featLabelProperties, testVec)
else: # 如果是葉子, 返回結果
classLabel = secondDict[key]
else:
partValue = float(str(firstStr)[lessIndex + 1:])
if testVec[featIndex] < partValue: # 進入左子樹
if type(secondDict['是']).__name__ == 'dict': # 該分支不是葉子節點,遞迴
classLabel = classify_c(secondDict['是'], featLabels,
featLabelProperties, testVec)
else: # 如果是葉子, 返回結果
classLabel = secondDict['是']
else:
if type(secondDict['否']).__name__ == 'dict': # 該分支不是葉子節點,遞迴
classLabel = classify_c(secondDict['否'], featLabels,
featLabelProperties, testVec)
else: # 如果是葉子, 返回結果
classLabel = secondDict['否']
return classLabel
3. 繪製決策樹並測試資料
我們利用上面的西瓜資料,繪製一個決策樹。
fr = open(r'D:\Projects\PyProject\DecisionTree\watermalon3.0.txt')
listWm = [inst.strip().split('\t') for inst in fr.readlines()]
labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感', '密度', '含糖率']
labelProperties = [0, 0, 0, 0, 0, 0, 1, 1] # 屬性的型別,0表示離散,1表示連續
Trees = trees.createTree_c(listWm, labels, labelProperties)
print(json.dumps(Trees, encoding="cp936", ensure_ascii=False))
treePlotter.createPlot(Trees)
返回的決策樹資料:{"紋理": {"模糊": "否", "清晰": {"密度<0.3815": {"是": "否", "否": "是"}}, "稍糊": {"觸感": {"軟粘": "是", "硬滑": "否"}}}}
再用一條測試資料測試一下演算法,看是否能得到正確的分類。
labels = ['色澤', '根蒂', '敲聲', '紋理', '臍部', '觸感', '密度', '含糖率']
labelProperties = [0, 0, 0, 0, 0, 0, 1, 1]
testData = ['淺白', '蜷縮', '濁響', '清晰', '凹陷', '硬滑', 0.585, 0.002]
testClass = trees.classify_c(Trees, labels, labelProperties, testData)
print(json.dumps(testClass, encoding="cp936", ensure_ascii=False))
測試返回的結果是好瓜,從構造的樹可以看出該資料進入紋理清晰,密度大於0.3815的分支,是正確的。
參考:
周志華《機器學習》
Peter Harrington 《機器學習實戰》