# 勤奋的凯尔森同学10000h刻意练习Day8:决策树与集成学习 —— 从一棵树到随机森林与Boosting

所属专栏: 凯尔森的10000h刻意练习
Slogan: 一个36岁程序员, 研究生(学硕),前世界500强top20软件开发Java工程师,用10000小时深耕AI, 目标Top10 AI资深工程师, 全程日更记录。
tips: 文章末尾附有jupyter完整代码

如果一棵决策树能把训练集切到100% 准确,为什么我们还需要随机森林?

答案有点反直觉:正因为一棵树太"聪明",它会把训练数据原样记住。今天这篇就沿着"一棵树 → 一片森林 → Boosting → Stacking"这条演进线,把这个问题彻底讲透。

昨天(Day7)的逻辑回归只能画一条直线决策边界,但现实数据的边界往往是弯的。今天进入决策树与集成学习——决策树通过一系列if-else问题把数据切分成越来越纯的子集,不需要特征缩放、可解释性强,但单棵树容易过拟合;随机森林用"多棵树投票"解决过拟合,是Kaggle常客;Boosting系列串行纠正错误,GBDT是工业级库XGBoost/LightGBM的理论基础。

# 1.1 一棵树是怎么"问"出结论的:决策树原理

# 1.1.1 一棵树在做什么

决策树的本质是递归地把数据集切分成更纯的子集。每次切分选一个特征和一个阈值,让切分后的子集尽量"纯"(同一类别的样本占比高)。

它真正迷人的地方,不是能处理非线性,而是几乎把模型的决策过程直接摊在你面前——每个if-else都看得懂、画得出、讲得清,这是少有的能跟业务方"对话"的模型。

根节点: 所有样本
  ├─ 特征A ≤ 阈值? → 是 → 子集1(比较纯)
  │                    └─ 特征B ≤ 阈值? → ...
  └─ 否 → 子集2(比较纯)
           └─ ...
1
2
3
4
5

叶子节点的预测值:分类任务取该节点样本的众数,回归任务取均值。

# 1.1.2 怎么选切分特征:不纯度

切分的目标是降低不纯度。两种主流度量:

基尼系数(Gini):

其中 是第 类样本的比例。基尼系数越小,纯度越高。当所有样本属于同一类时,Gini = 0。

信息熵(Entropy):

熵越小,纯度越高。

# 1.1.3 信息增益与基尼增益

切分前的不纯度减去切分后的加权不纯度,就是这次切分的"收益":

每次选择增益最大的特征和阈值来切分。

ID3 / C4.5 / CART的区别:

算法 不纯度度量 切分方式
ID3 信息熵 只能处理类别特征,做多分切分
C4.5 信息熵 + 增益率 能处理连续特征,用增益率防止偏向多值特征
CART 基尼系数 二叉树,既能分类又能回归

sklearn默认用CART。

# 1.1.4 手推信息增益小例子

下面用10个样本的二分类问题,手算一次"按年龄切分"的信息增益,把"父节点熵 → 条件熵 → 信息增益"三步走通。

# 1. 设定数据

10个样本,4正6负。特征"年龄"二分为 ≤30与 >30两组:

子集 正样本 负样本 合计
父节点D 4 6 10
年龄 ≤30(D1) 3 2 5
年龄 >30(D2) 1 4 5

# 2. 父节点熵H(D)

父节点正样本占比 ,负样本占比 :

手算:,,代入:

# 3. 子节点条件熵H(D|A)

D1(3正2负):。注意D1的正例比0.6与父节点的0.4恰好是互补(镜像)关系——比例并不相同。熵却没变,真正的原因是熵函数关于p=0.5对称:,所以 :

常见误区:容易误以为"D1比例和父节点一样所以熵相等"。其实D1是镜像而非相同——数字碰巧相等(都=0.9710)会掩盖这个错误,但熵的对称性才是真正原因。

D2(1正4负)::

两个子集各5个样本,权重各0.5,条件熵:

# 4. 信息增益

结论:按"年龄"切分能带来约0.12 bit的不纯度下降。D1的正例比0.6是父节点0.4的镜像,靠熵的对称性 熵值才相同(0.9710),本身没有变纯;真正贡献增益的是D2(1:4,更纯,熵0.7219)。这就是决策树"选增益最大的特征"的直觉——哪一刀能切出更纯的子集,就用哪一刀。

