吴恩达机器学习课程(PyTorch适配)学习笔记:1.5 决策树与集成学习

决策树是一种直观且易于解释的机器学习模型,而集成学习通过组合多个决策树能够显著提升模型性能。本文详细讲解决策树的原理、学习过程、纯度指标,以及随机森林、XGBoost等集成学习方法,并提供PyTorch实现示例。

1.5.1 决策树模型(结构 + 原理)

决策树(Decision Tree)是一种基于树状结构进行决策的模型,其核心思想是通过一系列"if-then"规则对数据进行分类或回归。

1. 基本结构

决策树由三种节点组成:

  • 根节点:整个决策树的起点,包含所有样本
  • 内部节点:表示一个特征测试,每个分支代表一个测试结果
  • 叶节点:表示最终的决策结果(分类标签或回归值)

2. 工作原理

决策树通过自顶向下递归分割的方式工作:

  1. 从根节点开始,选择最优特征对样本进行分割
  2. 对每个子节点重复分割过程,直到满足停止条件
  3. 每个叶节点对应一个决策结果

3. 决策树的优势与不足

优势 不足
直观易懂,可解释性强 容易过拟合(尤其是深度较大的树)
无需特征归一化/标准化 对噪声敏感
能处理混合类型特征 可能产生偏斜树(某些路径特别长)
训练过程快速 不稳定性(小的数据集变化可能导致树结构巨变)

4. 决策树的类型

  • 分类树:叶节点为离散的类别标签(如"是否违约"、"疾病类型")
  • 回归树:叶节点为连续的数值(如"房价"、"温度")
  • 多输出树:可同时预测多个目标变量

1.5.2 决策树学习过程(纯度测量 + 特征分割)

决策树的学习过程本质是寻找最优分割特征和分割点的过程,核心目标是使分割后的子节点"纯度"更高(即样本更相似)。

1. 学习过程四步曲

  1. 特征选择:从所有可用特征中选择一个最优特征作为当前节点的分割特征
  2. 分割点确定:为选定的特征确定最佳分割点(分类特征:每个取值为一个分割点;连续特征:寻找最优阈值)
  3. 节点分裂:根据选定的特征和分割点将当前节点分裂为子节点
  4. 停止条件 :当满足以下任一条件时停止分裂
    • 节点中所有样本属于同一类别(分类树)或预测值差异小于阈值(回归树)
    • 节点包含的样本数小于最小分裂样本数
    • 树的深度达到预设最大值

2. 最优特征与分割点的选择标准

选择最优特征和分割点的核心思想是:分割后子节点的"不纯度"下降最大

数学上,用"信息增益"(Information Gain)衡量不纯度下降的幅度:
IG(D,a)=I(D)−∑v∈Values(a)∣Dv∣∣D∣I(Dv)IG(D, a) = I(D) - \sum_{v \in Values(a)} \frac{|D_v|}{|D|} I(D_v)IG(D,a)=I(D)−v∈Values(a)∑∣D∣∣Dv∣I(Dv)

其中:

  • I(D)I(D)I(D) 是父节点的不纯度(用熵或基尼系数衡量)
  • aaa 是候选特征
  • DvD_vDv 是特征aaa取值为vvv的子节点样本集
  • ∣Dv∣∣D∣\frac{|D_v|}{|D|}∣D∣∣Dv∣ 是子节点样本占父节点样本的比例

3. 分割过程示例

假设我们有以下关于"是否购买电脑"的数据集:

年龄 收入 学生 信用评级 购买电脑
青年 一般
青年
中年 一般
老年 一般
老年 一般
老年
中年
青年 一般
青年 一般
老年 一般

分割过程:

  1. 计算根节点的不纯度(假设用熵)
  2. 分别计算每个特征(年龄、收入、学生、信用评级)的信息增益
  3. 选择信息增益最大的特征作为根节点的分割特征
  4. 对每个子节点重复上述过程

4. PyTorch实现简单特征分割

python 复制代码
import torch
import numpy as np

def entropy(y):
    """计算熵(不纯度指标)"""
    # 计算每个类别的概率
    _, counts = torch.unique(y, return_counts=True)
    probabilities = counts.float() / len(y)
    # 计算熵
    return -torch.sum(probabilities * torch.log2(probabilities + 1e-10))  # 加小值避免log(0)

def information_gain(parent_y, left_y, right_y):
    """计算信息增益"""
    parent_entropy = entropy(parent_y)
    left_entropy = entropy(left_y)
    right_entropy = entropy(right_y)
    # 信息增益 = 父节点熵 - 子节点加权熵
    return parent_entropy - (len(left_y)/len(parent_y)*left_entropy + 
                             len(right_y)/len(parent_y)*right_entropy)

def find_best_split(X, y):
    """寻找最佳分割特征和分割点"""
    best_gain = -float('inf')
    best_feature = -1
    best_threshold = None
    
    n_samples, n_features = X.shape
    
    # 遍历每个特征
    for feature in range(n_features):
        # 获取该特征的所有值
        values = X[:, feature]
        # 尝试所有可能的分割点( unique值)
        thresholds = torch.unique(values)
        
        for threshold in thresholds:
            # 分割样本
            left_mask = values <= threshold
            right_mask = ~left_mask
            left_y = y[left_mask]
            right_y = y[right_mask]
            
            # 跳过样本数为0的分割
            if len(left_y) == 0 or len(right_y) == 0:
                continue
            
            # 计算信息增益
            gain = information_gain(y, left_y, right_y)
            
            # 更新最佳分割
            if gain > best_gain:
                best_gain = gain
                best_feature = feature
                best_threshold = threshold
    
    return best_feature, best_threshold, best_gain

# 模拟数据(使用上面的"是否购买电脑"数据,转换为数值)
# 年龄:青年=0, 中年=1, 老年=2
# 收入:低=0, 中=1, 高=2
# 学生:否=0, 是=1
# 信用评级:一般=0, 好=1
# 购买电脑:否=0, 是=1
X = torch.tensor([
    [0, 2, 0, 0], [0, 2, 0, 1], [1, 2, 0, 0], [2, 1, 0, 0], [2, 0, 1, 0],
    [2, 0, 1, 1], [1, 0, 1, 1], [0, 1, 0, 0], [0, 0, 1, 0], [2, 1, 1, 0]
], dtype=torch.float32)
y = torch.tensor([0, 0, 1, 1, 1, 0, 1, 0, 1, 1], dtype=torch.long)

# 寻找最佳分割
best_feature, best_threshold, best_gain = find_best_split(X, y)
print(f"最佳分割特征: {best_feature} (年龄=0, 收入=1, 学生=2, 信用评级=3)")
print(f"最佳分割阈值: {best_threshold}")
print(f"信息增益: {best_gain:.4f}")

5. 易错点与注意事项

  • 分割点选择偏差:连续特征的分割点选择可能受样本数量影响,稀疏区域的分割点可能不可靠
  • 多重共线性:高度相关的特征可能具有相似的信息增益,导致选择不稳定
  • 过拟合风险:如果不设置停止条件,决策树可能会一直分裂直到每个叶节点只包含一个样本
  • 类别不平衡:在不平衡数据集中,决策树可能会倾向于分割多数类,需要特殊处理

1.5.3 纯度指标(熵 + 基尼系数)

纯度指标(Purity Metric)用于衡量节点中样本的同质性,纯度越高,说明节点中的样本越相似。常用的纯度指标有熵(Entropy)基尼系数(Gini Index)

1. 熵(Entropy)

熵源自信息论,用于衡量随机变量的不确定性,在决策树中表示样本集合的混乱程度。

计算公式

对于包含kkk个类别的样本集合DDD,其熵定义为:
H(D)=−∑i=1kpilog⁡2(pi)H(D) = -\sum_{i=1}^{k} p_i \log_2(p_i)H(D)=−i=1∑kpilog2(pi)

其中pip_ipi是第iii类样本在集合DDD中所占的比例。

