恒美微站
首页
关于我们
建站服务
主题模板
案例展示
资讯中心
联系我们
AdaBoost回归实战:波士顿房价预测与调参避坑指南
首页
资讯中心
/
AdaBoost回归实战:波士顿房价预测与调参避坑指南
AdaBoost回归实战:波士顿房价预测与调参避坑指南
发布时间:2026/10/17 5:14:28
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/NN是样本总数。训练一个弱学习器计算它在每个样本上的预测误差。根据误差计算这个弱学习器的话语权误差越小话语权越大。更新样本权重误差大的样本权重增加误差小的样本权重减少。重复上述过程直到达到预设的弱学习器数量或误差阈值。最终预测结果是所有弱学习器预测值的加权中位数。这里有个细节特别值得注意回归任务中最终汇总用的是加权中位数而不是加权平均。为什么因为中位数对异常值更鲁棒。如果某个弱学习器在某个样本上给出了一个离谱的预测加权平均会把这个错误直接带进最终结果而加权中位数能有效削弱这种影响。这个设计选择背后是对回归任务中异常值问题的深刻理解。3.2 弱学习器的选择决策树桩为什么是默认答案AdaBoost的弱学习器不需要多强只需要比随机猜测好一点就行。在回归场景下最常用的弱学习器是决策树桩也就是深度为1的决策树。它每次只根据一个特征做一个切分简单到不能再简单。但为什么是决策树桩而不是别的我自己的理解有三层第一决策树桩天然处理非线性关系。它通过切分特征空间来拟合目标变量不需要假设任何函数形式。波士顿房价里RM和MEDV的那种分段关系树桩能直接捕捉到。第二决策树桩对特征量纲不敏感。它只关心切分点的位置不关心特征值的绝对大小。这省去了标准化的步骤也避免了标准化方法选择带来的不确定性。第三决策树桩的偏差-方差特性适合Boosting。单个树桩偏差高、方差低而Boosting的核心就是通过逐步叠加弱学习器来降低偏差。如果弱学习器本身方差就很高Boosting过程中容易过拟合。当然你也可以用深度更大的树比如max_depth3或4。这时候每个弱学习器更强需要的迭代次数更少但过拟合的风险也更高。我一般会从树桩开始试如果欠拟合明显再逐步增加深度。3.3 学习率与迭代次数一对需要联合调参的冤家学习率learning_rate和迭代次数n_estimators是AdaBoost里最重要的两个超参数而且它们之间存在强烈的耦合关系。学习率控制的是每个弱学习器对最终结果的贡献程度。学习率越小每个弱学习器的影响力越弱模型需要更多的迭代次数才能达到同样的拟合效果。这就像下坡时迈小步虽然慢但不容易冲过头。我做过一组对比实验在波士顿房价数据集上固定其他参数不变学习率迭代次数训练集MSE测试集MSE训练耗时1.0508.212.50.3s0.51007.811.20.6s0.15007.110.32.8s0.0510006.910.15.6s0.0150006.710.828.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, columnsdata.feature_names) y pd.Series(data.target, nameMEDV) 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接近859.2的平方那说明模型跟直接猜均值差不多没有任何价值。4.2 数据划分与标准化顺序错了结果就废了数据划分和标准化这两步顺序绝对不能错。必须先在训练集上拟合标准化器再应用到测试集。如果先对整个数据集做标准化再划分测试集的信息就泄露到了训练过程中评估结果会偏乐观。X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.2, random_state42 ) 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_depth4, random_state42) 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.8MAE约3.1R2约0.80。这个结果不算差但也不算好。R20.80意味着模型解释了80%的目标变量方差还有20%的信息没被捕捉到。这就是AdaBoost的切入点。4.4 AdaBoost模型训练参数背后的取舍逻辑现在上AdaBoost。我第一轮不会直接上网格搜索而是先用手动设定的参数跑一遍看看模型的基本行为。ada AdaBoostRegressor( estimatorDecisionTreeRegressor(max_depth4), n_estimators200, learning_rate0.1, losslinear, random_state42 ) ada.fit(X_train_scaled, y_train) y_pred_ada ada.predict(X_test_scaled) print(fAdaBoost - MSE: {mean_squared_error(y_test, y_pred_ada):.2f}) print(fAdaBoost - MAE: {mean_absolute_error(y_test, y_pred_ada):.2f}) print(fAdaBoost - R2: {r2_score(y_test, y_pred_ada):.2f})这一轮跑下来MSE降到了约10.5MAE约2.6R2提升到约0.86。相比单棵决策树MSE改善了约29%这个提升幅度在回归任务里已经相当可观了。这里有几个参数选择需要解释losslinearAdaBoostRegressor支持三种损失函数——linear、square和exponential。linear损失对异常值更鲁棒square损失对大误差更敏感exponential损失则更激进。波士顿房价数据集中存在截断值我倾向于用linear损失来降低异常值的影响。estimatorDecisionTreeRegressor(max_depth4)我没有用默认的树桩max_depth1而是用了深度4的树。原因是波士顿房价数据集只有506条样本特征间的交互关系比较复杂树桩的表达能力太弱需要非常多的迭代次数才能拟合。深度4的树在偏差和方差之间取得了更好的平衡。n_estimators200这个数字是试出来的。我画了迭代次数与测试集MSE的关系曲线发现200次之后曲线基本走平再增加迭代次数只会增加训练时间不会带来实质性的性能提升。4.5 用学习曲线找到迭代次数的拐点说到迭代次数我强烈建议每个人都画一下学习曲线。这个图能直观地告诉你模型什么时候开始过拟合什么时候还没学够。train_scores [] test_scores [] for n in range(50, 1001, 50): ada_temp AdaBoostRegressor( estimatorDecisionTreeRegressor(max_depth4), n_estimatorsn, learning_rate0.1, losslinear, random_state42 ) 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, markero) plt.plot(range(50, 1001, 50), test_scores, label测试集MSE, markers) 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(estimatorDecisionTreeRegressor(), random_state42), param_grid_coarse, cv5, scoringneg_mean_squared_error, n_jobs-1, verbose1 ) grid_coarse.fit(X_train_scaled, y_train) print(f最佳参数: {grid_coarse.best_params_}) print(f最佳CV MSE: {-grid_coarse.best_score_:.2f})这一轮跑完最佳组合是n_estimators300, learning_rate0.1, max_depth4CV 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_splits5, n_repeats10, random_state42) scores cross_val_score( AdaBoostRegressor( estimatorDecisionTreeRegressor(max_depth4), n_estimators300, learning_rate0.1, losslinear, random_state42 ), X_train_scaled, y_train, cvrkf, scoringneg_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, axis1) 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。这样能发现模型在哪个价格区间表现最差。我跑出来的分组误差显示模型在低价区间MEDV15的MSE约6.2中价区间15-30约9.8高价区间30约18.5。高价区间的误差明显更大这跟数据截断和样本量少都有关系。7.2 与随机森林、梯度提升的横向对比为了确认AdaBoost在这个任务上的定位我顺手跑了随机森林和梯度提升回归作为对比。三个模型用相同的训练集和测试集相同的交叉验证策略。模型测试集MSE测试集MAE测试集R2训练时间单棵决策树14.83.10.800.1sAdaBoost10.52.60.861.2s随机森林9.82.50.872.5s梯度提升9.22.40.883.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_size0.2, random_stateseed ) scaler StandardScaler() X_tr_s scaler.fit_transform(X_tr) X_te_s scaler.transform(X_te) ada AdaBoostRegressor( estimatorDecisionTreeRegressor(max_depth4), n_estimators300, learning_rate0.1, losslinear, random_stateseed ) ada.fit(X_tr_s, y_tr) mse_list.append(mean_squared_error(y_te, ada.predict(X_te_s))) print(fMSE均值: {np.mean(mse_list):.2f}) print(fMSE标准差: {np.std(mse_list):.2f}) print(fMSE范围: {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是如何逐步修正预测的。虽然不能直接用于生产但对理解算法行为帮助极大。我就是在做这个调试时才真正理解了为什么回归任务要用加权中位数而不是加权平均。