机器学习实战之决策树

决策时是一个分类算法。本文主要讲了一下决策树的构造以及用绘图的形式把决策树绘画出来。

决策树的构造

本文使用ID3算法来划分数据集,通过计算每一个特征的香农熵来选取最优划分数据集的特征,之后在递归的构造决策树来遍历每一个特征。

下面公式是计算香农熵,p(xi)是选择该分类的概率 ,n是分类的数目。

img

算法步骤:
  1. 利用calcShannonEnt函数计算原始数据的原始香农熵,即最后的一个特征来划分计算出来的香农熵,如下方的’yes’,’no’的特征计算出来的香农熵。
  2. 利用 chooseBestFeatureToSplit函数来计算每一个特征的香农熵,选取最大的香农熵的那一个特征来进行数据划分和构造决策树.
  3. 利用splitDataSet函数将该特征去掉的数据集继续遍历递归的计算每个特征的香农熵构造决策树。
决策树的存储:
  1. 使用字典来构造存储决策树的信息,之后可以用pickle模块来存储决策树。

下面是构造决策树的具体代码。 使用的是python3。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# -*- coding: UTF-8 -*-
from math import log
from treePlotter import retrieveTree, createPlot
import operator
# ID3决策树算法

# 测试数据集
def createDataSet(): # 列表每一项最后一列为类别标签
dataSet = [[1, 1, 'yes'],
[1, 1, 'yes'],
[1, 0, 'no'],
[0, 1, 'no'],
[0, 1, 'yes']
]
labels = ['no surfacing', 'flippers']
return dataSet, labels


# 计算给定数据的香农熵
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


# 划分数据集 按axis列来划分 之后将axis列去掉
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) # 注意append和extend 方法的区别
return retDataSet


# 选择最好的数据集方式划分 香农熵越大代表选择该特征分类更好,选取最大的香农熵。
def chooseBestFeatureToSplit(dataSet):
numFeatures = len(dataSet[0]) - 1 # 特征数量
baseEntropy = calcShannonEnt(dataSet) # 原始香农熵
bestInfoGain = 0.0
bestFeature = -1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList) # set() 函数创建一个无序不重复元素集
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
if (infoGain > bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature


# 返回出现次数最多的分类名称
def majorityCnt(classList):
classCount = {}
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0
classCount[vote] += 1
# sortedClassCount = sorted(classCount.iteritems (), key=operator.itemgetter(1), reverse=True) # python 2.7的写法
sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1),
reverse=True) # python3 中iteritems 改为 items
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]
if len(dataSet[0]) == 1:
return majorityCnt(classList)

bestFeat = chooseBestFeatureToSplit(dataSet) # 选择最优的分类即香农熵最大的特征。
bestFeatLabel = labels[bestFeat]
myTree = {bestFeatLabel: {}} # 字典型存储树
del (labels[bestFeat])
featValues = [example[bestFeat] for example in dataSet]
uniqueVals = set(featValues)
for value in uniqueVals:
subLabels = labels[:]
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
return myTree


def classify(inputTree, featLabels, testVec):
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


# 决策树的存储
def storeTree(inputTree, filename):
import pickle
with open(filename, 'wb') as fw:
pickle.dump(inputTree, fw)


def grabTree(filename):
import pickle
fr = open(filename, 'rb') # 注意加上‘rb’,否则会读不出来
return pickle.load(fr)


if __name__ == '__main__':
# myDat, labels = createDataSet()
# myTree = retrieveTree(0)
# print(classify(myTree, labels, [1, 0]))
# storeTree(myTree, 'classifierStorage.txt')
# print(grabTree('classifierStorage.txt'))
# # print(splitDataSet(myDat,0,1))
# # print(chooseBestFeatureToSplit(myDat))
# # print(createTree(myDat, labels))

# 隐形眼镜的例子
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
lensesTree = createTree(lenses, lensesLabels)
createPlot(lensesTree)

决策树的树形图绘制

构造出决策树了然而字典的表示形式不易于理解,接下来使用Matplotlib库创建树形图。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# coding:utf-8
import matplotlib.pyplot as plt
import matplotlib

plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")


def plotNode(nodeText, centerPt, parentPt, nodeType):
createPlot.ax1.annotate(nodeText, xy=parentPt, xycoords='axes fraction', xytext=centerPt,
textcoords='axes fraction', va='center', ha='center', bbox=nodeType, arrowprops=arrow_args)


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 # x偏移
plotTree(inTree, (0.5, 1.0), '') # 绘制决策树
plt.show()
# plotNode(U'决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode)
# plotNode(U'叶节点', (0.8, 0.1), (0.3, 0.8), leafNode)
# plt.show()


# 获取叶节点的数目
def getNumLeafs(myTree):
numLeafs = 0
firstStr = list(myTree.keys())[0] # python3 和书中代码不一样,因为python3改变了dict.keys,返回 的是一个对象。
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 retrieveTree(i):
listOfTrees = [{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}},
{'no surfacing': {0: 'no', 1: {'flippers': {0: {'head': {0: 'no', 1: 'yes'}}, 1: 'no'}}}}]
return listOfTrees[i]


def plotMidText(cntrPt, parentPt, txtString): # 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 = next(iter(myTree)) # 下个字典
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 # y偏移
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


if __name__ == '__main__':
pass
# # retrieveTree(1)
# myTree = retrieveTree(0)
# # print(getNumLeafs(myTree))
# # print(getTreeDepth(myTree))
# createPlot(myTree)

总结

决策树的优点
  • 决策树易于理解和解释,可以可视化.

  • 几乎不需要数据预处理。其他方法经常需要数据标准化,创建虚拟变量和删除缺失值。决策树还不支持缺失值。

  • 使用树的花费(例如预测数据)是训练数据点(data points)数量的对数。
  • 可以同时处理数值变量和分类变量。其他方法大都适用于分析一种变量的集合。
  • 可以处理多值输出变量问题。
  • 使用白盒模型。如果一个情况被观察到,使用逻辑判断容易表示这种规则。相反,如果是黑盒模型(例如人工神经网络),结果会非常难解释。
  • 即使对真实模型来说,假设无效的情况下,也可以较好的适用。
决策树的缺点
  • 决策树可能会产生过多的数据集划分,从而产生过度匹配数据集的问题,可以通过裁剪决策树,合并相邻的无法产生大量信息增益的叶节点,从而消除过度匹配问题。

  • 决策树学习可能创建一个过于复杂的树,并不能很好的预测数据。也就是过拟合。修剪机制(现在不支持),设置一个叶子节点需要的最小样本数量,或者数的最大深度,可以避免过拟合。

  • 决策树可能是不稳定的,因为即使非常小的变异,可能会产生一颗完全不同的树。这个问题通过decision trees with an ensemble来缓解。
  • 学习一颗最优的决策树是一个NP-完全问题under several aspects of optimality and even for simple concepts。因此,传统决策树算法基于启发式算法,例如贪婪算法,即每个节点创建最优决策。这些算法不能产生一个全家最优的决策树。对样本和特征随机抽样可以降低整体效果偏差。
  • 概念难以学习,因为决策树没有很好的解释他们,例如,XOR, parity or multiplexer problems.
  • 如果某些分类占优势,决策树将会创建一棵有偏差的树。因此,建议在训练之前,先抽样使样本均衡。

还有其他决策树的构造算法,最流向的是C4.5和CART。本文是ID3构造算法。

本文参考:https://blog.csdn.net/c406495762/article/details/76262487

-------------The End-------------