简介:随机森林模型代码资源面向数据科学初学者及需要快速上手集成学习的开发者,提供从原理到实践的完整学习材料。压缩包共66个文件,大小29.8MB,涵盖14个cpp与12个m源码、4个mexw64/32编译文件、Matlab数据及PPT、演示视频、说明文档等,便于对照代码理解Bootstrap采样、特征随机选择与多树投票预测机制。目前已有3727人学习,资源包含随机森林PPT讲解、实际使用演示录屏以及配套的Matlab和C++实现,可支持分类与回归任务实践。通过调整树数量、树深度、特征选取策略等参数,读者能逐步掌握模型优化方法,并借助特征重要性分析提升模型解释能力,适用于信用评分、疾病诊断等典型场景。
1. 随机森林不是“多棵树的投票”:优势藏在两次随机扰动里
随机森林是机器学习里少有的“看着简单、实际很难翻车”的模型,也是我拆过的代码包里最常见的基线模型。它比单棵决策树稳,比 XGBoost 少一堆调参玄学,处理几十万行表格数据、几十个特征,一台笔记本就能跑完。经常有读者问我随机森林好还是 LightGBM 好,我的回答是:如果你要的是快速上线、可解释性和不用太精细的调参,随机森林往往更划算。这份随机森林模型代码覆盖分类、回归、特征重要性和遥感分类场景,适合做风控评分、土壤分类、植被提取、房价预测这类需求的人,拿到手删掉多余模块就能直接用。
2. 从 sklearn 跑通第一版:随机森林分类与回归的代码骨架
2.1 先看原理:样本扰动与特征扰动缺一不可
开始之前先把概念立住。随机森林是 Bagging 集成与 CART 决策树组合的产物,核心不是“树多”,而是两次随机。第一次随机叫样本扰动,每一棵子树通过自助采样(bootstrap)从原始训练集中有放回地抽出一批样本,大约 63% 的样本被抽中,剩下 37% 成为袋外数据(Out-of-Bag,简称 OOB),可以直接用来做无交叉验证的误差估计。第二次随机叫特征扰动,每棵树分裂时不是从全部特征里找最优切分点,而是先随机抽一个特征子集,再在这个子集里找最优分裂。这两次随机让树与树之间的相关性降下来,集成之后的方差才能真正被压住。
有人说“树越多模型越稳”,这话只对了一半。树数量够多之后,随机森林的误差会趋于一个稳定平台,再往上加树,精度提升微乎其微,训练时间倒是实打实往上走。所以代码里的 n_estimators 不是越大越好,得结合训练成本找拐点,这个我会在第 5 章展开。选型上直接给一个对比表,你就能理解为什么随机森林是多数表格任务的默认起点:
| 维度 | 单棵决策树 | 随机森林 | XGBoost |
|---|---|---|---|
| 过拟合风险 | 高,容易长成完全树 | 低,两层随机天然抗过拟合 | 中,需要配合正则项 |
| 调参成本 | 低 | 低,3~4 个参数即可 | 高,有十几个参数要照顾 |
| 训练速度 | 快 | 快,可并行到全 CPU 核 | 慢一些,但精度上限更高 |
| 可解释性 | 可以完整画树 | 特征重要性 + 树结构的折中 | 特征重要性可用,但树难画 |
| 小样本表现 | 容易抖动 | 相对稳定 | 容易在验证集上反复波动 |
随机森林不是精度上限最高的模型,但它几乎不需要预处理:特征量纲不一致没问题,缺失值有内置的近似策略,类别特征可以不做独热直接喂。这些特性让它特别适合“快速验证一个表格问题能不能学出来”的阶段。
2.2 分类器最小可用代码与三个必调参数
随机森林在 sklearn 里封装得很成熟,写代码的重点不在“从零实现算法”,而在把参数调到适配你的数据结构。下面是我常用的一份最小分类代码,可以直接存成 rf_classifier_demo.py 跑起来:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import classification_report, confusion_matrix # ---------- 1. 数据装载,X 为特征矩阵,y 为目标标签 ---------- data = pd.read_csv('your_dataset.csv') X = data.drop(columns=['target']) y = data['target'] # ---------- 2. 切分训练集与测试集 ---------- X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42, stratify=y ) # ---------- 3. 建模 ---------- rf = RandomForestClassifier( n_estimators=300, # 树数量,先给 300,后续用 OOB 找拐点 max_depth=12, # 限制深度,防止单棵树过拟合 min_samples_leaf=3, # 叶节点最少样本数,控制分裂粒度 max_features='sqrt', # 分类任务默认用 sqrt(特征数) n_jobs=-1, # 用满所有 CPU 核心 random_state=42, # 固定随机种子,保证可复现 class_weight='balanced' # 类别不平衡时自动加权 ) rf.fit(X_train, y_train) # ---------- 4. 评估 ---------- y_pred = rf.predict(X_test) print(classification_report(y_test, y_pred)) print(confusion_matrix(y_test, y_pred))逻辑说明:训练集和测试集用 stratify 做分层切分,保证每个类别的比例在两边保持一致。这一步在类别不平衡的数据上尤其关键,否则测试集里某个类别可能连一个样本都抽不到,后面的 classification_report 会直接报错甚至给出误导性分数。模型里的 n_jobs=-1 是把所有 CPU 核心拉满,几十万行数据也能在可接受时间内跑完。
参数说明里最值得花时间的是 max_depth 和 min_samples_leaf。max_depth 控制树的最大深度,不限制的话树会一直分裂到每个叶节点都纯净,训练集上几乎零误差,但测试集方差会很大。min_samples_leaf 控制叶节点最少样本数,它比 min_samples_split 更实用,因为直接约束了每个叶子上的最小样本量,对控制过拟合更稳。max_features 这里用 'sqrt',即每次分裂随机抽 sqrt(特征数) 个特征,这是分类任务的标准做法,特征维度很高时不要轻易改成全部特征,否则树之间会变得过于相似。
2.3 回归任务:连续值场景下参数要改什么
回归和分类在代码结构上几乎一样,但有三处参数语义完全不同。一是分裂依据,分类用 Gini 不纯度或信息熵,回归用均方误差(MSE)或绝对误差(MAE)。二是评估指标,分类看准确率和 F1,回归看 R²、RMSE、MAE,你不能拿准确率去要求回归模型。三是 max_features 的默认建议,分类默认取 sqrt(特征数),回归默认取特征总数,也就是每次分裂考察全部特征。
from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import r2_score, mean_squared_error, mean_absolute_error rf_reg = RandomForestRegressor( n_estimators=300, max_depth=10, min_samples_leaf=5, max_features=1.0, # 回归任务通常允许全部特征参与分裂 n_jobs=-1, random_state=42 ) rf_reg.fit(X_train, y_train) y_pred = rf_reg.predict(X_test) print('R2: %.4f' % r2_score(y_test, y_pred)) print('RMSE: %.4f' % np.sqrt(mean_squared_error(y_test, y_pred))) print('MAE: %.4f' % mean_absolute_error(y_test, y_pred))逻辑说明:回归场景我一般会把 min_samples_leaf 从 3 提高到 5 甚至 8,因为连续值预测对极端值更敏感,叶子分得太细会把噪声也学进去。max_features 设成 1.0 表示每次分裂考察全部特征,这是回归任务的常见起始值;如果你的特征维度超过几百,再退回到 0.7 或 'sqrt' 换取训练速度。
参数说明:R² 接近 1 说明模型解释了大部分方差,但只有一个数不够,必须配合 RMSE 看绝对误差量级。比如房价预测的 RMSE 是 2 万还是 20 万,直接决定模型能不能投入使用。另外记住一点:随机森林回归不会外推,它只能预测训练集样本值范围内的结果,这是它与线性回归最大的差异。做时序外推、销量预测这种需要预测历史区间之外数值的任务时,别拿随机森林硬扛。
3. 特征重要性与参数调优:把模型从黑匣子变成可解释工具
3.1 特征重要性两种算法:MDI 与置换法
随机森林最吸引人的地方是它自带特征重要性输出。sklearn 里 rf.feature_importances_ 默认用的是 MDI,也就是基于不纯度下降的平均值。这个值好算,但有个坑:它对高基数特征,比如用户 ID、城市编码这类取值很多的列,会给出虚高的分数。原因很简单,离散特征取值越多,越容易被找到“看起来”很厉害的分裂点,但换成另一份数据,这个分裂往往靠不住。
我实际处理时是先用 MDI 跑一遍排序,再用置换重要性复核。置换重要性的思路是把某个特征的值随机打乱,看模型分数掉多少;掉得越多说明该特征越关键;打乱了分数不掉,说明它本来就没参与多少决策。
from sklearn.inspection import permutation_importance # 先看 MDI 排序 imp_mdi = rf.feature_importances_ sorted_idx = np.argsort(imp_mdi)[::-1] for i in sorted_idx[:10]: print('MDI %s: %.4f' % (X.columns[i], imp_mdi[i])) # 再用置换重要性复核前 10 个特征 perm_res = permutation_importance( rf, X_test, y_test, n_repeats=10, # 每个特征打乱 10 次,取均值与标准差 random_state=42, n_jobs=-1 ) sorted_perm_idx = np.argsort(perm_res.importances_mean)[::-1] for i in sorted_perm_idx[:10]: print('Perm %s: %.4f ± %.4f' % ( X.columns[i], perm_res.importances_mean[i], perm_res.importances_std[i] ))逻辑说明:permutation_importance 是在模型训练完之后,在测试集上执行的,不消耗额外训练时间。n_repeats=10 表示每个特征打乱十次,输出均值和标准差。标准差大说明该特征对预测结果的影响很不稳定,即使均值高也要谨慎。
参数注意:置换重要性比 MDI 慢很多,特征上百个时 n_repeats 先用 5 跑一遍,确定候选特征池后再加大重复次数。另外做置换重要性时测试集不能太小,否则打乱一两次分数波动就很大,结果里全是噪声。特征重要性我只用来做排序和筛选,不会把它当成真实现对预测贡献的绝对权重,这个认知能帮你少走很多弯路。
3.2 用网格搜索锁参数:先调深度还是先调树数量
随机森林调参有个顺序问题:先调树数量还是先调深度,结果差别很大。我建议的顺序是,先用默认 max_features 和较大的树数量跑一遍 OOB,找到精度平台,再调 max_depth 和 min_samples_leaf 控制过拟合,最后回头微调 max_features。把顺序反过来容易踩坑:深度没限制时,树数量增加会放大过拟合,验证集分数反而下降。
网格搜索代码直接用 GridSearchCV:
from sklearn.model_selection import GridSearchCV param_grid = { 'n_estimators': [200, 300, 500], 'max_depth': [8, 12, None], 'min_samples_leaf': [1, 3, 5], 'max_features': ['sqrt', 0.7] } grid = GridSearchCV( RandomForestClassifier(random_state=42, n_jobs=-1, class_weight='balanced'), param_grid, cv=5, # 5 折交叉验证 scoring='f1_macro', # 类别不平衡时用 f1_macro 更稳 n_jobs=-1, verbose=1 ) grid.fit(X_train, y_train) print('best params:', grid.best_params_) print('best score:', grid.best_score_)网格大小要算一笔账:4 个参数,3×3×3×2=54 组组合,每组 5 折交叉验证,等于要训练 270 次随机森林。数据量大的话,这个搜索要跑到天荒地老。所以网格搜索之前,先用上一节的特征重要性把特征数量砍到 50 个以内,再考虑上 GridSearchCV。
参数说明:scoring 在类别不平衡时不要用默认 accuracy,f1_macro 对每个类别取了平均,能逼着模型照顾小类别;如果你更在意少数类的查全率,可以换成 'recall'。cv=5 是常规选择,数据量少于几千行时改成 cv=3,否则每次验证的训练集太小,分数波动会掩盖真实的参数好坏。
3.3 学习曲线判断瓶颈:多凑树还是多凑数据
网格搜索能调参数,但它回答不了“模型到底缺数据还是缺复杂度”这个问题。学习曲线是判断训练瓶颈的手段:横轴是训练集大小,纵轴是训练分数与验证分数。
import matplotlib.pyplot as plt from sklearn.model_selection import learning_curve train_sizes, train_scores, valid_scores = learning_curve( rf, X_train, y_train, train_sizes=[0.2, 0.4, 0.6, 0.8, 1.0], cv=5, scoring='f1_macro', n_jobs=-1 ) train_mean = train_scores.mean(axis=1) valid_mean = valid_scores.mean(axis=1) plt.plot(train_sizes, train_mean, label='train') plt.plot(train_sizes, valid_mean, label='validation') plt.xlabel('training size') plt.ylabel('f1_macro') plt.legend() plt.savefig('learning_curve.png', dpi=150)逻辑说明:如果两条曲线靠近且分数都不高,模型处于欠拟合状态,方向是加特征或放宽 max_depth 限制;如果训练分数高、验证分数低,且两线之间空隙大,模型过拟合,方向是加数据或收紧 min_samples_leaf。学习曲线最实务的用途是回答“要不要继续采集数据”这个预算问题:验证曲线在训练集增大时还在明显上升,值得补数据;如果已经走平,再加数据是白花钱。
4. 遥感随机森林实战:从波段读取到分类出图的完整流程
4.1 样本怎么构造:从栅格提光谱与纹理特征
遥感影像是随机森林用得最多的场景之一,地物分类天然是“像素特征 + 类别标签”的表格问题。一份 Sentinel-2 影像有 R、G、B、近红外等波段,再加上 NDVI 和纹理特征,每个像素就变成一行特征,每个有标注的像素就是一个训练样本。这份代码最有价值的地方也在这里:把栅格转成特征表、训练模型、再把预测结果写回栅格。
第一步是用 rasterio 读取多波段影像,把波段叠成特征矩阵:
import rasterio import numpy as np with rasterio.open('sentinel2.tif') as src: img = src.read() # shape: (bands, height, width) profile = src.profile # 保存投影和变换信息,最后写栅格要用 # 把 (bands, h, w) 转成 (h*w, bands) bands, h, w = img.shape feature_stack = np.transpose(img, (1, 2, 0)).reshape(-1, bands) # 每个波段做 2% 线性拉伸,避免云和阴影拉偏分布 for b in range(bands): p2, p98 = np.percentile(feature_stack[:, b], [2, 98]) feature_stack[:, b] = np.clip( (feature_stack[:, b] - p2) / max(p98 - p2, 1e-6), 0, 1 )逻辑说明:img.read() 读出来是 (波段数, 高, 宽),必须转成 (像素数, 波段数) 才能进 sklearn。归一化用 2% 到 98% 分位数拉伸,比 min-max 更抗异常值,因为影像里偶尔有云边缘过曝像素,min-max 会把正常像素的对比度压扁。
注意一个细节:训练样本不是从整幅影像全取,而是用标签栅格去挑有类别的像素。标签外的像素留给预测阶段统一跑,避免大量无标签像素混进训练集影响类别先验。我一般会先把 label 栅格读进来做 np.isin 筛选,再对应到 feature_stack 对应位置上。
4.2 模型训练与评估:类别不平衡怎么处理
遥感分类的典型问题是类别不平衡:水体、裸土这类地物样本通常很多,湿地、建筑阴影这类少数类很少。直接用 RandomForestClassifier 会把少数类几乎忽略。处理方法有两个,一个是 class_weight='balanced' 让 sklearn 自动按类别频率反比加权,另一个是手动对多数类降采样。遥感场景我建议直接上 class_weight='balanced_subsample',它在每棵树的 bootstrap 样本内部再计算权重,比全局加权更激进一点,适合类别差异大的情况。
from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import train_test_split from sklearn.metrics import cohen_kappa_score # 假设 label_stack 已经和 feature_stack 对齐 valid = ~np.isnan(label_stack) X_all = feature_stack[valid] y_all = label_stack[valid] X_train, X_test, y_train, y_test = train_test_split( X_all, y_all, test_size=0.2, random_state=42, stratify=y_all ) rf = RandomForestClassifier( n_estimators=300, max_depth=15, # 遥感里控制深度尤其重要,地物边缘噪声多 min_samples_leaf=4, max_features='sqrt', # 波段+纹理特征在 10~20 个时 sqrt 足够 class_weight='balanced_subsample', n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) y_pred = rf.predict(X_test) print(classification_report(y_test, y_pred, digits=4)) print('Kappa: %.4f' % cohen_kappa_score(y_test, y_pred))逻辑说明:遥感像素存在空间自相关,邻近像素高度相似,如果训练集和测试集按像素随机切,会严重高估模型效果,因为测试集里很多像素就是训练像素的紧邻。严谨做法是按地块或影像切片做分组切分,或者至少保证训练与测试像素之间留出缓冲距离。这个坑我在初版代码里翻过车,测试集 F1 有 0.92,换到另一块区域直接掉到 0.7。
参数说明:max_depth=15 在多数 10~30 米分辨率影像上够用,更大深度会让单棵树分裂到地物内部噪声上,分类图呈现碎椒盐效果。min_samples_leaf=4 会让预测结果更平滑,代价是少数类边缘的细节损失,2 米分辨率影像可以适当降到 2。遥感里除了打印 classification_report,我一般还会算 Kappa 系数,它在类别不平衡时比 accuracy 更可靠,学术论文里也常要求报告。
4.3 预测整幅影像与后处理
训练完模型,把整幅影像的特征矩阵全部丢进 rf.predict,得到 (h*w,) 的预测标签,再 reshape 回 (h, w),用 profile 写回 GeoTIFF:
pred = rf.predict(feature_stack) # feature_stack 是归一化后的全图特征 pred_img = pred.reshape(h, w).astype('uint8') # 去掉 nodata 区域:标签原本为空的位置预测值也置 0 pred_img[img[0] == 0] = 0 with rasterio.open( 'rf_classified.tif', 'w', driver='GTiff', height=h, width=w, count=1, dtype='uint8', crs=profile['crs'], transform=profile['transform'] ) as dst: dst.write(pred_img, 1)逻辑说明:写栅格时必须要带原始影像的 crs 和 transform,否则分类结果在 GIS 软件里位置对不上。预测完成后我通常会再做一次 majority filter,把孤立的单像素类别抹掉,能大幅提升目视效果。
参数说明:majority filter 的窗口大小我一般用 3×3 或 5×5,窗口越大分类图越平滑,但会把细窄地物如道路、小溪直接抹掉,所以做道路和水系提取时窗口不要超过 3×3。这个后处理可以用 scipy.ndimage 的 generic_filter 实现,也可以用 GDAL 的 sieve filter,后者在大影像上速度更快。
5. 避坑与常见问题:参数、数据与评估中最容易翻车的五个点
5.1 树数量越多越好的误解:OOB 曲线找拐点
现象:模型精度已经不再提升,但每次调参都要等很久,一个 1000 棵树的模型跑半小时,网格搜索根本没法做。
原因:树数量超过了精度平台,边际收益趋近于零,训练时间倒是随树数量线性增长。这是新手最常见的浪费。
解决:用 OOB 误差曲线找拐点,而不是拍脑袋定树数量。设置 oob_score=True 后,不同树数量下记录 oob_score_,画一条曲线看哪里走平。
import matplotlib.pyplot as plt oob_scores = [] trees = [50, 100, 200, 300, 500] for n in trees: rf = RandomForestClassifier( n_estimators=n, max_depth=12, min_samples_leaf=3, max_features='sqrt', oob_score=True, n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) oob_scores.append(rf.oob_score_) plt.plot(trees, oob_scores, marker='o') plt.xlabel('n_estimators') plt.ylabel('OOB score') plt.savefig('oob_curve.png', dpi=150)参数说明:OOB 分数和交叉验证分数高度接近,但成本低得多,因为它用的是每棵树没见过的 37% 数据。曲线走平的位置就是你的树数量拐点,一般 200 到 500 就足够,没必要追求 1000。
5.2 类别不平衡时 accuracy 骗人
现象:分类报告里准确率 0.95,但少数类 F1 是 0.1,模型把所有样本都判成了多数类。
原因:accuracy 被多数类样本量顶起来了,即使少数类一个都没分对,整体准确率依然很高。训练集里 95% 是 A 类、5% 是 B 类时,全猜 A 就能拿到 95% 准确率。
解决:评估指标换成 f1_macro、召回率或 Kappa 系数;训练端加 class_weight='balanced' 或 'balanced_subsample',数据端可以考虑对多数类降采样。
5.3 max_features 新旧版本差异导致报错
现象:代码在旧环境跑得好好的,升级 sklearn 后训练直接报错,提示 max_features 必须是 'sqrt' 或 0~1 之间的小数。
原因:sklearn 1.2 之后把分类任务的默认 max_features 从 'auto' 改成了 'sqrt','auto' 这个别名被移除,老代码里写 max_features='auto' 就会触发 ValueError。
解决:统一写成 'sqrt' 或具体浮点数如 0.7。'log2' 也是合法选项,但一般情况下不用,因为它的特征子集数量比 sqrt 更少,树之间相关性更低,但单棵树也更弱。
5.4 训练时间暴涨的元凶:没限 max_depth
现象:同样的数据,别人十分钟跑完,你的训练时间遥遥无期,而且特征越多越明显。
原因:多数情况是 max_depth 没限制,树一路长到叶节点完全纯净。极端情况下每棵树都是深度几十层的完全二叉树,节点数量指数级膨胀。这个问题在特征几百维时尤其致命。
解决:先设 max_depth=10~15 跑一遍,时间不满意继续压;再把 min_samples_leaf 设到 3 以上,剪枝效果比硬限深度更平滑。数据有几百万行时,还可以设置 max_samples=0.7,让每棵子树只用 70% 样本,牺牲少量偏差换训练速度。
5.5 高基数特征对 MDI 重要性虚高
现象:特征重要性排名前几的居然有用户 ID、日期时间戳这类明显不该具备预测力的特征。
原因:MDI 倾向于把高基数离散特征的重要性排得很高,因为可选的切分点多,更容易把节点分裂到“恰好纯”,但这个纯是纯过拟合。
解决:用 permutation_importance 复核一份特征排序,两个方法都排前面的才真正可靠。建模前也可以把高基数特征做分箱或目标编码,避免模型乱切。记住一点:特征重要性只用来排序,不要当绝对权重解释,这个认知能省掉很多返工。
6. 进阶技巧:用 OOB 与学习曲线在训练前就判断模型状态
随机森林有个天然优势:训练时留出的袋外数据本身就是一份验证集,设置 oob_score=True 就能拿到与交叉验证高度接近的分数,成本几乎为零。我现在做随机森林实验,第一件事就是先开 OOB,配合学习曲线一次性判断模型是欠拟合还是过拟合,而不是直接冲去调参。
rf = RandomForestClassifier( n_estimators=300, max_depth=12, min_samples_leaf=3, max_features='sqrt', oob_score=True, # 打开袋外分数 n_jobs=-1, random_state=42 ) rf.fit(X_train, y_train) print('OOB score: %.4f' % rf.oob_score_) print('Test score: %.4f' % rf.score(X_test, y_test))关键技巧是看两个数的关系:OOB 分数和测试分数差太多,说明数据切分方式有问题,或者训练分布和测试分布不一致;两者都低,说明欠拟合,该加特征或放宽深度限制;两者都高但 OOB 明显低于测试分数,就要警惕测试集是不是泄露了训练信息,信息泄露是最隐蔽的翻车方式。
我自己的固定流程是:先跑一个 200 棵树的模型,记 OOB 分数;OOB 不理想时直接看学习曲线判断缺数据还是缺复杂度;缺数据就回数据采集环节,缺复杂度才上网格搜索。从那以后我每次做随机森林建模都强制走一遍“OOB + 学习曲线”这个前置检查,省掉了大量在错误方向上调参的时间。希望帮到你。
本文还有配套的精品资源,点击获取