# 1.2 亲手种下第一棵树:sklearn决策树实战

原理走通后,用sklearn在鸢尾花数据集上训练一棵真实的决策树,并把树的结构画出来:

import matplotlib.pyplot as plt
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier, plot_tree
from sklearn.model_selection import train_test_split

iris = load_iris()
X = iris.data[:, 2:]  # 只用后两个特征(花瓣长/宽),方便可视化
y = iris.target

X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)

# 训练决策树
tree_clf = DecisionTreeClassifier(criterion='gini', max_depth=2, random_state=42)
tree_clf.fit(X_train, y_train)

# 可视化树
plt.figure(figsize=(12, 6))
plot_tree(tree_clf, feature_names=iris.feature_names[2:],
          class_names=iris.target_names, filled=True, rounded=True)
plt.show()

# 预测
print("测试集准确率:", tree_clf.score(X_test, y_test))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

关键参数:

参数 作用 常见值
criterion 不纯度度量 'gini' / 'entropy'
max_depth 树的最大深度 None(不限)或3~10
min_samples_split 节点分裂所需最小样本数 默认2
min_samples_leaf 叶子节点最小样本数 默认1
max_features 每次分裂考虑的特征数 决策树默认 None(全部);RandomForestClassifier 默认 'sqrt'

# 1.3 树也能做回归——但有个致命局限

决策树也能做回归,叶子节点返回该节点样本的均值,分裂准则用MSE:

import numpy as np
import matplotlib.pyplot as plt
from sklearn.tree import DecisionTreeRegressor

# 造非线性数据
np.random.seed(42)
X = np.random.rand(100, 1) * 6 - 3
y = (0.5 * X**2 + X + 2 + np.random.randn(100, 1) * 0.5).ravel()  # .ravel() 避免DataConversionWarning

tree_reg = DecisionTreeRegressor(max_depth=3, random_state=42)
tree_reg.fit(X, y)

# 在[-3, 3] 区间内均匀生成200个连续的点,用来绘制平滑的拟合曲线
# .reshape(-1, 1):把一维数组转为二维(200,1),因为scikit-learn的predict方法要求输入特征必须是二维数组
X_plot = np.linspace(-3, 3, 200).reshape(-1, 1)
# 用训练好的决策树,对这200个点进行预测,得到对应的预测值
y_pred = tree_reg.predict(X_plot)

# 绘制原始数据的散点图,alpha=0.5设置半透明,避免点重叠看不清
plt.scatter(X, y, alpha=0.5)
# 绘制决策树的预测拟合曲线,'r-'表示红色实线,linewidth=2设置线宽
plt.plot(X_plot, y_pred, 'r-', linewidth=2)
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

回归树的预测是阶梯状的——每个区间内预测值相同。

实战大坑:回归树无法外推。预测值永远是训练集叶子节点样本的均值,所以预测不出训练集范围外的值——如果训练集房价上限是1000万,不管输入多大的面积,模型最多输出1000万附近。线性回归反而能外推(虽然未必准)。这是回归树/基于树的集成(RF、GBDT)相对线性模型的根本局限,做时间序列或带趋势的特征时要格外注意。

# 1.4 树太"聪明"的代价:过拟合与剪枝

不限制深度的决策树会把训练集切到每个叶子只有一个样本,训练集准确率100%,但测试集一塌糊涂——你给它足够的自由,它真的会把训练集一个个背下来。

# 1.4.1 预剪枝(Pre-pruning)

在生长过程中限制:

# 限制深度 + 叶子最小样本数
tree_clf = DecisionTreeClassifier(
    max_depth=5,
    min_samples_leaf=10,
    min_samples_split=20,
    random_state=42
)
1
2
3
4
5
6
7

# 1.4.2 后剪枝(Post-pruning)

先长成完整的树,再自底向上剪掉收益不大的子树。sklearn用 ccp_alpha(代价复杂度剪枝)实现:

tree_clf = DecisionTreeClassifier(ccp_alpha=0.01, random_state=42)
1

ccp_alpha 越大,剪得越狠。

# 1.5 一棵树不够,那就种一片森林:集成学习

单棵树再怎么剪枝,能力也有天花板。换个思路:训练多个弱模型再组合起来,往往能得到一个强模型——这就是集成学习,正所谓"三个臭皮匠,顶个诸葛亮"。

