《Machine Learning in Action》—— 小朋友,快来玩啊,决策树呦

《Machine Learning in Action》—— hao朋友,快来玩啊,决策树呦

在上篇文章中,《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”主要讲述了决策树的理论内容,介绍了什么决策树,以及生成决策树时所需要优先选取的三种决策标准。有学习的过SVM,或阅读过Taoye之前写的几篇SVM内容的文章可以发现,决策树相对于SVM来讲要简单很多,没有太多且复杂的公式推导。

我们在把之前的内容稍微回顾下:

  • 属性特征的信息增益越高,按道理来讲应当被优先选取,常用于\(ID3\)算法
  • 属性特征的增益率越高,按道理来讲应当被优先选取,常用与\(C4.5\)算法
  • 属性特征的尼基指数低,按道理来讲应当被优先选取,常用于\(CART\)算法

有了前面的内容做基础,阅读本篇文章就会很轻松了。本篇主要讲述以下几部分的内容:

  • 基于ID3算法手动构建决策树,并通过Matplotlib进行可视化
  • 基于已经构建好的决策树进行分类预测
  • 构建好的决策树模型应当如何保存和读取?
  • 通过鸢尾花(Iris)数据集,使用Sklearn构建决策树,

一、基于ID3算法手动构建决策树,并通过Matplotlib进行可视化

构建决策树的算法有好几种,比如像ID3、C4.5、CART之类的,限于篇幅、时间和精力的关系,本篇文章主要采用ID3算法来进行构建,使用到的决策标准(指标)是上篇文章中所提到的信息增益。关于C4.5和CART算法构建决策树,有兴趣的读者可以参考上期中的增益率和尼基指数的内容。

本次构建决策树所需要用到的数据集仍然是李航——《统计学习方法》中的贷款数据,这里再次把数据集放出来瞅瞅:

前面也有提到,ID3算法主要是基于信息增益作为选取属性特征的准则,在上期我们也计算过各个属性特征所对应的信息增益值,如下:

\[\begin{aligned}
& Gain(D, 年龄) = 0.083 \\
& Gain(D, 工作)=0.324 \\
& Gain(D, 房子)=0.420 \\
& Gain(D, 信用)=0.363
\end{aligned}
\]

而根据ID3算法过程,我们可以知道,需要优先选取信息增益最大的属性进行决策,即房子,也就是说将房子作为决策树的根节点。由于房子这一个属性特征有两个取值,所以引申出两个子节点,一个对应“有房子”,另一个对应“无房子”,而“有房子”的六个样本的类别都是允许放款,也就是有同一类样本点,所以它理应成为一个叶子节点,且节点的类不妨标记为“允许”。

这样一来,我们的根节点以及其中的一个叶子节点就确定了。接下来,我们需要将“无房子”所对应的样本集再次选取一个新的属性特征进行决策。注意:此次做决策的数据样本总体就不再是初始数据了,而是“无房子”所对应的所有样本,这一点需要格外注意。

我们在“无房子的”的数据样本中再次计算其他属性所对应的信息增益,我们不妨讲此次的数据样本集合记为\(D_1\),计算结果如下:

\[\begin{aligned}
& Gain(D_1, 年龄) = 0.251 \\
& Gain(D_1, 工作)=0.918 \\
& Gain(D_1, 信用)=0.474
\end{aligned}
\]

与上同样分析,可以发现此时工作所对应的信息增益值最高,也就是说第二个优先选取的属性为“工作”。而且,我们可以发现,在数据样本集\(D_2\)中,总共有9个,其中允许放款的有三个,拒绝的有6个,且结果标签与工作值刚好完全都对应上了,也就是说有工作的都允许放款了,没工作的都拒绝放款了。所以,在第二个属性特征选取完成之后,此时产生了俩个叶子节点,节点结果与是否有工作对应。

通过如上分析,我们就得到了此次基于ID3算法所构建出的决策树,决策树如下:

接下来我们通过代码来生成这颗决策树,对于树形结构的数据,我们可以通过字典或者说是json类型来进行保存。比如上图中的决策树,我们可以通过如下结果来进行表示:

{"房子": {
    "1": "Y",
    "0":{"工作": {
        "1": "Y",
        "0": "N"
    }}
}}

