遥感之机器学习树集成模型-CART算法

决策树是一种流行而强大的机器学习算法。它是一种非参数化的有监督学习方法,可用于分类和回归任务。它通过学习样本数据集创建一个模型,获得一些决策规则来预测目标变量的值。对于分类模型来说,目标值在本质上是离散的,而对于回归模型来说,目标值由连续值表示。与人工神经网络等算法不同,决策树相对来说更容易理解和解释,因为它共享内部决策逻辑。尽管许多数据科学家认为这是一个老方法,而且
由于过拟合问题,他们可能对其准确性有一些怀疑,但最近的基于树的模型,特别是随机森林、梯度提升和XGBoost等建立在决策树算法之上的机器学习模型获得了巨大的成功,使古老的决策树模型焕发新春!因此,决策树背后的概念和算法是非常值得了解的。

1 经典决策树应用的一般流程

在具体介绍各种经典决策树之前,有必要先整理其决策树应用的一般流程,方便按照算法流程学习算法细节。

1.1 缺失值的处理

这个问题在数据分析领域经常遇到,不仅是遥感领域,不过遥感比较特殊,其特征一般来源于遥感图像,遥感图像是二维矩阵,如果存在个别异常值或缺少值,一般会提前做空间插值,重采样等处理掉。
但如果存在极少数缺省值,可能开始没有发现,这时候如果模型自动处理掉这些问题,那再好不过了。
一般处理缺失值有以下两种常用方式:

  1. 如果出现缺失值的样本数较少,则可能会省略这些样本。但是,如果样本中有大量的特征属性,每个特征属性即使出现一小部分缺失值,也会影响很多样本。例如,在30个特征属性的情况下,如果只有5%的数据缺失(假设在目标和特征属性间随机和独立地传播),则几乎80%的样本将不得不被忽略,因为0.9530=0.215
  2. 另一种处理缺失值的替代方法是,根据样本中该特征属性的其他值,将缺失值替换为估算值。例如,可以用所有样本中该特征属性的平均值替换该特征属性的缺失值。但是,使用此类技术将导致样本数据集缺乏变化,从而引入偏差。

另外可以对缺失值进行建模等,不过在遥感反演或分类领域,缺失值不是主要问题,基本上很少遇到。

1.2 连续数值属性的离散化处理

离散化连续数值属性有两种基本方法:

  1. 一种是对训练集中的样本的每个特征属性进行量化(即所谓的无监督离散化),
    无监督的离散化是指将连续数值属性按其范围划分为预定数量的间隔(桶)。例如,可以通过应用等宽或等频率分桶,然后用桶的平均值或中位数替换划分到每个桶里的样本属性值,从而对属性值进行离散化。在这两种分桶方法中,总的范围都被划分为用户指定的k个间隔。在等宽度分桶中,特征的连续范围均匀地划分为宽度相等的间隔,而在等频率分桶中,在每个桶中放置相同数量的连续值。
    当间隔数设置为k时,割点的最大数量为k-1。术语割点是指一个实数值,它将连续值的区间划分为两个间隔,一个间隔小于或等于割点,另一个间隔大于割点。在连续变量分布不均匀的情况下,上述分桶方法可能无法产生良好的效果。而且该方法容易受到异常值的影响,因为异常值会对范围产生重大影响。有监督的离散化方法克服了这一缺点,其中分类信息被用来寻找由割点划分的适当的间隔。
  2. 另一种是在离散化时考虑到这些样本分类(即有监督的离散化)。
    有监督的离散化方法通常采用“熵”作为度量来查找潜在的割点,将一系列连续值拆分为两个间隔。这些方法递归地二元划分该范围或其子范围,直到满足停止条件,其中许多方法使用特定的停止条件。MDLP(最小描述长度原则)被确定为离散化的首选,因为它提供了一种更有原则的方法来确定何时停止递归拆分。
    离散化的MDLP方法分为自上而下和自下而上两类。自上而下的方法从一组空的割点开始,并随着离散化的进行,通过拆分间隔来继续向列表中添加新的割点。自下而上的方法从特征的所有连续值作为割点的完整列表开始,并随着离散化的进展通过合并间隔来删除其中的一些值。选择合适的离散化方法通常是一个复杂的问题,在很大程度上取决于用户的需要和其他考虑因素。

2 CART算法