# 1.5.1 三大范式

范式 思路 代表
Bagging 并行训练多个独立模型,投票/平均 随机森林
Boosting 串行训练,每个模型纠正前一个的错误 AdaBoost、GBDT、XGBoost
Stacking 用一个元模型学习怎么组合基础模型的预测 各种比赛集成

关于Voting:严格说还有第四种组合方式——Voting(投票),和Stacking同属"训练好基模型后再组合"这一类:Voting用规则(多数票/加权平均),Stacking用学出来的元模型。而Bagging / Boosting是在训练阶段就集成(各自造树)。1.9节会把Voting和Stacking放一起对照讲。

# 1.5.2 Bagging

从训练集中有放回地采样多个子集(bootstrap),每个子集训练一个模型,最后投票(分类)或平均(回归)。

from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier

bag_clf = BaggingClassifier(
    DecisionTreeClassifier(),
    n_estimators=500,  # 500棵树
    max_samples=0.6,  # 每个子集抽60% 样本(留出多样性)
    bootstrap=True,  # 有放回采样
    n_jobs=-1,  # 并行
    random_state=42
)
bag_clf.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9
10
11
12

Out-of-Bag(OOB)评估:有放回采样天然留下一批没被某棵树见过的样本,可以拿来估计泛化性能,小数据时能省下一份验证集(但要做最终评估,仍建议保留独立测试集)。这个比例为什么约是37%、代码怎么开,放到1.6.1小节展开。

# 1.6 双重随机的森林:随机森林

随机森林 = Bagging + 决策树 + 特征随机。每棵树在分裂时,不是从所有特征里选最好的,而是从随机抽取的一部分特征里选最好的。这增加了树之间的多样性,进一步降低方差。

平均为什么能降方差?设 棵树两两相关系数为 、各自方差为 ,取平均后方差为:

第二项随树数 增大趋于0;第一项是树间"共性"带来的,平均消不掉,只能靠**降低树间相关性 **来压。随机森林的"双重随机"——bootstrap样本扰动 + 特征随机(max_features)——正是在做这件事;而500棵树让 足够大。这就是"单棵树高方差,集成后反而稳"的数学解释。

from sklearn.ensemble import RandomForestClassifier

rf_clf = RandomForestClassifier(
    n_estimators=500,  # 树的数量
    max_leaf_nodes=16,  # 限制叶子节点
    n_jobs=-1,
    random_state=42
)
rf_clf.fit(X_train, y_train)
print("随机森林准确率:", rf_clf.score(X_test, y_test))
1
2
3
4
5
6
7
8
9
10

# 1.6.1 OOB评估

随机森林的每棵树都用bootstrap有放回采样训练。数学上,每次采样一个样本被抽中的概率是 ,连续 次都没被抽中的概率为:

也就是说约37%的样本不会被某棵树用到,这些样本叫Out-of-Bag(OOB)样本。既然某棵树没见过它们,正好可以拿来评估这棵树,再把所有树的OOB评估平均,就得到了一个不额外占用数据的泛化性能估计——数据少时特别划算。但注意:它不能完全替代独立测试集,正式的最终评估仍建议保留一份测试集。

rf_clf = RandomForestClassifier(
    n_estimators=500,
    max_leaf_nodes=16,
    oob_score=True,  # 开启OOB评估
    n_jobs=-1,
    random_state=42
)
rf_clf.fit(X_train, y_train)
print("测试集准确率:", rf_clf.score(X_test, y_test))
print("OOB 分数:", rf_clf.oob_score_)
1
2
3
4
5
6
7
8
9
10

坑点:OOB分数是"树没见过的样本"上的评估,接近交叉验证的结果,但不完全等于测试集准确率。数据量小或树不够多时,OOB分数会比真实测试准确率可能明显偏低——因为每棵树只用了约63% 的样本训练,单棵树性能比全量训练差一些。要做最终、独立的模型评估,还是建议保留一份真正的测试集。

实测印证(iris,训练集仅112样本,500棵树):测试集准确率1.0000,OOB分数只有0.9554——差距不是"略低",而是相当明显。这正是"每棵树只用约63% 样本训练"在小数据集上的直接代价。

OOB评估最大的好处是省下划分验证集的样本——数据集小的时候特别有用,不用再从本就不多的数据里抠一份验证集出来。

# 1.6.2 特征重要性

