当前位置: 首页 > news >正文

吴恩达机器学习课程(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)vValues(a)DDvI(Dv)
其中:

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

3. 分割过程示例

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

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

分割过程:

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

4. PyTorch实现简单特征分割

import torch
import numpy as npdef 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 = -1best_threshold = Nonen_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 <= thresholdright_mask = ~left_maskleft_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 = gainbest_feature = featurebest_threshold = thresholdreturn 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=1kpilog2(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)=1i=1kpi2
其中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)=11/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. 可视化对比

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-101e10

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

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

1. 分类特征处理

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

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

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

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

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

PyTorch实现

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实现连续特征离散化
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和100bins = 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)=D1xD(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)vValues(a)DDvMSE(Dv)

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

3. 回归树的适用场景

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

典型应用场景:

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

4. 回归树的框架与实现

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_splitclass RegressionTreeNode:def __init__(self, depth=0, max_depth=5, min_samples_split=5):self.depth = depthself.max_depth = max_depthself.min_samples_split = min_samples_splitself.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 0mean = torch.mean(y)return torch.mean((y - mean) **2)def split(self, X, y):"""寻找最佳分割点"""n_samples, n_features = X.shapebest_mse_reduction = -float('inf')best_feature = -1best_threshold = None# 计算当前节点的MSEcurrent_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 <= thresholdright_mask = ~left_maskleft_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_reductionbest_feature = feature_idxbest_threshold = thresholdreturn best_feature, best_threshold, best_mse_reductiondef 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_featureself.threshold = best_threshold# 分割样本values = X[:, best_feature]left_mask = values <= best_thresholdright_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 selfdef 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=0M/2(kM)pk(1p)Mk
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. 集成学习框架对比

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

5. 多决策树融合的优势

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

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

6. 集成学习可视化

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_depthself.feature_idx = Noneself.threshold = Noneself.left = Noneself.right = Noneself.classes = Noneself.prediction = Nonedef 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 = 0self.threshold = torch.median(X[:, self.feature_idx])# 分割样本mask = X[:, self.feature_idx] <= self.thresholdself.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.thresholdy_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_estimatorsself.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() + 1y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1xx, 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实现随机森林

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 = depthself.max_depth = max_depthself.min_samples_split = min_samples_splitself.max_features = max_features  # 每次分裂时随机选择的特征数self.left = Noneself.right = Noneself.feature_idx = Noneself.threshold = Noneself.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.shapebest_gain = -float('inf')best_feature = -1best_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 <= thresholdright_mask = ~left_maskleft_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 = gainbest_feature = feature_idxbest_threshold = thresholdreturn best_feature, best_threshold, best_gaindef 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_featureself.threshold = best_threshold# 分割样本values = X[:, best_feature]left_mask = values <= best_thresholdright_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 selfdef predict_single(self, x):"""预测单个样本"""if self.left is None or self.right is None:return self.predictionif 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_featureselse: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 selfdef predict(self, X):"""预测"""# 收集所有树的预测predictions = torch.stack([tree.predict(X) for tree in self.estimators])# 多数投票return torch.mode(predictions, dim=0).valuesdef 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] += 1count_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=1nl(yi,y^i(t))+k=1tΩ(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接口调用),我们这里实现一个简化版以理解其核心原理:

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_housingdata = fetch_california_housing()
except ImportError:# 备用方案data = Noneprint("无法加载数据集,请确保scikit-learn版本正确")if data is not None:X, y = data.data, data.targetX = 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 = Noneself.right = Noneself.feature_idx = Noneself.threshold = Noneself.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.gammadef find_best_split(self, X, grad, hess):"""寻找最佳分裂点"""n_samples, n_features = X.shapebest_gain = 0best_feature = -1best_threshold = Nonebest_left_grad = Nonebest_left_hess = Nonebest_right_grad = Nonebest_right_hess = Nonetotal_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 <= thresholdleft_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 = gainbest_feature = feature_idxbest_threshold = thresholdbest_left_grad = left_gradbest_left_hess = left_hessbest_right_grad = right_gradbest_right_hess = right_hessreturn (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_featureself.threshold = best_thresholdself.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.weightif 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)  # 二阶导数为1return grad, hesselse: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 selfdef 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_alphaL1正则化参数0-5,用于特征选择
reg_lambdaL2正则化参数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. 决策树与其他算法的性能对比

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_scoresprint(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则通过梯度提升策略和精细的正则化控制,在各类结构化数据竞赛中表现优异,但需要更多的调参工作。

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

http://www.dtcms.com/a/457843.html

相关文章:

  • 网站推广营销方法成都网站建设 致尚
  • 常州网站建设网站做网站必须要加v吗
  • 理想星环 OS 深度解析:开源浪潮下的智能汽车操作系统革命
  • Qt开发的应用程序编译链接Fast DDS库
  • 网站开发人员属于旅行社手机网站建设成
  • VBA经典应用69例应用9:Preserve关键字利用,保留原值
  • WebRTC 入门与实战(三)之高级篇
  • 二:RAG 的 “语义密码”:向量、嵌入模型与 Milvus 向量数据库实操
  • 一元夺宝网站建设企业网站建设方案对比汇报
  • 《道德经》第十二章
  • 点击图片进入网站要怎么做做网站老板嫌弃太丑谁的锅
  • 如何在ubuntu20.04配置动态壁纸和bongoCat
  • 网站建设中外链与内链的技巧三视觉平面设计网
  • 小公司要不要建设网站网页打不开怎么处理
  • p2vr做的网站怎么再次打开163企业邮箱免费版
  • deepseek vs 元宝--人工智能还是人工智障?
  • 【C语言基础详细版】07. 字符串处理函数完全指南
  • 科室建设网站织梦怎么做的网站
  • 科研配色|信仰的颜色—中国红!
  • wordpress建站菜单栏 二级标题免费做问卷的网站好
  • Python print() 函数完整说明
  • 游戏攻略新闻资讯主题模板源码 YK一点资讯模版 Zblog主题模版(源码下载)
  • 计算机网络资源网站建设论文网站空间 推荐
  • 自助建站的平台wordpress 文章循环
  • Photoshop - Photoshop 工具栏(9)裁剪工具
  • 【练】C程序设计-01程序设计和C语言
  • 使用 Gunicorn 部署 FastAPI 应用程序:快速而强大的组合
  • JavaScript是web开发中一种功能强大的编程语言,
  • 网站建设策划实施要素无锡常州网络推广
  • 东莞建外贸网站龙华专业做网站公司