决策树
花了差不多三天时间,终于把《机器学习实战》这本书的第三章的决策树过了一遍,知道了决策树中ID3的一个具体编法和流程。
【一】计算数据信息熵
这段代码主要是用于计算数据的每个特征信息熵,信息熵用于描述数据的混乱程度,信息熵越大说明数据包含的信息越多,也就是数据的波动越大。而ID3算法采用的是信息增益作为计算指标来评价每个特征所包含的信息的多少,而信息增益方法对可取数值较多的特征有所偏好,即本身可取数值较多的特征本身就包含更多的信息,为了减少这种偏好,又有C4.5和CART树,C4.5用信息增益率来衡量数据特征,从而摒除了这种偏好;CART树使用了“基尼指数”来衡量特征,基尼指数越小说明他的数据纯度就越高,选取特征时选取划分后使基尼指数最小的特征作为划分标准,即选取特征后使数据集变得更加纯,从而减少了数据集本身的混乱程度,本身决策树做的事情就是这个。
#计算信息熵
def calcShannonEnt(dataSet):
##step 1:计算数据集中实例的总数
numEntries = len(dataSet)
## step 2: 统计每个类别出现的次数,并存放在一个字典中
labelCounts = {}
for featVec in dataSet:
currentLabel = featVec[-1] #获取每个实例中的最后一个元素,也就是当前的分类标签:
#计算每个类别出现的次数
if currentLabel not in labelCounts.keys():
labelCounts[currentLabel]=0
labelCounts[currentLabel] += 1
## step 3: 计算信息熵
shannonEnt = 0.0
for key in labelCounts:
#计算每个分类结果的概率P(某个结果的概率)=某个结果出现的次数/总数据条数
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
【三】划分数据集
选取特征的方法是计算每个特征的信息增益,然后再基于信息增益的值来选取信息增益最大的特征作为下一次决策树分类的依据,这样保证了每次选取的特征都能最大化程度的减少数据集的信息混乱程度。选取特征之后需要根据该特征下所包含的可能的取值将数据集进行切分。
##划分数据集(待划分的数据集、划分数据集的特征、特征的值)
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
抽取数据之后还牵扯到数据的添加,此段代码中用到了extend函数和append函数,都是做数据连接的,但是主要区别是append函数直接将连接的数据形成一个元素加入到列表中,而extend是将后段要加入的数据提取出其元素再加入到列表中去。
【四】选取数据集特征进行数据划分
选取特征的方法是计算每个特征的信息增益值,然后找到具有信息增益值最大的特征作为划分的依据进行数据集划分,具体代码为:
##选取最好的的数据集划分方式,也就是信息增益最大的特征
def chooseBestFeatureToSplit(dataSet):
#选取数据集中第一条数据的长度并减1(eg:[1,1,“yes”],最后一个元素为分类结果,需要排除结果,只取特征)
numFeatures = len(dataSet[0])-1
baseEntropy = calcShannonEnt(dataSet)#数据集的信息熵
bestInfoGain = 0.0
bestFeature = -1 #最好的特征,初始化为-1
for i in range(numFeatures):
featList = [example[i] for example in dataSet]#从数据集中将每条数据的第i个特征去除
uniqueVals = set(featList)#去重,得到第i个特征的所有取值
newEntropy = 0.0
#遍历第i个特征的每一个特征值,计算特征i的条件熵
for value in uniqueVals:
subDataSet = splitDataSet(dataSet, i, value)#根据第i个特征,并且i特征值为value,划分数据集
prob = len(subDataSet)/float(len(dataSet))#计算P(特征i取值为value)的概率
newEntropy += prob*calcShannonEnt(subDataSet) #计算条件熵
infoGain = baseEntropy-newEntropy #计算信息增益
#记录信息增益最大的特征
if(infoGain>bestInfoGain):
bestInfoGain = infoGain
bestFeature = i
return bestFeature
由于大部分代码都添加了注释,文中就不再细述
【五】寻找具有最大类别数量的叶节点
- 决策树构建结束的标准是该分支下面所有的数据都具有相同的分类,如果所有数据都具有相同的分类,则得到的叶子结点或者终止块,任何到达这个叶子结点的必属于这个分类。
- 但是分类过程中不可能到最后分支之后所有的数据集都属于同一类,因为存在噪声数据,所以我们是找到分类结束时某个类别最多的类别作为该分支的叶节点的取值,下述代码就是找到数量最多的那一类作为叶节点的取值
##寻找具有最大类别数量的叶节点
#针对所有特征都用完,但是最后一个特征中类别还是存在很大差异,无法进行划分,
#此时选取该类别中最多的类,作为划分的返回值,majoritycnt的作用就是找到类别最多的一个作为返回值
def majorityCnt(classList):
classCount={} #创建字典
for vote in classList:
if vote not in classCount.keys():
classCount[vote]=0 #如果现阶段的字典中缺少这一类的特征,创建到字典中并令其值为0
classCount[vote]+=1 #循环一次,在对应的字典索引vote的数量上加一
#利用sorted方法对class count进行排序,
#并且以key=operator.itemgetter(1)作为排序依据降序排序因为用了(reverse=True)
#3.0以上的版本不再有iteritems而是items
sortedClassCount = sorted(classCount.items(),key=operator.itemgetter(1),reverse=True)
return sortedClassCount[0][0]
其中用到了operator中的items函数,python3.x以上的版本不再有iteritems而是items,而《机器学习实战》代码时基于2.x的版本的代码,所以经常会报错,建议大家写代码时边写边思考,items() 方法是把dict对象转换成了包含tuple的list,栗子:
【六】递归构建决策树
- 构建决策树用到的最基本的思想是递归,而递归结束的条件有两个:(1)在该分支下所有的类标签都相同,即在该支叶节点下所有的样本都属于同一类;(2)使用了所有的特征,但是样本数据还是存在一定的分歧,这个时候就要使用上诉的majorityCnt(classList)函数了。
- 在树的构建过程中,是每一次利用chooseBestFeatureToSplit(dataSet)函数找到在剩下的数据集中信息增益最大的特征作为分类的依据,然后每次生成的tree是 myTree={bestFeatLabel:{}}这样的形式,保证了每次递归都在字典中存在子字典,从而最后完成决策树的构建。同时在构建过程中使用过的标签需要进行删除,从而不影响下一次特征的选取,同样也用到了set()函数。
##创建树
def createTree(dataSet,labels):
classList = [example[-1] for example in dataSet] #提取dataset中的最后一栏——种类标签
if classList.count(classList[0])==len(classList):#计算classlist[0]出现的次数,如果相等,说明都是属于一类,不用继续往下划分
return classList[0]
if len(dataSet[0])==1:#看还剩下多少个属性,如果只有一个属性,但是类别标签又多个,就直接用majoritycnt方法进行整理 选取类别最多的作为返回值
return majorityCnt(classList)
bestFeat = chooseBestFeatureToSplit(dataSet)#选取信息增益最大的特征作为下一次分类的依据
bestFeatLabel = labels[bestFeat] #选取特征对应的标签
myTree = {bestFeatLabel:{}}#创建tree字典,紧跟现阶段最优特征,下一个特征位于第二个大括号内,循环递归
del(labels[bestFeat]) #使用过的特征从中删除
featValues = [example[bestFeat] for example in dataSet]#特征值对应的该栏数据
uniqueVals = set(featValues)#找到featvalues所包含的所有元素,同名元素算一个
for value in uniqueVals:
subLabels = labels[:]#子标签的意思是循环一次之后会从中删除用过的标签 ,剩下的就是子标签了
myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)#循环递归生成树
return myTree
【七】使用matplotlib对生成的树进行注释
此过程中主要用到的是matplotlib中的annotate包,具体有很多方法和函数,我也只是过了一下书上的东西,有兴趣继续深入的可以参考:https://www.jianshu.com/p/1411c51194de
http://blog.csdn.net/u013457382/article/details/50956459
遇到的问题是中文边框转码之后不显示,英文边框之后自适应显示
import matplotlib.pyplot as plt
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(U'决策节点', (0.5, 0.1),(0.1,0.5), decisionNode)
plotNode(U'叶节点', (0.8,0.1),(0.3,0.8),leafNode)
plt.show()
【八】计算树的叶节点数目和树的层数
getNumLeafs()函数和getTreeDepth()函数在结构和方法上都比较类似,因为tree字典中的每一个大括号的第一个字符都是它对应的键值,所以我们只需要判断第二个还是不是键值,如果是的话说明还有树或者深度,都是用了递归的思想,在每次循环中都先找到该字典中的第一个字符,然后判断在该键值下的第二个字符是不是还是键,是的话说明还有深度或者层数,不是的话说明已经找完了。其中遇到了这样的问题:
注意:python2.x的版本中是firstStr=myTree.keys()[],但是dict_keys型的数据不支持索引,所以强制转换成list即可,即 firstStr=list(myTree.keys())[0]。
##获取叶节点的数目
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
【九】生成决策树图形
xOff和yOff用来记录当前要画的叶子结点的位置。
- xOff:
-(1) 画布的范围x轴和y轴都是0到1,我们希望所有的叶子结点平均分布在x轴上。totalW记录叶子结点的个数,那么 1/totalW正好是每个叶子结点的宽度
-(2) 如果叶子结点的坐标是 1/totalW , 2/totalW, 3/totalW, …, 1的话,就正好在宽度的最右边,为了让坐标在宽度的中间,需要减去0.5 / totalW 。所以createPlot函数中,初始化plotTree.xOff 的值为-0.5/plotTree.totalW。这样每次 xOff + 1/totalW,正好是下1个结点的准确位置 - yOff:yOff的初始值为1,每向下递归一次,这个值减去 1 / totalD
- cntrPt:cntrPt用来记录当前要画的树的树根的结点位置
#作用是计算tree的中间位置 cntrpt起始位置,parentpt终止位置,txtstring:文本标签信息
def plotMidText(cntrPt, parentPt, txtString):
xMid = (parentPt[0]-cntrPt[0])/2.0 + cntrPt[0]
yMid = (parentPt[1]-cntrPt[1])/2.0 + cntrPt[1]#找到x和y的中间位置
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#每绘制一次图,将y的坐标减少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
def createPlot(inTree):
fig = plt.figure(1, facecolor='white')
fig.clf()
axprops = dict(xticks=[], yticks=[])
# createPlot.ax1为全局变量,绘制图像的句柄,subplot为定义了一个绘图,111表示figure中的图有1行1列,即1个,最后的1代表第一个图
# frameon表示是否绘制坐标轴矩形
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()
【十】生成树之后利用测试数据测试代码
构建生成树之后需要利用测试数据测试代码的分类效果,具体代码如下:
#用于多条数据的测试,并且最后要计算出分类的准确率
def testEfficiency(inputTree, featLabels, testData):
flag = 0
for i in range(len(testData)):
result = classify(inputTree,featLabels,testData[i][:6])
if(result==testData[i][-1]):
flag +=1
print("the calculte result is %s, he true result is %s" % (result,testData[i][-1]))
print('the data number is %d,but the right number is %d'%(len(testData),flag))
代码也是使用了递归的思想,即每次都找到测试数据在每个特征下的值,根据值选取合适的分支,到了下一个分支再进行一次循环直到到了决策树的叶节点为止,从而将测试数据归类到对应的分类之下。
【十一】编写对于多数据的测试代码
这段代码是书上没有,主要是用于有多个数据集时的测试,相对简单,就是每次提取出测试数据中的一组测试数据用于代码的测试,并且最后计算出分类正确的个数和正确率。这里我是参考https://blog.csdn.net/cxjoker/article/details/79501887中的内容学习到的
##用于多条数据的测试,并且最后要计算出分类的准确率
def testEfficiency(inputTree, featLabels, testData):
flag = 0
for i in range(len(testData)):
result = classify(inputTree,featLabels,testData[i][:6])
if(result==testData[i][-1]):
flag +=1
print("the calculte result is %s, he true result is %s" % (result,testData[i][-1]))
print('the data number is %d,but the right number is %d'%(len(testData),flag))
【十二】存储与读取决策树
存储与读取决策树主要是用到了pickle模块,具体我也没有细细的去深究pickle模块,具体可以参考http://www.php.cn/python-tutorials-372984.html。
因为早期的pickle代码是二进制的pickle(除了最早的版本外)是二进制格式的,所以你应该带 ‘rb’ 标志打开文件。
##使用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)
【十三】利用决策树预测隐型眼镜类型
fr = open('lenses.txt')
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate']
lensesTree = trees.createTree(lenses,lensesLabels)
treePlotter.createPlot(lensesTree)
结果如图所示