ARTICLE DETAIL

资讯详情

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

线性回归实验全解析:从梯度下降到代码实现

线性回归实验全解析:从梯度下降到代码实现 线性回归这个实验基本上是每个学机器学习的人绕不过去的第一道坎。我在做山东大学机器学习课程实验一的时候感触特别深看似只是拟合一条直线但里面藏着的梯度下降、损失函数、特征缩放这些概念几乎是后面所有模型的地基。这篇文章不打算照搬实验指导书而是按我自己从拿到题目到最终跑通、调参、写报告的全过程把线性回归从数学原理到代码实现完整拆一遍。不管你是刚接触机器学习还是正在被某个实验卡住这篇内容应该都能给你一些可以直接用的东西。我先把话放在前面这绝不是一个只调 sklearn 包就能交差的实验。实验一的核心目的是让你亲手把线性回归的每一个环节抠明白——为什么用最小二乘、梯度下降到底在下降什么、学习率设成多少才不会翻车。搞清楚这些后面的逻辑回归、神经网络、SVM 学起来会顺畅得多。1. 实验整体设计与思路拆解1.1 线性回归解决的是什么问题我们先从最朴素的问题说起。假设你有一堆数据点每个点有若干特征和一个真实取值你想找到一个函数用这些特征去逼近真实值。线性回归就是假设这个函数是特征的线性组合也就是[ y w_1 x_1 w_2 x_2 ... w_n x_n b ]这里的 (w) 是权重(b) 是偏置。训练模型的过程就是不断调整 (w) 和 (b)让预测值尽可能接近真实值。听起来很简单但尽可能接近这四个字是需要量化的。你得定义一个指标告诉模型现在离目标还有多远。这个指标就是损失函数。线性回归最常用的损失函数是均方误差MSE它把每个样本的预测误差平方后取平均[ L \frac{1}{m} \sum_{i1}^{m} (y^{(i)} - \hat{y}^{(i)})^2 ]为什么要用平方而不是绝对值两个原因一是平方函数处处可导方便用梯度下降求导更新参数二是平方放大了大误差的惩罚力度让模型更重视那些偏差大的样本。当然这也带来副作用——对异常点特别敏感后面我们会专门讲这个问题。我当初做实验时最大的误区就是以为线性回归只是画一条线。实际上一旦特征维度超过三维你根本无法直观画出那条线所有判断都要靠数字损失值、梯度大小、评估指标。所以做这个实验的关键不在于看到拟合效果而在于建立一套数值上的反馈闭环。1.2 为什么实验一偏偏选线性回归很多同学会问第一个实验为什么不直接上神经网络这个问题我思考了很久后来在课程中才慢慢明白。线性回归是整个机器学习地图里最基础、也最完整的一个闭环第一它完整覆盖机器学习的基本流程。数据预处理 → 模型假设 → 损失函数定义 → 优化求解 → 评估调参这五步在任何一个模型里都缺一不可。线性回归把这套流程以最简单的方式呈现出来让你在一两天内就能建立全局观。第二它的数学推导相对友好。损失函数对参数的导数只需要链式法则就能搞定不需要矩阵微积分等高阶工具。这样你能把精力放在理解梯度下降为什么有效而不是卡在求导上。第三它有闭式解可以作为参照系。线性回归除了可以用梯度下降迭代求解还可以用正规方程Normal Equation一步算出最优参数。这意味着你可以用正规方程的结果来验证梯度下降是否收敛到了正确的位置。这在后面的复杂模型里是做不到的算是实验一独有的校对工具。我当时做实验时会故意让梯度下降跑出不同的初始值看它是否都能收敛到和正规方程一致的结果。这个习惯让我后来调试神经网络时能够更快判断模型是收敛到了局部最优还是根本没收敛。1.3 梯度下降模型学习的本质梯度下降是整个实验的灵魂。它的核心思想非常直白你在山上往下走每一步都沿着最陡的下坡方向走最终就能到达山脚。数学上最陡的下坡方向就是损失函数梯度的反方向。参数更新公式长这样[ w_j : w_j - \alpha \frac{\partial L}{\partial w_j} ]其中 (\alpha) 是学习率控制每一步迈多大。(\frac{\partial L}{\partial w_j}) 是损失函数对第 (j) 个参数的偏导数它告诉你当前这个参数应该往哪个方向调、调多少。我第一次看这个公式时觉得太抽象了直到我手动推导了一次偏导数。对于 MSE 损失损失对权重的偏导是[ \frac{\partial L}{\partial w_j} -\frac{2}{m} \sum_{i1}^{m} (y^{(i)} - \hat{y}^{(i)}) x_j^{(i)} ]看到没这个导数其实就是在算误差乘以特征值的平均。如果整体上某个特征和误差正相关说明增大这个特征对应的权重会让误差更大那就该往反方向调。这个直觉非常重要——它解释了为什么梯度下降不是一个黑箱它的每一步调整都有明确的统计意义。2. 核心数学模型解析代价函数与参数更新2.1 损失函数的设计逻辑为什么是MSE我见过不少同学直接复制损失函数代码从没想过为什么长这样。这里把它的来历说透。MSE 损失本质上是极大似然估计的产物。假设真实值和预测值之间的误差服从均值为 0 的高斯分布那么给定数据模型参数的最大似然估计恰好等价于最小化 MSE。换句话说你用 MSE 做损失函数隐含着误差来自大量微小随机因素叠加这个假设。这也解释了为什么 MSE 对异常值敏感。高斯分布的尾部很快衰减一旦出现一个偏离特别远的点平方项会让它的影响急剧放大。模型为了照顾这个离群点往往会牺牲其他正常样本的拟合质量。我在实验报告里做了一组小实验在数据集中加入一个偏差极大的异常点观察拟合直线被拽过去的情况。对比使用 MSE 和绝对值误差MAE的模型你会发现 MSE 的拟合线偏移非常明显。理解这个性质对你后面做数据清洗会很有帮助——异常值不能随便留着否则模型会花大量精力去讨好它。2.2 梯度下降的三种变体批量、随机、小批量梯度下降不是一个固定模式根据每次更新使用的样本量不同分为三种批量梯度下降BGD每次更新用全部训练样本。优点是方向准确缺点是大数据量下计算量太大。随机梯度下降SGD每次更新只用一个随机样本。优点是快缺点是更新方向噪声大损失曲线会抖动。小批量梯度下降Mini-batch GD每次用一小批样本比如 32 个。这是工程上最常用的折中方案。实验一的数据量通常不大用批量梯度下降完全没问题而且能看到平滑的收敛曲线。但我在实验中会额外写一个 SGD 版本目的就是对比观察两者的损失曲线差异——SGD 的曲线会像锯齿一样抖动但总体仍在下降。这个经验在我后来调神经网络时非常有用因为看到抖动的损失曲线不会再慌张知道这是正常现象。2.3 学习率的选择策略与收敛性分析学习率是实验中一个最让人头疼的超参数。设太大参数会在最优解附近震荡甚至发散设太小收敛慢得像蜗牛爬。判断学习率是否合适最直接的方法是画损失曲线。一个正常的训练过程损失应该单调下降或近似单调下降最终趋于平稳。如果损失曲线出现明显上升说明学习率太大如果下降非常缓慢几百轮迭代还没看到收敛迹象说明学习率太小。我习惯采用对数网格搜索的策略先试 0.1、0.01、0.001、0.0001 几档找到一个量级后再在这个量级附近精细调整。对于实验一的简单数据学习率一般在 0.01 到 0.1 之间就能获得不错的效果。这里有个很多教程不会提的细节如果你的特征没有做归一化不同特征的量纲差异会导致损失函数形成一个狭长的山谷梯度下降会在山谷两侧来回震荡效率极低。这就是为什么要做特征缩放的根本原因。3. 实操过程与核心环节实现3.1 数据准备与探索性分析实验一的数据集,可以直接用 sklearn 自带的波士顿房价数据集也可以自己构造带噪声的线性数据。我建议两种都试因为它们的难度梯度不同。自造数据的逻辑很简单生成一组 (x)给定真实的 (w) 和 (b)再加一点高斯噪声。这样做的好处是你心里清楚真实参数是什么可以直观验证模型学出来的参数是否接近真相。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) n 100 true_w 2.5 true_b 1.0 x np.random.uniform(0, 10, n) noise np.random.normal(0, 1.5, n) y true_w * x true_b noise拿到数据的第一件事不是建模而是可视化。把数据点画出来看看分布形状、有没有明显异常点、是不是真的满足线性关系。这一步花 10 分钟能避免后面 1 小时在错误方向上白费功夫。我在第一次做实验时没有画图直接闷头训练结果拟合效果差得离谱。后来画图才发现我把 x 和 y 的顺序搞反了。可视化不仅仅是展示手段更是一种调试工具。3.2 特征缩放与数据划分特征缩放的道理在 2.3 节已经说过。常用的方法有两种标准化Standardization和归一化Normalization。标准化将特征转换为均值为 0、方差为 1 的分布公式是 [ x \frac{x - \mu}{\sigma} ]归一化将特征缩放到 [0, 1] 区间公式是 [ x \frac{x - x_{min}}{x_{max} - x_{min}} ]线性回归用标准化更合适因为它不假设特征的边界而且与梯度下降的数学推导更契合。注意一个细节缩放参数均值和方差只能用训练集计算然后用这套参数去变换验证集和测试集。如果直接用全部数据计算均值方差会造成数据泄露导致评估结果偏乐观。数据划分我习惯按照 7:3 划分训练集和测试集必要时再把训练集切出一部分做验证集来调超参数。实验一数据量小不需要太复杂的划分策略但训练集/测试集必须分开这条原则要养成肌肉记忆。3.3 手写线性回归的完整代码实现下面是我做实验时的核心代码。先看用 NumPy 手写的梯度下降版本这是实验的灵魂。def linear_regression_gd(X, y, alpha0.01, epochs1000): m, n X.shape # 初始化参数全零或者随机都可以 w np.zeros(n) b 0.0 loss_history [] for epoch in range(epochs): y_pred X w b error y_pred - y # 形状: (m,) # 计算梯度这里用到的是 2.1 节的偏导公式 dw (2/m) * (X.T error) db (2/m) * np.sum(error) # 参数更新 w - alpha * dw b - alpha * db loss np.mean(error ** 2) loss_history.append(loss) return w, b, loss_history这里有几个容易犯的错我说一下参数初始化用全零还是随机线性回归的损失函数是凸函数只有一个全局最优解所以全零初始化没问题。但如果是逻辑回归或神经网络全零初始化会导致对称性问题那时就必须用随机初始化。我建议从实验一开始就养成用随机初始化的习惯为后面做铺垫。为什么要除以 2我在梯度公式里写的是 (2/m)而不是 (1/m)。这是为了求导时抵消平方项带来的 2让梯度表达更简洁。很多教材会这样做但这只是一个缩放常数不影响最终收敛结果只会影响损失曲线的绝对值大小。能不能直接用正规方程当然可以而且代码非常简洁# 正规方程 X_b np.c_[np.ones((n, 1)), X] # 在 X 前加一列 1用来拟合偏置 b theta np.linalg.inv(X_b.T X_b) X_b.T y正规方程一步到位不需要迭代。但它的瓶颈在于矩阵求逆的复杂度是 (O(n^3))当特征数量很大比如上万时计算会非常慢。这就是为什么梯度下降在实践中的地位无可替代。实验里我强烈建议你把两种方法都写出来然后对比结果验证梯度下降是否正确收敛。3.4 模型评估不只是看损失模型训练完还需要回答一个问题这个模型到底好不好实验报告里需要给出量化评估指标。线性回归最常用的三个指标MSE均方误差预测误差平方的平均值。受异常值影响大但最常用。RMSE均方根误差MSE 开根号量纲与原始数据一致更直观。R²决定系数模型解释了多少数据方差最大值是 1越接近 1 越好。R² 的计算公式是 [ R^2 1 - \frac{\sum (y_i - \hat{y}_i)^2}{\sum (y_i - \bar{y})^2} ]我在评估时一定会同时看训练集和测试集的指标。如果训练集 R² 很高但测试集很低说明过拟合了如果两者都很低说明模型本身就不够强可能需要引入更多特征。有一个经验交流R² 达到多少算好没有绝对标准取决于问题的难度和数据本身的噪声水平。自造数据时R² 可能能达到 0.9 以上但现实数据比如波士顿房价达到 0.7 已经算不错的模型。不要盲目追求 R² 接近 1那是自欺欺人。3.5 用 sklearn 验证手写结果手写代码跑通后我建议用 sklearn 的 LinearRegression 做一次交叉验证确认手写结果没有系统性偏差。from sklearn.linear_model import LinearRegression from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42 ) scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) model LinearRegression() model.fit(X_train_scaled, y_train) print(系数:, model.coef_, 截距:, model.intercept_) print(测试集 R²:, model.score(X_test_scaled, y_test))注意我用fit_transform处理训练集、用transform处理测试集这一步很重要前面说过原因。sklearn 的 LinearRegression 采用的是最小二乘解法和你的手写梯度下降理论上应该得到几乎相同的结果。如果两者差异很大那就要回头检查梯度下降的实现了。4. 实验中的常见问题与排查技巧实录4.1 损失不降反升问题出在哪这是实验里最高频的问题。我自己第一次跑的时候也遇到过损失曲线像过山车一样往上冲。原因几乎永远是学习率太大。排查思路是这样的先把学习率调小一个数量级比如从 0.1 调到 0.01看损失是否恢复正常下降。如果还是不行再调小一个数量级直到损失稳定下降为止。这样做虽然耗时但能最快定位问题。另一个可能的原因是特征没有标准化导致损失曲面过于狭长。这时即使学习率看起来合理梯度下降也会在某个方向上震荡。建议先做特征标准化再回头调学习率两个问题要区分开。4.2 损失曲线收敛到很大值模型完全不能用如果损失在训练集上收敛到非常大的值说明模型根本没有学到数据里的规律。可能的原因特征与目标确实没有线性关系。这是模型的假设错了你需要换模型或者做特征变换。数据里有严重异常值MSE 被异常值主导了。可以画箱线图检查或者用 MAE 作为辅助判断。代码逻辑有 bug比如梯度计算时把正负号搞反了或者参数更新公式写错了。这里分享一个我常用的调试技巧在训练前先用最简单的数据比如一条完全没有噪声的直线验证模型。如果模型连这种理想情况都拟合不了那一定是代码有问题而不是数据问题。把问题简化到不能再简化往往能快速定位 bug。4.3 训练集和测试集表现差异过大这种情况基本可以断定是过拟合。但线性回归在低维情况下过拟合的概率不算高如果出现差异大更可能是以下原因测试集和训练集分布不一致。比如你按时间顺序划分数据但数据的规律随时间发生了漂移。数据量太少测试集只有十几个样本方差极大评估结果不可靠。线性回归出现明显过拟合通常是在特征维度接近样本数量甚至超过样本数量时。这时候模型会记住每一个训练样本但完全无法泛化。解决办法有两个方向一是减少特征特征选择二是使用正则化岭回归、Lasso 回归。我在实验报告里会专门做一组高维特征导致的过拟合演示这能直观展示为什么加入正则化是必要的。4.4 梯度计算出现 NaN 或超大值这种情况多见于特征数值很大且没有做标准化。当特征数值在万级以上时梯度的量级也会非常大参数更新一步就可能把损失推到无穷大最终溢出为 NaN。解决方法是先做标准化再把学习率调到足够小。如果已经出现 NaN重启内核重新跑一遍因为状态已经污染了继续迭代没有意义。我见过一些同学遇到 NaN 后一直在调学习率调了半天也没用。这时先检查数据的规模如果你发现 (x) 的取值范围是几万而 (y) 的取值范围是几十那问题一定出在特征缩放上。4.5 实验报告的常规结构与避坑建议实验一通常要求提交实验报告结构上可以参考实验目的、实验原理损失函数、梯度下降推导、实验过程数据说明、代码实现、结果展示、分析与总结。写报告时我建议你多放图和对比实验结果。用代码画一张损失随迭代次数变化的图画一张拟合直线与原始数据的图再画一张不同学习率效果对比的图。这三张图基本就能把实验做扎实的证据链补完整。有一个我不太认同的做法是为了追求完美的收敛曲线把学习率调到极小值导致迭代几千次才收敛。这看起来漂亮但并不是好的工程习惯。合理的做法是选择适中的学习率让模型在几十到几百次迭代内收敛这才说明了参数的合理性。5. 实验之后可以做的几个扩展练习基础实验做完如果你还有余力我强烈建议做以下三个扩展。它们每一步都只加一点点复杂度但对理解的提升是质的飞跃。5.1 自己实现一个 Batch Generator实验一通常是一次性把所有数据丢进去算梯度。但现实中模型训练往往需要用小批量数据迭代很多轮。你可以手写一个简单的 batch 数据生成器每次从训练集中随机取出指定大小的子集计算梯度更新参数。这个练习会让你提前熟悉深度学习框架中 DataLoader 的核心逻辑后面学 PyTorch 时会轻松很多。5.2 给线性回归加上 L2 正则化L2 正则化的损失函数在原来的基础上加了一项[ L \frac{1}{m}\sum_{i}(y_i - \hat{y}_i)^2 \lambda \sum_j w_j^2 ]加正则化之后梯度公式只在原有梯度上多了一项 (2\lambda w_j)实现起来非常简单但效果立竿见影——它能有效抑制权重过大导致的过拟合。这就是岭回归。我在实验中会专门对比正则化前/后在高维特征数据上的表现这个对比放在报告里很有说服力。5.3 尝试对非线性数据进行多项式扩展线性回归不能处理明显的非线性关系但可以通过添加多项式特征来曲线救国。比如原本只有一个特征 (x)你可以构造 (x^2)、(x^3) 作为新特征加入模型。这样模型就能拟合抛物线甚至更复杂的曲线。这个扩展练习的意义在于它让你意识到线性回归的线性指的是参数和特征之间的线性关系而不是特征本身必须是线性的。理解了这一点你就打开了特征工程的大门。我在实际做这个扩展时踩过一个小坑多项式特征不加缩放直接训练导致梯度爆炸。加了标准化之后问题立刻消失。这个经验不太容易从课本上得到写进报告里也算是一个亮点。写在最后的一些体会做完线性回归实验我的最大感受是机器学习的核心不是模型有多复杂而是你是否真正控制住了每一个超参数、每一个数据的流动方向。线性回归看起来简单但当你亲手把它的每个细节调通以后后面学什么都快。我个人在实际操作中发现带着问题做实验远比照着代码敲一遍收获大。你可以在每一步追问为什么这里要除以 m为什么梯度方向是反的为什么标准化能让收敛更快把这些问题逐个弄清楚实验一才算真正过关。最后分享一个小技巧写实验报告时不要只贴代码和运行结果把你调试过程中遇到的错误、你的排查思路、最后如何解决都写进去。这些内容看起来不如结果图漂亮但恰恰是老师最想看到的思考痕迹也是你几年后回看时最珍贵的记录。线性回归只是起点后面还有逻辑回归、神经网络、卷积网络。但只要你把这一步走扎实了后面都是水到渠成的事情。祝实验顺利。
返回列表