作者:WenWu_Both
出处:http://blog.csdn.net/wenwu_both/article/
版权:本文版权归作者和CSDN博客共有
转载:欢迎转载,但未经作者同意,必须保留此段声明;必须在文章中给出原文链接;否则必究法律责任

本文作为周志华《机器学习》的阅读笔记。

(1)基本流程
一般的,一棵决策树包含一个根节点、若干个内部节点和若干个叶节点。叶节点对应于决策结果,其他每个节点则对应于一个属性测试;每个节点包含的样本集合根据属性测试的结果被划分到子节点中;根节点包含样本全集。从根节点到每个叶节点的路径对应一个判定测试序列。决策树的学习目的是得到一棵泛化能力强的决策树。

决策树是递归过程,三种情形会导致递归返回:
1、当前节点包含的样本全属于同一类别,无需划分
2、当前属性集为空,或是所有样本在所有属性上取值相同,无法划分
3、当前节点包含的样本集为空,不能划分

(2)划分选择
期望的目标:决策树的分支节点所包含的样本尽可能属于同一类别,即节点的“纯度”越来越高

1、信息增益
假定当前样本集合D中的第k类样本所占的比例为pk,则D的信息熵定义为:
这里写图片描述
Ent(D)的值越高,则D的纯度越高
假定离散属性a有V个可能的取值这里写图片描述,若使用a来对样本集D进行划分,则会产生V个分支节点,其中第v个分支节点包含了D中所有在属性a上取值为这里写图片描述的样本,记为这里写图片描述,可计算出用属性a对样本集D进行划分所获得的“信息增益”(information gain)
这里写图片描述

一般而言,信息增益越大,则意味着使用属性a来进行划分所获得的“纯度提升”越大。因此,我们可用信息增益来进行决策树的划分属性选择。

著名的ID3决策树算法就是以信息增益为准则来选择划分属性。

2、增益率
实际上,信息增益准则对可取值数目较多的属性有所偏好,为减少这种偏好可能带来的不利影响,著名的C4.5决策树算法不直接使用信息增益,而是使用“增益率”来选择最优划分模型。增益率定义为:
这里写图片描述

3、基尼指数(Gini index)
CART决策树使用“基尼指数”来选择划分属性,数据集的纯度可用基尼值来度量:
这里写图片描述

选择那个使得划分后基尼指数最小的属性作为最优划分属性。

(3)剪枝处理
剪枝是决策树学习算法对付过拟合的主要手段。

预剪枝:在决策树生成过程中,对每个节点在划分前进行估计,若当前节点的划分不能带来决策树泛化能力的提高,则将当前节点定为叶节点。
后剪枝:完整决策树生成后,由底向上对非叶节点进行考察,若将该非叶结点替换为叶节点能带来决策树泛化能力提高,则完成替换。

下面基于python3.5进行基于信息增益的决策树(ID3)编程实现,数据集采用西瓜数据集2.0,具体如下:

编号,色泽,根蒂,敲声,纹理,脐部,触感,好瓜
1,青绿,蜷缩,浊响,清晰,凹陷,硬滑,是
2,乌黑,蜷缩,沉闷,清晰,凹陷,硬滑,是
3,乌黑,蜷缩,浊响,清晰,凹陷,硬滑,是
4,青绿,蜷缩,沉闷,清晰,凹陷,硬滑,是
5,浅白,蜷缩,浊响,清晰,凹陷,硬滑,是
6,青绿,稍蜷,浊响,清晰,稍凹,软粘,是
7,乌黑,稍蜷,浊响,稍糊,稍凹,软粘,是
8,乌黑,稍蜷,浊响,清晰,稍凹,硬滑,是
9,乌黑,稍蜷,沉闷,稍糊,稍凹,硬滑,否
10,青绿,硬挺,清脆,清晰,平坦,软粘,否
11,浅白,硬挺,清脆,模糊,平坦,硬滑,否
12,浅白,蜷缩,浊响,模糊,平坦,软粘,否
13,青绿,稍蜷,浊响,稍糊,凹陷,硬滑,否
14,浅白,稍蜷,沉闷,稍糊,凹陷,硬滑,否
15,乌黑,稍蜷,浊响,清晰,稍凹,软粘,否
16,浅白,蜷缩,浊响,模糊,平坦,硬滑,否
17,青绿,蜷缩,沉闷,稍糊,稍凹,硬滑,否

