ML刻意练习第1周之决策树算法

决策树的解释:
1、决策树是基于树结构进行决策的
2、一般地,一棵决策树包含一个根结点、若干个内部结点和若干个叶结点。叶结点对应于决策结果,其他每个结点则对应于一个属性测试。
3、每个结点包含的样本集合根据属性测试的结果被划分到子结点中;根结点包含样本全集。
4、从根结点到每个叶结点的路径对应了一个判定测试序列。
常用的决策树算法有ID3,C4.5,CART算法等,本练习所采用的是最简单的ID3算法。

一步步构建最简单的决策树

1.构建决策树时,要根据对不同特征的判断来不断将数据集进行划分,所以我们首先面临的第一个问题就是如恶化选择特征值呢?即我们应该优先选择哪个特征值用来对那么庞大的数据进行划分呢?
我们首先引入熵的概念:
假设数据集中类别Xi的概率是pi:即P(X=xi)=pi,其中i=1,2,3,…,n.
①数据的熵可由以下公式计算:
在这里插入图片描述
当所有数据都属于同一个类别的时候H(p)=0,当所有的数据均分为n类时H(p)=log n。
②条件熵H(Y|X)表示在已知随机变量X的条件下随机变量Y的不确定性。条件熵的推导如下:
在这里插入图片描述
③信息增益表示某特征对最终分类的影响,信息增益越大,表明该特征对分类的影响越大,但是信息增益准则对可取值数目较多的属性有所偏好。(ID3使用的选择特征方法即为信息增益,而C4.5算法对此进行了改进,用增益率代替信息增益来选择特征,从而避免了数目较多的属性的影响)
信息增益数学上等于经验熵与条件熵的差值:
g(D,A)=H(D)−H(D∣A)
其实这点也比较好理解:经验熵是指所有数据的分类情况,经验熵越大表示越分散;条件熵表示用某个特征划分后数据的分类情况,如果该特征分类对分类的影响越大,则条件熵H(D∣A)应越小,即信息增益g(D,A)越大。(还是不太理解的话可以查阅一下《统计学习方法》,上面讲的听清楚)

针对具体的数据集,可使用以下公式计算:

在这里插入图片描述
以上公式中的符号含义与《统计学习方法》中内容一致。

2.在懂了决策树的ID3算法是如何选择特征后,我们开始基于一个小的关于判断是否是鱼类的例子来逐步实现该算法:
该例子如下表所示:
在这里插入图片描述
首先计算数据的经验熵:(注释均已添加在代码中)

def calcShannonEnt(dataSet):#用来计算熵
    numEntries = len(dataSet)#所有实例的个数
    labelCounts = {}#创建字典,用来存储各类别实例的个数
    for featVec in dataSet: #the the number of unique elements and their occurance
        currentLabel = featVec[-1]#currentLabel表示该数据的类别标签
        if currentLabel not in labelCounts.keys():#如果该类别是第一次出现,则在字典中添加该关键字,出现次数为0
            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    #返回经验熵

①.我们可以创建一个小的数据集验证一下经验熵代码实现的效果:

def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no']]
    labels = ['no surfacing','flippers']
    #change to discrete values
    return dataSet, labels

运行结果如下:

E:\anaconda\envs\tf1\python.exe C:/Users/范淑卷/PycharmProjects/untitled/venv/practise.py
0.9709505944546686

Process finished with exit code 0

②.在数据集中增加数据[‘1’,‘1’,‘maybe’],即

def createDataSet():
    dataSet = [[1, 1, 'yes'],
               [1, 1, 'yes'],
               [1, 0, 'no'],
               [0, 1, 'no'],
               [0, 1, 'no'],
               [1,1,'maybe']]
    labels = ['no surfacing','flippers']
    #change to discrete values
    return dataSet, labels

运行结果为

E:\anaconda\envs\tf1\python.exe C:/Users/范淑卷/PycharmProjects/untitled/venv/practise.py
1.4591479170272448

Process finished with exit code 0

以上表明经验熵越大,信息混乱程度越大。

3.计算出经验熵之后,我们赢开始实现条件熵的计算,然而计算条件熵需要按照给定特征对样本数据进行划分,所以接下来我们实现通过特征对数据集的划分:

def splitDataSet(dataSet, axis, value):
    #dataSet:待划分的数据集;axis:划分数据集的特征是第axis个特征;value:该特征的值是否等于value
    retDataSet = []
    for featVec in dataSet:#featVec为一个列表
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])#该步与上一步实现的结果为去掉第axis个特征
            retDataSet.append(reducedFeatVec)#将特征值与value值相等的数据(已去掉第axis个特征)存放到列表retDataSet中
    return retDataSet

返回的列表retDataSet中存储的即为符合第axis个特征值等于value的所有数据。

例如输入

result = splitDataSet(myDat,2,'no')
print(result)

就可以得到所有的第2个特征值等于‘no’的数据,结果如下:

E:\anaconda\envs\tf1\python.exe C:/Users/范淑卷/PycharmProjects/untitled/venv/practise.py
[[1, 0], [0, 1], [0, 1]]

Process finished with exit code 0

*小Tips:
append函数与extend函数的区别:
①.append函数

a = [1,2,3]
b = [4,5,6]
a.append(b)
print(a)

得到的结果是:[1, 2, 3, [4, 5, 6]]
②.extend函数

a = [1,2,3]
b = [4,5,6]
a.extend(b)
print(a)

得到的结果是:[1, 2, 3, 4, 5, 6]
③.结论:append是将列表b作为一个整体添加到列表a中,整个列表b只占用a中的一个元素位置;而extend是将列表b与列表a拼接在一起,占用a中的位置个数等于b中元素的个数。