CART(Classification And Regression Trees)即分类和回归树,是第一种比较经典的决策树算法,由Leo Breiman、Jerome Friedman、Richard Olshen和Charles Stone于1984年正式提出,可用于分类或回归预测建模问题。
CART算法总是创建一棵二元树(二叉树),这意味着每个非终端节点有两个子节点。
CART的构建过程与人类的决策方式非常相似,因此,人们很容易理解和接受CART决策过程得出的结果。这种直观的可解释能力是CART以及决策树方法非常重要的一个原因。CART另一个非常吸引人的地方是,它允许多样化的输入数据类型,这与许多线性组合方法(如逻辑回归或支持向量机)不同。可以混合连续数值变量,如价格或面积,也可以混合标称分类或枚举变量,如房屋类型或位置。这种灵活性使得CART成为各种应用中的首选工具。CART使用代价复杂度剪枝(Cost Complexness Pruning,CCP)方法,将不可靠的分支从决策树移除,以提高准确率。从CART算法的名字中可以看出,它支持构建分类(决策)树和回归(决策)树。所谓分类树,是指目标变量是标称分类或枚举值数据类型,用于确定目标变量可能属于的“类”别。所谓回归树,是指目标变量是连续的数值数据类型,用来预测目标变量的值。

2.1 基尼不纯度,基尼增益与基尼指数

训练决策树模型包括迭代地将当前数据分成两个分支。
如何分割以及如何定量评估分割的优劣是待解决的核心问题。
假设有如下的两类数据点,分别用正方形和圆形表示,其分布如图所示。可以看到,图中有5个正方形点和5个圆形点。如果以直线x=2为分界线对其进行分割,如图2.3所示,则可获得一个完美的分割。它将这个数据集完美地分割成两个子区域(分支),左边是5个正方形点,右边是5个圆形点。
在这里插入图片描述

但是,如果以直线x=1.5进行分割呢?这将导致如图2.4所示的不完美分割。左边是4个正方形点,右边是1个正方形点加上5个圆形点。很明显,这种分割是存在问题的,但是怎么量化呢?
在这里插入图片描述

哪种分割方式更好呢?这已经不是一目了然的事情了,我们需要一种方法来量化评估分割的好坏。
CART算法使用基尼不纯度(Gini impurity)来量化评估分割的好坏。

2.1.1 基尼不纯度

概念与定义
基尼不纯度的概念源于信息论,它是一个概率分布的不确定性的度量。对于一个包含N个样本的数据集D,其基尼不纯度Gini(D)的定义如下

G i n i ( D ) = 1 − ∑ i = 1 C P ( C i ) 2 Gini(D) = 1 - \sum_{i=1}^{C} P(C_i)^2 Gini(D)=1i=1CP(Ci)2

其中,C是数据集D中的类别数量,|D_i|是数据集D中属于第i类的样本数量,N是数据集D中的总样本数量。
设计思想
设计思想是通过衡量数据集中各个类别分布的均匀程度来评估数据的纯度。
基尼不纯度的计算考虑了每个类别在数据集中的相对频率,并通过比较类别间的频率来衡量数据集的纯度。基尼不纯度越低,表示数据集的纯度越高,即样本在类别上的分布越均匀。

示例

假设一个数据集D有三个类别,其样本分布如下:

● 类别0: A(5), B(3), C(2)
● 类别1: A(2), B(6), C(1)
● 类别2: A(1), B(4), C(5)
总样本数N = 5 + 3 + 2 + 2 + 6 + 1 + 4 + 5 = 28
计算每个类别的频率P(C_i):
● P(C0) = 10/28
● P(C1) = 9/28
● P(C2) = 10/28
计算基尼不纯度Gini(D)
G i n i ( D ) = 1 − ( ( 10 / 28 ) 2 + ( 9 / 28 ) 2 + ( 10 / 28 ) 2 ) = 1 − ( 100 / 784 + 81 / 784 + 100 / 784 ) = 1 − ( 281 / 784 ) = 403 / 784 Gini(D) = 1 - ((10/28)^2 + (9/28)^2 + (10/28)^2) = 1 - (100/784 + 81/784 + 100/784) = 1 - (281/784) = 403/784 Gini(D)=1((10/28)2+(9/28)2+(10/28)2)=1(100/784+81/784+100/784)=1(281/784)=403/784

基尼不纯度Gini(D)为403/784。这个值越低,表示数据集的纯度越高。
在决策树算法中,通常选择基尼不纯度作为节点分裂的依据,以创建更纯的子集,从而提高模型的预测准确性。

2.1.2 基尼增益与基尼指数

基尼增益(Gini Gain)是决策树算法中一个重要的概念,用于衡量通过某个特征进行分割后,数据集的纯度提升程度。在决策树中,节点分裂的目的是减少数据集的不纯度,而基尼增益就是衡量这种减少程度的指标。

