第2章:随机森林原理:Bagging、决策树与集成学习
这一章,我们聊聊随机森林的根基。很多人一上来就调参,却忽略了它背后的逻辑。我个人觉得,搞懂原理比记住一百个参数更重要。毕竟你知道了它为什么好,才知道怎么用。
2.1 集成学习:三个臭皮匠的智慧
先问一个问题:如果让你猜一个瓶子里有多少颗糖,你是自己猜准,还是问100个人取平均值准?
答案是后者。这就是集成学习的核心思想——把多个“弱”模型组合成一个“强”模型。单个模型可能犯错误,但一群模型同时犯错的概率就低得多。
集成学习主要有两派:
- Bagging(自举汇聚):并行训练多个模型,然后投票或平均。随机森林就是用的这个套路。
- Boosting(提升):串行训练,后一个模型纠正前一个的错误。XGBoost是这派的代表。
你看,这俩路子完全不同。随机森林讲究“民主决策”,而Boosting是“层层纠偏”。我当时做用户流失预测时测试过,数据噪声大时Bagging更鲁棒,数据有偏时Boosting调参好了更强。你得根据场景选。
2.2 Bagging:为什么随机采样这么管用?
Bagging全名叫Bootstrap Aggregating。名字听着玄乎,其实流程特别简单:
- 从原始数据里有放回地抽样,生成M个子数据集。
- 在每个子数据集上训练一个模型。
- 预测时,分类任务用投票,回归任务用平均值。
我问你,为什么不直接训练一棵大树?非要弄这么多份数据?
原因有两个。第一,降低方差。单个决策树容易过拟合,一换数据集结果就变。但多棵树取平均,波动就被平滑了。第二,稳定性。如果某棵树的训练集里恰好有异常值,其他树的训练集里可能没有,投票时那棵“坏树”就被淹没了。
| 特性 | 单棵决策树 | Bagging集成 |
|---|---|---|
| 方差 | 高,容易过拟合 | 低,泛化强 |
| 训练速度 | 快 | 慢(但可并行) |
| 对异常值敏感度 | 非常敏感 | 鲁棒性好 |
我有一个经验值:基模型之间的相关性越低,Bagging效果越好。如果所有模型都一样,那投票就是自欺欺人。随机森林在此基础上又加了“随机特征选择”,就是为了进一步降低模型间的相关性。
2.3 决策树:随机森林最小单元
随机森林里的“树”,就是决策树。它长什么样?我给你拆开看看。
2.3.1 树是怎么长起来的?
决策树的本质是一堆 if-else 规则的嵌套。每次分裂选一个特征和一个阈值,让分出来的两个子集“纯度”尽可能高。衡量纯度的方法很多:
- 基尼系数:默认选项,计算快。值越小越纯。
- 信息增益:基于熵,对特征取值多的列有偏好。
- 增益率:给信息增益加了惩罚,防止特征取值太多导致过拟合。
我一般在做分类任务时用基尼系数就够了,计算快,结果跟信息增益差不了多少。但做文本分类时,特征特别稀疏,我建议用信息增益,它会更倾向选择有区分度的特征。
2.3.2 为什么决策树是随机森林的基石?
决策树有个天生的好处:非线性拟合能力强。它不假设特征之间存在线性关系,也不需要做特征缩放。比如你有一个特征取值是“红色、蓝色、绿色”,树可以直接按某个类别划分,而像SVM就得先独热编码。
另一个好处是可解释性。虽然随机森林几百棵树堆一起,解释性变差了,但单棵决策树可以画出来——你跟业务方交流时,拿出一个简单的树结构,说“年龄小于30且收入大于5万的用户流失概率高”,对方立刻就能理解。
我曾经有个项目,需要向风控部门解释模型为什么拒绝了一笔贷款。我用单棵决策树做了个“轻量级解释器”,虽然准确率降了两三个点,但合规那边直接通过了。这就是模型可解释性的力量。
2.4 随机森林:Bagging + 决策树 + 随机特征选择
把上面两块拼一起,就是随机森林了。那它跟最基础的Bagging决策树有什么不同?
核心区别:每次分裂时,不是从所有特征里选最佳切分点,而是随机选一个子集(通常为sqrt(total features)或log2(total features))。
为什么会这样?你想想看,如果数据里有一个“强特征”,所有树都会优先用它分裂。那每棵树长得都差不多,Bagging的效果就被削弱了。随机选特征,就是为了强迫不同的树从不同角度学东西。
顺便提一下,随机森林的随机有两层:一是随机选样本(行),二是随机选特征(列)。这两层随机让随机森林几乎成了“开箱即用”的模型。我记得有一次拿了一堆乱七八糟的数据,什么字段都有,用默认参数跑随机森林,效果居然比调了半天的逻辑回归还好——当然,后来我调了调,又提升了一些。
下面这个SVG图展示了随机森林从训练到预测的完整流程:
2.5 用代码感受一下
光看原理不过瘾?我给你一段scikit-learn的代码,跑跑就明白了:
from sklearn.ensemble import RandomForestClassifier
from sklearn.datasets import make_classification
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score
# 造一份分类数据
X, y = make_classification(n_samples=1000, n_features=20,
n_informative=8, random_state=42)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3)
# 创建随机森林模型
clf = RandomForestClassifier(
n_estimators=100, # 100棵树
max_depth=10, # 每棵树最多10层
max_features='sqrt', # 每次分裂随机选sqrt个特征
oob_score=True, # 用袋外数据评估
random_state=42
)
clf.fit(X_train, y_train)
# 看一下袋外分数和测试集准确率
print("OOB score:", clf.oob_score_)
y_pred = clf.predict(X_test)
print("Test accuracy:", accuracy_score(y_test, y_pred))
这里有几个参数,我重点讲一下:
- n_estimators:森林里树的棵树。我一般从100开始试。太少稳定性差,太多耗时增加但提升不大。
- max_features:每次分裂考虑的特征数。分类默认sqrt,回归默认全部特征。但如果你特征特别多,可以设小一点,比如log2。
- oob_score=True:利用没被抽到的样本做验证,省得再切一份验证集出来。
2.6 避坑指南
我曾经在一个信贷评分项目中用随机森林,跑出来准确率高达99%,心里还挺美。结果一上线,坏账率反而上升了——你知道为什么吗?
后来我发现:数据存在严重的类别不平衡。正样本(违约用户)只占5%。模型学到的全是“怎么识别不违约的人”,对真正的违约用户毫无分辨力。这个坑我踩得很痛。
所以给你几个我觉得实用的建议:
- 检查类别分布:用clf.classes_看看每类的样本数,如果严重失衡,考虑调class_weight='balanced'或者用SMOTE过采样。
- 不要迷信默认参数:sklearn的默认参数是通用型,但不是最优的。比如max_depth默认None,会在高维数据上过拟合。
- 看OOB误差曲线:画一下n_estimators从1到500的OOB误差变化,能找到最经济的树数量。通常一两百棵就稳定了,再增加就是浪费算力。