上述表示的数据格式我们一般称其为Json,这个在前后端、爬虫,亦或是在其他各种领域中都是接触的非常多的。另外,我们可以发现在决策树生成的过程中,在一个属性特征选取完成之后,需要经过同样的操作再次选取一个属性特征,其实就相当于一个周期,换句话讲正好满足了递归的特性,只是我们的数据总体发生了变化而已。既然我们明确了保存树形结构数据所需要的类型,下面我们通过代码来实现:

此次的代码相较于上篇文章中计算信息增益的变化主要有三个地方:

  • 由于我们最终需要生成属性的具体内容,而非仅仅索引,所以我们在创建数据的时候,除了返回数据本身之外,还需要返回对应的属性,修改establish_data方法如下所示:
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建训数据集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年纪", "工作", "房子", "信用"]
    return np.array(data), labels
  • 在上篇文章中,我们的handle_data方法仅仅是找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集。而要想构建决策树,在第一次选取了属性之后,应当将该属性从数据集中移除,所以修改handle_data如下:
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data
  • 第三个就是需要一个创建决策树的方法establish_decision_tree,具体代码如下,其中在选取完成一个属性之后需要递归调用本身来选取第二个属性
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建决策树
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 数据集中的类别只有一种
    best_feature_index = calc_information_gain(data)    # 通过信息增益优先选取最好的属性特征
    best_label = labels[best_feature_index]   # 属性特征对应的标签内容
    # feat_labels表示已选取的属性;新建一个决策树节点;将属性标签列表中删除已选取的属性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 获取最优属性对应值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

该部分的完整代码如下所示:

import numpy as np
import pandas as pd

np.__version__
pd.__version__

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建训数据集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年纪", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:计算信息熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:计算最大的信息增益,并输出其所对应的特征索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 属性特征的数量
    base_entropy = calc_information_entropy(data)                 # 计算总体数据集的信息熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大信息增益和对应的特征索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 计算属性特征划分后的信息增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 计算子集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 计算信息增益
        print("第%d个属性特征所对应的的增益为%.3f" % (index + 1, temp_information_gain))            # 输出每个特征的信息增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新信息增益,确定的最大的信息增益对应的索引
    return best_feature

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建决策树
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 数据集中的类别只有一种
    best_feature_index = calc_information_gain(data)    # 通过信息增益优先选取最好的属性特征
    best_label = labels[best_feature_index]   # 属性特征对应的标签内容
    # feat_labels表示已选取的属性;新建一个决策树节点;将属性标签列表中删除已选取的属性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 获取最优属性对应值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree

if __name__ == "__main__":
    data, labels = establish_data()
    print(establish_decision_tree(data, labels, list()))

运行结果:

{'房子': {'1': 'Y', '0': {'工作': {'1': 'Y', '0': 'N'}}}}

可见,代码运行的结果与我们手动创建的决策树如出一辙,完美,哈哈哈~~~

可是,如上决策树的显示未免有点太不亲民了,生成的决策树比较简单那还好点,假如我们生成的决策树比较复杂,那通过Json格式的数据来输出决策树就有点懵了。

对此,我们需要将决策树进行可视化,可视化主要使用到了Matplotlib包。关于Matplotlib的使用大家可以参考文档及其他资料,Taoye后期也会整理出一篇自己常用的接口。