熵的特性
  • 当所有样本属于同一类别时,H(D)=0H(D) = 0H(D)=0(纯度最高)
  • 当样本均匀分布在所有类别时,H(D)H(D)H(D)达到最大值
    • 二分类问题:H(D)=1H(D) = 1H(D)=1(当p1=p2=0.5p_1 = p_2 = 0.5p1=p2=0.5时)
    • 多分类问题:H(D)=log⁡2(k)H(D) = \log_2(k)H(D)=log2(k)(当p1=p2=...=pk=1/kp_1 = p_2 = ... = p_k = 1/kp1=p2=...=pk=1/k时)

2. 基尼系数(Gini Index)

基尼系数衡量从样本集合中随机抽取两个样本,其类别不同的概率。

计算公式

G(D)=1−∑i=1kpi2G(D) = 1 - \sum_{i=1}^{k} p_i^2G(D)=1−i=1∑kpi2

其中pip_ipi是第iii类样本在集合DDD中所占的比例。

基尼系数的特性
  • 当所有样本属于同一类别时,G(D)=0G(D) = 0G(D)=0(纯度最高)
  • 当样本均匀分布在所有类别时,G(D)G(D)G(D)达到最大值
    • 二分类问题:G(D)=0.5G(D) = 0.5G(D)=0.5(当p1=p2=0.5p_1 = p_2 = 0.5p1=p2=0.5时)
    • 多分类问题:G(D)=1−1/kG(D) = 1 - 1/kG(D)=1−1/k(当p1=p2=...=pk=1/kp_1 = p_2 = ... = p_k = 1/kp1=p2=...=pk=1/k时)

3. 熵与基尼系数的对比

特性 基尼系数
计算复杂度 较高(涉及对数运算) 较低(仅涉及平方运算)
对不纯度的敏感度 对中间值更敏感(曲线更陡峭) 敏感度较低
计算结果范围 [0, log₂k] [0, 1-1/k]
常用场景 需要更精细分割的场景 追求计算效率的场景

4. 可视化对比

python 复制代码
import torch
import matplotlib.pyplot as plt

# 计算二分类问题中的熵和基尼系数
p = torch.linspace(0, 1, 100)  # 正类比例从0到1
entropy = -p * torch.log2(p + 1e-10) - (1-p) * torch.log2((1-p) + 1e-10)
gini = 1 - p**2 - (1-p)** 2

# 绘图
plt.figure(figsize=(10, 6))
plt.plot(p.numpy(), entropy.numpy(), label='熵 (Entropy)', linewidth=2)
plt.plot(p.numpy(), gini.numpy(), label='基尼系数 (Gini Index)', linewidth=2)
plt.xlabel('正类样本比例 p')
plt.ylabel('不纯度')
plt.title('二分类问题中熵与基尼系数的对比')
plt.grid(alpha=0.3)
plt.legend()
plt.axvline(x=0.5, color='gray', linestyle='--', alpha=0.5)
plt.text(0.51, 0.8, 'p=0.5 (最大不纯度)', rotation=90)
plt.savefig('entropy_vs_gini.png', dpi=300)
plt.show()

5. 实际应用中的选择建议

  • 当计算资源有限或数据集较大时,优先选择基尼系数(计算更快)
  • 当需要更精细的分割(尤其是类别较多时),可以尝试使用熵
  • 在大多数情况下,两种指标会产生相似的决策树,差异通常不大
  • 可以通过交叉验证比较两种指标在特定任务上的表现

6. 注意事项

  • 熵和基尼系数都是相对指标,只用于比较同一节点的不同分割方式,不适合跨节点比较
  • 对于高度不平衡的数据集,两种指标都可能倾向于分割多数类,需要结合其他策略(如类别权重)
  • 实现时要注意数值稳定性,避免pi=0p_i=0pi=0时的log⁡(0)\log(0)log(0)问题(通常加一个极小值如1e−101e-101e−10)

1.5.4 特征处理(分类特征独热编码 + 连续特征离散化)

决策树对输入特征有特定要求,需要对不同类型的特征进行适当处理,尤其是分类特征和连续特征。

1. 分类特征处理

分类特征是指取值为离散类别的特征(如颜色、职业、学历等),可分为:

  • 名义特征:无顺序关系(如颜色:红、绿、蓝)
  • 序数特征:有顺序关系(如学历:高中、本科、硕士、博士)
(1)独热编码(One-Hot Encoding)

适用于名义特征,将每个类别转换为一个二进制特征。

示例:颜色特征(红、绿、蓝)

  • 红 → [1, 0, 0]
  • 绿 → [0, 1, 0]
  • 蓝 → [0, 0, 1]

PyTorch实现

python 复制代码
import torch
from torch.nn.functional import one_hot

# 原始分类特征(0:红, 1:绿, 2:蓝)
color_features = torch.tensor([0, 1, 2, 0, 1], dtype=torch.long)

# 独热编码
one_hot_encoded = one_hot(color_features, num_classes=3)

print("原始特征:", color_features)
print("独热编码后:", one_hot_encoded)

优点

  • 避免模型误认为类别之间存在数值关系
  • 保持各类别之间的平等地位

缺点

  • 特征维度会随类别数量增加而急剧增加(维度灾难)
  • 对于高基数特征(类别数多)效果不佳
(2)标签编码(Label Encoding)

适用于序数特征,将每个类别映射为一个整数。

示例:学历特征(高中=0, 本科=1, 硕士=2, 博士=3)

注意:仅适用于有明确顺序的特征,否则会引入虚假的数值关系。

(3)目标编码(Target Encoding)

用类别在目标变量上的统计值(如均值)来编码特征,适用于高基数特征。

优点 :不会增加特征维度,对高基数特征效果好
缺点:容易过拟合,需要使用交叉验证进行正则化

2. 连续特征离散化

连续特征是指取值为连续数值的特征(如年龄、收入、温度等),决策树需要将其离散化(即划分为若干区间)。

(1)常用离散化方法
  1. 等宽离散化:将特征值范围等分为k个区间
  2. 等频离散化:将特征值分为k个区间,每个区间包含相同数量的样本
  3. 基于决策树的离散化:使用决策树自动找到最优分割点
  4. 聚类离散化:使用聚类算法(如K-Means)将特征值聚类后离散化
(2)PyTorch实现连续特征离散化
python 复制代码
import torch
import numpy as np
import matplotlib.pyplot as plt

# 生成连续特征数据(模拟年龄分布)
np.random.seed(42)
ages = np.random.normal(40, 15, 1000)
ages = np.clip(ages, 0, 100)  # 限制在0-100岁
ages_tensor = torch.tensor(ages, dtype=torch.float32)

# 1. 等宽离散化
def equal_width_discretization(x, num_bins):
    min_val = x.min()
    max_val = x.max()
    bins = torch.linspace(min_val, max_val, num_bins+1)
    # 找到每个值所属的区间
    discretized = torch.bucketize(x, bins[1:-1])  # 排除首尾,避免边界问题
    return discretized, bins

# 2. 等频离散化
def equal_freq_discretization(x, num_bins):
    # 计算分位数
    percentiles = torch.linspace(0, 100, num_bins+1)[1:-1]  # 排除0和100
    bins = torch.tensor([torch.quantile(x, p/100) for p in percentiles])
    # 去重,避免重复的分位数
    bins = torch.unique(bins)
    # 找到每个值所属的区间
    discretized = torch.bucketize(x, bins)
    return discretized, bins

# 应用两种离散化方法
num_bins = 5
ew_discretized, ew_bins = equal_width_discretization(ages_tensor, num_bins)
ef_discretized, ef_bins = equal_freq_discretization(ages_tensor, num_bins)

# 可视化离散化结果
plt.figure(figsize=(12, 5))

# 原始数据分布
plt.subplot(1, 3, 1)
plt.hist(ages, bins=30, alpha=0.7)
plt.title('原始年龄分布')
plt.xlabel('年龄')
plt.ylabel('频数')

# 等宽离散化
plt.subplot(1, 3, 2)
plt.hist(ages, bins=ew_bins, alpha=0.7)
for bin_val in ew_bins:
    plt.axvline(bin_val, color='r', linestyle='--', alpha=0.5)
plt.title('等宽离散化 (5个区间)')
plt.xlabel('年龄')

