决策树代码详解——真正地一行一行来解读

假如想象 提交于 2020-03-11 03:56:18

因为决策树代码构建有点复杂,整体贴上来会显得有点难懂,所以我们在这里将从每个函数开始一点一点进行讲解,最后把这些函数按顺序复制到程序里就能运行:

首先是第一个函数:生成数据集。

def createDataSet():
    dataSet = [[1, 1, 1, 'yes'],
               [1, 0, 1, 'yes'],
               [1, 0, 2, 'no'],
               [0, 1, 2, 'no'],
               [0, 1, 2, 'no'],
               [1, 1, 2, 'yes'],
               [0, 0, 2, 'yes'],
               [0, 1, 0, 'no'],
               ]
    labels = ['ddddd','fffff','sssss']
    #change to discrete values
    return dataSet, labels

这个函数没啥可说的了,就是先建立一个数据集,里面每行数据代表一个样本,每个样本的前三个数据代表三个特征,第四个数据代表样本的分类。下面的标签表示每个样本的这三个特征分别是啥。这里为了省事直接用几个字母代替了。

然后是第二个函数:计算信息熵。

# 计算信息熵
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

我们来分析一下这个函数。首先它输入一个数据集,就用上面的函数返回的dataSet就好了。

然后我们先计算出样本数,并采用中括号构建一个字典。

然后遍历每个样本,featVec[-1]表示每个样本中的数据的最后一个数据的值,也就是分类用的类别,在我们的数据集里分别是yes和no。如果该类别在字典里没有,则说明是一个新类别,然后我们就让字典包含该类别。然后数值+1。

在我们的例子中,我们先读到第一个数据 [1, 1, 1, 'yes'],此时字典还是空的,所以我们把yes放入字典里,然后yes的值+1。

紧接着我们读到第二个数据:[1, 0, 1, 'yes'],然后发现字典里已经有该类别的值了,所以字典里的数值+1.

然后读到第三个数据:[1, 0, 2, 'no'],发现字典里没有'no',所以字典里生成新的类别,并+1。

等都读取完以后,建立一个变量shannonEnt,表示香农能量熵。

在该for循环中,遍历字典里所有的类别(该例子只有yes和no两个类别),然后计算香农能量熵。

之后我们可以打印测试一下:

myDat,labels=createDataSet()
shannonEnt=calcShannonEnt(myDat)
print(shannonEnt)

输出1.0

然后是第三个函数:划分子数据集。

# 划分数据集,axis:按第几个属性划分,value:要返回的子集对应的属性值
def splitDataSet(dataSet, axis, value):
    retDataSet = []
    featVec = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis + 1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

在我们构建决策树需要进行进一步分类时,我们需要用到该函数。

该函数的意义是根据用于分类的标签生成子数据集。

举个例子:假如我们输入的是之前建立的dataSet:splitDataSet(DataSet,0,1)

    dataSet = [[1, 1, 1, 'yes'],
               [1, 0, 1, 'yes'],
               [1, 0, 2, 'no'],
               [0, 1, 2, 'no'],
               [0, 1, 2, 'no'],
               [1, 1, 2, 'yes'],
               [0, 0, 2, 'yes'],
               [0, 1, 0, 'no'],
               ]

之后在for中,我们判段dataSet里面的每个样本数据的第0个特征的值,如果是1,就保留,保留的方式是把该特征去掉,用剩下的特征加分类值构成一个子样本数据,如果是0,就不用管。

在dataSet中,第一个样本数据的第0个特征是1,符合,所以去掉该特征然后生成子样本[ 1, 1, 'yes']。

第二个样本数据的第0个特征是1,符合,所以去掉该特征然后生成子样本[ 0, 1, 'yes']

遍历完整个dataSet以后,就得到了根据特征值划分的子空间,打印一下:

asa = splitDataSet(myDat, 0, 1)
print(asa)

打印结果:

[[1, 1, 'yes'], [1, 1, 'yes'], [0, 2, 'no'], [1, 2, 'yes']]

然后是第四个函数:选择最好的数据集划分方式

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1  # 属性的个数。 -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为上一个函数生成的子数据集,即 [[1, 1, 'yes'], [1, 1, 'yes'], [0, 2, 'no'], [1, 2, 'yes']] 。

现在详细解释一下这个函数:

dataSet[0]代表第一个样本,即[1, 1, 'yes'],长度为3, 3-1=2,表示该样本的特征数。

然后计算出香农熵。