随机森林能输出每个特征的重要性——一个特征被用来分裂的次数越多、带来的不纯度下降越大,重要性越高。

# rf_clf.feature_importances_:随机森林模型训练完成后自带的属性,返回一个数组,每个数值对应一个特征的重要性得分
importances = rf_clf.feature_importances_
# X只取了后两个花瓣特征,名字也要对应取 [2:],否则标签会错位
for name, imp in zip(iris.feature_names[2:], importances):
    print(f"{name:20s}: {imp:.4f}")
1
2
3
4
5

光看数字不够直观,画成排序后的条形图一眼就能看出谁重要:

import numpy as np
import matplotlib.pyplot as plt

# 按重要性从小到大排序,条形图从下往上画最大的
indices = np.argsort(importances)

plt.figure(figsize=(8, 5))
# 绘制横向条形图,barh=bar horizontal
# 第一个参数range(len(importances)):y轴上每个条形的位置(0, 1, ...)
# 第二个参数importances[indices]:条形的长度,即排序后的重要性数值
plt.barh(range(len(importances)), importances[indices], align='center')
# plt.yticks(刻度位置, 刻度标签):给y轴每个位置配上对应的特征名
# 先把特征名列表转成numpy数组,才能用索引数组indices按顺序取值;最终y轴从下到上,特征名按重要性从低到高排列,和条形长度一一对应
plt.yticks(range(len(importances)), np.array(iris.feature_names[2:])[indices])
plt.xlabel('重要性')
plt.title('特征重要性排序')
plt.tight_layout()
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

这是特征选择的利器:直接砍掉重要性接近0的特征。乳腺癌数据集有30个特征,通常只有几个真正重要,条形图能立刻把它们挑出来——但iris只有2个特征,实测重要性0.5238 vs 0.4762,几乎一样重要,根本挑不出"少数关键",所以必须换成乳腺癌才能体现这个卖点:

from sklearn.datasets import load_breast_cancer

# 用乳腺癌30特征,才能真正体现"少数关键特征"的卖点
cancer = load_breast_cancer()
X_c, y_c = cancer.data, cancer.target
X_train_c, X_test_c, y_train_c, y_test_c = train_test_split(
    X_c, y_c, test_size=0.2, random_state=42)

rf_cancer = RandomForestClassifier(n_estimators=500, n_jobs=-1, random_state=42)
rf_cancer.fit(X_train_c, y_train_c)

importances_c = rf_cancer.feature_importances_
indices = np.argsort(importances_c)

plt.figure(figsize=(8, 8))
plt.barh(range(len(importances_c)), importances_c[indices], align='center')
plt.yticks(range(len(importances_c)), np.array(cancer.feature_names)[indices])
plt.xlabel('重要性')
plt.title('乳腺癌 30 特征重要性排序')
plt.tight_layout()
plt.show()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在30个特征里,通常只有 worst concave points、worst perimeter、mean concave points 等少数几个真正重要,其余接近0——这才是"条形图能立刻挑出来"的真实场景。

# 1.6.3 Extra-Trees(极端随机树)

比随机森林更激进:分裂时不仅随机选特征,连阈值也是随机选的。进一步随机化通常能降低方差,但会增加偏差;至于速度和最终效果,取决于数据集与参数配置,不是必然规律。

from sklearn.ensemble import ExtraTreesClassifier

# 用法与随机森林一致,区别在于分裂时连阈值也是随机选的
extra_clf = ExtraTreesClassifier(n_estimators=500, n_jobs=-1, random_state=42)
extra_clf.fit(X_train, y_train)
1
2
3
4
5

# 1.7 换个思路:让后一棵树专修前一棵的错(Boosting)

看完并行投票的Bagging,再看另一条路线。Boosting串行训练:每个新模型都专注于前一个模型预测错的样本。

# 1.7.1 AdaBoost

给每个样本赋权重,预测错的样本权重加大,下一个模型重点关注这些难样本。最终所有模型按准确率加权投票。

from sklearn.ensemble import AdaBoostClassifier

ada_clf = AdaBoostClassifier(
    DecisionTreeClassifier(max_depth=1),  # 弱学习器:树桩
    n_estimators=200,
    learning_rate=0.5,
    random_state=42
)
ada_clf.fit(X_train, y_train)
1
2
3
4
5
6
7
8
9

max_depth=1 的树叫树桩,是AdaBoost最常用的弱学习器。