import numpy as np
import pandas as pd

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建训数据集
"""
def establish_data():
    data = [[0, 0, 0, 0, 'N'],         # 样本数据集相关信息,前面几项代表属性特征,最后一项表示是否放款
            [0, 0, 0, 1, 'N'],
            [0, 1, 0, 1, 'Y'],
            [0, 1, 1, 0, 'Y'],
            [0, 0, 0, 0, 'N'],
            [1, 0, 0, 0, 'N'],
            [1, 0, 0, 1, 'N'],
            [1, 1, 1, 1, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [1, 0, 1, 2, 'Y'],
            [2, 0, 1, 2, 'Y'],
            [2, 0, 1, 1, 'Y'],
            [2, 1, 0, 1, 'Y'],
            [2, 1, 0, 2, 'Y'],
            [2, 0, 0, 0, 'N']]
    labels = ["年纪", "工作", "房子", "信用"]
    return np.array(data), labels

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:计算信息熵
"""
def calc_information_entropy(data):
    data_number, _ = data.shape
    information_entropy = 0
    for item in pd.DataFrame(data).groupby(_ - 1):
        proportion = item[1].shape[0] / data_number
        information_entropy += - proportion * np.log2(proportion)
    return information_entropy

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:找出对应属性特征值的样本,比如找出所有年纪为青年的样本数据集
"""
def handle_data(data, axis, value):
    result_data = list()
    for item in data:
        if item[axis] == value:
            reduced_data = item[: axis].tolist()
            reduced_data.extend(item[axis + 1:])
            result_data.append(reduced_data)
    return result_data

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:计算最大的信息增益,并输出其所对应的特征索引
"""
def calc_information_gain(data):
    feature_number = data.shape[1] - 1                    # 属性特征的数量
    base_entropy = calc_information_entropy(data)                 # 计算总体数据集的信息熵
    max_information_gain, best_feature = 0.0, -1                 # 初始化最大信息增益和对应的特征索引
    for index in range(feature_number):
        feat_list = [item[index] for item in data]
        feat_set = set(feat_list)
        new_entropy = 0.0
        for set_item in feat_set:                         # 计算属性特征划分后的信息增益
            sub_data = handle_data(data, index, set_item)
            proportion = len(sub_data) / float(data.shape[0])           # 计算子集的比例
            new_entropy += proportion * calc_information_entropy(np.array(sub_data))
        temp_information_gain = base_entropy - new_entropy                     # 计算信息增益
        print("第%d个属性特征所对应的的增益为%.3f" % (index + 1, temp_information_gain))            # 输出每个特征的信息增益
        if (temp_information_gain > max_information_gain):
            max_information_gain, best_feature = temp_information_gain, index       # 更新信息增益,确定的最大的信息增益对应的索引
    return best_feature

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建决策树
"""
def establish_decision_tree(data, labels, feat_labels):
    cat_list = [item[-1] for item in data]
    if (cat_list.count(cat_list[0]) == len(cat_list)): return cat_list[0]   # 数据集中的类别只有一种
    best_feature_index = calc_information_gain(data)    # 通过信息增益优先选取最好的属性特征
    best_label = labels[best_feature_index]   # 属性特征对应的标签内容
    # feat_labels表示已选取的属性;新建一个决策树节点;将属性标签列表中删除已选取的属性
    feat_labels.append(best_label); decision_tree = {best_label: dict()}; del(labels[best_feature_index])
    feature_values = [item[best_feature_index] for item in data]
    unique_values = set(feature_values)      # 获取最优属性对应值的set集合
    for value in unique_values:
        sub_label = labels[:]
        decision_tree[best_label][value] = establish_decision_tree(np.array(handle_data(data, best_feature_index, value)), sub_label, feat_labels)
    return decision_tree
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:统计决策树当中的叶子节点数目,以及决策树的深度
"""
def get_leaf_number_and_tree_depth(decision_tree):
    leaf_number, first_key, tree_depth = 0, next(iter(decision_tree)), 0; second_dict = decision_tree[first_key]
    for key in second_dict.keys():
        if type(second_dict.get(key)).__name__ == "dict":
            temp_number, temp_depth = get_leaf_number_and_tree_depth(second_dict[key])
            leaf_number, curr_depth = leaf_number + temp_number, 1 + temp_depth
        else: leaf_number += 1; curr_depth = 1
        if curr_depth > tree_depth: tree_depth = curr_depth
    return leaf_number, tree_depth

