一.算法流程
構建決策樹:
輸入:訓練集 $D = { (x_1,y_1),(x_2,y_2),...,(x_m,y_m) }$
屬性集 $A = { a_1,a_2,....,a_n }$
createTree(D,A)
if D 中樣本全屬于同一類C:
標記當前節點node標記為C類葉子節點
return C
else if A = 空 OR D中樣本在A上取值相同:
當前節點node標記為葉子節點,類別C為D中樣本最多的類
return C
else
從A中選擇最優屬性劃分a*
劃分數據集Di
創建子節點
for 每個劃分的子集:
createTree(Di,A/a*)
二.實現
我們的實驗數據
判斷一個生物是否是魚:
labels = ['no surfacing', 'flippers', 'head']
dataset = [[0, 1, 1, 'yes'],
[1, 1, 1, 'yes'],
[1, 0, 1, 'yes'],
[1, 1, 0, 'no'],
[0, 0, 1, 'no'],
[1, 1, 0, 'no'],
[0, 0, 0, 'yes']]
尋找最優屬性劃分
算法流程中第8行提到了選擇最有屬性劃分,那么怎么劃分最優屬性呢。劃分屬性的原則就是將無序的數據變得更有序。
劃分數據集之前和之后信息發生的變化稱為信息增益,計算每個屬性劃分數據集的信息增益,信息增益最高的屬性就是最好的劃分屬性。集合的信息度量方式是香農熵,熵(entropy)的定義是信息的期望值。對于根據某個屬性做的分類,$x_i$表示其中一個被分類的一類數據則$x_i$的信息期望值為:
$$l(x_i)=-log_2 p(x_i)$$
$p(x_i)$表示$x_i$出現的概率
所有可能的類別的信息期望值就是熵:
$$H = -\sum_{i=1}^n p(x_i)log_2p(x_i)$$
計算香農熵代碼:
這里featVec最后一項是分類,我們用labelcount記錄每一個分類的出現個數
labelcount是一個dict ,使用labelcount.get(curLabel,0)返回當前字典中curLabel的值(這里表示出現次數),第二個參數0表示如果字典中沒有curLabel這個key則插入到字典中,默認值為0
將所有分類出現的次數都記錄到labelcount之后就可以遍歷labelcount字典用出現次數計算概率,從而計算香農熵
def calShannonEnt(dataset):
dataSize = len(dataset)
labelCount = {}
for featVec in dataset:
curLabel = featVec[-1]
labelCount[curLabel] = labelCount.get(curLabel, 0) + 1
shannonEnt = 0.0
for key in labelCount:
prob = float(labelCount[key]) / dataSize
shannonEnt -= prob * log(prob, 2)
return shannonEnt
選擇最優劃分
首先計算劃分前數據集的信息熵baseEntropy.
遍歷每一個屬性,計算每一個屬性劃分數據集的信息增益
featNum = len(dataset[0]-1)
for i in range(featNum):
對于第i個屬性劃分之后,獲取存在的所有屬性值,統計這個屬性存在的屬性值,為了我們的到的屬性值唯一,我們使用set來保存屬性值
featList = [X[i] for X in dataset]
uniqueFeatValue = set(featList)
這時我們就可以通過屬性值來劃分子數據集,遍歷屬性axis所有可能的屬性值, 將屬性axis值為value的數據取出作為子數據集的元素
同時計算根據這個屬性劃分數據集得到的信息增益
注意計算子數據集的香農熵之后還要乘上這個子數據集的概率
for value in uniqueFeatValue:
subDataset = splitDataset(dataset, i, value)
prob = float(len(subDataset)) / len(dataset)
newEntropy += prob * calShannonEnt(subDataset)
劃分數據集:
將屬性axis值為value的數據取出作為子數據集的元素
數據加入到子數據集后,需要把原劃分的屬性標簽去掉
假設我們要得到axis == i的屬性,屬性值為v的子集
遍歷數據集的每一條數據記錄 $Data_i = [ x_1,x_2,...,x_i,...x_n]$
當$x_i=v$時,獲取x_i前后的元素再連接起來就得到新的數據項${Data_i}_{new} = [ x_1,x_2,...,x_i-1,x_i+1,...x_n]$
使用extend將x_i前后兩個list
extend作用是將兩個list連接起來
append的作用是向list添加一個元素
axis:需要劃分的屬性
value:類別
retDataset:返回dataset中屬性axis為value的子集
def splitDataset(dataset, axis, value):
retDataset = []
for featVec in dataset:
if featVec[axis] == value:
retFeatVec = featVec[:axis]
retFeatVec.extend(featVec[axis + 1:])
retDataset.append(retFeatVec)
return retDataset
構建決策樹
構建決策樹的函數creatTree是一個遞歸函數,輸入為數據集和列表集,返回的是當前創建的節點,遞歸返回的條件是:
1. 當前數據集中所有數據都屬于同一類
2. 只剩一條數據時
3. 屬性集為空
else
先選取最優劃分屬性,創建節點,節點名字為劃分屬性,然后在屬性集中刪除這個屬性
使用字典作為數的節點,這樣dict的key可以作為當前節點名字,對應的value也用一個dict表示, value的字典保存子節點,這樣層層潛逃就可以構成一個樹.
用于保存子節點的dict中,key保存的是當前劃分屬性的屬性值,val為對應的子節點通過遞歸調用createTree得到
獲取當前劃分屬性的所有屬性值,用set做唯一儲存
featlist = [f[bestFeat] for f in dataset]
uniqueFeatValue = set(featlist)
對于每一個屬性值,屬性值作為子節點dict的key,將createTree返回的節點作為val
這里調用splitDataset獲得指定屬性值的子數據集作為下一層createTree的數據集
for value in uniqueFeatValue:
subLabels = curLabels[:]
curTreeNode[bestLabel][value] = createTree(
splitDataset(dataset, bestFeat, value), subLabels)
分類
當都建好決策樹后就可以用這個決策樹來做分類了
分類函數classify也是一個遞歸函數,根據輸入的屬性和屬性值從決策樹的根節點搜索,直到搜索到葉子節點
我們可以用判斷當前節點是不是字典類型來判斷當前節點是否是葉子節點,如果是字典類型,則不是葉子節點,不是葉子節點就繼續向下搜索
否則返回當前類型
def classify(inTree,featLabel,featVec):
label = inTree.keys()[0]
featIndex = featLabel.index(label)
childs = inTree[label]
nextNode = childs.get(featVec[featIndex],'error')
if type(nextNode) == type({}):
result = classify(nextNode,featLabel,featVec)
else:
result = nextNode
return result