# 等频离散化
plt.subplot(1, 3, 3)
plt.hist(ages, bins=ef_bins, alpha=0.7)
for bin_val in ef_bins:
    plt.axvline(bin_val, color='r', linestyle='--', alpha=0.5)
plt.title('等频离散化 (5个区间)')
plt.xlabel('年龄')

plt.tight_layout()
plt.savefig('continuous_discretization.png', dpi=300)
plt.show()

3. 特征处理注意事项

  1. 避免数据泄露:特征处理的统计量(如均值、分位数)必须仅基于训练集计算,再应用到验证集和测试集
  2. 高基数特征处理:类别数超过100的特征称为高基数特征,不适合独热编码,可考虑目标编码或嵌入(Embedding)
  3. 缺失值处理:决策树可以将缺失值作为一个单独的类别处理,或用中位数/众数填充
  4. 特征缩放:决策树对特征缩放不敏感,不需要进行标准化或归一化处理
  5. 特征选择:冗余特征会增加计算复杂度并可能导致过拟合,可通过特征重要性评分进行筛选

4. 常见错误与解决方案

错误 解决方案
对名义特征使用标签编码 使用独热编码或目标编码
对高基数特征使用独热编码 使用目标编码或嵌入技术
离散化区间划分不当 结合领域知识或使用基于决策树的自动离散化
特征处理时使用了测试集数据 严格遵循"先分割数据,再进行特征处理"的原则

1.5.5 回归树(适用场景 + 框架)

回归树(Regression Tree)是决策树的一种变体,用于解决连续值预测问题,其叶节点存储的是预测值而非类别标签。

1. 回归树的基本原理

与分类树的主要区别:

  • 预测目标:连续数值(如房价、温度、销售额)
  • 分裂标准:使用均方误差(MSE)或平均绝对误差(MAE)而非熵或基尼系数
  • 叶节点输出:该节点所有样本的平均值(或中位数)

2. 分裂标准:均方误差(MSE)

回归树使用均方误差减少量(Reduction in MSE)作为分裂标准:

  1. 节点的均方误差:
    MSE(D)=1∣D∣∑x∈D(y(x)−yˉD)2MSE(D) = \frac{1}{|D|} \sum_{x \in D} (y(x) - \bar{y}_D)^2MSE(D)=∣D∣1x∈D∑(y(x)−yˉD)2

    其中yˉD\bar{y}_DyˉD是节点DDD中所有样本的平均值。

  2. 分裂后的均方误差减少量:
    ΔMSE=MSE(D)−∑v∈Values(a)∣Dv∣∣D∣MSE(Dv)\Delta MSE = MSE(D) - \sum_{v \in Values(a)} \frac{|D_v|}{|D|} MSE(D_v)ΔMSE=MSE(D)−v∈Values(a)∑∣D∣∣Dv∣MSE(Dv)

回归树选择使ΔMSE\Delta MSEΔMSE最大的特征和分割点进行分裂。

3. 回归树的适用场景

  • 特征与目标变量之间存在非线性关系
  • 特征之间存在交互作用
  • 需要模型具有高解释性
  • 数据中包含异常值(适当设置后对异常值不敏感)

典型应用场景:

  • 房价预测
  • 销售额预测
  • 客户终身价值预测
  • 温度/降雨量预测

4. 回归树的框架与实现

python 复制代码
import torch
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import fetch_california_housing
from sklearn.model_selection import train_test_split

class RegressionTreeNode:
    def __init__(self, depth=0, max_depth=5, min_samples_split=5):
        self.depth = depth
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.left = None  # 左子树
        self.right = None  # 右子树
        self.feature_idx = None  # 分割特征索引
        self.threshold = None  # 分割阈值
        self.value = None  # 叶节点的预测值
        self.mse = None  # 节点的均方误差

    def mse_loss(self, y):
        """计算均方误差"""
        if len(y) == 0:
            return 0
        mean = torch.mean(y)
        return torch.mean((y - mean) **2)

    def split(self, X, y):
        """寻找最佳分割点"""
        n_samples, n_features = X.shape
        best_mse_reduction = -float('inf')
        best_feature = -1
        best_threshold = None

        # 计算当前节点的MSE
        current_mse = self.mse_loss(y)

        # 遍历每个特征
        for feature_idx in range(n_features):
            # 获取该特征的所有值
            values = X[:, feature_idx]
            unique_values = torch.unique(values)

            # 尝试每个可能的分割点
            for threshold in unique_values:
                # 分割样本
                left_mask = values <= threshold
                right_mask = ~left_mask
                left_y = y[left_mask]
                right_y = y[right_mask]

                # 跳过样本数不足的分割
                if len(left_y) < self.min_samples_split or len(right_y) < self.min_samples_split:
                    continue

                # 计算分割后的MSE减少量
                left_mse = self.mse_loss(left_y)
                right_mse = self.mse_loss(right_y)
                mse_reduction = current_mse - (len(left_y)/len(y)*left_mse + 
                                              len(right_y)/len(y)*right_mse)

                # 更新最佳分割
                if mse_reduction > best_mse_reduction:
                    best_mse_reduction = mse_reduction
                    best_feature = feature_idx
                    best_threshold = threshold

        return best_feature, best_threshold, best_mse_reduction

    def fit(self, X, y):
        """训练回归树"""
        # 计算当前节点的预测值(均值)
        self.value = torch.mean(y)
        self.mse = self.mse_loss(y)

        # 检查停止条件
        if self.depth >= self.max_depth or len(y) < 2*self.min_samples_split:
            return self

        # 寻找最佳分割
        best_feature, best_threshold, best_mse_reduction = self.split(X, y)

        # 如果没有找到有意义的分割,停止分裂
        if best_feature == -1 or best_mse_reduction <= 0:
            return self

        # 记录最佳分割
        self.feature_idx = best_feature
        self.threshold = best_threshold

        # 分割样本
        values = X[:, best_feature]
        left_mask = values <= best_threshold
        right_mask = ~left_mask

        # 递归训练左右子树
        self.left = RegressionTreeNode(self.depth + 1, self.max_depth, self.min_samples_split)
        self.right = RegressionTreeNode(self.depth + 1, self.max_depth, self.min_samples_split)
        self.left.fit(X[left_mask], y[left_mask])
        self.right.fit(X[right_mask], y[right_mask])

        return self

    def predict_single(self, x):
        """预测单个样本"""
        # 如果是叶节点,返回预测值
        if self.left is None or self.right is None:
            return self.value

        # 否则递归预测
        if x[self.feature_idx] <= self.threshold:
            return self.left.predict_single(x)
        else:
            return self.right.predict_single(x)

    def predict(self, X):
        """预测多个样本"""
        return torch.tensor([self.predict_single(x) for x in X], dtype=torch.float32)

# 加载加州房价数据集
housing = fetch_california_housing()
X = torch.tensor(housing.data, dtype=torch.float32)
y = torch.tensor(housing.target, dtype=torch.float32)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练回归树
reg_tree = RegressionTreeNode(max_depth=5, min_samples_split=10)
reg_tree.fit(X_train, y_train)

# 预测
y_pred = reg_tree.predict(X_test)

# 评估
mse = torch.mean((y_pred - y_test)** 2)
print(f"测试集MSE: {mse:.4f}")
print(f"测试集RMSE: {torch.sqrt(mse):.4f}")

# 可视化预测结果(取前100个样本)
plt.figure(figsize=(10, 6))
plt.scatter(range(100), y_test[:100], alpha=0.6, label='真实值', color='blue')
plt.scatter(range(100), y_pred[:100], alpha=0.6, label='预测值', color='red')
plt.xlabel('样本索引')
plt.ylabel('房价(单位:$100k)')
plt.title('回归树预测结果(加州房价)')
plt.legend()
plt.grid(alpha=0.3)
plt.savefig('regression_tree_predictions.png', dpi=300)
plt.show()

5. 回归树的优缺点

优点
  • 能够捕捉非线性关系和特征交互
  • 不需要对特征进行缩放或标准化
  • 对异常值不敏感(相比线性回归)
  • 可解释性强,能明确显示哪些特征对预测最重要
