decision tree Learning 決策樹學習筆記
決策樹學習是一種相對比較簡單的分類學習方法,但是分類效果較好並且表示直觀,主要針對離散型目標,它也等價於用if-then規則表示。
決策樹學習在Mitchell的《機器學習》第三章中有詳細講解,也有許多人做了詳細的筆記,比如http://www.cnblogs.com/pangxiaodong/archive/2011/05/11/2042882.html 。這裏主要簡單介紹最經典的決策樹學習算法ID3的python實現。
1. 熵(Entropy)
決策樹中用到了信息論中熵的概念,熵主要表示的是體系的混亂程度,也稱不確定程度。在信息論中,熵可以理解爲對於信息S,用統一的概率表示,至少需要多少位來編碼才能清楚的表示該信息。(這裏概率的意思可以理解爲:設S爲全樣本空間,可以根據某一組值(也就是我們的分類值和預測值)來標記S爲S1...Sn,每一份在全樣本空間中所佔的比率。) 熵的公式爲:
舉個例子:設S爲全體實數空間,以正、負屬性來標記S,則屬性正、負的概率各爲1/2,根據公式可以得到Entropy(S)=1,即我們需要1位來表示屬性是正還是負;而如果S爲正實數時,屬性正、負的概率分別爲1和0,Entropy(S)=0,即我們不需要任何位來表示該屬性,因爲它一定是正的。這就是熵的含義,取值範圍爲[0,1],值越大表示樣本空間不確定性越高。
2. 信息增益(Information Gain)
理解清楚熵的含義後,我們可以很容易理解信息增益的概念。S依然爲樣本空間,設A是S中的某一屬性,當我們用屬性A劃分S時,會根據在A上的不同值將S劃分爲不同的子樣本空間S1,S2…,然後我們計算所有子樣本空間的熵Entropy(Si),以及Si佔S的比例,並用類似全概率公式的方法求劃分後的結果的熵Entropy(S’)。從信息論的角度看,這個熵就是用Entropy(S’)位編碼才能表示劃分後的結果。
由此,我們可以得到信息增益的值Gain(S,A),就是用劃分前的熵Entropy(S)減去劃分後的熵Entropy(S’)。即公式
那麼這個值是越大越好,還是越小越好呢?根據上述討論,當然是劃分後的熵越小越好,越小表示劃分後的結果越有序,也就越有利於我們進行決策;那麼反過來,Gain(S,A)的結果越大說明屬性A的劃分效果越好,我們就越應該優先用A來劃分樣本空間S。
OK,這就是最經典的決策樹算法ID3的核心問題,即每次劃分時哪個屬性的劃分效果最好?答案就是依次用沒有用過的屬性去劃分父空間(因爲涉及到遞歸),然後用信息增益最大的屬性作爲當前劃分屬性。
3. 求解決策樹的思想和流程
1)首先,我們知道決策樹的建立是一個遞歸過程,那麼遞歸的結束條件是什麼呢?a)如果所有的屬性都已用來劃分,那麼說明決策樹建立已經完成,則結束;b)如果待劃分空間內所有樣本都屬於同一分類,那麼說明在該空間內已不需要再繼續劃分,則結束。
2)然後,如果遞歸沒有結束,則計算各屬性劃分的信息增益,並選擇信息增益值最大的屬性作爲當前劃分屬性,並寫入存儲劃分屬性的數據結構中。
3)最後,我們要對當前劃分屬性的每個值,再繼續執行遞歸建立決策樹的過程。
這就是ID3決策樹學習算法的主要流程,下面將《Machine Learning In Action》中的python實現代碼學習着實現了一遍,如下所示:
<span style="font-family:SimSun;font-size:12px;">#coding=utf-8
'''
Created on Sep 13, 2014
Aim: To test decision tree algorithm
@author: lemon
'''
# 要用到log運算
from math import log
import operator
import types
'''
函數功能:計算給定集合的熵
輸入:給定數據集
輸出:熵的值
'''
def calculateEntropy(dataSet):
# 計算數據集共有多少元組
numOfObj = len(dataSet)
# 用labelCounts存儲數據集的分類標籤
labelCounts = {}
# 遍歷數據集
for obj in dataSet:
# 取分類標籤
currentLabel = obj[-1]
# 如果標籤不在labelCounts中,則在labelCounts中插入
# 該標籤鍵,並設值爲0
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel] = 0
# 該標籤值 +1
labelCounts[currentLabel] += 1
entropy = 0.0
# 遍歷labelCounts,根據熵的公式計算值
for label in labelCounts:
# 該標籤在總數據集中佔的概率
prob = float(labelCounts[label])/numOfObj
entropy -= prob * log(prob,2)
return entropy
'''
函數功能:根據劃分屬性軸和某一屬性值,得到劃分後的數據子集
輸入:給定數據集dataSet,劃分屬性軸axis,給定屬性值value
輸出:劃分後的數據子集
'''
def splitDataSet(dataSet, axis, value):
retDataSet = []
# 遍歷dataSet中的所有對象,如果其在axis軸上值爲value
# 則將該元組存入retDataSet[],除了該軸數據
for obj in dataSet:
if obj[axis] == value:
reducedObj = obj[:axis]
reducedObj.extend(obj[axis+1:])
retDataSet.append(reducedObj)
return retDataSet
'''
函數功能:選擇劃分效果最好的屬性
輸入:給定數據集
輸出:劃分屬性軸的下標
'''
def bestFeatureToSplit(dataSet):
# 初始化信息增益和劃分屬性的軸下標
maxInfoGain = 0.0
splitAxis = -1
# 計算給定數據集的熵
wholeEntropy = calculateEntropy(dataSet)
# 計算共有多少屬性,-1的含義是去掉分類標籤那一列
featureNum = len(dataSet[0]) - 1
# 計算共有多少元組
trainingNum = len(dataSet)
# 對每一個屬性,對其進行劃分,並計算信息增益,如果該信息增益比
# 當前記錄的信息增益值大,則更新;否則,繼續下一個屬性
for i in range(featureNum):
# 獲得屬性軸爲i的所有屬性值,並存儲在featureList中
'''
注意這裏書上用兩行代碼實現,更簡單
featureList = [example[j] for example in dataSet]
uniqueVals = set(featureList) # set is unique
'''
featureList = []
for j in range(trainingNum):
if dataSet[j][i] not in featureList:
featureList.append(dataSet[j][i])
else:
continue
# 現在已獲得了第i+1列屬性的所有值,計算共有多少不同值
iFeatureNum = len(featureList)
# 計算劃分後所有數據子集的熵和
splitedEntropy = 0.0
for k in range(iFeatureNum):
# 獲得劃分子集
retDataSet = splitDataSet(dataSet, i, featureList[k])
# 得到子集元組數量
retDataSetNum = len(retDataSet)
# 得到子集熵
retDataSetEntropy = calculateEntropy(retDataSet)
# 累加
splitedEntropy += (float(retDataSetNum)/float(trainingNum))*retDataSetEntropy
# 判斷當前劃分的信息增益是否大於maxInfoGain
if (wholeEntropy - splitedEntropy) > maxInfoGain:
maxInfoGain = wholeEntropy - splitedEntropy
splitAxis = i
else:
continue
return splitAxis
'''
函數功能:當劃分結束時,如果某一子集中的所有分類標籤還不相同,那麼
用這個函數,來選擇該空間中某一標籤值最多的作爲標籤,與kNN
算法中的相同
輸入:標籤list
輸出:標籤值最多的標籤
'''
def majorityCount(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys():
classCount[vote] = 0
classCount[vote] += 1
# sort
sortedClassCount = sorted(classCount.iteritems(),
key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
'''
函數功能:遞歸建立決策樹
輸入:給定數據集dataSet,劃分屬性名稱labels
輸出:決策樹(dictionary格式)
'''
def createTree(dataSet,labels):
# 獲得該dataSet的所有標籤
classList = [example[-1] for example in dataSet]
# 結束條件1:所有標籤相同,則返回該標籤
if classList.count(classList[0]) == len(classList):
return classList[0]
# 結束條件2:所有屬性已經劃分,返回最多數量的標籤
if len(dataSet[0]) == 1:
return majorityCount(classList)
# 若沒結束,則劃分
bestFeature = bestFeatureToSplit(dataSet)
bestFeatureLabel = labels[bestFeature]
# 用dictionary格式存儲該劃分
myTree = {bestFeatureLabel:{}}
# 從labels中刪除該屬性
del(labels[bestFeature])
# 獲得該劃分屬性的所有屬性值
featureValues = [example[bestFeature] for example in dataSet]
uniqueVals = set(featureValues) # 取唯一值
# 對每一屬性值遞歸求決策樹
for value in uniqueVals:
# 用subLabels存儲labels的值是因爲,遞歸的時候會修改list的內容
subLabels = labels[:]
myTree[bestFeatureLabel][value] = createTree(splitDataSet\
(dataSet,bestFeature,value),subLabels)
return myTree
'''
函數功能:創建數據集和屬性名稱
輸出:返回數據集和屬性名稱list
'''
def createDataset():
dataSet = [['Sunny','Hot','High','Weak','No'],
['Sunny','Hot','High','Strong','No'],
['Overcast','Hot','High','Weak','Yes'],
['Rain','Mild','High','Weak','Yes'],
['Rain','Cool','Normal','Weak','Yes'],
['Rain','Cool','Normal','Strong','No'],
['Overcast','Cool','Normal','Strong','Yes'],
['Sunny','Mild','High','Weak','No'],
['Sunny','Cool','Normal','Weak','Yes'],
['Rain','Mild','Normal','Weak','Yes'],
['Sunny','Mild','Normal','Strong','Yes'],
['Overcast','Mild','High','Strong','Yes'],
['Overcast','Hot','Normal','Weak','Yes'],
['Rain','Mild','High','Strong','No'],]
labels = ['Outlook','Temperature','Humidity','Wind']
return dataSet, labels
dataSet, labels = createDataset()
labelsCopy = labels[:]
decisionTree = createTree(dataSet,labels)
print (decisionTree)
'''
函數功能:根據得到的決策樹進行預測
輸入:決策樹inputTree,屬性名稱列表featLabels,測試數據testVector
輸出:預測分類標籤
'''
def classify(inputTree, featLabels, testVector):
# 決策樹是dictionary格式,所以取第一個鍵firstStr和值secondDict
firstStr = tuple(inputTree.keys())[0]
secondDict = inputTree[firstStr]
# 根據鍵(屬性)獲得其軸下標
featIndex = featLabels.index(firstStr)
# 循環遞歸判斷
for key in secondDict.keys():
if testVector[featIndex] == key:
# 如果鍵的類型是dict,說明是dictionary,還需遞歸判斷
if type(secondDict[key]).__name__ == 'dict':
classLabel = classify(secondDict[key],featLabels,testVector)
# 否則,判斷結束,直接返回
else:
classLabel = secondDict[key]
return classLabel
classLabel = classify(decisionTree,labelsCopy,['Sunny','Mild','High','Weak'])
print(classLabel)
</span>
程序的結構很簡單,創建決策樹的主函數是createTree(),分類函數是classify(),註釋也比較詳細,較好理解,值得提的是,createTree()函數返回的結果是嵌套的dictionary格式,比如{'outLook':{‘a’:{''...}, 'b': 'yes'}...}。然後classify()就是按照這個結構進行分類。這裏對其中遇到的新用法記錄一下:
1)其中兩次用到了featureValues = [example[bestFeature] for example in dataSet]這種用法,相對於我用的for然後判定要簡潔的多,以後多用這種用法來填充list;
2)其中用到了type()這個用法,在classify()函數中,它主要是返回括號內變量的類型,然後用classes.__name__用法來獲取變量的類型名稱,從而進行比較;
其實在《機器學習》書中,還有一些關於ID3算法的優化和討論,以及C4.5算法等,在上面提到的那個筆記中有簡單介紹,後面有時間的話會整理一篇關於決策樹的算法討論出來。