简介面向随机森林算法初学者的Python学习资料通过手写实现与配套数据帮助理解集成学习的核心原理。压缩包共4个文件包含2个Python脚本、1个CSV数据集和1个阅读说明整体仅34KB轻量便于快速下载。两份代码中一份来自网络整理、一份为作者重新编写均附有详尽中文注释并调试通过能清晰展示特征选取、决策树构建与投票集成的流程同时提供sonar数据集用于实践验证。需说明的是受数据集规模及默认参数影响当前正确率不算高但恰好可作为学习范例便于读者在此基础上调整参数、观察不同设置对模型的影响从而加深对随机森林调参的理解整体代码结构清晰注释覆盖关键步骤。已有15740人学习下载适合希望掌握随机森林底层实现并动手练习代码的Python初学者。1. 随机森林 Python 实现一个模型吃透分类、回归与特征筛选做结构化数据建模时随机森林几乎是我第一个上手的模型。它不像单棵决策树那样容易因为“背题”而翻车也不会让刚接触 Python 的人一上来就面对复杂的网络结构。随机森林的本质是把几十上百棵决策树的结果汇总起来分类看投票回归取均值顺带还能输出每个特征的重要程度。如果你手里有一份干净的中小规模表格数据想在短时间内拿到一个可靠的基线随机森林就是用起来最顺手的方案。这份 Python 代码和配套数据集正好覆盖分类、回归、特征重要性三条主线起步门槛很低参数改动空间却能撑起不少深入的话题。2. 随机森林为什么管用Bagging 与随机特征选择的协作机制2.1 决策树的短板低偏差、高方差单棵决策树的工作方式并不复杂从根节点开始递归地在特征上找分裂点让分裂后子节点的基尼不纯度或信息熵下降最多一直切到达到max_depth限制或者叶子节点足够纯为止。这个机制最大的问题是它太“努力”了。树会为了把训练集里每个样本都安排进正确的叶子把分裂边界切得非常碎。训练集上准确率接近满分换一批新数据边界稍微一偏结果立刻变差。这就是经典的低偏差、高方差问题偏差低说明模型对训练集的解释能力很强方差高说明结果对输入数据的小变动非常敏感。我见过很多初学者只调max_depth想解决这个问题但把深度压到很小之后树又变得过于粗糙欠拟合。实际上与其只在一棵树上反复权衡不如直接改变模型的组合方式。2.2 Bagging用有放回抽样把“吵架”变成“投票”Bagging 的全称是 Bootstrap Aggregating做法很直白从原始训练集里有放回地随机抽样生成 B 份子数据集每份子数据集训练一棵决策树最后把结果聚合。分类任务取多数投票回归任务取平均值。为什么这招能降方差可以这样理解如果每棵树犯错的方向不一致那么投票或平均之后错误会互相抵消。理想状态下如果 B 棵树之间的误差完全独立平均结果的方差会缩到单棵树的 1/B。问题在于同一个训练集里抽出来的子样本相似度很高树之间不可能完全独立它们会共享很大一部分“错误习惯”。所以光靠有放回抽样效果仍然有限。2.3 随机特征选择为什么必须限制 max_features随机森林在 Bagging 基础上加了一步关键操作每次节点分裂时不让树看到全部特征只允许从随机选出的一个特征子集里挑最优分裂特征。这个随机子集的大小就是max_features。在分类任务里scikit-learn 默认取特征总数的平方根在回归任务里默认取全部特征。这一步的作用是强行“去相关”。如果所有树都能看到同一个最强特征它们大概率会在同一位置分裂树之间长得越来越像投票结果基本等于一棵树的意见Bagging 就白做了。限制候选特征后部分树去不到最优特征只能在次优特征上寻找分裂方向树与树之间的结构差异被拉大。用专业一点的话说Random Forest Bagging 随机特征选择。两件事少了任何一件都不叫随机森林。2.4 选型边界什么样的数据适合随机森林随机森林的优势区间是比较明确的。它适合结构化表格数据特征以数十到数百维为常见场景样本量从几千到几十万都能跑对缺失值有一定容忍度不需要像线性模型那样严格做标准化。它在分类和回归上都能用还能直接给出特征重要性天然适合做基线模型和特征筛选。但也有不适合的场景。文本数据经过词袋或 TF-IDF 变换后会变得非常稀疏维度动辄几万甚至几十万随机森林在稀疏矩阵上分裂效率低效果也不如线性模型或朴素贝叶斯。图像和序列数据更适合卷积或循环网络。另外随机森林的预测过程要遍历全部树模型体量随n_estimators线性增长高并发低延迟的服务端推理场景里预测耗时和后端内存都是需要提前评估的隐形成本。选型时先确认自己手里的数据是稠密表格再动手能省掉后面一大半的调试时间。3. Python 实战分类、回归与特征重要性的完整代码3.1 环境准备与数据说明这份代码包依赖几个常用库numpy、pandas、scikit-learn、matplotlib。建议直接用以下命令安装python -m pip install numpy pandas scikit-learn matplotlib数据集没有用外网下载源而是选择了 scikit-learn 自带的数据集。分类部分用乳腺癌数据集load_breast_cancer()共 569 条样本、30 个数值特征目标是二分类回归部分用糖尿病数据集load_diabetes()共 442 条样本、10 个特征。这两个数据集的优点是不需要联网、格式统一、维度适中拿来验证随机森林的行为非常合适。如果你愿意用自己的数据只需要把data load_breast_cancer()换成pd.read_csv(your_data.csv)但要注意把特征矩阵和标签向量分开并确认没有泄漏预处理步骤必须在划分训练集和测试集之后再进行尤其是做缺失值填充或标准化的时候。3.2 分类任务完整训练与评估先看分类场景的核心代码from sklearn.datasets import load_breast_cancer from sklearn.model_selection import train_test_split, cross_val_score from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, roc_auc_score # 加载内置数据集 data load_breast_cancer() X, y data.data, data.target # 划分训练集和测试集stratify 保证正负样本比例一致 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) # 建立随机森林分类器 rf_clf RandomForestClassifier( n_estimators200, max_depth6, max_featuressqrt, min_samples_leaf2, oob_scoreTrue, random_state42, ) # 训练并在测试集上预测 rf_clf.fit(X_train, y_train) y_pred rf_clf.predict(X_test) y_proba rf_clf.predict_proba(X_test)[:, 1] # 输出评估指标 print(classification_report(y_test, y_pred)) print(fROC AUC: {roc_auc_score(y_test, y_proba):.4f}) print(fOOB Score: {rf_clf.oob_score_:.4f})代码逻辑并不复杂train_test_split先用固定随机种子把数据划分成训练和测试两部分stratifyy确保划分后训练集里正负样本比例跟原始数据一致避免某类样本在测试集里缺失。RandomForestClassifier里的n_estimators控制树的数量200 是一个性价比不错的值max_depth6限制单棵树的深度防止过拟合max_featuressqrt表示每次分裂只从特征总数的平方根个随机特征里选最优分裂点min_samples_leaf2要求每个叶子至少包含 2 个样本进一步平滑边界oob_scoreTrue会在训练过程中用袋外样本计算一个参考精度不需要额外划分验证集。跑完这段代码你通常会看到测试集 ROC AUC 在 0.98 左右OOB Score 在 0.95 到 0.97 之间。OOB 分数和测试集分数比较接近说明模型没有明显过拟合如果 OOB 很高但测试分数大幅下跌就要回到数据划分上找原因比如预处理泄漏或样本分布不一致。3.3 回归任务随机森林不只是分类器随机森林在回归上是一模一样的思路区别在聚合方式分类取少数服从多数回归取所有树预测值的平均。糖尿病数据集是典型的回归问题代码如下from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score data_diabetes load_diabetes() X_r, y_r data_diabetes.data, data_diabetes.target X_train_r, X_test_r, y_train_r, y_test_r train_test_split( X_r, y_r, test_size0.2, random_state42 ) rf_reg RandomForestRegressor( n_estimators300, max_depthNone, max_features1.0, min_samples_leaf1, random_state42, ) rf_reg.fit(X_train_r, y_train_r) y_pred_r rf_reg.predict(X_test_r) rmse mean_squared_error(y_test_r, y_pred_r, squaredFalse) print(fRMSE: {rmse:.4f}) print(fR2 Score: {r2_score(y_test_r, y_pred_r):.4f})这里故意把max_depth保持为None也就是不限制树深度让树自由生长然后靠 300 棵树去平均结果。回归任务中max_features1.0表示每棵树分裂时可以看到全部特征这是 scikit-learn 对回归器的默认设定。如果你发现单棵回归树输出方差很大可以用min_samples_leaf3之类稍大的值代价是拟合精度略微下降。比较常见的误区是拿 RMSE 单独判断模型好坏。RMSE 的单位跟预测目标完全一致但不同量纲的数据之间不可比。更重要的是看R2它表示模型解释了多少比例的目标方差。R2 接近 1 说明预测很贴合接近 0 说明模型基本等于瞎猜。3.4 特征重要性训练完之后最该看的东西随机森林训练完成后feature_importances_属性会给出每个特征的重要度分数分数之和为 1。这个分数基于节点的不纯度下降总量做加权平均可以直接用来做特征筛选import numpy as np import matplotlib.pyplot as plt importances rf_clf.feature_importances_ indices np.argsort(importances)[::-1] # 打印前 10 个重要特征 for i in range(10): print(f{i1}. {data.feature_names[indices[i]]}: {importances[indices[i]]:.4f}) # 画前 15 个特征的柱状图 plt.figure(figsize(10, 6)) plt.bar(range(15), importances[indices[:15]]) plt.xticks(range(15), data.feature_names[indices[:15]], rotation90) plt.tight_layout() plt.show()画图时需要把旋转角度设为 90 度否则特征名会重叠成一团这也是我第一次画特征重要性图时踩过的细节。特征重要性可以视为“这个特征在森林中被用来分裂的频率和贡献度”但它有一个明显偏向取值个数多的连续特征或高基数类别特征往往显得更重要因为它们被选中作为分裂点的机会更多。所以拿重要性做初筛可以遇到关键决策建议用后文的 permutation importance 复核。4. 参数调优该搜哪些参数、不该碰哪些参数4.1 参数敏感性地图先分清主次随机森林的参数不少但很多参数完全没有调的必要。我习惯把它们分成三档必调、偶尔调、基本不碰。参数默认值调整方向说明n_estimators100200-500太大后边际收益递减单调提升稳定性但增加训练和推理耗时max_depthNone小数据设 8-15大数据可以不限制限制树深度控制过拟合max_featuressqrt分类/ 1.0回归sqrt、log2、0.3-0.5控制每次分裂的候选特征数min_samples_leaf12-5叶子太薄容易记住噪声class_weightNonebalanced / 自定义字典类别不平衡时救命的参数criteriongini基本不用换换 entropy 通常提升极小白白增加时间n_estimators的值不是越大越好。200 棵之后精度曲线会变得非常平缓但推理耗时和模型体积还在线性增长。max_depth才是控制随机森林过拟合的核心旋钮尤其是小数据集上None很容易让每棵树都长到叶子纯净训练集分数满分测试集却一塌糊涂。max_features是最能体现“随机森林”本质的参数建议优先尝试sqrt和log2再从 0.3 到 0.5 之间选一轮。4.2 GridSearchCV 调参一份可以直接复制的网格网格搜索是最直观的调参方式适合参数组合不多的情况。下面这份网格覆盖了四个最重要的参数共 3×3×3×3 81 组组合配合 5 折交叉验证实际要训练 405 个随机森林模型。数据量不大时几分钟能跑完数据量大时请自觉换成随机搜索。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 300, 500], max_depth: [4, 8, None], max_features: [sqrt, log2, None], min_samples_leaf: [1, 2, 4], } grid_search GridSearchCV( estimatorRandomForestClassifier(random_state42), param_gridparam_grid, cv5, scoringroc_auc, n_jobs-1, verbose1, ) grid_search.fit(X_train, y_train) print(fBest params: {grid_search.best_params_}) print(fBest CV score: {grid_search.best_score_:.4f})网格的逻辑是让 scikit-learn 自动穷举 81 个参数组合每一组都跑 5 折交叉验证最后把平均 ROC AUC 最高的组合选出来。n_jobs-1表示用满所有 CPU 核心多核机器上能省一大半时间verbose1会在控制台打印进度方便确认程序还在跑而不是卡死了。必须强调的是GridSearchCV返回的best_score_是调参过程中选出来的最优交叉验证分数不是对最终模型的乐观估计。如果你拿着这个分数直接跟别人模型汇报属于把验证集当测试集用后面的测试集分数大概率会略低一点这不是翻车是统计上的正常现象。4.3 随机搜索替代方案组合多到网格跑不动时当样本量大、参数网格变宽网格搜索的成本会迅速失控。RandomizedSearchCV 不穷举所有组合而是从参数分布里随机抽固定数量的组合。通常抽 50 到 100 组就能覆盖到和网格搜索差不多水平的参数区间计算量小一个量级from sklearn.model_selection import RandomizedSearchCV from scipy.stats import randint param_dist { n_estimators: randint(100, 500), max_depth: [4, 8, 12, None], max_features: [sqrt, log2, None], min_samples_leaf: randint(1, 5), } random_search RandomizedSearchCV( estimatorRandomForestClassifier(random_state42), param_distributionsparam_dist, n_iter50, cv5, scoringroc_auc, n_jobs-1, random_state42, ) random_search.fit(X_train, y_train) print(fBest params: {random_search.best_params_})注意randint是 scipy.stats 里的分布对象抽样时从 100 到 500 均匀取值离散参数仍然用列表。调参的最终一步是用测试集评估最优参数y_pred random_search.predict(X_test)查看classification_report。测试集只在最后一刻碰一次能用尽量多次等下次有新数据再重新调。5. 随机森林避坑指南五条高频翻车记录与处理办法5.1 训练集得分满分测试集崩了现象训练集准确率 1.0测试集准确率只有 0.82看起来模型像中了邪。原因最常见的是max_depthNone让每棵树都无限生长加上min_samples_leaf1树把训练集里的噪声也当成了规律。小数据集上尤其明显几百个样本几棵树就能背完。解决先把max_depth压到 6 到 15 之间min_samples_leaf调成 2 或 3再观察 OOB 分数和测试集分数的差距。若差距仍然很大回到数据本身检查是否有特征泄漏例如把目标变量或目标变量的后处理结果混进了特征矩阵。5.2 特征重要性排名和业务认知完全相反现象业务上大家公认最重要的字段排在倒数一个取值特别杂的字段却排到了第一位。原因随机森林默认的特征重要性基于不纯度下降高基数的连续特征或类别特征更容易被选作分裂点树会频繁使用这些特征导致重要性虚高。解决用排列重要性复核。sklearn.inspection.permutation_importance的做法是打乱某个特征列的值观察模型指标下降多少下降越多说明该特征越重要。它不受分裂次数影响对高基数特征的偏差小得多。5.3 类别不平衡时少数类召回率直接归零现象二分类问题里正样本占比只有 5%测试集上精确率很高召回率却是 0.0。原因随机森林优化的是整体不纯度下降多数类对损失的贡献远大于少数类树根本学不到少数类的模式。默认参数下模型输出几乎全部是多数类。解决先尝试class_weightbalanced让 scikit-learn 根据类别频率自动加权。更精细的做法是手写字典class_weight{0: 1.0, 1: 5.0}权重代表少数类样本被错分时要承担多高的惩罚。如果数据太不均衡还要考虑按类别分别抽样或用RandomForestClassifier的class_weightbalanced_subsample后者会同时影响袋外样本和后代的训练权重。5.4 跑fetch_*数据集一直报网络错误现象代码里用了fetch_california_housing()之类需要联网下载的数据集运行时报连接超时或下载失败。原因这类数据集并不会随 scikit-learn 自动安装首次使用要从远程服务器拉取离线环境或网络受限时必然失败。解决开发环境如果联网受限直接用load_breast_cancer()、load_diabetes()、load_iris()这些随库附带的数据集。如果是自己的数据把 CSV 放到脚本同目录用pd.read_csv读取并确认没有额外空白字符或非法列名。5.5 predict 和 predict_proba 的结果对不上业务需求现象业务上希望把有 30% 可能性的样本也筛出来但predict的输出永远是 0仿佛概率没生效。原因predict不是直接输出概率而是以 0.5 为阈值把概率大于等于 0.5 的样本判为正类。当正类样本占比很低时大多数样本的概率都达不到 0.5于是全部被归为负类。解决不要改模型改决策边界。用predict_proba拿到第二列正类概率自己设定业务阈值y_proba rf_clf.predict_proba(X_test)[:, 1] y_pred_custom (y_proba 0.35).astype(int)阈值 0.35 意味着只要模型认为有 35% 的概率为正类就纳入正类名单。调阈值时要同时看精确率和召回率的变化曲线优先用 PR 曲线选择业务可接受的点而不是只看 ROC AUC。6. 模型持久化与手写验证把基线能力沉淀成习惯6.1 用 joblib 保存模型和特征列顺序模型调好之后一定要落盘不然下次重跑又是一次完整训练。scikit-learn 官方推荐的持久化工具是 joblibimport joblib joblib.dump(rf_clf, rf_model.joblib) # 记录训练时的特征列顺序防止新数据的列顺序不一致 feature_names list(X_train.columns) joblib.dump(feature_names, feature_names.joblib)加载模型时先joblib.load模型和特征列表再对新数据按同样的 column 顺序做reindex。这步容易被忽略pandas 后续读取的 CSV 如果列顺序变了模型预测出来的结果哪怕计算不出错含义也可能是错的。跨 Python 版本部署时要特别注意 joblib 序列化的兼容性我的习惯是上线前在目标环境里重新跑一次保存和加载循环避免到生产环境才暴露 dtype 或 pickle 版本问题。6.2 用一份手写代码验证 bagging 思想理解了随机森林之后值得手写一个简化版来验证直觉。下面这个函数只实现 bootstrap 抽样和随机特征选择不处理剪枝但能让你看到“不同树之间为什么不一样”import numpy as np from sklearn.tree import DecisionTreeClassifier def simple_rf(X_train, y_train, X_test, n_trees50, seed42): n_samples, n_features X_train.shape rng np.random.default_rng(seed) trees [] for _ in range(n_trees): # 有放回抽样样本数跟原训练集一致但允许重复 idx rng.choice(n_samples, sizen_samples, replaceTrue) # 随机选 sqrt(n_features) 个特征 cols rng.choice(n_features, sizeint(np.sqrt(n_features)), replaceFalse) tree DecisionTreeClassifier(max_featuresNone) tree.fit(X_train[idx][:, cols], y_train[idx]) trees.append((tree, cols)) # 每棵树各自预测最后取众数作为投票结果 votes np.array([tree.predict(X_test[:, cols]) for tree, cols in trees]) return np.array([np.bincount(votes[:, i]).argmax() for i in range(votes.shape[1])])这里replaceTrue是 bootstrap 的核心有的样本会重复出现有的样本会被漏掉正是这些漏掉的样本构成了 OOB 的天然验证集。cols的随机选择让不同树看到不同视角的特征空间即使第一棵树的最优特征是 A第二棵树可能根本看不到 A只能从 B、C、D 里选两棵树的结构差异被强制拉开。这个简化版没有做剪枝也没有调参数但在中小数据集上通常能拿到和正规随机森林八成的效果用来验证“投票为什么比单棵树稳”完全够用。从那以后我每拿到一个新数据集都会先跑一遍随机森林基线记录 OOB 分数、交叉验证分数和测试集分数再决定要不要换梯度提升或线性模型。这份代码和数据集正是按这个习惯整理的先把基线稳住再去追求复杂的模型能少走很多弯路。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?