分类问题:决策树

这是我们为拥有 Python 基础的同学推出的精进技能的“机器学习实战” 刻意练习活动,这也是我们本学期推出的第三次活动了。

我们准备利用8周时间,夯实机器学习常用算法,完成以下任务:

分类问题:K邻近算法
分类问题:决策树
分类问题:朴素贝叶斯
分类问题:逻辑回归
分类问题:支持向量机
分类问题:AdaBoost
回归问题:线性回归、岭回归、套索方法、逐步回归等
回归问题:树回归
聚类问题:K均值聚类
相关问题:Apriori
相关问题:FP-Growth
简化数据:PCA主成分分析
简化数据:SVD奇异值分解

本次任务的核心是熟悉K邻近算法的原理,并实现《机器学习实战》这本书给出的两个案例。


分类问题:决策树

算法原理

决策树模型是一种描述对实例进行分类的树形结构。其由结点(node)和有向边(directed edge)组成。结点有两种类型:内部结点(internal node)和叶结点(leaf node)。内部结点表示一个特征或属性(features),叶结点表示一个类(labels)。

用决策树对需要测试的实例进行分类:从根节点开始,对实例的某一特征进行测试,根据测试结果,将实例分配到其子结点;这时,每一个子结点对应着该特征的一个取值。如此递归地对实例进行测试并分配,直至达到叶结点。最后将实例分配到叶结点的类中。

决策树模型可以认为是 if-then 规则的集合,也可以认为是定义在特征空间与类空间上的条件概率分布。

决策树学习通常包括 3 个步骤:特征选择、决策树的生成和决策树的修剪。

信息熵

  • 熵(entropy):熵指的是体系的混乱的程度,在不同的学科中也有引申出的更为具体的定义,是各领域十分重要的参量。
  • 信息论(information theory)中的熵(香农熵):是一种信息的度量方式,表示信息的混乱程度,也就是说:信息越有序,信息熵越低。
  • 信息增益(information gain):在划分数据集前后信息发生的变化称为信息增益。
    划分数据集的原则是:将无序的数据变得更加有序。选择信息增益最大的特征来划分数据集。所用到的公式如下:
    在这里插入图片描述
    如何构造一个决策树?
    我们使用 createBranch() 方法,如下所示:
def createBranch():
'''
此处运用了迭代的思想。
'''
    检测数据集中的所有数据的分类标签是否相同:
        If so return 类标签
        Else:
            寻找划分数据集的最好特征(划分之后信息熵最小,也就是信息增益最大的特征)
            划分数据集
            创建分支节点
                for 每个划分的子集
                    调用函数 createBranch (创建分支的函数)并增加返回结果到分支节点中
            return 分支节点

项目案例1:判定鱼类和非鱼类

项目概述
根据以下 2 个特征,将动物分成两类:鱼类和非鱼类。

  • 不浮出水面是否可以生存
  • 是否有脚蹼
    开发流程
    Step1:收集数据
    在这里插入图片描述
    Step2:准备数据
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

Step3:分析数据
可以使用任何方法,构造树完成之后,我们可以将树画出来。

计算给定数据集的香农熵的函数如下:

import math

def calcShannonEnt(dataSet):
    # 求list的长度,表示计算参与训练的数据量
    numEntries = len(dataSet)
    # 计算分类标签label出现的次数
    labelCounts = {}
    # the the number of unique elements and their occurrence
    for featVec in dataSet:
        # 将当前实例的标签存储,即每一行数据的最后一个数据代表的是标签
        currentLabel = featVec[-1]
        # 为所有可能的分类创建字典,如果当前的键值不存在,则扩展字典并将当前键值加入字典。每个键值都记录了当前类别出现的次数。
        if currentLabel not in labelCounts.keys():
            labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1

    # 对于 label 标签的占比,求出 label 标签的香农熵
    shannonEnt = 0.0
    for key in labelCounts:
        # 使用所有类标签的发生频率计算类别出现的概率。
        prob = float(labelCounts[key]) / numEntries
        # 计算香农熵,以 2 为底求对数
        shannonEnt -= prob * math.log(prob, 2)
    return shannonEnt

按照给定特征划分数据集

将指定特征的特征值等于 value 的行剩下列作为子数据集。

def splitDataSet(dataSet, index, value):
    """
    splitDataSet(通过遍历dataSet数据集,求出index对应的colnum列的值为value的行)
        就是依据index列进行分类,如果index列的数据等于 value的时候,就要将 index 划分到我们创建的新的数据集中
    Args:
        dataSet 数据集                 待划分的数据集
        index 表示每一行的index列        划分数据集的特征
        value 表示index列对应的value值   需要返回的特征的值。
    Returns:
        index列为value的数据集【该数据集需要排除index列】
    """
    retDataSet = []
    for featVec in dataSet:
        # index列为value的数据集【该数据集需要排除index列】
        # 判断index列的值是否为value
        if featVec[index] == value:
            # chop out index used for splitting
            # [:index]表示前index行,即若 index 为2,就是取 featVec 的前 index 行
            reducedFeatVec = featVec[:index]
            reducedFeatVec.extend(featVec[index + 1:])
            # [index+1:]表示从跳过 index 的 index+1行,取接下来的数据
            # 收集结果值 index列为value的行【该行需要排除index列】
            retDataSet.append(reducedFeatVec)
    return retDataSet

