决策树——机器学习分类问题

决策树

同样是完成多分类任务:k-近邻算法无法给出数据的内在含义,决策树的优势在于形式易于理解!
决策树正如他的名字,可以用于决策,例如专家系统。
决策树的重要任务是为了数据中所蕴含的知识信息,因此决策树可以使用不熟悉的数据集合,从中提取出一系列规则,提取规则的过程就是机器学习的过程

  • 讨论构造决策树的方法
  • 如何编写构造树的Python代码
  • 提出一些度量算法成功率的方法
  • 使用递归建立分类器
  • 使用Matplotlib绘制决策树图

01决策树的构造

划分数据集可以选择很多特征,那么关键问题在于现在使用哪个特征能得到最好的划分结果,因此需要对特征进行评估!
根据该特征进行数据集划分会得到很多分支,如果某个分支下的数据属于同一类型,则已经正确分类,如果不属于同一类型,继续选取此时的最好特征继续划分!
最后:所有具有相同类型的数据均在同一个数据子集内。
对决策树构造过程进行总结:

  1. 评估最好特征
  2. 划分分支
  3. 对分支继续评估最好特征+划分分支
    什么时候结束:相同类型数据在同一个子集中
    伪代码createBranch

我们采用ID3算法划分数据集,如果依据某个属性可能会产生4个可能的值,我们就把数据划分成4块,并创建4个不同的分支,每次划分数据集我们只选用1个特征属性。现在我们该考虑的问题就是:我们首先选择哪个特征去划分数据集呢?也就是如何评估最好特征?

01.1信息增益——评估最好特征的依据

划分数据集的最大原则:将无序数据变得更加有序。
在划分数据之前之后信息发生的变化称为信息增益,那么信息增益怎么用呢?
信息增益的计算:
熵entropy:信息的期望值(其实就是类似均值)
符号xi的信息定义为:l(xi) = -log2p(xi)
上式中p(xi)是选择该分类的概率
所有类别所有可能值包含的信息期望值:

所有类别所有可能值包含的信息期望值

n是指按照这个特征分类产生的类数目。

计算熵选取特征

  • 计算不同特征带来的信息增益
  • 选择信息增益最高的特征值
#计算给定数据集的香农熵
def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet: #the the number of unique elements and their occurance
        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) #log base 2
    return shannonEnt
海洋生物数据

对于以上海洋生物数据,我们可以将数据构造成数据集和标签。再利用该数据验证calcShannonEnt()的准确性。

def createDataSet():
    dataSet = [[1, 1,'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no'],
    ]
    lables = ['no surfacing', 'flippers']
    return dataSet,lables

#用来本地测试
if __name__ == "__main__":
    myDat, lable = createDataSet()
    print(myDat)
    print(calcShannonEnt(myDat))

如果我们给数据集添加更多的分类,熵会增加,因为混乱度增加了!
例如,再本地测试时增加这行代码:myDat[0][-1] = 'maybe'
分类除了yes和no之外,又多了maybe,混乱都增加,熵也从原来的0.9709505944546686变成了1.3709505944546687

0.1.2 划分数据集

entropy可以度量数据集的无序程度,我们将对每个特征划分数据集的结果计算一次信息熵,然后判断哪个特征划分数据集是最好的!

#按照给定特征划分数据集
def splitDataSet(dataSet, axis, value):
    retDataSet = [] #创建新的list对象
    for featVec in dataSet:
        if featVec[axis] == value:
            #将符合特征的数据抽取出来加入retDataSet
            reducedFeatVec = featVec[:axis]     #chop out axis used for splitting
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

这个划分数据集的函数使用了3个参数:

  • 待划分的数据集dataSet
  • 划分数据集的特征axis
  • 需要返回的特征的值value
    value的含义是:dataSet中某条数据的axis位置的值等于value时,就将这条数据加入某一个分支。
    需要注意的是,Python语言不用考虑内存分配问题。Python语言在函数中传递的是列表的引用,在函数内部对列表对象的修改,将会影响该列表对象的整个生存周期。为了消除这个不良影响,我们需要在函数的开始声明一个新列表对象。

