决策树之CART算法分类树原理及python实现

决策树

决策树模型是一种传统的算法,与人类思维十分相似

  • 基本思想:模拟人类进行级联选择或决策的过程,按照属性的某个优先级依次对数据的全部属性进行判别,从而得到输入数据对应的预测输出。
  • 树形模型是一个一个特征值进行处理,而线性模型是所有特征赋权相加得到新值。
  • 监督学习模型:通过对训练集的学习,挖掘其中的规则,预测新数据
  • 基本结构:根节点为训练全集,中间节点为属性测试,叶子节点为决策结果

决策树的特点

优点

  1. 计算简单,可解释性强。
  2. 适合处理有缺失属性的样本。
  3. 可自动忽略目标变量中没有贡献的属性。

缺点

  1. 容易造成过拟合,需要采取剪枝操作。
  2. 忽略数据之间的相关性。
  3. 信息增益偏向于变量取值更多的离散特征。

决策树构造

基本思路:
  决策树的构造本质上是一种贪心算法,每次都根据分类规则找到最优的划分特征,根据该特征将节点样本划分若干部个样本集,不断重复,构造新的节点,重复递归直至满足终止条件。

终止条件:

  • 当前节点包含样本全属于同一类别,无需划分
  • 当前节点包含样本集为空,不能划分
  • 当前属性集为空,或所有样本在所有属性上取值相同,无法划分

决策树生成算法

三种经典的决策树生成算法

  • 基于信息增益的ID3算法
    -基于信息增益率的C4.5算法
    -基于基尼指数的CART算法

请添加图片描述

CART算法

CART算法构造一棵分类回归树,既可以作分类用,也可作回归用。构造的决策树为二叉树,对于特征取值大于2的需要进行二元划分。
如婚姻状况有三种取值:{单身,已婚,离异},在进行划分时,首先将其划分为{已婚,非已婚}两类,其次再将非已婚划分为{单身,离异}两类。

构造分类决策树

请添加图片描述

python代码实现

'''
导入训练集数据,注意这里的特征值score为连续型变量
'''
def train_dataset():
    dataset = [[10, 'low'],
               [26, 'low'],
               [37, 'low'],
               [27, 'mid'],
               [48, 'mid'],
               [52, 'low'],
               [60, 'mid'],
               [72, 'mid'],
               [68, 'high'],
               [80, 'high'],
               [89, 'high'],
               [95, 'high']]
    features = ['score']
    return dataset, features


'''
计算当前集合的Gini系数
'''
def calGini(dataset):
    # 得到样本总数
    data_count = len(dataset)
    labels = {}
    for data in dataset:
        # 统计标签出现的次数
        if data[-1] not in labels:
            labels[data[-1]] = 1
        else:
            labels[data[-1]] += 1
    # 计算当前集合中各标签样本数出现的概率
    for label in labels:
        labels[label] /= data_count
        labels[label] = labels[label] * labels[label]
    # 计算按照当前方式分类的Gini系数
    Gini = 1 - sum(labels.values())
    return Gini


'''
分割样本集,按照特征值index可能的取值value,将样本集分成取值为value和取值不为value的两部分
二分法构造二叉树
'''
def split_dataset(dataset, index, value):
    sub_dataset1 = []
    sub_dataset2 = []
    for data in dataset:
        # 离散值划分:等于value / 不等于value
        # 连续值划分:<= value / > value
        if data[index] <= value:
            # 注意特征值要再用来划分(cart算法可以重复利用特征值)
            # current_list = data[: index]
            # current_list.extend(data[index + 1:])
            sub_dataset1.append(data)
        else:
            # current_list = data[: index]
            # current_list.extend(data[index + 1:])
            sub_dataset2.append(data)
    return sub_dataset1, sub_dataset2


