补充:
本文是关于《Sklearn 与 TensorFlow 机器学习实用指南》的学习笔记,基于七、集成学习和随机森林 - 【布客】Sklearn 与 TensorFlow 机器学习实用指南 第二版,感谢译者
本文和原文的区别:
本文会更精简、系统地表述书中概念,会对书中未介绍的陌生概念加以解释,每行我都会添加注释,介绍具体做了什么。
后续会持续更新所有章节
正文开始 ~
————————————————
前言
对一个复杂问题来说,很多人的答案的组合,往往比一个专家的答案要好。这就叫做群体智慧。
合并一组分类器的预测(像分类或者回归),往往也会得到比单一分类器更好的预测结果。
上述一组分类器就叫做集成,例如,决策树的集成就叫做随机森林
本章中我们会讨论特别著名的集成方法,包括 bagging, boosting, stacking等,会讨论随机森林
1. 投票分类:
投票分类:通过结合多个基分类器(弱学习器:低准确率,例如:logistic回归、 SVM、随机森林、 KNN)的预测结果,得到新的投票分类器(强学习器:高准确率),提高整体性能。
原理:当基分类器的数量增加时,多数投票的结果趋向于正确的类别。这是因为大数定律确保了随着分类器数量的增加,错误分类器的影响会被稀释,整体正确率提高。
大数定律(Law of Large Numbers, LLN):在大量独立重复试验下,随机事件的频率会趋近于其理论概率。
# 适用条件:随机变量是独立同分布(i.i.d.)的
可能会降低准确率的问题:
1.如果每一个分类器都在同一个数据集上训练,可能无法满足独立同分布。
2.如果每一个分类器类型相同,可能会犯同一种错误,做出同一种错误识别,将很多票投给了错误类别,导致集成的准确率下降。
针对上述问题的解决办法:
1.用独立的数据来训练不同分类器
2.分类器用完全不同的算法,因此不会犯不同种类的错误,提高集成的正确率
投票分类有两种投票方式:
1.硬投票(Hard Voting):每个基模型独立地对测试样本进行分类,并直接输出类别标签。
过程:
- 各个基模型预测出一个类别标签。
- 最终结果由多数投票决定,即出现次数最多的标签被选为最终结果
特点:
- 简单直接,实现容易,计算效率高。
- 忽略了概率信息,每个模型的预测仅以“有或无”的形式计入。
2.软投票(Soft Voting):基模型不仅输出类别标签,还提供该类别的概率或置信度(有predict_proba函数)。
过程:
- 每个模型为每个可能的类别提供一个概率值。
- 这些概率被汇总(如平均)以决定最终结果。例如,计算各模型对某一类别的概率总和,概率最高的类别即为预测结果。
特点:
- 利用概率信息,考虑了各模型对自身预测的信心程度,给高自信的投票更大的权重。
- 更灵活且可能更准确,尤其是在基模型的输出质量较高时。
from sklearn.ensemble import RandomForestClassifier # 随机森林分类器
from sklearn.ensemble import VotingClassifier # 投票
from sklearn.linear_model import LogisticRegression # logistic回归
from sklearn.svm import SVC # 支持向量机
log_clf = LogisticRegression() # Logistic回归
rnd_clf = RandomForestClassifier() # 随机森林书分类器
svm_clf = SVC() # 支持向量机
voting_clf = VotingClassifier(estimators=[('lr', log_clf), ('rf', rnd_clf), ('svc', svm_clf)], voting='hard') # 投票分类器,voting:hard代表硬投票、soft代表软投票
voting_clf.fit(X_train, y_train) # 拟合训练数据
from sklearn.metrics import accuracy_score
for clf in (log_clf, rnd_clf, svm_clf, voting_clf):
clf.fit(X_train, y_train)
y_pred = clf.predict(X_test)
print(clf.__class__.__name__, accuracy_score(y_test, y_pred)) # 打印一下上述的各单一分类器的准确率,和投票分类的准确率
"""
LogisticRegression 0.864
RandomForestClassifier 0.872
SVC 0.888
VotingClassifier 0.896
上述例子,如果我们采用软投票voting='soft'(需要把SVC分类器的probability设置为True(这会使 SVC 使用交叉验证去预测类别概率,其降低了训练速度,但会添加predict_proba()方法)),正确率高达 91%
"""
2.Bagging和Pasting
对每一个分类器都使用相同的训练算法,但是在不同的训练集上去训练它们。其中
有放回采样被称为装袋(Bagging,bootstrap aggregating的缩写)
无放回采样称为粘贴(Pasting)
当所有的分类器被训练后,集成可以通过对所有分类器结果的简单聚合来对新的实例进行预测。对分类来说,上述聚合函数通常是统计(例如硬投票分类器)。对回归问题来说,是取平均。
集成的特点:
通常情况下,对比单一分类器,集成的结果能得到相似的偏差,但有更小的方差。
所有单一的分类器可以通过不同的 CPU 核或其他的服务器被一起训练,Bagging 和 Pasting 的可扩展性很好。
2.1 在 sklearn 中的 Bagging 和 Pasting
from sklearn.ensemble import BaggingClassifier
from sklearn.tree import DecisionTreeClassifier
bag_clf = BaggingClassifier( # BaggingClassifier(或者对于回归可以是BaggingRegressor)
DecisionTreeClassifier(), # 分类器选择决策树。
n_estimators=500, # 500 个决策树分类器
max_samples=100, # 采样100 个训练实例
bootstrap=True, # Bagging有放回采样 (如果是Pasting,就设置bootstrap=False)
n_jobs=-1 # 用于训练和预测所需要 CPU 核的数量。(-1 代表着 sklearn 会使用所有空闲核)
)
bag_clf.fit(X_train, y_train) # 拟合数据
y_pred = bag_clf.predict(X_test) # 预测
如果基分类器可以预测类别概率(拥有predict_proba方法),那么BaggingClassifier会自动的运行软投票。
由于Bagging使用有放回抽样,生成的子集之间差异性更大,导致基预测器之间的相关性较低。虽然这可能使Bagging的偏差略高于Pasting,但通过减少基预测器之间的相关性,也降低了集成模型的整体方差,最终提升了模型的泛化能力。
补充:
Q1.为什么有放回抽样会导致子集之间更大的差异性?
在有放回抽样中,每个子集是从原始数据集中独立抽取的样本,允许同一个样本被多次选入同一个子集。这意味着不同子集之间的样本组成可能会有很大的差异,特别是在样本量较大的情况下。
相比之下,无放回抽样一般不会分割成太多(相较原数据)的子集,这可能导致子集都包含足够多的数据,因此子集在样本组成上更加相似。因此,Bagging通过有放回抽样引入了更多的分集,使得各个基预测器训练的数据分布更不一致。
Q2.数据的差异如何影响基预测器?
训练数据之间的差异,决定了基预测器之间的差异,数据的相似性越高,基预测器的相关性也就越高。
Q3.偏差和方差是如何在Bagging和Pasting中相互影响的?
偏差(Bias):是指模型预测值与真实值之间的系统误差。高偏差意味着模型对训练数据和测试数据之间的一致性较差。
方差(Variance):是指模型对不同训练集敏感程度的度量。高方差意味着模型在不同的训练集上表现差异较大,容易过拟合。
Q4. 基预测器之间的相关性如何影响集成模型的整体性能?
如果基预测器之间的相关性很高,意味着它们的预测结果趋于一致,这样在集成时无法有效减少方差,反而可能导致集成效果不如预期。
相反,如果基预测器之间具有较低的相关性,那么每个预测器能够提供不同的信息和预测能力。这种多样性使得在集成时,各个预测器的预测结果相互补充,从而有效降低整体方差,提升模型的泛化性能。
2.2 Out-of-Bag 评价
Bagging 每个训练集都是从原始数据集中有放回地随机抽取样本形成的,在每次自助抽样中,有些样本会被多次选中,而有些则从未被选中。未被任何基分类器选中的样本即为OOB实例。
OB评估的作用:
OOB实例可以用于测试模型的整体性能,因为它们没有参与任何基分类器的训练,避免了过拟合的问题。
这种方法提供了一种无偏估计,类似于交叉验证,但计算效率更高,简化了模型调优的过程。
# 查看模型在oob实例上的准确率
bag_clf = BaggingClassifier(DecisionTreeClassifier(), n_estimators=500, bootstrap=True, n_jobs=-1, oob_score=True) # oob_score=True表示返回模型在oob实例上的准确率
bag_clf.fit(X_train, y_train)
print(bag_clf.oob_score_)
# 0.93066666666666664
# 查看模型在测试集上的准确率
from sklearn.metrics import accuracy_score
y_pred = bag_clf.predict(X_test)
print(accuracy_score(y_test, y_pred))
# 0.93600000000000005
# oob 决策函数也可通过oob_decision_function_变量来展示,(当基决策器有predict_proba()时)决策函数会对每个训练实例返回类别概率
print(bag_clf.oob_decision_function_)
#array([[ 0., 1.], [ 0.60588235, 0.39411765],[ 1., 0. ], ... [ 1. , 0. ],[ 0., 1.],[ 0.48958333, 0.51041667]])
# 预测第二个训练实例有 60.6% 的概率属于正类,39.4% 属于负类
3. 随机贴片(Random Patch)和随机子空间(Random Subspace)
随机贴片(Random Patch)
操作对象:数据块
方法:从原始数据集中随机选择多个不重叠的子集或小区域。每个基模型使用这些子集进行训练,确保不同子集的数据分布略有差异。
与参数的关系:bootstrap=False(Pasting) max_samples=1.0
效果:增加模型对数据分布变化的适应能力,减少过拟合。
随机子空间(Random Subspace)
操作对象:特征集合(对于特征采样而不是实例采样)
方法:每次训练时随机选择部分特征,而不是使用全部特征。每个基模型基于不同的特征子集进行训练。
与参数的关系:bootstrap_features=True并且/或者max_features小于 1.0
效果:采样特征导致更多的预测多样性,降低模型之间的相关性,用高偏差换低方差,提高整体泛化能力。
总结:
随机贴片: 关注数据的随机分割,确保多样性。
随机子空间: 关注特征的随机选择,减少模型间的关联。
4. 随机森林
随机森林(Random Forest):利用多个决策树的预测结果进行集成,通常通过投票或平均获得最终结果,从而提升模型的准确性和鲁棒性,主要应用于分类和回归任务。
步骤:
1. 袋装法(Bootstrap Aggregating):从训练数据中有放回地多次抽样,每次生成一个样本集,每个样本集用于训练一棵决策树。
2. 特征子集选择:在每个决策树节点的分裂过程中,并非遍历所有特征寻找最优分裂点,而是从随机选取的一部分特征中选择最佳特征。
3. 预测过程:
- 分类任务:通过多数投票法确定类别。
- 回归任务:通过取平均值或中位数得到结果。
优点:
- 高准确性,尤其在高维数据上表现优异。
- 对噪声和缺失值具有一定的鲁棒性。
- 自动处理特征标准化问题。
- 计算效率较高,适合大数据集。
与传统决策树的比较:
随机森林通过在每个节点分裂时随机选取一部分特征进行最优分裂特征的选择,这种引入的额外随机性增加了每棵树之间的多样性,减少了模型的方差,并提高了整体性能和鲁棒性。尽管这可能导致单棵树的准确率略有下降,但通过集成多棵树的优势,最终提升了整体预测效果。
from sklearn.ensemble import RandomForestClassifier
rnd_clf = RandomForestClassifier(n_estimators=500, # 500 个树
max_leaf_nodes=16, # 限制为最大 16 叶子结点
n_jobs=-1) # 使用所有空闲的 CPU 核
rnd_clf.fit(X_train, y_train)
y_pred_rf = rnd_clf.predict(X_test)
# 下面的写法,大致相当于之前的RandomForestClassifier
bag_clf = BaggingClassifier(DecisionTreeClassifier(splitter="random", max_leaf_nodes=16), n_estimators=500, max_samples=1, bootstrap=True, n_jobs=-1)
4.1 极随机树
当你在随机森林上生长树时,在每个结点分裂时只考虑随机特征集上的特征。相比于找到更好的特征,我们可以通过使用对特征使用随机阈值使树更加随机(像规则决策树一样)。
补充:
规则决策树:是基于规则生成的一种决策树。与传统的分裂方式不同,规则决策树通常采用更简单、随机化的规则来进行数据分割。
举例说明:
假设我们有一个简单的数据集,用于根据学生的“年龄”和“成绩”预测是否会被某所大学录取:
| 学生ID | 年龄(岁) | 成绩(分) | 录取结果 |
|--------|------------|------------|----------|
| 1 | 20 | 85 | 是 |
| 2 | 22 | 78 | 否 |
| 3 | 24 | 92 | 是 |
| 4 | 26 | 70 | 否 |
| 5 | 28 | 88 | 是 |
传统规则决策树举例:
假设基于“年龄”进行第一次分裂,判断条件为“年龄 > 24岁”。这样,学生ID 1(20岁)会被分到第一个子集,而学生ID 2、3、4、5会被分到第二个子集。
第二个子集中继续根据“成绩”进行判断,如“成绩 > 80分”,从而决定录取结果。
极随机树举例:
在构建过程中,可能在每个节点选择分裂特征时引入了随机性。
例如,在第一次分裂时,系统可能随机选择“年龄”或“成绩”作为分裂特征。假设选择了“成绩”进行首次分裂,判断条件为“成绩 > 80分”。这样,学生ID1(85分)、ID3(92分)和ID5(88分)会被分到一个子集,而学生ID2(78分)和 ID4(70分)会被分到另一个子集。
接下来的分裂可能会继续随机选择特征,进一步提高模型的多样性和泛化能力。
通过这种方式,极随机树能够在同一棵决策树中引入更多的变异性,从而提升分类或预测的效果。多个这样的高度随机化的决策树组合在一起(如投票法),形成一个集成模型,显著提高整体性能。
可以使用 sklearn 的ExtraTreesClassifier来创建一个 Extra-Tree 分类器
from sklearn.ensemble import ExtraTreesClassifier
et_clf = ExtraTreesClassifier(n_estimators=100, # 构建 100 棵树
max_depth=None, # 树的最大深度(None 表示不限制)
min_samples_split=2, # 分裂内部节点所需的最小样本数
random_state=42) # 随机种子,确保结果可重复
后续再使用网格搜索,来调整超参数
4.2 特征重要度
单一决策树中,重要的特征会出现在更靠近根部的位置,而不重要的特征会经常出现在靠近叶子的位置。因此我们可以通过计算特征在森林的全部树中出现的平均深度来预测特征的重要性。
from sklearn.datasets import load_iris # 鸢尾花数据集
iris = load_iris() # 导入数据
rnd_clf = RandomForestClassifier(n_estimators=500, n_jobs=-1) # 随机森林树
rnd_clf.fit(iris["data"], iris["target"]) # 拟合数据
for name, score in zip(iris["feature_names"], rnd_clf.feature_importances_): # rnd_clf.feature_importances_:每个特征的重要度
print(name, score)
"""
sepal length (cm) 0.112492250999
sepal width (cm) 0.0231192882825 # 分数最低,特征重要度最低
petal length (cm) 0.441030464364 # 分数最高,特征重要度最高
petal width (cm) 0.423357996355
"""
随机森林可以非常方便快速得了解哪些特征实际上是重要的,特别是需要进行特征选择的时候。
5.提升
提升(Boosting):将几个弱学习者组合成强学习者的集成方法。
思想:按顺序去训练分类器,每一个分类器都尝试修正前面的分类。
主要有:
Adaboost(Adaptive Boosting 适应性提升)
Gradient Boosting(梯度提升)
5.1 Adaboost 适应性提升
思路:对之前分类结果不对的训练实例多加关注,增加权重
步骤:
a. 初始时训练一个弱分类器,并计算其在训练数据上的错误率。
b. 根据错误率调整样本权重,使后续分类器更加关注被错误分类的样本。
c. 重复步骤a和b,直到达到预定的迭代次数或满足停止条件。
d. 最终将所有基分类器进行加权投票,得到最终预测结果。
举例:第一个基分类器(例如一个决策树)被训练,然后在训练集上做预测,在误分类训练实例上的权重就增加了。第二个分类机使用更新过的权重然后再一次训练,权重更新,以此类推
下图显示连续五次预测的 moons 数据集的决策边界(在本例中,每一个分类器都是高度正则化带有 RBF 核的 SVM)。
第一个分类器误分类了很多实例,所以它们的权重被提升了。第二个分类器因此对这些误分类的实例分类效果更好,以此类推。
右边的图代表了除了学习率减半外(误分类实例权重每次迭代上升一半)相同的预测序列。
序列学习技术的一个重要的缺点就是:它不能被并行化(只能按步骤),因为每个分类器只能在之前的分类器已经被训练和评价后再进行训练。
计算过程(来自deepseek):
from sklearn.ensemble import AdaBoostClassifier
ada_clf = AdaBoostClassifier(DecisionTreeClassifier(max_depth=1),
n_estimators=200, # 基于 200 个决策树桩
algorithm="SAMME.R", # SAMME:多分类任务的提升算法,使用了适合多类别的损失函数,并对每个类别进行独立优化。它是AdaBoost方法的扩展版本。如果分类器可以预测类别概率(有predict_proba),则可以使用SAMME.R的变量(R代表“REAL”)
learning_rate=0.5 # 学习率0.5
)
ada_clf.fit(X_train, y_train) # 拟合数据
# 如果过拟合,可以尝试减少基分类器的数量或者对基分类器使用更强的正则化。
5.2 Gradient Boosting 梯度提升
思路:使用新的分类器去拟合前面分类器预测的残差
让我们通过一个使用决策树当做基分类器的简单的回归例子,这被叫做梯度提升回归树(GBRT Gradient Boosted Regression Trees)。
首先我们用DecisionTreeRegressor去拟合训练集(例如一个有噪二次训练集)
from sklearn.tree import DecisionTreeRegressor
tree_reg1 = DecisionTreeRegressor(max_depth=2) # 第一个分类器,决策树回归模型
tree_reg1.fit(X, y) # 拟合数据,X、y是有噪二次训练集
y2 = y - tree_reg1.predict(X) # 第一个分类器的残差
tree_reg2 = DecisionTreeRegressor(max_depth=2) # 第二个分类器
tree_reg2.fit(X, y2) # 在第一个分类器的残差上,训练第二个分类器
y3 = y2 - tree_reg1.predict(X) # 第二个分类器的残差
tree_reg3 = DecisionTreeRegressor(max_depth=2) # 第三个分类器
tree_reg3.fit(X, y3) # 在第二个分类器的残差上,训练第三个分类器
y_pred = sum(tree.predict(X_new) for tree in (tree_reg1, tree_reg2, tree_reg3)) # 通过集成所有树的预测,在一个新的实例上进行预测
可以看到集成的预测会变的更好
使用 sklean 中的GradientBoostingRegressor来训练 GBRT 集成
from sklearn.ensemble import GradientBoostingRegressor
gbrt = GradientBoostingRegressor(max_depth=2, # 最大深度
n_estimators=3, # 分类器数量
learning_rate=1.0 # 学习率:确立了每个树的贡献。如果减小,例如 0.1,在集成中就需要更多的树去拟合训练集,但预测通常会更好。这个正则化技术叫做 shrinkage。
)
gbrt.fit(X, y)
学习率和树的数量,对拟合的影响,如下
在低学习率上训练的 GBRT 集成:其中左面是一个没有足够树去拟合训练集的树,右面是有过多的树过拟合训练集的树。
为了找到树的最优数量,可以使用早停技术(第四章讨论)。
方式一:借助staged_predict函数,先在一大堆树中训练,然后再回头去找最优数目
import numpy as np
from sklearn.model_selection import train_test_split
from sklearn.metrics import mean_squared_error
X_train, X_val, y_train, y_val = train_test_split(X, y)
gbrt = GradientBoostingRegressor(max_depth=2, n_estimators=120) # 用 120 个树训练的 GBRT 集成
gbrt.fit(X_train, y_train)
errors = [mean_squared_error(y_val, y_pred) for y_pred in gbrt.staged_predict(X_val)] # staged_predict函数:是在不同的训练阶段(即每棵树的生长过程中)返回预测值
bst_n_estimators = np.argmin(errors) # 找到验证错误最小时,对应的树的数量,也是最佳数量
gbrt_best = GradientBoostingRegressor(max_depth=2, n_estimators=bst_n_estimators) # 使用最佳数量,训练另一个集成
gbrt_best.fit(X_train, y_train)
验证错误在左面展示,最优模型预测被展示在右面。
方式二:也可以早早的停止训练来实现早停,发现误差越来越大时,就及时停止。
设置warm_start=True,这使得当fit()方法被调用时 sklearn 保留现有树,并允许增量训练。接下来当5次迭代验证错误没有改善时,会停止训练
gbrt = GradientBoostingRegressor(max_depth=2, warm_start=True) # 设置warm_start=True
min_val_error = float("inf") # 记录最小误差
error_going_up = 0 # 误差允许变大次数
for n_estimators in range(1, 120):
gbrt.n_estimators = n_estimators # 设置树的数量
gbrt.fit(X_train, y_train) # 拟合数据
y_pred = gbrt.predict(X_val) # 预测
val_error = mean_squared_error(y_val, y_pred) # 求误差
if val_error < min_val_error:
min_val_error = val_error # 记录最小误差
error_going_up = 0 # 更新参数
else:
error_going_up += 1
if error_going_up == 5: # 当5次迭代验证错误没有改善时,会停止训练
break
GradientBoostingRegressor也支持很多超参数,例如:
subsample:指定每棵树训练的训练实例比例。如果subsample=0.25,那么每个树都会在 25% 随机选择的训练实例上训练。
subsample作用:高偏差换低方差,加速了训练。这个技术叫做随机梯度提升。
也可以对其他损失函数使用梯度提升,例如loss=quantile等,见sklearn文档
6.Stacking
思路:训练一个模型(blender 元模型)来聚合各个分类器的预测结果
步骤:
1.数据集划分:将训练数据集划分为多个子集,这些子集可以按照不同的方式划分,例如随机划分或使用交叉验证。
2.基础模型训练和预测:对于每个子集,使用不同的基础模型进行训练和预测,生成对未知数据的预测结果。
3.特征组合:将每个基础模型的预测结果作为新的特征,组合成一个新的训练数据集。
4.元模型训练:使用新的训练数据集来训练一个元模型。元模型的目标是学习如何结合基础模型的预测结果,以最大化整体模型的准确性。
5.预测:使用训练好的元模型来对测试数据进行预测,生成最终的预测结果。
(参考:https://blog.csdn.net/ueke1/article/details/137190677)
在这个过程中,也可以在其他基模型对未知数据的预测结果的基础上,继续堆叠基模型,不断产生新的预测结果,最终生成元模型,进行预测。
然而sklearn并不直接支持 stacking ,但可以使用开源的项目例如 brew(https://github.com/viisar/brew)