上篇文章介紹了K-近鄰的分類方法,這篇文章介紹另外一種分類的方法:決策樹。和K-近鄰不同,決策樹的方法包括了一個訓練的過程。通過這個訓練過程,我們可以構造一棵決策樹。然后我們可以使用這課決策樹來對輸入的樣本進行分類。
直觀的理解
假如我們現在需要對郵件進行分類,我們收集郵件的兩個特征:①郵件的域名地址是否是myEmployer.com?②郵件中是否包含“曲棍球”這個單詞。同時,我們希望把郵件分成三類:①無聊時需要閱讀的郵件;②需要及時處理的朋友郵件;③無需閱讀的垃圾郵件。現在通過訓練,我們得到了一棵決策樹,其結構如下:
分類的過程非常簡單,首先看收到的郵件的域名是否是myEmployer.com? 如果是,那么就把它歸為“無聊時需要閱讀的郵件”;如果不是,那么再看郵件中是否包含“曲棍球”這個詞。如果包含,那么把她歸為“需要及時處理的朋友郵件”;否則,將其歸為“無需閱讀的垃圾郵件”。
構造決策樹
從上面的敘述,我們可以看出,決策樹分類的本質在于構造成一棵樹,內節點由特征組成,而葉子節點由最終的分類結果組成。那么如何使用已有的訓練樣本構造決策樹呢?
熵 在這之前,我們先來熟悉一個概念:熵。 詳細的概念理解請參考知乎。簡單來說,熵代表信息的混亂程度。越混亂,熵值就會越大。在未分類之前,訓練集中的數據非常混亂,我們的目的是通過將訓練集以某個特征分類,使得熵值慢慢減少(減少?對,是減少),最終我們的訓練集會變得越來越有序,并且所有的訓練樣本都分到它們本應在的類別。
所以說,構造決策樹的過程,就是一個使得熵值逐漸減少的過程。具體的思路如下:
輸入:訓練集
輸出:決策樹
repeat:
尋找使得熵值下降最大的特征;
將訓練集沿著這個特征進行劃分;
until:
每個分支類別完全相同 OR 遍歷完所有特征
代碼實現
熵的代碼實現 熵的計算公式如下:
其中,p(xi)表示在樣本屬于xi類的概率,-log2p(xi)表示樣本屬于xi類的信息量。
假設我們樣本的格式由(n+1)維的向量組成,其中前n維表示數據的特征,最后的維度表示數據的類別。那么計算熵值的代碼如下:
from math import log
import operator
def calcShannonEnt(dataset):
numEntries = len(dataset)
labelCounts = {}
shannonEnt = 0.0
for featVec in dataset:
currentLabel = featVec[-1]
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
labelCounts[currentLabel] += 1
for key in labelCounts:
prob = float(labelCounts[key])/numEntries
shannonEnt -= prob*log(prob,2)
return shannonEnt
訓練集的劃分 接下來我們實現訓練集的劃分。
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為該特征取的值。
小的例子 下面舉一個小的例子,來看一下特征劃分前后數據的熵值變化。數據如下:
|序號|不浮出水面是否可以生存|是否有腳蹼|屬于魚類|
|-|
|1|是|是|是|
|2|是|是|是|
|3|是|否|否|
|4|否|是|否|
|5|否|是|否|
下面的代碼計算以第一個特征劃分前后的熵值:
def createDataSet():
dataSet = [[1,1,'yes'], [1,1,'yes'], [1,0,'no'], [0,1,'no'], [0,1,'no']]
labels = ['no surfacing', 'flippers']
dataset,labels = createDataSet()
values = [example[0] for example in dataset]
uniqueValues = set(values)
newEntropy = 0.0
for value in uniqueValues:
subDataset = splitDataSet(dataset, 0, value)
prob = len(subDataset)/float(len(dataset))
newEntropy += prob*calcShannonEnt(subDataset)
oldEntropy = calcShannonEnt(dataset)
print 'before split: %f' %oldEntropy
print 'after split: %f' %newEntropy
輸出的結果為:
現在我們已經知道如何依據一個特征來劃分數據集及如何計算一個數據集的熵值,現在我們可以著手構造一棵決策樹了。但在構造決策樹之前,我們要明白:有時候我們并不能依據特征將所有的樣本分開。如果特征已經使用完,樣本還沒有完全分開,這時候我們取大多數的標簽,作為剩下樣本的標簽。具體的細節,在構造決策樹的時候我會再說明。
構造決策樹 從決策樹的結構,我們很容易想到使用遞歸的算法來實現遞歸樹。在這里,我們以字典的數據結構來存儲一顆樹。代碼如下:
def createTree(dataSet, Labels):
#獲取所有的類別標簽
classlist = [example[-1] for example in dataSet]
#如果所有的數據都屬于同一類,則不需要使用后面的特征來構造新的節點,決策樹構造完成
if classlist.count(classlist[0]) == len(classlist):
return classlist[0]
#如果數據集中的特征已用完,將剩下樣本中出現次數最大的標簽,作為剩下所有樣本的標簽
if dataSet[0] == 1:
return majorityCnt[classlist]
#選擇熵值變化最大的特征
bestFeat = chooseBestFeatureToSplit(dataSet)
#取得對應特征的標簽
bestFeatLabel = Labels[bestFeat]
#構造對應特征的字典
myTree = {bestFeatLabel:{}}
#將對應的標簽從標簽數組中刪除
labels = Labels[:]
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
其中majorityCnt函數的代碼如下:
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]
其中,chooseBestFeatureToSplit的代碼如下:
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 bestFeat
至此,決策樹我們已經構造完成了。使用上面的數據構造的決策樹如下:
用決策樹來分類
決策樹構造完成之后,我們就可以進行分類了,分類的代碼如下:
def classify(inputTree, featLabels, testVec):
firstStr = inputTree.keys()[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
分類的代碼也是遞歸的,結合上面的決策樹,非常容易理解。
技巧 我們可以將訓練生成的決策樹保存到文件中,這樣每次分類的時候,直接從文件總讀取即可。保存和讀取的代碼如下:
def storeTree(inputTree, filename):
import pickle
fw = open(filename, 'w')
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename)
return pickle.load(fr)
適用范圍
我們可以看到,決策樹的算法只能適用于離散的數據。
今天的算法分享就到這里,咱們下次再見~