缺点
  • 容易过拟合,尤其是在深度较大时
  • 预测结果是分段常数,不够平滑
  • 对训练数据的小变化敏感(稳定性差)
  • 可能产生偏斜树,影响预测性能

6. 正则化策略(防止过拟合)

  1. 限制树的深度 :设置max_depth参数
  2. 最小分裂样本数 :设置min_samples_split,当节点样本数少于该值时不分裂
  3. 最小叶节点样本数 :设置min_samples_leaf,确保叶节点有足够样本
  4. 最大叶节点数:限制叶节点总数
  5. 后剪枝:先构建完整树,再移除对性能提升不大的分支

7. 注意事项

  • 回归树的预测值范围不会超过训练数据中目标变量的范围
  • 对于时间序列数据,需要特别注意分割方式,避免使用未来信息
  • 当特征维度远大于样本数时,回归树容易过拟合
  • 回归树的预测结果是阶梯函数形式,适合捕捉突变关系而非渐变关系

1.5.6 集成学习(多决策树融合思路)

集成学习(Ensemble Learning)通过组合多个弱学习器(通常是决策树)的预测结果,来获得比单个学习器更好的性能。其核心思想是"三个臭皮匠,顶个诸葛亮"。

1. 集成学习的优势

  • 提高泛化能力:减少单个模型的偏差和方差
  • 增强稳定性:降低对训练数据微小变化的敏感性
  • 处理复杂模式:能捕捉单个模型难以发现的复杂关系
  • 降低过拟合风险:通过多个模型的投票/平均抵消个体误差

2. 集成学习的基本原理

集成学习的效果取决于两个因素:

  1. 个体模型的准确性:每个模型都应优于随机猜测
  2. 个体模型的多样性:模型之间的预测误差应尽可能不相关

数学上,如果有MMM个独立的分类器,每个分类器的错误率为ppp,则多数投票的错误率为:
P(错误)=∑k=0⌊M/2⌋(Mk)pk(1−p)M−kP(\text{错误}) = \sum_{k=0}^{\lfloor M/2 \rfloor} \binom{M}{k} p^k (1-p)^{M-k}P(错误)=k=0∑⌊M/2⌋(kM)pk(1−p)M−k

当p<0.5p < 0.5p<0.5时,随着MMM增大,集成错误率会指数级下降。

3. 三种主流集成方法

(1)Bagging( bootstrap aggregating )
  • 核心思想:通过bootstrap抽样(有放回抽样)生成多个不同的训练集,分别训练模型,最后通过投票(分类)或平均(回归)组合结果
  • 多样性来源:不同的训练数据子集
  • 代表算法:随机森林(Random Forest)
(2)Boosting
  • 核心思想:迭代地训练模型,每次关注前一轮被错误分类的样本(增加其权重),最后通过加权投票/平均组合结果
  • 多样性来源:不同的样本权重分布
  • 代表算法:AdaBoost、GBDT、XGBoost、LightGBM
(3)Stacking
  • 核心思想:训练多个不同类型的基础模型,将它们的预测结果作为新特征,再训练一个元模型(meta-model)来组合这些预测
  • 多样性来源:不同类型的基础模型
  • 特点:灵活性高,但复杂度也高

4. 集成学习框架对比

特性 Bagging Boosting Stacking
训练顺序 并行 串行 通常并行基础模型,串行元模型
样本权重 相同 随迭代调整 通常相同
模型权重 相同 随性能调整 由元模型学习
过拟合风险 中高(需控制迭代次数) 中高(需正则化)
计算效率 高(可并行) 中(需串行) 低(模型多)
调参难度 中高

5. 多决策树融合的优势

以决策树作为基础模型的集成方法具有以下优势:

  • 解决单个决策树的过拟合问题
  • 保留决策树处理非线性和特征交互的能力
  • 相比单个决策树,稳定性和泛化能力显著提升
  • 可以提供特征重要性评估

6. 集成学习可视化

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.colors import ListedColormap
import torch

# 生成二维分类数据
np.random.seed(42)
X = np.random.randn(200, 2)
y = np.logical_xor(X[:, 0] > 0, X[:, 1] > 0)
y = np.where(y, 1, 0)

X = torch.tensor(X, dtype=torch.float32)
y = torch.tensor(y, dtype=torch.long)

# 定义一个简单的决策树分类器(简化版)
class SimpleDecisionTree:
    def __init__(self, max_depth=3):
        self.max_depth = max_depth
        self.feature_idx = None
        self.threshold = None
        self.left = None
        self.right = None
        self.classes = None
        self.prediction = None

    def fit(self, X, y):
        self.classes = torch.unique(y)
        if self.max_depth == 0 or len(torch.unique(y)) == 1:
            # 叶节点:预测为多数类
            counts = torch.bincount(y)
            self.prediction = torch.argmax(counts)
            return
        
        # 简单分割:找第一个特征的中间值作为阈值
        self.feature_idx = 0
        self.threshold = torch.median(X[:, self.feature_idx])
        
        # 分割样本
        mask = X[:, self.feature_idx] <= self.threshold
        self.left = SimpleDecisionTree(self.max_depth - 1)
        self.right = SimpleDecisionTree(self.max_depth - 1)
        self.left.fit(X[mask], y[mask])
        self.right.fit(X[~mask], y[~mask])

    def predict(self, X):
        if self.prediction is not None:
            return torch.full((X.shape[0],), self.prediction, dtype=torch.long)
        
        mask = X[:, self.feature_idx] <= self.threshold
        y_pred = torch.zeros(X.shape[0], dtype=torch.long)
        y_pred[mask] = self.left.predict(X[mask])
        y_pred[~mask] = self.right.predict(X[~mask])
        return y_pred

# 定义Bagging集成
class BaggingEnsemble:
    def __init__(self, n_estimators=5, max_depth=3):
        self.n_estimators = n_estimators
        self.estimators = [SimpleDecisionTree(max_depth) for _ in range(n_estimators)]
        
    def fit(self, X, y):
        n_samples = X.shape[0]
        for estimator in self.estimators:
            # Bootstrap抽样
            indices = torch.randint(0, n_samples, (n_samples,))
            X_boot = X[indices]
            y_boot = y[indices]
            estimator.fit(X_boot, y_boot)
    
    def predict(self, X):
        # 收集所有模型的预测
        predictions = torch.stack([est.predict(X) for est in self.estimators])
        # 多数投票
        return torch.mode(predictions, dim=0).values

# 训练单个决策树和Bagging集成
tree = SimpleDecisionTree(max_depth=3)
tree.fit(X, y)

ensemble = BaggingEnsemble(n_estimators=5, max_depth=3)
ensemble.fit(X, y)

# 可视化决策边界
def plot_decision_boundary(model, X, y, title):
    h = 0.02  # 网格步长
    x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1
    y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1
    xx, yy = np.meshgrid(np.arange(x_min, x_max, h),
                         np.arange(y_min, y_max, h))
    
    X_grid = torch.tensor(np.c_[xx.ravel(), yy.ravel()], dtype=torch.float32)
    y_pred = model.predict(X_grid)
    y_pred = y_pred.reshape(xx.shape)
    
    cmap_light = ListedColormap(['#FFAAAA', '#AAFFAA'])
    cmap_bold = ListedColormap(['#FF0000', '#00FF00'])
    
    plt.figure(figsize=(8, 6))
    plt.contourf(xx, yy, y_pred, cmap=cmap_light, alpha=0.8)
    plt.scatter(X[:, 0], X[:, 1], c=y, cmap=cmap_bold, edgecolor='k', s=20)
    plt.xlim(xx.min(), xx.max())
    plt.ylim(yy.min(), yy.max())
    plt.title(title)
    plt.xlabel('特征1')
    plt.ylabel('特征2')

# 绘制单个决策树和集成模型的决策边界
plot_decision_boundary(tree, X, y, '单个决策树的决策边界')
plt.savefig('single_tree_boundary.png', dpi=300)

plot_decision_boundary(ensemble, X, y, 'Bagging集成的决策边界')
plt.savefig('bagging_boundary.png', dpi=300)
plt.show()

