决策树-CART(上)

CART(Classification And Regression Trees,分类回归树)算法是一种树构建算法,既可以用于分类任务,又可以用于回归。相比于 ID3 和 C4.5 只能用于离散型数据且只能用于分类任务,CART 算法的适用面要广得多,既可用于离散型数据,又可以处理连续型数据,并且分类和回归任务都能处理。

CART 算法生成的决策树模型是二叉树,而 ID3 以及 C4.5 算法生成的决策树是多叉树,从运行效率角度考虑,二叉树模型会比多叉树运算效率高。

特征选择

在分类任务中 CART 算法使用基尼系数作为特征选择的依据,在回归任务中则以均方误差作为特征选择的依据。先来讲讲作为分类任务特征选择依据的基尼系数。

基尼系数

基尼系数代表模型的不纯度(混乱度),基尼系数越小,则不纯度越低,这和 C4.5 的信息增益比恰好相反。
G i n i ( p ) = ∑ i = 1 k p k ( 1 − p k ) = 1 − ∑ i = 1 k p k 2 Gini(p) = \sum_{i=1}^k p_k(1-p_k) = 1 - \sum_{i=1}^k p_k^2 Gini(p)=i=1kpk(1pk)=1i=1kpk2
其中,k 表示类别数, p k p_k pk 表示第 k 个类别的概率。
p k = ∣ C k ∣ ∣ D ∣ p_k = \frac{|C_k|}{|D|} pk=DCk
上式中, ∣ C k ∣ |C_k| Ck 表示第 k 个类别的数量,|D| 表示数据集的数量。

假设使用特征 A 将数据集 D 划分为两部分 D1 和 D2,此时按照特征 A 划分的数据集的基尼系数为:
G i n i ( D , A ) = ∣ D 1 ∣ ∣ D ∣ G i n i ( D 1 ) + ∣ D 2 ∣ ∣ D ∣ G i n i ( D 2 ) Gini(D, A) = \frac{|D_1|}{|D|}Gini(D_1) + \frac{|D_2|}{|D|}Gini(D_2) Gini(D,A)=DD1Gini(D1)+DD2Gini(D2)
说明:因为 CART 算法生成的树是二叉树,因此特征选择过程中特征的取值只有两种可能,这样就能确保当前节点仅有两个子节点。

决策树-CART(上)_第1张图片

使用基尼系数来代替信息熵存在一定的误差,从上图可以看出,基尼系数和熵之半的曲线非常接近,仅仅在 45 度角附近误差稍大。因此,基尼系数可以做为熵模型的一个近似替代。

【与信息增益的比较】:无论是信息增益还是信息增益比,其中都涉及大量的对数运算,计算开销自然要比普通的乘除操作要大。使用基尼系数可以减少计算量,起到简化模型的作用,并且也不会完全丢失熵模型的优点。

均方误差

CART 回归树的度量目标是,对于任意划分特征 A,对应的任意划分点 s,可切分成数据集 D1 和 D2,求出使 D1 和 D2 各自集合的均方差最小,同时 D1 和 D2 的均方差之和最小所对应的特征和特征值划分点。

均方误差计算公式.png

其中,c1 为 D1 数据集的样本输出均值,c2 为 D2 数据集的样本输出均值。

需要注意的是,这里的均方误差和平时所见到的均方误差有所不同。一般来说,均方误差的计算形式是:
∑ i = 1 n ( y i − y i ^ ) 2 \sum_{i=1}^n(y_i - \hat{y_i})^2 i=1n(yiyi^)2
其中 y i ^ \hat{y_i} yi^ 是第 i 个样本的预测值。但 CART 算法中却用数据集的样本输出均值来代替。为什么会这样?

这是因为在决策树模型还未建立前是无法求出具体的预测值 y ^ \hat{y} y^,所以只好用每一个类别的均值作为这个类别的预测值。

构建分类树

CART 算法构建分类树的步骤与 C4.5 与 ID3 相似,不同点在于特征选择以及生成的树是二叉树。在此,主要谈谈特征选择如何进行。

