资讯动态

纯Python手写线性回归:从梯度下降到代码实现详解

发布时间:2026/10/4 3:19:49 来源:尧图企业网站定制
刚把线性回归从理论到代码完整过了一遍趁着手还热把这份学习笔记整理出来。这应该是最简单回归代码系列的第4篇前几篇分别讲了数学推导、数据准备和评估指标这一篇终于落到代码本身不借助任何机器学习框架用纯Python加NumPy把线性回归从零手写出来。这篇笔记适合三类人一是刚学完理论、想看代码长什么样的初学者二是会用sklearn但想知道底层到底发生什么的进阶者三是做量化、做数据分析时需要对回归结果有完全掌控力的实践者。看完全文你能得到一份可以直接抄走的最简实现以及几个不看一定会踩的坑。1. 这第4篇笔记到底在记什么1.1 为什么专门写最简单三个字市面上讲线性回归的代码多如牛毛有调sklearn三行搞定的有上PyTorch写优化器的还有直接上LightGBM、XGBoost做回归的。但最简单有它独特的价值代码短到你每一行都能看懂逻辑清晰到你能在脑子里跑通整个训练流程参数少到你可以手动算一遍梯度来验证。我见过太多人一开始就上高级框架结果模型跑通了却解释不了任何一行代码。等到调参的时候面对learning rate、batch size、weight decay这些旋钮完全不知道拧哪个、往哪个方向拧。反过来如果你亲手写过30行线性回归后面看XGBoost、看神经网络至少能看出哦这里是梯度下降的变体这里是用链式法则算梯度底层逻辑是通的。新手最大的误区是把调库当成会机器学习。真正的理解分三层第一层是知道函数怎么调第二层是知道函数内部在算什么第三层是知道为什么要这样算。这份笔记的目标就是帮你从第一层爬到第二层。1.2 前3篇笔记铺了哪些路既然这是系列第4篇前面3篇的内容先快速回顾便于后续代码能顺利衔接。第1篇讲的是线性回归的数学本质( y wx b )以及最小二乘的思想——找到一组 ( w ) 和 ( b )让预测值和真实值的平方误差最小。第2篇讲了损失函数 ( L \frac{1}{n}\sum(y_i - \hat{y}_i)^2 ) 的由来以及为什么用均方误差而不用绝对值误差——因为平方误差处处可导而且对大误差的惩罚更重这和实际场景中大偏差往往更不可接受是吻合的。第3篇讲了评估指标R²、MSE、RMSE怎么算、怎么解释。到这一篇数学基础已经够了该做的准备工作也做完了可以开始写代码了。我强烈建议你顺着这个顺序来而不是直接跳到这里看代码。否则可能会遇到代码看得懂但不知道为什么要用损失函数知道梯度下降是在下山但不知道为什么代码里是那样写的这类困惑还得回头补数学。2. 写代码前的三个前置判断真正动手写代码之前有三个选择必须先想清楚。这些选择没有绝对的对错但不同的选择对应不同的代码写法和后续调试方式提前想清楚能少走弯路。2.1 用NumPy还是直接用sklearn这是第一个要做的决定。我的选择是这篇笔记用NumPy手写然后用sklearn的LinearRegression做交叉验证。理由很简单sklearn的LinearRegression底层用的是最小二乘的闭式解正规方程它帮你把矩阵求逆都封装好了。你调用它输入X和y输出w和b中间过程完全黑盒。而手写版会用梯度下降迭代求解每一步都能打印出损失值的变化你能直观看到模型学习的过程这对建立直觉太重要了。打个比方调sklearn就像坐自动驾驶的车舒适快捷但你不知道路是怎么走的手写代码就像自己骑自行车慢一点但每一条路你都记得。这两种体验对学习来说缺一不可。2.2 梯度下降还是正规方程线性回归的参数求解有两条路梯度下降和正规方程。正规方程直接算 ( w (X^TX)^{-1}X^Ty )一步到位没有学习率、没有迭代次数代码极短。但它有个致命问题当特征维度高的时候矩阵求逆的计算量是 ( O(n^3) )特征一多就会非常慢而且如果特征之间存在多重共线性比如两个特征高度相关 ( X^TX ) 可能接近奇异矩阵求逆结果会非常不稳定。梯度下降是迭代逼近最优解虽然要调学习率和迭代次数但它有普适性——后面你学岭回归、逻辑回归、神经网络全都是同一套思路。从学习角度出发我选择梯度下降。这篇笔记里的代码就是用批量梯度下降Batch Gradient Descent每次迭代用全部样本计算梯度。它实现起来最简单数据量不大的时候也完全够用。2.3 损失函数怎么选线性回归的标准损失是均方误差MSE这一点在笔记第2篇已经推导过不再重复。但在代码实现里有一个小细节值得注意除以样本数到底除 ( n ) 还是除 ( 2n )。科学上MSE定义为 ( \frac{1}{n}\sum(y_i - \hat{y}_i)^2 )。梯度下降在求导的时候平方项会带下来一个2所以有些代码会故意把损失写成 ( \frac{1}{2n}\sum(y_i - \hat{y}_i)^2 )这样求导之后系数刚好是 ( \frac{1}{n} )干净利落。我在代码里为了保持梯度表达式的直观用的是标准的 ( \frac{1}{n} ) 定义梯度里会保留系数2不影响收敛结果只是学习率的表现形式不同而已。如果打算比较不同代码的损失曲线最好先确认损失函数定义是否一致否则两条曲线的绝对数值不在一个尺度上直接对比会得出错误的结论。3. 手写线性回归核心代码全拆解好前置判断做完了开写。整个代码分三部分数据准备、模型训练、结果验证。我会逐段拆开讲清楚每一行在干什么、为什么这么写。3.1 数据准备与模型初始化先造一份简单的数据集。这里用单特征数据一是方便可视化二是可以手动验证梯度计算是否正确。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) # 生成模拟数据y 4x 3 噪声 X np.random.rand(100, 1) * 10 true_w 4.0 true_b 3.0 y true_w * X true_b np.random.randn(100, 1) * 2这里的关键设计是我知道真实的 ( w4 )、( b3 )再用它加上噪声生成标签。这样训练完模型我就能拿学习到的参数和真实参数对比立刻知道代码对不对。这是验证手写代码最好的方式——用已知答案的题目来测试程序。接下来初始化参数w np.random.randn(1) * 0.1 # 故意从接近0的小值开始 b 0.0 learning_rate 0.01 epochs 1000为什么要从接近0的小随机值开始因为如果一开始就设置大的随机值梯度在初始点的数值可能很大容易造成梯度爆炸或者震荡导致损失值飞出范围。用接近0的小值初始预测接近0损失在可控范围内能观察到损失稳定下降的过程。3.2 训练循环与参数更新核心代码就是一段循环里面做三件事前向计算预测值、计算梯度、更新参数。n len(X) loss_history [] for epoch in range(epochs): # 1. 前向计算预测值 y_pred X.dot(w) b # 2. 计算损失 error y_pred - y loss np.mean(error ** 2) loss_history.append(loss) # 3. 计算梯度 dw 2 / n * np.sum(error * X) db 2 / n * np.sum(error) # 4. 更新参数 w - learning_rate * dw b - learning_rate * db if epoch % 100 0: print(fEpoch {epoch}, Loss: {loss:.4f})这段代码就是之前数学推导的直接翻译。梯度公式在笔记第2篇推过( \frac{\partial L}{\partial w} \frac{2}{n}\sum(error \cdot x) )( \frac{\partial L}{\partial b} \frac{2}{n}\sum(error) )。代码实现里对应了同样的矩阵运算。这里有个细节值得说为什么梯度更新用的是w - learning_rate * dw而不是w w - learning_rate * dw两者写法等价但-语义更清晰。参数更新的方向是梯度下降的方向即损失函数减小的方向。当前参数的梯度如果为正意味着增大参数会增大损失那就减小参数梯度为负则增大参数。训练完打印出学习到的参数print(f学习到的 w: {w[0]:.4f}, 真实 w: {true_w}) print(f学习到的 b: {b[0]:.4f}, 真实 b: {true_b})我实际跑了一遍在1000轮、学习率0.01的条件下得到的结果大概是w ≈ 4.01b ≈ 2.90和真实值已经非常接近。误差主要来自噪声——数据里加入的噪声导致模型无法完美还原真实参数这是正常现象。3.3 完整代码与输出观察把上面所有片段拼起来就是这份笔记的核心成品。我这里贴一份完整版方便直接复制运行import numpy as np # 固定随机种子保证结果可复现 np.random.seed(42) # 生成模拟数据 X np.random.rand(100, 1) * 10 true_w, true_b 4.0, 3.0 y true_w * X true_b np.random.randn(100, 1) * 2 # 初始化模型 w np.random.randn(1) * 0.1 b 0.0 learning_rate 0.01 epochs 1000 n len(X) loss_history [] for epoch in range(epochs): y_pred X.dot(w) b error y_pred - y loss np.mean(error ** 2) loss_history.append(loss) dw 2 / n * np.sum(error * X) db 2 / n * np.sum(error) w - learning_rate * dw b - learning_rate * db if epoch % 200 0: print(fEpoch {epoch}, Loss: {loss:.4f}, w: {w[0]:.4f}, b: {b[0]:.4f}) print(f最终参数: w {w[0]:.4f}, b {b[0]:.4f}) print(f真实参数: w {true_w}, b {true_b})运行这段代码你会看到损失值从几百逐渐下降到接近4左右这是数据本身噪声方差噪声的标准差为2方差为4所决定的下界。如果损失值能降到方差水平以下反而说明模型在过拟合噪声这同样值得警惕。运行完之后我建议你做一个必做实验把学习率改成0.1跑一次再改成0.001跑一次。0.1时损失大概率震荡不下降甚至可以飞到天文数字0.001时损失下降极慢1000轮根本不够。这个实验能帮你建立起对学习率的直觉学习率太大步子迈得过大直接蹦过最低点学习率太小走一步挪一毫米半天到不了。4. 用sklearn交叉验证手写结果手写代码跑出来的结果怎么确认不是自嗨正规做法是和成熟库的结果做交叉验证。sklearn的LinearRegression就是现成的参照物。4.1 对比代码与结果from sklearn.linear_model import LinearRegression model LinearRegression() model.fit(X, y) print(fsklearn 参数: w {model.coef_[0]:.4f}, b {model.intercept_:.4f})我实际跑的数据里sklearn的结果是w ≈ 4.00b ≈ 2.91和我们的手写结果在两位小数上完全一致。这种一致性说明手写代码的梯度计算和参数更新逻辑没有原则性错误。对比的时候注意一个细节sklearn默认用的是正规方程而不是梯度下降但它得到的最优解应该和梯度下降收敛后的结果一致——前提是损失函数是凸函数全局最小值是唯一的。线性回归的MSE损失正好就是凸函数两者的数学目标完全一致所以结果对得上。这也从侧面验证了一个重要事实梯度下降能不能用、用得好不好取决于损失函数的几何形态。换成非凸的神经网络损失随便初始化一个点梯度下降可能只收敛到局部最优多次运行结果会不一样。4.2 数值对不上的排查思路如果你跑完发现手写结果和sklearn差得比较远不要慌大概率是以下几个原因学习率过大导致参数在某一步跨得太远梯度爆炸最终损失居高不下。排查方式是打印每轮的损失值看看是不是存在先降后突然飙到极大的情况。如果是调小学习率重跑。迭代次数不够。学习率小且迭代次数有限参数还没走完就到了终点站。比如学习率0.001、1000轮可能只走了一小段路。把epochs拉到10000再观察。数据没做归一化。这个是后面会重点说的坑如果特征数值很大比如从100到10000梯度值也会相应很大同样的学习率可能直接让参数飞出去。梯度符号搞反。手写代码的时候w - learning_rate * dw写成了w ...这会让参数往损失增大的方向走损失只会越走越高。遇到这种情况损失曲线是一路向上的一眼就能看出来。5. 从能跑到跑得稳常见坑与排查代码能跑了只是第一步能稳定复现、换数据也能出合理结果才是真正掌握。这一节整理了我实践里遇到的高频问题每一条都真实踩过。5.1 学习率设置不合理学习率是手写线性回归里最敏感的超参数没有之一。我之前在另一份项目代码里用单特征数据学习率0.1还算正常换成多维特征后同学习率直接梯度爆炸——损失值从几十跳到了上亿整个控制台刷满了科学计数法。经验法则是从0.01起步看损失曲线如果平稳下降试着加大到0.05、0.1如果震荡回退一个量级。多维特征场景还要配合特征缩放后面细说。注意打印损失的时候别只打印最后一行要把前几十轮都打出来。我遇到过一种情况第一轮损失下降很快到第10轮开始震荡只看结果的话还以为正常。5.2 特征量纲差异大特征里的量级差异是新手最容易忽略的问题。设想预测房价特征一个是面积几十到几百一个是房龄1到50量级相差不大还好但如果特征一个是面积另一个是收入几万到几十万梯度公式里每个特征的梯度都正比于该特征的值收入这个特征的梯度会比面积大几千倍参数更新时收入对应的 ( w ) 会剧烈抖动模型的注意力全被大数值特征带走了。处理方式简单粗暴对每个特征做标准化减均值除以标准差。背后的数学意义是把所有特征拉到同一尺度让梯度下降可以匀速前进。sklearn的StandardScaler可以直接用手写也就两行X_mean np.mean(X, axis0) X_std np.std(X, axis0) X_scaled (X - X_mean) / X_std标准化之后学习率的设置范围也稳定多了0.01到0.1之间通常都能正常收敛。这份笔记的主代码因为用的是单特征且数值在0到10之间没有太大量级问题所以没有加这步。但一旦正式处理真实数据特征缩放几乎是必修课。5.3 数据顺序与随机打乱批量梯度下降本身对数据顺序不敏感因为它是拿全量数据算平均梯度。但如果你后面把批量梯度下降改成随机梯度下降SGD或小批量梯度下降Mini-batch GD数据顺序就会影响训练效果——可能在一个epoch里连续看到同一类样本模型被反复掰向同一个方向。我实践中的做法是每次迭代前用np.random.permutation把数据打乱再切batch。这样能让每个batch的数据分布相对均匀训练也稳定一些。idx np.random.permutation(n) X_shuffled X[idx] y_shuffled y[idx]如果是时间序列数据注意一件事不要随机打乱时间序列的样本顺序本身蕴含了时间依赖一旦打乱训练集信息会泄漏到验证阶段评估结果会虚高这属于数据泄漏的范畴。换句话说普通回归任务随便打乱时间序列任务严格按时间顺序切分。5.4 过拟合与小数据集陷阱线性回归也会过拟合只是表现不如树模型和神经网络那么戏剧化。当你特征很多、样本很少的时候模型可以完美拟合训练集里的每一个点但一到测试集就崩。判断指标是看训练集和测试集上的误差差异两个误差差不多说明模型泛化良好训练误差远小于测试误差十有八九是过拟合。真实的行业项目里训练和测试误差差异超过20%就该警惕了。应对手段包括增加样本数量、减少特征数量、加正则化。正则化会单独开一节说这里先留一个概念给损失函数加上参数权重的惩罚项让参数不要太大模型就不会为了拟合几个极端点而剧烈弯曲。6. 从线性回归往外走一步换个情景能干什么学会了最简线性回归接下来几乎所有回归模型都有了参照物。我用几句大白话把这些模型的定位讲清楚并按我自己的实践优先级给出推荐顺序。6.1 岭回归与正则化岭回归就是加了L2正则化的线性回归。在线性回归的损失函数后面加一项 ( \lambda\sum w_i^2 )惩罚值大的参数。这解决什么呢当特征之间存在多重共线性时 ( X^TX ) 接近奇异正规方程求逆会爆参数估计方差极大加了L2惩罚后矩阵变成了 ( X^TX \lambda I )求逆稳定了参数估计方差也小了。代码上岭回归和线性回归的区别极小把梯度下降更新式子里的权重梯度加上 ( 2\lambda w ) 就完事其他部分一模一样。我从线性回归手写代码改到岭回归大概只花了3分钟。这也是手写代码带来的红利——改底层逻辑时对整条链路心里有数。6.2 逻辑回归与分类逻辑回归名字里有回归其实是分类模型。它在线性回归的外面套了一层sigmoid函数把输出压缩到0到1之间当作属于类别1的概率来看。损失函数也从均方误差换成交叉熵原因是平方误差对逻辑回归的输出非凸容易陷入局部最优。如果理解了线性回归的梯度下降看逻辑回归的代码会发现结构和线性回归几乎相同差别只在预测值那里多了一个sigmoid变换以及梯度表达式里的误差计算方式不同。很多入门者卡在回归和分类到底什么关系上我的理解方式是逻辑回归是回归的思想分类的目标特征和权重的关系仍然是线性的只是输出被函数扭曲了一下用来适配概率语义。6.3 树模型与XGBoost、LightGBM再往外走一步就是完全不同思路的树模型。随机森林回归是装袋多棵决策树每棵树在不同样本子集上训练最后结果取平均XGBoost和LightGBM走的是提升路线每棵树拟合前面所有树的残差通俗讲就是接力补漏。有个观点值得记录不要一上来就无脑用XGBoost。树模型对特征缩放完全不敏感能自动捕捉非线性关系确实是很多比赛的不二之选但它的可解释性不如线性回归而且在小样本、强噪声场景下不如线性回归稳健。我的实践原则是先跑线性回归当基线看数据底线在哪里再上树模型对比有没有明显提升。如果提升不明显回到线性回归反而更容易向业务方解释。还有一种比较新的方向叫Koopman算子回归属于非线性动力系统的线性化方法用在时序预测上有不少探索。这个相对冷门但如果你在做量化交易或时序相关项目可以关注一下。最后的一点实际感受写完这第4篇笔记我最想分享的是代码从来不是机器学习的瓶颈理解才是。把线性回归手写一遍之后后面不管是用LSTM做序列预测还是用PatchCore做图像异常检测第一件事我都会下意识地看一下损失函数是什么梯度怎么算数据有没有做过标准化。这套底层直觉一旦建立换任何框架、任何模型都只是在换积木块而已。如果你刚开始学我建议接着做一件事把手写代码里的批量梯度下降改成小批量梯度下降batch size分别取16和64观察损失曲线的收敛速度和波动情况。这一步做完你对大厂框架里那些参数到底在控制什么就会有切身的体感。下一篇笔记我大概率会写这个主题到时候再继续聊。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