定义增益初始为0.0,然后初始的特征坐标为-1。

之后用i来把样本的特征遍历一遍。这里只有两个特征。

在每个特征里,首先用 [example[i] for example in dataSet] 提取所有样本第 i 个特征值的列表,此处我们假设正在第0个特征中,把每个样本的第0个特征分别提取出来,就生成了如下值:[1,1,0,1]。之后,通过uniqueVals = set(featList)可以把列表中不同的值都取出来。然后生成新的列表。这里一共就只有两个不同的特征值,一个是1,一个是0。

然后我们进行划分:遍历该特征所有不同的值,分别用该特征不同的值进行划分:subDataSet = splitDataSet(dataSet, i, value),在该例子中我们划分出来的结果是:第0个特征为1时:[[1, 'yes'], [1, 'yes'], [2, 'yes']] ; 第0个特征为0时:[[2, 'no']]。在for循环里计算累加这种靠这种划分方式的熵。

之后再计算增益。找到按哪个特征进行划分增益最大。然后返回该划分方式的索引。

我们把上一个函数中输出的asa输入进去:

bestFeteTest = chooseBestFeatureToSplit(asa)
print(bestFeteTest)

打印结果为 0

我们研究一下这个结果:

如果根据第0个特征划分asa,则生成结果为:

第0个特征为1时:[[1, 'yes'], [1, 'yes'], [2, 'yes']] ; 第0个特征为0时:[[2, 'no']]

如果根据第1个特征划分asa,则生成结果为:

第1个特征为1时:[[1, 'yes'], [1, 'yes']] ; 第1个特征为2时:[[0, 'no'],[1, 'yes']]

显然根据第一个特征进行划分,结果是对半分的(每边都是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(), #注意python3.x中要改成.items()函数
                              key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

这个函数有什么用呢?我们想象这样一种情况:因为每次我们划分数据的时候,都会用掉一个特征来进行划分。用掉的特征以后不会再使用了(因为用一个特征划分好以后,其每个子分支各自的该特征的值都是一样的)。假设在我们不断细分的过程中,发现所有的特征值都用完了,这时候该怎么办呢?很显然我们没法再继续下分了,所以需要返回当前类别,这个类别就用分到这个树枝的所有类别中比例最大的来代替,即假如分到该树枝的类别中有5个yes,2个no,最后我们就把它作为yes返回。

这种统计计数的方式还是靠创建字典来完成实现的。假设在for循环里统计的时候我们生成了这样的数据格式:{'no':2, 'yes':5}。然后用sorted函数对字典记录的每个类别的键值进行排序,排序完之后得到如下结果:[('yes', 5), ('no', 2)]。sortedClassCount[0][0]代表数量最大的那个类别值。

我们做个测试:

aks = ['yes','yes','no','yes','no','yes','yes']
majorityCnt(aks)

输出 

[('yes', 5), ('no', 2)]

至于为什么我们的输入是 ['yes','yes','no','yes','no','yes','yes']而不是 [['yes'],['yes'],['no'],['yes'],['no'],['yes'],['yes']],下一个构建树的程序会首先提取最后一列:[example[-1] for example in dataSet],提取出来的值是一个一维列表,而不再是二维列表了。后面还会再说。

最后一个函数:构建决策树。

终于到最后一个函数了。

# 递归构建决策树
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]
    uniqueValue = set(featValues)  # 该属性所有可能取值,也就是节点的分支
    for value in uniqueValue:  # 对每个分支,递归构建树
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(
            splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

构建过程涉及递归操作。递归一般很难描述,这里想办法把递归的结构描述一下。

首先要注意我们在函数开头把最后面的类别读出开。然后用两个if判断,如果都是属于同一类别的,则直接将该类作为该树枝里的样本所代表的类。如果用于划分的特征都用完了,就直接把该类中数量最多的类别返回。

如果没有用完特征并且树枝里有许多别的类别,就继续处理:

构建一个树,采用最好的划分特征进行划分。删除该特征的列,然后对每个分支进行递归构建决策树。

至此,整个决策树的程序就构建好了。

我们运行验证一下:

print(createTree(myDat, labels))

打印结果为:

{'sssss': {0: 'no', 1: 'yes', 2: {'ddddd': {0: {'fffff': {0: 'yes', 1: 'no'}}, 1: {'fffff': {0: 'no', 1: 'yes'}}}}}}

勉强能看出来我们的分类方式。

下面将介绍如何把生成的决策树图表示出来。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!