ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

随机森林实战:泰坦尼克号生存预测与超参数调优

随机森林实战:泰坦尼克号生存预测与超参数调优 写在前面如果你刚开始学机器学习想在真实数据集上完整跑通“数据清洗 → 特征工程 → 建模 → 调参 → 结果分析”这条链路泰坦尼克号生存预测绝对是最经典的起点。这个数据集不大不小特征有数值有类别还有缺失值要处理正好能把 sklearn 那套 API 摸熟。本文我就用随机森林RandomForest硬刚这个经典题从原理讲到代码再讲超参数到底怎么调才不是瞎试。全程附可运行代码你复制下来就能跑。先说清楚这篇要解决什么问题很多人看教程能看懂代码但换个数据集就懵了特征怎么构造参数设多少为什么别人调完准度提升了我调完反而过拟合这篇文章会把这几个问题逐个说透并给出一条经过验证的调参顺序。代码基于 Sklearn 1.xPython 3.8 都能直接跑适合有 Python 基础、想快速上手随机森林实战的朋友。1. 泰坦尼克号数据集分析与特征工程1.1 数据长什么样预测什么泰坦尼克号数据集的核心目标是根据乘客的个人信息预测该乘客是否在船难中幸存。这本质上是一个二分类问题标签是Survived1 表示幸存0 表示遇难。用 seaborn 自带的 Titanic 数据集一个load_dataset就能拿到不需要去 Kaggle 下载文件。代码很简单import seaborn as sns df sns.load_dataset(titanic) print(df.shape) print(df.columns.tolist())输出长这样(891, 15) [survived, pclass, sex, age, sibsp, parch, fare, embarked, class, who, adult_male, deck, embark_town, alive, alone]注意seaborn 版本里已经给你拆好了class、who、adult_male、alive这些衍生列。实际做特征工程时这些列反而要删掉因为它们是从原始字段直接映射出来的会造成严重的标签泄漏——alive就是survived的文本版本留着它模型就等于直接看答案了。真正值得用的原始字段是pclass舱位等级1/2/3这是一个等级变量数字大小有意义sex性别需要编码age年龄存在大量缺失sibsp船上兄弟姐妹/配偶数量parch船上父母/子女数量fare票价浮点数embarked登船港口类别变量缺失较少1.2 三个关键判断哪些特征能用、哪些要删、哪些要造常有人说“特征工程决定了模型上限”这句话在泰坦尼克号上体现得很明显。先看一下各特征的缺失情况df.isnull().sum()结果是age缺了 177 个deck缺了 688 个embarked缺了 2 个。面对缺失值第一反应不应该是“用均值填充”这种惯性操作而是先想清楚这个特征到底有没有预测力我的判断deck甲板编号缺失率超过 77%信息量太低直接删。做填充反而会把噪声当信号喂给模型。age缺失率约 20%不能删年龄和生存率有明显的相关性儿童幸存率高老人低要用合理的策略填充。embarked只有两个缺失值直接用出现次数最多的类别众数填充即可。除了处理缺失值我通常还会构造一个“家庭规模”特征df[family_size] df[sibsp] df[parch] 1为什么要造这个特征因为sibsp和parch单独看都是稀疏分布大量为 0但合并成家庭规模后可以区分“一个人出行”和“和家人一起出行”两种情况。历史上单人乘客的幸存率偏低这个特征比两个原始字段更有区分度。顺带说一个通用经验随机森林这种树模型对特征尺度不敏感不需要做标准化。这和 SVM、逻辑回归有本质区别。很多新手拿到数据就先 StandardScaler 一套对随机森林来说纯属多余操作不会让结果更好只是增加了不必要的复杂度。1.3 特征编码类别变量怎么喂给树模型sex和embarked是类别变量需要转成数值。有两种常见手段LabelEncoder / 手动 map适合二分类变量male和female直接映射为 0 和 1OneHotEncoder /pd.get_dummies适合无序多分类变量比如embarked的 C / Q / S 三个港口这里有个容易犯的错误有人把pclass也当成类别变量做 OneHot。pclass虽然是数字但它本质上是有序等级1、2、3 的大小关系有实际含义头等舱、二等舱、三等舱的生存率递减。这种情况下把它当数值直接喂给树模型通常效果更好因为树模型可以直接切分“pclass 2”这种节点保留顺序信息。如果你非要 OneHot 也不是不行三种做法各有取舍变量推荐做法原因sex映射为 0/1二分类直接编码最简单embarkedOneHot 编码无序三分类无法用 0/1/2 表示顺序关系pclass保持数值1/2/3有序等级数值本身代表舱位档次我在项目里直接用了一行代码把性别和登港口处理掉df[sex] df[sex].map({male: 0, female: 1}) df pd.get_dummies(df, columns[embarked], prefixemb)pclass和fare保持数值age用中位数填充。为什么不选均值因为年龄分布右偏个别的老年人数据会把均值拉高中位数更稳健。2. 随机森林核心原理为什么它能打2.1 从决策树到随机森林两个随机性的来源随机森林的本质是“一堆决策树投票”。但如果你只是把同一棵决策树复制一百份投票结果和单棵树没有区别所以随机森林的关键在于“让每一棵树长得不一样”。这个不一样来自两个随机性样本随机性Bagging每一棵树训练时从原始数据中有放回地抽取同样数量的样本。不同树拿到的训练集大致相同但有差异单棵树的随机性就产生了。举个例子891 条样本每棵树随机抽 891 条可能会有 30% 的样本被重复抽到也会有个别样本一次都没被抽到。特征随机性每棵树的每个节点分裂时不是从全部特征里选最优切分点而是从随机挑选的max_features个特征里选。这保证了即使两棵树用同一批样本训练它们的树结构也不同。这两层随机性合在一起让每棵树都是“偏科生”但把几百个偏科生放在一起投票群体的判断却相当稳定。这就是随机森林泛化能力强的基本逻辑。从工程角度看随机森林还有两个天然优势支持并行训练n_jobs-1直接吃满 CPU而且对特征尺度不敏感。这两个特性让它成为我处理结构化数据时的默认首选模型。2.2 Sklearn 中 RandomForestClassifier 关键参数RandomForestClassifier的参数很多但实际调起来就几个关键项。我把它们分成“树的结构”和“训练策略”两组说人话解释参数作用说明n_estimators森林中树的数量越多越稳定但边际收益递减一般 100~500 够用max_depth树的最大深度控制模型复杂度太深容易过拟合min_samples_split内部节点继续分裂所需的最少样本数调大可以限制树过度生长min_samples_leaf叶子节点的最少样本数调大能让预测结果更平滑max_features每个节点随机选择的特征数默认是 sqrt(总特征数)分类问题通常不用改n_jobs并行使用的 CPU 核数-1 表示使用所有核心random_state随机种子固定后结果可复现调试必设使用随机森林时理论上不需要太多的剪枝pruning因为随机性和投票机制天然抑制了过拟合。但如果你发现训练集准确率 99%测试集只有 81%那就要检查max_depth是否太深、min_samples_leaf是否太小这是最常见的过拟合信号。2.3 第一个 baseline 模型跑出基准分数在调参前先用默认参数跑一个基线。这一步很重要后续所有调参工作都以这个分数为参照否则你不知道调参到底有没有用。from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score features [pclass, sex, age, sibsp, parch, fare, family_size] [col for col in df.columns if col.startswith(emb_)] X df[features] y df[survived] X[age] X[age].fillna(X[age].median()) X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) model RandomForestClassifier(n_estimators100, random_state42, n_jobs-1) model.fit(X_train, y_train) y_pred model.predict(X_test) print(fTest Accuracy: {accuracy_score(y_test, y_pred):.4f})这个 baseline 在 891 条样本上测试集准确率通常在 0.80~0.82 之间。如果你跑出来的分数偏低先检查数据预处理部分特别是embarked缺失值是否处理干净了。跑这个模型时你可能注意到训练过程非常快891 条样本 100 棵树在普通笔记本上大约 1~2 秒。很多人问“随机森林需要跑多长时间”这里给出大概参考万级别的样本、几十个特征n_estimators100时几秒到几十秒百万级别的数据建议用HistGradientBoosting或 XGBoost而不是硬刚随机森林。3. 超参数调优实战从手动试探到自动搜索3.1 调参顺序不要一上来就网格搜索网上很多教程教你直接GridSearchCV一把梭那思路其实有问题。网格搜索是组合爆炸的你给每个参数列 5 个候选值5 个参数就是 5^5 3125 组组合每组都要做交叉验证跑起来非常煎熬。我推荐的调参思路是“从最敏感的参数开始逐步逼近”先定n_estimators这个参数主要影响稳定性和时间与过拟合关系不大。做法是固定其他参数画一条“树数量 vs 准确率”的曲线找到拐点。再调树的复杂度max_depth、min_samples_split、min_samples_leaf这三个参数是控制过拟合的主力。最后微调max_features它控制了每棵树的随机性强度对最终精度影响不大但值得一试。先用代码找n_estimators的拐点import matplotlib.pyplot as plt import numpy as np n_estimators_range [50, 100, 200, 300, 500] scores [] for n in n_estimators_range: model RandomForestClassifier(n_estimatorsn, random_state42, n_jobs-1) model.fit(X_train, y_train) y_pred model.predict(X_test) scores.append(accuracy_score(y_test, y_pred)) plt.plot(n_estimators_range, scores, markero) plt.xlabel(n_estimators) plt.ylabel(Accuracy) plt.title(n_estimators vs Accuracy) plt.show()实际跑下来你会发现 100 棵和 500 棵的准确率可能只差 0.005 左右。原因很简单随机森林的误差由偏差和方差组成树多了只减小方差但偏差的下限由单棵树的复杂度决定。你就算种一万棵树如果每棵树都是浅层树整体误差也不会降到零。所以我的实战习惯是n_estimators直接设 200不再纠结。3.2 GridSearchCV 与 RandomizedSearchCV 的取舍确定了树的数量后再来动树的结构参数。这里有两条路GridSearchCV穷举所有组合适合小参数网格RandomizedSearchCV在参数空间里随机采样适合参数多、范围广的情况对于泰坦尼克号这个量级的数据GridSearchCV完全够用。交叉验证折数建议cv5数据只有 891 条10 折会让每折训练集只有 800 条左右方差太大。我实际用的搜索代码from sklearn.model_selection import GridSearchCV param_grid { max_depth: [3, 5, 7, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4], } grid_search GridSearchCV( RandomForestClassifier(n_estimators200, random_state42, n_jobs-1), param_gridparam_grid, cv5, scoringaccuracy, verbose1, n_jobs-1 ) grid_search.fit(X_train, y_train) print(fBest params: {grid_search.best_params_}) print(fBest CV accuracy: {grid_search.best_score_:.4f})一组很常见的输出是{max_depth: 5, min_samples_leaf: 2, min_samples_split: 5}之类的组合。4 × 3 × 3 36 组5 折交叉验证就是 180 次训练以这个数据量几十秒内就能跑完。观察搜索结果你会发现max_depthNone不限制深度通常也能跑出不错的分数但测试集上往往会略逊于有限深度的模型——这就是过拟合的迹象。3.3 怎么判断调优真的有效看交叉验证和测试集差距调参完毕后拿最优参数重新训练然后看两个分数交叉验证均值分数与测试集准确率。如果两者接近说明模型泛化良好如果交叉验证分数很高、测试集分数掉得厉害说明模型过拟合了。best_model grid_search.best_estimator_ best_model.fit(X_train, y_train) y_pred best_model.predict(X_test) test_score accuracy_score(y_test, y_pred) cv_score grid_search.best_score_ print(fCV Score: {cv_score:.4f}) print(fTest Score: {test_score:.4f})如果调优后测试集准确率在 0.815 左右已经是一个相当合理的成绩。有一点必须提醒不要试图把所有数据都拿来调参调参过程是“看测试集分数 → 改参数”的循环如果反复用测试集评估会不自觉地对测试集过拟合。正确做法是只在最后用一次测试集确定最终模型的分数中间过程用交叉验证。3.4 特征重要性模型告诉你的隐藏信息随机森林有一个其他模型很难替代的优势——直接给出特征重要性。通过feature_importances_属性可以看到模型认为哪些特征对预测贡献最大import pandas as pd importance pd.DataFrame({ feature: X_train.columns, importance: best_model.feature_importances_ }).sort_values(importance, ascendingFalse) print(importance)泰坦尼克号上跑出来的排名通常非常稳定sex性别第一fare票价和age年龄紧随其后pclass也排在前列。这不难理解——女性幸存率远高于男性头等舱乘客幸存率远高于三等舱。如果你构造的family_size排在倒数几位也不要气馁这是正常的说明在这个数据集里它的边际贡献有限。特征重要性还有一个妙用排查“脏特征”。如果某个你明知很重要却被模型无视的特征排名倒数很可能是数据清洗环节出了问题比如缺失值填充策略不当或者编码方式破坏了原有信息。4. 完整可运行代码与常见问题排查4.1 一次跑通的完整代码脚本前面每个环节的代码都是零散的这里给一份可以直接复制运行的完整脚本。从数据加载到最终预测不依赖外部 CSV所有操作都在内存中完成import seaborn as sns import pandas as pd import numpy as np from sklearn.model_selection import train_test_split, GridSearchCV from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import accuracy_score, confusion_matrix, classification_report # 1. 加载数据 df sns.load_dataset(titanic) print(f原始数据: {df.shape}) # 2. 特征工程 df[sex] df[sex].map({male: 0, female: 1}) df[family_size] df[sibsp] df[parch] 1 df pd.get_dummies(df, columns[embarked], prefixemb) features [pclass, sex, age, sibsp, parch, fare, family_size, emb_C, emb_Q, emb_S] X df[features].copy() y df[survived] # 3. 缺失值处理 X[age] X[age].fillna(X[age].median()) print(f缺失值统计:\n{X.isnull().sum()}) # 4. 数据划分 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42, stratifyy ) print(f训练集: {X_train.shape}, 测试集: {X_test.shape}) # 5. Baseline 模型 base_model RandomForestClassifier(n_estimators200, random_state42, n_jobs-1) base_model.fit(X_train, y_train) base_pred base_model.predict(X_test) print(f\nBaseline 测试集准确率: {accuracy_score(y_test, base_pred):.4f}) # 6. 超参数调优 param_grid { max_depth: [3, 5, 7, None], min_samples_split: [2, 5, 10], min_samples_leaf: [1, 2, 4], } grid_search GridSearchCV( RandomForestClassifier(n_estimators200, random_state42, n_jobs-1), param_gridparam_grid, cv5, scoringaccuracy, n_jobs-1 ) grid_search.fit(X_train, y_train) print(f\n最优参数: {grid_search.best_params_}) print(f最优交叉验证分数: {grid_search.best_score_:.4f}) # 7. 最终模型评估 best_model grid_search.best_estimator_ final_pred best_model.predict(X_test) print(f\n最终模型测试集准确率: {accuracy_score(y_test, final_pred):.4f}) print(\n混淆矩阵:) print(confusion_matrix(y_test, final_pred)) print(\n分类报告:) print(classification_report(y_test, final_pred)) # 8. 特征重要性 importance pd.DataFrame({ feature: X_train.columns, importance: best_model.feature_importances_ }).sort_values(importance, ascendingFalse) print(\n特征重要性排名:) print(importance)这段代码完整跑完大约需要 1~2 分钟其中包括网格搜索的时间。如果你机器的 CPU 比较弱可以把n_estimators降到 100或者去掉max_depth的None选项减少组合数。4.2 Sklearn 安装与环境问题速查“sklearn 安装”是很多新手第一个遇到的坑。绝大多数情况下你不需要单独装 sklearn因为它依赖numpy和scipy版本不对就装不上。我整理了一份速查最省事的方式直接装 Anaconda自带了 sklearn、pandas、seaborn、matplotlib省去所有依赖地狱。装好后在终端输入python -c import sklearn; print(sklearn.__version__)验证。若是已经在用 Python 的普通环境用 pip 安装pip install scikit-learn pandas seaborn matplotlib如果提示“Microsoft Visual C 14.0 is required”说明你的 Python 版本太新或环境缺少编译工具。最简单的解决方案是换成 Python 3.8~3.10 的 64 位版本不要用 3.12 跑机器学习老项目。用了 conda 可以用conda install scikit-learn pandas seaborn matplotlibconda 的好处是自动帮你匹配兼容的 numpy 版本不容易翻车。4.3 实战中踩过的坑与排查清单我从头到尾跑这个项目印象最深的是 3 个坑列给你避雷坑一seaborn 的deck字段缺失率极高用了它反而掉分。很多教程会带你花大量时间填充deck比如根据pclass和fare推测甲板层数。我的建议是——放弃。缺失率超过 77% 的字段填充出来的信息大部分是模型脑补的对精度提升有限反而让你怀疑人生。坑二SibSp和Parch特征单独用的时候树模型的切分很容易忽略它们取值稀疏的问题。我在 baseline 里试过只用原始特征不加family_size测试集准确率大概掉 1~2 个百分点。加了family_size之后分类结果更平滑这是因为树可以做一个干净的切分“family_size 1”相当于把“孤身一人”这个强特征直接表达出来了。坑三用GridSearchCV时没有固定random_state。如果随机森林在交叉验证的每组参数下都重新换随机种子结果会飘不同参数之间的优劣对比就不可靠了。务必在模型初始化时固定random_state42。再补充一个容易忽略的点分类问题要记得stratifyy保证训练集和测试集中幸存者的比例一致。泰坦尼克号数据中幸存率约 38%如果不分层采样某个随机划分可能让训练集幸存率变成 45%模型就会偏向预测生存测试集分数自然不稳定。4.4 关于随机森林的更多拓展分类之外和更大数据量的思路顺便说一句随机森林不仅能做分类RandomForestRegressor就是做回归的接口几乎一样只是评价指标换成mean_squared_error或r2_score。你后续如果遇到房价预测、销量预测、遥感影像分类这类场景思路可以直接平移。数据量特别大的时候可以试试 sklearn 里的HistGradientBoostingClassifier它是梯度提升树的直方图实现训练速度比随机森林快几个量级精度通常也能打平。另外如果你想在这个项目上继续提分可以重点考虑三件事一是用cross_val_score替换手动的单次划分分数更稳定二是尝试 Stacking把随机森林和逻辑回归、朴素贝叶斯结合起来做集成通常能再提升 1~2 个百分点三是用RandomizedSearchCV把max_features也纳入搜索范围虽然这个参数对结果影响不大但组合起来往往会有惊喜。个人做下来最大的体会是调参的收益远不如特征工程来得多。一个合理的family_size特征比你把min_samples_leaf从 1 调到 2 带来的提升大得多。所以建议你把精力主要放在数据理解上把 sklearn 当作一个稳定的执行工具先把特征处理好再去考虑调参细节。最后再分享一个小技巧任何一次建模实验一定要把随机种子固定好把每次实验的配置参数特征列表、模型参数、数据划分方式记在同一份笔记里。这个习惯能帮你少走很多回头路不然调了两天参最后连自己最优结果用的是什么配置都忘了那就真的白跑一趟了。
RELATED READING

延伸阅读

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