ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

随机森林全攻略:从集成原理到Python调参与遥感应用

随机森林全攻略:从集成原理到Python调参与遥感应用 说起集成算法随机森林绝对是多数人入门机器学习时最先接触到、也最容易让人产生“原来还可以这么玩”的模型之一。它把“三个臭皮匠顶个诸葛亮”这件事用数学方法做到了极致训练一批决策树最后让它们投票或者取平均。这篇文章不打算把公式从头抄一遍而是从实际工程和科研应用的角度把随机森林从底层逻辑讲到Python代码从调参策略讲到遥感影像分类再附上一些踩坑记录。刚入门的朋友可以按顺序读用过但没深究细节的同行可以重点看参数解释和问题排查。1. 随机森林为什么能“神”集成算法的底层逻辑1.1 从单一决策树的困境说起决策树本身是一种非常直观的模型它通过一系列“如果特征满足某条件就走向哪边”的规则把样本空间切分开。一棵树训练完之后可以画成一张流程图解释性很强而且对输入特征是否标准化不敏感这对我们做表格数据非常友好。但它有一个很烦人的问题单棵树的方差很大。什么意思呢训练数据稍微换一批树的结构可能完全不一样预测结果波动也会很明显。打个比方你到了一个陌生小区想找一家最好吃的面馆问一个人他可能因为自己只吃过附近几家而给你指错路但如果你随机拦住几十个路人让他们投票推荐大多数情况下得到的结果会更靠谱。这里随机森林做的就是把“问几十个人投票”这件事变成算法每一棵决策树相当于一个“有一定自身经验的判断者”树和树之间有差异最后一起“投票决策”从而压制单棵树的错误。这种“多个模型组合起来一起做决策”的思路就是集成算法的核心价值。它并不是发明了一个全新的孤立模型而是通过巧妙地组合一堆基础模型让整体性能比任何单棵树都更好。1.2 Bagging让每棵树都看到不同的世界要让几十棵树组合起来有效前提是树之间要有差异不能每棵树都长得一模一样。随机森林使用的第一种“制造差异”的手段叫Bagging全称是Bootstrap Aggregating中文通常叫引导聚合或自助聚合。具体做法很简单假设原始训练集有N个样本第一步从这N个样本中有放回地随机抽取N个样本作为一棵新树的训练集。因为有放回每次抽取时有些样本会被抽到多次有些样本可能一次也抽不到。算一下概率就知道一个样本在每次抽取中被抽中的概率是1/N抽完N次后大约有63.2%的原始样本会出现在某一棵树的训练集里剩下的约36.8%就成为了这棵树的“袋外数据”简称OOB。每棵树都在不同的自助样本集上训练相当于给每个“判断者”安排了一段不同的“见识”树与树之间的相关性天然就降低了。最终做分类时每棵树投一票少数服从多数做回归时把每棵树的预测结果取平均。由于偏差不会因为抽样而明显增加而方差因为平均作用被显著降低整体泛化能力通常比单棵树强一大截。1.3 随机子空间不只是数据随机特征也要随机如果只用Bagging其实就已经能产生一定多样性了但是在特征维度很高或者特征间相关性强的场景下很多树可能都把注意力放在同一批强特征上树之间的差异仍然有限。随机森林在Bagging之外还多做了一个动作每一棵树在每次节点分裂时不是从全部特征里去选最优划分而是先从全部特征中随机挑出一个子集再在这个子集里选择最优特征和最优切分点。这个“随机子空间”思想是随机森林和普通Bagged Tree最本质的区别。分类任务中特征子集大小一般取特征总数的平方根回归任务中一般取特征总数的三分之一也可以根据实际效果调整。这样做之后每棵树的分裂视野被刻意限制了强特征不会每次都被选到弱特征也会有展示机会树与树之间的差异进一步变大。把Bagging和随机子空间放在一起你就明白随机森林为什么叫“随机”了样本随机、特征随机双随机让整片森林既有广度又有稳定性。2. 随机森林关键参数与调参实操2.1 核心参数速查n_estimators、max_depth、max_features随机森林在sklearn里用起来特别简单往往两三行代码就能跑出不错的结果但想要把它的性能发挥到最好有几个参数是绕不开的。先说最重要的三个。n_estimators是树的数量。树太少模型方差大树太多收益逐渐递减计算成本、内存占用和预测时间都会上升。实践里我一般先在100~200左右起步看交叉验证分数是否还在明显上升如果还在上升就继续加如果基本平稳就不必盲目堆到几百上千。max_depth是树的最大深度。随机森林默认不限制深度让每棵树自由生长由于有样本和特征双重随机性整体模型通常不会像单棵决策树那样轻易过拟合。但在噪声比较多、数据规模又不大的场景下限制深度或者把min_samples_leaf调大一点往往能让模型更稳。max_features是每次分裂时考虑的特征数量。它是影响随机森林随机程度的关键参数。分类默认是sqrt回归默认是1.0。参数值越小随机性越强单棵树更弱参数值越大树之间相关性越高接近Bagging效果。下面是我常用的一份参数参考表参数名默认值作用设太小设太大n_estimators100森林中树的数量方差大精度低训练和推理耗时增加收益递减max_features分类sqrt回归1.0每次分裂随机选取的特征数单棵树太弱整体偏差增加树间相关性增加丧失随机性max_depthNone单棵树最大深度模型欠拟合规则太简单树过长可能局部过拟合min_samples_leaf1叶子节点最少样本数对噪声敏感可能过拟合模型过于平滑精度下降min_samples_split2内部节点继续分裂所需最少样本数分裂过细树太粗可能欠拟合class_weightNone分类任务中各类别权重少数类被忽略可根据样本比例调整2.2 调参顺序与验证曲线随机森林参数组合很多最忌讳的是上来就开一个大网格做全量GridSearchCV因为行列数稍微一多训练时间就会爆炸。我个人的调参顺序是先粗调n_estimators确定一个计算量和性能兼顾的树数量再调max_features这是影响随机森林核心随机度的参数接着看max_depth和min_samples_leaf用来控制过拟合最后微调min_samples_split和class_weight。如果参数空间比较大建议用RandomizedSearchCV而不是GridSearchCV因为前者可以在指定迭代次数内随机搜索组合同等时间内覆盖范围更大。举个例子from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import RandomizedSearchCV param_dist { n_estimators: [100, 200, 300, 500], max_features: [sqrt, log2, 0.5, 0.8], max_depth: [None, 10, 20, 30], min_samples_leaf: [1, 2, 4], max_samples: [0.7, 0.8, 1.0] } rf RandomForestClassifier(random_state42, n_jobs-1) search RandomizedSearchCV( rf, param_distributionsparam_dist, n_iter40, cv5, scoringroc_auc, verbose1, random_state42 ) search.fit(X_train, y_train) print(search.best_params_)注意代码里我加了max_samples也就是每棵树实际使用的样本抽样比例。默认是1.0表示每棵树都用Bagging的N个样本如果你希望进一步降低树间相关性可以试试0.7~0.9有时候效果会出奇地好。2.3 过拟合与欠拟合的平衡OOB_score与样本权重很多人觉得随机森林天生抗过拟合所以完全不约束参数。实际上在面对高噪声数据时如果每棵树都无限制生长细节会被噪声记录进叶子节点最终同样会过拟合。这时候一个非常实用的工具是OOB_score在创建随机森林时直接设置oob_scoreTrue训练结束后会自动用每棵树的袋外数据做验证得到一个近似交叉验证的分数。rf RandomForestClassifier(oob_scoreTrue, random_state42) rf.fit(X_train, y_train) print(rf.oob_score_)我之前在一个信贷评分项目里遇到过这种情况训练集准确率98%测试集只有87%OOB分数大概在88%左右。后来把min_samples_leaf从1调到5max_depth限制到15训练集准确率降到92%测试集提到了91%。这说明观察训练集和OOB/测试集的差距比单纯追求训练集高分要重要得多。另外如果分类任务中类别不平衡比较严重可以用class_weightbalanced_subsample。这个参数不仅会按样本频率调整权重还会在Bagging抽样的每个自助子集上重新调整相当于在随机森林里专门给少数类“加戏”。如果效果还不够再考虑SMOTE等过采样方法但要注意先划分训练集再做处理否则数据泄露会给你一个虚高的精度。3. 随机森林分类与回归动手写Python代码3.1 分类任务实现我猜很多人学随机森林的第一个完整代码就是分类。这里我用经典的iris鸢尾花数据集演示不是为了炫技而是帮大家把整个流程走顺训练集划分、模型训练、预测评估、特征重要性输出。from sklearn.datasets import load_iris from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import accuracy_score, classification_report data load_iris() X data.data y data.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) rf RandomForestClassifier( n_estimators200, max_depth5, min_samples_leaf2, random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred rf.predict(X_test) print(准确率:, accuracy_score(y_test, y_pred)) print(classification_report(y_test, y_pred, target_namesdata.target_names)) print(特征重要性:, rf.feature_importances_)这个数据集本身维度低、样本结构清晰随机森林随便调调就能达到95%以上准确率。注意我用stratifyy做分层抽样保证训练集和测试集里三个类别的比例都接近原始分布。如果你自己的分类任务也是多分类这种划分习惯建议保留。训练完之后rf.feature_importances_会输出每个特征的重要性得分所有特征得分之和等于1。在iris数据集里你会发现花瓣长度和花瓣宽度的重要性通常远高于萼片长度和萼片宽度这跟领域经验是一致的。3.2 回归任务实现随机森林回归算法和分类的实现很相似区别主要在模型、评估指标和输出。回归树在每个叶子节点存放的不是类别投票而是落在这个叶子里的训练样本目标值的平均。最后预测时所有树的结果取平均值。我常用加州房价数据集来演示回归任务因为它是sklearn内置数据集中比较适合练手的。from sklearn.datasets import fetch_california_housing from sklearn.ensemble import RandomForestRegressor from sklearn.model_selection import train_test_split from sklearn.metrics import mean_squared_error, r2_score housing fetch_california_housing() X housing.data y housing.target X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) rf_reg RandomForestRegressor( n_estimators300, max_depth15, min_samples_leaf4, random_state42, n_jobs-1 ) rf_reg.fit(X_train, y_train) y_pred rf_reg.predict(X_test) mse mean_squared_error(y_test, y_pred) r2 r2_score(y_test, y_pred) print(MSE:, mse) print(R2:, r2)回归任务里我特别想提醒一点随机森林回归算法的外推能力很弱。因为每棵树的输出都是叶子节点上目标值的平均当测试样本的特征取值超出训练集范围时树结构里根本没有对应区域预测结果会被限制在训练集目标值的范围内。如果你预测的是一个有明显趋势、未来值可能超过历史最大值的时间序列随机森林不是好选择线性回归或者专门的时序模型会更合适。回归任务里还有个小细节max_features默认是1.0也就是每次分裂考虑全部特征。如果特征数量很大我一般会把它调整为例0.3~0.5这样能增加树的多样性同时降低特征间的竞争问题。3.3 特征重要性的两种类型与解读陷阱随机森林一个非常吸引人的副产品就是特征重要性。但这东西用的时候有坑。默认的feature_importances_是基于不纯度减少量的重要性它统计的是每次分裂时该特征带来的不纯度下降总量然后按树的数目平均。这个指标会天然偏向取值维度高、连续型强的特征比如一个随机的连续噪声特征如果被选中的次数多重要性也可能不低。遇到这类情况我建议使用permutation importance也就是置换重要性。基本思路是随机打乱某个特征的值破坏它与标签之间的关系观察模型预测指标下降多少。下降越多说明这个特征对模型越重要。from sklearn.inspection import permutation_importance result permutation_importance( rf, X_test, y_test, n_repeats10, random_state42, scoringaccuracy ) for i in range(X_test.shape[1]): print(f{housing.feature_names[i]}: {result.importances_mean[i]:.4f})需要注意特征重要性描述的是模型内部的依赖关系不等于真实世界的因果关系。你可能发现A特征对模型贡献很大但这只是因为A特征和B特征相关真正影响业务的可能是B。做业务解释时要结合permutation importance、SHAP甚至简单的单变量分析一起看不要拿一个feature_importances_图就下结论。4. 随机森林的进阶应用遥感、大数据与工程部署4.1 遥感随机森林像素级分类实战遥感领域几乎是随机森林的“主场”之一原因很简单遥感影像波段多、地域覆盖大、样本标注成本高而随机森林能处理高维特征训练速度快对数据分布假设要求低还能输出每个类别的概率。遥感随机森林分类的一般流程是这样的先获取多光谱影像比如哨兵二号或者Landsat整理出可见光、近红外、短波红外等多个波段然后计算一些光谱指数例如NDVI归一化植被指数、NDWI归一化水体指数这些指数能放大植被和水体之间的差异接下来在影像中选取若干带有类别标签的像元作为训练样本比如水体、林地、农田、裸地、建筑等每个像元的所有波段值和指数值拼接起来就是一行特征向量类别就是标签再用随机森林训练最后对整个影像逐像元预测并输出一张分类图。这里有个很关键的经验遥感分类中样本不是越多越好而是越有代表性越好。空间自相关性极强一块区域里的相邻像元通常高度相似如果你从同一地块里圈了一大堆训练样本模型很容易学会“位置记忆”而不是“光谱规律”精度虚高。更稳妥的做法是分区域抽样一块地物区域只取若干像元作为训练样本另一块独立区域作为验证样本。我用这种做法做过一次地物分类验证集精度下降了大约5个百分点但换来了更真实的精度估计。另外遥感分类的类别分布往往天然不平衡。比如一个小范围的项目中水体可能只占5%建筑占40%。这时如果直接用默认参数模型会偏向大类别。建议设置class_weightbalanced_subsample或者用分层抽样确保每个类别在训练集中都有足够比例。4.2 随机森林在大规模数据下的性能优化随机森林在中小规模数据集上跑得飞快但到了百万级样本、上万维特征时也需要仔细优化。首先是并行计算设置n_jobs-1会让所有CPU核心一起来训练训练时间几乎可以线性下降。不过要注意如果同时用RandomizedSearchCV做交叉验证n_jobs会被交叉验证和树同时消耗可能把内存跑满建议让交叉验证用n_jobs4树内部用n_jobs1之类的组合。其次是特征数量控制。随机森林对冗余特征有一定耐受性但无用的高维特征会拉长每次分裂的搜索时间也会污染特征重要性排序。可以先做一轮简单的单变量筛选或基于permutation importance降维把明显没有贡献的特征剔除再重新训练。随机森林还有一个特点不能增量学习。sklearn的RandomForest没有partial_fit方法如果你想在新增数据上更新模型只能把旧模型和新数据合并后重新训练。对于超大规模数据流可以考虑Spark MLlib中的随机森林实现或者在工程上引入在线学习的替代方案比如LightGBM在增量数据上可以继续训练。普通业务场景如果数据量还没有到单机内存扛不住的地步直接用随机森林就好别用复杂分布式架构给自己找麻烦。4.3 随机森林的局限与替代方案再好的算法也有短板。随机森林最大的问题有三个回归外推能力差、可解释性弱于单棵决策树、数据集特别大时有内存压力。另外随机森林对无关特征的增加不那么敏感但无关特征多了之后会稀释有效特征被选中的机会导致整体性能缓慢下降。在实践里如果你已经有了一个稳定的随机森林模型但追求更高精度可以考虑梯度提升树系列比如XGBoost、LightGBM。它们在很多结构化数据竞赛中精度更高可调参空间也更大但代价是更容易过拟合调参成本直线上升。我的经验是新项目拿到数据后先花15分钟跑一个默认参数随机森林把结果当成baseline如果业务要求不高这个baseline可能已经够用了如果还要更高精度再上LightGBM对比。这种策略可以避免一开始就陷入复杂的调参泥潭。5. 常见问题与排查技巧实录5.1 常见问题速查表随机森林使用频率高提问也特别集中。我把这些年遇到的典型问题整理成了一个速查表排查时可以对照着看症状可能原因解决思路训练速度极慢树太多、特征维度太高、无用特征多降低n_estimators设置max_features先做特征筛选内存溢出数据量大且每棵树完全展开限制max_depth调大min_samples_leaf减小max_samples训练集超高但测试集低树长得太细噪声被记录增加min_samples_leaf限制max_depth观察OOB分数特征重要性分布奇怪特征强相关、噪声特征多用permutation importance剔除部分重复特征少数类预测很差类别不平衡class_weightbalanced_subsample分层抽样调整阈值回归预测结果总是落在训练集目标值范围内数据有趋势树模型外推能力弱换线性模型或时序模型必要时对目标做差分处理预测概率不靠谱树模型概率校准度差用predict_proba时注意校准可选CalibratedClassifierCV5.2 调参与特征工程经验分享这里说几个我亲测有效的细节。第一个是不要一上来就把所有类别特征全部独热编码。随机森林本身基于树结构支持切分“某个特征值等于某类”如果你把有序的类别变量比如学历小学、初中、高中、大学做成独热不仅维度膨胀还会丢失顺序信息。对于这种有序类别我一般直接用整数编码。对于无序类别且类别数量很多的情况独热后很可能出现几百列稀疏特征这会拖慢训练而且会稀释重要性排序。更推荐的做法是先用OrdinalEncoder把类别转成编号让树自己决定怎么切分有时候效果会比独热更好。第二个是缺失值处理。很多教程说随机森林可以容忍缺失值确实有一定的容忍度但sklearn里的实现并不会自动利用缺失信息做分裂。保险起见我先用SimpleImputer统一填充中位数或众数再把带有缺失标志的额外特征加进模型这样模型能学会“缺失本身也是一种信息”。第三个是分类阈值调整。随机森林预测的是概率默认以0.5为阈值。如果业务更关注召回率或者精确率不要急着调模型参数。先画出PR曲线或者ROC曲线找到最合适的阈值再去做业务决策。我见过很多人因为准确率上不去就一直调参结果换了一个阈值之后问题就解决了。5.3 从项目复盘看随机森林的决策边界最后用一个虚构但很有代表性的项目来复盘。假设我们在做一个借款用户违约预测特征包括年龄、收入、历史逾期次数、近半年查询次数、负债率等目标是预测用户是否会违约。第一版用逻辑回归AUC大概0.75特征少解释清晰。后来换成随机森林默认参数直接让AUC到了0.82这意味着模型抓住了很多非线性关系。但在业务侧遇到了新麻烦随机森林输出的变量重要性里历史逾期次数一枝独秀其他特征几乎看不见。这并不完全合理。通过permutation importance进一步验证发现收入、负债率也有明显贡献只是因为它们和逾期次数有相关性在基于不纯度的重要性中被压低了。最后删掉了一些高度相关的冗余特征重新训练后模型精度保持稳定特征重要性分布也更容易向业务解释。这个案例告诉我们随机森林的黑盒属性只是相对单棵树而言它依然可以借助特征工具变得“半透明”。不要只盯着准确率或AUC还要关心特征稳定性和业务逻辑一致性否则模型上线后遇到分布偏移很容易被业务方质疑。最后再分享一个我常用的习惯每次用随机森林跑完一个项目我都会顺手看看OOB分数和测试集分数的差异。如果差距小于两个百分点说明模型泛化比较健康如果差得很多一定先返查数据和标签而不是急着调参。这个习惯帮我少走了很多弯路也算是在随机森林使用中最值得养成的一个好习惯。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进