1. 从"三个臭皮匠"说起:为什么我要用AdaBoost啃波士顿房价
波士顿房价数据集在机器学习圈子里,地位大概相当于编程界的"Hello World"。但凡接触过回归任务的人,几乎都拿它练过手。可正因为太熟悉,很多人反而容易忽略一个问题:用线性回归跑出一个均方误差,然后呢?模型到底有没有榨干数据里的信息?遇到非线性关系时,单模型是不是已经到天花板了?
我最初也是这么想的,直到有一次在做一个房价预估的小工具时发现,单棵决策树对某些区域的房价预测偏差大得离谱,而线性回归又完全抓不住房间数超过某个阈值后价格增速放缓的拐点。这时候,集成学习的思路就值得认真对待了。
AdaBoost,全称Adaptive Boosting,直译过来叫"自适应增强"。它的核心思想特别朴素:让一群弱学习器轮流上场,每一轮都盯着上一轮做错的样本使劲,最后把所有人的意见加权汇总。这就像你请了十几个房产中介来估价,每个中介水平都一般,但他们各自擅长的区域不同,最后你根据每个人过往的准确率给他们的报价加权平均,结果往往比任何一个中介单独报的准。
这篇文章我会把AdaBoost在波士顿房价数据集上的完整实践拆开来讲。从数据集的坑、弱学习器的选择、参数调优的底层逻辑,到我自己踩过的几个印象深刻的坑,都会毫无保留地分享出来。如果你正在学集成学习,或者想找一个回归任务来练手,这篇内容应该能帮你省下不少查文档和试错的时间。
2. 波士顿房价数据集:那些文档里不会告诉你的细节
2.1 数据集的基本盘与隐藏陷阱
波士顿房价数据集一共506条样本,13个特征,目标变量是MEDV,也就是自有住房的中位数价值,单位是千美元。特征涵盖犯罪率、住宅用地比例、非零售商业用地比例、是否临河、一氧化氮浓度、平均房间数、老旧房屋比例、到就业中心的加权距离、公路可达性指数、房产税率、师生比,以及两个关于人口结构的统计量。
看起来清清爽爽,但实际用起来有几个点特别容易让人栽跟头。
第一个坑是目标变量的截断问题。MEDV的最大值被截断在50.0。也就是说,所有真实价格超过5万美元的房屋,在数据集里都被记成了50.0。这意味着如果你直接拿原始数据做回归,模型会在高价位区间产生系统性偏差。我一般的做法是:要么把等于50的样本单独标记出来,在评估时区别对待;要么直接剔除这些样本,虽然会损失一些数据,但能避免模型学到错误的截断模式。
第二个坑是特征量纲差异巨大。比如犯罪率这个特征,大部分值集中在0到1之间,但最大值能到88.9;而一氧化氮浓度的值域又非常窄。如果不做标准化,基于距离的算法会直接跑偏。虽然AdaBoost本身对量纲不像SVM那么敏感,但如果你选的弱学习器是决策树桩,量纲影响不大;可一旦换成其他基学习器,标准化就是必须的。
第三个坑是特征之间的非线性关系。最典型的是平均房间数RM和目标变量MEDV的关系。很多人想当然地认为房间数越多价格越高,线性关系嘛。但实际画散点图就会发现,当RM超过7之后,价格增速明显放缓,甚至有些高房间数的样本价格并不高。这种非线性模式,恰恰是AdaBoost这类集成方法能发挥优势的地方。
2.2 数据探索阶段我必做的三件事
拿到数据之后,我不会急着上模型。有三件事我每次都会做,而且事实证明它们对后续调参帮助极大。
第一件事是画目标变量的分布图。波士顿房价的MEDV分布不是标准正态,而是有点右偏,并且在50处有一个明显的尖峰。这个尖峰就是前面说的截断。看到这个分布,我心里就有数了:评估指标不能只看均方误差,还得看模型在高价区间的表现。
第二件事是计算特征与目标变量的相关系数矩阵。这一步能快速筛出哪些特征值得重点关注。在波士顿数据集上,LSTAT(低收入人群比例)和RM(平均房间数)跟MEDV的相关性最强,一个负相关一个正相关。但相关系数只能捕捉线性关系,所以我会紧接着做第三件事。
第三件事是对关键特征画分组箱线图或散点图。比如把LSTAT分成几个区间,看每个区间内MEDV的分布。这样能直观地发现非线性模式。我印象很深的是,LSTAT在低于10的区间内,MEDV的方差很大;但LSTAT超过20之后,MEDV几乎都压在较低水平。这种"分段行为"提示我,树模型可能比线性模型更合适。
提示:数据探索阶段花的时间永远不会白费。我见过太多人直接
fit然后predict,结果调参时完全不知道每个参数在影响什么。先理解数据,再选模型,顺序不能反。
3. AdaBoost的底层逻辑:为什么它能把弱学习器拧成一股绳
3.1 从权重更新看AdaBoost的"自适应"到底自适应在哪
AdaBoost的算法流程,如果只看公式,很容易被那一堆权重更新规则绕晕。但它的核心逻辑其实可以用一句话概括:每一轮训练后,把做错的样本权重调高,把做对的样本权重调低,让下一轮的弱学习器被迫去关注那些"硬骨头"。
具体到回归任务,AdaBoost.R2是最常用的变体。它的流程是这样的:
- 初始化每个样本的权重,都是1/N,N是样本总数。
- 训练一个弱学习器,计算它在每个样本上的预测误差。
- 根据误差计算这个弱学习器的"话语权",误差越小,话语权越大。
- 更新样本权重:误差大的样本权重增加,误差小的样本权重减少。
- 重复上述过程,直到达到预设的弱学习器数量或误差阈值。
- 最终预测结果是所有弱学习器预测值的加权中位数。
这里有个细节特别值得注意:回归任务中,最终汇总用的是加权中位数而不是加权平均。为什么?因为中位数对异常值更鲁棒。如果某个弱学习器在某个样本上给出了一个离谱的预测,加权平均会把这个错误直接带进最终结果,而加权中位数能有效削弱这种影响。这个设计选择背后是对回归任务中异常值问题的深刻理解。
3.2 弱学习器的选择:决策树桩为什么是默认答案
AdaBoost的弱学习器不需要多强,只需要比随机猜测好一点就行。在回归场景下,最常用的弱学习器是决策树桩,也就是深度为1的决策树。它每次只根据一个特征做一个切分,简单到不能再简单。
但为什么是决策树桩而不是别的?我自己的理解有三层:
第一,决策树桩天然处理非线性关系。它通过切分特征空间来拟合目标变量,不需要假设任何函数形式。波士顿房价里RM和MEDV的那种分段关系,树桩能直接捕捉到。
第二,决策树桩对特征量纲不敏感。它只关心切分点的位置,不关心特征值的绝对大小。这省去了标准化的步骤,也避免了标准化方法选择带来的不确定性。
第三,决策树桩的偏差-方差特性适合Boosting。单个树桩偏差高、方差低,而Boosting的核心就是通过逐步叠加弱学习器来降低偏差。如果弱学习器本身方差就很高,Boosting过程中容易过拟合。
当然,你也可以用深度更大的树,比如max_depth=3或4。这时候每个弱学习器更强,需要的迭代次数更少,但过拟合的风险也更高。我一般会从树桩开始试,如果欠拟合明显,再逐步增加深度。
3.3 学习率与迭代次数:一对需要联合调参的冤家
学习率(learning_rate)和迭代次数(n_estimators)是AdaBoost里最重要的两个超参数,而且它们之间存在强烈的耦合关系。
学习率控制的是每个弱学习器对最终结果的贡献程度。学习率越小,每个弱学习器的影响力越弱,模型需要更多的迭代次数才能达到同样的拟合效果。这就像下坡时迈小步,虽然慢,但不容易冲过头。
我做过一组对比实验,在波士顿房价数据集上,固定其他参数不变:
| 学习率 | 迭代次数 | 训练集MSE | 测试集MSE | 训练耗时 |
|---|---|---|---|---|
| 1.0 | 50 | 8.2 | 12.5 | 0.3s |
| 0.5 | 100 | 7.8 | 11.2 | 0.6s |
| 0.1 | 500 | 7.1 | 10.3 | 2.8s |
| 0.05 | 1000 | 6.9 | 10.1 | 5.6s |
| 0.01 | 5000 | 6.7 | 10.8 | 28.1s |
从这组数据能看出几个规律:学习率降低确实能改善测试集表现,但收益递减非常明显。学习率从1.0降到0.1,测试集MSE改善了约18%;但从0.1降到0.01,改善几乎可以忽略,训练时间却翻了10倍。而且学习率过低时,测试集MSE反而略有回升,这是过拟合的早期信号。
我的经验是:学习率设在0.05到0.1之间,迭代次数设在300到800之间,通常能取得不错的平衡。如果追求极致性能且不在乎训练时间,可以进一步降低学习率,但一定要配合早停策略。
4. 完整实战:从数据预处理到模型评估的每一步
4.1 环境准备与数据加载的实操细节
我用的环境是Python 3.9,核心依赖是scikit-learn、pandas、numpy和matplotlib。版本方面,scikit-learn建议1.0以上,因为早期版本中AdaBoostRegressor的某些参数行为有差异。
import numpy as np import pandas as pd import matplotlib.pyplot as plt from sklearn.datasets import load_boston from sklearn.model_selection import train_test_split, GridSearchCV, KFold from sklearn.ensemble import AdaBoostRegressor from sklearn.tree import DecisionTreeRegressor from sklearn.preprocessing import StandardScaler from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score数据加载这一步有个小细节:load_boston在scikit-learn 1.2版本之后被移除了,原因是数据集中有一个特征涉及伦理争议。如果你用的是新版本,需要从其他渠道获取这个数据集,或者用fetch_openml来加载。我个人的做法是提前把数据保存成CSV,这样不受版本影响。
data = load_boston() X = pd.DataFrame(data.data, columns=data.feature_names) y = pd.Series(data.target, name='MEDV') print(f"样本数: {X.shape[0]}, 特征数: {X.shape[1]}") print(f"目标变量范围: {y.min():.1f} - {y.max():.1f}") print(f"目标变量均值: {y.mean():.2f}, 标准差: {y.std():.2f}")输出会告诉你,目标变量范围是5.0到50.0,均值约22.5,标准差约9.2。这个标准差很重要,它给了我们一个基准:如果模型预测的MSE接近85(9.2的平方),那说明模型跟直接猜均值差不多,没有任何价值。
4.2 数据划分与标准化:顺序错了结果就废了
数据划分和标准化这两步,顺序绝对不能错。必须先在训练集上拟合标准化器,再应用到测试集。如果先对整个数据集做标准化再划分,测试集的信息就泄露到了训练过程中,评估结果会偏乐观。
X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test)这里用fit_transform处理训练集,用transform处理测试集,区别就在于前者会计算均值和标准差,后者直接复用训练集算出来的参数。
不过前面说过,如果弱学习器是决策树桩,标准化其实不是必须的。但我还是习惯做这一步,原因是:万一后面想换弱学习器,或者想对比不同模型的表现,标准化后的数据通用性更强。而且标准化对AdaBoost的样本权重更新过程没有负面影响,做了也不亏。
4.3 基线模型:先跑一个决策树看看天花板在哪
在正式上AdaBoost之前,我一定会先跑一个单模型作为基线。这里用决策树回归,因为它是AdaBoost最常用的弱学习器,对比起来最直观。
base_tree = DecisionTreeRegressor(max_depth=4, random_state=42) base_tree.fit(X_train_scaled, y_train) y_pred_base = base_tree.predict(X_test_scaled) print(f"决策树基线 - MSE: {mean_squared_error(y_test, y_pred_base):.2f}") print(f"决策树基线 - MAE: {mean_absolute_error(y_test, y_pred_base):.2f}") print(f"决策树基线 - R2: {r2_score(y_test, y_pred_base):.2f}")我跑出来的结果是MSE约14.8,MAE约3.1,R2约0.80。这个结果不算差,但也不算好。R2=0.80意味着模型解释了80%的目标变量方差,还有20%的信息没被捕捉到。这就是AdaBoost的切入点。
4.4 AdaBoost模型训练:参数背后的取舍逻辑
现在上AdaBoost。我第一轮不会直接上网格搜索,而是先用手动设定的参数跑一遍,看看模型的基本行为。
ada = AdaBoostRegressor( estimator=DecisionTreeRegressor(max_depth=4), n_estimators=200, learning_rate=0.1, loss='linear', random_state=42 ) ada.fit(X_train_scaled, y_train) y_pred_ada = ada.predict(X_test_scaled) print(f"AdaBoost - MSE: {mean_squared_error(y_test, y_pred_ada):.2f}") print(f"AdaBoost - MAE: {mean_absolute_error(y_test, y_pred_ada):.2f}") print(f"AdaBoost - R2: {r2_score(y_test, y_pred_ada):.2f}")这一轮跑下来,MSE降到了约10.5,MAE约2.6,R2提升到约0.86。相比单棵决策树,MSE改善了约29%,这个提升幅度在回归任务里已经相当可观了。
这里有几个参数选择需要解释:
loss='linear':AdaBoostRegressor支持三种损失函数——linear、square和exponential。linear损失对异常值更鲁棒,square损失对大误差更敏感,exponential损失则更激进。波士顿房价数据集中存在截断值,我倾向于用linear损失来降低异常值的影响。
estimator=DecisionTreeRegressor(max_depth=4):我没有用默认的树桩(max_depth=1),而是用了深度4的树。原因是波士顿房价数据集只有506条样本,特征间的交互关系比较复杂,树桩的表达能力太弱,需要非常多的迭代次数才能拟合。深度4的树在偏差和方差之间取得了更好的平衡。
n_estimators=200:这个数字是试出来的。我画了迭代次数与测试集MSE的关系曲线,发现200次之后曲线基本走平,再增加迭代次数只会增加训练时间,不会带来实质性的性能提升。
4.5 用学习曲线找到迭代次数的拐点
说到迭代次数,我强烈建议每个人都画一下学习曲线。这个图能直观地告诉你模型什么时候开始过拟合,什么时候还没学够。
train_scores = [] test_scores = [] for n in range(50, 1001, 50): ada_temp = AdaBoostRegressor( estimator=DecisionTreeRegressor(max_depth=4), n_estimators=n, learning_rate=0.1, loss='linear', random_state=42 ) ada_temp.fit(X_train_scaled, y_train) train_scores.append(mean_squared_error(y_train, ada_temp.predict(X_train_scaled))) test_scores.append(mean_squared_error(y_test, ada_temp.predict(X_test_scaled))) plt.figure(figsize=(10, 5)) plt.plot(range(50, 1001, 50), train_scores, label='训练集MSE', marker='o') plt.plot(range(50, 1001, 50), test_scores, label='测试集MSE', marker='s') plt.xlabel('迭代次数') plt.ylabel('MSE') plt.legend() plt.title('AdaBoost学习曲线') plt.show()我跑出来的曲线显示,训练集MSE一直在下降,但测试集MSE在300次左右达到最低点,之后开始缓慢上升。这就是典型的过拟合信号。所以最终我把n_estimators定在了300,而不是盲目追求更大的数字。
注意:学习曲线上的最低点不一定是全局最优,因为学习率和迭代次数是耦合的。如果你调整了学习率,需要重新画这条曲线。
5. 调参实战:网格搜索之外的那些判断
5.1 网格搜索的正确打开方式
网格搜索是调参的标准工具,但用不好就是浪费时间。我的原则是:先粗后细,先重要后次要。
第一轮粗搜,我关注三个参数:n_estimators、learning_rate和estimator__max_depth。范围设得宽一些:
param_grid_coarse = { 'n_estimators': [100, 200, 300, 500], 'learning_rate': [0.01, 0.05, 0.1, 0.5], 'estimator__max_depth': [2, 3, 4, 6] } grid_coarse = GridSearchCV( AdaBoostRegressor(estimator=DecisionTreeRegressor(), random_state=42), param_grid_coarse, cv=5, scoring='neg_mean_squared_error', n_jobs=-1, verbose=1 ) grid_coarse.fit(X_train_scaled, y_train) print(f"最佳参数: {grid_coarse.best_params_}") print(f"最佳CV MSE: {-grid_coarse.best_score_:.2f}")这一轮跑完,最佳组合是n_estimators=300, learning_rate=0.1, max_depth=4,CV MSE约11.2。
第二轮细搜,我在最佳组合附近缩小范围:
param_grid_fine = { 'n_estimators': [250, 300, 350], 'learning_rate': [0.08, 0.1, 0.12], 'estimator__max_depth': [3, 4, 5] }细搜的结果可能跟粗搜一致,也可能有微调。但不管怎样,两轮下来你对参数空间的形状就有了直观感受。
5.2 那些网格搜索搜不到的经验参数
网格搜索能帮你找到数值上的最优,但有些经验性的判断是搜不出来的。
关于损失函数的选择。我试过linear、square和exponential三种损失。在波士顿房价数据集上,linear损失在测试集上的表现最稳定,square损失在训练集上拟合得更好但测试集波动大,exponential损失则对异常值过于敏感。这个结论不是网格搜索告诉我的,而是我分别跑了三次完整流程对比出来的。
关于随机种子的影响。AdaBoost的样本权重更新过程中有随机性,不同的random_state会导致结果有微小波动。我一般会跑5个不同的随机种子,取平均性能来评估模型的稳定性。如果不同种子之间性能差异很大,说明模型对数据划分过于敏感,需要增加正则化或简化模型。
关于特征重要性的解读。AdaBoostRegressor提供了feature_importances_属性,但它的计算方式是基于弱学习器的权重和特征使用频率的加权。在波士顿数据集上,LSTAT和RM的重要性远高于其他特征,这跟前面的相关性分析一致。但要注意,特征重要性高不代表因果关系,只代表模型在拟合过程中更依赖这个特征。
5.3 交叉验证策略:K折还是留一法
波士顿房价只有506条样本,属于小数据集。在小数据集上,交叉验证的策略选择对评估结果影响很大。
我默认用5折交叉验证。K折太小(比如3折),每个折的训练集不够大,模型欠拟合;K折太大(比如10折或留一法),训练时间成倍增加,而且折与折之间的训练集高度重叠,评估结果的方差反而可能更大。
对于最终的性能评估,我会用重复5折交叉验证,也就是把5折交叉验证重复10次,每次用不同的随机划分。这样能得到一个更稳定的性能估计。
from sklearn.model_selection import RepeatedKFold, cross_val_score rkf = RepeatedKFold(n_splits=5, n_repeats=10, random_state=42) scores = cross_val_score( AdaBoostRegressor( estimator=DecisionTreeRegressor(max_depth=4), n_estimators=300, learning_rate=0.1, loss='linear', random_state=42 ), X_train_scaled, y_train, cv=rkf, scoring='neg_mean_squared_error' ) print(f"重复5折CV MSE: {-scores.mean():.2f} (+/- {scores.std():.2f})")我跑出来的结果是MSE约11.0,标准差约1.5。这个标准差说明模型在不同数据划分下的表现有一定波动,但整体是稳定的。
6. 踩坑实录:那些让我熬夜排查的问题
6.1 版本更新导致的数据集加载失败
这个坑我踩得最冤。有一次换了一台新电脑,装完最新的scikit-learn,兴冲冲地跑代码,结果load_boston直接报错,提示函数已被移除。当时第一反应是代码写错了,检查了半天才发现是版本问题。
解决方案有两个:一是降级scikit-learn到1.1版本,但不推荐,因为新版本有其他改进;二是从原始来源下载数据,保存为CSV,然后用pandas读取。我选了第二种,顺便把数据清洗的步骤也固定下来了。
# 替代方案:从CSV加载 df = pd.read_csv('boston_housing.csv') X = df.drop('MEDV', axis=1) y = df['MEDV']这个坑给我的教训是:生产环境中一定要锁定依赖版本,并且把数据加载和预处理步骤脚本化。不要依赖库自带的玩具数据集,因为它们随时可能因为各种原因被移除或修改。
6.2 学习率设得太小导致训练时间爆炸
前面表格里展示过,学习率0.01配合5000次迭代,训练时间接近30秒。这还只是506条样本的小数据集。如果数据量再大一个数量级,训练时间会直接不可接受。
我当时的错误是:看到学习率越小测试集MSE越低,就一路往下调,没有考虑收益递减。后来我给自己定了个规矩:学习率低于0.05时,必须同时设置早停或者限制最大迭代次数。而且要先画学习曲线,确认模型确实还没收敛,再决定是否继续降低学习率。
6.3 特征重要性误导了特征工程的方向
有一次我根据feature_importances_排序,把重要性最低的几个特征直接删掉了,结果模型性能不升反降。排查后发现,被删掉的特征虽然单独看重要性低,但它们跟其他特征存在交互效应。比如某个特征单独对目标变量影响不大,但跟另一个特征组合后能显著提升预测精度。
这个坑让我明白:特征重要性是模型拟合后的结果,不是特征选择的依据。要判断一个特征是否该保留,应该做消融实验——删掉这个特征重新训练,看性能变化。而不是直接看重要性排序。
6.4 忽略目标变量截断导致的评估偏差
前面提过MEDV在50处截断,但我一开始没当回事。直到有一次分析残差图,发现模型在所有高价样本上都预测偏低,而且偏低幅度很一致。原因就是模型学到了"价格不会超过50"这个错误模式。
修复方法是在评估时把截断样本单独拿出来看,或者在训练时给这些样本更低的权重。我最终选择的是在数据预处理阶段把MEDV等于50的样本标记出来,训练时正常使用,但评估时分别报告截断样本和非截断样本的MSE。
7. 模型评估与结果解读:数字背后的故事
7.1 除了MSE和R2,还应该看什么
MSE和R2是回归任务的标准指标,但它们不能告诉你全部故事。我还会看以下三个东西:
残差分布图。把预测值减去真实值,画直方图。理想情况下残差应该围绕0对称分布。如果残差有偏或者有长尾,说明模型在某些区间存在系统性偏差。
预测值vs真实值散点图。如果所有点都落在45度线附近,说明模型预测准确。如果高价区间的点普遍低于45度线,说明模型对高价样本预测偏低。
分组误差分析。把测试集按目标变量分成几个区间,比如低价、中价、高价,分别计算每个区间的MSE。这样能发现模型在哪个价格区间表现最差。
我跑出来的分组误差显示,模型在低价区间(MEDV<15)的MSE约6.2,中价区间(15-30)约9.8,高价区间(>30)约18.5。高价区间的误差明显更大,这跟数据截断和样本量少都有关系。
7.2 与随机森林、梯度提升的横向对比
为了确认AdaBoost在这个任务上的定位,我顺手跑了随机森林和梯度提升回归作为对比。三个模型用相同的训练集和测试集,相同的交叉验证策略。
| 模型 | 测试集MSE | 测试集MAE | 测试集R2 | 训练时间 |
|---|---|---|---|---|
| 单棵决策树 | 14.8 | 3.1 | 0.80 | 0.1s |
| AdaBoost | 10.5 | 2.6 | 0.86 | 1.2s |
| 随机森林 | 9.8 | 2.5 | 0.87 | 2.5s |
| 梯度提升 | 9.2 | 2.4 | 0.88 | 3.8s |
从这组数据能看出,AdaBoost相比单模型有显著提升,但相比随机森林和梯度提升略逊一筹。这符合预期,因为随机森林通过Bagging降低了方差,梯度提升通过拟合残差进一步降低了偏差,而AdaBoost在这两方面都不如它们极致。
但AdaBoost的优势在于参数少、解释性强、训练速度快。在需要快速迭代或者解释模型决策路径的场景下,AdaBoost仍然是一个值得考虑的选择。
7.3 模型稳定性的检验方法
最后一步,我会检验模型的稳定性。方法很简单:用不同的随机种子重复训练和评估10次,看性能指标的波动范围。
mse_list = [] for seed in range(10): X_tr, X_te, y_tr, y_te = train_test_split( X, y, test_size=0.2, random_state=seed ) scaler = StandardScaler() X_tr_s = scaler.fit_transform(X_tr) X_te_s = scaler.transform(X_te) ada = AdaBoostRegressor( estimator=DecisionTreeRegressor(max_depth=4), n_estimators=300, learning_rate=0.1, loss='linear', random_state=seed ) ada.fit(X_tr_s, y_tr) mse_list.append(mean_squared_error(y_te, ada.predict(X_te_s))) print(f"MSE均值: {np.mean(mse_list):.2f}") print(f"MSE标准差: {np.std(mse_list):.2f}") print(f"MSE范围: {np.min(mse_list):.2f} - {np.max(mse_list):.2f}")我跑出来的MSE均值约10.8,标准差约1.3,范围在8.9到13.2之间。这个波动范围是可以接受的,说明模型没有过度依赖某一次特定的数据划分。
如果标准差超过均值的20%,我就会警惕了。那通常意味着模型对训练数据的分布过于敏感,需要增加正则化、减少模型复杂度,或者检查数据中是否存在影响较大的异常样本。
8. 如果重新做一遍,我会在哪些地方省下时间
回头看整个流程,有几个地方我走了弯路,如果重新来一次,我会调整策略。
第一,先做数据探索再选模型。我一开始直接上了AdaBoost,调了半天参数才发现数据里有截断问题。如果先画分布图、先做分组分析,就能更早发现这个问题,避免在错误的方向上浪费时间。
第二,学习率和迭代次数一起调,不要分开调。我最初先固定学习率调迭代次数,再固定迭代次数调学习率,来回折腾了好几轮。后来发现这两个参数必须联合调优,用二维网格搜索一次搞定效率最高。
第三,不要迷信网格搜索的最优结果。网格搜索找到的是在交叉验证框架下的最优参数,但交叉验证的划分方式会影响结果。我一般会在网格搜索之后,用不同的交叉验证策略再验证一遍,确认最优参数不是过拟合到某一种划分方式上。
第四,早停策略能省大量时间。如果一开始就设置早停,学习率0.01配合5000次迭代那个实验根本不需要跑完。早停会在验证集误差连续若干轮不下降时自动终止训练,既省时间又避免过拟合。
提示:AdaBoostRegressor本身没有内置早停参数,但你可以通过监控验证集误差手动实现。或者用
sklearn.model_selection中的learning_curve来辅助判断。
最后分享一个我常用的调试技巧:把弱学习器的数量设成一个很小的值(比如5),然后打印每个弱学习器的预测结果和权重。这样能直观地看到AdaBoost是如何逐步修正预测的。虽然不能直接用于生产,但对理解算法行为帮助极大。我就是在做这个调试时,才真正理解了为什么回归任务要用加权中位数而不是加权平均。