深度学习视角下的线性回归:从手写梯度下降到PyTorch实现
发布时间:2026/10/12 9:43:37 锦皓数字建站

刚学深度学习那会儿我一度很纳闷为什么几乎所有入门教程都要从线性回归讲起这玩意儿不是统计学里一百多年前就有的老朋友吗跟“深度学习”四个字看上去完全不搭界。直到我把这层窗户纸捅破才意识到线性回归其实是整个深度学习的第一块骨架一个线性回归模型放到神经网络语境下就是一个没有激活函数的单神经元网络它的训练方式和你以后训练任何深度网络所用的梯度下降、反向传播完全是同一套逻辑。这篇博文想换个角度用深度学习的视角把简单线性回归重新拆解一遍从数学本质讲到手写代码再对比PyTorch实现最后聊几个训练时容易被忽略的实战细节。适合刚接触机器学习或者深度学习、又或者学过一点理论但还没真正动手训练过模型的同学。1. 为什么“简单线性回归”会被归到深度学习话题下1.1 两种视角下的身份切换传统统计学家拿到一堆数据会问这两个变量之间是否真的有线性关系斜率是多少这个关系显著吗他们关心的是参数估计、假设检验、置信区间这些东西。深度学习开发者拿到同一堆数据问的却是另一件事我能不能让一个可微的模型通过反复迭代自动学会从输入到输出的映射这里没有显著性检验只有训练误差、验证误差和泛化能力。简单线性回归恰好是这两套话语体系的一个交叉点。在统计里它是最基础的回归工具在深度学习里它是“一个不带激活函数的单神经元全连接网络”。同一个数学式子 y wx b在不同语境下承载的意义完全不同。很多深度学习教学材料把线性回归放在第一章不是因为它在统计学里资历最老而是因为它是理解“训练过程”的最小单元。所谓训练本质上就是反复做四件事前向传播算出预测值、和真实值比较算出损失、通过梯度回传算出参数应该往哪个方向调、然后迈出一小步更新参数。线性回归把这四个步骤压缩到了最简单的情形一眼就能看穿每一步在做什么不会被复杂的网络结构、卷积核、注意力机制之类的东西干扰。这是它身上最珍贵的学习价值。1.2 一张表格看清两种身份维度统计学视角深度学习视角核心目标解释变量间的线性关系学习输入到输出的映射主要工具最小二乘、显著性检验梯度下降、反向传播参数的含义有明确解释意义的系数网络权重偏向功能化理解评价标准置信区间、p值测试集误差、泛化能力扩展方向多元回归、广义线性模型多层网络、非线性激活函数不要小看这张表的对比。它提醒你一件容易被忽略的事同一个公式在不同领域里“为什么做”和“做什么”是完全不同的。如果你一直用统计学的直觉去理解线性回归总想着系数是不是显著、置信区间有多宽那么在走向深度学习的时候可能连“模型是怎么被训练出来的”这个基本问题都问不出来。反过来一旦你开始用“输入、参数、损失、梯度”这套语言去重述线性回归就等于拿到了通读绝大多数深度学习教程的底层词汇。2. 线性回归的数学本质模型、损失与优化2.1 y wx b 到底在做什么模型做的事很朴素给定一个输入 x预测输出 y。w 是权重也叫斜率决定 x 变化一个单位时 y 跟着变化多少b 是偏置也叫截距决定直线在坐标系里的上下位置。模型训练的目标就是根据已有的数据样本反推最合理的 w 和 b。用生活化的类比来解释如果说 x 是大米的公斤数y 是蒸出来的米饭分量w 就是“每公斤米出多少饭”的转换比例b 则是蒸锅本身的固定损耗。模型要做的就是根据以前“买过多少米、蒸出多少饭”的经验反推出这个转换比例和固定损耗。这里有一个很多人没意识到的点在深度学习框架里全连接层的数学本质就是 y wx b 这个式子只是输入从标量扩展成了向量甚至矩阵。所以你理解了线性回归就等于理解了神经网络里最常见的一种层结构。2.2 损失函数为什么偏偏选MSE有了模型还需要一把尺子衡量“预测得有多差”这把尺子就叫损失函数。线性回归最常用的损失是均方误差MSEloss 1/N * Σ (y_pred - y)^2选择 MSE 有两个关键原因。第一它处处可导。基于梯度下降的训练算法需要计算损失对参数的导数如果损失函数在某个点不可导优化过程就会很麻烦。MAE平均绝对误差虽然对异常值更鲁棒但在零点不可导对梯度优化并不友好。第二它把大误差放大得更明显。由于平方的存在偏差为2的样本贡献的损失是偏差为1的样本的4倍。这会逼迫优化过程优先修正那些“错得离谱”的样本。代价是它对极端异常值比较敏感不过在入门阶段这不是主要矛盾。2.3 从最小二乘到梯度下降优化视角的切换线性回归其实存在解析解。用最小二乘法可以直接通过矩阵运算求出最优参数w (XᵀX)⁻¹Xᵀy。也就是说对于这个简单的模型理论上不需要“训练”一步就能算出答案。那为什么深度学习场景下还要用梯度下降反复迭代因为一旦模型复杂一点点——加个隐藏层、换个非线性激活函数——解析解就不存在了你只能依赖梯度下降这种迭代式优化方法。所以深度学习教学里会有意让线性回归也通过梯度下降来“训练”目的是提前熟悉这套通用机制。梯度下降的思路很直白当前参数朝哪个方向调整损失函数下降最快答案是负梯度方向。于是每次迭代都往负梯度方向迈一小步慢慢逼近损失最低点。这个过程就像大雾天在山里下山你看不清整座山的全貌但踩着脚下最陡的地方往下走大概率能走到谷底。3. 从零手写梯度下降不用框架也能动起来3.1 准备模拟数据给自己造一份标准答案在动手写模型之前先造一份“已知答案”的模拟数据。为什么非得这么做因为只有知道真实参数才能验证训练出来的模型到底对不对。如果真实 w2.0、b3.0最后训练出来 w≈2.0、b≈3.0说明整套流程是通的。生成数据时记得加一点随机噪声。没有噪声的数据是完美直线模型拟合起来毫无难度也失去了实战意义。加了噪声之后我们只能期待模型逼近真实的 w 和 b而不可能完全精确——这才是真实世界的样子。import numpy as np np.random.seed(42) x np.random.rand(100, 1) * 10 w_true 2.0 b_true 3.0 y w_true * x b_true np.random.randn(100, 1)这段代码生成100个在0到10之间均匀分布的 x对应的 y 由 y 2x 3 加上标准正态噪声构成。这样一份数据里既有明确的“标准答案”又有足够的不确定性来模拟真实场景。3.2 梯度公式手把手推导一遍MSE损失对 w 和 b 的梯度其实可以亲手推出来。设 y_pred w*x b损失为loss 1/N * Σ (w*x_i b - y_i)²对 w 求偏导链式法则会把平方项放下来∂loss/∂w 2/N * Σ (w*x_i b - y_i) * x_i对 b 求偏导多出来的 x_i 就没有了∂loss/∂b 2/N * Σ (w*x_i b - y_i)这里有一个细节很多人不会讲为什么要除以 N为了让梯度的量级不随样本数量变化。如果样本从100变成10000梯度求和之后会大100倍学习率就得从头再调。除以 N 之后梯度的尺度就和“平均每个样本”对齐了后续调参的体验会稳定很多。这个细节看似不起眼实际训练中影响很大。3.3 完整代码与运行结果解读w 0.0 b 0.0 learning_rate 0.001 epochs 1000 for epoch in range(epochs): y_pred w * x b loss np.mean((y_pred - y) ** 2) dw 2 * np.mean((y_pred - y) * x) db 2 * np.mean(y_pred - y) w - learning_rate * dw b - learning_rate * db if epoch % 100 0: print(fepoch {epoch:4d}, loss {loss:.6f}, w {w:.4f}, b {b:.4f}) print(ffinal: w {w:.4f}, b {b:.4f})运行之后预期你会看到 loss 从一百多逐步下降最终收敛到接近噪声方差大约1左右w 和 b 逐渐逼近 2.0 和 3.0。需要强调的是如果你只盯着最终的 w 和 b会觉得“这不就是求了个斜率和截距嘛”但如果你盯着每个 epoch 的 loss 变化那种“模型在一点一点变聪明”的过程才是深度学习训练最核心的体验。模型没有开挂它只是靠反复迈出小步自己走到了最优解附近。4. 用深度学习框架重新表达从手写公式到PyTorch4.1 框架替你省掉的三类繁琐工作手写代码能让你理解原理但真实工程里没人会手动推导梯度。PyTorch 这类框架主要替我们承担了三件事。第一参数管理。nn.Linear(1, 1) 会自动创建并初始化一个权重和一个偏置你不用手动定义 w 和 b也不用担心忘记加偏置项。第二自动微分。这是最核心的能力。你只需要定义模型的前向计算调用一次 loss.backward()框架就会自动把梯度算好并填进每个参数的 .grad 属性里。前面手写的 dw 和 db 公式到这里完全不需要自己写了。第三优化器封装。torch.optim.SGD 把参数更新这一步标准化了传入 model.parameters() 和学习率每次调用 optimizer.step() 就完成一次更新。要特别强调一点这些封装并没有改变训练的本质。你仍然在重复前向传播、算损失、反传梯度、更新参数这四个动作只是繁琐的细节被框架接管了。很多人用 PyTorch 用了一年对训练的理解还停留在“照着模板抄”就是因为跳过了上一章的手写推导直接钻进框架的封装里。4.2 手写代码和PyTorch实现逐行对照手写代码PyTorch 对应w 0.0; b 0.0model nn.Linear(1, 1)手算 dw, dbloss.backward() 自动计算w - lr * dw; b - lr * dboptimizer.step()np.mean((y_pred - y)**2)nn.MSELoss()这样对着看你会更清楚框架存在的意义它没有发明新的训练逻辑只是把最繁琐的梯度推导和参数管理工程化、自动化了。理解这一点之后以后不管遇到多花哨的模型你都能一眼看穿它的训练闭环——无非还是那四步只是网络结构变得复杂了。4.3 PyTorch 完整代码与“零梯度”提醒import torch import torch.nn as nn torch.manual_seed(42) x torch.rand(100, 1) * 10 y 2.0 * x 3.0 torch.randn(100, 1) model nn.Linear(1, 1) criterion nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.001) for epoch in range(1000): y_pred model(x) loss criterion(y_pred, y) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 100 0: print(fepoch {epoch:4d}, loss {loss.item():.4f}) print(ffinal: w {model.weight.item():.4f}, b {model.bias.item():.4f})这里必须说一个新手最容易踩的坑optimizer.zero_grad() 一定不能省。PyTorch 的梯度是累加模式如果上一步的梯度不清零下一次 backward 时新旧梯度会叠加在一起参数更新方向就会完全错乱loss 的表现也会非常诡异。有时候 loss 不降反升有时候掉到某个位置就卡住了。忘了这行代码的后果往往让人摸不着头脑排查半天才发现只是少清了一次梯度。从第一天起就把“backward 之前先 zero_grad”写进肌肉记忆能省下大量调试时间。5. 训练中那些容易被忽视的实战细节5.1 学习率调参的本质是掌控步长前面代码里我把学习率设成 0.001这是有讲究的。你可以试着把它改成 0.1 再跑一次loss 会从一开始就剧烈震荡甚至直接变成 NaN。原因在于x 的量级是 0 到 10初始预测值和真实值的误差大约在十几左右梯度 dw 的量级可能到上百。学习率 0.1 意味着参数一次更新十几甚至几十参数直接就被“甩飞”了。学习率过小则是另一种体会loss 下降得像蜗牛爬跑几千轮可能还没到理想位置。所以调学习率本质上是调“参数更新的步长”。常见做法是从 0.001 或者 0.01 起步然后观察 loss 曲线发生爆炸或震荡就把学习率调小下降太慢就适当调大。一个很实用的经验是如果某次训练 loss 突然变成 NaN十有八九是学习率开大了。5.2 特征缩放为什么梯度下降对数据尺度这么敏感上面的数据 x 在 0 到 10 之间算是比较温和的量级。可如果 x 是房价动辄几百万的量级梯度计算里乘上 x 之后就会变得特别巨大同一个学习率根本无法正常工作。更麻烦的是如果模型有多个输入特征一个特征量级是0.01另一个是10000它们对损失的贡献会被量级大的特征主导训练效率会非常糟糕。解决方案是对特征做标准化常用的 z-score 方法减去均值后除以标准差让所有特征都以0为中心、标准差为1。几何上的直觉更直白在没有标准化的损失等高线图上等高线是一圈圈拉长的椭圆梯度方向并不能直接指向圆心训练路径会左右摇摆标准化之后等高线接近正圆梯度方向更直接指向中心收敛路径又短又直。这也是为什么工业级项目里几乎都会对原始特征做预处理而不是直接丢给模型。5.3 训练集/验证集/测试集别用同一批数据自我感动模型训练完之后如果还用训练数据来评估效果属于典型的“开卷考试”。模型完全可能在训练集上表现不错换一批新数据就拉胯。简单线性回归因为模型太简单过拟合问题不明显但这个习惯必须在入门阶段就建立起来。标准做法是先把数据划分成训练集和测试集比如80%训练、20%测试。如果后面还要调超参数比如学习率、网络层数通常还会在训练集内部再切出一部分验证集。评估模型时只看它在测试集上的表现。这样你才能知道模型是真正学到了规律还是只是把训练样本背下来了。这一步看着简单却是通向深度学习“泛化能力”这个概念的第一级台阶。5.4 评估指标MSE之外还可以看什么除了 MSE还有一个常用指标叫 R²决定系数定义是R² 1 - SS_res / SS_tot其中 SS_res 是残差平方和SS_tot 是 y 相对于自身均值的总平方和。R² 越接近1说明模型解释了因变量越多的方差如果等于0说明模型和直接拿均值预测差不多如果是负数说明模型比均值预测还差基本可以认为训练出了bug。另外一个容易被忽略的检查方式是画残差图。把每个样本的真实值减去预测值得到残差然后画出来看看如果残差在0附近随机分布说明线性假设基本合理如果残差呈现明显的曲线或者漏斗形状说明数据背后可能还有非线性成分简单线性回归不够用了。这个技巧比看单个数字更早暴露问题建议养成习惯。6. 从线性回归走向深度学习的下一站6.1 从单个神经元到多层网络线性回归本质上就是一个神经元输入 x加权求和输出结果。如果把多个这样的“神经元”拼在一起——输入先送到第一层多个神经元第一层的输出再送到下一层——你就得到了一个多层神经网络。这也是为什么很多深度学习框架会把全连接层直接叫做 Linear 层因为数学上它们很相似。但这里有一个关键问题如果不加任何非线性变换无论堆多少层整个网络在数学上仍然等价于一个线性变换。想想看第一层输出是 W₁x b₁再经过第二层变成 W₂(W₁xb₁)b₂展开之后仍然可以写成某个 Wx b 的形式。层数再多也只是线性组合的线性组合。6.2 引入非线性激活函数真正的意义要让网络具备拟合复杂函数的能力必须在层与层之间插入非线性激活函数比如 ReLU(x) max(0, x)或者 sigmoid。激活函数的加入打破了“多层线性等价单层线性”的困局。用生活化的说法线性回归只能画直线而多层非线性网络可以用海量的“小线段”去逼近任意曲线就像用无数微小的直线段拼出一条弯曲的路径。这一小步就是“简单线性回归”和“深度学习”之间的那扇门。门的钥匙你已经握在手里了——理解训练闭环、理解梯度下降、理解损失函数。剩下的无非是层数、激活函数和数据规模的变化。6.3 从回归到分类逻辑回归这块跳板线性回归输出的是连续值但很多实际问题要的却是分类结果。最简单的分类模型之一叫逻辑回归做法就是在线性回归的输出后面接一个 sigmoid 函数把任意实数压缩到0到1之间作为“属于正类的概率”。损失函数也相应地从MSE换成交叉熵。逻辑回归名字里带“回归”但它其实是一个分类模型。它很好地展示了深度学习里一种常见的设计模式先用线性层做特征变换再用非线性函数做输出映射最后配一个合适的损失函数。这个模式在后面的卷积网络、循环网络里会以不同形式反复出现。到这里简单线性回归的来龙去脉基本上算是讲透了。最后再分享一点个人体会我见过太多人一上来就抱着注意力机制、大模型猛啃结果连“参数是怎么被一点一点调出来的”都没概念。花一个晚上把这条只有一百多行的线性回归跑熟你会获得一个特别扎实的底层直觉。往后再复杂的模型溯洄到训练的核心还是那四步前向传播、算损失、求梯度、更新参数。这个闭环值得你亲手跑通一次。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。