決策樹的實現太...繁瑣了。
如果只是接受他的原理的話還好說,但是要想用代碼去實現比較糟心,目前運用了《機器學習實戰》的代碼手打了一遍,決定在這里一點點摸索一下該工程。
實例的代碼在使用上運用了香農熵,並且都是來處理離散數據的,因此有一些局限性,但是對其進行深層次的解析有利於對於代碼的運作,python語言的特點及書寫肯定是有幫助的。
我們分別從每個函數開始:
- 計算香農熵
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] +=1
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2)
return shannonEnt
該函數為當前的數據集計算香農熵。
其中,numEntries用來計數數據數目
numEntries = len(dataSet)
其后,該函數運用了一個字典來計算各個最終類(即我們所要最終分開的特點的所有類型,比如Titanic題目中就是是否生還,Coursera課程中的例子就是這個貸款是否安全)的出現數目,其中,該最終數據是處在數據集的最后一列的,因此運用
currentLabel = featVec[-1]
讓currentLabel暫時記住當前數據的最終類型,倘使該類型不存在,就要用
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
將其插入字典並將它的鍵值初始化為0(即出現0次),最后用
labelCounts[currentLabel] +=1
計數代表當前最終類型出現數目+1
之后便是對於香農熵的計算,這里的代碼上的理解並不困難
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob * log(prob,2)
log(prob,2)是以2為底數求對數
- 划分數據集
def splitDataSet(dataset, axis, value):
retDataSet = []
for featVec in dataset:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
return retDataSet
該函數使用了三個參數:待划分的數據集,划分數據集的特征(即第axis個屬性),需要返回的特征的值(該屬性的取值value)。
首先由於需要將一類數據放於一起,但是python在函數中傳遞的是列表的引用,直接修改也會在全局上對列表產生變化,為了避免這種影響就需要新建一個空表存儲目標數據。
retDataSet = []
再之后就是要將相應的數據放入這個列表中。
其中為了更好的進行以下的操作,因為本工程控制划分數目的方式看來是限定在一條線路中每個屬性最多有一次作為划分指標,因此需要將其在加入retDataSet時去除,由下列代碼實現:
reducedFeatVec = featVec[:axis]
reducedFeatVec.extend(featVec[axis+1:])
retDataSet.append(reducedFeatVec)
書中提到了append()與extend()函數的區別,前者可以將列表整體作為一個元素加入新列表,后者則是將列表的元素分別加入新列表。
- 選出最好的划分方式
def chooseBestFeatureToSplit(dataset):
numFeatures = len(dataset[0]) - 1
baseEntropy = calcShannonEnt(dataset)
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataset]
uniqueVals=set(featList)
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataset,i,value)
prob = len(subDataSet)/float(len(dataset))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
if(infoGain>bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
當下我們所要做的是要遍歷當前的數據集,不斷計算所有屬性划分對應的香農熵,由此選出最適合作為當前狀況下划分標准的屬性。
為了防止最終屬性被選擇,我們需要將其排除在外,默認最終屬性一般會出現在最后一列,
numFeatures = len(dataset[0]) - 1
之后在for循環中numFeatures數目便不會再囊括最終屬性了。
在這個時候我糾結了一下,萬一最好的划分就是最終屬性呢?不過目前我已經說服自己了,畢竟最終屬性是我們的目標,不管怎樣我們都要避開它並使用別的屬性來進行划分。
之后初始化變量,分別來存儲新的香農熵變化和當前數據。
bestInfoGain = 0.0
bestFeature = -1
現在進入這個遍歷所有屬性的for大循環。一開始我們先將當前屬性的所有可能值傳入一個空集合(即無重復元素的集合)。
featList = [example[i] for example in dataset]
uniqueVals=set(featList)
而下面這個變量來計算當前香農熵。
newEntropy = 0.0
for value in uniqueVals:
subDataSet = splitDataSet(dataset,i,value)
prob = len(subDataSet)/float(len(dataset))
newEntropy += prob * calcShannonEnt(subDataSet)
infoGain = baseEntropy - newEntropy
其中用splitDataSet(),將當前處理的屬性作為划分依據,用其內部所有可能的取值來分出子集,並計算對應的香農熵,然后根據比例再加權求和,得到該屬性划分下的香農熵newEntropy以及相比直接作為葉子節點得到的香農熵的差infoGain。
if(infoGain>bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
最后與當前記錄的最優屬性的結果比對,如果更好就覆蓋。最后返回最優屬性。
- 選取出現最多的屬性
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys():classCount[vote]=0
classCount[vote] += 1
sortedClassCount = sorted(classClount.iteritems(),\
key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
如果當前划分時已經沒有更多的屬性了,那么該節點自動變為葉子節點,其分類由其數據中最終屬性出現最多的分類決定,即根據屬性的分類創建一個字典classCount,並用分類作為鍵,出現次數為對應值。
隨后用operator庫中的sort()函數為其排序,選出出現最多的最終屬性,以此作為該葉子節點的分類。
- 主體:創建樹
def createTree(dataSet,labels):
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 = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet,bestFeat,value),subLabels)
return myTree
這個函數運用了遞歸的想法,最終結果是以字典中的字典形式來表示的,雖然並不是特別直觀,但至少是我能接受的工程量。
首先進行了兩個判斷,一是若當前數據集在最終分類上已經達成了共識,那么就不需要在進行分類了:
if classList.count(classList[0]) == len(classList):
return classList[0]
二,如果所有的屬性已經被使用(排除,僅剩最終屬性),也會停止遞歸:
if len(dataSet[0])==1:
return majorityCnt(classList)
在排除了以上情況后就會選擇出當前最優的划分屬性,並在其中開辟對應的子字典:
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{ } }
並刪除已經使用的屬性:
del(labels[bestFeat])
緊接着要為上面剛選定的屬性分類划分出各個子節點,並為其遞歸再次生成子樹,方法集合了上文的許多技巧,暫時不再贅述。
以上,是我對於《機器學習實戰》中的決策樹實現代碼的解析。