7. 集成学习的注意事项

  • 基础模型选择:基础模型应具有多样性,同时保持一定的准确性
  • 集成规模:并非模型越多越好,超过一定数量后性能提升会趋于平缓
  • 计算资源:集成学习通常比单个模型需要更多的计算资源和时间
  • 过拟合风险:虽然集成学习降低了过拟合风险,但设计不当仍可能过拟合
  • 可解释性:集成模型通常比单个决策树的可解释性差(但优于神经网络等黑盒模型)

8. 集成学习调优方向

  1. 基础模型多样性:使用不同类型的模型或不同超参数的同类型模型
  2. 集成策略:尝试不同的组合方式(投票、平均、加权等)
  3. 正则化:对基础模型添加正则化约束,防止过拟合
  4. 模型数量:通过交叉验证找到性能最佳的模型数量
  5. 并行化:对Bagging等可并行的集成方法,利用多核CPU或GPU加速训练

1.5.7 随机森林(算法框架 + PyTorch 适配)

随机森林(Random Forest)是Bagging集成方法的典型代表,通过组合多个决策树的预测结果来提高性能,同时引入了特征随机性进一步增强模型多样性。

1. 随机森林的核心思想

随机森林在Bagging的基础上增加了特征随机选择

  1. 对每个决策树,使用bootstrap抽样生成不同的训练集
  2. 在每个节点分裂时,仅从随机选择的部分特征中寻找最优分割
  3. 最终预测通过所有树的投票(分类)或平均(回归)得到

这种双重随机性(样本随机+特征随机)使得随机森林比单一决策树和普通Bagging具有更好的泛化能力。

2. 算法框架

  1. 样本随机采样

    • 对每个树,从原始数据中有放回地随机采样NNN个样本(bootstrap抽样)
    • 每个样本被选中的概率约为63.2%,未被选中的样本组成"袋外样本"(OOB,Out-of-Bag)
  2. 特征随机选择

    • 在每个节点分裂时,从MMM个特征中随机选择mmm个特征(通常m=Mm = \sqrt{M}m=M )
    • 仅使用这mmm个特征来确定最佳分割点
  3. 树的构建

    • 每个树都尽可能生长(不剪枝)
    • 树的数量通常在100-1000之间
  4. 预测组合

    • 分类:多数投票(每个树一票,得票最多的类别为最终预测)
    • 回归:平均值(所有树的预测值的平均)

3. 随机森林的优势

  • 性能优异:在许多任务上表现接近或超过SVM和神经网络
  • 鲁棒性强:对噪声和异常值不敏感
  • 不易过拟合:即使树的数量很多,也不容易过拟合
  • 能处理高维数据:不需要特征选择也能表现良好
  • 提供特征重要性:可以评估每个特征对预测的贡献
  • 训练高效:树之间相互独立,可并行训练

4. PyTorch实现随机森林

python 复制代码
import torch
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, classification_report

# 决策树节点
class TreeNode:
    def __init__(self, depth=0, max_depth=5, min_samples_split=5, max_features=None):
        self.depth = depth
        self.max_depth = max_depth
        self.min_samples_split = min_samples_split
        self.max_features = max_features  # 每次分裂时随机选择的特征数
        self.left = None
        self.right = None
        self.feature_idx = None
        self.threshold = None
        self.class_counts = None  # 用于分类的类别计数
        self.prediction = None  # 叶节点的预测值

    def entropy(self, y):
        """计算熵"""
        _, counts = torch.unique(y, return_counts=True)
        probabilities = counts.float() / len(y)
        return -torch.sum(probabilities * torch.log2(probabilities + 1e-10))

    def best_split(self, X, y):
        """寻找最佳分割点(考虑随机选择的特征)"""
        n_samples, n_features = X.shape
        best_gain = -float('inf')
        best_feature = -1
        best_threshold = None

        # 如果指定了max_features,则随机选择特征子集
        if self.max_features is not None and self.max_features < n_features:
            feature_indices = torch.randperm(n_features)[:self.max_features]
        else:
            feature_indices = range(n_features)

        # 计算当前节点的熵
        current_entropy = self.entropy(y)

        # 遍历每个候选特征
        for feature_idx in feature_indices:
            # 获取该特征的所有值
            values = X[:, feature_idx]
            unique_values = torch.unique(values)

            # 尝试每个可能的分割点
            for threshold in unique_values:
                # 分割样本
                left_mask = values <= threshold
                right_mask = ~left_mask
                left_y = y[left_mask]
                right_y = y[right_mask]

                # 跳过样本数不足的分割
                if len(left_y) < self.min_samples_split or len(right_y) < self.min_samples_split:
                    continue

                # 计算信息增益
                left_entropy = self.entropy(left_y)
                right_entropy = self.entropy(right_y)
                gain = current_entropy - (len(left_y)/len(y)*left_entropy + 
                                         len(right_y)/len(y)*right_entropy)

                # 更新最佳分割
                if gain > best_gain:
                    best_gain = gain
                    best_feature = feature_idx
                    best_threshold = threshold

        return best_feature, best_threshold, best_gain

    def fit(self, X, y):
        """训练决策树"""
        # 记录类别计数和预测值
        self.class_counts = torch.bincount(y)
        self.prediction = torch.argmax(self.class_counts)

        # 检查停止条件
        if self.depth >= self.max_depth or len(y) < 2*self.min_samples_split or len(torch.unique(y)) == 1:
            return self

        # 寻找最佳分割
        best_feature, best_threshold, best_gain = self.best_split(X, y)

        # 如果没有找到有意义的分割,停止分裂
        if best_feature == -1 or best_gain <= 0:
            return self

        # 记录最佳分割
        self.feature_idx = best_feature
        self.threshold = best_threshold

        # 分割样本
        values = X[:, best_feature]
        left_mask = values <= best_threshold
        right_mask = ~left_mask

        # 递归训练左右子树
        self.left = TreeNode(self.depth + 1, self.max_depth, self.min_samples_split, self.max_features)
        self.right = TreeNode(self.depth + 1, self.max_depth, self.min_samples_split, self.max_features)
        self.left.fit(X[left_mask], y[left_mask])
        self.right.fit(X[right_mask], y[right_mask])

        return self

    def predict_single(self, x):
        """预测单个样本"""
        if self.left is None or self.right is None:
            return self.prediction

        if x[self.feature_idx] <= self.threshold:
            return self.left.predict_single(x)
        else:
            return self.right.predict_single(x)

    def predict(self, X):
        """预测多个样本"""
        return torch.tensor([self.predict_single(x) for x in X], dtype=torch.long)

# 随机森林
class RandomForest:
    def __init__(self, n_estimators=10, max_depth=5, min_samples_split=5, max_features='sqrt'):
        self.n_estimators = n_estimators  # 树的数量
        self.max_depth = max_depth  # 树的最大深度
        self.min_samples_split = min_samples_split  # 最小分裂样本数
        self.max_features = max_features  # 每次分裂时考虑的最大特征数
        self.estimators = []  # 存储所有树

    def fit(self, X, y):
        """训练随机森林"""
        n_samples, n_features = X.shape
        
        # 确定每次分裂时考虑的特征数
        if self.max_features == 'sqrt':
            max_features = int(torch.sqrt(torch.tensor(n_features)))
        elif self.max_features == 'log2':
            max_features = int(torch.log2(torch.tensor(n_features)))
        elif isinstance(self.max_features, int):
            max_features = self.max_features
        else:
            max_features = n_features  # 使用所有特征
            
        # 训练每棵树
        self.estimators = []
        for _ in range(self.n_estimators):
            # Bootstrap抽样
            indices = torch.randint(0, n_samples, (n_samples,))
            X_boot = X[indices]
            y_boot = y[indices]
            
            # 训练一棵树
            tree = TreeNode(
                max_depth=self.max_depth,
                min_samples_split=self.min_samples_split,
                max_features=max_features
            )
            tree.fit(X_boot, y_boot)
            self.estimators.append(tree)
            
        return self

    def predict(self, X):
        """预测"""
        # 收集所有树的预测
        predictions = torch.stack([tree.predict(X) for tree in self.estimators])
        # 多数投票
        return torch.mode(predictions, dim=0).values
    
    def feature_importance(self, X):
        """计算特征重要性"""
        n_features = X.shape[1]
        importance = torch.zeros(n_features)
        
        # 遍历每棵树,计算特征被用作分割点的次数
        for tree in self.estimators:
            # 递归计算树中每个特征的使用次数
            def count_features(node):
                if node.feature_idx is not None:
                    importance[node.feature_idx] += 1
                    count_features(node.left)
                    count_features(node.right)
            
            count_features(tree)
        
        # 归一化
        importance = importance / torch.sum(importance)
        return importance