基尼增益是指在数据集D中,通过选择一个特征A并对该特征的某个值A_i进行分割后,得到两个子集D_1和D_2时,数据集D的基尼不纯度减少的量。基尼增益的计算公式如下:
G a i n ( D , A ) = G i n i ( D ) − ∑ i = 1 k ∣ D i ∣ N × G i n i ( D i ) Gain(D, A) = Gini(D) - \sum_{i=1}^{k} \frac{|D_i|}{N} \times Gini(D_i) Gain(D,A)=Gini(D)i=1kNDi×Gini(Di)
其中:
● D是原始数据集。
● A是用于分割的特征。
● N是数据集D的总样本数量。
● |D_i|是分割后第i个子集的样本数量。
● Gini(D)是数据集D的基尼不纯度。
● Gini(D_i)是分割后第i个子集的基尼不纯度。
● k是分割后子集的数量。
基尼增益的计算公式反映了在特征A上进行分割后,数据集D的基尼不纯度减少的量。

公式的第一部分是原始数据集D的基尼不纯度,第二部分是分割后每个子集的基尼不纯度乘以对应子集的样本比例。
通过比较不同特征的基尼增益,决策树算法可以选择最佳的分割特征进行节点分裂。

根据上述公式Gini(D)在根据不同的特征划分数据集确定划分效果时其计算都是一样的,所以将公式变换一下,引入基尼指数:
G I ( D , A ) = ∑ i = 1 k ∣ D i ∣ N × G i n i ( D i ) GI(D, A) = \sum_{i=1}^{k} \frac{|D_i|}{N} \times Gini(D_i) GI(D,A)=i=1kNDi×Gini(Di)
因此,更高的基尼增益等价于更好的分割,更低的基尼指数等价于更好的分割。

设计思想是通过计算在特定特征上进行分割后,数据集的基尼不纯度减少的量,来确定这个特征是否是一个好的分割特征。一个高的基尼增益表示通过这个特征进行分割可以显著提高数据集的纯度,因此这个特征是一个好的分割特征。

示例
假设一个数据集D有三个类别,其基尼不纯度为0.5,总样本数量为100。通过特征A进行分割,得到两个子集D_1和D_2,它们的基尼不纯度分别为0.1和0.4。计算特征A的属性值ai划分数据集的的基尼增益:
G i n i G a i n ( A , a i ) = 100 100 × 0.5 − ( 50 100 × 0.1 + 50 100 × 0.4 ) = 0.5 − ( 0.05 + 0.2 ) = 0.25 Gini_Gain(A, a_{i}) = \frac{100}{100} \times 0.5 - \left(\frac{50}{100} \times 0.1 + \frac{50}{100} \times 0.4\right) = 0.5 - (0.05 + 0.2) =0.25 GiniGain(A,ai)=100100×0.5(10050×0.1+10050×0.4)=0.5(0.05+0.2)=0.25
特征A的基尼增益为0.25。这个值越高,表示特征A是一个更好的分割特征。
在决策树算法中,特征的基尼增益用于选择最佳的分割特征进行节点分裂。

2.2 CART分类决策树原理

CART算法可以生成分类决策树和回归决策树。
首先介绍CART分类决策树的算法流程,注意在遥感领域决策树算法经常用于其他机器学习的对比算法,在ENVI软件中可以通过专家经验设置划分的特征
以及方式,并不是通过数据自动计算划分的特征。

2.2.1 CART分类决策树的算法流程

1. 输入
输入为训练数据集D和停止计算的条件。其中,训练数据集D包含多条记录,每个记录由多个属性构成。每个属性的数据类型均为离散的,例如二值类型、标称值或枚举值类型等。停止计算的条件可以是节点中的样本个数小于预定阈值,或样本集的基尼指数小于预定阈值,或没有更多特征属性等。
2. 输出
输出即为决策规则,这里决策规则在ENVI软件里面可以自定义设置
3. 分类决策树的生成流程
CART算法从根节点开始,用训练集递归地建立CART分类决策树。
1)设训练数据集为D,计算数据集现有的所有属性特征对该训练集的基尼增益。
对每一个属性特征F,对其所有可能取值的每个值f,根据D中的样本实例对F=f的测试为“是”或“否”,将数据集D分割成D1和D2两个子集,利用基尼增益公式计算F=f时的基尼增益。
2)在所有可能的属性特征F以及它们所有可能的切分点fi中,选择基尼增益最大的特征及其对应的切分点作为最优切分点,
依据该最优切分点切割,生成两个子节点,左子节点为D1,右子节点为D2。
3)对两个子节点递归调用步骤1~2,直至满足停止条件。
4)生成一棵完整的二叉CART分类决策树。
4. 决策树的优化
使用决策树模型拟合数据时容易产生过拟合,解决办法是对决策树进行剪枝处理。决策树剪枝有两种思路:预剪枝(pre-pruning)和后剪枝(post-pruning)。
5. CART分类决策树模型的使用
对生成的CART分类决策树做预测的时候,如果测试集里的某个样本A落到了某个叶子节点,且该叶子节点里存在多个类别的训练样本,则概率最大的训练样本是样本A的类别。