【示例】:假设某个特征 A 被选取建立决策树节点,特征 A 有 A1,A2,A3 三种取值情况。

  • 首先 CART 分类树会考虑把 A 分成 {A1} 和 {A2, A3},{A2} 和 {A1, A3},{A3} 和 {A1, A2} 三种情况。
  • 然后,找到基尼系数最小的组合,假设该组合是 {A2} 和 {A1, A3}。
  • 接着,建立二叉树节点,一个节点是 A2 对应的样本,另一个节点是 {A1, A3} 对应的节点。

需要注意的是,由于这次没有把特征 A 的取值完全分开,之后还有机会在子节点继续选择到特征 A 来划分 A1 和 A3。

构建回归树

在树的构建过程中需要解决多种类型数据的存储问题,这里通过字典(dict)来存储树的数据结构。该字典包含以下四个元素:

  • 待切分的特征
  • 待切分的特征值
  • 右子树:当不再需要切分的时候,也可以是单个值
  • 左子树:同右子树

观察存储树的字典可以发现,它只有两个子节点,而 ID3、C4.5 算法中的树一般都有两个或两个以上的子节点。

除了通过字典的方式来表示树结构外,我们还可以用面向对象编程模式中的类来建立树的数据结构。

class treeNode():
    
    def __init__(self, feat, val, right, left):
        feature_to_split_on = feat
        value_of_split = val
        right_branch = right
        left_branch = left

【建议】:Python 具有足够的灵活性,可以直接使用字典来存储树结构而无须另外自定义一个类,从而有效地减少代码量。

有了树的数据结构之后,我们要考虑如何创建树?将创建树的函数命名为 create_tree(),下面给出该函数的伪代码:

找到最佳的待切分特征:
    如果该节点不能再分,将该节点存为叶节点
    执行二元切分
    在右子树调用 create_tree() 方法
    在左子树调用 create_tree() 方法
def bin_split_dataset(dataset, feature, value):
    mat_0 = dataset[np.nonzero(dataset[:, feature] > value)[0], :]
    mat_1 = dataset[np.nonzero(dataset[:, feature] <= value)[0], :]
    return mat_0, mat_1

def create_tree(dataset, leaf_type=reg_leaf, err_type=reg_err, ops=(1, 4)):
    feat, val = choose_best_split(dataset, leaf_type, err_type, ops)
    if feat == None:
        return val
    ret_tree = {}
    ret_tree['spInd'] = feat
    ret_tree['spVal'] = val
    lset, rset = bin_split_dataset(dataset, feat, val)
    ret_tree['left'] = create_tree(lset, leaf_type, err_type, ops)
    ret_tree['right'] = create_tree(rset, leaf_type, err_type, ops)
    return ret_tree

bin_split_dataset()

bin_split_dataset() 函数用以在给定特征和特征值的情况下,对数据集进行切分,从而得到并返回两个子集。该函数接受三个参数,分别是数据集、待切分的特征和该特征的值。

mat_0 = dataset[np.nonzero(dataset[:, feature] > value)[0], :]
mat_1 = dataset[np.nonzero(dataset[:, feature] <= value)[0], :]
return mat_0, mat_1

【注意】:《机器学习实战》关于这部分的代码存在错误,我们需要将 mat_0 以及 mat_1 这两条语句最后的 [0] 给去除。因为我们需要的是两个集合,而不是具体的一条数据。

create_tree()

create_tree() 函数用以创建树,该函数接受四个参数,数据集和其他 3 个可选参数。这些可选参数决定了树的类型:

  • leat_tree:给出建立叶节点的函数;

  • err_type:代表误差计算函数;

  • ops:包含构建树所需其他参数的元组。

  • create_tree() 函数是一个递归函数,首先尝试将数据集分成两个部分,切分由函数 choose_best_split() (后续会介绍,顾名思义是获取最好的划分)函数完成,该函数返回特征以及特征值。

feat, val = choose_best_split(dataset, leaf_type, err_type, ops)
  • 如果满足停止条件——无需继续划分,则返回特征值。