代码参考博客:http://whatbeg.com/2016/04/23/decisiontree.html

# Python 3.5
from math import log
from operator import itemgetter

def filetoDataSet(filename): # 构建训练集(从txt文件读入)
    fr = open(filename,'r')
    all_lines = fr.readlines()
    featname = all_lines[0].strip().split(',')[1:-1]
    print(featname)
    dataSet = []
    for line in all_lines[1:]:
        line = line.strip()
        lis = line.split(',')[1:]
        dataSet.append(lis)
    fr.close()
    return dataSet,featname
    
def calcEnt(dataSet):           #计算信息熵
    numEntries = len(dataSet) # 样本数
    labelCounts = {}
    for featVec in dataSet:
        label = featVec[-1]  # 结果标签
        if label not in labelCounts.keys(): # 计算不同结果标签的样本个数
            labelCounts[label] = 0
        labelCounts[label] += 1
    Ent = 0.0
    for key in labelCounts.keys():
        p_i = float(labelCounts[key]/numEntries)
        Ent -= p_i * log(p_i,2) # 累减
    return Ent
    
def splitDataSet(dataSet, axis, value):   #划分数据集,找出第axis个属性为value的数据
    returnSet = []
    for featVec in dataSet:
        if featVec[axis] == value:
            retVec = featVec[:axis]
            retVec.extend(featVec[axis+1:])
            returnSet.append(retVec)
    return returnSet
    
def chooseBestFeat(dataSet):
    numFeat = len(dataSet[0])-1 # 属性数量
    Entropy = calcEnt(dataSet) # 样本集的信息熵
    DataSetlen = float(len(dataSet)) # 样本数量 
    bestGain = 0.0
    bestFeat = -1 # 初始化
    for i in range(numFeat):
        allvalue = [featVec[i] for featVec in dataSet] # 每一列
        specvalue = set(allvalue) # 去除重复值
        nowEntropy = 0.0
        for v in specvalue:
            Dv = splitDataSet(dataSet,i,v)
            p = len(Dv)/DataSetlen
            nowEntropy += p * calcEnt(Dv)
        if Entropy - nowEntropy > bestGain:
            bestGain = Entropy - nowEntropy
            bestFeat = i
    return bestFeat
    
def Vote(classList): # 如果属性用完,类别仍不一致,投票决定
    classdic = {}
    for vote in classList:
        if vote not in classdic.keys():
            classdic[vote] = 0
        classdic[vote] += 1
    sortedclassDic = sorted(classdic.items(),key=itemgetter(1),reverse=True)
    return sortedclassDic[0][0]

def createDecisionTree(dataSet,featnames):
    featname = featnames[:]              ################
    classlist = [featvec[-1] for featvec in dataSet]  #此节点的分类情况
    if classlist.count(classlist[0]) == len(classlist):  #全部属于一类
        return classlist[0]
    if len(dataSet[0]) == 1:         #分完了,没有属性了
        return Vote(classlist)       #少数服从多数
    # 选择一个最优特征进行划分
    bestFeat = chooseBestFeat(dataSet)
    bestFeatname = featname[bestFeat]
    del(featname[bestFeat])     #防止下标不准
    DecisionTree = {bestFeatname:{}}
    # 创建分支,先找出所有属性值,即分支数
    allvalue = [vec[bestFeat] for vec in dataSet]
    specvalue = sorted(list(set(allvalue)))  #使有一定顺序
    for v in specvalue:
        copyfeatname = featname[:]
        DecisionTree[bestFeatname][v] = createDecisionTree(splitDataSet(dataSet,bestFeat,v),copyfeatname) # 递归
    return DecisionTree
    