from matplotlib.font_manager import FontProperties

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:绘制节点
"""
def plot_node(node_text, center_pt, parent_pt, node_type):
    arrow_args = dict(arrowstyle = "<-")
    font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 设置字体
    create_plot.ax1.annotate(node_text, xy=parent_pt,  xycoords='axes fraction',
                            xytext=center_pt, textcoords='axes fraction',
                            va="center", ha="center", bbox=node_type, arrowprops=arrow_args, FontProperties=font)

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:标注有向边的值
"""
def tag_text(cntr_pt, parent_pt, node_text):
    x_mid = (parent_pt[0] - cntr_pt[0]) / 2.0 + cntr_pt[0]
    y_mid = (parent_pt[1] - cntr_pt[1]) / 2.0 + cntr_pt[1]
    create_plot.ax1.text(x_mid, y_mid, node_text, va="center", ha="center", rotation=30)
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:绘制决策树
"""
def plot_tree(decision_tree, parent_pt, node_text):
    decision_node = dict(boxstyle="sawtooth", fc="0.8")
    leaf_node = dict(boxstyle = "round4", fc = "0.8")
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(decision_tree)
    first_key = next(iter(decision_tree))
    cntr_pt = (plot_tree.xOff + (1.0 + float(leaf_number)) / 2.0 / plot_tree.totalW, plot_tree.yOff)
    tag_text(cntr_pt, parent_pt, node_text); plot_node(first_key, cntr_pt, parent_pt, decision_node)
    second_dict = decision_tree[first_key]
    plot_tree.yOff = plot_tree.yOff - 1.0 / plot_tree.totalD
    for key in second_dict.keys():
        if type(second_dict[key]).__name__ == 'dict': plot_tree(second_dict[key], cntr_pt, str(key))
        else:
            plot_tree.xOff = plot_tree.xOff + 1.0 / plot_tree.totalW
            plot_node(second_dict[key], (plot_tree.xOff, plot_tree.yOff), cntr_pt, leaf_node)
            tag_text((plot_tree.xOff, plot_tree.yOff), cntr_pt, str(key))
    plot_tree.yOff = plot_tree.yOff + 1.0 / plot_tree.totalD

from matplotlib import pyplot as plt
"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:创建决策树
"""
def create_plot(in_tree):
    fig = plt.figure(1, facecolor = "white")
    fig.clf()
    axprops = dict(xticks = [], yticks = [])
    create_plot.ax1 = plt.subplot(111, frameon = False, **axprops)
    leaf_number, tree_depth = get_leaf_number_and_tree_depth(in_tree)
    plot_tree.totalW, plot_tree.totalD = float(leaf_number), float(tree_depth)
    plot_tree.xOff = -0.5 / plot_tree.totalW; plot_tree.yOff = 1.0
    plot_tree(in_tree, (0.5,1.0), '')
    plt.show()
    
if __name__ == "__main__":
    data, labels = establish_data()
    decision_tree = establish_decision_tree(data, labels, list())
    print(decision_tree)
    print("决策树的叶子节点数和深度:", get_leaf_number_and_tree_depth(decision_tree))
    create_plot(decision_tree)

手动可视化决策树的结果如下所示:

实话实说,通过Matplotlib手动对决策树进行可视化,对于之前没什么经验的码手来讲确实有点不友好。上述代码能看懂就行,没必要死揪着不放,后面的话会介绍通过Graphviz来绘制决策树。这里对上方的代码做个简短的说明:

  • get_leaf_number_and_tree_depth主要用于统计决策树当中的叶子节点数目,以及决策树的深度。选取key对应的value,判断value是否为一个字典类型,否的话说明是一个叶子节点,是的话说明非叶子节点,不同情况做不同处理
  • plot_node方法用于绘制节点,这里设置了font类型是在windows下的,如果是linux则需要额外设置
  • tag_text用于标注有向边的属性值,在这里主要是用1和0来进行标注,1代表对属性的肯定,0表示否定
  • plot_tree遍历绘制决策树,在这里需要调用前面所定义的几个方法

二、基于已经构建好的决策树进行分类预测

依靠训练数据构造了决策树之后,我们既可以通过该决策树模型应用于实际数据来进行分类。在对数据进行分类时,需要使用决策树以及用于构造决策树的标签向量;然后,程序比较测试数据与决策树上的数值,递归执行该过程直到进入到叶子节点;最后将测试数据定义为叶子节点所属的类型。——《机器学习实战》

在已经获取到决策树模型的前提下对测试数据进行分类还是挺好理解的

对此,我们定义一个classify方法来进行分类:

"""
    Author: Taoye
    微信公众号: 玩世不恭的Coder
    Explain:通过决策树模型对测试数据进行分类
""" 
def classify(decision_tree, best_feature_labels, test_data):
    first_node = next(iter(decision_tree))
    second_dict = decision_tree[first_node]
    feat_index = best_feature_labels.index(first_node)
    for key in second_dict.keys():
        if int(test_data[feat_index]) == int(key):
            if type(second_dict[key]).__name__ == "dict":   # 为字典说明还没到叶子节点
                result_label = classify(second_dict[key], best_feature_labels, test_data)
            else: result_label = second_dict[key]
    return result_label