# 加载乳腺癌数据集
data = load_breast_cancer()
X = torch.tensor(data.data, dtype=torch.float32)
y = torch.tensor(data.target, dtype=torch.long)

# 划分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 训练随机森林
rf = RandomForest(n_estimators=10, max_depth=5, min_samples_split=10)
rf.fit(X_train, y_train)

# 预测与评估
y_pred = rf.predict(X_test)
accuracy = accuracy_score(y_test.numpy(), y_pred.numpy())
print(f"随机森林准确率: {accuracy:.4f}")
print("\n分类报告:")
print(classification_report(y_test.numpy(), y_pred.numpy(), target_names=data.target_names))

# 特征重要性
importance = rf.feature_importance(X_train)
indices = torch.argsort(importance, descending=True)

# 可视化特征重要性(取前10个特征)
plt.figure(figsize=(10, 6))
plt.bar(range(10), importance[indices[:10]].numpy())
plt.xticks(range(10), [data.feature_names[i] for i in indices[:10]], rotation=90)
plt.title('随机森林特征重要性(前10名)')
plt.tight_layout()
plt.savefig('rf_feature_importance.png', dpi=300)
plt.show()

5. 随机森林超参数调优

关键超参数及调优建议:

超参数 作用 调优建议
n_estimators 树的数量 通常100-1000,增加树的数量可提高性能,但会增加计算成本
max_depth 树的最大深度 控制过拟合,较小的值(如5-10)可防止过拟合
min_samples_split 最小分裂样本数 较大的值(如10-20)可防止过拟合
max_features 每次分裂考虑的最大特征数 分类问题常用'sqrt',回归问题常用'log2'或0.5
min_samples_leaf 叶节点最小样本数 较大的值可使模型更稳健
bootstrap 是否使用bootstrap抽样 通常设为True,使用OOB样本评估性能

调优策略:

  1. 先调整n_estimators到合理范围(如100)
  2. 调整max_depth和min_samples_split控制树结构
  3. 调整max_features控制特征随机性
  4. 最后微调n_estimators

6. 随机森林的适用场景

  • 分类问题:欺诈检测、客户流失预测、疾病诊断等
  • 回归问题:房价预测、销售额预测、风险评估等
  • 特征选择:通过特征重要性识别关键特征
  • 异常检测:利用OOB误差识别异常样本

7. 注意事项与常见错误

  • 类别不平衡:随机森林在类别不平衡数据上可能倾向于多数类,需使用class_weight参数
  • 高基数类别特征:对类别数很多的特征,随机森林可能需要更多的树才能捕捉模式
  • 特征缩放:随机森林不需要特征缩放,但对特征单位敏感
  • 过度调参:随机森林对超参数通常不敏感,轻微调整不会显著影响性能
  • 解释性限制:虽然随机森林提供特征重要性,但不如单个决策树直观

1.5.8 XGBoost(原理 + 应用场景)

XGBoost(Extreme Gradient Boosting)是一种高效的梯度提升算法,通过优化的工程实现和正则化策略,在各类机器学习竞赛中表现优异,成为数据科学领域的重要工具。

1. XGBoost的核心原理

XGBoost基于梯度提升机(GBDT) 框架,其核心思想是:

  1. 串行训练多个弱学习器(通常是CART树)
  2. 每个新学习器都致力于拟合前序学习器的残差(预测误差)
  3. 最终预测是所有学习器预测结果的加权和

XGBoost在GBDT基础上的关键改进:

  • 正则化:在目标函数中加入正则项,控制树的复杂度
  • 并行化:在特征粒度上实现并行计算,加速树的构建
  • 缺失值处理:自动学习缺失值的处理方式
  • 稀疏感知:对稀疏数据有专门优化
  • 自定义损失函数:支持自定义可微损失函数

2. 数学原理

XGBoost的目标函数定义为:
L(ϕ)=∑i=1nl(yi,y^i(t))+∑k=1tΩ(fk)\mathcal{L}(\phi) = \sum_{i=1}^n l(y_i, \hat{y}i^{(t)}) + \sum{k=1}^t \Omega(f_k)L(ϕ)=i=1∑nl(yi,y^i(t))+k=1∑tΩ(fk)

其中:

  • l(yi,y^i(t))l(y_i, \hat{y}_i^{(t)})l(yi,y^i(t)) 是第ttt轮的损失函数
  • Ω(fk)=γT+12λ∑j=1Twj2\Omega(f_k) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^T w_j^2Ω(fk)=γT+21λ∑j=1Twj2 是正则项,控制树的复杂度
    • TTT 是树的叶节点数量
    • wjw_jwj 是叶节点的权重
    • γ,λ\gamma, \lambdaγ,λ 是正则化参数

通过泰勒展开近似损失函数,并使用贪心算法构建每一棵树,使得目标函数最小化。

3. XGBoost与随机森林的对比

特性 XGBoost 随机森林
集成策略 Boosting(串行) Bagging(并行)
偏差/方差 低偏差,需注意控制方差 低方差,可能有较高偏差
训练效率 中高(有并行优化) 高(完全并行)
调参难度 高(参数敏感) 低(参数不敏感)
过拟合风险 中高(需严格调参) 低(增加树数量影响小)
内存占用 高(存储多棵树)
处理不平衡数据 好(支持权重) 一般(需特殊处理)

4. XGBoost的应用场景

XGBoost在以下场景中表现优异:

  • 结构化数据(表格数据)的分类和回归任务
  • 特征维度适中(10-1000)的问题
  • 对预测性能要求高的业务场景
  • 数据存在缺失值或异常值的情况

典型应用案例:

  • 信用评分和风险评估
  • 客户流失预测
  • 点击率预测(CTR)
  • 推荐系统排序
  • Kaggle等数据科学竞赛

5. PyTorch实现简化版XGBoost

虽然XGBoost有成熟的C++实现(可通过Python接口调用),我们这里实现一个简化版以理解其核心原理:

python 复制代码
import torch
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import load_boston
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error

# 注意:sklearn的boston数据集已移除,这里使用替代方法
try:
    from sklearn.datasets import fetch_california_housing
    data = fetch_california_housing()
except ImportError:
    # 备用方案
    data = None
    print("无法加载数据集,请确保scikit-learn版本正确")

if data is not None:
    X, y = data.data, data.target
    X = torch.tensor(X, dtype=torch.float32)
    y = torch.tensor(y, dtype=torch.float32)
    
    # 划分训练集和测试集
    X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)

