回归作业实战复盘:数据清洗、梯度下降与正则化调参完整记录

发布时间:2026/10/15 1:14:00
回归作业实战复盘:数据清洗、梯度下降与正则化调参完整记录
很多人觉得机器学习课程的第一份编程作业就是热热身——回归嘛不就是一个LinearRegression函数跑完交个图就行。但我这周被这份作业结结实实地上了一课身边不少朋友和我一样模型早早跑通了误差却怎么都压不下去回头查才发现问题压根不在模型上而在数据没看、特征没处理、超参数没调这些看起来不起眼的环节。下面按我实际操作的顺序记录这份回归作业从拿到数据到最终调参的完整过程。1. 作业拆解这份回归作业到底要解决什么问题1.1 数据长什么样以及拿到数据后我做的第一件事这次作业来自某机器学习公开课的第一单元任务很明确给定一个表格数据包含若干特征列和一个连续的标签列训练回归模型预测新样本的标签值。数据量不算大训练集大概几百行特征有连续型也有离散型测试集需要把预测结果按指定格式提交作业平台会直接按误差给分。我刚拿到数据的第一反应不是急着建模而是先打开数据看一眼。用 pandas 的info()和describe()快速扫一遍确认每列的类型、缺失情况和取值范围。这个习惯是我做前几个小项目时养成的磨刀不误砍柴工。这次果然发现了问题有一个特征在训练集和测试集上的取值范围差异很大训练集集中在某个区间测试集却出现了明显超出该区间的样本。如果不提前意识到这一点后面做标准化时就会用训练集的均值方差去强行变换测试集导致预测值在那些极端样本上发生不必要的偏移。另外我还会顺手画一下特征的相关性矩阵。这不是为了删特征而是为了在拟合完模型后有个预期如果某几个特征高度相关线性回归的系数往往会变得不太稳定解释系数时也要多留个心眼。回归任务里数据好不好往往比模型选型更决定最终效果。因为线性模型的表达空间相对有限它最终能学到的规律上限很大程度上取决于输入数据的质量。你不把数据分布看清楚后面调参就像盲人摸象运气好碰对了运气不好折腾半天也找不到方向。同时我也建议大家把训练集和测试集的列名对齐检查一遍。做过几个项目之后我发现有些作业数据会出现训练集多一列或者少一列的情况看似小事真跑起来却容易让你反复怀疑自己的人生。提前用set()比较一下两边列名能省下很多不必要的排查时间。1.2 回归任务为什么被安排在课程最前面回归是机器学习里最容易上手、也最容易讲透的任务之一。模型的数学形式很简单y w^T x b给定输入特征找到一组参数让预测值尽量贴近真实标签。但别小看这份简单理解了回归你就能理解机器学习算法的一个通用骨架先定模型假设再定损失函数再用优化算法去最小化损失最后评估模型能不能推广到新数据。这个骨架在后面的逻辑回归、神经网络、甚至更复杂的模型里都会反复出现。课程把回归放在第一份作业我的理解是它并不要求你调出一个多漂亮的模型而是想让你亲手体会一次“模型 损失 优化”的最小闭环。因为我在跟这门课的过程中发现很多人听理论时觉得线性回归简单到不值得花时间真到了写代码的时候才发现自己连最基本的几个坑都躲不过去忘了加偏置项、没做特征缩放、学习率设置不合理、验证集划分太随意。这份作业恰好把这些坑集中暴露出来早点踩一遍后面学逻辑回归和神经网络时会轻松很多至少你不会再对着损失曲线一头雾水。我建议所有跟课的朋友别只满足于把模板代码跑通。老师给的 baseline 只是一个起点你要试着把公式推导、手写实现、工具库调参这三条线串起来。如果时间允许闭式解和梯度下降都亲手写一遍再去用 scikit-learn 的现成接口理解深度会完全不一样。2. 数据清洗和特征观察建模前最容易翻车的环节2.1 特征分布不均衡的影响一开始我偷了个懒跳过数据观察直接把数据塞进LinearRegression跑了一版。结果训练集上的误差高得离谱我一时间还以为是代码写错了追查了半天才发现根源在于不同特征的取值范围差异太大。有的特征取值在 0 到 100 之间有的特征却到了几千甚至几万而标签本身也有不小的波动幅度。这种情况下梯度下降的收敛路径会变得非常扭曲模型权重也容易被那些大数值特征主导其他特征就算有真实贡献也被淹没了。解决办法很常规做 z-score 标准化对每个连续特征减去均值再除以标准差。sklearn 里的StandardScaler可以直接搞定但有一个关键点经常被忽略标准化用的均值和标准差必须在训练集上计算然后把同一套参数保存下来再去变换验证集和测试集。如果图省事直接把全量数据拿去 fit就相当于在建模前就把“测试集长什么样”的信息泄漏给了模型后续所有验证指标都会偏乐观。这个坑最阴险的地方在于不会报错只会让你的线下表现和线上分数对不上找不到原因。标准化前后的差异我用梯度下降实验直观感受了一番没标准化时迭代几百次损失曲线还在缓慢下降标准化之后同样的学习率下大概几十次就能看到明显收敛。数据尺度的作用真的只有动手对比过才能留下深刻印象。2.2 缺失值、异常值和数据泄漏这三个坑数据里存在少量缺失值比例不高直接删除对应行不会带来太大影响。但在实际项目中缺失处理一定要有明确策略连续特征可以考虑均值、中位数填充偏态比较严重的变量宁可用中位数离散特征可以用众数填充或者干脆单独增加一个“是否缺失”的指示列。关键不在于填充方法有多高级而在于训练和预测阶段必须使用同一套规则避免两边处理不一致否则你在离线验证时看到的误差根本不能代表线上表现。异常值对回归的影响比很多人想象中更大。线性回归用的是平方误差一个极端大的离群样本它的损失可能顶得上几百个正常样本模型会宁可牺牲大多数样本来迁就这个离群点。我这次做残差分析时发现有几个样本的预测误差特别大顺着样本查回去发现它们的某个特征值明显偏离主体区间。对于这份作业数据除非有明确理由认定它们是错误数据否则我没有直接删除而是先尝试对标签做对数变换缩小极端值的影响预测完成后再做指数还原。这种处理比硬删数据更稳也能保留样本包含的其他有效信息。第三个坑是数据泄漏。我中途曾经试过一个“巧办法”用标签的统计量去构造了一个新特征训练误差立马变得非常好看。但冷静下来一想这种特征在真实预测时根本拿不到模型不过是用另一种方式把答案偷偷藏在了特征里。这个例子给我提了个醒做特征工程的时候一定要反复问自己这个信息在预测阶段还能拿到吗拿不到就别往模型里放哪怕训练指标再好看最后都是纸面繁荣。3. 从正规方程到梯度下降两条求解路线我都跑了一遍3.1 闭式解的推导和数值稳定性问题线性回归的损失函数是凸函数这意味着可以直接通过求导并令梯度为 0得到全局最优解的解析表达式。推导过程不算复杂把损失写成矩阵形式 L(w) (1/2m)‖Xw - y‖²对 w 求导令导数为 0就得到 w (X^T X)^(-1) X^T y。第一次用代码把这个公式实现出来的时候还挺有成就感的因为那意味着我不是在调包而是真的理解了这个模型是怎么被“算出来”的def normal_equation(X, y): X_b np.c_[np.ones((X.shape[0], 1)), X] # 加一列偏置 theta np.linalg.inv(X_b.T X_b) X_b.T y return theta[0], theta[1:]不过真正用起来闭式解有一些不容忽视的限制。当特征数量增多或者特征之间存在较强的相关性时X^T X 会接近奇异矩阵np.linalg.inv算出的结果极不稳定甚至出现数值上非常离谱的权重。一个实用的改进方案是用伪逆np.linalg.pinv它内部做了奇异值分解数值稳定性好很多。另一个思路就是引岭回归在对角线上加一个小小的惩罚项矩阵求逆自然就稳定下来。我这次实验到后面优先选择了带正则化的求解方式很大程度上就是因为亲身体会到了纯闭式解在多特征场景下的脆弱。闭式解适合数据量小的场景算得快也不需要调超参数。但它的局限同样明显当样本量达到百万级或者特征维度高到一定程度时X^T X 的求逆计算会慢到让人失去耐心这时梯度下降这类迭代优化方法才是更现实的选择。所以我的态度是两条路线都要会什么场景用哪个心里要有数。3.2 手写梯度下降理解收敛和损失函数课程要求最好能手动实现梯度下降不能只依赖工具库。我于是照着公式手写了一个 batch 梯度下降版本这段代码帮助我把几个理论课上比较抽象的概念落到了实处def gradient_descent(X, y, lr0.01, epochs1000): m, n X.shape X_b np.c_[np.ones((m, 1)), X] theta np.zeros(n 1) losses [] for epoch in range(epochs): error X_b theta - y grad (1 / m) * X_b.T error theta theta - lr * grad loss (1 / (2 * m)) * (error.T error) losses.append(loss) return theta, losses刚开始写的时候我一度忘记在 X 矩阵里加偏置列模型怎么拟合误差都很大。后来才反应过来只有权重 w 而没有偏置 b等于强行让回归直线必须过原点数据不满足这个假设时模型的表现自然好不到哪里去。这个细节真的非常基础但也很容易在埋头写代码时被忽略。实现完以后我每次迭代都把 loss 记录下来画出损失下降曲线。这条曲线才是真正诊断训练过程的工具如果损失剧烈震荡说明学习率偏大如果曲线平稳下降说明参数合适如果下降得异常缓慢就要考虑调大学习率或者增加迭代次数。我第一次跑的时候损失曲线在 100 次迭代后基本不动了我还以为模型已经收敛后来才发现是学习率设得太小模型还停在半山腰步子太小根本迈不动。3.3 学习率实验对比为了直观感受学习率的作用我同时跑了 0.1、0.01、0.001 三组实验把收敛情况整理成了下面的表格学习率前50次迭代损失下降速度收敛表现最终损失0.1非常快但伴随轻微震荡在较小范围内波动数值低但噪声偏大0.01稳定下降约300次迭代后基本平稳稳定且偏低0.001缓慢下降迭代2000次仍在下降相对偏高这个实验让我彻底明白了一件事学习率不是一个孤立参数它和迭代次数、数据尺度、甚至初始化方式都有关联。不存在一个“万能学习率”能适配所有场景。更好的做法是每次训练时把 loss 曲线打出来观察它是震荡、下降还是停滞再决定下一轮往哪个方向调参数。说实话这个习惯在后面的神经网络训练里也帮了我很大的忙因为判断优化问题是否出现逻辑几乎一模一样。4. 多项式特征与正则化从欠拟合到过拟合的完整轨迹4.1 什么时候需要用多项式特征用原始特征跑出来的线性模型验证集误差一直压不下去。把预测值和真实值画成散点图之后我注意到标签和主要特征之间存在明显的曲线关系某个特征从低到高时标签先上升后下降这是单条直线完全没法表达的。这时候就需要对特征做非线性扩展。多项式扩展是其中最直接的一种把单个特征 x 扩展成 x、x²、x³ 等相当于把原来的曲线关系在高维空间里转换成线性关系让线性回归也能拟合出弯弯曲曲的边界。用 scikit-learn 可以很轻松地搭出这个流程from sklearn.preprocessing import PolynomialFeatures, StandardScaler from sklearn.pipeline import make_pipeline from sklearn.linear_model import LinearRegression poly_model make_pipeline( PolynomialFeatures(degree3, include_biasFalse), StandardScaler(), LinearRegression() )这里有一个容易搞反的顺序问题PolynomialFeatures生成的高次项和交互项取值范围会被平方级放大所以后面必须接StandardScaler否则数值稳定性会很差。我第一次搭 pipeline 时顺手把StandardScaler放到了PolynomialFeatures前面效果明显变差。原因在于标准化后再生成多项式和先扩展再统一标准化两种操作的数学含义并不等价。踩坑之后我建议保持“多项式 → 标准化 → 线性回归”这个顺序既好记也稳定。4.2 过拟合是怎么出现的正则化如何起作用把多项式次数从 1 调到 3、5 的过程中我完整地看了一遍“欠拟合 → 合适 → 过拟合”的完整轨迹。degree1 时训练误差和验证误差都偏高模型欠拟合degree3 时训练误差下降验证误差也跟着下降degree5 时训练误差继续走低但验证误差开始反弹。这就是过拟合的典型症状模型在训练集上记得太多甚至把噪声也背了下来反而失去了对新样本的概括能力。对抗过拟合的常用手段是正则化。岭回归也就是 L2 正则化在损失函数后面加了一个权重平方和项优化目标从“只减小拟合误差”变成“减小拟合误差的同时不让权重过大”。这个约束的直观含义是如果某个特征对应的权重非常大模型对这个特征就会异常敏感输入稍微变一点点预测值就剧烈浮动显然不够稳。限制权重大小相当于给模型加了一个平滑约束。Lasso也就是 L1 正则化则走的是另一条路线它倾向于把一部分权重直接压到 0天然就是在做特征选择。实际作业里我优先尝试了岭回归因为它的预测精度通常更稳定也不会像 L1 那样在特征相关性强时随机丢弃变量。如果你想快速试一组对比可以用Ridge和Lasso两个类alpha 控制惩罚强度越大模型越保守。4.3 用验证集选一个合适的模型复杂度为了避免凭感觉选多项式次数和正则化强度我把训练集再切出一部分作为验证集对 degree 取 1、2、3、4alpha 取 0.1、1、10做了个小范围的网格搜索。每次用训练集拟合在验证集上计算误差选择误差最小的参数组合最后才在测试集上评估一次。这个过程最大的收获是彻底意识到了“训练集、验证集、测试集”三者的分工。以前我做事比较粗糙一套数据既用来调参又用来评估最后自我感觉良好换到新数据就翻车。原因很简单超参数已经在测试集上调过一轮了测试集就不再有资格代表未知数据。这份回归作业让我把“验证集调参、测试集终评”的习惯真正建立了起来后面做更复杂的项目时少了那类低级失误整体效率反而高了不少。从结果来看degree3、alpha1 的组合在验证集上表现最好而 degree5 虽然训练误差最低验证集误差却明显更高。如果只盯着训练误差去选模型我几乎肯定会踩进过拟合的坑里。5. 评估指标与调参复盘分数背后的真实含义5.1 RMSE、MAE 和 R² 各自看到了什么到了收尾阶段指标的选择也值得认真对待。作业平台直接按预测误差排名但 RMSE、MAE、R² 这三个指标分别从不同角度刻画模型表现不能混为一谈。指标计算方式对离群点敏感度主要用途RMSE误差平方均值的平方根敏感与作业打分直接挂钩放大极端误差MAE误差绝对值的平均不敏感直观反映平均偏差R²1 - 残差平方和 / 总平方和敏感衡量模型相对“取均值”的改进比例我第一次跑出来的 RMSE 差不多是 MAE 的两倍多这个差距意味着误差分布里有少数样本贡献了相当大的平方项也就是存在着离群预测误差在拖低分数。顺着这个线索我找出误差最大的几个样本发现它们大多集中在某个特征取值极端的位置。这提示我训练数据里相对缺少该区间的样本模型在这个局部区域的表达能力不足单纯继续调现有参数帮助不大得回到特征层面想办法。R² 则提供了另一个视角如果 R² 已经比较高但 MAE 仍然偏大说明整体趋势拟合得不错但绝对误差还有压缩空间。这时候可以考虑在残差较大的局部区域做更细致的特征处理比如添加交互特征或者对目标变量做变换后再回归。5.2 残差图让我发现了模型漏掉的规律评估完常规指标后我又画了残差图横轴是模型预测值纵轴是真实值减预测值的残差。如果模型拟合足够充分残差应该围绕 0 随机分布不呈现任何系统性形状。但我画出来的图里预测值偏大的区域残差几乎清一色是负的也就是说模型在系统性低估高值样本。这个现象比单看 RMSE 下降更有指导意义。顺着这条线索我重新观察了那些高值样本的特征组合发现有两三个特征在这一段区间内存在明显的交互效应单独看线性贡献根本表达不出来。于是我在特征集里加入了对应的交互特征再用岭回归重新训练验证集误差又降了一截残差图也均匀了很多。这段经历让我很强烈地体会到回归作业真正想训练你的不是把提交分数刷高而是通过误差分析找出模型没捕捉到的结构再回到特征工程环节补上缺口。5.3 几个文档里不会写的实操细节最后记录一些我在本次作业里反复踩过的细节它们非常琐碎但每一个都可能直接影响最终结果。第一标准化的参数只从训练集计算。训练时把 mean 和 std 保存下来验证集和测试集一律用同一套转换不要在测试集上重新 fit。这一点前面提过但我愿意再强调一次因为它的后果往往是指标虚高、实战崩盘而且很难定位到原因。第二PolynomialFeatures和StandardScaler的顺序要固定成“多项式 → 标准化”。我踩坑时发现顺序反了会让模型效果明显变差而且这种差异不容易一眼看出来。第三超参数网格搜索的结果要随手记录。我当时调 degree 和 alpha 时只顾着看验证误差没留日志后来想回溯某个结果只能重跑一遍。建议在本地维护一张小表格参数组合、验证集误差、备注。这份日志在写实验报告和复盘时非常管用。第四残差图的形态比单个指标更能反映模型的问题。如果残差出现明显趋势或规律性形状说明模型还没有把应该用的信息用干净下一步方向大概率是特征工程而不是继续盲目调整现有参数。说实话这份回归作业做下来我感觉自己收获最大的不是“会调包”而是把模型、数据、优化、评估这条完整链路亲手走了一遍。如果你也在跟这门课我的建议是别急着追求测试集高分先把每个环节的图都画出来把每个参数为什么这么设置弄清楚。这些慢功夫恰恰是后续学更复杂模型时最值钱的底子。