我们分别对四个数据样本进行测试,样本分别是(有房子,没工作),(有房子,有工作),(没房子,没工作),(没房子,有工作),使用列表表示分别是[1, 0], [1, 1], [0, 0], [0, 1],运行结果如下:

可见,四组数据都能够分类成功。

三、构建好的决策树模型应当如何保存和读取?

构建好决策树之后,我们要想保存该模型就可以通过pickle模块来进行。

保存好模型之后,下次使用该模型就不需要的再次训练了,只需要加载模型即可。保存和加载模型的示例代码如下(挺简单的,就不多说了):

import pickle
with open("DecisionTreeModel.txt", "wb") as f:
    pickle.dump(decision_tree, f)           # 保存决策树模型

f = open("DecisionTreeModel.txt", "rb")
decision_tree = pickle.load(f)             # 加载决策树模型

四、通过鸢尾花(Iris)数据集,使用Sklearn构建决策树

现在我们通过sklearn来实现一个小案例,数据集采用的是机器学习中比较常用的鸢尾花(Iris)数据集。更多其他关于决策树分类的案例,大家可以去sklearn.tree.DecisionTreeClassifier的文档中学习://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html

在sklearn中实现决策树分类主要用到的接口是sklearn.tree.DecisionTreeClassifier,这个主要是通过数据样本集构建一个决策树模型。此外,如果我们要想将决策树可视化,还需要使用到export_graphviz。当然了,在sklearn.tree`下还有其他接口可供大家调用,这里不做的过多介绍了,读者可自行学习。

关于sklearn.tree.DecisionTreeClassifier的使用,可参考://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html。其中内置了很多的参数,这里主要记录8个参数,也方便后期回顾,其他的有机会用到再查找资料:

  • criterion:属性选取的标准,默认采用的是gini,也可以自行选择entropygini是基尼值,entropy是信息熵,这两个我们在上篇文章中已经讲到过了
  • splitter:特征划分节点的选择标准,默认是best,可以设置为random。默认的”best”适合样本量不大的时候,而如果样本数据量非常大,此时决策树构建推荐”random”。
  • max_depth:决策树最大深度,默认是None。需要注意一点的是,该深度是不包含根节点的。一般来说,数据少或者特征少的时候可以不管这个值。如果模型样本量多,特征也多的情况下,推荐限制这个最大深度,具体的取值取决于数据的分布。
  • max_features:划分时考虑的最大特征数,默认是None。一般来说,如果样本特征数不多,比如小于50,我们用默认的”None”就可以了,如果特征数非常多,我们可以灵活使用其他取值来控制划分时考虑的最大特征数,以控制决策树的生成时间。用到的时候查看下文档即可。
  • min_samples_split:内部节点再划分所需最小样本数,默认为2。意思就是说,比如我们某个属性对应样本数目小于min_samples_split,即使它满足优先选取条件,依然会被剔除掉。
  • min_samples_leaf:叶子节点最少样本数,默认是1。这个值限制了叶子节点最少的样本数,如果某叶子节点数目小于样本数,则会和兄弟节点一起被剪枝。叶结点需要最少的样本数,也就是最后到叶结点,需要多少个样本才能算一个叶结点。如果设置为1,哪怕这个类别只有1个样本,决策树也会构建出来。
  • max_leaf_nodes:最大叶子节点数,默认是None。通过限制最大叶子节点数,可以防止过拟合。如果加了限制,算法会建立在最大叶子节点数内最优的决策树。如果特征不多,可以不考虑这个值,但是如果特征分成多的话,可以加以限制,具体的值可以通过交叉验证得到。
  • random_state:随机数种子,默认是None。如果没有设置随机数,随机出来的数与当前系统时间有关,每个时刻都是不同的。如果设置了随机数种子,那么相同随机数种子,不同时刻产生的随机数也是相同的。

此外,在DecisionTreeClassifier下也有很多方法可供调用,详情可参考文档进行使用,如下:

接下来,我们就用sklearn来对鸢尾花数据集进行分类吧。参考资料://scikit-learn.org/stable/auto_examples/tree/plot_iris_dtc.html#sphx-glr-auto-examples-tree-plot-iris-dtc-py

构建决策树本身的代码并不难,主要在于可视化,其中涉及到了Matplotlib的不少操作,以增强可视化效果,完整代码如下:

import numpy as np
import matplotlib.pyplot as plt

from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree

class IrisDecisionTree:
    """
        Explain:属性的初始化
        Parameters:
            n_classes: 鸢尾花的类别数
            plot_colors: 不同类别花的颜色
            plot_step: meshgrid网格的步长
    """
    def __init__(self, n_classes, plot_colors, plot_step):
        self.n_classes = n_classes
        self.plot_colors = plot_colors
        self.plot_step = plot_step
    
    """
        Explain: 通过load_iris构建数据集
    """
    def establish_data(self):
        iris_info = load_iris()
        return iris_info.data, iris_info.target, iris_info.feature_names, iris_info.target_names
    
    """
        Explain:分类的可视化
    """
    def show_result(self, x_data, y_label, feature_names, target_names):
        # 选取两个属性来构建决策树,以方便可视化,其中列表内部元素代表属性对应的索引
        for index, pair in enumerate([[0, 1], [0, 2], [0, 3], [1, 2], [1, 3], [2, 3]]):
            sub_x_data, sub_y_label = x_data[:, pair], y_label
            clf = DecisionTreeClassifier().fit(sub_x_data, sub_y_label)   # 选取两个属性构建决策树
            plt.subplot(2, 3, index + 1)
            x_min, x_max = sub_x_data[:, 0].min() - 1, sub_x_data[:, 0].max() + 1   # 第一个属性
            y_min, y_max = sub_x_data[:, 1].min() - 1, sub_x_data[:, 1].max() + 1   # 第二个属性
            xx, yy = np.meshgrid(np.arange(x_min, x_max, self.plot_step), np.arange(y_min, y_max, self.plot_step))
            plt.tight_layout(h_pad=0.5, w_pad=0.5, pad=2.5)
            Z = clf.predict(np.c_[xx.ravel(), yy.ravel()]).reshape(xx.shape)    # 预测meshgrid内部每个元素的分类
            cs = plt.contourf(xx, yy, Z, cmap = plt.cm.RdYlBu)   # 绘制带有颜色的网格图
            plt.xlabel(feature_names[pair[0]]); plt.ylabel(feature_names[pair[1]])     # 标注坐标轴标签
            for i, color in zip(range(self.n_classes), self.plot_colors):
                idx = np.where(sub_y_label == i)
                plt.scatter(sub_x_data[idx, 0], sub_x_data[idx, 1], c=color, label=target_names[i],
                            cmap=plt.cm.RdYlBu, edgecolor='black', s=15)    # 绘制数据样本集的散点图
        
        from matplotlib.font_manager import FontProperties
        font = FontProperties(fname=r"c:\windows\fonts\simsun.ttc", size=14)    # 定义中文字体
        plt.suptitle("通过决策树对鸢尾花数据进行可视化", fontproperties=font)
        plt.legend(loc='lower right', borderpad=0, handletextpad=0)
        plt.axis("tight")
        plt.figure()
        clf = DecisionTreeClassifier().fit(x_data, y_label)     # 针对鸢尾花数据集的多重属性来构建决策树
        plot_tree(clf, filled=True)
        plt.show()
    
if __name__ == "__main__":
    iris_decision_tree = IrisDecisionTree(3, "ryb", 0.02)
    x_data, y_label, feature_names, target_names = iris_decision_tree.establish_data()
    iris_decision_tree.show_result(x_data, y_label, feature_names, target_names)

运行结果如下所示:

通过可视化结果,我们可以发现主要有两个结果,我们分别对其进行说明下:于鸢尾花数据集来讲,总共有四种属性特征以及三种标签结果。为了方便可视化,第一张图只选取了两个属性来构建决策树,四种属性,选两个,很简单,学过排列组合的都应该知道有\(C_4^2=6\)种可能,所第一张图中的每张子图分别对应一种可能。且颜色不同代表不同的分类,假如数据集颜色与网格内部颜色一致,则说明分类正确。因此,从直观上来看,选取sepal length和petal length这两种属性构建的决策树相对较好。而第二张图是针对数据集中的所有属性特征来构建决策树。具体的结果可自行运行上方代码查看(由于设置了font字体,所以上方代码需在windows下运行)

我们可以发现,上面代码可视化决策树的时候采用的是sklearn.tree.plot_tree,前面我们在讲解通过Matplotlib绘制决策树的时候也有说到,使用graphviz亦可可视化决策树,下面我们不妨来看看吧!

graphviz不能采用pip进行安装,采用anaconda安装的时候也会很慢,甚至多次尝试都可能安装失败,前几天帮同学安装就出现这种情况(windows下是这样的,linux环境下会很方便),所以这里我们采用直接通过whl文件来安装。

建议:对于使用Python有过一段时间的Pyer来讲,都会经常安装一些第三方模块,有些可以直接通过pip或者conda完美的解决,而有些在安装的过程中会遇到各种不明所以的错误。所以,对于在安装过程中遇到错误的读者不妨尝试通过whl文件进行安装,whl目标地址://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud,其中整合了各种Python模块的whl文件。

打开上述url地址 –> ctrl + f搜索graphviz –> 下载需要的graphviz安装文件

在本地目标路径中执行安装即可:

pip install graphviz‑0.15‑py3‑none‑any.whl

此外对于windows来将 ,还需要前往官网安装graphviz的exe文件,然后将bin目录添加到环境变量即可。exe文件的下载地址://graphviz.org/download/

如果是Linux用户,那就比较方便了,直接通过命令安装即可:

$ sudo apt install graphviz         # Ubuntu
$ sudo apt install graphviz         # Debian

至此,Graphviz就已经安装好了。我们通过它来实现决策树的可视化吧,在IrisDecisionTree下添加如下show_result_by_graphviz方法:

"""
    Explain:通过graphviz实现决策树的可视化
"""
def show_result_by_graphviz(self, x_data, y_label):
    clf = DecisionTreeClassifier().fit(x_data, y_label)
    iris_dot_data = tree.export_graphviz(clf, out_file=None, 
                                         feature_names=iris.feature_names,  
                                         class_names=iris.target_names,  
                                         filled=True, rounded=True,  
                                         special_characters=True) 
    import graphviz
    graph = graphviz.Source(iris_dot_data); graph.render("iris")

运行之后会在当前目录下生成一个pdf文件,其中就是可视化之后的决策树。注意:以上只是实现简单的鸢尾花的决策树分类,读者可通过调解DecisionTreeClassifier的参数构建不同的决策树,以此来判别各个决策树的优劣。

以上就是本篇全部内容了,决策树的相关内容暂时就更新到这了,其他内容像过拟合、剪枝等等以后有时间再更新,下期的机器学习系列文章就是肝SVM的非线性模型了。

就不唠嗑了~~~

我是Taoye,爱专研,爱分享,热衷于各种技术,学习之余喜欢下象棋、听音乐、聊动漫,希望借此一亩三分地记录自己的成长过程以及生活点滴,也希望能结实更多志同道合的圈内朋友,更多内容欢迎来访微信公主号:玩世不恭的Coder

参考资料:

[1] 《机器学习实战》:Peter Harrington 人民邮电出版社
[2] 《统计学习方法》:李航 第二版 清华大学出版社
[3] 《机器学习》:周志华 清华大学出版社
[4] Python Extension Packages://www.lfd.uci.edu/~gohlke/pythonlibs/#wordcloud
[5] sklearn.tree.DecisionTreeClassifier://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
[6] Graphviz官网://graphviz.org/

推荐阅读

《Machine Learning in Action》—— Taoye给你讲讲决策树到底是支什么“鬼”
《Machine Learning in Action》—— 剖析支持向量机,优化SMO
《Machine Learning in Action》—— 剖析支持向量机,单手狂撕线性SVM
print( “Hello,NumPy!” )
干啥啥不行,吃饭第一名
Taoye渗透到一家黑平台总部,背后的真相细思极恐
《大话数据库》-SQL语句执行时,底层究竟做了什么小动作?
那些年,我们玩过的Git,真香
基于Ubuntu+Python+Tensorflow+Jupyter notebook搭建深度学习环境
网络爬虫之页面花式解析
手握手带你了解Docker容器技术