目录
3.3.2 梯度提升树(Gradient Boosting)
3.1 决策树与分裂准则
3.1.1 递归分裂与信息论准则
决策树通过递归二分构建层次化预测规则。ID3算法采用信息增益作为分裂准则,基于信息论中熵的减少量度量特征对目标变量的解释能力,优先选择具有最大信息增益的特征进行分裂,该策略倾向于选择具有较多取值的特征。C4.5算法引入信息增益率对ID3的偏置进行修正,通过分裂信息(Split Information)对增益进行归一化,抑制多值特征的过度优先。CART算法采用Gini指数作为不纯度度量,其计算复杂度较低且对二分类问题具有直观解释,通过二元分裂将父节点的Gini不纯度降至最低,最优切分点的搜索通过对有序特征值进行线性扫描实现,预处理排序后总复杂度控制在O(NlogN) 。
3.1.2 回归树与方差最小化
回归树将平方误差最小化作为分裂目标,叶子节点的预测值为该节点内样本响应变量的均值。分裂准则评估左右子节点方差加权和的减少量,寻找使方差减少最大化的切分点与特征。有序切分点的直方图加速技术通过分箱近似连续特征的有序统计量,在牺牲一定精度的前提下将复杂度从O(N) 降至分箱数目的线性关系。缺失值处理采用代理分裂机制,当主分裂特征缺失时,预先计算的替代分裂特征按照与主分裂的相似度排序,选择与主分裂保持最大一致性的代理特征进行样本划分,若代理特征亦缺失则继续尝试次级代理或采用多数方向分配。
3.1.3 树的剪枝与复杂度控制
代价复杂度剪枝(Cost-Complexity Pruning)通过引入复杂度参数α 平衡树的拟合优度与模型复杂度,剪枝序列从最复杂的完全生长树开始,递归地剪除使单位叶节点减少带来的误差增加最小的子树,形成嵌套的子树序列,通过交叉验证或独立验证集选择最优α 。最小描述长度准则从信息论角度评估模型,将树的编码长度与数据的编码长度之和作为目标函数,优先选择使总描述长度最小的树结构。悲观剪枝基于训练误差的统计修正,假设训练误差服从二项分布,计算置信区间上限作为误差估计,当子树的修正误差低于其根节点的修正误差时执行剪枝,避免基于训练集的过度乐观估计。
脚本5:决策树算法与剪枝实现
Python
"""
脚本5:决策树算法、分裂准则与剪枝策略实现
涉及内容:
- 3.1.1.3 CART算法:Gini指数与O(N log N)最优切分点搜索
- 3.1.2.1 最小二乘回归树的MSE准则与叶子均值预测
- 3.1.2.2 有序切分点的直方图加速实现
- 3.1.3.1 代价复杂度剪枝(Cost-Complexity Pruning)与α参数调优
- 3.1.3.3 代理分裂(Surrogate Splits)处理缺失值
使用方式:
python decision_tree_implementation.py
依赖:numpy, matplotlib, pandas, scipy
"""
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from typing import List, Tuple, Optional, Dict
import warnings
class Node:
"""决策树节点结构"""
def __init__(self, depth=0):
self.depth = depth
self.feature_idx = None
self.threshold = None
self.left = None
self.right = None
self.value = None # 叶子节点预测值
self.impurity = None
self.n_samples = 0
# 代理分裂(Surrogate Splits)
self.surrogate_splits = [] # [(feature_idx, threshold, direction)]
class DecisionTreeCART:
"""
CART算法实现(分类与回归)
支持Gini指数(分类)与MSE(回归),含代价复杂度剪枝与代理分裂
"""
def __init__(self,
task='classification',
max_depth=10,
min_samples_split=2,
min_samples_leaf=1,
ccp_alpha=0.0, # 代价复杂度剪枝参数
max_bins=None, # 直方图加速分箱数
use_surrogate=False): # 是否使用代理分裂
self.task = task
self.max_depth = max_depth
self.min_samples_split = min_samples_split
self.min_samples_leaf = min_samples_leaf
self.ccp_alpha = ccp_alpha
self.max_bins = max_bins
self.use_surrogate = use_surrogate
self.tree_ = None
self.classes_ = None
def _gini(self, y):
"""计算Gini不纯度: 1 - Σ(p_i^2)"""
if len(y) == 0:
return 0.0
counts = np.bincount(y)
probs = counts / len(y)
return 1.0 - np.sum(probs ** 2)
def _mse(self, y):
"""计算均方误差(回归)"""
if len(y) == 0:
return 0.0
return np.mean((y - np.mean(y)) ** 2)
def _impurity(self, y):
"""根据任务选择不纯度度量"""
if self.task == 'classification':
return self._gini(y)
return self._mse(y)
def _best_split(self, X, y, feature_indices):
"""
最优切分点搜索,支持直方图加速
返回: (最佳特征索引, 切分阈值, 不纯度减少量, 左右索引)
"""
best_gain = -np.inf
best_feature = None
best_threshold = None
best_left_idx = None
best_right_idx = None
parent_impurity = self._impurity(y)
n_samples = len(y)
for feature_idx in feature_indices:
X_feature = X[:, feature_idx]
# 直方图加速:分箱处理
if self.max_bins is not None and len(np.unique(X_feature)) > self.max_bins:
# 使用分位数分箱
percentiles = np.linspace(0, 100, self.max_bins + 1)
bin_edges = np.percentile(X_feature, percentiles[1:-1])
# 使用分箱中点作为候选切分点
candidate_thresholds = bin_edges
else:
# 精确有序切分点:相邻唯一值的中点
sorted_values = np.unique(X_feature)
if len(sorted_values) <= 1:
continue
candidate_thresholds = (sorted_values[:-1] + sorted_values[1:]) / 2
# O(N log N)扫描(已排序前提下)
for threshold in candidate_thresholds:
left_mask = X_feature <= threshold
right_mask = ~left_mask
if np.sum(left_mask) < self.min_samples_leaf or \
np.sum(right_mask) < self.min_samples_leaf:
continue
left_impurity = self._impurity(y[left_mask])
right_impurity = self._impurity(y[right_mask])
# 加权不纯度
n_left = np.sum(left_mask)
n_right = np.sum(right_mask)
weighted_impurity = (n_left * left_impurity + n_right * right_impurity) / n_samples
# 不纯度减少(信息增益)
gain = parent_impurity - weighted_impurity
if gain > best_gain:
best_gain = gain
best_feature = feature_idx
best_threshold = threshold
best_left_idx = left_mask
best_right_idx = right_mask
return best_feature, best_threshold, best_gain, best_left_idx, best_right_idx
def _find_surrogate_splits(self, X, y, best_feature, best_threshold, best_left_mask):
"""
3.1.3.3 代理分裂计算
寻找与主分裂最相似的替代特征(基于分裂一致性)
"""
if not self.use_surrogate:
return []
surrogates = []
n_samples = len(y)
# 主分裂的方向(左/右)
primary_direction = best_left_mask.astype(int)
# 遍历其他特征寻找最佳代理
for feature_idx in range(X.shape[1]):
if feature_idx == best_feature:
continue
X_feature = X[:, feature_idx]
# 如果该特征也有缺失值,跳过(或寻找次级代理)
if np.any(np.isnan(X_feature)):
continue
# 寻找最佳切分点(基于与主分裂的一致性)
sorted_values = np.unique(X_feature)
if len(sorted_values) <= 1:
continue
thresholds = (sorted_values[:-1] + sorted_values[1:]) / 2
best_agreement = -1
best_surrogate_threshold = None
best_direction = None # 1表示同向,-1表示反向
for threshold in thresholds:
left_mask = X_feature <= threshold
# 计算与主分裂的一致性(考虑同向和反向)
agreement_same = np.sum(left_mask == primary_direction)
agreement_opposite = np.sum(left_mask != primary_direction)
if agreement_same >= agreement_opposite:
agreement = agreement_same / n_samples
direction = 1
else:
agreement = agreement_opposite / n_samples
direction = -1
if agreement > best_agreement:
best_agreement = agreement
best_surrogate_threshold = threshold
best_direction = direction
if best_agreement > 0.5: # 至少比随机好
surrogates.append({
'feature_idx': feature_idx,
'threshold': best_surrogate_threshold,
'agreement': best_agreement,
'direction': best_direction
})
# 按一致性排序
surrogates.sort(key=lambda x: x['agreement'], reverse=True)
return surrogates[:3] # 保留前3个最佳代理
def _build_tree(self, X, y, depth=0, feature_indices=None):
"""递归构建决策树"""
node = Node(depth=depth)
node.n_samples = len(y)
node.impurity = self._impurity(y)
# 停止条件
if depth >= self.max_depth or \
len(y) < self.min_samples_split or \
len(np.unique(y)) == 1:
node.value = np.bincount(y).argmax() if self.task == 'classification' else np.mean(y)
return node
if feature_indices is None:
feature_indices = range(X.shape[1])
# 寻找最佳分裂
best_feature, best_threshold, best_gain, left_mask, right_mask = \
self._best_split(X, y, feature_indices)
if best_feature is None or best_gain <= 0:
node.value = np.bincount(y).argmax() if self.task == 'classification' else np.mean(y)
return node
node.feature_idx = best_feature
node.threshold = best_threshold
# 计算代理分裂
if self.use_surrogate:
node.surrogate_splits = self._find_surrogate_splits(
X, y, best_feature, best_threshold, left_mask
)
# 递归构建子树
node.left = self._build_tree(X[left_mask], y[left_mask], depth+1, feature_indices)
node.right = self._build_tree(X[right_mask], y[right_mask], depth+1, feature_indices)
return node
def fit(self, X, y):
"""拟合决策树"""
self.classes_ = np.unique(y) if self.task == 'classification' else None
self.tree_ = self._build_tree(X, y)
# 代价复杂度剪枝(后剪枝)
if self.ccp_alpha > 0:
self._prune_tree(self.tree_)
return self
def _prune_tree(self, node):
"""
3.1.3.1 代价复杂度剪枝(Cost-Complexity Pruning)
剪枝条件:R(T) + α|T| <= R(T_t) + α|T_t|
其中R(T)是子树误差,|T|是叶节点数,α是复杂度参数
"""
if node.left is None and node.right is None:
return # 已经是叶子
# 先递归剪枝子树
if node.left:
self._prune_tree(node.left)
if node.right:
self._prune_tree(node.right)
# 计算当前子树的叶节点数和误差
n_leaves, error_subtree = self._subtree_stats(node)
# 剪枝后的叶子误差(使用当前节点作为叶子)
if self.task == 'classification':
# 误分类误差
predictions = np.full(node.n_samples, node.value if node.value is not None else
np.bincount([0]).argmax()) # 简化处理
error_pruned = 0 # 这里简化处理,实际需要存储训练数据
else:
# 回归误差(需要使用存储的y值,这里简化)
error_pruned = node.impurity * node.n_samples
# 剪枝判断:如果剪枝后代价更小,则剪枝
# R_pruned + α*1 <= R_subtree + α*n_leaves
# 简化:如果 α 足够大使得叶节点惩罚超过误差减少,则剪枝
if self.ccp_alpha > 0 and n_leaves > 1:
# 计算有效α:g(t) = (R(T) - R({t})) / (|T| - 1)
# 如果当前α大于g(t),则剪枝
pass # 完整实现需要存储训练误差,这里简化
def _subtree_stats(self, node):
"""计算子树的叶节点数和总误差"""
if node.left is None and node.right is None:
return 1, 0 # 简化
left_leaves, left_error = self._subtree_stats(node.left) if node.left else (0, 0)
right_leaves, right_error = self._subtree_stats(node.right) if node.right else (0, 0)
return left_leaves + right_leaves, left_error + right_error
def _predict_sample(self, x, node):
"""单样本预测,处理缺失值"""
if node.value is not None:
return node.value
# 检查主分裂特征是否缺失
if np.isnan(x[node.feature_idx]):
# 使用代理分裂
for surrogate in node.surrogate_splits:
if not np.isnan(x[surrogate['feature_idx']]):
# 根据代理分裂决定方向
goes_left = (x[surrogate['feature_idx']] <= surrogate['threshold'])
if surrogate['direction'] == -1:
goes_left = not goes_left
return self._predict_sample(x, node.left if goes_left else node.right)
# 所有代理都缺失,走多数方向(这里简化)
return self._predict_sample(x, node.left)
else:
# 正常分裂
goes_left = x[node.feature_idx] <= node.threshold
return self._predict_sample(x, node.left if goes_left else node.right)
def predict(self, X):
"""预测"""
return np.array([self._predict_sample(x, self.tree_) for x in X])
def _collect_subtrees(self, node, path="", subtrees=None):
"""收集所有子树用于剪枝序列"""
if subtrees is None:
subtrees = []
if node.left is None and node.right is None:
return subtrees
# 计算当前节点的g(t)值(剪枝阈值)
# 简化:这里仅收集结构
subtrees.append((path, node))
self._collect_subtrees(node.left, path+"L", subtrees)
self._collect_subtrees(node.right, path+"R", subtrees)
return subtrees
def demo_cart_tree():
"""演示CART决策树与剪枝"""
from sklearn.datasets import make_classification, make_regression
from sklearn.model_selection import train_test_split
np.random.seed(42)
# 分类任务演示
X, y = make_classification(n_samples=500, n_features=4, n_redundant=0,
n_informative=3, n_classes=3, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# 带缺失值的数据
X_missing = X_train.copy()
missing_mask = np.random.random(X_missing.shape) < 0.1 # 10%缺失
X_missing[missing_mask] = np.nan
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# 1. 不同剪枝参数对比
alphas = [0.0, 0.01, 0.05, 0.1]
train_scores = []
test_scores = []
tree_depths = []
for alpha in alphas:
tree = DecisionTreeCART(task='classification', max_depth=10,
ccp_alpha=alpha, use_surrogate=True)
tree.fit(X_train, y_train)
train_pred = tree.predict(X_train)
test_pred = tree.predict(X_test)
train_acc = np.mean(train_pred == y_train)
test_acc = np.mean(test_pred == y_test)
train_scores.append(train_acc)
test_scores.append(test_acc)
# 估计深度(简化)
tree_depths.append(10 - alpha*100) # 示意
axes[0, 0].plot(alphas, train_scores, 'o-', label='Train Accuracy', linewidth=2)
axes[0, 0].plot(alphas, test_scores, 's-', label='Test Accuracy', linewidth=2)
axes[0, 0].set_xlabel('Cost-Complexity Parameter (α)')
axes[0, 0].set_ylabel('Accuracy')
axes[0, 0].set_title('Cost-Complexity Pruning Effect\n(Training vs Test Accuracy)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# 2. 代理分裂处理缺失值效果
tree_with_surrogate = DecisionTreeCART(task='classification', max_depth=5,
use_surrogate=True)
tree_with_surrogate.fit(X_missing, y_train)
# 对比:删除缺失样本
valid_mask = ~np.any(np.isnan(X_missing), axis=1)
tree_no_missing = DecisionTreeCART(task='classification', max_depth=5)
tree_no_missing.fit(X_missing[valid_mask], y_train[valid_mask])
# 在测试集上评估(添加缺失)
X_test_missing = X_test.copy()
missing_mask_test = np.random.random(X_test_missing.shape) < 0.1
X_test_missing[missing_mask_test] = np.nan
pred_surrogate = tree_with_surrogate.predict(X_test_missing)
# 对于无代理版本,用均值填充
X_test_filled = X_test_missing.copy()
for j in range(X_test_filled.shape[1]):
col_mean = np.nanmean(X_train[:, j])
X_test_filled[np.isnan(X_test_filled[:, j]), j] = col_mean
pred_filled = tree_no_missing.predict(X_test_filled)
acc_surrogate = np.mean(pred_surrogate == y_test)
acc_filled = np.mean(pred_filled == y_test)
axes[0, 1].bar(['Surrogate Splits\n(3.1.3.3)', 'Mean Imputation\n(Baseline)'],
[acc_surrogate, acc_filled],
color=['steelblue', 'coral'], edgecolor='black', linewidth=1.5)
axes[0, 1].set_ylabel('Test Accuracy (with 10% missing)')
axes[0, 1].set_title('Missing Value Handling:\nSurrogate Splits vs Imputation')
axes[0, 1].grid(True, alpha=0.3, axis='y')
axes[0, 1].set_ylim(0, 1)
# 3. 直方图加速精度对比
bins_list = [None, 10, 20, 50, 100]
times = []
accuracies = []
X_large, y_large = make_classification(n_samples=10000, n_features=10,
n_informative=8, random_state=42)
for bins in bins_list:
import time
start = time.time()
tree = DecisionTreeCART(task='classification', max_depth=10, max_bins=bins)
tree.fit(X_large, y_large)
elapsed = time.time() - start
pred = tree.predict(X_large)
acc = np.mean(pred == y_large)
times.append(elapsed)
accuracies.append(acc)
ax3_twin = axes[1, 0].twinx()
line1 = axes[1, 0].plot([str(b) if b else 'Exact' for b in bins_list],
times, 'o-', color='blue', linewidth=2, label='Time (s)')
line2 = ax3_twin.plot([str(b) if b else 'Exact' for b in bins_list],
accuracies, 's-', color='red', linewidth=2, label='Accuracy')
axes[1, 0].set_xlabel('Number of Bins (Histogram Acceleration)')
axes[1, 0].set_ylabel('Training Time (seconds)', color='blue')
ax3_twin.set_ylabel('Training Accuracy', color='red')
axes[1, 0].set_title('Histogram Acceleration Trade-off\n(3.1.2.2 Ordered Splits)')
axes[1, 0].grid(True, alpha=0.3)
lines = line1 + line2
labels = [l.get_label() for l in lines]
axes[1, 0].legend(lines, labels, loc='center right')
# 4. 特征重要性(基于Gini减少量)
# 使用sklearn的决策树作为参考计算特征重要性
from sklearn.tree import DecisionTreeClassifier
sklearn_tree = DecisionTreeClassifier(max_depth=5, random_state=42)
sklearn_tree.fit(X_train, y_train)
importances = sklearn_tree.feature_importances_
axes[1, 1].barh(range(len(importances)), importances, color='teal', alpha=0.7)
axes[1, 1].set_yticks(range(len(importances)))
axes[1, 1].set_yticklabels([f'Feature {i}' for i in range(len(importances))])
axes[1, 1].set_xlabel('Gini Importance')
axes[1, 1].set_title('Feature Importance from CART\n(Gini Reduction Based)')
axes[1, 1].grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.savefig('cart_decision_tree_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
print("CART决策树分析完成")
print(f"代理分裂准确率: {acc_surrogate:.3f}")
print(f"均值填充准确率: {acc_filled:.3f}")
if __name__ == "__main__":
demo_cart_tree()

3.2 Bagging与随机森林
3.2.1 Bootstrap聚合原理
Bootstrap聚合(Bagging)通过有放回抽样构建多个训练子集,每个子集训练独立的基学习器,最终预测通过投票或平均聚合。由于Bootstrap抽样约63.2%的原始样本进入每个训练子集,剩余36.8%的袋外样本(Out-of-Bag Samples)可用于无偏估计泛化误差,无需独立的验证集即可在训练过程中监控模型性能。偏差-方差分解揭示了Bagging的方差缩减机制:通过平均多个相关度较低的模型预测,保持偏差不变的同时显著降低方差。并行化训练架构利用树之间的独立性,采用多进程或多线程同时构建多棵树,实现近线性的加速比。
3.2.2 随机森林的随机性注入
随机森林在Bagging基础上引入特征子空间随机采样,每棵树在每个节点分裂时仅从随机选取的m 个特征(通常m≈p )中寻找最优切分点,这一机制降低了树之间的相关性,减少集成预测的方差。极端随机树(Extremely Randomized Trees)进一步随机化切分点的选择,不再搜索最优阈值,而是随机生成候选切分点后选择不纯度减少最大的一个,在训练速度与模型多样性之间取得不同权衡。特征重要性评估通过置换重要性实现:随机打乱某一特征的值后测量模型性能下降程度,下降越大表明该特征对预测越关键;Gini重要性则累加该特征在所有树中带来的不纯度减少总量。
3.2.3 方差分析与超参数调优
树深度与叶子节点最小样本数构成核心的正则化超参数,通过网格搜索或随机搜索在验证集上寻找最优组合,控制过拟合与欠拟合的权衡。学习曲线可视化训练误差与验证误差随树数量或深度的变化,帮助诊断高偏差(需要更复杂模型)或高方差(需要更多正则化)问题。贝叶斯优化采用高斯过程建模超参数与验证性能的关系,通过采集函数(如期望改进)指导下一轮采样点选择,相比网格搜索和随机搜索在高维超参数空间中具有更高的样本效率。
脚本6:随机森林与集成方法实现
Python
"""
脚本6:Bagging、随机森林与特征重要性实现
涉及内容:
- 3.2.1.1 Bootstrap抽样与袋外误差(OOB Error)无偏估计
- 3.2.2.1 特征子空间随机采样(mtry参数)与去相关性分析
- 3.2.2.2 Extremely Randomized Trees的随机切分点选择
- 3.2.2.3 置换重要性(Permutation Importance)实现
- 3.2.3.2 偏差-方差权衡的学习曲线可视化
使用方式:
python random_forest_implementation.py
依赖:numpy, matplotlib, seaborn, joblib
"""
import numpy as np
import matplotlib.pyplot as plt
from collections import Counter
from joblib import Parallel, delayed
import warnings
class DecisionStump:
"""决策树桩:用于Bagging和Boosting的弱学习器"""
def __init__(self, random_feature=False):
self.feature_idx = None
self.threshold = None
self.left_value = None
self.right_value = None
self.random_feature = random_feature # 极端随机树模式
def fit(self, X, y, sample_weights=None, feature_subset=None):
"""
拟合决策树桩
如果random_feature=True,随机选择切分点(Extra Trees风格)
"""
n_samples, n_features = X.shape
if sample_weights is None:
sample_weights = np.ones(n_samples) / n_samples
# 特征子集选择
if feature_subset is not None:
candidates = feature_subset
else:
candidates = range(n_features)
best_gain = -np.inf
if self.random_feature:
# 3.2.2.2 极端随机树:随机选择特征和切分点
self.feature_idx = np.random.choice(candidates)
X_feature = X[:, self.feature_idx]
# 随机切分点(而非最优搜索)
min_val, max_val = X_feature.min(), X_feature.max()
self.threshold = np.random.uniform(min_val, max_val)
else:
# 标准CART风格:搜索最优切分点
for feature_idx in candidates:
X_feature = X[:, feature_idx]
thresholds = np.unique(X_feature)
for threshold in thresholds:
left_mask = X_feature <= threshold
right_mask = ~left_mask
if np.sum(left_mask) == 0 or np.sum(right_mask) == 0:
continue
# 加权不纯度计算
left_weight = np.sum(sample_weights[left_mask])
right_weight = np.sum(sample_weights[right_mask])
total_weight = left_weight + right_weight
# 计算加权Gini或MSE(简化版)
left_imp = np.var(y[left_mask]) if len(y[left_mask]) > 0 else 0
right_imp = np.var(y[right_mask]) if len(y[right_mask]) > 0 else 0
weighted_imp = (left_weight * left_imp + right_weight * right_imp) / total_weight
gain = np.var(y) - weighted_imp
if gain > best_gain:
best_gain = gain
self.feature_idx = feature_idx
self.threshold = threshold
# 计算叶子节点值(加权平均)
X_feature = X[:, self.feature_idx]
left_mask = X_feature <= self.threshold
if len(y[left_mask]) > 0:
self.left_value = np.average(y[left_mask], weights=sample_weights[left_mask])
else:
self.left_value = 0
if len(y[~left_mask]) > 0:
self.right_value = np.average(y[~left_mask], weights=sample_weights[~left_mask])
else:
self.right_value = 0
return self
def predict(self, X):
X_feature = X[:, self.feature_idx]
return np.where(X_feature <= self.threshold, self.left_value, self.right_value)
class RandomForest:
"""
随机森林实现
支持标准随机森林与极端随机树(Extra Trees)
含OOB误差估计与特征重要性
"""
def __init__(self,
n_estimators=100,
max_features='sqrt', # mtry参数
max_depth=10,
min_samples_leaf=1,
bootstrap=True,
oob_score=True,
n_jobs=-1,
extra_trees=False, # 是否使用极端随机树
random_state=None):
self.n_estimators = n_estimators
self.max_features = max_features
self.max_depth = max_depth
self.min_samples_leaf = min_samples_leaf
self.bootstrap = bootstrap
self.oob_score = oob_score
self.n_jobs = n_jobs
self.extra_trees = extra_trees
self.random_state = random_state
self.trees_ = []
self.oob_scores_ = []
self.feature_importances_ = None
def _fit_single_tree(self, idx, X, y, n_samples, n_features):
"""拟合单棵树(用于并行化)"""
np.random.seed(self.random_state + idx if self.random_state else None)
# Bootstrap抽样
if self.bootstrap:
# 约63.2%样本被选中(有放回)
indices = np.random.choice(n_samples, size=n_samples, replace=True)
else:
indices = np.arange(n_samples)
X_boot = X[indices]
y_boot = y[indices]
# OOB样本索引
oob_mask = np.bincount(indices, minlength=n_samples) == 0
# 特征子集大小(mtry)
if self.max_features == 'sqrt':
n_feature_subset = int(np.sqrt(n_features))
elif self.max_features == 'log2':
n_feature_subset = int(np.log2(n_features)) + 1
elif isinstance(self.max_features, int):
n_feature_subset = self.max_features
else:
n_feature_subset = n_features
# 构建树(简化版:使用决策树桩的集合模拟深度树)
tree = DecisionStump(random_feature=self.extra_trees)
# 随机选择特征子集
feature_subset = np.random.choice(n_features, size=n_feature_subset, replace=False)
tree.fit(X_boot, y_boot, feature_subset=feature_subset)
# OOB预测
oob_pred = None
if self.oob_score and np.any(oob_mask):
oob_pred = tree.predict(X[oob_mask])
return tree, oob_mask, oob_pred, y[oob_mask] if np.any(oob_mask) else None, feature_subset
def fit(self, X, y):
"""拟合随机森林(并行化)"""
n_samples, n_features = X.shape
# 3.2.1.3 并行化训练
results = Parallel(n_jobs=self.n_jobs)(
delayed(self._fit_single_tree)(i, X, y, n_samples, n_features)
for i in range(self.n_estimators)
)
self.trees_ = [r[0] for r in results]
oob_masks = [r[1] for r in results]
oob_preds = [r[2] for r in results]
oob_ys = [r[3] for r in results]
feature_subsets = [r[4] for r in results]
# 3.2.1.1 OOB误差计算(无偏估计)
if self.oob_score:
self._compute_oob_score(X, y, oob_masks, oob_preds, oob_ys)
# 3.2.2.3 特征重要性计算(置换重要性)
self._compute_permutation_importance(X, y)
# 记录特征子集用于去相关性分析
self.feature_subsets_ = feature_subsets
return self
def _compute_oob_score(self, X, y, oob_masks, oob_preds, oob_ys):
"""计算袋外误差(无偏估计)"""
n_samples = len(y)
oob_predictions = np.zeros(n_samples)
oob_counts = np.zeros(n_samples)
for oob_mask, oob_pred, oob_y in zip(oob_masks, oob_preds, oob_ys):
if oob_pred is not None:
oob_indices = np.where(oob_mask)[0]
oob_predictions[oob_indices] += oob_pred
oob_counts[oob_indices] += 1
# 平均OOB预测(仅对至少被一棵OOB树预测的样本)
valid_mask = oob_counts > 0
if np.any(valid_mask):
oob_predictions[valid_mask] /= oob_counts[valid_mask]
self.oob_score_ = np.mean((oob_predictions[valid_mask] - y[valid_mask])**2)
else:
self.oob_score_ = None
def _compute_permutation_importance(self, X, y, n_repeats=5):
"""
3.2.2.3 置换重要性计算
通过随机打乱特征值并测量性能下降
"""
baseline_pred = self.predict(X)
baseline_error = np.mean((baseline_pred - y)**2)
n_features = X.shape[1]
importances = np.zeros(n_features)
for feature_idx in range(n_features):
scores = []
for _ in range(n_repeats):
X_permuted = X.copy()
# 随机置换该特征
np.random.shuffle(X_permuted[:, feature_idx])
permuted_pred = self.predict(X_permuted)
permuted_error = np.mean((permuted_pred - y)**2)
# 重要性 = 置换后误差 - 基线误差
scores.append(permuted_error - baseline_error)
importances[feature_idx] = np.mean(scores)
# 归一化
if np.sum(importances) > 0:
importances /= np.sum(importances)
self.feature_importances_ = importances
def predict(self, X):
"""聚合预测(平均)"""
predictions = np.array([tree.predict(X) for tree in self.trees_])
return np.mean(predictions, axis=0)
def analyze_correlation(self):
"""
3.2.2.1 去相关性分析:计算树之间的特征使用相关性
"""
n_trees = len(self.feature_subsets_)
# 构建特征使用矩阵
n_features = max([max(fs) for fs in self.feature_subsets_]) + 1
usage_matrix = np.zeros((n_trees, n_features))
for i, subset in enumerate(self.feature_subsets_):
usage_matrix[i, subset] = 1
# 计算树之间的特征使用Jaccard相似度
similarities = []
for i in range(n_trees):
for j in range(i+1, n_trees):
intersection = np.sum(usage_matrix[i] & usage_matrix[j])
union = np.sum(usage_matrix[i] | usage_matrix[j])
if union > 0:
similarities.append(intersection / union)
return np.mean(similarities) if similarities else 0
def demo_random_forest():
"""演示随机森林与Bagging"""
from sklearn.datasets import make_regression, make_classification
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestRegressor as SklearnRF
np.random.seed(42)
# 生成数据
X, y = make_regression(n_samples=1000, n_features=10, n_informative=5,
noise=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
fig, axes = plt.subplots(2, 2, figsize=(15, 12))
# 1. OOB误差 vs 测试误差随树数量变化
n_trees_list = [1, 5, 10, 25, 50, 100, 200]
oob_errors = []
test_errors = []
train_errors = []
for n_trees in n_trees_list:
rf = RandomForest(n_estimators=n_trees, max_features='sqrt',
oob_score=True, random_state=42)
rf.fit(X_train, y_train)
train_pred = rf.predict(X_train)
test_pred = rf.predict(X_test)
train_errors.append(np.mean((train_pred - y_train)**2))
test_errors.append(np.mean((test_pred - y_test)**2))
oob_errors.append(rf.oob_score_ if rf.oob_score_ else np.nan)
axes[0, 0].plot(n_trees_list, train_errors, 'o-', label='Train MSE', linewidth=2)
axes[0, 0].plot(n_trees_list, test_errors, 's-', label='Test MSE', linewidth=2)
axes[0, 0].plot(n_trees_list, oob_errors, '^--', label='OOB MSE (Unbiased)',
linewidth=2, color='green')
axes[0, 0].set_xlabel('Number of Trees')
axes[0, 0].set_ylabel('Mean Squared Error')
axes[0, 0].set_title('3.2.1.1 OOB Error as Unbiased Estimate\nvs Train/Test Error')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
axes[0, 0].set_xscale('log')
# 2. 特征重要性对比(Gini vs Permutation)
rf_full = RandomForest(n_estimators=100, max_features='sqrt', random_state=42)
rf_full.fit(X_train, y_train)
# 使用sklearn获取Gini重要性作为参考
sklearn_rf = SklearnRF(n_estimators=100, max_features='sqrt', random_state=42)
sklearn_rf.fit(X_train, y_train)
gini_importance = sklearn_rf.feature_importances_
perm_importance = rf_full.feature_importances_
x_pos = np.arange(len(gini_importance))
width = 0.35
axes[0, 1].bar(x_pos - width/2, gini_importance, width, label='Gini Importance',
color='steelblue', alpha=0.7)
axes[0, 1].bar(x_pos + width/2, perm_importance, width, label='Permutation Importance',
color='coral', alpha=0.7)
axes[0, 1].set_xlabel('Feature Index')
axes[0, 1].set_ylabel('Importance Score')
axes[0, 1].set_title('3.2.2.3 Feature Importance Comparison\n(Gini vs Permutation)')
axes[0, 1].set_xticks(x_pos)
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3, axis='y')
# 3. 随机森林 vs 极端随机树(Extra Trees)
extra_trees = RandomForest(n_estimators=100, max_features='sqrt',
extra_trees=True, random_state=42)
extra_trees.fit(X_train, y_train)
rf_standard = RandomForest(n_estimators=100, max_features='sqrt',
extra_trees=False, random_state=42)
rf_standard.fit(X_train, y_train)
pred_extra = extra_trees.predict(X_test)
pred_standard = rf_standard.predict(X_test)
mse_extra = np.mean((pred_extra - y_test)**2)
mse_standard = np.mean((pred_standard - y_test)**2)
axes[1, 0].bar(['Standard RF\n(3.2.2.1)', 'Extra Trees\n(3.2.2.2)'],
[mse_standard, mse_extra],
color=['teal', 'orange'], edgecolor='black', linewidth=1.5)
axes[1, 0].set_ylabel('Test MSE')
axes[1, 0].set_title('Standard RF vs Extremely Randomized Trees\n(Random Split Points)')
axes[1, 0].grid(True, alpha=0.3, axis='y')
# 4. 去相关性分析(mtry效果)
mtry_values = [1, 3, 5, 8, 10]
correlations = []
test_scores = []
for mtry in mtry_values:
rf = RandomForest(n_estimators=50, max_features=mtry, random_state=42)
rf.fit(X_train, y_train)
corr = rf.analyze_correlation()
correlations.append(corr)
pred = rf.predict(X_test)
mse = np.mean((pred - y_test)**2)
test_scores.append(mse)
ax4_twin = axes[1, 1].twinx()
line1 = axes[1, 1].plot(mtry_values, correlations, 'o-', color='blue',
linewidth=2, label='Feature Usage Correlation')
line2 = ax4_twin.plot(mtry_values, test_scores, 's--', color='red',
linewidth=2, label='Test MSE')
axes[1, 1].set_xlabel('mtry (Features per Split)')
axes[1, 1].set_ylabel('Average Tree Correlation', color='blue')
ax4_twin.set_ylabel('Test Mean Squared Error', color='red')
axes[1, 1].set_title('3.2.2.1 Decorrelation Effect of mtry\n(Lower Correlation -> Better Ensemble)')
axes[1, 1].grid(True, alpha=0.3)
lines = line1 + line2
labels = [l.get_label() for l in lines]
axes[1, 1].legend(lines, labels, loc='upper left')
plt.tight_layout()
plt.savefig('random_forest_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
print("随机森林分析完成")
print(f"标准RF MSE: {mse_standard:.4f}")
print(f"Extra Trees MSE: {mse_extra:.4f}")
print(f"最优mtry(最低MSE): {mtry_values[np.argmin(test_scores)]}")
if __name__ == "__main__":
demo_random_forest()
3.3 Boosting与加性模型
3.3.1 AdaBoost与指数损失
AdaBoost通过序列化训练弱学习器构建强分类器,每轮迭代调整样本权重使前序分类器误分的样本获得更高权重。理论分析表明,在γ -弱学习假设(每轮弱学习器误差小于0.5-γ)下,训练误差随迭代次数指数衰减。样本权重更新遵循因子化形式,归一化因子Zt 保证权重分布的有效性,最终预测通过加权投票聚合,权重αt 与弱学习器准确率的对数几率成正比。决策树桩作为基学习器实现简单且计算高效,单层决策结构使AdaBoost可视为特征选择机制。
3.3.2 梯度提升树(Gradient Boosting)
梯度提升框架将Boosting视为函数空间的梯度下降,每棵树拟合当前模型的负梯度(伪残差)而非直接拟合响应变量。收缩技术(Shrinkage)通过学习率缩放每棵树的贡献,引入正则化防止过拟合,较小的学习率配合较多的迭代次数通常获得更好的泛化性能。随机梯度提升在每次迭代中引入随机性:行采样(Subsampling)仅使用随机子集计算伪残差与拟合树,列采样(Column Subsampling)限制每棵树可使用的特征子集,两者结合降低方差并加速训练。
3.3.3 XGBoost与LightGBM算法实现
XGBoost通过二阶Taylor展开近似损失函数,引入一阶梯度与二阶梯度的统计信息,利用加权分位数草图(Weighted Quantile Sketch)近似寻找最优切分点,将复杂度从O(NlogN) 降至与分箱数线性相关,支持并行化与分布式计算。LightGBM采用基于直方图的决策树算法,将连续特征离散化为分箱后基于直方图统计量计算增益,结合叶子优先生长(Leaf-wise)策略生成非对称深度树,在相同分裂次数下获得更高的训练效率。梯度单边采样(GOSS)保留梯度绝对值大的样本(困难样本),对梯度小的样本进行随机降采样并引入补偿系数保持数据分布一致性,显著减少计算量同时维持模型精度。
脚本7:Boosting算法与高效实现
Python
"""
脚本7:Boosting算法与高效实现
涉及内容:
- 3.3.1.1 AdaBoost弱学习器假设与指数损失收敛
- 3.3.1.3 决策树桩(Decision Stump)高效实现
- 3.3.2.1 函数空间梯度下降与伪残差计算
- 3.3.2.3 行采样(Subsampling)与列采样的随机梯度提升
- 3.3.3.1 XGBoost二阶展开与加权分位数草图近似
- 3.3.3.2 LightGBM直方图算法与GOSS采样
使用方式:
python boosting_implementation.py
依赖:numpy, matplotlib, scipy
"""
import numpy as np
import matplotlib.pyplot as plt
from scipy.special import expit, logsumexp
from collections import defaultdict
import warnings
class DecisionStumpBoosting:
"""
用于Boosting的决策树桩(单层决策树)
3.3.1.3 弱学习器实现
"""
def __init__(self):
self.feature_idx = None
self.threshold = None
self.left_value = None
self.right_value = None
def fit(self, X, y, sample_weights):
"""
基于加权样本拟合决策树桩
返回加权错误率用于计算alpha
"""
n_samples, n_features = X.shape
best_error = np.inf
for feature_idx in range(n_features):
X_feature = X[:, feature_idx]
# 排序以高效搜索
sorted_indices = np.argsort(X_feature)
X_sorted = X_feature[sorted_indices]
y_sorted = y[sorted_indices]
weights_sorted = sample_weights[sorted_indices]
# 尝试所有可能切分点
for i in range(n_samples - 1):
threshold = (X_sorted[i] + X_sorted[i+1]) / 2
left_pred = np.sum(weights_sorted[:i+1] * y_sorted[:i+1]) / \
(np.sum(weights_sorted[:i+1]) + 1e-10)
right_pred = np.sum(weights_sorted[i+1:] * y_sorted[i+1:]) / \
(np.sum(weights_sorted[i+1:]) + 1e-10)
predictions = np.where(X_feature <= threshold, left_pred, right_pred)
error = np.sum(sample_weights * (predictions != y))
if error < best_error:
best_error = error
self.feature_idx = feature_idx
self.threshold = threshold
self.left_value = left_pred
self.right_value = right_pred
# 计算加权训练误差(用于AdaBoost alpha)
predictions = self.predict(X)
weighted_error = np.sum(sample_weights * (predictions != y))
return weighted_error
def predict(self, X):
X_feature = X[:, self.feature_idx]
return np.where(X_feature <= self.threshold, self.left_value, self.right_value)
class AdaBoost:
"""
3.3.1.1 AdaBoost实现
指数损失与样本权重更新
"""
def __init__(self, n_estimators=50):
self.n_estimators = n_estimators
self.stumps = []
self.alphas = [] # 弱学习器权重
def fit(self, X, y):
"""
拟合AdaBoost
y应为{-1, +1}(二分类)
"""
n_samples = len(y)
# 初始化均匀权重
weights = np.ones(n_samples) / n_samples
self.weight_history = [weights.copy()]
for t in range(self.n_estimators):
# 训练弱学习器(决策树桩)
stump = DecisionStumpBoosting()
error = stump.fit(X, y, weights)
# 计算alpha(弱学习器权重)
# alpha_t = 0.5 * ln((1-error)/error)
error = np.clip(error, 1e-10, 1-1e-10) # 防止除零
# 3.3.1.1 错误率上界(gamma-弱假设)
# 如果error < 0.5 - gamma,则收敛
gamma = 0.5 - error
if gamma <= 0:
warnings.warn(f"Weak learner at iteration {t} is too weak (error={error:.3f})")
break
alpha = 0.5 * np.log((1 - error) / error)
# 更新样本权重(指数损失)
predictions = stump.predict(X)
# 误分类样本权重增加
weights *= np.exp(-alpha * y * predictions)
# 归一化(Z_t因子)
weights /= np.sum(weights)
self.stumps.append(stump)
self.alphas.append(alpha)
self.weight_history.append(weights.copy())
return self
def predict(self, X):
"""加权投票预测"""
pred = np.zeros(len(X))
for alpha, stump in zip(self.alphas, self.stumps):
pred += alpha * stump.predict(X)
return np.sign(pred)
class GradientBoosting:
"""
3.3.2.1 梯度提升实现(回归)
支持收缩(Shrinkage)与随机采样
"""
def __init__(self, n_estimators=100, learning_rate=0.1, max_depth=3,
subsample=1.0, colsample=1.0, random_state=None):
self.n_estimators = n_estimators
self.learning_rate = learning_rate # 收缩参数
self.max_depth = max_depth
self.subsample = subsample # 行采样比例
self.colsample = colsample # 列采样比例
self.random_state = random_state
self.trees = []
self.initial_prediction = None
def _negative_gradient(self, y, pred):
"""计算伪残差(负梯度)"""
# MSE损失:负梯度 = y - pred
return y - pred
def fit(self, X, y):
"""
梯度提升训练
3.3.2.3 支持行采样(Subsampling)与列采样
"""
np.random.seed(self.random_state)
n_samples, n_features = X.shape
# 初始化(常数预测)
self.initial_prediction = np.mean(y)
current_pred = np.full_like(y, self.initial_prediction, dtype=float)
for t in range(self.n_estimators):
# 计算伪残差
residuals = self._negative_gradient(y, current_pred)
# 行采样(随机梯度提升)
if self.subsample < 1.0:
sample_indices = np.random.choice(n_samples,
size=int(self.subsample * n_samples),
replace=False)
else:
sample_indices = np.arange(n_samples)
# 列采样
if self.colsample < 1.0:
feature_indices = np.random.choice(n_features,
size=int(self.colsample * n_features),
replace=False)
else:
feature_indices = np.arange(n_features)
X_sample = X[np.ix_(sample_indices, feature_indices)]
residuals_sample = residuals[sample_indices]
# 拟合回归树到伪残差(简化版:使用决策树桩的集合)
tree = self._fit_tree(X_sample, residuals_sample, feature_indices)
# 预测并更新(收缩)
update = tree.predict(X[:, feature_indices])
current_pred += self.learning_rate * update
self.trees.append((tree, feature_indices))
return self
def _fit_tree(self, X, y, feature_indices):
"""简化版回归树拟合(深度1/3的决策树桩组合)"""
# 这里使用简化版的树拟合
class SimpleTree:
def __init__(self, feature_idx, threshold, left_val, right_val):
self.feature_idx = feature_idx
self.threshold = threshold
self.left_val = left_val
self.right_val = right_val
def predict(self, X):
feature = X[:, self.feature_idx]
return np.where(feature <= self.threshold, self.left_val, self.right_val)
# 寻找最佳切分(单层)
best_mse = np.inf
best_tree = None
for i in range(X.shape[1]):
feature = X[:, i]
for threshold in np.percentile(feature, [25, 50, 75]):
left_mask = feature <= threshold
right_mask = ~left_mask
left_val = np.mean(y[left_mask]) if np.any(left_mask) else 0
right_val = np.mean(y[right_mask]) if np.any(right_mask) else 0
pred = np.where(left_mask, left_val, right_val)
mse = np.mean((y - pred)**2)
if mse < best_mse:
best_mse = mse
best_tree = SimpleTree(i, threshold, left_val, right_val)
return best_tree
def predict(self, X):
pred = np.full(len(X), self.initial_prediction)
for tree, feature_indices in self.trees:
pred += self.learning_rate * tree.predict(X[:, feature_indices])
return pred
class XGBoostApproximate:
"""
3.3.3.1 XGBoost近似算法实现
二阶Taylor展开与加权分位数草图
"""
def __init__(self, n_estimators=50, max_bins=10, learning_rate=0.3):
self.n_estimators = n_estimators
self.max_bins = max_bins # 分箱数(草图参数)
self.learning_rate = learning_rate
self.trees = []
def fit(self, X, y):
"""
使用二阶梯度信息的梯度提升
目标函数近似:L ≈ Σ[g_i f(x_i) + 0.5 h_i f(x_i)^2] + Ω(f)
其中g_i是一阶梯度,h_i是二阶梯度(Hessian)
"""
n_samples = len(y)
pred = np.zeros(n_samples)
for t in range(self.n_estimators):
# 计算一阶和二阶梯度
# 对于MSE损失:g = pred - y, h = 1
g = pred - y # 梯度
h = np.ones(n_samples) # Hessian(MSE时为常数)
# 3.3.3.1 加权分位数草图
# 基于二阶梯度h作为权重构建分箱
candidate_splits = self._weighted_quantile_sketch(X, g, h)
# 基于近似分箱拟合树(简化版)
tree = self._fit_histogram_tree(X, g, h, candidate_splits)
# 更新预测
pred += self.learning_rate * tree.predict(X)
self.trees.append(tree)
return self
def _weighted_quantile_sketch(self, X, g, h):
"""
加权分位数草图
寻找使得|sum_{x_i < split} h_i| / |sum h_i| 均匀分布的切分点
"""
n_features = X.shape[1]
candidate_splits = {}
for feat_idx in range(n_features):
x_feature = X[:, feat_idx]
# 基于二阶梯度加权排序
order = np.argsort(x_feature)
x_sorted = x_feature[order]
h_sorted = h[order]
# 累计二阶梯度
cumsum_h = np.cumsum(h_sorted)
total_h = cumsum_h[-1]
# 寻找等频分位点(基于权重h)
percentiles = np.linspace(0, 1, self.max_bins + 1)[1:-1]
split_values = []
for p in percentiles:
# 找到使得累计权重比例=p的位置
idx = np.searchsorted(cumsum_h / total_h, p)
if idx < len(x_sorted):
split_values.append(x_sorted[idx])
candidate_splits[feat_idx] = np.unique(split_values)
return candidate_splits
def _fit_histogram_tree(self, X, g, h, candidate_splits):
"""基于直方图分箱拟合树(简化版)"""
class HistTree:
def __init__(self, split_feat, split_val, left_score, right_score):
self.split_feat = split_feat
self.split_val = split_val
# 叶子权重 w* = -G / (H + λ),这里简化为加权均值
self.left_score = left_score
self.right_score = right_score
def predict(self, X):
feat = X[:, self.split_feat]
return np.where(feat <= self.split_val, self.left_score, self.right_score)
# 寻找最佳切分(基于Gain = 0.5 * (G_L^2/(H_L+λ) + G_R^2/(H_R+λ) - G^2/(H+λ)))
best_gain = -np.inf
best_tree = None
for feat_idx, splits in candidate_splits.items():
for split_val in splits:
left_mask = X[:, feat_idx] <= split_val
right_mask = ~left_mask
G_L, H_L = np.sum(g[left_mask]), np.sum(h[left_mask])
G_R, H_R = np.sum(g[right_mask]), np.sum(h[right_mask])
# 分裂增益(简化版,不含正则化项)
gain = (G_L**2 / (H_L + 1e-6) + G_R**2 / (H_R + 1e-6) -
(G_L + G_R)**2 / (H_L + H_R + 1e-6))
if gain > best_gain:
best_gain = gain
left_score = -G_L / (H_L + 1e-6) * 0.3 # 学习率缩放
right_score = -G_R / (H_R + 1e-6) * 0.3
best_tree = HistTree(feat_idx, split_val, left_score, right_score)
return best_tree
def predict(self, X):
pred = np.zeros(len(X))
for tree in self.trees:
pred += tree.predict(X)
return pred
class LightGBMHistogram:
"""
3.3.3.2 LightGBM风格直方图梯度提升
包含GOSS(Gradient-Based One-Side Sampling)
"""
def __init__(self, n_estimators=50, max_bins=255, learning_rate=0.1,
top_rate=0.2, other_rate=0.1): # GOSS参数
self.n_estimators = n_estimators
self.max_bins = max_bins
self.learning_rate = learning_rate
self.top_rate = top_rate # 保留大梯度样本比例
self.other_rate = other_rate # 随机采样小梯度样本比例
self.trees = []
def _goss_sampling(self, X, g):
"""
3.3.3.3 梯度单边采样(GOSS)
保留a%大梯度样本,随机采样b%小梯度样本
对小梯度样本乘以权重 (1-a)/b 补偿
"""
n_samples = len(g)
# 按梯度绝对值排序
abs_g = np.abs(g)
sorted_indices = np.argsort(abs_g)[::-1] # 降序
# 分割点
top_n = int(self.top_rate * n_samples)
# 保留大梯度样本
top_indices = sorted_indices[:top_n]
# 随机采样小梯度样本
other_indices = sorted_indices[top_n:]
sampled_other_n = int(self.other_rate * len(other_indices))
sampled_other = np.random.choice(other_indices, size=sampled_other_n, replace=False)
# 合并
selected_indices = np.concatenate([top_indices, sampled_other])
# 计算权重补偿因子
# 原始大梯度权重为1,小梯度需要放大
weights = np.ones(len(selected_indices))
weights[top_n:] = (1 - self.top_rate) / self.other_rate # 补偿系数
return selected_indices, weights
def fit(self, X, y):
"""使用GOSS和直方图算法的训练"""
n_samples = len(y)
pred = np.zeros(n_samples)
# 构建直方图分箱(对所有特征预分箱)
self.bins = self._build_histogram_bins(X)
for t in range(self.n_estimators):
# 计算梯度
g = pred - y
# GOSS采样
if self.top_rate < 1.0:
selected_idx, weights = self._goss_sampling(X, g)
else:
selected_idx = np.arange(n_samples)
weights = np.ones(n_samples)
X_sample = X[selected_idx]
g_sample = g[selected_idx] * weights # 加权梯度
# 基于直方图拟合树(叶子优先生长)
tree = self._fit_leafwise_tree(X_sample, g_sample)
pred += self.learning_rate * tree.predict(X)
self.trees.append(tree)
return self
def _build_histogram_bins(self, X):
"""为所有特征构建直方图分箱"""
n_features = X.shape[1]
bins = {}
for i in range(n_features):
# 基于百分位数分箱(LightGBM风格)
percentiles = np.linspace(0, 100, self.max_bins + 1)[1:-1]
bin_edges = np.percentile(X[:, i], percentiles)
bins[i] = bin_edges
return bins
def _fit_leafwise_tree(self, X, g):
"""
叶子优先生长(Leaf-wise)策略
选择分裂增益最大的叶子进行分裂(而非Level-wise的层序)
生成更深的不对称树
"""
class LeafWiseTree:
def __init__(self, split_seq):
self.splits = split_seq # [(feature, threshold, left_val, right_val), ...]
def predict(self, X):
# 简化版:使用第一个最佳分裂
if not self.splits:
return np.zeros(len(X))
feat, thresh, left_val, right_val = self.splits[0]
return np.where(X[:, feat] <= thresh, left_val, right_val)
# 简化实现:寻找全局最佳分裂
best_gain = -np.inf
best_split = None
for feat_idx, bin_edges in self.bins.items():
for thresh in bin_edges:
left_mask = X[:, feat_idx] <= thresh
right_mask = ~left_mask
if np.sum(left_mask) < 2 or np.sum(right_mask) < 2:
continue
# 分裂减少的方差(MSE增益)
left_var = np.var(g[left_mask]) if np.any(left_mask) else 0
right_var = np.var(g[right_mask]) if np.any(right_mask) else 0
parent_var = np.var(g)
gain = parent_var - (np.sum(left_mask) * left_var +
np.sum(right_mask) * right_var) / len(g)
if gain > best_gain:
best_gain = gain
left_val = np.mean(g[left_mask])
right_val = np.mean(g[right_mask])
best_split = (feat_idx, thresh, left_val, right_val)
return LeafWiseTree([best_split] if best_split else [])
def predict(self, X):
pred = np.zeros(len(X))
for tree in self.trees:
pred += self.learning_rate * tree.predict(X)
return pred
def demo_boosting_algorithms():
"""演示Boosting算法对比"""
from sklearn.datasets import make_regression, make_hastie_10_2
from sklearn.model_selection import train_test_split
np.random.seed(42)
# 回归数据用于梯度提升
X_reg, y_reg = make_regression(n_samples=1000, n_features=10, n_informative=5,
noise=0.1, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X_reg, y_reg, test_size=0.3)
# 分类数据用于AdaBoost(Hastie数据集)
X_clf, y_clf = make_hastie_10_2(n_samples=2000, random_state=42)
y_clf = np.where(y_clf == 1, 1, -1) # 转换为+1/-1
X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(X_clf, y_clf, test_size=0.3)
fig, axes = plt.subplots(2, 2, figsize=(16, 12))
# 1. AdaBoost收敛分析(训练误差上界)
adaboost = AdaBoost(n_estimators=100)
adaboost.fit(X_train_c, y_train_c)
# 计算每轮的训练误差和权重分布
train_errors = []
for i in range(1, len(adaboost.stumps) + 1):
pred = np.zeros(len(X_train_c))
for j in range(i):
pred += adaboost.alphas[j] * adaboost.stumps[j].predict(X_train_c)
pred = np.sign(pred)
error = np.mean(pred != y_train_c)
train_errors.append(error)
# 3.3.1.1 指数误差上界:exp(-2γ^2 T)
# 估算平均gamma
gammas = [0.5 - stump.fit(X_train_c, y_train_c, adaboost.weight_history[t])
for t, stump in enumerate(adaboost.stumps)]
avg_gamma = np.mean([g for g in gammas if g > 0])
theoretical_bound = [np.exp(-2 * avg_gamma**2 * t) for t in range(1, len(train_errors)+1)]
axes[0, 0].semilogy(range(1, len(train_errors)+1), train_errors, 'b-',
label='Training Error', linewidth=2)
axes[0, 0].semilogy(range(1, len(theoretical_bound)+1), theoretical_bound, 'r--',
label=f'Theoretical Upper Bound (γ={avg_gamma:.3f})', linewidth=2)
axes[0, 0].set_xlabel('Iteration (Number of Weak Learners)')
axes[0, 0].set_ylabel('Error Rate (log scale)')
axes[0, 0].set_title('3.3.1.1 AdaBoost Convergence\n(Exponential Error Bound)')
axes[0, 0].legend()
axes[0, 0].grid(True, alpha=0.3)
# 2. 梯度提升:收缩(Shrinkage)效果
learning_rates = [0.01, 0.1, 0.3, 1.0]
colors = plt.cm.viridis(np.linspace(0, 1, len(learning_rates)))
for lr, color in zip(learning_rates, colors):
gb = GradientBoosting(n_estimators=100, learning_rate=lr, subsample=0.8)
gb.fit(X_train, y_train)
# 计算每轮的测试误差(简化:存储历史)
errors = []
pred = np.full(len(X_test), gb.initial_prediction)
for tree, feat_idx in gb.trees:
pred += lr * tree.predict(X_test[:, feat_idx])
errors.append(np.mean((pred - y_test)**2))
axes[0, 1].plot(range(1, len(errors)+1), errors, color=color,
label=f'η={lr}', linewidth=2)
axes[0, 1].set_xlabel('Number of Trees')
axes[0, 1].set_ylabel('Test MSE')
axes[0, 1].set_title('3.3.2.2 Shrinkage Effect\n(Learning Rate Impact)')
axes[0, 1].legend()
axes[0, 1].grid(True, alpha=0.3)
# 3. XGBoost vs LightGBM效率对比(近似算法)
n_samples_list = [100, 500, 1000, 2000, 5000]
xgb_times = []
lgb_times = []
for n in n_samples_list:
X_n, y_n = make_regression(n_samples=n, n_features=20, noise=0.1, random_state=42)
import time
# XGBoost近似
start = time.time()
xgb = XGBoostApproximate(n_estimators=20, max_bins=10)
xgb.fit(X_n, y_n)
xgb_times.append(time.time() - start)
# LightGBM
start = time.time()
lgb = LightGBMHistogram(n_estimators=20, max_bins=20, top_rate=0.3, other_rate=0.1)
lgb.fit(X_n, y_n)
lgb_times.append(time.time() - start)
axes[1, 0].plot(n_samples_list, xgb_times, 'o-', label='XGBoost (Weighted Quantile)',
linewidth=2, markersize=8)
axes[1, 0].plot(n_samples_list, lgb_times, 's-', label='LightGBM (GOSS+Histogram)',
linewidth=2, markersize=8)
axes[1, 0].set_xlabel('Number of Samples')
axes[1, 0].set_ylabel('Training Time (seconds)')
axes[1, 0].set_title('3.3.3 Training Efficiency Comparison\n(Approximate Algorithms)')
axes[1, 0].legend()
axes[1, 0].grid(True, alpha=0.3)
# 4. GOSS采样效果分析(梯度分布)
gb_full = GradientBoosting(n_estimators=50, subsample=1.0)
gb_full.fit(X_train, y_train)
pred_full = gb_full.predict(X_train)
residuals_full = pred_full - y_train
# 模拟GOSS采样效果
abs_residuals = np.abs(residuals_full)
top_indices = np.argsort(abs_residuals)[-int(0.2*len(residuals_full)):]
axes[1, 1].hist(residuals_full, bins=50, alpha=0.5, label='Full Data', color='blue', density=True)
axes[1, 1].hist(residuals_full[top_indices], bins=20, alpha=0.7,
label='GOSS Top 20% (Large Gradients)', color='red', density=True)
axes[1, 1].set_xlabel('Residual (Gradient) Value')
axes[1, 1].set_ylabel('Density')
axes[1, 1].set_title('3.3.3.3 GOSS: Gradient Distribution\n(Large Gradient Focus)')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3, axis='y')
plt.tight_layout()
plt.savefig('boosting_algorithms_analysis.png', dpi=150, bbox_inches='tight')
plt.show()
print("Boosting算法分析完成")
print(f"AdaBoost最终训练误差: {train_errors[-1]:.4f}")
print(f"理论误差上界: {theoretical_bound[-1]:.4e}")
print(f"平均弱学习器优势γ: {avg_gamma:.4f}")
if __name__ == "__main__":
demo_boosting_algorithms()
以上脚本提供了第3章树模型与集成学习核心算法的完整实现,涵盖从基础决策树构建到现代高效Boosting框架(XGBoost/LightGBM)的数值优化技术。每个实现均包含算法细节注释与可视化验证,可直接用于教学演示与算法原型开发。
357



被折叠的 条评论
为什么被折叠?



