吴恩达机器学习课程(PyTorch适配)学习笔记:1.5 决策树与集成学习
决策树是一种直观且易于解释的机器学习模型,而集成学习通过组合多个决策树能够显著提升模型性能。本文详细讲解决策树的原理、学习过程、纯度指标,以及随机森林、XGBoost等集成学习方法,并提供PyTorch实现示例。
1.5.1 决策树模型(结构 + 原理)
决策树(Decision Tree)是一种基于树状结构进行决策的模型,其核心思想是通过一系列"if-then"规则对数据进行分类或回归。
1. 基本结构
决策树由三种节点组成:
- 根节点:整个决策树的起点,包含所有样本
- 内部节点:表示一个特征测试,每个分支代表一个测试结果
- 叶节点:表示最终的决策结果(分类标签或回归值)
2. 工作原理
决策树通过自顶向下递归分割的方式工作:
- 从根节点开始,选择最优特征对样本进行分割
- 对每个子节点重复分割过程,直到满足停止条件
- 每个叶节点对应一个决策结果
3. 决策树的优势与不足
优势 | 不足 |
---|---|
直观易懂,可解释性强 | 容易过拟合(尤其是深度较大的树) |
无需特征归一化/标准化 | 对噪声敏感 |
能处理混合类型特征 | 可能产生偏斜树(某些路径特别长) |
训练过程快速 | 不稳定性(小的数据集变化可能导致树结构巨变) |
4. 决策树的类型
- 分类树:叶节点为离散的类别标签(如"是否违约"、“疾病类型”)
- 回归树:叶节点为连续的数值(如"房价"、“温度”)
- 多输出树:可同时预测多个目标变量
1.5.2 决策树学习过程(纯度测量 + 特征分割)
决策树的学习过程本质是寻找最优分割特征和分割点的过程,核心目标是使分割后的子节点"纯度"更高(即样本更相似)。
1. 学习过程四步曲
- 特征选择:从所有可用特征中选择一个最优特征作为当前节点的分割特征
- 分割点确定:为选定的特征确定最佳分割点(分类特征:每个取值为一个分割点;连续特征:寻找最优阈值)
- 节点分裂:根据选定的特征和分割点将当前节点分裂为子节点
- 停止条件:当满足以下任一条件时停止分裂
- 节点中所有样本属于同一类别(分类树)或预测值差异小于阈值(回归树)
- 节点包含的样本数小于最小分裂样本数
- 树的深度达到预设最大值
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. 分割过程示例
假设我们有以下关于"是否购买电脑"的数据集:
年龄 | 收入 | 学生 | 信用评级 | 购买电脑 |
---|---|---|---|---|
青年 | 高 | 否 | 一般 | 否 |
青年 | 高 | 否 | 好 | 否 |
中年 | 高 | 否 | 一般 | 是 |
老年 | 中 | 否 | 一般 | 是 |
老年 | 低 | 是 | 一般 | 是 |
老年 | 低 | 是 | 好 | 否 |
中年 | 低 | 是 | 好 | 是 |
青年 | 中 | 否 | 一般 | 否 |
青年 | 低 | 是 | 一般 | 是 |
老年 | 中 | 是 | 一般 | 是 |
分割过程:
- 计算根节点的不纯度(假设用熵)
- 分别计算每个特征(年龄、收入、学生、信用评级)的信息增益
- 选择信息增益最大的特征作为根节点的分割特征
- 对每个子节点重复上述过程
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=1kpilog2(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)=log2(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. 可视化对比
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实现:
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)常用离散化方法
- 等宽离散化:将特征值范围等分为k个区间
- 等频离散化:将特征值分为k个区间,每个区间包含相同数量的样本
- 基于决策树的离散化:使用决策树自动找到最优分割点
- 聚类离散化:使用聚类算法(如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. 特征处理注意事项
- 避免数据泄露:特征处理的统计量(如均值、分位数)必须仅基于训练集计算,再应用到验证集和测试集
- 高基数特征处理:类别数超过100的特征称为高基数特征,不适合独热编码,可考虑目标编码或嵌入(Embedding)
- 缺失值处理:决策树可以将缺失值作为一个单独的类别处理,或用中位数/众数填充
- 特征缩放:决策树对特征缩放不敏感,不需要进行标准化或归一化处理
- 特征选择:冗余特征会增加计算复杂度并可能导致过拟合,可通过特征重要性评分进行筛选
4. 常见错误与解决方案
错误 | 解决方案 |
---|---|
对名义特征使用标签编码 | 使用独热编码或目标编码 |
对高基数特征使用独热编码 | 使用目标编码或嵌入技术 |
离散化区间划分不当 | 结合领域知识或使用基于决策树的自动离散化 |
特征处理时使用了测试集数据 | 严格遵循"先分割数据,再进行特征处理"的原则 |
1.5.5 回归树(适用场景 + 框架)
回归树(Regression Tree)是决策树的一种变体,用于解决连续值预测问题,其叶节点存储的是预测值而非类别标签。
1. 回归树的基本原理
与分类树的主要区别:
- 预测目标:连续数值(如房价、温度、销售额)
- 分裂标准:使用均方误差(MSE)或平均绝对误差(MAE)而非熵或基尼系数
- 叶节点输出:该节点所有样本的平均值(或中位数)
2. 分裂标准:均方误差(MSE)
回归树使用均方误差减少量(Reduction in MSE)作为分裂标准:
-
节点的均方误差:
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中所有样本的平均值。 -
分裂后的均方误差减少量:
Δ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. 回归树的框架与实现
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. 正则化策略(防止过拟合)
- 限制树的深度:设置
max_depth
参数 - 最小分裂样本数:设置
min_samples_split
,当节点样本数少于该值时不分裂 - 最小叶节点样本数:设置
min_samples_leaf
,确保叶节点有足够样本 - 最大叶节点数:限制叶节点总数
- 后剪枝:先构建完整树,再移除对性能提升不大的分支
7. 注意事项
- 回归树的预测值范围不会超过训练数据中目标变量的范围
- 对于时间序列数据,需要特别注意分割方式,避免使用未来信息
- 当特征维度远大于样本数时,回归树容易过拟合
- 回归树的预测结果是阶梯函数形式,适合捕捉突变关系而非渐变关系
1.5.6 集成学习(多决策树融合思路)
集成学习(Ensemble Learning)通过组合多个弱学习器(通常是决策树)的预测结果,来获得比单个学习器更好的性能。其核心思想是"三个臭皮匠,顶个诸葛亮"。
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. 集成学习可视化
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. 集成学习调优方向
- 基础模型多样性:使用不同类型的模型或不同超参数的同类型模型
- 集成策略:尝试不同的组合方式(投票、平均、加权等)
- 正则化:对基础模型添加正则化约束,防止过拟合
- 模型数量:通过交叉验证找到性能最佳的模型数量
- 并行化:对Bagging等可并行的集成方法,利用多核CPU或GPU加速训练
1.5.7 随机森林(算法框架 + PyTorch 适配)
随机森林(Random Forest)是Bagging集成方法的典型代表,通过组合多个决策树的预测结果来提高性能,同时引入了特征随机性进一步增强模型多样性。
1. 随机森林的核心思想
随机森林在Bagging的基础上增加了特征随机选择:
- 对每个决策树,使用bootstrap抽样生成不同的训练集
- 在每个节点分裂时,仅从随机选择的部分特征中寻找最优分割
- 最终预测通过所有树的投票(分类)或平均(回归)得到
这种双重随机性(样本随机+特征随机)使得随机森林比单一决策树和普通Bagging具有更好的泛化能力。
2. 算法框架
-
样本随机采样:
- 对每个树,从原始数据中有放回地随机采样NNN个样本(bootstrap抽样)
- 每个样本被选中的概率约为63.2%,未被选中的样本组成"袋外样本"(OOB,Out-of-Bag)
-
特征随机选择:
- 在每个节点分裂时,从MMM个特征中随机选择mmm个特征(通常m=Mm = \sqrt{M}m=M)
- 仅使用这mmm个特征来确定最佳分割点
-
树的构建:
- 每个树都尽可能生长(不剪枝)
- 树的数量通常在100-1000之间
-
预测组合:
- 分类:多数投票(每个树一票,得票最多的类别为最终预测)
- 回归:平均值(所有树的预测值的平均)
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样本评估性能 |
调优策略:
- 先调整n_estimators到合理范围(如100)
- 调整max_depth和min_samples_split控制树结构
- 调整max_features控制特征随机性
- 最后微调n_estimators
6. 随机森林的适用场景
- 分类问题:欺诈检测、客户流失预测、疾病诊断等
- 回归问题:房价预测、销售额预测、风险评估等
- 特征选择:通过特征重要性识别关键特征
- 异常检测:利用OOB误差识别异常样本
7. 注意事项与常见错误
- 类别不平衡:随机森林在类别不平衡数据上可能倾向于多数类,需使用class_weight参数
- 高基数类别特征:对类别数很多的特征,随机森林可能需要更多的树才能捕捉模式
- 特征缩放:随机森林不需要特征缩放,但对特征单位敏感
- 过度调参:随机森林对超参数通常不敏感,轻微调整不会显著影响性能
- 解释性限制:虽然随机森林提供特征重要性,但不如单个决策树直观
1.5.8 XGBoost(原理 + 应用场景)
XGBoost(Extreme Gradient Boosting)是一种高效的梯度提升算法,通过优化的工程实现和正则化策略,在各类机器学习竞赛中表现优异,成为数据科学领域的重要工具。
1. XGBoost的核心原理
XGBoost基于梯度提升机(GBDT) 框架,其核心思想是:
- 串行训练多个弱学习器(通常是CART树)
- 每个新学习器都致力于拟合前序学习器的残差(预测误差)
- 最终预测是所有学习器预测结果的加权和
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接口调用),我们这里实现一个简化版以理解其核心原理:
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_alpha | L1正则化参数 | 0-5,用于特征选择 |
reg_lambda | L2正则化参数 | 0-10,控制过拟合 |
调优策略:
- 先设置一个相对较高的学习率(如0.1),找到n_estimators的大致范围
- 调优max_depth、min_child_weight和gamma控制树结构
- 调优subsample和colsample_bytree增加随机性
- 调优正则化参数reg_alpha和reg_lambda
- 降低学习率(如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. 决策树选择的决策流程
-
数据类型判断:
- 是结构化数据?→ 考虑决策树
- 是图像/文本/音频?→ 优先考虑深度学习
-
问题复杂度评估:
- 特征与目标关系简单?→ 考虑线性模型
- 存在复杂非线性和交互?→ 考虑决策树集成
-
业务需求分析:
- 需高解释性?→ 考虑单个决策树或浅层集成
- 需高精度?→ 考虑XGBoost等高级集成方法
- 需实时预测?→ 考虑轻量级决策树或蒸馏模型
-
实验验证:
- 在验证集上对比不同算法性能
- 考虑训练时间、预测速度、资源消耗等因素
- 结合业务指标(如ROI、错误成本)做最终决策
小结
决策树是一种直观且强大的机器学习模型,通过递归分割的方式构建预测规则。其核心优势在于可解释性强、能处理非线性关系和混合类型特征。通过集成学习方法(如随机森林和XGBoost),可以显著提升决策树的性能和稳定性。
随机森林通过Bagging策略和特征随机选择,在保持决策树优势的同时,大幅降低了过拟合风险,适合作为许多问题的基准模型。XGBoost则通过梯度提升策略和精细的正则化控制,在各类结构化数据竞赛中表现优异,但需要更多的调参工作。
在实际应用中,应根据数据特征、业务需求和计算资源,综合判断是否选择决策树及其集成方法,并通过实验对比确定最优算法。决策树及其集成方法特别适合结构化数据、需要解释性和快速部署的场景,是数据科学家工具箱中的重要工具。