《ML in Action》笔记(2) —— ID3决策树

算法概述

  • 决策树是一种分类器,通过一系列“精心设计”的检验记录属性的问题路径,将记录归入某一个类别。构造决策树需要使用训练数据集,找到最优的问题路径设置。建立决策树后,既可用于对新纪录的分类判决。

  • 决策树的问题路径并不是随意设置的,而是要遵循最优/次最优的规则。理论上对于给定的属性集,可构造的决策树数量为指数级,而在实际构造中,一般采用贪心策略的递归算法。其核心思路是在构建每层分类问题是都遵循信息增益最大原则(即熵或基尼不纯性最小)。

  • 直观地理解,最优划分策略就是能够将不同类别的记录尽可能的区分开来。如果经过一个检验问题后,不同类别的记录还是混杂在每个叶结点,这个问题肯定不是一个好的分类问题。

  • 最优划分策略的度量有几种:熵、基尼不纯性、分类误差。这三种度量是一致的,而熵的区分度较好,所以一般就使用熵。(关于熵的直观理解比较困难,但熵是一个超经典的度量变量,可参见信息论有关内容)

  • 建立决策树的另一个关键问题是决策树的停止条件,也就是决策树的大小,每个叶结点终止下分的条件。

构建决策树(ID3算法)

  • 主要步骤:
    1. 按照某一个属性进行数据集划分
    2. 计算每一种划分的香农熵(信息增益)
    3. 选出最优的(信息增益最大)的划分属性
    4. 按该属性划分后,对子结点进行递归
    5. 对于达到停止条件的子结点,根据记录数选举分类结果

【trees.py: part1 —— 决策树核心函数calcShannonEnt()】

def calcShannonEnt(dataSet):
    # 数据集记录个数
    numEntries = len(dataSet)
    # 初始化,保存各分类的记录个数
    labelCounts = {}
    # 遍历数据集,统计各分类标签的记录个数
    for featVec in dataSet: 
        # 当前记录的分类标签
        currentLabel = featVec[-1]
        # 分类标签计数+1
        if currentLabel not in labelCounts.keys(): labelCounts[currentLabel] = 0
        labelCounts[currentLabel] += 1
    # 初始化,保存香农熵计算结果
    shannonEnt = 0.0
    # 遍历标签dict
    for key in labelCounts:
        # 计算频率
        prob = float(labelCounts[key])/numEntries
        # 累加熵
        shannonEnt -= prob * log(prob,2) #log base 2
    #返回熵
    return shannonEnt

【trees.py: part2 —— 决策树核心函数splitDataSet()】

# 按照给定特征划分数据集
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

【trees.py: part3 —— 决策树核心函数chooseBestFeatureToSplit()】

# 选出划分后信息增益最大的属性
def chooseBestFeatureToSplit(dataSet):
    # 取数据集的属性数
    numFeatures = len(dataSet[0]) - 1
    # 计算数据集的初始香农熵
    baseEntropy = calcShannonEnt(dataSet)
    # 初始化,最大增益、最佳属性ID
    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
    # 输出信息增益最大的属性ID
    return bestFeature

【trees.py: part4 —— 决策树核心函数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=lambda item:item[1], reverse=True)
    # 返回类别标签
    return sortedClassCount[0][0]

【trees.py: part5 —— 决策树核心函数createTree()】

# 创建递归决策树
def createTree(dataSet,lb):
    # 由于传址参数的问题。故增加一行代码,将lb变量另外复制一份。使用copy.copy()
    labels = copy.copy(lb)
    # 取出训练数据集的所有类别信息
    classList = [example[-1] for example in dataSet]
    # 巧用count(),统计分类列表中与第一个结果相同的个数,若该个数等于数组总长度,则说明所有记录归属同一类别,停止划分
    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:{}}
    # 在labels中删去已用于划分的属性
    # 注:labels列表是可变对象,在PYTHON函数中作为参数时传址引用,能够被全局修改,所以这行代码导致函数外的同名变量被删除了元素,造成例句无法执行,提示'no surfacing' is not in list
    del(labels[bestFeat])
    # 取出该属性的所有值
    featValues = [example[bestFeat] for example in dataSet]
    # 剔重数值
    uniqueVals = set(featValues)
    # 遍历数值,对所有数值划分后的数据子集进行递归
    for value in uniqueVals:
        # 复制当前labels,以便原变量不被改变
        subLabels = labels[:]
        # splitDataSet对最佳属性的当前数值进行划分,然后递归调用createTree,子进程构建的决策树逐层代入myTree树结构
        myTree[bestFeatLabel][value] = createTree(splitDataSet(dataSet, bestFeat, value),subLabels)
    return myTree 

【trees.py: part6 —— 决策树核心函数classify()】

def classify(inputTree,featLabels,testVec):
    # 获得首层属性名称
    # 注:原代码为firstStr = myTree.keys()[0]。在python3中报错'dict_keys' object does not support indexing。因为python3的dict.keys返回dict_keys对象,支持iterable但不支持indexable,所以要将类型转换为list。
    firstStr = list(inputTree.keys())[0]
    # 获得首层树DICT
    secondDict = inputTree[firstStr]
    # 反查首层树所用属性的ID
    featIndex = featLabels.index(firstStr)
    # 获得测试向量的该属性数值
    key = testVec[featIndex]
    # 获得该数值对应的子结点
    valueOfFeat = secondDict[key]
    # 如果子结点是字典,则递归继续分类;否则就得到分类结果
    if isinstance(valueOfFeat, dict): 
        # valueOfFeat是下层树,可直接用于递归
        classLabel = classify(valueOfFeat, featLabels, testVec)
    else: classLabel = valueOfFeat
    # 返回分类结果
    return classLabel

【trees.py: part7 —— 存储和调用决策树】

# 将决策树存储
def storeTree(inputTree,filename):
    import pickle
    # pickle(除了最早的版本外)是二进制格式的,打开文件需带 'b' 标志。
    fw = open(filename,'wb')
    pickle.dump(inputTree,fw)
    fw.close()
    
# 读取决策树
def grabTree(filename):
    import pickle
    fr = open(filename,'rb')
    return pickle.load(fr)

【IPYTHON —— 运行案例lenses】

fr = open('lenses.txt')
#读取行、拆分、去除回车符
lenses = [inst.strip().split('\t') for inst in fr.readlines()]
lensesLabels = ['age','prescript','astigmatic','tearRate']
lensesTree = trees.createTree(lenses,lensesLabels)

算法小结

PYTHON小结

posted @ 2017-02-08 14:30  公爵  阅读(382)  评论(1编辑  收藏  举报