if feat == None:
    return val
  • 如果不满足停止条件(也就是说需要继续划分),则创建一个新的字典,并将数据集分成两份,在这两份子集上分别继续递归调用 create_tree() 函数。
ret_tree = {}
ret_tree['spInd'] = feat
ret_tree['spVal'] = val
lset, rset = bin_split_dataset(dataset, feat, val)
ret_tree['left'] = create_tree(lset, leaf_type, err_type, ops)
ret_tree['right'] = create_tree(rset, leaf_type, err_type, ops)
return ret_tree

reg_leaf()

reg_leaf() 函数负责生成叶节点。当 choose_best_split() 函数确定不再对数据进行切分时,将调用 reg_leaf() 函数来得到叶节点的模型。在回归树中,该模型其实就是目标变量的均值。

def reg_leaf(dataset):
    return np.mean(dataset[:, -1])

reg_err()

reg_err() 用以计算给定数据上目标变量的平方误差。我们可以直接调用 numpy 的 var() 函数来计算数据集的均方差,具体使用请参考官方文档 var()。在计算完数据集的均方差之后,我们再用均方差乘以数据集中样本的个数来获得总方差。

def reg_err(dataset):
    return np.var(dataset[:, -1]) * np.shape(dataset)[0]

choose_best_split()

choose_best_split() 函数是回归树构建的核心函数,目的是找到数据的最佳二元切分方式。该函数接受四个参数:

  • dataset:数据集
  • leaf_type:生成叶节点的函数的引用
  • err_type:误差估计函数的引用
  • ops:用户指定的参数,用于控制函数的停止时机。
    • tol_s:容许的误差下降值;
    • tol_n:切分的最小样本数。
def choose_best_split(dataset, leaf_type=reg_leaf, err_type=reg_err, ops=(1, 4)):
    tol_s, tol_n = ops[0], ops[1]
    # 如果所有标签的值都相等,则无须切分可直接退出
    if dataset[:, -1].shape[0] == 1:
        return None
    m, n = np.shape(dataset)
    s = err_type(dataset)
    best_s, best_index, best_value = np.inf, 0, 0
    for feat_index in range(n - 1):
        for split_val in set(dataset[:, feat_index]):
            mat_0, mat_1 = bin_split_dataset(dataset, feat_index, split_val)
            if (np.shape[mat_0][0] < tol_n) or (np.shape(mat_1)[0] < tol_n):
                continue
            new_s = err_type(mat_0) + err_type(mat_1)
            if new_s < best_s:
                best_index = feat_index
                best_value = split_val
                best_s = new_s
    # 如果误差减少不大则退出
    if (s - best_s) < tol_s:
        return None, leaf_type(dataset)
    mat_0, mat_1 = bin_split_dataset(dataset, best_index, best_value)
    # 如果切分出的数据集很小则退出
    if (np.shape(mat_0)[0] < tol_n) or (np.shape(mat_1)[0] < tol_n):
        return None, leaf_type(dataset)
    return best_index, best_value
  • 首先判断当前数据集的标签种类,若都相等,则没有必要继续划分。怎么实现呢?对数据集建立一个集合,然后统计标签的数目,若为 1,则不需要再切分,可以直接返回。
if dataset[:, -1].shape[0] == 1:
    return None
  • 然后计算当前数据集的样本个数以及特征数,并计算数据集的误差。该误差将用于与新切分的误差进行对比,来检查新切分能否降低误差。
m, n = np.shape(dataset)
s = err_type(dataset)
  • 初始化所需的变量,最小的误差、最佳的切分值以及最佳切分值对应的下标。
best_s, best_index, best_value = np.inf, 0, 0
  • 接着在所有可能的特征及其可能取值上进行遍历,找到最佳的切分方式。需要注意的是,在循环遍历的过程中,若当前切分使得切分后的子集(mat_0 或 mat_1)的样本个数小于用户设定的最少样本数,则取消当前切分。
