ARTICLE DETAIL

资讯详情

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

随机森林从原理到实战:集成学习、参数调优与泰坦尼克案例

随机森林从原理到实战:集成学习、参数调优与泰坦尼克案例 简介一份面向机器学习入门与进阶者的随机森林专题PDF系统讲解Bagging集成学习框架下随机森林的原理、算法流程与Python实现。内容从随机森林的构成、“随机”二重含义自助采样与特征子集、抗过拟合与稳定性等特点到生成过程、函数模型再到数据的读取、清洗、划分与代码实现逻辑完整适合算法学习、期末复习或项目选型参考。资源采用单个PDF文件封装全篇约619KB便于直接阅读与检索。目前已有1019人在CSDN下载学习配套内容中对RandomForestClassifier常见参数n_estimators、criterion、max_features等的作用也有解析可帮助读者理解模型调参与实际应用要点。1. 随机森林为什么是集成学习里最皮实的分类器随机森林大概是集成学习里最“皮实”的分类器不用费劲调参默认参数跑一版准确率通常也不会太难看。它的套路就两句话——用有放回抽样造出多份不一样的训练集再在每棵树的节点分裂时随机挑一部分特征来选最优划分。这份PDF从随机森林的生成规则讲起一直落到泰坦尼克号数据的完整实现属于少见能把原理和代码对上号的资料。适合刚学完决策树、想搞懂集成学习的同学也适合机器学习期末复习、面试前快速捡起随机森林细节的人。我按能跑通的标准把全文过了一遍坑都标在对应章节里。2. 从 Bagging 到随机森林样本扰动和属性扰动是怎么叠加的2.1 集成学习的两条路线Bagging 和 Boosting 怎么选先放大框架。随机森林属于集成学习里的 Bagging 路线。Bagging 的核心做法是并行训练一堆基学习器每个学习器独立给出预测最后投票或取平均用“多数表决”把单个模型的失误压下去。打个比方Bagging 像开全员大会投票Boosting 则像小组长盯着上次答错的人逐个辅导后一个模型专门纠正前一个的错误。随机森林选的是前者而且把基学习器固定为决策树。为什么偏偏是决策树因为决策树本身方差大、容易过拟合稍微换一批样本树的结构就大变。这种“不稳定”恰好是 Bagging 最喜欢的性质每个基学习器差异越大集成后抵消的噪声越多。如果你拿一个稳定的模型做 Bagging比如线性回归收益很小。这也能顺带回答一个高频面试问题——随机森林和决策树的区别单棵决策树会过拟合、对数据扰动敏感随机森林通过样本扰动和属性扰动把多棵不相关的树集成起来用多样性换稳定性。随机森林在训练时还额外引入属性扰动直接从候选属性集里随机抽 K 个属性再在子集里选最优划分K 一般取 log2(d)。这相当于在 Bagging 的样本随机之上再叠加一层特征随机基学习器之间的差异度被进一步拉大。2.2 bootstrap 有放回采样为什么不能不放回随机森林生成规则第一条训练集大小 N对每棵树随机且有放回地从训练集中抽 N 个样本。这个操作叫 bootstrap是有放回的。要理解“必须有放回”可以看两个反例。如果不抽样每棵树的训练集完全一样训练出来的树也几乎一样投票等于一个人说了算集成没意义。如果无放回地抽 N 个样本每棵树的训练集互不重叠相当于每棵树都在“盲人摸象”各自只能看到数据的一个角落投票结果自然没有共识。有放回抽样走的是中间路线每棵树既能看到大部分重复样本保证基本盘又会因为抽样随机性产生差异。有放回采样还有一个容易被忽略的数学事实当 N 足够大时某棵树的训练集里大约只会出现 63.2% 的不重复样本剩下约 36.8% 的样本从未进入该树的训练。这些没被抽中的样本叫袋外样本out-of-bag它们可以在不切验证集的情况下直接用来评估模型——sklearn 里 oob_scoreTrue 就是干这个的。这也是随机森林“自带验证集”的由来。2.3 属性扰动Klog2(d) 是怎么来的生成规则第二条每个样本有 M 个特征指定常数 m M每次分裂时先从 M 个特征里随机抽 m 个再在这 m 个里选最优划分属性。推荐值 Klog2(d)。以泰坦尼克数据为例筛选后剩 7 个特征log2(7)≈2.8也就是每个节点只看 2~3 个特征就能决定怎么切。sklearn 里分类任务默认的 max_featuresauto 等价于 sqrt(n_features)7 个特征时取 2~3 个和 log2 落在同一个级别。m 的取值是一把双刃剑m 越小单棵树的分类能力越弱但树与树之间的相关性也越低m 越大单棵树能力越强但树之间越来越像。这就是为什么随机森林在树很少时表现不如 Bagging 决策树——属性扰动把每棵树的准确度拉低了。但随着树的数量增加多样性带来的收益盖过了单树劣势随机森林最终收敛到更低的泛化误差。同时因为每棵树只在特征子集里找最优划分训练时计算量更小高维数据下效率优势尤其明显这也是随机森林“不用做特征选择”的底层原因。2.4 错误率的两头权衡相关性和单树能力不能兼得原文把随机森林的错误率归结为两个因素森林中任意两棵树的相关性相关性越大错误率越大每棵树的分类能力能力越强错误率越低。这两个因素在 m 上直接打架。减小 m相关性和分类能力一起降增大 m两者一起涨。所以 m 存在一个最优区间这也是随机森林里真正需要花心思调的参数。实战里判断这个区间不需要太玄学直接看 oob_score 或者交叉验证分数的走势把 m 从小到大列一组值跑一遍分数从低到高再回落峰值所在区域就是甜区。一般分类问题从 sqrt 起步回归问题从 1/3 特征数起步再在附近做小范围搜索。这个流程没有标准答案但比闷头刷参数列表靠谱得多。3. RandomForestClassifier 参数说明书从默认值到调参踩坑3.1 n_estimators 和 max_features先抓这两个先把影响最大的两个参数讲透。n_estimators 是森林里决策树的棵数默认只有 10这个数字在真实任务里偏小。一般我会先跑到 100 再看曲线准确率前期随树的数量快速上升到某个点后开始震荡平缓加树只剩训练时间成本几乎不再涨分。文档里的网格搜索把 n_estimators 从 1 取到 1000、步长 5跑完能明显看到这个趋势。max_features 决定每次分裂考虑的特征数。取值类型比较灵活int 是具体特征个数float 是占全部特征的比例“sqrt”等于“auto”“log2”取 log2(n_features)None 表示全部特征。注意取 None 时属性扰动消失随机森林退化成 Bagging 决策树树的多样性只剩样本扰动撑着不建议默认使用。对于分类任务我会在“auto”和“log2”之间做对比实验看验证集分数差多少再决定要不要动它。对于高维稀疏数据max_features 的敏感度会更高。特征从几十维涨到几千维时sqrt 规则选出的特征子集相对变小单棵树的准确度会下降这时可以适当把 max_features 往上调比如取 0.3 或 0.5 的比例值。反过来如果发现树与树之间长得太像、oob_score 低于同规模 Bagging 的预期就要往下调。调参调多了你会发现随机森林的玄学程度比神经网络低很多基本都能落到相关性-能力这个权衡框架里解释。3.2 树的生长与剪枝四个参数是联动关系max_depth 默认 None表示树自由生长到叶子足够纯为止这正是随机森林的设计意图——每棵树尽量长满、不剪枝靠集成来消化过拟合。但如果数据噪声大完全放开的树会深得离谱训练时间和内存都涨。我在大规模数据上会给 max_depth 设一个上界比如 10~20小数据集反而可以直接放开。min_samples_split 默认 2表示内部节点至少要有 2 个样本才继续分裂min_samples_leaf 默认 1表示叶子节点至少要有 1 个样本。这两个是联动参数min_samples_leaf 调大叶子变“胖”树变矮对噪声更钝感但模型也会变粗糙。文档搜索 leaf_size 从 1 到 500、间距 3就是因为这个参数在小数据集上对精度的扰动非常明显——在泰坦尼克这种 891 行的数据上叶子太大会丢掉太多细节导致欠拟合。min_impurity_decrease 要求分裂后杂质度减少量超过阈值才分裂min_impurity_split 是节点分裂的杂质度下限两者都用于控制树的生长。区别在于前者衡量的是“分裂带来的收益”后者衡量的是“当前节点的杂质度水平”。日常调参优先级不高数据特别脏的时候再碰。3.3 工程参数bootstrap、oob_score、n_jobs 和 class_weightbootstrap 默认 True保持它就行改成 False 等于取消样本扰动随机森林少了一条腿。oob_score 默认 False我会在每次实验里开成 True用袋外样本打分相当于白送一个验证集和测试集分数交叉验证很省事。n_jobs 默认 1改成 -1 用满所有 CPU 核心随机森林的树之间天然独立并行效率接近线性这是它相对其他集成方法的明显优势。class_weight 用于类别不平衡“balanced”会按类别频率反比自动调权重公式是 n_samples / (n_classes * np.bincount(y))。处理欺诈检测这类严重不平衡的数据时这个参数往往比动树结构更直接优先级应该往前放。另外还有两个低频参数criterion 选分裂质量的度量方式默认“gini”基尼指数也可以换“entropy”信息熵两者在多数数据集上差异不显著warm_start 可以在原有森林上增量加树适合反复加 n_estimators 看曲线但要注意配合固定 random_state。3.4 参数速查表我常用的调参顺序参数默认值作用我的习惯n_estimators10决策树棵数先 100 起步看准确率曲线是否平缓max_featuresauto每次分裂考虑的特征数分类试 sqrt 和 log2对比验证集max_depthNone树最大深度小数据不限制大数据限制在 10~20min_samples_split2内部节点最少样本数保持默认除非数据噪声大min_samples_leaf1叶子节点最少样本数过拟合时从 3 起步调bootstrapTrue是否放回抽样保持 Trueoob_scoreFalse是否用袋外样本打分实验时必开n_jobs1并行作业数直接用 -1这张表浓缩了我常用的调参顺序先固定 n_estimators100、max_featuresauto开 oob_score看默认分数然后单独动 max_features 和 min_samples_leaf最后才考虑 max_depth 和 class_weight。其余参数保持默认大部分场景已经够用。如果默认分数就挺好看那就别再折腾参数了留着时间处理数据比什么都值。4. 泰坦尼克号实战从 CSV 清洗到参数网格搜索4.1 数据读取891 行里有什么泰坦尼克数据集是 Kaggle 上最经典的入门赛题之一train.csv 共 891 行每行对应一名乘客字段包括乘客等级 Pclass、姓名 Name、性别 Sex、年龄 Age、同行的兄弟姐妹或配偶数 SibSp、同行的父母或子女数 Parch、票号 Ticket、票价 Fare、客舱号 Cabin、登船港口 Embarked 和生存标签 Survived。标签分布并不均匀约 38% 的人幸存直接用准确率评估时要留个心眼。字段含义是否进模型处理方式Pclass乘客等级 1/2/3是直接使用Sex性别是male0female1Age年龄是缺失值补中位数SibSp兄弟姐妹/配偶数是直接使用Parch父母/子女数是直接使用Fare票价是缺失值补中位数Embarked登船港口 S/C/Q是补 S 后映射 0/1/2Cabin客舱号否缺失太多排除Name/Ticket姓名/票号否高基数排除读取代码里有个小细节值得注意dtype 指定 Age 为 float64是为了防止空值混入后 pandas 把整列推断成 object 类型import numpy as np import pandas as pd from sklearn.ensemble import RandomForestClassifier train pd.read_csv(train.csv, dtype{Age: np.float64}) print(len(train)) # 891 train.head(10)这一步直接决定后面 fillna 能不能顺利执行。head(10) 只是瞄一眼结构真正决定字段去留的是 4.3 的分析。4.2 数据清洗与填充先补洞再编码sklearn 的随机森林输入必须是数值所以清洗分两步填缺失、转编码。文档的 harmonize_data 是经典写法def harmonize_data(titanic): # 年龄缺失用中位数填充比均值更抗长尾分布 titanic[Age] titanic[Age].fillna(titanic[Age].median()) # 性别映射male0female1 titanic.loc[titanic[Sex] male, Sex] 0 titanic.loc[titanic[Sex] female, Sex] 1 # 登船港口缺失值补 SS/C/Q 分别映射 0/1/2 titanic[Embarked] titanic[Embarked].fillna(S) titanic.loc[titanic[Embarked] S, Embarked] 0 titanic.loc[titanic[Embarked] C, Embarked] 1 titanic.loc[titanic[Embarked] Q, Embarked] 2 # 票价缺失值补中位数 titanic[Fare] titanic[Fare].fillna(titanic[Fare].median()) return titanic train_data harmonize_data(train)这里有几个选型理由值得说。Age 用中位数不用均值因为年龄分布有长尾个别高龄乘客会拉高均值中位数更稳。Sex 用 loc 条件赋值而不是遍历是 pandas 的向量化写法891 行看不出差别数据量大时速度差很多。Embarked 缺失值补“S”是因为 S 是出现频率最高的港口用众数填充缺失值是这类离散字段的默认做法。4.3 特征选择与数据划分7 个字段的依据从 11 个字段里筛出 7 个Pclass、Sex、Age、SibSp、Parch、Fare、Embarked。排除 Name、Ticket 是因为每个值几乎唯一树会沿着它们疯狂分裂直接背下训练集导致过拟合排除 Cabin 是因为缺失太多补出来的值没有业务含义。这些字段不参与训练但 pandas 读进来仍然占用内存数据量大的时候可以直接用 usecols 参数在读取阶段就过滤掉。数据划分用保留法前 600 行训练、后 291 行测试。注意这个切法没有 shuffle如果原始行序里幸存者和遇难者分布不均匀前后两段的数据分布会不一致导致分数虚高或虚低。文档在泰坦尼克上跑出 84%但换一种切法可能就不同这是保留法的固有局限。严谨的做法是切分前先打乱或者直接用 train_test_split 的 stratify 参数按标签比例分层抽样。4.4 参数网格搜索两层循环找最优核心搜索代码长这样两层循环分别遍历 min_samples_leaf 和 n_estimatorspredictors [Pclass, Sex, Age, SibSp, Parch, Fare, Embarked] results [] # 叶子节点最少样本数1 到 500步长 3约 167 个取值 sample_leaf_options list(range(1, 500, 3)) # 决策树棵树1 到 1000步长 5约 200 个取值 n_estimators_options list(range(1, 1000, 5)) groud_truth train_data[Survived][601:] for leaf_size in sample_leaf_options: for n_estimators_size in n_estimators_options: alg RandomForestClassifier( min_samples_leafleaf_size, n_estimatorsn_estimators_size, random_state50 ) alg.fit(train_data[predictors][:600], train_data[Survived][:600]) predict alg.predict(train_data[predictors][601:]) # 三元组记录叶子样本数、树数、测试集准确率 results.append((leaf_size, n_estimators_size, (groud_truth predict).mean())) # 按准确率这一维取最大值 print(max(results, keylambda x: x[2]))random_state50 固定随机种子保证同样参数下两次运行结果一致否则参数搜索的结果没法比较。results 里存的是三元组最后用 max 按第三维取最大比重写一堆 if 判断省事。组合数要心里有数167 × 200 ≈ 33400 个组合每个组合都要完整拟合一片森林再预测 291 条样本跑得慢是正常的。两层循环本身有一个方法论上的坑它在测试集上挑最优组合属于拿测试集调参最优分数会略微高估模型在未知数据上的表现。所以这个 84% 只能叫“在测试集上的最优精度”不能直接当作泛化精度。4.5 结果解读84% 是怎么来的84% 对应 291 条里约 46 条预测错误。这个数字在泰坦尼克号这类数据上属于正常偏上的水平既不是惊艳也不是翻车。实际复现时因为步长是 3 和 5最优参数组合可能落在 (1,1) 到 (499,996) 之间的任意一对取值结果以你本地跑出来的为准。如果嫌慢建议先把网格缩到 leaf∈[1,3,5,10,20,50] 和 n_estimators∈[50,100,200,500]24 个组合先看趋势确定甜区再细搜能省掉大量无效训练时间。下面专门写几个我在这份代码上踩过的坑。5. 避坑指南随机森林实战里的五个翻车现场这些坑基本都是我照着文档代码跑一遍时真实撞过的每一条都按“现象、原因、解决”三个层次写清楚。5.1 缺失值和字符串没处理sklearn 直接报错或精度崩塌现象读取 CSV 后直接 fit报 ValueError: Input contains NaN或者把 Sex 列原样传入算法把字符串当成类别乱切结果精度惨不忍睹。原因sklearn 的随机森林实现不接受 NaN也不支持字符串特征这是和某些 R 语言实现最大的区别。解决参考 4.2 的 harmonize_data先 fillna 再映射成 int养成“清洗函数独立封装、fit 之前必须调用”的习惯。我后来还会加一句断言比如assert train_data.isnull().sum().sum() 0在建模前强制检查一次。5.2 把 PassengerId、Name 当特征过拟合和虚高现象把 PassengerId、Ticket 加进 predictors 后训练集准确率接近 100%测试集反而掉到 80% 以下。原因这些高基数特征每个值几乎唯一树很容易沿着它们把样本分到各自所在的叶子相当于背下了训练集。解决特征选择阶段先做直觉筛选只保留有业务含义的字段想验证就做个对比实验把高基数特征加进去看验证集是否明显下降。泰坦尼克数据里 Name 是典型的陷阱字段包含称呼、姓氏、名字三部分真要挖掘也得先拆开做特征工程不能整列直接塞。5.3 数据划分没打乱600/291 的切分可能引入顺序偏差现象同样的参数换一种切分方式准确率能从 0.84 掉到 0.78或者反复跑结果忽高忽低。原因train.csv 的行序并不保证标签均匀分布前 600 行的幸存率可能与后 291 行不同保留法默认顺序切分等于把分布偏差直接带进模型评估。解决切分前先 shuffle或者直接用 train_test_split 并带上 stratifyy如果追求更稳就上交叉验证它天然不会因为一次切分而产生偏置。这个坑的隐蔽性在于不对比你根本不知道自己的分数是运气好还是运气差。5.4 网格搜索组合数爆炸33400 轮训练跑很久是正常的现象两层循环跑了一个小时还在转控制台刷屏但迟迟不结束。原因167 个 leaf 取值 × 200 个 n_estimators 取值约等于 33400 个组合每个组合都要完整拟合一片森林。解决先算组合数再决定步长原文档的组合规模就属于偏大的实际使用时把粗网格和细网格分开粗网格找趋势细网格定参数。另外给参数搜索套上 n_jobs-1能利用全部 CPU 核心但要注意随机森林本身是 CPU 密集任务笔记本上全核跑会发热降频反而拖慢速度。5.5 max_features 设为 None随机森林退化成 Bagging 决策树现象把 max_features 改成 None 之后训练集精度很高测试集波动变大且树与树之间的差异明显变小。原因每个节点都在全部特征里找最优划分属性扰动消失多样性只能靠样本扰动支撑整个模型退化成 Bagging 决策树。解决除非做对照实验否则保留默认“auto”高维数据可以试“log2”但不要用 None。这个坑最容易出现在刚接触随机森林的人身上因为直觉上会觉得“特征全用上不是更好吗”实际上集成模型的多样性就靠随机性撑着。5.6 不固定 random_state同一个参数组合两次结果对不上现象同一个脚本跑两遍打印的准确率不一样以为是代码有 bug。原因随机森林的样本抽样和特征抽样都是随机的不固定种子就无法复现。解决所有实验里设置 random_state50并在记录实验结果时把种子写进备注。这是个很小的习惯但会直接影响参数搜索结果的可信度。我见过有人跑网格搜索不固定种子最后挑出来一个“最优”组合其实是运气好抽到的一次高分换一批随机数结果完全不一样。所有实验固定种子是最便宜的后悔药。6. 评估随机森林的两种手段混淆矩阵与交叉验证准确率数字本身会骗人。当测试集里幸存者只占 38% 时一个永远预测“遇难”的模型也能拿到 62% 的准确率。所以真正上线或交报告前我会补两个检查混淆矩阵和交叉验证。6.1 混淆矩阵四个格子说明错在哪二分类场景下混淆矩阵是一个 2×2 表行是真实类别列是预测类别。TP 是正确预测的正例FP 是把负例预测成正例FN 是把正例预测成负例TN 是正确预测的负例。真实\预测预测为正预测为负实际为正TPFN实际为负FPTN由这四个格子可以算四个指标分类准确度 (TPTN)/(TPFPFNTN)召回率 TP/(TPFN)虚警率 FP/(FPTN)精确度 TP/(TPFP)。只看准确度会忽略“模型到底在幸存者还是遇难者上犯错”召回率和虚警率能补上这块盲区。6.2 用交叉验证取代固定切分保留法切一次训一次结果依赖那一次切分。交叉验证把数据切成 k 份每份轮流当测试集其余当训练集重复 k 次取平均每一行都被用于测试恰好一次。泰坦尼克这种 891 行的小样本交叉验证比保留法稳得多。from sklearn.model_selection import cross_val_score from sklearn.metrics import accuracy_score, precision_score, recall_score, confusion_matrix alg RandomForestClassifier( n_estimators200, min_samples_leaf3, random_state50 ) # 5 折交叉验证分数取平均不依赖单次切分 scores cross_val_score(alg, train_data[predictors], train_data[Survived], cv5) print(CV:, scores.mean()) # 保留法切出测试集后看混淆矩阵和召回率 alg.fit(train_data[predictors][:600], train_data[Survived][:600]) pred alg.predict(train_data[predictors][601:]) print(Accuracy:, accuracy_score(groud_truth, pred)) print(Precision:, precision_score(groud_truth, pred)) print(Recall:, recall_score(groud_truth, pred)) tn, fp, fn, tp confusion_matrix(groud_truth, pred).ravel() print(TP%d FP%d FN%d TN%d % (tp, fp, fn, tn))从那以后我每次跑随机森林都强制走一遍这个流程先固定 random_state再开 oob_score最后补一版交叉验证分数三个数字对得上才敢把结论写进报告。希望帮到你。本文还有配套的精品资源点击获取
返回列表