ML in Action Note - Day 2 - Decision Tree

第3章:Decision Tree

决策树:根据不同特征建立分支

优点:计算复杂度不高,输出结果容易理解,对中间值的缺失不敏感,可以处理不相关特征数据。

缺点:可能会产生overfitting。

P1:划分数据集,创建决策树

信息增益:划分数据集前后信息发生的变化称为信息增益。

熵entropy:集合信息的度量方式就称为熵。熵定义为信息的期望值。

如果袋分类的事务可能划分在多个分类中,则x_i的信息定义为:l(x_i) = -log_{_2}p(x_i),其中p(x_i)是选择该分类的概率。为了计算熵,需要计算所有类别所有可能值包含的信息期望值:H = -\sum_{i=1}^{n}p(x_i)log_{2}p(x_i)

熵越高,则混合的数据也越多。得到熵以后,可以按照获取最大信息增益的方法划分数据集。

另一个度量集合无序程度的方法是基尼不纯度(Gini impurity):从一个数据集中随机选子项,度量其被错误分类到其他分组的概率。

计算熵:

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for featVec in dataSet: # 每一行
        currentLabel = featVec[-1] # 本行最后一列为特征
        labelCounts[currentLabel] = labelCounts.get(currentLabel, 0) + 1 # 特征放入词典计算出现次数
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries # label出现总次数/数据集长度 得到出现概率
        shannonEnt -= prob * log(prob, 2)
    return shannonEnt

# sample
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

 执行trees.calcShannonEnt(dataSet)得到

0.9709505944546686

划分数据集:

def splitDataSet(dataSet, axis, value): # 3个参数:待划分数据集,划分数据集特征,特征返回值
    retDataset = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis] # featVec是个list
            reducedFeatVec.extend(featVec[axis+1:])
            retDataset.append(reducedFeatVec)
    return retDataset

补充知识:extend和append的区别

a = [1, 2, 3]
b = [4, 5, 6]

a.append(b) # [1, 2, 3, [4, 5, 6]]
a.extend(b) # [1, 2, 3, 4, 5, 6]

求出信息增益最大的列:

数据需要满足:1. 所有列表元素具有同样的长度(每一行都一样多) 2. 每一行最后一个元素是当前实例的类别标签

def chooseBestFeatureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 # 每行list减去1个特征
    totalEntropy = calcShannonEnt(dataSet) # 整个数据集的熵
    bestInfoGain = 0.0
    bestFeature = -1
    for i in range(numFeatures):
        featList = [example[i] for example in dataSet] # example[i] 提取第i列的特征list 看作横向遍历时候纵向取值
        uniqueVals = set(featList) # 第i列的特征set
        colEntropy = 0.0
        for value in uniqueVals: # 当前列特征的熵总和
            subDataSet = splitDataSet(dataSet, i, value)
            prob = len(subDataSet)/float(len(dataSet))
            colEntropy += prob * calcShannonEnt(subDataSet)
        colInfoGain = totalEntropy - colEntropy  # 当前列信息增益 = 数据集熵 - 当前列熵
        if (colInfoGain > bestInfoGain): # 找到最大的信息增益,得出该列
            bestInfoGain = colInfoGain
            bestFeature = i
    return bestFeature

得到最好属性值划分数据集,划分之后数据到下一个节点,还可以再次划分,所以可以采用递归的方式继续处理。

递归结束的条件:程序遍历完所有划分数据集的属性,或者每个分支下的所有实例都具有相同的分类。

如下图,圈起来的就是已经完成相同分类的了。

决策树算法也有很多种,比如C4.5和CART(TODO:我自己都还没有研究过T T),这些算法在运行时并不总是在每次划分分组时都会消耗特征。特征数目并不是每次划分数组都减少,所以算法在实际使用时候可能引起一定问题。

如果数据集已经处理了所有属性,但是类标签依然不是唯一的,此时我们需要决定如何定义该叶子节点。通常采用多数表决的方法决定该叶子节点的分类。

求出次数最多的分类名称:

def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    sortedClassCount = sorted(classCount.items(), key=operator.itemgetter(1), reverse=True)
    return sortedClassCount[0][0]

这里递归求出树……(花了我半天时间才大致看懂这一节递归传递T T我需要try harder!)