接下来,如何选择最好的数据集划分方式呢?

# 选择最好的数据集划分方式
def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #特征数,最后一列是标签
    baseEntropy = calcShannonEnt(dataSet) #计算初始熵,为计算信息增益做准备
    bestInfoGain = 0.0; bestFeature = -1 #初始信息增益记为0,最佳特征暂时是-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                      #返回整数:最佳特征的列号

0.1.3 递归构建决策树

工作原理:得到原始数据集,然后基于最好的属性划分数据集,由于特征值可能多于2个,因此可能存在大于两个分支节点的数据集划分。第一次划分之后,数据将被传递到树分支的下一个节点,在这个节点上,我们可以再次划分数据,因此我们可以采取递归的原则处理数据集。
递归出口:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类
如果数据集已经处理了所有的属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点,在这种情况下,我们会采用多数表决的方法决定该叶子节点的分类。

# 多数表决
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)
    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[:]       #copy all of labels, so trees don't mess up existing labels
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree

0.2 在Python中使用Matplotlib注解绘制树形图

0.1节,我们学习了从数据集中创建树,然而字典的形式非常不易于理解,直接绘图也很难,本节我们使用Matplotlib库创建树形图——直观且易于理解

0.2.1Matplotlib注解+绘制树节点

提供注解工具类:annotations,可以在数据图形上添加文本注释,并且可以指定注释的位置。如图所示,在坐标(0.2, 0.1)的位置有一个点,我们将对该点的描述信息放在(0.35,0.3)的位置,并用箭头指向数据点(0.2, 0.1):


Matplotlib注解示例

使用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 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) #ticks for demo puropses
    plotNode('a decision node', (0.5, 0.1), (0.1, 0.5), decisionNode)
    plotNode('a leaf node', (0.8, 0.1), (0.3, 0.8), leafNode)
    plt.show()
函数plotNode的例子

0.2.2 构造注解树

我们有x和y坐标,但是如何放置树节点是个问题,我们需要知道有多少叶节点以确定x轴的长度;我们还需要知道树有多少层,以便确定y轴的高度。因此,我们构造两个新函数getNumLeafs()和getTreeDepth(),来获取叶节点的数目和树的层数。

def getNumLeafs(myTree):
    numLeafs = 0
    firstStr = list(myTree.keys())[0]  #请注意,python3在这里需要转list类型
    secondDict = myTree[firstStr]
    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
            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':#test to see if the nodes are dictonaires, if not they are leaf nodes
            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]

我们在用书中代码运行时,会报错
TypeError: 'dict_keys' object is not subscriptable
遇到这个问题,其实是python3中keys不允许切片,先转list再切片就好了!

简单数据集绘制的树形图

0.3 测试和存储分类器

我们学习机器学习是为了能够获得预测能力。前面学的这些东西只是解决了如何构建决策树+如何使用python函数库绘制树形图。为了了解数据的真实含义,下面将重点转移到如何利用决策树执行数据分类上。
本节内容:

  • 使用决策树构建分类器
  • 实际应用中如何存储分类器。

0.3.1 使用分类器执行分类

依靠训练数据构造了决策树之后,我们可以将它用于实际数据的分类。在执行数据分类时,需要决策树以及用于构造树的标签向量。然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入叶子节点;最后将测试数据定义为叶子节点所属的类型。

# 执行分类
def classify(inputTree,featLabels,testVec):
    firstStr = list(inputTree.keys())[0] #转list类型
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    if isinstance(valueOfFeat, dict):
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    return classLabel

第一节点名为no surfacing,它有两个子节点:一个是名字为0的叶子节点,类标签为no;另一个是名为flippers的判断节点,此处进入递归调用,flippers节点有两个子节点。以前绘制的树形图和此处代表树的数据结构完全相同。

0.3.2 使用算法:决策树的存储

