机器学习——决策树ID3算法(预测隐形眼镜类型)

克劳德·香农被公认为是二十世纪最聪明的人之一,威廉·庞德斯通在其2005年出版的《财富公式》一书中是这样描写的:“贝尔实验室和MIT有很多人将香农合爱因斯坦相提并论,而其他人则认为这种对比是不公平的——对香农是不公平的。”

目录:

克劳德·香农

克劳德·香农他定义了信息,用‘信息熵’对信息进行了量化。通过信息出现的概率来度量一个信息的重要程度。假设,一个事件发生的概率为 p ( x ) 于是就有了以下定义,

( i n f o r m a t i o n ) = l o g 2 p ( x )

很显然,当事件发生概率为 p ( x ) = 1 时,信息量为0。相反,当概率趋近于0时,

lim p ( x ) 0 l o g 2 p ( x ) =

信息熵

以上,讨论的只是单个事件的信息量。那么,多个事件的混乱程度要如何度量呢?香农提出了‘信息熵’的概念,

( H ) = i = 1 n p ( x i ) l o g 2 p ( x i ) _

很明显,信息熵就是将单个事件的‘信息量’,按照其发生的概率进行加权的结果。下面给出一个dataSet实战一下信息熵的计算,其中,前两列为特征 ( f e a t u r e 1 , f e a t u r e 2 ) ,最后一列为类别标签 l a b e l

d a t a S e t = [ 1 1 y e s 1 1 y e s 1 0 n o 0 1 n o 0 1 n o ]

计算信息熵,输出结果应为 H = 0.9709505944546686

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for fectVec in dataSet:
        currentLabel = fectVec[-1]
        labelCounts[currentLabel] = labelCounts.get(currentLabel,0) + 1
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * math.log(prob,2)
    return shannonEnt

信息熵的计算很简单,只要计算出 p ( x i ) 即可, l a b e l C o u n t s < k e y , v a l u e > 字典中的key对应类别(‘yes’ or ‘no’),value表示的是出现的次数。

prob = float(labelCounts[key])/numEntries

很显然, p ( y e s ) = 2 5 , p ( n o ) = 3 5 ,立即可得出信息熵 H ( d a t a S e t )

H ( d a t a S e t ) = i = 1 2 p ( x i ) l o g 2 p ( x i ) = ( 2 5 × l o g 2 2 5 + 3 5 × l o g 2 3 5 ) = 0.97

信息增益

在信息增益中,衡量标准是看特征能够为分类系统带来多少信息。对一个特征而言,系统有它没它时信息量将发生变化,而前后信息量的差值就是这个特征给系统带来的信息量(亦即,信息增益)。

划分数据集

计算信息增益时,需讨论每一特征(比如 a x i s = 0 ,则对应 f e a t u r e 1 )对信息熵的影响。而每一特征又有不同的值(下图一特征只有两种值——‘是’ 1 、‘否’ 0 ),需要分别统计1、0出现的概率。很显然,对于特征1有 p r o b 1 = 3 5 p r o b 0 = 2 5 。这时候再求出 [ 1 n o 1 n o ] 的信息熵 E n t 0 ,接着求 [ 1 y e s 1 y e s 0 n o ] 的信息熵 E n t 1 。那么,特征1带来的信息增益则为,

I n f o r m a t i o n   G a i n ( I G ) = p r o b 0 × E n t 0 + p r o b 1 × E n t 1

插图(补充)

数无形时少直觉,以下图示划分过程,