3 核心源码示例

简单决策树的基本逻辑是,递归创建决策树,然后预测时递归预测,所以先需要掌握递归算法的思想

def train(X, y, feature_names,min_samples_split=1):
    """  
        X: 特征变量 
        y: 决策目标变量 
        feature_names: 属性名 
        min_samples_split:节点中包含的最小的样本数,少于此数值就不继续划分子集了
        return tree: 生成的决策树 
        """
    
    min_samples_split = 1
    # 处理字符串映射
    X_copy, y_copy = self.__deal_value_map(X,y)
    # 创建CART决策树
    tree = create_tree(X_copy, y_copy,feature_names,min_samples_split)
    return tree

上述train函数,里面核心代码是根据输入的数据集,创建一颗决策树,对于数据集的要求是,y要是离散的,比如遥感地物分类时,分为多少类,0表示背景,1表示建筑物等
而x即是特征变量(自变量,波段特征等在遥感中有多种称呼),这里特征变量也要求是离散的,即都是整数,这在遥感领域暂时没想到有什么离散的特征变量,好像都是浮点数连续性的,除非对特征强行取整操作。后续会介绍如何处理连续性特征变量,这里只是简化版用于说明决策树的搭建流程。

其中关键的是如何创建决策树的函数create_tree

def choose_best_point_to_split(X, y:
    """选择最优切分点    
        return best_feature_index: 最优切分点所在属性的序号索引
        return best_split_point: 最优切分点 ,即是对应最优特征变量下的,用于划分子数据集的最优属性值所在的序号索引
        
    """
    best_split_point = 0
    best_feature_index = -1
    best_gini_gain = -1.0
    num_feature = X.shape[1]  # 属性的个数
    gini_impurity =cal_gini_impurity(y) #计算原始整个数据集的基尼不纯度

    # 下面代码主要是两层循环逻辑,即先遍历每一个特征,在每一个特征的所有无重复的离散属性值下进行再次遍历,
    # 获取每一个特征变量根据每一个特征属性划分数据集之下的基尼增益的大小(这里可以换为其他评估指标,不过其决策树划分数据集的大概流程逻辑是一致的,只是不同指标的计算细节不一样)
    for i in range(num_feature): # 遍历每个属性
        feature_value_list = X[:, i] # 得到某个属性下的所有值,即某列
        split_points = list(set(feature_value_list)) # 这里在前面描述过,都是离散性数据,即转为集合后,会去重处理,再转为list后,得到的是无重复的属性特征值
        # 计算各个候选切分点的基尼不纯度
        for split_point in split_points:
            # 计算左子树的基尼不纯度
            sub_y_left = y[X[:, i]==split_point]
            gini_impurity_left = cal_gini_impurity(sub_y_left)  # 根据上述提到的公式计算相应子集的基尼不纯度
            # 计算右子树的基尼不纯度
            sub_y_right = y[X[:, i]!=split_point] # 根据上述提到的公式计算相应子集的基尼不纯度
            gini_impurity_right = cal_gini_impurity(sub_y_right)
            # 计算该切分点的基尼增益
            pro_left = len(sub_y_left)/len(y)  # 划分的子数据集占总数据集的个数,在计算基尼增益时用作权重
            pro_right = len(sub_y_right)/len(y)  # 划分的子数据集占总数据集的个数,在计算基尼增益时用作权重
            gini_gain =  gini_impurity - pro_left*gini_impurity_left - pro_right*gini_impurity_right
            # 取损失函数最小的属性索引和切分点
            if best_gini_gain < gini_gain:
                best_gini_gain = gini_gain
                best_feature_index = i
                best_split_point = split_point
    
    return best_split_point, best_feature_index

def create_tree( X, y, feature_names,min_samples_split=1):
    """
        feature_names: numpy.array
        return tree: dict 
    """
    # 若X中样本全属于同一类别C,则停止划分
    if y.max() == y.min():
        return get_value(y[0], to_X=False)
    # 若节点样本数小于min_samples_split,或者属性集上的取值均相同
    if len(y) <= min_samples_split or X.max()==X.min():
        return get_value(majority_y_id(y))

    # 上述代码为停止条件,用于判断程序什么时候对于某节点不继续划分数据集
    # 下述开始对每一个特征变量进行处理,获取其基尼增益,最后将所有特征的基尼增益进行对比,以便于确定下次根据什么特征继续划分样本
    # 按照“基尼增益”,从属性值中选择最优分裂属性的最优切分点
    best_split_point, best_feature_index = choose_best_point_to_split(X, y)
    best_feature_name = feature_names[best_feature_index]

    # 根据最优切分点,进行子树的划分
    tree = {best_feature_name: {}}
    sub_feature_names = feature_names.copy()
    sub_X_1 = X[X[:, best_feature_index]==best_split_point, :]
    sub_y_1 = y[X[:, best_feature_index]==best_split_point]
    sub_X_2 = X[X[:, best_feature_index]!=best_split_point, :]
    sub_y2 = y[X[:, best_feature_index]!=best_split_point]
    leaf_left = "=={}".format(get_value(best_split_point))
    leaf_right = "!={}".format(get_value(best_split_point))
    tree[best_feature_name][leaf_left] = create_tree(sub_X_1, sub_y_1, sub_feature_names)
    tree[best_feature_name][leaf_right] = create_tree(sub_X_2, sub_y2, sub_feature_names)
    return tree

**上述create_tree函数是个递归,递归算法在容易理解也不好理解,总的来说就是一层套一层,俄罗斯套娃,调试简单斐波拉契数列的即可理解,这是放学习递归的网站:
学习递归

上述决策树构建之后,就可以对数据进行预测了,下面是预测的逻辑,是将决策树一层一层,从上到下一直找到其样本的所在最后的子集中,在该子集中根据最多类别赋予此样本类别。

def classify(tree, feature_names, X_tensor):
        """分类预测  
            tree: dict  
            feature_names: numpy.array  
            X_tensor: torch.tensor  
            return y_pred: 预测类y str
        """
        first_str = list(tree.keys())[0]                        # 根节点
        second_dict = tree[first_str]                           # 根节点子树集合
        feature_index = feature_names.index(first_str) # 根节点分裂属性索引
        for key in second_dict.keys():
            key_list = [key[:2], key[2:]]
            split_point = key_list[1]                                 # 切分点属性值
            current_value = get_value(X_tensor[feature_index]) # 预测样本属性值
            if (key_list[0] == "==" and current_value == split_point) or \
                (key_list[0] == "!=" and current_value != split_point):
                if type(second_dict[key]).__name__ == 'dict':
                    y_pred = classify(second_dict[key], feature_names, X_tensor)
                else:
                    y_pred = second_dict[key]
                return y_pred

def predict(X):
    """使用树模型进行预测  
        X:新的样本数据集,其格式要与训练集格式一致
        return y_pred: np.array  
    """
    y_preds = []
    # 循环预测每个样本,这里逻辑可以改成并行处理
    for x in X:
        y_pred = classify(tree, feature_names, x)
        y_preds.append(y_pred)
    return np.array(y_preds)

4 总结

本文介绍了CART经典决策树,介绍了在决策树构建过程中会常遇到的缺失值如何处理,以及连续属性如何离散化的问题,其次对CART算法中数据集划分好坏的几个评估指标进行了详细描述,如基尼不纯度,基尼指数等,并提供了相关示例用于描述相关公式的计算,便于理解。进一步的,为描述决策树构建过程中的细节,对其核心源码进行了解释和说明,其中的关键在于递归算法的掌握和理解。

欢迎点赞,收藏,关注,支持小生,打造一个好的遥感领域知识分享专栏。
考虑到csdn平台刚起步,完整及时的文章更新请参考:专栏

**同时欢迎私信咨询讨论学习,咨询讨论的方向不限于:
地物分类/语义分割(如水体,云,建筑物,耕地,冬小麦等各种地物类型的提取),变化检测,夜光遥感数据处理,目标检测,图像处理(几何矫正,辐射矫正(大气校正),图像去噪等),遥感时空融合,定量遥感(土壤盐渍化/水质参数反演/气溶胶反演/森林参数(生物量,植被覆盖度,植被生产力等)/地表温度/地表反射率等反演)以及高光谱数据处理等领域以及深度学习,机器学习等技术算法讨论,以及相关实验指导/论文指导等多方面。

如果对具体的详细示例有兴趣,可以参考本专栏的参考书目:
<现代决策树模型及其编程实践:从传统决策树到深度决策树 黄智濒>
其电子书和其相关的源码可通过下述咨询链接获取资源获取

  • 26
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值