# 定义回归树节点(用于XGBoost)
class XGBoostTreeNode:
    def __init__(self, gamma=0, lambda_reg=1):
        self.gamma = gamma  # 控制分裂的最小损失减少量
        self.lambda_reg = lambda_reg  # L2正则化参数
        self.left = None
        self.right = None
        self.feature_idx = None
        self.threshold = None
        self.weight = None  # 叶节点权重
        self.gain = 0  # 分裂增益

    def compute_weight(self, grad, hess):
        """计算叶节点权重"""
        return -torch.sum(grad) / (torch.sum(hess) + self.lambda_reg)

    def split_gain(self, left_grad, left_hess, right_grad, right_hess, total_grad, total_hess):
        """计算分裂增益"""
        gain = (torch.sum(left_grad)**2) / (torch.sum(left_hess) + self.lambda_reg) + \
               (torch.sum(right_grad)** 2) / (torch.sum(right_hess) + self.lambda_reg) - \
               (torch.sum(total_grad)**2) / (torch.sum(total_hess) + self.lambda_reg)
        return gain / 2 - self.gamma

    def find_best_split(self, X, grad, hess):
        """寻找最佳分裂点"""
        n_samples, n_features = X.shape
        best_gain = 0
        best_feature = -1
        best_threshold = None
        best_left_grad = None
        best_left_hess = None
        best_right_grad = None
        best_right_hess = None

        total_grad = torch.sum(grad)
        total_hess = torch.sum(hess)

        # 遍历每个特征
        for feature_idx in range(n_features):
            # 获取该特征的所有值
            values = X[:, feature_idx]
            unique_values = torch.unique(values)

            # 尝试每个可能的分割点
            for threshold in unique_values:
                # 分割样本
                mask = values <= threshold
                left_grad = grad[mask]
                left_hess = hess[mask]
                right_grad = grad[~mask]
                right_hess = hess[~mask]

                # 跳过样本数为0的分割
                if len(left_grad) == 0 or len(right_grad) == 0:
                    continue

                # 计算分裂增益
                gain = self.split_gain(left_grad, left_hess, right_grad, right_hess, total_grad, total_hess)

                # 更新最佳分割
                if gain > best_gain:
                    best_gain = gain
                    best_feature = feature_idx
                    best_threshold = threshold
                    best_left_grad = left_grad
                    best_left_hess = left_hess
                    best_right_grad = right_grad
                    best_right_hess = right_hess

        return (best_feature, best_threshold, best_gain,
                best_left_grad, best_left_hess, best_right_grad, best_right_hess)

    def grow(self, X, grad, hess, max_depth=3, depth=0):
        """生长树"""
        # 计算当前节点的权重
        self.weight = self.compute_weight(grad, hess)

        # 达到最大深度,停止生长
        if depth >= max_depth:
            return

        # 寻找最佳分裂
        (best_feature, best_threshold, best_gain,
         left_grad, left_hess, right_grad, right_hess) = self.find_best_split(X, grad, hess)

        # 如果增益不大于0,停止生长
        if best_gain <= 0:
            return

        # 记录分裂信息
        self.feature_idx = best_feature
        self.threshold = best_threshold
        self.gain = best_gain

        # 分裂样本
        values = X[:, best_feature]
        left_mask = values <= best_threshold

        # 递归生长左右子树
        self.left = XGBoostTreeNode(self.gamma, self.lambda_reg)
        self.right = XGBoostTreeNode(self.gamma, self.lambda_reg)
        self.left.grow(X[left_mask], left_grad, left_hess, max_depth, depth + 1)
        self.right.grow(X[~left_mask], right_grad, right_hess, max_depth, depth + 1)

    def predict_single(self, x):
        """预测单个样本"""
        if self.left is None or self.right is None:
            return self.weight

        if x[self.feature_idx] <= self.threshold:
            return self.left.predict_single(x)
        else:
            return self.right.predict_single(x)

    def predict(self, X):
        """预测多个样本"""
        return torch.tensor([self.predict_single(x) for x in X], dtype=torch.float32)

# 简化版XGBoost
class XGBoostRegressor:
    def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3,
                 gamma=0, lambda_reg=1, objective='reg:squarederror'):
        self.n_estimators = n_estimators  # 树的数量
        self.learning_rate = learning_rate  # 学习率(步长)
        self.max_depth = max_depth  # 树的最大深度
        self.gamma = gamma  # 分裂所需的最小损失减少量
        self.lambda_reg = lambda_reg  # L2正则化参数
        self.objective = objective  # 目标函数
        self.trees = []  # 存储所有树
        self.base_score = 0  # 初始预测值

    def _compute_gradient_hessian(self, y_true, y_pred):
        """计算梯度和二阶导数(Hessian)"""
        if self.objective == 'reg:squarederror':
            # 平方误差的梯度和二阶导数
            grad = y_pred - y_true  # 梯度
            hess = torch.ones_like(y_pred)  # 二阶导数为1
            return grad, hess
        else:
            raise NotImplementedError(f"目标函数 {self.objective} 尚未实现")

    def fit(self, X, y):
        """训练XGBoost"""
        # 初始化预测值(均值)
        self.base_score = torch.mean(y)
        y_pred = torch.full_like(y, self.base_score)

        # 训练每一棵树
        self.trees = []
        for _ in range(self.n_estimators):
            # 计算梯度和二阶导数
            grad, hess = self._compute_gradient_hessian(y, y_pred)

            # 训练一棵新树拟合残差
            tree = XGBoostTreeNode(self.gamma, self.lambda_reg)
            tree.grow(X, grad, hess, self.max_depth)
            self.trees.append(tree)

            # 更新预测值
            y_pred += self.learning_rate * tree.predict(X)

        return self

    def predict(self, X):
        """预测"""
        y_pred = torch.full((X.shape[0],), self.base_score, dtype=torch.float32)
        for tree in self.trees:
            y_pred += self.learning_rate * tree.predict(X)
        return y_pred

# 如果数据加载成功,训练模型
if data is not None:
    # 训练XGBoost回归器
    xgb = XGBoostRegressor(n_estimators=50, learning_rate=0.1, max_depth=3, gamma=0.1, lambda_reg=1)
    xgb.fit(X_train, y_train)

    # 预测与评估
    y_pred = xgb.predict(X_test)
    mse = mean_squared_error(y_test.numpy(), y_pred.numpy())
    print(f"XGBoost测试集MSE: {mse:.4f}")
    print(f"XGBoost测试集RMSE: {np.sqrt(mse):.4f}")

    # 可视化预测结果(取前50个样本)
    plt.figure(figsize=(10, 6))
    plt.plot(range(50), y_test[:50].numpy(), 'b-', label='真实值')
    plt.plot(range(50), y_pred[:50].numpy(), 'r--', label='预测值')
    plt.xlabel('样本索引')
    plt.ylabel('房价(单位:$100k)')
    plt.title('XGBoost预测结果(加州房价)')
    plt.legend()
    plt.grid(alpha=0.3)
    plt.savefig('xgboost_predictions.png', dpi=300)
    plt.show()

6. XGBoost关键超参数调优

XGBoost的性能高度依赖超参数调优,关键参数包括:

超参数 作用 调优建议
n_estimators 树的数量 通常100-1000,结合early stopping确定
learning_rate 学习率 通常0.01-0.3,较小的学习率需要更多的树
max_depth 树的最大深度 3-10,控制过拟合,值越大越容易过拟合
min_child_weight 子节点最小样本权重和 1-10,较大的值防止过拟合
gamma 分裂所需的最小损失减少量 0-5,较大的值防止过拟合
subsample 每棵树的样本采样比例 0.6-1.0,随机性越大越不容易过拟合
colsample_bytree 每棵树的特征采样比例 0.6-1.0,减少过拟合风险
reg_alpha L1正则化参数 0-5,用于特征选择
reg_lambda L2正则化参数 0-10,控制过拟合

调优策略:

  1. 先设置一个相对较高的学习率(如0.1),找到n_estimators的大致范围
  2. 调优max_depth、min_child_weight和gamma控制树结构
  3. 调优subsample和colsample_bytree增加随机性
  4. 调优正则化参数reg_alpha和reg_lambda
  5. 降低学习率(如0.01)并增加n_estimators,进一步优化

7. 注意事项与最佳实践

  • 特征工程:XGBoost对特征工程敏感,良好的特征可显著提升性能
  • 缺失值处理:XGBoost可自动处理缺失值,无需提前填充
  • 类别特征:需要手动编码(如独热编码或目标编码),XGBoost不会自动处理
  • 特征缩放:XGBoost不需要特征缩放,但对特征分布敏感
  • 早停策略:使用early_stopping_rounds避免过拟合,提高训练效率
  • 交叉验证:XGBoost对训练数据分布敏感,建议使用交叉验证评估性能

1.5.9 决策树适用场景判断

决策树及其集成方法(随机森林、XGBoost等)在许多场景中表现优异,但也有其适用范围。正确判断决策树是否适合特定问题,对于选择合适的算法至关重要。