d a t a S e t = [ 1 1 y e s 1 1 y e s 1 0 n o 0 1 n o 0 1 n o ] { a x i s = 0 , v a l u e = 0 _ [ 1 n o 1 n o ] a x i s = 0 , v a l u e = 1 _ [ 1 y e s 1 y e s 0 n o ]

d a t a S e t 按照 a x i s 所在的特征列、 v a l u e 所对应的值,进行划分(见上图),

def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

补充1:关于extend

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

补充2:关于append

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

IG公式

上面已经说的很清楚了,如果非要给出完整的计算公式。那么,信息增益计算公式应该如下,

I G = H ( C ) H ( C | T ) = H ( C ) [ p ( t ) H ( C | t ) + p ( t ) H ( C | t ) _ ] = i = 1 n p ( x i ) l o g 2 p ( x i ) [ p ( t ) i = 1 n p ( x i | t ) l o g 2 p ( x i | t ) p ( t ) i = 1 n p ( x i | t ) l o g 2 p ( x i | t ) _ ]

其中, T 为选取的某一特征; p ( t ) 为特征 T 的出现概率, p ( t ) 为不出现概率; H ( C ) C 的信息熵。

以上的公式,只是一种最简单的形式,因为只考虑某一特征出现 t 、不出现 t 的情况。实际上,一个特征可能有好几种取值(而不仅仅是‘是’或者‘否’)。比如,学校就有双一流、985、211、一本、二本、三本、专科…

最好的划分方式

通过比较不同的划分方式所带来的信息增益,选择带来信息增益最大的那种划分方式,即为最好的划分方式,

def chooseBestFectureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 #remove label column
    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

插图(补充)

以下的过程图较好的分析了,上述划分代码执行的过程…

[ 1 1 y e s 1 1 y e s 1 0 n o 0 1 n o 0 1 n o ] { < c o l u m n = 0 > [ 1 1 1 0 0 ] < s e t > [ 0 1 ] { < v a l u e = 0 > [ 1 n o 1 n o ] < v a l u e = 1 > [ 1 y e s 1 y e s 0 n o ] C a l c   I G < c o l u m n = 1 > [ 1 1 0 1 1 ] < s e t > [ 0 1 ]

构造决策树

构造决策树的过程就是不断的选取特征的过程,再判断特征的其中每一个值是否可以最终区分类型,如果不能,则继续选择特征。举个例子,这里第一列(特征1)为 N o   s u r f a c i n g ,第二列(特征2)为 F l i p p e r s

d a t a S e t = [ 1 1 y e s 1 1 y e s 1 0 n o 0 1 n o 0 1 n o ] < N o   s u r f a c i n g > { < 0 > [ 1 n o 1 n o ] N o < 1 > [ 1 y e s 1 y e s 0 n o ] < F l i p p e r s > { < 0 > [ n o ] N o < 1 > [ y e s y e s ] Y e s

以上是一个很好的例子,但是有些特别的例子,当所有特征都被选择完毕了,仍然不能区分类别。比如,假设上图中 F l i p p e r s 取值为 < 1 > 得到的类别为 [ y e s n o y e s ] 。此时,已经没有特征可以提取了,这时候需要一种投票的方式来区分类别。

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

根据上图中的描述,以下给出生成决策树的代码。整个过程就是根据‘信息增益’递归选取特征的过程。那么,递归终止条件是?

  • 当某一特征取某一值时,全部为相同类别;(比如,上文中当 N o s u r f a c i n g 特征取值为 < 0 > 时,类别全部为 N o ,达到区分目的)
  • 当再无特征可提取时,即上文中提到的投票法 m a j o r i t y C n t
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 = chooseBestFectureToSplit(dataSet)
    bestFeatLabel = labels[bestFeat]
    myTree = {bestFeatLabel:{}}
    del(labels[bestFeat])
    featValues = [example[bestFeat] for example in dataSet]
    print(featValues)
    uniqueVals = set(featValues)
    for value in uniqueVals:
        subLabels = labels[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

上文所使用的数据集将最终产生一个如下决策树,

{ n o   s u r f a c i n g : { 0 : n o , 1 : { f l i p p e r s : { 0 : n o , 1 : y e s } } } }

使用决策树分类

决策树构建完毕之后。对于输入的测试数据,根据决策树找到叶子节点即可,其值即为最终类别。代码实现过程为不停的寻找 k e y ,一直到叶子节点(也就是直到执行下文代码中的 e l s e 部分,确定分类)。

决策树分类

#{'no surfacing': {0: 'no', 1: {'flippers': {0: 'no', 1: 'yes'}}}}
def classify(inputTree, featLabels, testVec):
    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

给出一个主函数,这里注意由于 c r e a t e T r e e 过程中会修改 L a b e l s ,这里传递了一个拷贝。

if __name__ == '__main__':
    dataSet,labels = createDataSet()
    myTree = createTree(dataSet, labels[:])
    cls = classify(myTree, labels, [1,0])
    print(cls)

存储、读取决策树

p i c k l e 模块来序列化对象(该默认存储的为二进制),这里为了不出现编码错误,直接存储、读取时都直接指定为 w b w r 格式。
存储决策树

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

读取决策树

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

再次,给出一个主函数,

if __name__ == '__main__':
    dataSet,labels = createDataSet()
    storeTree(createTree(dataSet, labels[:]), 'store_d_tree.txt')
    cls = classify(grabTree('store_d_tree.txt'), labels, [1,0])
    print(cls)

References:
[1] 李锐, 李鹏, 曲亚东, 王斌[译]. 机器学习实战[M]. 北京:人民邮电出版社, 2013.
[2] 百度百科,信息增益,https://baike.baidu.com/item/%E4%BF%A1%E6%81%AF%E5%A2%9E%E7%9B%8A,2018年06月29日


© qingdujun
2018-7-1 于 北京 怀柔


附录A (使用决策树预测隐形眼镜类型)

数据集地址: https://pan.baidu.com/s/1qHvawOiAxnBHieZdp9z_BQ 密码:7paq

#coding=utf-8
import math, operator, pickle

def calcShannonEnt(dataSet):
    numEntries = len(dataSet)
    labelCounts = {}
    for fectVec in dataSet:
        currentLabel = fectVec[-1]
        labelCounts[currentLabel] = labelCounts.get(currentLabel,0) + 1
    shannonEnt = 0.0
    for key in labelCounts:
        prob = float(labelCounts[key])/numEntries
        shannonEnt -= prob * math.log(prob,2)
    return shannonEnt

def splitDataSet(dataSet, axis, value):
    retDataSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            reducedFeatVec = featVec[:axis]
            reducedFeatVec.extend(featVec[axis+1:])
            retDataSet.append(reducedFeatVec)
    return retDataSet

def chooseBestFectureToSplit(dataSet):
    numFeatures = len(dataSet[0]) - 1 #remove label column
    baseEntropy = calcShannonEnt(dataSet)
    bestInfoGain = 0.0; bestFeature = -1
    for i in range(numFeatures):
        fectList = [example[i] for example in dataSet]
        uniqueVals = set(fectList)
        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

def majorityCnt(classList):
    classCount = {}
    for vote in classList:
        classCount[vote] = classCount.get(vote, 0) + 1
    sortedClassCount = sorted(classCount.items(), key = operator.intemgetter(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 = chooseBestFectureToSplit(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[:]
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value), subLabels)
    return myTree

if __name__ == '__main__':
    fr = open('lenses.txt')
    lenses = [inst.strip().split('\t') for inst in fr.readlines()]
    lensesLabels = ['age', 'prescript', 'astigmatic', 'tearRate']
    print(createTree(lenses, lensesLabels))

决策树应该为,

{
    'tearRate': {
        'normal': {
            'astigmatic': {
                'no': {
                    'age': {
                        'young': 'soft',
                        'presbyopic': {
                            'prescript': {
                                'myope': 'no lenses',
                                'hyper': 'soft'
                            }
                        },
                        'pre': 'soft'
                    }
                },
                'yes': {
                    'prescript': {
                        'myope': 'hard',
                        'hyper': {
                            'age': {
                                'young': 'hard',
                                'presbyopic': 'no lenses',
                                'pre': 'no lenses'
                            }
                        }
                    }
                }
            }
        },
        'reduced': 'no lenses'
    }
}

猜你喜欢

转载自blog.csdn.net/u012339743/article/details/80838840