选择最好的数据集划分方式:

def chooseBestFeatureToSplit(dataSet):
    """
    chooseBestFeatureToSplit(选择最好的特征)
    Args:
        dataSet 数据集
    Returns:
        bestFeature 最优的特征列
    """
    # 求第一行有多少列的 Feature, 最后一列是label列
    numFeatures = len(dataSet[0]) - 1
    # 数据集的原始信息熵
    baseEntropy = calcShannonEnt(dataSet)
    # 最优的信息增益值, 和最优的Featurn编号
    bestInfoGain, bestFeature = 0.0, -1
    # iterate over all the features
    for i in range(numFeatures):
        # 获取对应的feature下的所有数据
        featList = [example[i] for example in dataSet]
        # 获取剔重后的集合,使用set对list数据进行去重
        uniqueVals = set(featList)
        # 创建一个临时的信息熵
        newEntropy = 0.0
        # 遍历某一列的value集合,计算该列的信息熵
        # 遍历当前特征中的所有唯一属性值,对每个唯一属性值划分一次数据集,计算数据集的新熵值,并对所有唯一特征值得到的熵求和。
        for value in uniqueVals:
            subDataSet = splitDataSet(dataSet, i, value)
            # 计算概率
            prob = len(subDataSet) / float(len(dataSet))
            # 计算条件熵
            newEntropy += prob * calcShannonEnt(subDataSet)
        # gain[信息增益]: 划分数据集前后的信息变化, 获取信息熵最大的值
        # 信息增益是熵的减少或者是数据无序度的减少。最后,比较所有特征中的信息增益,返回最好特征划分的索引值。
        infoGain = baseEntropy - newEntropy
        print('infoGain=', infoGain, 'bestFeature=', i, baseEntropy, newEntropy)
        if infoGain > bestInfoGain:
            bestInfoGain = infoGain
            bestFeature = i
    return bestFeature

Step4:训练算法
造树的数据结构,创建树的函数代码如下:

import operator

def majorityCnt(classList):
    """
    majorityCnt(选择出现次数最多的一个结果)
    Args:
        classList label列的集合
    Returns:
        bestFeature 最优的特征列
    """
    classCount = {}
    for vote in classList:
        if vote not in classCount.keys():
            classCount[vote] = 0
        classCount[vote] += 1
    # 倒叙排列classCount得到一个字典集合,然后取出第一个就是结果(yes/no),即出现次数最多的结果
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet]
    # 如果数据集的最后一列的第一个值出现的次数=整个集合的数量,也就说只有一个类别,就只直接返回结果就行
    # 第一个停止条件:所有的类标签完全相同,则直接返回该类标签。
    # count() 函数是统计括号中的值在list中出现的次数
    if classList.count(classList[0]) == len(classList):
        return classList[0]
    # 如果数据集只有1列,那么最初出现label次数最多的一类,作为结果
    # 第二个停止条件:使用完了所有特征,仍然不能将数据集划分成仅包含唯一类别的分组。
    if len(dataSet[0]) == 1:
        return majorityCnt(classList)

    # 选择最优的列,得到最优列对应的label含义
    bestFeat = chooseBestFeatureToSplit(dataSet)
    # 获取label的名称
    bestFeatLabel = labels[bestFeat]
    # 初始化myTree
    myTree = {bestFeatLabel: {}}
    # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改
    # 所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    del (labels[bestFeat])
    # 取出最优列,然后它的branch做分类
    featValues = [example[bestFeat] for example in dataSet]
    uniqueVals = set(featValues)
    for value in uniqueVals:
        # 求出剩余的标签label
        subLabels = labels[:]
        # 遍历当前选择特征包含的所有属性值,在每个数据集划分上递归调用函数createTree()
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

存储决策树

def storeTree(inputTree, filename):
    import pickle
    fw = open(filename, 'wb')
    pickle.dump(inputTree, fw)
    fw.close()


def loadTree(filename):
    import pickle
    fr = open(filename, 'rb')
    return pickle.load(fr)

代码测试

if __name__ == '__main__':
    dataSet, labels = createDataSet()
    tree = createTree(dataSet, labels)
    print(tree)