# 1.7.2 GBDT(梯度提升决策树)

AdaBoost通过加大错样本权重来纠正错误,GBDT更直接:让新模型去拟合前一个模型的残差(真实值减预测值)。

每一步:

训练新树拟合 ,然后 ,其中 是学习率。

GBDT是XGBoost、LightGBM、CatBoost的理论基础,后面专门讲这三个工业级库。

# 1.8 实战对比:三种集成谁更强?

理论讲完,在乳腺癌数据集上把随机森林、AdaBoost、GBDT拉出来跑同一个训练/测试集,用准确率和F1横向对比:

from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier
from sklearn.metrics import accuracy_score, f1_score

data = load_breast_cancer()
X_train, X_test, y_train, y_test = train_test_split(
    data.data, data.target, test_size=0.2, random_state=42
)

models = {
    'RandomForest': RandomForestClassifier(n_estimators=500, random_state=42),
    'AdaBoost':     AdaBoostClassifier(n_estimators=200, random_state=42),
    'GBDT':         GradientBoostingClassifier(n_estimators=200, random_state=42)
}

for name, model in models.items():
    model.fit(X_train, y_train)
    y_pred = model.predict(X_test)
    print(f"{name:15s} Acc={accuracy_score(y_test, y_pred):.4f}  F1={f1_score(y_test, y_pred):.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

实测输出(乳腺癌,test_size=0.2,random_state=42):

模型 Acc F1
RandomForest (500) 0.9649 0.9722
AdaBoost (200) 0.9737 0.9790
GBDT (200) 0.9561 0.9650

实验前,我的预期:GBDT是AdaBoost的"升级版",我原以为它稳赢。

实验结果:AdaBoost(0.9737) > RF(0.9649) > GBDT(0.9561)——默认参数的GBDT反而垫底。

**为什么会这样?**默认参数不代表模型上限:GBDT对 learning_rate、树深、n_estimators 都很敏感,不像随机森林那么"开箱即用"——这恰好印证了下面表格里"Boosting需调参"。工业级库强在调参空间和工程优化,不等于默认参数更强。

这次真正学到什么:没有"银弹"模型,AdaBoost在这个数据集上反而最好,说明谁赢是数据相关的——这也是Kaggle选手要横向对比多种集成的根本原因。

Bagging vs Boosting对比:

维度 Bagging(随机森林) Boosting(GBDT)
训练方式 并行 串行
目标 降方差 降偏差
过拟合风险 低 较高(需调参)
速度 快(可并行) 慢(串行)
典型代表 随机森林 XGBoost / LightGBM

# 1.9 让模型互相取长补短:Stacking与Voting

在三大范式(Bagging / Boosting / Stacking)之外,还有一个常被一起讨论的Voting(投票):它和Stacking同属"组合多个已经训练好的模型"这一类——区别是Voting用规则(多数票/加权平均)组合,Stacking用一个学出来的元模型组合。而Bagging / Boosting是在训练阶段就集成(各自造树),区别于前两者。本节先实现Stacking,再用完全相同的基模型让Voting做公平对照。

Voting是把几个模型的预测直接投票/平均,而Stacking(堆叠)更进一步:把多个不同基模型的预测当作新特征,再训练一个**元模型(meta-model)**来学习"怎么组合这些预测最好"。

基模型1 (RF)   ─┐
基模型2 (Ada)   ├─→ 预测作为新特征 ─→ 元模型(逻辑回归) ─→ 最终预测
基模型3 (GBDT) ─┘
1
2
3

为避免基模型在训练集上过拟合导致元模型学到假规律,Stacking用交叉验证生成元特征:基模型在每折的验证集上预测,这些预测拼起来作为元模型的训练数据。sklearn的 StackingClassifier 把这套流程封装好了。

from sklearn.ensemble import (StackingClassifier, RandomForestClassifier,
                              AdaBoostClassifier, GradientBoostingClassifier)
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, f1_score

# 基模型:三种风格各一个(并行Bagging + 两种Boosting)
base_estimators = [
    ('rf',   RandomForestClassifier(n_estimators=100, random_state=42)),
    ('ada',  AdaBoostClassifier(n_estimators=100, random_state=42)),
    ('gbdt', GradientBoostingClassifier(n_estimators=100, random_state=42))
]

# 元模型:逻辑回归(简单、快、不容易过拟合)
stacking_clf = StackingClassifier(
    estimators=base_estimators,
    final_estimator=LogisticRegression(),
    cv=5,  # 5折交叉验证生成元特征
    n_jobs=-1
)
stacking_clf.fit(X_train, y_train)
y_pred = stacking_clf.predict(X_test)
print(f"Stacking 准确率: {accuracy_score(y_test, y_pred):.4f}")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

公平对照:Voting(同样的三个基模型)——为了说明"集成不是银弹",用完全一致的基模型做对照,而且soft(平均概率)和hard(数票数)两种投票都跑一遍:

from sklearn.ensemble import VotingClassifier

# 直接复用Stacking的同一份base_estimators,保证基模型完全一致
voting_soft = VotingClassifier(estimators=base_estimators,
                               voting='soft', n_jobs=-1)
voting_hard = VotingClassifier(estimators=base_estimators,
                               voting='hard', n_jobs=-1)
for name, clf in [('Voting-soft', voting_soft), ('Voting-hard', voting_hard)]:
    clf.fit(X_train, y_train)
    yp = clf.predict(X_test)
    print(f"{name:12s} Acc={accuracy_score(y_test, yp):.4f}  F1={f1_score(y_test, yp):.4f}")
1
2
3
4
5
6
7
8
9
10
11

实测输出(乳腺癌,test_size=0.2,random_state=42,sklearn 1.9.0):

模型 Acc F1 备注
Stacking(基模型100, cv=5) 0.9561 0.9650 元模型=逻辑回归
Voting-soft(同基模型) 0.9561 0.9650 平均概率,与Stacking逐条预测完全相同
Voting-hard(同基模型) 0.9649 0.9722 只数票数

实验前,我的预期:Stacking有元模型"学习组合",理应比规则投票的Voting更聪明;soft用了概率信息,也应该比hard强。

实验结果:两点都被打脸——① **Stacking和soft Voting完全打平;② hard Voting反而最好(0.9649)。

为什么会这样?先看错在哪:三个基模型在最难的3个样本上集体预测错——错误高度相关时,元模型手里根本没有"正确信息"可以挖,学再复杂的权重也救不回来,所以Stacking ≈ soft平均。再看soft为什么输给hard:有一个样本RF(0.58)和AdaBoost(0.52)微弱地投了正确答案,GBDT却以0.75的高置信度投错;soft平均概率被这张"自信的错票"拉偏,而hard只数票数、2:1判对。soft信任概率幅度,一张过度自信的错票就能翻盘——这正是概率校准的意义。

这次真正学到什么:① Stacking不是银弹,收益严格依赖基模型错误的互补性(呼应下面坑点),错误相关时多一层模型纯属浪费;② soft不天然优于hard,基模型概率不准时hard可能更稳。组合方式必须靠实验说话,不能因为"理论上更高级"就默认更强。

版本说明:不同sklearn版本下末位小数可能不同——1.4起AdaBoost默认算法由SAMME.R改为SAMME(新版本中SAMME.R已移除),soft投票用的概率会变;但"Stacking与soft打平、基模型在难样本上集体犯错"的结论不变。跑代码时请固定版本并报告版本号。我这个版本是二者是打平的。

坑点:元模型一定要选简单的(逻辑回归、岭回归)。元模型太复杂会过拟合基模型的预测,反而不如直接Voting。基模型之间要差异大(不同算法、不同特征子集),都一样的模型堆起来没有意义——Stacking的收益来自"不同模型犯错的地方不一样"。

# 小结

决策树通过递归切分数据学习非线性决策边界,可解释性强但单棵树易过拟合;随机森林用Bagging + 特征随机降方差并提供特征重要性,OOB评估能在训练阶段估计泛化性能、省下一份验证集;Boosting系列通过串行纠正错误提升精度,GBDT是XGBoost/LightGBM的理论基础;Stacking/Voting用元模型或规则组合异质基模型,是比赛刷分的常用手段。下一天我们学习SVM支持向量机,看它如何用最大间隔和核技巧处理非线性。

最后沉淀一条本篇的认知升级:模型没有银弹,真正决定结果的,是数据、特征、模型、参数和验证方式共同作用——这也是"刻意练习"要坚持用实验说话的原因。

最近更新: 2026/9/30
 |  Copyright © -Present Kelsen All Rights Reserved. | Author KelsenJing