注:此系列文章里的部分算法和深度学习笔记系列里的内容有重合的地方,深度学习笔记里是看教学视频做的笔记,此处文章是看《机器学习实战》这本书所做的笔记,虽然算法相同,但示例代码有所不同,多敲一遍没有坏处,哈哈。(里面用到的数据集、代码可以到网上搜索,很容易找到。)。Python版本3.6
机器学习十大算法系列文章:
机器学习实战笔记1—k-近邻算法
机器学习实战笔记2—决策树
机器学习实战笔记3—朴素贝叶斯
机器学习实战笔记4—Logistic回归
机器学习实战笔记5—支持向量机
机器学习实战笔记6—AdaBoost
机器学习实战笔记7—K-Means
机器学习实战笔记8—随机森林
机器学习实战笔记9—人工神经网络
此系列源码在我的GitHub里:https://github.com/yeyujujishou19/Machine-Learning-In-Action-Codes
一,算法原理:
决策树:是一种基本的分类和回归方法。它是基于实例特征对实例进行分类的过程,我们可以认为决策树就是很多if-then的规则集合。
信息熵:香农给出了这个公式:H=-(P1logP1+P2logP2+....) (P(X)为出现的概率),变量的不确定性越大,熵也就越大,把它弄清楚所需要的信息量也就越大。
说太多文字不容易理解,看一个例子就明白了深度学习基础课程1笔记-决策树(Desision Tree)
二,算法的优缺点:
优点:
1)计算复杂度不高
2)输出结果易于理解
3)对中间值的缺失不敏感
4)可以处理不相关特征数据
缺点:
1)可能会产生过度匹配问题
适用数据类型:数值型和标称型
标称型:一般在有限的数据中取,而且只存在‘是’和‘否’两种不同的结果(一般用于分类)
数值型:可以在无限的数据中取,而且数值比较具体化,例如4.02,6.23这种值(一般用于回归分析)
三,实例代码:
1)计算信息熵函数
#计算信息熵 sum=-p1*logp1-p2*logp2-p3*logp3...
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 #已有该类别,则计数加1
shannonEnt = 0.0 #信息熵变量初始化
for key in labelCounts:
prob = float(labelCounts[key])/numEntries #计算概率
shannonEnt -= prob * log(prob,2) #log base 2
return shannonEnt
输入测试集,计算信息熵
from math import log
#产生数据集合标签
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
dataSet, labels=createDataSet()
shannonEnt=calcShannonEnt(dataSet)
print("原数据为:",dataSet)
print("标签为:",labels)
print("香农熵为:",shannonEnt)
计算结果为:
2)划分数据集
'''
函数功能:按照给定特征划分数据集
dataSet :待划分的数据集
axis :划分依据的特征所在下标
value :划分依据的特征
返回结果:返回axis处,所有值为value的数据集
'''
def splitDataSet(dataSet, axis, value):
retDataSet = []
for featVec in dataSet:
if featVec[axis] == value: #查找特征数据指定维度中等于value的值
#下面两句主要功能就是剔除原数据中axis轴的特征数据
reducedFeatVec = featVec[:axis] #取0到aixs(不包括axis)的数据
reducedFeatVec.extend(featVec[axis+1:]) #取axis+1到最后的数据
retDataSet.append(reducedFeatVec) #加入到列表中
return retDataSet
#测试
dataSet, labels = createDataSet()
print("原数据为:",dataSet)
print("标签为:",labels)
split = splitDataSet(dataSet,0,1) #找第0维为1的数据
print("划分后的结果为:",split)
计算结果为:
找出了第0维为1的数据,返回的数据集不包含第0维数据
3)下面遍历整个数据集,循环计算香农熵和splitDataSet()函数,找到最好的划分方式:
#选择最好的数据集划分方式,返回该特征所在列
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) #保留唯一值,去掉重复的
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 #返回最佳特征所在列
#测试
dataSet, labels=createDataSet()
bestFeature=chooseBestFeatureToSplit(dataSet)
print(bestFeature)
计算结果为:
说明以第0维的特征划分最好
4)递归构建决策树
基于之前的分析,我们选取划分结果最好的特征划分数据集,由于特征很可能多与两个,因此可能存在大于两个分支的数据集划分,第一次划分之后,可以将划分的数据继续向下传递,如果将每一个划分的数据看成是原数据集,那么之后的每一次划分都可以看成是和第一次划分相同的过程,据此我们可以采用递归的原则处理数据集。递归结束的条件是:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都有相同的分类。编程实现:
import operator
#统计各类的数量,按条件返回指定值
def majorityCnt(classList):
classCount={} #创建字典
for vote in classList:
if vote not in classCount.keys(): classCount[vote] = 0 #如果该类不在字典中,则创建该类,初始值为0
classCount[vote] += 1 #对应类值加1
sortedClassCount = sorted(classCount.iteritems(), 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] #返回该类别
if len(dataSet[0]) == 1: #如果数据集中只有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
#测试
dataSet, labels=createDataSet()
myTree=createTree(dataSet,labels)
print(myTree)
计算结果为:
5)使用matplotlib注解绘制树形图
如4)我们已经从数据集中成功的创建了决策树,但是字典的形式非常的不易于理解,因此本节采用Matplotlib库创建树形图。
import matplotlib.pyplot as plt
#定义文本框和箭头格式
decisionNode = dict(boxstyle="sawtooth", fc="0.8")
leafNode = dict(boxstyle="round4", fc="0.8")
arrow_args = dict(arrowstyle="<-")
#获取树的叶子数
def getNumLeafs(myTree):
numLeafs = 0 #存储叶子数变量
firstStr = list(myTree.keys())[0]
secondDict = myTree[firstStr]
for key in secondDict.keys():
if type(secondDict[key]).__name__=='dict':#查看节点是否是dictonaires,如果不是,则它们是叶节点
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':#查看节点是否是dictonaires,如果不是,则它们是叶节点
thisDepth = 1 + getTreeDepth(secondDict[key])
else: thisDepth = 1
if thisDepth > maxDepth: maxDepth = thisDepth
return maxDepth
#绘制带箭头的注解
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 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, va="center", ha="center", rotation=30)
def plotTree(myTree, parentPt, nodeTxt):#if the first key tells you what feat was split on
numLeafs = getNumLeafs(myTree) #this determines the x width of this tree
depth = getTreeDepth(myTree)
firstStr = list(myTree.keys())[0] #the text label for this node should be this
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':#test to see if the nodes are dictonaires, if not they are leaf nodes
plotTree(secondDict[key],cntrPt,str(key)) #recursion
else: #it's a leaf node print the leaf node
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 you do get a dictonary you know it's a tree, and the first element will be another dict
#创建树,并显示
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
createPlot.ax1 = plt.subplot(111, frameon=False, **axprops) #no ticks
#createPlot.ax1 = plt.subplot(111, frameon=False) #ticks for demo puropses
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()
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]
#测试
mytree = retrieveTree(0)
print(mytree)
createPlot(mytree)
计算结果为:
6)测试算法
在本章中,我们首先使用决策树对实际数据进行分类,然后使用决策树预测隐形眼镜类型对算法进行验证。
(a)使用决策树执行分类
在使用了训练数据构造了决策树之后,我们便可以将它用于实际数据的分类:
###决策树的分类函数,返回当前节点的分类标签
def classify(inputTree, featLabels, testVec): ##传入的数据为dict类型
firstSides = list(inputTree.keys())
firstStr = firstSides[0] # 找到输入的第一个元素
##这里表明了python3和python2版本的差别,上述两行代码在2.7中为:firstStr = inputTree.key()[0]
secondDict = inputTree[firstStr] ##建一个dict
# print(secondDict)
featIndex = featLabels.index(firstStr) # 找到在label中firstStr的下标
for i in secondDict.keys():
print(i)
for key in secondDict.keys():
if testVec[featIndex] == key:
if type(secondDict[key]) == dict: ###判断一个变量是否为dict,直接type就好
classLabel = classify(secondDict[key], featLabels, testVec)
else:
classLabel = secondDict[key]
return classLabel ##比较测试数据中的值和树上的值,最后得到节点
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]
# 测试
myData, labels = createDataSet()
print(labels)
mytree = retrieveTree(0)
print(mytree)
classify = classify(mytree, labels, [1, 0])
print(classify)
计算结果为:
(b)使用决策树预测隐形眼镜类型
基于之前的分析,我们知道可以根据决策树学习到眼科医生是如何判断患者需要佩戴的眼镜片,据此我们可以帮助人们判断需要佩戴的镜片类型。
在此从UCI数据库中选取隐形眼镜数据集lenses.txt,它包含了很多患者眼部状况的观察条件以及医生推荐的隐形眼镜类型。我们选取此数据集,结合Matplotlib绘制树形图,进一步观察决策树是如何工作的,具体的代码如下:
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['ages','prescript','astigmatic','tearRate']
lensesTree = creatTree(lenses,lensesLabels)
print(lensesTree)
createPlot(lensesTree)
计算结果为:
沿着决策树的不同分支,我们可以得到不同患者需要佩戴的隐形眼镜类型,从该图中我们可以得到,只需要问四个问题就可以确定出患者需要佩戴何种隐形眼镜。
欢迎扫码关注我的微信公众号