'''
选择最佳的划分特征属性和对应属性值
'''
def choose_best_feature(dataset):
    # 特征值个数
    feature_count = len(dataset[0]) - 1
    # 定义最佳基尼系数、最优特征的索引、最优划分点
    bestGini = 1
    index_of_best_feature = -1
    best_split_point = 0.0
    
    for i in range(feature_count):
        # 当取值为离散型变量时:得到当前特征属性的所有取值(去重,每个取值只出现一次),如婚姻状况的三种取值:单身、已婚、离异
        # colValue = set(data[i] for data in dataset)
        # 当取值为连续型变量时:得到当前特征属性的所有取值,从小到大进行排序
        colValueRow = list(data[i] for data in dataset)
        colValueRow.sort()
        colValue = []
        for j in range(len(colValueRow) - 1):
            # 对排序后的取值,取相邻两个值的平均数,
            # 如对特征值序列{10,20,30},划分点为<= 15, >15, <=25, >25
            colValue.append((colValueRow[j] + colValueRow[j + 1]) / 2)

        # 构建Gini字典, key为特征属性的取值,value为对应的gini系数
        Gini = {}
        for v in colValue:
            sub_dataset1, sub_dataset2 = split_dataset(dataset, i, v)
            prob1 = len(sub_dataset1) / len(dataset)
            prob2 = len(sub_dataset2) / len(dataset)

            sub_gini1 = calGini(sub_dataset1)
            sub_gini2 = calGini(sub_dataset2)
            # 由当前切分点划分后的基尼指数
            Gini[v] = prob1 * sub_gini1 + prob2 * sub_gini2
            # 更新最优划分点和最优特征
            if Gini[v] < bestGini:
                bestGini = Gini[v]
                index_of_best_feature = i
                best_split_point = v
        print(Gini)
    return index_of_best_feature, best_split_point


'''
构造决策树
1. 递归停止的条件:
> 对于当前节点的数据集,样本个数小于阈值或者没有特征,则返回决策子树,停止递归
> 
3. 算计当前节点各个特征属性的取值的gini系数,选择最优特征和最优切分点,划分样本为两部分,生成两个子节点
4. 对子节点递归调用决策树构造函数,生成CART分类树
'''
def create_decision_tree(dataset, features, i, labelValues):
    labels = [data[-1] for data in dataset]

    # 若当前集合中所有样本标签相等,返回标签值
    if labels.count(labels[0]) == len(labels):
        labelValues.remove(labels[0])
        return labels[0]
   # 若当前集合重样本大部分被分纯(阈值),返回标签值
    for label in labels:
        if labels.count(label) >= 0.8 * len(labels):
            labelValues.remove(label)
            return label

    # 这里i代表了决策树的高度,当决策树分类到指定高度时,递归停止
    if i == 0:
        return labelValues[0]

    # 开始构建决策树
    index_of_best_feature, best_split_point = choose_best_feature(dataset)
    best_feature = features[index_of_best_feature]
    # 初始化决策树
    decision_tree = {best_feature: {}}

    # CART可以重复使用特征值,只要能利用该特征继续进行划分就可以使用
    sub_dataset1, sub_dataset2 = split_dataset(dataset, index_of_best_feature, best_split_point)
    # 递归调用,构建子树,左子树是等于切分点值的,右子树是不等于切分点的
    decision_tree[best_feature][best_split_point] = create_decision_tree(sub_dataset1, features, i - 1, labelValues)
    decision_tree[best_feature]['others'] = create_decision_tree(sub_dataset2, features, i - 1, labelValues)
    return decision_tree


'''
用训练好的决策树对新样本进行分类
'''
def classify(decision_tree, features, test_example):
    # 根节点代表的属性
    root_feature = list(decision_tree.keys())[0]
    # 第一个分类属性的值
    second_dict = decision_tree[root_feature]
    classLabel = ''
    # 判断该属性值是属性中的第几个
    index_of_first_feature = features.index(root_feature)
    for key in second_dict.keys():
        if key != 'others':
            if test_example[index_of_first_feature] <= key:
                # 若当前属性值是字典,则递归查询
                if type(second_dict[key]).__name__ == 'dict':
                    classLabel = classify(second_dict[key], features, test_example)
                else:
                    classLabel = second_dict[key]
            else:
                if isinstance(second_dict['others'], str):
                    classLabel = second_dict['others']
                else:
                    classLabel = classify(second_dict['others'], features, test_example)
    return classLabel