4.通过以上操作我们便得到了根据某个指定特征划分好的数据集,接下来我们应该遍历数据集,通过计算条件熵与信息增益挑选出最好的特征划分方式:

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1      #the last column is used for the labels
    baseEntropy = calcShannonEnt(dataSet)  #计算经验熵
    bestInfoGain = 0.0#存储最佳信息增益
    bestFeature = -1#存储最佳特征的位置
    for i in range(numFeatures):        #iterate over all the features
        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                      #返回最佳特征在存储特征的列表中的下标

该段程序中需要注意的是:
①.

featList = [example[i] for example in dataSet]

实际上是以下代码的简写:

featlist = []
for example in dataSet:
    featList.append(example[i])

②.set是在列表中创建集合,因为集合中元素都是唯一的,所以set可以实现去重操作。该方法是python语言中得到列表唯一元素值最快的方法,同时注意输出集合中的元素变为有序(可能与原顺序不同),例如以下例子:

list = [6,6,5,4,4,4,4,6,1,2,3]
list = set(list)
print(list)

输出为:{1, 2, 3, 4, 5, 6}。结果是有序的,与原来的list中元素的顺序无关。

5.运行程序后,输出结果为:0,表示下标为0的特征对该数据集分类的信息增益最大,应优先选择该特征对数据集进行初步划分。

6.通过以上过程我们已经学会了如何选择对数据划分增益最大的特征,每选择一个特征,都会将该部分的数据集分为两部分,但是每一部分的数据可能仍然属于不同的类别,这就要求我们需要重新选择其他的特征对该部分的数据继续划分。
我们将使用递归的原则处理数据集,使得程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。如下图所示:
在这里插入图片描述
接着我们便来看看如何用程序创建类似上图的树:

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet] #遍历所有实例的类别,并存储到列表classList中
    if classList.count(classList[0]) == len(classList):
        return classList[0]#如果该分支均属于同一类别,则结束
    if len(dataSet[0]) == 1: #如果所有特征都遍历完,则结束(dataSet存放的是特征值+类别)
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]#最佳特征值对应的特征名字
    myTree = {bestFeatLabel:{}}#创建树
    del(labels[bestFeat])#特征名字列表中删除最佳特征(只是删除了其与列表的联系,并没有删除该数据,即myTree中的bestFeatLabel还是存在的)
    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 

其中majorityCnt()函数的实现功能如下:

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]#返回出现次数最多的类别

运行结果为:{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

为了方便观察通过递归形成树的过程,将上面代码稍作修改,

def createTree(dataSet,labels):
    classList = [example[-1] for example in dataSet] #遍历所有实例的类别,并存储到列表classList中
    if classList.count(classList[0]) == len(classList):
        return classList[0]#如果该分支均属于同一类别,则结束
    if len(dataSet[0]) == 1: #如果所有特征都遍历完,则结束(dataSet存放的是特征值+类别)
        return majorityCnt(classList)
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]#最佳特征值对应的特征名字
    myTree = {bestFeatLabel:{}}#创建树
    del(labels[bestFeat])#特征名字列表中删除最佳特征(只是删除了其与列表的联系,并没有删除该数据,即myTree中的bestFeatLabel还是存在的)
    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)
        #splitDataSet函数:将特征值与value值相等的数据存放到列表中,然后返回该列表
    return myTree

得到以下结果:

{'no surfacing': {0: 'no'}}
{'flippers': {0: 'no'}}
{'flippers': {0: 'no', 1: 'yes'}}
{'no surfacing': {0: 'no', 1: None}}

通过比较以上两个结果易得以下结论:首先‘no surfacing’作为最主要的特征用来划分,其特征值等于0时自成一类,其结果为‘no’,其特质正取1时,分支类别还不唯一,所以需要用其他特征继续划分;第二,对于第二层的右侧,用特征‘flippers’对右侧数据划分,得其特征值为0时结果为‘no’,特征值为1时结果为‘yes’,最终形成树:{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
为了使结果更明显,在最初的DataSet中添加数据 [0,0,‘yes’]。
得到的树为:{'no surfacing': {0: {'flippers': {0: 'yes', 1: 'no'}}, 1: {'flippers': {0: 'no', 1: 'yes'}}}}

7.通过以上步骤,我们已经得到了一个完整的决策树ID3算法,接下来给出测试决策树的代码:

def classify(inputTree, featLabels, testVec):
    # 获取tree的根节点对于的key值
    firstStr = list(inputTree.keys())[0]
    # 通过key得到根节点对应的value
    secondDict = inputTree[firstStr]
    # 判断根节点名称获取根节点在label中的先后顺序,这样就知道输入的testVec怎么开始对照树来做分类
    featIndex = featLabels.index(firstStr)
    # 测试数据,找到根节点对应的label位置,也就知道从输入的数据的第几位来开始分类
    key = testVec[featIndex]
    valueOfFeat = secondDict[key]
    print('+++', firstStr, 'xxx', secondDict, '---', key, '>>>', valueOfFeat)
    # 判断分枝是否结束: 判断valueOfFeat是否是dict类型
    if isinstance(valueOfFeat, dict):
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else:
        classLabel = valueOfFeat
    return classLabel

其中main函数如下所示:

def main():
    dataSet, labels = createDataSet()
    inputTree = createTree(dataSet, labels)
    result = classify(inputTree,['no surfacing','flippers'], [1, 1])
    print(result)

运行结果如下所示:

+++ no surfacing xxx {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}} --- 1 >>> {'flippers': {0: 'no', 1: 'yes'}}
+++ flippers xxx {0: 'no', 1: 'yes'} --- 1 >>> yes
yes

小结:用字典构建树的递归算法看起来很费力,课后查找相关资料和视频进一步学习。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值