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算法等,在上面提到的那个笔记中有简单介绍,后面有时间的话会整理一篇关于决策树的算法讨论出来。