一般流程:
(1)收集数据:可以使用任何方法。
(2)准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3)分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4)训练算法:构造树的数据结构。
(5)测试算法:使用经验树计算错误率。
(6)使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据的内在含义。
首先,评估每个特征,根据结果确定划分数据依据的数据特征。原始数据集就被划分为几个数据子集。这些数据子集会分布在第一个决策点的所有分支上。如果某个分支下的数据属于同一类型,则无需进一步对数据集进行分割。如果数据子集内的数据不属于同一类型,则需要重复划分数据子集的过程。如何划分数据子集的算法和划分原始数据集的方法相同,直到所有具有相同类型的数据均在一个数据子集内。
划分数据集的大原则是:将无序的数据变得更加有序。
信息增益
符号 的信息量:
熵:
其中n是分类的数目.
在划分数据集之前之后信息发生的变化。知道如何计算信息增益,我们就可以计算每个特征值划分数据集获得的信息增益。
熵:定义为信息的期望值。得到熵之后,我们就可以按照获取最大信息增益的方法划分数据集。
例子
trees.py
'''
功能:计算给定数据集的香农熵
'''
from math import log
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
def createDataSet():
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'no']]
labels = ['no surfacing','flippers']
return dataSet, labels
main.py
import trees
myDat, labels = trees.createDataSet()
print(trees.calcShannonEnt(myDat))
'''
熵越高,则混合的数据也越多
在数据集中添加更多的分类,观察熵是如何变化的。
'''
myDat[0][-1] = 'maybe'
print(trees.calcShannonEnt(myDat))
output:
0.9709505944546686
1.3709505944546687
在trees.py中添加:
#按照给定特征划分数据集
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
'''
遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的特征划分方式。
熵计算将会告诉我们如何划分数据集是最好的数据组织方式。
'''
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 #the last column is used for the labels
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0; bestFeature = -1
for i in range(numFeatures): #iterate over all the features
featList = [example[i] for example in dataSet]#create a list of all the examples of this feature
uniqueVals = set(featList) #get a set of unique values
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 #calculate the info gain; ie reduction in entropy
if (infoGain > bestInfoGain): #compare this to the best gain so far
bestInfoGain = infoGain #if better than current best, set to best
bestFeature = i
return bestFeature #returns an integer
main.py中添加
#测试splitDataSet()函数
print(trees.splitDataSet(myDat,0,1))
print(trees.splitDataSet(myDat,0,0))
#测试chooseBestFeatureToSplit()函数
print(trees.chooseBestFeatureToSplit(myDat))
print(myDat)
output
[[1, 'maybe'], [1, 'yes'], [0, 'no']]
[[1, 'no'], [1, 'no']]
0 #第0个特征是最好的用于划分数据集的特征。
[[1, 1, 'maybe'], [1, 1, 'yes'], [1, 0, 'no'], [0, 1, 'no'], [0, 1, 'no']]
工作原理总结:
得到原始数据集,然后基于最好的属性值划分数据集,由于特征值可能多于两个,因此可能存在大于两个分支的数据集划分。第一次划分之后,数据将被向下传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据。因此我们可以采用递归的原则处理数据集。
递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如果所有实例具有相同的分类,则得到一个叶子节点或者终止块。任何到达叶子节点的数据必然属于叶子节点的分类。
在trees.py中增加如下代码:
import operator
'''
该函数使用分类名称的列表,然后创建键值为classList中唯一值的数据字典。
字典对象存储了classList中每个类标签出现的频率。
最后利用operator操作键值排序字典,并返回出现次数最多的分类名称。
'''
def majorityCnt(classList):
classCount={}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
#创建树
def createTree(dataSet,labels): #参数:数据集和标签列表
classList = [example[-1] for example in dataSet]
if classList.count(classList[0]) == len(classList):
return classList[0]#stop splitting when all of the classes are equal
if len(dataSet[0]) == 1: #stop splitting when there are no more features in dataSet
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}} #字典myTree存储了树的所有信息
del(labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
#遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
for value in uniqueVals:
subLabels = labels[:] #copy all of labels, so trees don't mess up existing labels
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
return myTree
在main.py中添加:
#测试createTree()函数
myTree = trees.createTree(myDat,labels)
print(myTree)
output:
{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'maybe'}}}}
第一个关键字 no surfacing
是第一个划分数据集的特征名称,该关键字的值也是另一个数据字典。
第二个关键字是 no surfacing
特征划分的数据集,这些关键字的值是no surfacing
节点的子节点。
这些值可能是类标签,也可能是另一个数据字典。
如果值是类标签,则该子节点是叶子节点;
如果值是另一个数据字典,则子节点是一个判断节点。
这种格式结构不断重复就构成了整棵树。
依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进人叶子节点;最后将测试数据定义为叶子节点所属的类型。
在trees.py中添加:
#使用决策树的分类函数
def classify(inputTree,featLabels,testVec):
firstStr = list(inputTree)[0]
secondDict = inputTree[firstStr]
featIndex = featLabels.index(firstStr) #将标签换为索引
key = testVec[featIndex]
valueOfFeat = secondDict[key]
if isinstance(valueOfFeat, dict):
classLabel = classify(valueOfFeat, featLabels, testVec)
else: classLabel = valueOfFeat
return classLabel
在main.py中添加:
#测试classify()函数
trees.classify(myTree,labels,[1,0])
trees.classify(myTree,labels,[1,1])
output:
'no'
'yes'
比较上述输出结果。第一节点名为 no surfacing
,它有两个子节点:一个是名字为0的叶子节点,类标签为no; 另一个是名为flippers
的判断节点,此处进人递归调用,flippers
节点有两个子节点。
构造决策树是很耗时的任务,即使处理很小的数据集,如前面的样本数据,也要花费几秒的时间,如果数据集很大,将会耗费很多计算时间。然而用创建好的决策树解决分类问题,则何以很快完成。因此,为了节省计算时间,最好能够在每次执行分类时调用巳经构造好的决策树。
在trees.py中添加:
def storeTree(inputTree,filename):
import pickle
fw = open(filename,'wb')
pickle.dump(inputTree,fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename, 'rb')
return pickle.load(fr)
在main.py中添加:
trees.storeTree(myTree,'classifierStorage.txt')
trees.grabTree('classifierStorage.txt')
通过上面的代码,我们可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍,这也是决策树的优点之一。