ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

随机森林模型实战:分类回归、特征重要性、调参与遥感分类完整代码解析

随机森林模型实战:分类回归、特征重要性、调参与遥感分类完整代码解析 简介随机森林模型代码资源面向数据科学初学者及需要快速上手集成学习的开发者提供从原理到实践的完整学习材料。压缩包共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_size0.2, random_state42, stratifyy ) # ---------- 3. 建模 ---------- rf RandomForestClassifier( n_estimators300, # 树数量先给 300后续用 OOB 找拐点 max_depth12, # 限制深度防止单棵树过拟合 min_samples_leaf3, # 叶节点最少样本数控制分裂粒度 max_featuressqrt, # 分类任务默认用 sqrt(特征数) n_jobs-1, # 用满所有 CPU 核心 random_state42, # 固定随机种子保证可复现 class_weightbalanced # 类别不平衡时自动加权 ) 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_estimators300, max_depth10, min_samples_leaf5, max_features1.0, # 回归任务通常允许全部特征参与分裂 n_jobs-1, random_state42 ) 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_repeats10, # 每个特征打乱 10 次取均值与标准差 random_state42, 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_repeats10 表示每个特征打乱十次输出均值和标准差。标准差大说明该特征对预测结果的影响很不稳定即使均值高也要谨慎。参数注意置换重要性比 MDI 慢很多特征上百个时 n_repeats 先用 5 跑一遍确定候选特征池后再加大重复次数。另外做置换重要性时测试集不能太小否则打乱一两次分数波动就很大结果里全是噪声。特征重要性我只用来做排序和筛选不会把它当成真实现对预测贡献的绝对权重这个认知能帮你少走很多弯路。3.2 用网格搜索锁参数先调深度还是先调树数量随机森林调参有个顺序问题先调树数量还是先调深度结果差别很大。我建议的顺序是先用默认 max_features 和较大的树数量跑一遍 OOB找到精度平台再调 max_depth 和 min_samples_leaf 控制过拟合最后回头微调 max_features。把顺序反过来容易踩坑深度没限制时树数量增加会放大过拟合验证集分数反而下降。网格搜索代码直接用 GridSearchCVfrom 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_state42, n_jobs-1, class_weightbalanced), param_grid, cv5, # 5 折交叉验证 scoringf1_macro, # 类别不平衡时用 f1_macro 更稳 n_jobs-1, verbose1 ) grid.fit(X_train, y_train) print(best params:, grid.best_params_) print(best score:, grid.best_score_)网格大小要算一笔账4 个参数3×3×3×254 组组合每组 5 折交叉验证等于要训练 270 次随机森林。数据量大的话这个搜索要跑到天荒地老。所以网格搜索之前先用上一节的特征重要性把特征数量砍到 50 个以内再考虑上 GridSearchCV。参数说明scoring 在类别不平衡时不要用默认 accuracyf1_macro 对每个类别取了平均能逼着模型照顾小类别如果你更在意少数类的查全率可以换成 recall。cv5 是常规选择数据量少于几千行时改成 cv3否则每次验证的训练集太小分数波动会掩盖真实的参数好坏。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], cv5, scoringf1_macro, n_jobs-1 ) train_mean train_scores.mean(axis1) valid_mean valid_scores.mean(axis1) plt.plot(train_sizes, train_mean, labeltrain) plt.plot(train_sizes, valid_mean, labelvalidation) plt.xlabel(training size) plt.ylabel(f1_macro) plt.legend() plt.savefig(learning_curve.png, dpi150)逻辑说明如果两条曲线靠近且分数都不高模型处于欠拟合状态方向是加特征或放宽 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_weightbalanced 让 sklearn 自动按类别频率反比加权另一个是手动对多数类降采样。遥感场景我建议直接上 class_weightbalanced_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_size0.2, random_state42, stratifyy_all ) rf RandomForestClassifier( n_estimators300, max_depth15, # 遥感里控制深度尤其重要地物边缘噪声多 min_samples_leaf4, max_featuressqrt, # 波段纹理特征在 10~20 个时 sqrt 足够 class_weightbalanced_subsample, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) y_pred rf.predict(X_test) print(classification_report(y_test, y_pred, digits4)) print(Kappa: %.4f % cohen_kappa_score(y_test, y_pred))逻辑说明遥感像素存在空间自相关邻近像素高度相似如果训练集和测试集按像素随机切会严重高估模型效果因为测试集里很多像素就是训练像素的紧邻。严谨做法是按地块或影像切片做分组切分或者至少保证训练与测试像素之间留出缓冲距离。这个坑我在初版代码里翻过车测试集 F1 有 0.92换到另一块区域直接掉到 0.7。参数说明max_depth15 在多数 10~30 米分辨率影像上够用更大深度会让单棵树分裂到地物内部噪声上分类图呈现碎椒盐效果。min_samples_leaf4 会让预测结果更平滑代价是少数类边缘的细节损失2 米分辨率影像可以适当降到 2。遥感里除了打印 classification_report我一般还会算 Kappa 系数它在类别不平衡时比 accuracy 更可靠学术论文里也常要求报告。4.3 预测整幅影像与后处理训练完模型把整幅影像的特征矩阵全部丢进 rf.predict得到 (h*w,) 的预测标签再 reshape 回 (h, w)用 profile 写回 GeoTIFFpred 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, driverGTiff, heighth, widthw, count1, dtypeuint8, crsprofile[crs], transformprofile[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_scoreTrue 后不同树数量下记录 oob_score_画一条曲线看哪里走平。import matplotlib.pyplot as plt oob_scores [] trees [50, 100, 200, 300, 500] for n in trees: rf RandomForestClassifier( n_estimatorsn, max_depth12, min_samples_leaf3, max_featuressqrt, oob_scoreTrue, n_jobs-1, random_state42 ) rf.fit(X_train, y_train) oob_scores.append(rf.oob_score_) plt.plot(trees, oob_scores, markero) plt.xlabel(n_estimators) plt.ylabel(OOB score) plt.savefig(oob_curve.png, dpi150)参数说明OOB 分数和交叉验证分数高度接近但成本低得多因为它用的是每棵树没见过的 37% 数据。曲线走平的位置就是你的树数量拐点一般 200 到 500 就足够没必要追求 1000。5.2 类别不平衡时 accuracy 骗人现象分类报告里准确率 0.95但少数类 F1 是 0.1模型把所有样本都判成了多数类。原因accuracy 被多数类样本量顶起来了即使少数类一个都没分对整体准确率依然很高。训练集里 95% 是 A 类、5% 是 B 类时全猜 A 就能拿到 95% 准确率。解决评估指标换成 f1_macro、召回率或 Kappa 系数训练端加 class_weightbalanced 或 balanced_subsample数据端可以考虑对多数类降采样。5.3 max_features 新旧版本差异导致报错现象代码在旧环境跑得好好的升级 sklearn 后训练直接报错提示 max_features 必须是 sqrt 或 0~1 之间的小数。原因sklearn 1.2 之后把分类任务的默认 max_features 从 auto 改成了 sqrtauto 这个别名被移除老代码里写 max_featuresauto 就会触发 ValueError。解决统一写成 sqrt 或具体浮点数如 0.7。log2 也是合法选项但一般情况下不用因为它的特征子集数量比 sqrt 更少树之间相关性更低但单棵树也更弱。5.4 训练时间暴涨的元凶没限 max_depth现象同样的数据别人十分钟跑完你的训练时间遥遥无期而且特征越多越明显。原因多数情况是 max_depth 没限制树一路长到叶节点完全纯净。极端情况下每棵树都是深度几十层的完全二叉树节点数量指数级膨胀。这个问题在特征几百维时尤其致命。解决先设 max_depth10~15 跑一遍时间不满意继续压再把 min_samples_leaf 设到 3 以上剪枝效果比硬限深度更平滑。数据有几百万行时还可以设置 max_samples0.7让每棵子树只用 70% 样本牺牲少量偏差换训练速度。5.5 高基数特征对 MDI 重要性虚高现象特征重要性排名前几的居然有用户 ID、日期时间戳这类明显不该具备预测力的特征。原因MDI 倾向于把高基数离散特征的重要性排得很高因为可选的切分点多更容易把节点分裂到“恰好纯”但这个纯是纯过拟合。解决用 permutation_importance 复核一份特征排序两个方法都排前面的才真正可靠。建模前也可以把高基数特征做分箱或目标编码避免模型乱切。记住一点特征重要性只用来排序不要当绝对权重解释这个认知能省掉很多返工。6. 进阶技巧用 OOB 与学习曲线在训练前就判断模型状态随机森林有个天然优势训练时留出的袋外数据本身就是一份验证集设置 oob_scoreTrue 就能拿到与交叉验证高度接近的分数成本几乎为零。我现在做随机森林实验第一件事就是先开 OOB配合学习曲线一次性判断模型是欠拟合还是过拟合而不是直接冲去调参。rf RandomForestClassifier( n_estimators300, max_depth12, min_samples_leaf3, max_featuressqrt, oob_scoreTrue, # 打开袋外分数 n_jobs-1, random_state42 ) 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 学习曲线”这个前置检查省掉了大量在错误方向上调参的时间。希望帮到你。本文还有配套的精品资源点击获取
返回列表