今天带来的是《机器学习实战》第三章 决策树的知识笔记
决策树
决策树也是目前最常使用的数据挖掘算法
k-近邻算法可以完成很多分类任务,但是它最大的缺点就是无法给出数据的内在含义,决策树的主要优势就在于数据形式非常容易理解
决策树的一个重要任务是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,并从中提取出一系列规则,在这些机器根据数据集创建规则时,就是机器学习的过程
专家系统中经常使用决策树,而且决策树给出结果往往可以匹敌在当前领域具有几十年工作经验的人类专家
优点:计算复杂度不高,输出结果易于理解,对中间值的缺失不敏感,可以处理不相关特征数据。 缺点:可能会产生过度匹配问题。
适用数据类型:数值型和标称型
决策树一般流程:
(1) 收集数据:可以使用任何方法。
(2) 准备数据:树构造算法只适用于标称型数据,因此数值型数据必须离散化。
(3) 分析数据:可以使用任何方法,构造树完成之后,我们应该检查图形是否符合预期。
(4) 训练算法:构造树的数据结构。
(5) 测试算法:使用经验树计算错误率。
(6) 使用算法:此步骤可以适用于任何监督学习算法,而使用决策树可以更好地理解数据
的内在含义。
和上一次分享的一样,把决策树代码贴出来,然后将运行的命令在下面给出,代码全部在自己电脑上跑通了,关于中间一些python2和py3的不一致也都改了过来,并进行了说明
决策树构造
1、信息增益,计算熵
划分数据集原则:将无序的数据变得有序
在划分数据集之前之后信息发生的变化称为信息增益,知道如何计算信息增益,我们就可以
计算每个特征值划分数据集获得的信息增益,获得信息增益最高的特征就是最好的选择
集 合信息的度量方式称为香农熵或者简称为熵,熵定义为信息的期望值
# 计算给定数据集的香农熵
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, 0, 'yes'],
[0, 1, 'no'],
[0, 0, 'no']]
labels = ['no surfacing', 'flippers']
return dataSet, labels
在用命令行运行trees文件时会出现错误,改成这个样子即可
import imp
import trees
imp.reload(trees)
myDat, labels = trees.createDataSet()
myDat
trees.calcShannonEnt(myDat)
myDat[0][-1]=‘maybe’
myDat
trees.calcShannonEnt(myDat)
2、划分数据集
# 按照给定特征划分数据集
# 三个输入参数:待划分的数据集、划分数据集的特征、需要返回的特征的值。
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
imp.relaod(trees)
myDat, labels = trees.createDataSet()
trees.splitDataSet(myDat, 0, 1)
trees.splitDataSet(myDat, 0, 0)
# 选择最好的数据集划分方式
# 实现选取特征,划分数据集,计算得出最好的划分数据集的特征
def chooseBestFeatureToSplit(dataSet):
# 判定当前数据集包含多少特征属性
numFeatures = len(dataSet[0]) - 1
# 计算了整个数据集的原始香农熵
baseEntropy = calcShannonEnt(dataSet)
bestInfoGain = 0.0
bestFeature = -1
# 第1个for循环遍历数据集中的所有特征。使用列表推导(List Comprehension)来创建新的列表,
# 将数据集中所有第i个特征值或者所有可能存在的值写入这个新list中
for i in range(numFeatures):
featList = [example[i] for example in dataSet]
uniqueVals = set(featList)
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
imp.reload(trees)
myDat, labels = trees.createDataSet()
trees.chooseBestFeatureToSplit(myDat)
myDat
3、构建递归决策树
在Python 3.x 里面,iteritems()方法已经废除了。在3.x里用 items()替换iteritems() ,可以用于 for 来循环遍历。
# 该函数使用分类名称的列表,
# 然后创建键值为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的列表变量,其中包含了数据集的所有类标签
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: {}}
# 当前数据集选取的最好特征存储在变量bestFeat中,得到列表包含的所有属性值
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
imp.reload(trees)
myDat, labels = trees.createDataSet()
myTree = trees.createTree(myDat,labels)
myTree
在python中使用Matplotlib注解绘制树形图
1、Matplotlib注解
注解工具annotations
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")
array_args = dict(arrowstyle="<-")
# 绘制带箭头的注解
# plotNode()函数执行了实际的绘图功能,该函数需要一个绘图区,该区域由全局变量createPlot.ax1定义。
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=array_args)
# createPlot()函数首先创建了一个新图形并清空绘图区,然后在绘图区上绘制两个代表不同类型的树节点,
# 后面我们将用这两个节点绘制树形图
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), ' ')
# plotNode(U'决策节点', (0.5, 0.1), (0.1, 0.5), decisionNode)
# plotNode(U'叶节点', (0.8, 0.1), (0.3, 0.8), leafNode)
plt.show()
import treePlotter
treePlotter.createPlot()
中文乱码问题解决方法
from pylab import *
mpl.rcParams[‘font.sans-serif’] = [‘SimHei’]
2、构造注解树
# 获取叶节点的数目和树的层数
def getNumLeafs(myTree):
numLeafs = 0
# 第一个关键字是第一次划分数据集的类别标签,附带的数值表示子节点的取值。
# 从第一个关键字出发,我们可以遍历整棵树的所有子节点。使用Python提供的type()函数可以判断子节点是否为字典类型
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
# 测试节点的数据类型是否为字典
if type(secondDict[key])==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])==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]
imp.reload(treePlotter)
treePlotter.retrieveTree(1)
myTree = treePlotter.retrieveTree(0)
treePlotter.getNumLeafs(myTree)
treePlotter.getTreeDepth(myTree)
‘dict_keys’ object is not subscriptable问题用list()转换为列表
type object ‘str’ has no attribute 'name’问题,版本问题
Python3中类型对象“ str ”没有“name”属性,所以我们需要将属性去掉。除此之外,这句判断的语句本来是判断该节点类型是否为dict(字典),但是因为加上了单引号(‘’),就变成字符串了,这样会产生其他错误,所以我们也需要将单引号去掉。
# 函数plotMidText()计算父节点和子节点的中间位置,并在此处添加简单的文本标签信息
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]
# 减少y偏移
plotTree.yOff = plotTree.yOff - 1.0/plotTree.totalD
for key in secondDict.keys():
if type(secondDict[key]) == 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
函数createPlot()在前面已经给出,这里就没有再次展出
imp.reload(treePlotter)
myTree = treePlotter.retrieveTree(0)
treePlotter.createPlot(myTree)
myTree[‘no surfacing’][3]=‘maybe’
myTree
treePlotter.createPlot(myTree)
测试和存储分类器
1、测试算法:使用决策树执行分类
def classify(inputTree, featLabels, testVec):
firstStr = list(inputTree.keys())[0]
secondDict = inputTree[firstStr]
# 使用index方法查找当前列表中第一个匹配firstStr变量的元素
featIndex = featLabels.index(firstStr)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]) == dict:
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel
import trees
myDat, labels = trees.createDataSet()
labels
myTree = treePlotter.retrieveTree(0)
myTree
trees.classify(myTree, labels, [1, 0])
trees.classify(myTree, labels, [1, 1])
2、使用算法:决策树的存储
构造决策树是很耗时的任务
最好能够在每次执行分类时调用已经构造好的决策树
使用Python模块pickle序列化对象
# 使用pickle模块存储决策树
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, encoding='utf-8')
trees.storeTree(myTree, ‘classifierStorage.txt’)
trees.grabTree(‘classifierStorage.txt’)
报错:①write() argument must be str, not bytes
改为fw = open(filename, ‘wb+’) 使用二进制
②’gbk’ codec can’t decode byte 0x80 in position 0: illegal multibyte sequence
改为fr = open(filename, ‘rb’)
return pickle.load(fr, encoding=‘utf-8’)
后面加一点东西,应该是格式问题
示例:使用决策树预测隐形眼镜类型
(1) 收集数据:提供的文本文件。
(2) 准备数据:解析tab键分隔的数据行。
(3) 分析数据:快速检查数据,确保正确地解析数据内容,使用createPlot()函数绘制
最终的树形图。
(4) 训练算法:使用3.1节的createTree()函数。
(5) 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例。
(6) 使用算法:存储树的数据结构,以便下次使用时无需重新构造树。
lenses.txt文件在书中有,随着源码下载
fr=open(‘lenses.txt’)
lenses=[inst.strip().split(‘\t’) for inst in fr.readlines()]
lensesLabels=[‘age’,‘prescript’, ‘astigmatic’, ‘tearRate’]
lensesTree=trees.createTree(lenses, lensesLabels)
lensesTree
treePlotter.createPlot(lensesTree)
过度匹配(overfitting)
为了减少过度匹配问题,我们可以裁剪决策树,去掉一些不必要的叶子节点。如果叶子节点只能增加少许信息,则可以删除该节点,将它并入到其他叶子节点中
本章使用的算法称为ID3
第2章、第3章讨论的是结果确定的分类算法,数据实例最终会被明确划分到某个分类中。