'''
对测试集进行分类,统计各标签下的数据量
'''
def getClassifyResult(test_dataset):
    dataset, features = train_dataset()
    labelValues = ['low', 'mid', 'high']
    decision_tree = create_decision_tree(dataset, features, 2, labelValues)
    labels = {}
    for example in test_dataset:
        label = classify(decision_tree, features, example)
        if label not in labels:
            labels[label] = 0
        labels[label] += 1
    return labels


if __name__ == '__main__':

    test_example = [[63]]
    print(getClassifyResult(test_example))

测试集为score=63,得到结果为:分类为mid
请添加图片描述

决策树可视化

上面只能看到分类的一个结果,更加直观的方式为画出决策树。

import matplotlib.pyplot as plt
import CART

# 设置显示中文字体
plt.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):
    # matplotlib.pyplot.annotate(text, xy, *args, **kwargs
    # text:文本注释
    # xy: 被指向的数据点(x,y)的位置坐标
    # xytext: 注释文本的坐标点
    # textcoords / xtcoords: 注释文本的坐标系属性
    createPlot.ax1.annotate(nodeTxt, xy=parentPt, xycoords='axes fraction',
                            xytext=centerPt, textcoords='axes fraction',
                            va="center", ha="center", bbox=nodeType, arrowprops=arrow_args)


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]
    # 计算子节点的y偏移
    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:
            # 计算子节点的x偏移
            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. / 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()

得到绘图的结果为:
在这里插入图片描述
上图的划分结果为:
score <= 26.5 : low
26.5 < score <= 64: mid
score > 64: high
从而印证了上述代码分类结果的正确性。

sklearn构造决策树

除手动构建决策树外,sklearn提供了构造决策树的函数,能够快速构造出CART决策树。

def train(x, y):
    # 训练集和测试集划分
    x_train, x_test, y_train, y_test = train_test_split(x, y, test_size=0.2)
    # 建立决策树模型
    model = tree.DecisionTreeClassifier(
        criterion='gini',  # 采用gini还是entropy进行特征选择
        max_depth=2,  # 树的最大深度
        min_samples_split=2,  # 内部节点分裂所需要的最小样本数量
        min_samples_leaf=1,  # 叶子节点所需要的最小样本数量
        max_features=None  # 寻找最优分割点时的最大特征数
    )
    # 对模型进行训练
    model.fit(x_train, y_train)
    # 预测,比较结果
    print("预测结果准确性:")
    print(model.score(x_test, y_test))
    return model

'''
以dot形式导出决策树
可采用以下命令生成图形渲染: 
dot -Tpng tree.dot -o tree.png
或者直接调用drawTree函数,得到Sourve.gv.pdf
'''
def drawTree(model, features, labels):
    with open("tree.dot", 'w') as f:
        f = tree.export_graphviz(model,
                                 out_file=f,
                                 feature_names=features,
                                 class_names=labels)
    with(open("tree.dot")) as f:
        dot_graph = f.read()
    graph = graphviz.Source(dot_graph)
    graph.view()

画出的决策树如下图(PS:这里画的树数据集改变了,所以与上面分类结果不同)
请添加图片描述

参数解读:

  1. 内部分支上第一行为分类点
  2. gini / entropy为该节点的对应gini系数/信息熵
  3. samples为该分类下样本的容量
  4. value表示该分类下各个label中包含的样本数量
    example: value=[1,3,0]表示该分类中包含1个标签为low,3个标签为mid的样本
  5. class主要显示出容量多的样本

参考内容:
【1】https://zhuanlan.zhihu.com/p/32164933
【2】https://blog.csdn.net/qq_45717425/article/details/120992980
【3】Peter Harrington.Machine Learning in Action(《机器学习实战》)[M].北京:人民邮电出版社,2013

  • 2
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值