# infoGain= 0.4199730940219749 bestFeature= 0 0.9709505944546686 0.5509775004326937
# infoGain= 0.17095059445466854 bestFeature= 1 0.9709505944546686 0.8
# infoGain= 0.9182958340544896 bestFeature= 0 0.9182958340544896 0.0
# {'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

    storeTree(tree, 'classifierStorage.pkl')
    tree = loadTree('classifierStorage.pkl')
    print(tree)

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

Step5:测试算法
使用决策树执行分类

def classify(inputTree, featLabels, testVec):
    """
    classify(给输入的节点,进行分类)
    Args:
        inputTree  决策树模型
        featLabels Feature标签对应的名称
        testVec    测试输入的数据
    Returns:
        classLabel 分类的结果值,需要映射label才能知道名称
    """
    # 获取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

Step6:使用算法
此步骤可以适用于任何监督学习任务,而使用决策树可以更好地理解数据的内在含义。

if __name__ == '__main__':
    dataSet, labels = createDataSet()
    labelsCopy = labels[:]
    inputTree = createTree(dataSet, labels)
    result = classify(inputTree, labelsCopy, [1, 0])
    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'} --- 0 >>> no
# no

项目案例2:使用决策树预测隐形眼镜类型

项目概述

隐形眼镜类型包括硬材质、软材质以及不适合佩戴隐形眼镜。我们需要使用决策树预测患者需要佩戴的隐形眼镜类型。

开发流程

Step1:收集数据

文本文件数据格式如下:

young    myope   no    reduced    no lenses
young    myope   no    normal    soft
pre    myope    yes    reduced    no lenses
pre    myope    yes    normal    hard
presbyopic    myope    yes    normal    hard
presbyopic    hyper    no    reduced    no lenses

Step2:准备数据
解析 tab 键分隔的数据行

# 加载隐形眼镜相关的 文本文件 数据
fr = open('lenses.txt')
# 解析数据,获得 features 数据
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
# 得到数据的对应的 Labels
lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']

Step3:分析数据
同案例1,计算香农熵、按照给定的特征划分数据集、选择最好的数据集划分方式。
Step4:训练算法
使用 createTree() 函数

# 使用上面的创建决策树的代码,构造预测隐形眼镜的决策树
lensesTree = createTree(lenses, lensesLabels)
storeTree(lensesTree, 'classifierLenes.pkl')
print(lensesTree)

# infoGain= 0.03939650364612124 bestFeature= 0 1.3260875253642983 1.286691021718177
# infoGain= 0.039510835423565815 bestFeature= 1 1.3260875253642983 1.2865766899407325
# infoGain= 0.37700523001147723 bestFeature= 2 1.3260875253642983 0.9490822953528211
# infoGain= 0.5487949406953986 bestFeature= 3 1.3260875253642983 0.7772925846688997
# infoGain= 0.22125183600446618 bestFeature= 0 1.5545851693377994 1.3333333333333333
# infoGain= 0.09543725231055489 bestFeature= 1 1.5545851693377994 1.4591479170272446
# infoGain= 0.7704260414863776 bestFeature= 2 1.5545851693377994 0.7841591278514218
# infoGain= 0.2516291673878229 bestFeature= 0 0.9182958340544896 0.6666666666666666
# infoGain= 0.4591479170272448 bestFeature= 1 0.9182958340544896 0.4591479170272448
# infoGain= 0.9182958340544896 bestFeature= 0 0.9182958340544896 0.0
# infoGain= 0.3166890883150208 bestFeature= 0 0.6500224216483541 0.3333333333333333
# infoGain= 0.19087450462110933 bestFeature= 1 0.6500224216483541 0.4591479170272448
# infoGain= 1.0 bestFeature= 0 1.0 0.0
# {'tearRate': {'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'presbyopic': 'no lenses', 'young': 'hard', 'pre': 'no lenses'}}, 'myope': 'hard'}},
# 'no': {'age': {'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft', 'pre': 'soft'}}}},
# 'reduced': 'no lenses'}}

Step5:测试算法
同案例1,编写测试函数以便验证决策树可以正确分类给定的数据实例。
Step6:使用算法

if __name__ == '__main__':
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    lensesTree = loadTree('classifierLenes.pkl')
    result = classify(lensesTree, lensesLabels, ['young', 'myope', 'no', 'reduced'])
    print(result)

# +++ tearRate xxx {'reduced': 'no lenses', 'normal': {'astigmatic': {'yes': {'prescript': {'hyper': {'age': {'presbyopic': 'no lenses', 'young': 'hard', 'pre': 'no lenses'}}, 'myope': 'hard'}}, 'no': {'age': {'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}}, 'young': 'soft', 'pre': 'soft'}}}}} --- reduced >>> no lenses
# no lenses
发布了144 篇原创文章 · 获赞 3 · 访问量 9546

猜你喜欢

转载自blog.csdn.net/NumberOneStudent/article/details/103104450