1. 决策树适用的场景特征

当问题具有以下特征时,决策树及其集成方法通常是不错的选择:

(1)数据特征
  • 结构化数据:表格形式的数据(如CSV文件、数据库表)
  • 混合类型特征:同时包含数值型和类别型特征
  • 特征交互明显:特征之间存在显著的交互作用
  • 非线性关系:特征与目标变量之间存在非线性关系
(2)业务需求
  • 可解释性要求:需要理解模型决策过程和关键因素
  • 快速部署:需要简单、高效的模型部署
  • 处理缺失值:数据中存在缺失值且难以填充
  • 异常值鲁棒性:数据中存在异常值,且难以预处理
(3)计算资源
  • 有限的计算资源:决策树训练和预测速度快,资源消耗低
  • 实时预测需求:需要快速响应的预测服务

2. 决策树不适用的场景

以下场景中,决策树及其集成方法可能不是最佳选择:

(1)数据特征
  • 高维稀疏数据:如文本数据(词袋模型)、推荐系统用户-物品矩阵
  • 低维连续特征:特征少且与目标变量呈线性关系
  • 图像/音频数据:原始像素或音频波形数据(需先提取特征)
  • 时间序列数据:需要捕捉时间依赖关系的纯时间序列预测
(2)业务需求
  • 极致预测精度:在某些高维复杂问题上,深度学习可能表现更好
  • 平滑预测需求:需要预测值连续平滑变化(决策树预测是阶梯式的)
  • 严格的概率校准:需要精确的概率估计(需额外校准)

3. 不同算法的选择指南

问题类型 推荐算法 备选算法
结构化数据分类 随机森林、XGBoost 逻辑回归、SVM
结构化数据回归 XGBoost、随机森林 线性回归、神经网络
高维稀疏数据 线性模型、神经网络 带正则化的树模型
图像识别 卷积神经网络 特征工程+树模型
自然语言处理 Transformer、LSTM 词嵌入+树模型
时间序列预测 ARIMA、LSTM 带时间特征的XGBoost
异常检测 隔离森林、One-Class SVM 带异常评分的树模型
推荐系统 协同过滤、神经网络 特征工程+XGBoost

4. 决策树与其他算法的性能对比

python 复制代码
import numpy as np
import matplotlib.pyplot as plt
from sklearn.datasets import make_classification, make_regression
from sklearn.model_selection import cross_val_score
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.linear_model import LogisticRegression, LinearRegression
from sklearn.svm import SVC, SVR
import torch

# 设置中文字体
plt.rcParams["font.family"] = ["SimHei", "WenQuanYi Micro Hei", "Heiti TC"]

# 分类问题性能对比
def compare_classification_algorithms():
    # 生成分类数据
    X, y = make_classification(
        n_samples=1000, n_features=20, n_informative=10,
        n_redundant=5, random_state=42
    )
    X = torch.tensor(X, dtype=torch.float32)
    y = torch.tensor(y, dtype=torch.long)
    
    # 定义算法
    algorithms = {
        "决策树": DecisionTreeClassifier(max_depth=5),
        "随机森林": RandomForestClassifier(n_estimators=100),
        "逻辑回归": LogisticRegression(max_iter=1000),
        "SVM": SVC()
    }
    
    # 交叉验证评估
    scores = {}
    for name, clf in algorithms.items():
        cv_scores = cross_val_score(clf, X.numpy(), y.numpy(), cv=5, scoring='accuracy')
        scores[name] = cv_scores
        print(f"{name} 准确率: {cv_scores.mean():.4f} ± {cv_scores.std():.4f}")
    
    # 可视化
    plt.figure(figsize=(10, 6))
    plt.boxplot(scores.values(), labels=scores.keys())
    plt.title('不同分类算法的准确率对比')
    plt.ylabel('准确率')
    plt.grid(alpha=0.3)
    plt.savefig('classification_comparison.png', dpi=300)
    plt.show()

# 回归问题性能对比
def compare_regression_algorithms():
    # 生成回归数据
    X, y = make_regression(
        n_samples=1000, n_features=20, n_informative=10,
        noise=0.1, random_state=42
    )
    X = torch.tensor(X, dtype=torch.float32)
    y = torch.tensor(y, dtype=torch.float32)
    
    # 定义算法
    algorithms = {
        "回归树": DecisionTreeRegressor(max_depth=5),
        "随机森林回归": RandomForestRegressor(n_estimators=100),
        "线性回归": LinearRegression(),
        "SVM回归": SVR()
    }
    
    # 交叉验证评估
    scores = {}
    for name, reg in algorithms.items():
        cv_scores = cross_val_score(reg, X.numpy(), y.numpy(), cv=5, scoring='neg_mean_squared_error')
        scores[name] = -cv_scores  # 转为正数(MSE)
        print(f"{name} MSE: {scores[name].mean():.4f} ± {scores[name].std():.4f}")
    
    # 可视化
    plt.figure(figsize=(10, 6))
    plt.boxplot(scores.values(), labels=scores.keys())
    plt.title('不同回归算法的MSE对比')
    plt.ylabel('均方误差(MSE)')
    plt.grid(alpha=0.3)
    plt.savefig('regression_comparison.png', dpi=300)
    plt.show()

# 运行对比
compare_classification_algorithms()
compare_regression_algorithms()

5. 决策树选择的决策流程

  1. 数据类型判断

    • 是结构化数据?→ 考虑决策树
    • 是图像/文本/音频?→ 优先考虑深度学习
  2. 问题复杂度评估

    • 特征与目标关系简单?→ 考虑线性模型
    • 存在复杂非线性和交互?→ 考虑决策树集成
  3. 业务需求分析

    • 需高解释性?→ 考虑单个决策树或浅层集成
    • 需高精度?→ 考虑XGBoost等高级集成方法
    • 需实时预测?→ 考虑轻量级决策树或蒸馏模型
  4. 实验验证

    • 在验证集上对比不同算法性能
    • 考虑训练时间、预测速度、资源消耗等因素
    • 结合业务指标(如ROI、错误成本)做最终决策

小结

决策树是一种直观且强大的机器学习模型,通过递归分割的方式构建预测规则。其核心优势在于可解释性强、能处理非线性关系和混合类型特征。通过集成学习方法(如随机森林和XGBoost),可以显著提升决策树的性能和稳定性。

随机森林通过Bagging策略和特征随机选择,在保持决策树优势的同时,大幅降低了过拟合风险,适合作为许多问题的基准模型。XGBoost则通过梯度提升策略和精细的正则化控制,在各类结构化数据竞赛中表现优异,但需要更多的调参工作。

在实际应用中,应根据数据特征、业务需求和计算资源,综合判断是否选择决策树及其集成方法,并通过实验对比确定最优算法。决策树及其集成方法特别适合结构化数据、需要解释性和快速部署的场景,是数据科学家工具箱中的重要工具。

相关推荐
菜鸟‍9 小时前
【论文学习】2025年图像处理顶会论文
图像处理·人工智能·学习
Logintern099 小时前
【学习篇】Redis 分布式锁
redis·分布式·学习
A9better9 小时前
嵌入式开发学习日志38——stm32之看门狗
stm32·嵌入式硬件·学习
Qiuner9 小时前
【机器学习】(一)实用入门指南——如何快速搭建自己的模型
人工智能·机器学习
iceslime10 小时前
头歌Educator机器学习与数据挖掘-逻辑回归
机器学习·数据挖掘·逻辑回归
kalvin_y_liu10 小时前
PyTorch、ONNX Runtime、Hugging Face、NVIDIA Triton 和 LangChain 五个概念的关系详解
人工智能·pytorch·langchain
武子康12 小时前
AI-调查研究-96-具身智能 机器人场景测试全攻略:从极端环境到实时仿真
人工智能·深度学习·机器学习·ai·架构·系统架构·具身智能
Vizio<12 小时前
《基于 ERT 的稀疏电极机器人皮肤技术》ICRA2020论文解析
论文阅读·人工智能·学习·机器人·触觉传感器
weixin_5142218514 小时前
FDTD与matlab、python耦合
python·学习·matlab·fdtd