# 所有可能的特征
for feat_index in range(n - 1):
    # 所有可能的特征取值
    for split_val in set(dataset[:, feat_index]):
    mat_0, mat_1 = bin_split_dataset(dataset, feat_index, split_val)
    # 若切分后左子树或右子树小于用户设定的最少样本数,则取消当前切分
    if (np.shape[mat_0][0] < tol_n) or (np.shape(mat_1)[0] < tol_n):
        continue
    new_s = err_type(mat_0) + err_type(mat_1)
    # 若当前切分后的误差减小,则更新最小误差、最佳切分值以及最佳切分值对应的下标
    if new_s < best_s:
        best_index = feat_index
        best_value = split_val
        best_s = new_s
  • 在循环结束后,如果切分数据集后效果提升不够大,那么就不必进行切分操作而直接创建叶节点。
if (s - best_s) < tol_s:
    return None, leaf_type(dataset)
  • 另外,再检查两个切分后的子集的样本个数,若某个子集的样本个数小于用户定义的最少样本数,那么也不应切分。
mat_0, mat_1 = bin_split_dataset(dataset, best_index, best_value)
if (np.shape(mat_0)[0] < tol_n) or (np.shape(mat_1)[0] < tol_n):
    return None, leaf_type(dataset)
  • 如果这些提前终止条件都不满足,就返回切分特征和特征值。
return best_index, best_value

运行代码

下面在一些数据上查看代码的实际效果。

>>> my_dat = load_dataset('data/ex00.txt')
>>> my_dat = np.mat(my_dat)
>>> fig = plt.figure()
>>> ax = fig.add_subplot(111)
>>> ax.scatter(my_dat[:, 0].tolist(), my_dat[:, 1].tolist())
>>> ax.set_title('ex00.txt dataset')
>>> ax.set_xlabel('X')
>>> ax.set_ylabel('Y')
>>> plt.show()

决策树-CART(上)_第2张图片

接着创建回归树。

>>> create_tree(my_dat)
{'spInd': 0,
 'spVal': 0.48813,
 'left': 1.0180967672413792,
 'right': -0.04465028571428572}

我们再看另外一个多次切分的例子。

>>> my_dat1 = load_dataset('data/ex0.txt')
>>> my_dat1 = np.mat(my_dat1)
>>> fig = plt.figure()
>>. ax = fig.add_subplot(111)
>>> ax.scatter(my_dat1[:, 1].tolist(), my_dat1[:, 2].tolist())
>>> ax.set_title('ex0.txt dataset')
>>> ax.set_xlabel('X')
>>> ax.set_ylabel('Y')
>>> plt.show()

决策树-CART(上)_第3张图片

为数据集 my_dat2 创建回归树。

>>> create_tree(my_dat1)
{'spInd': 1,
 'spVal': 0.39435,
 'left': {'spInd': 1,
  'spVal': 0.582002,
  'left': {'spInd': 1,
   'spVal': 0.797583,
   'left': 3.9871632,
   'right': 2.9836209534883724},
  'right': 1.980035071428571},
 'right': {'spInd': 1,
  'spVal': 0.197834,
  'left': 1.0289583666666666,
  'right': -0.023838155555555553}}

可以看到该树包含 5 个叶节点和数据集的情况相符合。到现在为止,已经完成回归树的构建,但是需要某种措施来检查构建过程是否得当。下面将介绍树剪枝(tree pruning)技术,它通过对决策树剪枝来达到更好的预测效果。

树剪枝

一棵树如果节点过多,则表明该模型可能对数据进行了“过拟合”。我们可通过降低决策树的复杂度来避免过拟合,最有效的手段是进行剪枝处理(pruning)。

先前在函数 choose_best_split() 中的提前终止条件,实际上在进行一种所谓的预剪枝(prepruning)操作。另一种形式的剪枝需要使用测试集和训练集,称作后剪枝(postpruning)。接下来,我们将先讨论预剪枝存在的不足之处,然后再讨论后剪枝的处理方式。

后续内容请浏览 模型选择-CART(下)。

参考

  • 《机器学习实战》
  • scikit-learn决策树算法类库使用小结:https://www.cnblogs.com/pinard/p/6056319.html

你可能感兴趣的:(机器学习)