def createTree(dataSet, labels):
    classList = [example[-1] for example in dataSet] # 每一行最后一列的分类,组成list
    if classList.count(classList[0]) == len(classList): # 如果第一个元素的个数等于list长度,则无需继续划分
        return classList[0] # 返回第一个分类标签
    if len(dataSet[0]) == 1: # 只剩下最后一列分类时,就无需继续划分
        return majorityCnt(classList) # 返回该列出现最多的分类
    bestFeat = chooseBestFeatureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat]) # 从特征list删除当前特征,往后传递特征列表筛选
    featValues = [example[bestFeat] for example in dataSet] # 当前特征列变成list
    uniqueVals = set(featValues) # 该列特征去重组成set
    for value in uniqueVals:
        subLabels = labels[:] # 剩下的特征
        subDataSet = splitDataSet(dataSet, bestFeat, value)
        myTree[bestFeatLabel][value] = createTree(subDataSet, subLabels)
    return myTree

执行treePlotter.createPlot()得到{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}

新建一个python文件来画图(plot)

import matplotlib.pyplot as plt
from pylab import *
mpl.rcParams['font.sans-serif'] = ['SimHei']

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('node name', arrow head, arrow tail, nodeType)
    plotNode('AAA', (0.2, 0.1), (0.1, 0.5), decisionNode)
    plotNode('BBB', (0.4, 0.4), (0.8, 0.6), leafNode)
    plt.show()

执行treePlotter.createPlot()得到下面图片~

计算叶子节点个数和树的最大深度

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

下面画图这里有点难。。不过能循着规矩去发现也是通过递归。

def plotMidText(cntrPt, parentPt, txtString):
    xMid = (parentPt[0] - cntrPt[0])/2.0 + cntrPt[0]
    yMid = (parentPt[1] - cntrPt[1])/2.0 + cntrPt[1]
    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
    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 = 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()

可以通过特征值list得到数据集中第一个吻合此类特征的分类:

def classify(inputTree, featLabels, testVec): # testVec是list like
    firstStr = list(inputTree.keys())[0]
    secondDict = inputTree[firstStr]
    featIndex = featLabels.index(firstStr)
    for key in secondDict.keys():
        if testVec[featIndex] == key:
            if type(secondDict[key]).__name__=='dict':
                classLabel = classify(secondDict[key], featLabels, testVec)
            else:
                classLabel = secondDict[key]
    return classLabel

比如trees.classify(myTree, labels, [1,1])的话,会得到分类结果yes。

构造决策树很耗时,即使处理很小的数据集。用pickle序列化,在每次执行分类时调用已经构造好的决策树。

def storeTree(inputTree, filename):
    import pickle
    fw = open(filename, 'wb+') # 在py2里面是'w',py3里面需要改成'wb+'
    pickle.dump(inputTree, fw)
    fw.close()

def grabTree(filename):
    import pickle
    fr = open(filename, 'rb') # 在py3中需要加上'rb',否则会报错读取了str而不是byte
    return pickle.load(fr)

P2:通过决策树预测con类型

原数据集txt文件格式:

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

读取文件,按空格拆分:

fr = open('lenses.txt')
lenses = [i.strip().split('\t') for i in fr.readlines()]

得到如下matrix:

组成特征标签:

lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']

创建树:

lensesTree = trees.createTree(lenses, lensesLabels)
{'tearRate': {'normal': {'astigmatic': {'no': {'age': {'pre': 'soft',
      'presbyopic': {'prescript': {'hyper': 'soft', 'myope': 'no lenses'}},
      'young': 'soft'}},
    'yes': {'prescript': {'hyper': {'age': {'pre': 'no lenses',
        'presbyopic': 'no lenses',
        'young': 'hard'}},
      'myope': 'hard'}}}},
  'reduced': 'no lenses'}}

画图:

treePlotter.createPlot(lensesTree)

可以从树看到这里最长只需要4个特征节点就能得出分类结果,也就是问4个问题能确定适合哪种类型的隐形眼镜。

决策树在根据数据集匹配后,因为这些选项很多,会出现overfitting的问题,所以会需要减少一些节点。

以上的称为ID3算法,用于划分标称型数据集,无法直接处理数值型数据,尽管可以通过量化把数值型数据转为标称型数据。构建决策树时,采用递归方法将数据集转化为决策树。

补充知识:

标称型数据:在有限数据中取值,比如“是”和“否”,一般用于分类。

数值型数据:在无限数据中取值,比如1.25,主要用于回归分析。

自我小吐槽:

这一节的思路可能比较不容易融入自己的知识中,不好记住并转化为自己在处理数据时可用。但我相信多练习会越来越好的~~加油!

猜你喜欢

转载自blog.csdn.net/gritsissi/article/details/86762279
今日推荐