决策树构造好后,需要存储,避免每次使用都重新构造,如何存储?
使用python模块pickle序列化对象,序列化对象可以在磁盘上保存对象,并在需要的时候读取出来,任何对象都可以执行序列化操作,包括字典对象

def storeTree(inputTree, filename):
    import pickle
    fw = open(filename, 'wb') #TypeError: write() argument must be str, not bytes 改成wb能否行?
    pickle.dump(inputTree, fw)
    fw.close()


def grabTree(filename):
    import pickle
    fr = open(filename,'rb') #改成"rb",即可正常运行
    return pickle.load(fr)

使用pycharm,一开始可能存在进制错误。类似这个 python提示错误TypeError: write() argument must be str, not bytes
当我们将写入模式更改为二进制之后,那么读取也应该以二进制格式读取!
这时候咱们可以补充补充python文件相关知识。Python3 输入和输出

就是这些知识啦

这样,我们可以将分类器存储在硬盘上,而不用每次对数据分类时重新学习一遍,这也是决策树的优点之一!

0.4 示例:使用决策树预测隐形眼镜类型

使用小数据集,我们就可以来利用决策树学到很多知识:眼科医生如何判断患者需要佩戴的镜片类型。一旦了解了决策树的工作原理,我们甚至也可以帮助人们判断需要佩戴的镜片类型。
步骤如下:

  1. 收集数据:提供文本文件
  2. 准备数据:解析tab键分隔的数据行
  3. 分析数据:快速检查数据,确保正确解析数据内容,使用creatPlot()函数绘制最终的树形图
  4. 训练算法:使用createTree()函数
  5. 测试算法:编写测试函数验证决策树可以正确分类给定的数据实例
  6. 使用算法:存储树的数据结构,以便下次使用树时无需重新构造树

隐形眼镜包括硬材质、软材质以及不适合佩戴隐形眼镜。数据存储在lenses.txt中,最后一行调用creatPlot()函数绘制了树形图。

if __name__ == "__main__":
    fr = open("lenses.txt")
    lenses = [inst.strip().split("\t") for inst in fr.readlines()]
    lensesLables = ['age', 'prescript', 'astigmatic', 'tearRate']
    lensesTree = createTree(lenses,lensesLables)
    print(lensesTree)
    treePlotter.createPlot(lensesTree)
由ID3算法产生的决策树

决策树非常好的匹配了实验数据,但是可能这些匹配选项太多了,我们将这个问题称为overfitting!为了减少过度匹配,我们可以裁剪掉一些不必要的叶子节点,如果叶子节点只能增加少量信息,则可以删除这些节点,并将它加入到其他叶子节点中!

决策树算法总结

决策树分类器就像流程图一样,终止块表示分类结果,开始处理数据集时,我们首先需要测量集合中数据的不一致,也就是熵(计算不同分类的信息期望),然后寻找最优方案划分数据集(需要信息增益最高),经过多次递归划分数据集,直到所有数据属于同一分类。ID3算法可以用于划分标称型数据集。一般我们并不构造新的数据结构,而是使用Python语言内嵌的数据结构字典存储树节点信息。
使用Matplotlib的注解功能,我们可以将存储的树结构转化为容易理解的图形。Python语言的pickle模块可用于存储决策树的结构。隐形眼镜的例子表明决策树可能会产生过多的数据集划分,从而产生过度匹配数据集的问题。我们可以通过裁剪决策树,合并相邻的无法产生大量信息增益的叶节点,消除过度匹配问题。

更精简总结:

  1. 计算熵(entropy)
  2. 计算不同特征划分数据集带来的信息增肌(information gain)
  3. 划分处数据子集后,递归前两步操作
  4. 递归边界:没有更多的特征可以使用 / 子集中的数据标签已经一样
  5. 划分数据集,构造决策树,图形化决策树
  6. 存储决策树使用pickle模块
  7. 决策树存在overfitting问题待解决
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,012评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,628评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,653评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,485评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,574评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,590评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,596评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,340评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,794评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,102评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,276评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,940评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,583评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,201评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,441评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,173评论 2 366
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,136评论 2 352

推荐阅读更多精彩内容