if __name__ == '__main__':
    filename = "C:\\Users\\JiaoTong\\Desktop\\CSDN\\tree_data.txt"
    DataSet,featname = filetoDataSet(filename)
    Tree = createDecisionTree(DataSet,featname)
    print(Tree)

顺带基于matplotlib进行决策树可视化:
treePlot.py

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

decNode = dict(boxstyle="sawtooth",fc='0.8')
leafNode = dict(boxstyle="round4",fc='0.8')
arrow_args = dict(arrowstyle="<-")

def plotNode(nodeTxt, centerPt, fatherPt, nodeType):
    createPlot.ax1.annotate(nodeTxt, xy=fatherPt, xycoords='axes fraction', xytext=centerPt, textcoords = 'axes fraction', va='center', ha='center', bbox=nodeType, arrowprops=arrow_args)

def getNumLeafs(Tree):
    num = 0
    root = list(Tree.keys())[0]
    firstGen = Tree[root]
    for key in firstGen.keys():
        if type(firstGen[key]) == type({}):
            num += getNumLeafs(firstGen[key])
        else:
            num += 1
    return num

def DepthofTree(Tree):
    maxdepth = 0
    root = list(Tree.keys())[0]
    firstGen = Tree[root]
    for key in firstGen.keys():
        if type(firstGen[key]) == type({}):
            depth = 1 + DepthofTree(firstGen[key])
        else:
            depth = 1
        if depth > maxdepth:
            maxdepth = depth
    return maxdepth

def plotMidText(nowPt, fatherPt, txt):
    xMid = (fatherPt[0]-nowPt[0]) / 2.0 + nowPt[0]
    yMid = (fatherPt[1]-nowPt[1]) / 2.0 + nowPt[1]
    createPlot.ax1.text(xMid,yMid,txt)

def plotTree(Tree, fatherPt, nodeTxt):
    numLeafs = getNumLeafs(Tree)
    depth = DepthofTree(Tree)
    root = list(Tree.keys())[0]
    nowPt = (plotTree.xoff + (1.0 + float(numLeafs))/2.0/plotTree.totalW, plotTree.yoff)
    plotMidText(nowPt,fatherPt,nodeTxt)
    plotNode(root, nowPt, fatherPt, decNode)
    firstGen = Tree[root]
    plotTree.yoff = plotTree.yoff - 1.0/plotTree.totalD
    for key in firstGen.keys():
        if type(firstGen[key]) == type({}):
            plotTree(firstGen[key], nowPt, str(key))
        else:
            plotTree.xoff = plotTree.xoff + 1.0/plotTree.totalW
            plotNode(firstGen[key], (plotTree.xoff, plotTree.yoff), nowPt, leafNode)
            plotMidText((plotTree.xoff, plotTree.yoff), nowPt, str(key))
    plotTree.yoff = plotTree.yoff + 1.0/plotTree.totalD

def createPlot(Tree):
    fig = plt.figure(1, facecolor='white')
    fig.clf()
    axprops = dict(xticks=[],yticks=[])
    createPlot.ax1 = plt.subplot(111,frameon=False,**axprops)
    plotTree.totalW = float(getNumLeafs(Tree))
    plotTree.totalD = float(DepthofTree(Tree)) 
    plotTree.xoff = -1.0 / 2.0 / plotTree.totalW    # 1分成2倍叶子数那么多份
    plotTree.yoff = 1.0
    plotTree(Tree, (0.5,1.0), '')
    plt.show()

if __name__ == '__main__':
    filename = "C:\\Users\\JiaoTong\\Desktop\\CSDN\\tree_data.txt"
    DataSet,featname = decTree.filetoDataSet(filename)
    Tree = decTree.createDecisionTree(DataSet,featname)
    print(Tree)
    createPlot(Tree)

执行程序,生成结果如下:

这里写图片描述

如果想将上面的程序改成基于增益率的决策树算法(C4.5),只需要修改chooseBestFeat函数即可,其修改如下:

def chooseBestFeat(dataSet):  # 该步骤直接决定了不同的决策树算法
    # 基于增益率
    numFeat = len(dataSet[0])-1 # 属性数量
    Entropy = calcEnt(dataSet) # 样本集的信息熵
    DataSetlen = float(len(dataSet)) # 样本数量 
    bestGain_ratio = 0.0
    bestFeat = -1 # 初始化
    for i in range(numFeat):
        allvalue = [featVec[i] for featVec in dataSet] # 每一列
        specvalue = set(allvalue) # 去除重复值
        nowEntropy = 0.0
        IV = 0.0
        for v in specvalue:
            Dv = splitDataSet(dataSet,i,v)
            p = len(Dv)/DataSetlen
            nowEntropy += p * calcEnt(Dv)
            IV -= p * log(p,2)  # 此处计算的是IV(a)
        if (Entropy - nowEntropy)/IV > bestGain_ratio:
            bestGain_ratio = (Entropy - nowEntropy)/IV
            bestFeat = i
    return bestFeat

这里写图片描述

同样,我们可以继续改写,得到基于基尼系数的决策树算法(CART),只需要修改calcEnt函数及chooseBestFeat函数,修改如下:

a. calcEnt函数换为calcGini函数

def calcGini(dataSet):           #计算基尼系数
    numEntries = len(dataSet) # 样本数
    labelCounts = {}
    for featVec in dataSet:
        label = featVec[-1]  # 结果标签
        if label not in labelCounts.keys(): # 计算不同结果标签的样本个数
            labelCounts[label] = 0
        labelCounts[label] += 1
    Gini = 0.0
    for key in labelCounts.keys():
        p_i = float(labelCounts[key]/numEntries)
        Gini -= p_i * p_i # 累减
    Gini += 1
    return Gini

b. 改写chooseBestFeat函数

def chooseBestFeat(dataSet):  # 该步骤直接决定了不同的决策树算法
    # 基于基尼系数
    numFeat = len(dataSet[0])-1 # 属性数量
    DataSetlen = float(len(dataSet)) # 样本数量 
    bestGini_index = 1.0
    bestFeat = -1 # 初始化
    for i in range(numFeat):
        allvalue = [featVec[i] for featVec in dataSet] # 每一列
        specvalue = set(allvalue) # 去除重复值
        Gini_index = 0.0
        for v in specvalue:
            Dv = splitDataSet(dataSet,i,v)
            p = len(Dv)/DataSetlen
            Gini_index += p * calcGini(Dv)
        if Gini_index < bestGini_index:
            bestGini_index = Gini_index
            bestFeat = i
    return bestFeat

这里写图片描述

可以看到,在西瓜集上,基于信息增益及基尼系数得到的结果是一样的,而基于增益率生成的树与上面两种略有差别。

(4)连续与缺失值

1、连续值处理
由于连续属性的可取值数目不再有限,因此,不能直接根据连续属性的可取值对节点进行划分,此时,连续属性离散化技术派上用场。最简单的策略是采用二分法(bi-partition)对连续属性进行处理,这也是C4.5决策树采用的机制。
其基本过程如下:
a. 将样本集D(样本数为n)的连续属性a的值从小到大排序。
b. 相邻两个值求平均数,则得到n-1个值(t1,t2,…ti,…tn-1)。
c. 计算划分点为ti时的信息增益,谁的信息增益最大,则选择谁作为划分点。

2、缺失值处理

主要面临两个问题:
a. 如何在属性值缺失的情况下进行划分属性选择?
b. 给定划分属性,若样本在该属性上的值缺失,如何对样本进行划分?

下面给出C4.5的解决策略:
实际上是基于权重法,为每一个属性添加一个权重。

这里写图片描述

(5)多变量决策树

这一部分以后专门用一篇博文来讲。

PS: 记录书中的一道习题,感觉非常有意思。

Q: 试将决策树生成的深度优先搜索过程改为广度优先搜索,以参数MaxNode控制树的最大节点数,并与基于队列的决策树算法进行比较,分析哪种方式更易于控制决策树所需存储不超过内存。

Logo

CSDN联合极客时间,共同打造面向开发者的精品内容学习社区,助力成长!

更多推荐