第3章:Decision Tree
决策树:根据不同特征建立分支
优点:计算复杂度不高,输出结果容易理解,对中间值的缺失不敏感,可以处理不相关特征数据。
缺点:可能会产生overfitting。
P1:划分数据集,创建决策树
信息增益:划分数据集前后信息发生的变化称为信息增益。
熵entropy:集合信息的度量方式就称为熵。熵定义为信息的期望值。
如果袋分类的事务可能划分在多个分类中,则的信息定义为:,其中是选择该分类的概率。为了计算熵,需要计算所有类别所有可能值包含的信息期望值:
熵越高,则混合的数据也越多。得到熵以后,可以按照获取最大信息增益的方法划分数据集。
另一个度量集合无序程度的方法是基尼不纯度(Gini impurity):从一个数据集中随机选子项,度量其被错误分类到其他分组的概率。
计算熵:
def calcShannonEnt(dataSet):
numEntries = len(dataSet)
labelCounts = {}
for featVec in dataSet: # 每一行
currentLabel = featVec[-1] # 本行最后一列为特征
labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1 # 特征放入词典计算出现次数
shannonEnt = 0.0
for key in labelCounts:
prob = float(labelCounts[key])/numEntries # label出现总次数/数据集长度 得到出现概率
shannonEnt -= prob * log(prob, 2)
return shannonEnt
# sample
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
执行trees.calcShannonEnt(dataSet)得到
0.9709505944546686
划分数据集:
def splitDataSet(dataSet, axis, value): # 3个参数:待划分数据集,划分数据集特征,特征返回值
retDataset = []
for featVec in dataSet:
if featVec[axis] == value:
reducedFeatVec = featVec[:axis] # featVec是个list
reducedFeatVec.extend(featVec[axis+1:])
retDataset.append(reducedFeatVec)
return retDataset
补充知识:extend和append的区别
a = [1, 2, 3]
b = [4, 5, 6]
a.append(b) # [1, 2, 3, [4, 5, 6]]
a.extend(b) # [1, 2, 3, 4, 5, 6]
求出信息增益最大的列:
数据需要满足:1. 所有列表元素具有同样的长度(每一行都一样多) 2. 每一行最后一个元素是当前实例的类别标签
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 每行list减去1个特征
totalEntropy = calcShannonEnt(dataSet) # 整个数据集的熵
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet] # example[i] 提取第i列的特征list 看作横向遍历时候纵向取值
uniqueVals = set(featList) # 第i列的特征set
colEntropy = 0.0
for value in uniqueVals: # 当前列特征的熵总和
subDataSet = splitDataSet(dataSet, i, value)
prob = len(subDataSet)/float(len(dataSet))
colEntropy += prob * calcShannonEnt(subDataSet)
colInfoGain = totalEntropy - colEntropy # 当前列信息增益 = 数据集熵 - 当前列熵
if (colInfoGain > bestInfoGain): # 找到最大的信息增益,得出该列
bestInfoGain = colInfoGain
bestFeature = i
return bestFeature
得到最好属性值划分数据集,划分之后数据到下一个节点,还可以再次划分,所以可以采用递归的方式继续处理。
递归结束的条件:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。
如下图,圈起来的就是已经完成相同分类的了。
决策树算法也有很多种,比如C4.5和CART(TODO:我自己都还没有研究过T T),这些算法在运行时并不总是在每次划分分组时都会消耗特征。特征数目并不是每次划分数组都减少,所以算法在实际使用时候可能引起一定问题。
如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点。通常采用多数表决的方法决定该叶子节点的分类。
求出次数最多的分类名称:
def majorityCnt(classList):
classCount = {}
for vote in classList:
classCount[vote] = classCount.get(vote, 0) + 1
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
return sortedClassCount[0][0]
这里递归求出树……(花了我半天时间才大致看懂这一节递归传递T T我需要try harder!)
def createTree(dataSet, labels):
classList = [example[-1] for example in dataSet] # 每一行最后一列的分类,组成list
if classList.count(classList[0]) == len(classList): # 如果第一个元素的个数等于list长度,则无需继续划分
return classList[0] # 返回第一个分类标签
if len(dataSet[0]) == 1: # 只剩下最后一列分类时,就无需继续划分
return majorityCnt(classList) # 返回该列出现最多的分类
bestFeat = chooseBestFeatureToSplit(dataSet)
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel:{}}
del(labels[bestFeat]) # 从特征list删除当前特征,往后传递特征列表筛选
featValues = [example[bestFeat] for example in dataSet] # 当前特征列变成list
uniqueVals = set(featValues) # 该列特征去重组成set
for value in uniqueVals:
subLabels = labels[:] # 剩下的特征
subDataSet = splitDataSet(dataSet, bestFeat, value)
myTree[bestFeatLabel][value] = createTree(subDataSet, subLabels)
return myTree
执行treePlotter.createPlot()得到{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
新建一个python文件来画图(plot)
import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']
decisionNode = dict(boxstyle='sawtooth', fc='0.8')
leafNode = dict(boxstyle='round4', fc='0.8')
arrow_args = dict(arrowstyle='<-')
def plotNode(nodeTxt, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeTxt, xy = parentPt, xycoords = 'axes fraction',
xytext = centerPt, textcoords = 'axes fraction', va = 'center',
ha = 'center', bbox = nodeType, arrowprops = arrow_args)
def createPlot():
fig = plt.figure(1, facecolor='white')
fig.clf()
createPlot.ax1 = plt.subplot(111, frameon=False)
# plotNode('node name', arrow head, arrow tail, nodeType)
plotNode('AAA', (0.2, 0.1), (0.1, 0.5), decisionNode)
plotNode('BBB', (0.4, 0.4), (0.8, 0.6), leafNode)
plt.show()
执行treePlotter.createPlot()得到下面图片~
计算叶子节点个数和树的最大深度
def getNumLeafs(myTree): # 累计叶子节点的个数
numLeafs = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
numLeafs += getNumLeafs(secondDict[key])
else:
numLeafs += 1
return numLeafs
def getTreeDepth(myTree): # 计算最长树深度
maxDepth = 0
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
thisDepth = 1 + getTreeDepth(secondDict[key])
else:
thisDepth = 1
if thisDepth > maxDepth:
maxDepth = thisDepth
return maxDepth
下面画图这里有点难。。不过能循着规矩去发现也是通过递归。
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
createPlot.ax1.text(xMid, yMid, txtString)
def plotTree(myTree, parentPt, nodeTxt):
numLeafs = getNumLeafs(myTree)
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[0]
cntrPt = (plotTree.xOff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yOff)
plotMidText(cntrPt, parentPt, nodeTxt)
plotNode(firstStr, cntrPt, parentPt, decisionNode)
secondDict = myTree[firstStr]
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':
plotTree(secondDict[key], cntrPt, str(key))
else:
plotTree.xOff = plotTree.xOff + 1.0/plotTree.totalW
plotNode(secondDict[key], (plotTree.xOff, plotTree.yOff), cntrPt, leafNode)
plotMidText((plotTree.xOff, plotTree.yOff), cntrPt, str(key))
plotTree.yOff = plotTree.yOff + 1.0/plotTree.totalD
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops)
plotTree.totalW = float(getNumLeafs(inTree))
plotTree.totalD = float(getTreeDepth(inTree))
plotTree.xOff = -0.5/plotTree.totalW
plotTree.yOff = 1.0
plotTree(inTree, (0.5, 1.0), '')
plt.show()
可以通过特征值list得到数据集中第一个吻合此类特征的分类:
def classify(inputTree, featLabels, testVec): # testVec是list like
firstStr = list(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
比如trees.classify(myTree, labels, [1,1])的话,会得到分类结果yes。
构造决策树很耗时,即使处理很小的数据集。用pickle序列化,在每次执行分类时调用已经构造好的决策树。
def storeTree(inputTree, filename):
import pickle
fw = open(filename, 'wb+') # 在py2里面是'w',py3里面需要改成'wb+'
pickle.dump(inputTree, fw)
fw.close()
def grabTree(filename):
import pickle
fr = open(filename, 'rb') # 在py3中需要加上'rb',否则会报错读取了str而不是byte
return pickle.load(fr)
P2:通过决策树预测con类型
原数据集txt文件格式:
young myope no reduced no lenses
young myope no normal soft
young myope yes reduced no lenses
young myope yes normal hard
young hyper no reduced no lenses
读取文件,按空格拆分:
fr = open('lenses.txt')
lenses = [i.strip().split('\t') for i in fr.readlines()]
得到如下matrix:
组成特征标签:
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
创建树:
lensesTree = trees.createTree(lenses, lensesLabels)
{'tearRate': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
'young': 'soft'}},
'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
'presbyopic': 'no lenses',
'young': 'hard'}},
'myope': 'hard'}}}},
'reduced': 'no lenses'}}
画图:
treePlotter.createPlot(lensesTree)
可以从树看到这里最长只需要4个特征节点就能得出分类结果,也就是问4个问题能确定适合哪种类型的隐形眼镜。
决策树在根据数据集匹配后,因为这些选项很多,会出现overfitting的问题,所以会需要减少一些节点。
以上的称为ID3算法,用于划分标称型数据集,无法直接处理数值型数据,尽管可以通过量化把数值型数据转为标称型数据。构建决策树时,采用递归方法将数据集转化为决策树。
补充知识:
标称型数据:在有限数据中取值,比如“是”和“否”,一般用于分类。
数值型数据:在无限数据中取值,比如1.25,主要用于回归分析。
自我小吐槽:
这一节的思路可能比较不容易融入自己的知识中,不好记住并转化为自己在处理数据时可用。但我相信多练习会越来越好的~~加油!