尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

从线性回归到深度学习:手写梯度下降与PyTorch实战

从线性回归到深度学习:手写梯度下降与PyTorch实战 线性回归大概是深度学习这条路上最容易被低估的知识点。很多人一上手就想啃卷积神经网络、Transformer这类重型模型觉得线性回归“太简单”翻两眼就跳过去。但以我带项目、带新人的经验来说凡是能把简单线性回归亲手写明白的人后面学深度学习几乎不会卡壳反过来连梯度下降都没手推过的人往往会在某个深夜对着一条不下降的损失曲线怀疑人生。这篇文章写给两类读者一是刚入门深度学习、想找一块扎实垫脚石的同学二是已经在用框架调参、但对底层原理心里没底、想回头补课的同行。我会把线性回归要解决的问题、模型与损失函数、梯度下降的原理讲透再用NumPy和PyTorch各实现一遍最后把常见坑逐个列出来。1. 线性回归为什么是深度学习的起点1.1 从一个最简单的预测问题说起假设你要估算一套房子的价格手里只有一个信息——建筑面积。你下意识会想面积越大价格越高大概是一个“每平方米单价”乘以面积、再加一个基础成本的关系。这本质上就是线性回归用一条直线去拟合数据公式写成 ŷ wx b其中 x 是面积ŷ 是预测价格w 是每平米单价b 是基础成本。我们不知道真实的 w 和 b 是多少只能靠一批“面积-价格”的历史样本把它们反推出来。这个过程在数学上叫“参数估计”在深度学习里叫“训练”。这种问题到处都有根据学习时长预测考试成绩、根据广告投放预测销售额、根据气温数据预测用电量……它们的共同点是输入是一个连续数值输出也是一个连续数值我们想找到一个从输入到输出的映射关系。线性回归就是最朴素、也最可靠的那条映射假设——先假定关系是线性的再让数据来修正这个假设。很多教程用“房价预测”讲线性回归我也沿用这个例子但会用一个人工构造的带噪声数据集方便验证模型学到的参数是否接近“真值”。这是入门阶段非常推荐的做法先知道标准答案再看模型能不能逼近它这样对“模型到底学没学会”会有非常直观的体感。数据是自己生成的参数是可控的训练完对比一下就明白整个流程在干什么不用等到最后做真实项目时才去面对模棱两可的评估。1.2 线性回归和深度学习到底什么关系必须先泼一盆冷水线性回归本身不是“深度学习”它没有隐藏层、没有非线性激活函数甚至严格说它属于统计学习而不是神经网络。那为什么所有深度学习入门路线都把它放在第一课因为深度学习的训练循环和线性回归完全同构。随便拿一个神经网络来看每一层的计算都是 z Wx b这跟线性回归的 ŷ wx b 长得一模一样层与层之间靠激活函数引入非线性最终输出经过损失函数计算误差再用梯度下降更新所有参数。也就是说神经网络就是“多次线性变换 非线性激活”的堆叠而线性回归就是剥掉所有花哨外壳之后剩下的那个骨架定义模型、计算损失、求梯度、更新参数。把这个四步循环刻进脑子后面不管是全连接网络、卷积网络还是Transformer你都会发现它们只是在换模型结构、换损失函数、换优化器训练主流程从来没变过。另外从实现层面讲线性回归是检验你对“数据处理、梯度计算、参数更新、结果评估”这一整套工程流程是否熟练的最好试金石。它代码量小、公式简单、出错容易定位非常适合用来建立完整的实验习惯。等你以后跑复杂模型代码动辄上百行、训练动辄几小时再想回头排查基础问题就难得多。所以这个“简单”的模型恰恰是投入产出比最高的一块敲门砖。2. 原理拆解模型、损失函数、梯度下降2.1 模型表达式与参数的含义简单线性回归的模型就是一条直线ŷ wx b。这里有两个可学习的参数w 叫权重weightb 叫偏置bias。在这个公式里w 决定了“x 每增加一个单位预测值跟着增加多少”b 则负责兜底保证 x 0 时也有一个合理的基准输出。还是用房子举例如果数据告诉你每平米 2 万元、基础杂费 1 万元那么真实模型就是 ŷ 2x 1。但现实中的问题是我们不知道这个 2 和 1手里只有一堆散点。线性回归的目标就是从散点里把这两个数“猜”出来而且猜得越准越好。这里有个关键认知模型学习到的不是数据本身而是数据背后的规律也就是那组参数。数据量多了以后习惯用矩阵一次处理所有样本。把 n 个样本的输入排成一个向量 x预测值就是 ŷ wx b。这种向量化的写法看起来很装但它是深度学习标配的思维方式以后你遇到的每个模型都是在张量多维数组上做批量计算而不是用一个 for 循环处理单个样本。这一点尽早适应后面读框架源码会轻松很多。实际用 PyTorch 这类框架时数据天然就是张量批量计算是默认操作理解向量化就等于提前理解了框架的底层逻辑。2.2 损失函数如何量化“猜得准不准”有了模型下一步要有个标尺来衡量预测值跟真实值差多少这个标尺就是损失函数。线性回归最常用的是均方误差MSEMean Squared ErrorL (1/n) Σ(y_i - ŷ_i)²其中 n 是样本数y_i 是第 i 个样本的真实值ŷ_i 是预测值。为什么用平方而不是直接算误差的平均因为正负误差会互相抵消平方之后所有误差都变成正数能真实反映整体偏离程度。而且平方意味着大误差会被放大更多——这个性质让模型更愿意去修正那些偏差特别大的点符合直觉。均方误差还有一个很讨喜的数学性质它是关于 w 和 b 的凸函数。凸函数意味着只有一个全局最低点梯度下降不管从哪出发最终都能稳稳走到最优解附近。这在深度学习里是很大的恩赐——等以后你接触神经网络损失函数变成非凸的训练过程会多出很多“局部最优、鞍点”之类的烦恼。但是在线性回归这里你可以放心地观察梯度下降在各种学习率下的表现因为结果一定是收敛的只是快慢好坏的问题。偶尔也会有人用平均绝对误差MAE它更抗异常值但在接近最优解时梯度不连续优化起来没 MSE 平滑。入门阶段建议先把 MSE 吃透其他损失函数都是在这个思路上做变形——有的是为了更鲁棒有的是为了配合概率解释底层逻辑始终是“给误差定一个可优化的度量”。2.3 梯度下降参数自我修正的引擎损失函数告诉我们“现在猜得有多差”但还没告诉我们“怎么改参数”。梯度下降就是回答这个问题的。先回忆一下导数的直觉导数描述的是函数在某一点的变化方向。如果 L 对 w 的偏导是正的说明 w 增大时损失会增大那我们就应该减小 w反之亦然。所以最朴素的更新规则就是朝导数相反方向走一步w ← w - lr × ∂L/∂w b ← b - lr × ∂L/∂b这里 lr 是学习率也就是每次迈的步子大小。把损失函数当一座山梯度就是当前脚下最陡的上坡方向我们偏偏要往下坡走每次走 lr 那么远。步子太大可能一脚跨过最低点甚至滚下山步子太小走到天黑都到不了山脚。对 MSE 求偏导可以得到非常简洁的两个公式∂L/∂w (2/n) Σ(ŷ_i - y_i) x_i ∂L/∂b (2/n) Σ(ŷ_i - y_i)注意两个公式里都出现了同一个量预测误差 e_i ŷ_i - y_i。w 的梯度等于“误差乘以对应的输入”的平均b 的梯度等于“误差”的平均。这个现象是有深刻含义的误差通过乘 x_i 这个操作“分配”到了 w 上而 b 接受的是误差的平均水平。这就是所谓反向传播的最简形态——虽然只有一层不够格叫“深度”但信息流动的方式已经一模一样了。我见过不少人直接跳过这个推导去看反向传播的链式法则结果越看越晕其实就是因为在最简单的例子上没建立“误差如何流回参数”的直觉。3. 用NumPy从零手写线性回归3.1 构造带噪声的训练数据在动手之前最好先想清楚线性回归缺了什么真实世界的数据几乎永远带噪声——房价不可能严格按照面积线性变化楼层、朝向、装修、砍价都会引入随机波动。所以我会先生成一组“理想直线 高斯噪声”的数据让模型在接近真实的环境里做实验import numpy as np true_w 2.0 true_b 1.0 n_samples 100 x np.random.uniform(0.0, 10.0, sizen_samples) noise np.random.normal(0.0, 0.5, sizen_samples) y true_w * x true_b noise这里 true_w 和 true_b 就是我们想找回的“标准答案”noise 是均值为 0、标准差为 0.5 的高斯噪声。之所以把标准答案藏起来是为了训练结束后可以直接对比模型学到的 (w, b) 和真实值立刻知道学得对不对。生成数据时有几个细节值得注意x 用均匀分布铺满 0 到 10保证整个区间都有样本不会出现“数据全挤在左边”导致右边完全靠外推的情况噪声用固定标准差控制数据整体难度。如果想复现实验可以在生成前加一句 np.random.seed(0)否则每次跑数据都不一样调参时容易被随机性误导。3.2 前向传播、反向计算与参数更新接下来分三步写核心函数前向传播用当前参数预测、损失计算量化误差、梯度计算指导参数怎么改。def predict(x, w, b): return w * x b def mse_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) def compute_gradient(x, y_true, y_pred): error y_pred - y_true dw 2.0 * np.mean(error * x) db 2.0 * np.mean(error) return dw, dbpredict 就是模型本身mse_loss 对应前面讲的均方误差重点是 compute_gradient。代码里 error y_pred - y_true 是预测误差把它乘以 x 再取平均就得到对 w 的梯度直接取平均就得到对 b 的梯度。这两行和我们手推的公式一一对应没有任何黑魔法。如果你第一次写这段代码我强烈建议你不要直接抄而是先自己在纸上推一遍 ∂L/∂w 的公式再对照代码看每一行对应公式里的哪一项。这个“公式到代码”的翻译过程就是你理解反向传播的最佳训练场。很多人学深度学习卡在“背公式”和“写代码”之间的断层上其实问题就出在这个翻译环节练得太少。3.3 训练循环与结果验证有了这三个函数训练循环短得惊人w, b 0.0, 0.0 lr 0.01 epochs 200 for epoch in range(epochs): y_pred predict(x, w, b) loss mse_loss(y, y_pred) dw, db compute_gradient(x, y, y_pred) w - lr * dw b - lr * db if epoch % 20 0: print(fepoch {epoch:3d}, loss {loss:.4f}, w {w:.4f}, b {b:.4f})初始化把 w 和 b 都设为 0然后循环里四件事预测、算损失、算梯度、更新参数。这就是前面说的“训练主循环”以后所有模型的训练代码都是这个套路。跑完 200 轮输出大概是这样的走势epoch 0, loss 18.2531, w 0.2070, b 0.0940 epoch 20, loss 4.2150, w 1.5120, b 0.6840 epoch 40, loss 0.9320, w 1.8910, b 0.9040 ... epoch 200, loss 0.2320, w 1.9832, b 0.9927可以看到损失从 18 一路降到 0.23w 从 0 爬到 1.98b 爬到 0.99离真实值 2.0 和 1.0 已经很接近。剩下的偏差主要来自噪声——这是正常的因为你不可能在带噪声的数据上把参数学成精确值学到“在统计意义上最接近”就够了。验证阶段建议再跑两件事。一是用训练好的参数对整批数据重新预测画一张散点图和拟合直线肉眼看直线是否穿过数据云的中间二是算一下决定系数 R²它表示模型解释了数据中多少比例的方差通常 0.9 以上说明拟合效果已经很好了。R² 的计算很简单1 减去“预测残差平方和”除以“数据总平方和”。这两个小动作看着不起眼但能帮你建立“训练完必须验证”的肌肉记忆往后做任何模型都会习惯性地先画图、再算指标。4. 用PyTorch实现线性回归向现代框架平滑过渡4.1 为什么建议尽早接触框架手写 NumPy 版本的意义在于理解原理但真实项目里没人会手推梯度。深度学习的模型动辄成百上千层每层都有几十上百个参数如果梯度全靠手推算到第三层就该崩溃了。PyTorch 这类框架通过自动微分autograd机制帮你自动计算所有梯度你只需要定义前向计算反向传播由框架完成。而且框架的优势不只是省事它天然支持 GPU 加速、批量采样、丰富的优化器、现成的网络层与损失函数生态里有大量预训练模型可以直接复用。对入门者来说越早熟悉 PyTorch 的“模型定义—损失—优化器—训练循环”这套范式后面看论文复现代码就越轻松。代码风格也是行业通用的你写出的训练代码可以和开源社区的例子无缝衔接。我见过不少同学陷入一个误区觉得“先把 NumPy 版本吃透再学框架”才是正统顺序结果在纯手写阶段耗了太久。我的建议是两条腿走路——手写一遍理解原理框架跟一遍熟悉生态两个都别落下。手写告诉你“梯度是什么”框架告诉你“工程上怎么高效地算它”两者不冲突。顺序上我甚至建议框架版和手写版同一天各跑一遍趁热打铁把两者对应起来效果比隔一周再学要强得多。4.2 核心代码逐行解读直接用前面生成的 (x, y) 数据转换成 PyTorch 张量就可以开写import torch import torch.nn as nn import torch.optim as optim x_t torch.tensor(x, dtypetorch.float32).reshape(-1, 1) y_t torch.tensor(y, dtypetorch.float32).reshape(-1, 1) model nn.Linear(1, 1) loss_fn nn.MSELoss() optimizer optim.SGD(model.parameters(), lr0.01) for epoch in range(200): optimizer.zero_grad() y_pred model(x_t) loss loss_fn(y_pred, y_t) loss.backward() optimizer.step() if epoch % 20 0: print(fepoch {epoch:3d}, loss {loss.item():.4f})逐行拆解。nn.Linear(1, 1) 定义了一个单输入单输出的线性层内部自动初始化了权重和偏置功能等同于 ŷ wx b。loss_fn 用的是均方误差和手写版完全同一个公式。optimizer 选了 SGD也就是最朴素的随机梯度下降学习率 0.01 和手写版保持一致。训练循环里有四个关键调用顺序一步都不能错。optimizer.zero_grad() 把上一轮积累的梯度清零——这一步最容易漏漏了梯度就会跨轮次累加导致损失曲线莫名其妙地抖动甚至爆掉。loss.backward() 触发反向传播框架自动把每个参数的梯度算好并存进参数张量的 grad 字段。optimizer.step() 拿着这些梯度去更新参数。这三步配合一轮循环正好对应手写版的“算梯度、更新参数”。训练结束后模型参数可以通过 model.weight 和 model.bias 取出来print(model.weight.item(), model.bias.item())你会看到两个数同样逼近 2.0 和 1.0。到这里你已经用业界标准工具完成了一次完整的深度学习训练流程。这种“熟悉感”很重要以后你跑任何 PyTorch 项目看到的都是这一套动作只是模型更复杂、数据更多、循环更长核心逻辑一字未变。4.3 手写版与框架版对比两种写法跑同一份数据结果几乎一样但代码组织方式差别很大。列个表看得更清楚对比项NumPy手写版PyTorch框架版模型定义自己写 predict 函数nn.Linear 一行搞定梯度计算手动推公式并编码自动微分自动完成参数更新自己写 w - lr * dwoptimizer.step()损失函数自己写均值平方nn.MSELoss() 现成扩展隐藏层每加一层都要重推公式继续堆 nn.Linear 即可这张表最值得回味的是最后一行。手写版想加一个隐藏层变成两层的 MLP你得重新推导损失对第一层参数的梯度链式法则推得头发掉一把框架版只需要在模型定义里多加一行 nn.Linear再塞一个 nn.ReLU()其他代码一行都不用改。这就是为什么复杂模型几乎必须依赖框架——你省下的不是“写代码的时间”而是“推导数学公式的时间”。当然框架也不是万能的它屏蔽了梯度细节初学者很容易“只会调包、不懂原理”遇到梯度爆炸、梯度消失这类问题只能干瞪眼。所以我的观点始终是先手写、再框架两个版本都亲手跑通才算真正掌握了线性回归。等哪一天你发现手写版和框架版跑出来结果对不上还能自己定位出是哪里出了问题那这一课你就彻底毕业了。5. 常见问题排查与避坑经验5.1 学习率不是越大越快新手最爱犯的错就是把学习率调得很大觉得“步子大才能跑得快”。在我们的例子中把 lr 改成 1.0训练几轮损失就会变成 nan——因为参数一步跨过头梯度越来越大形成正反馈爆炸。反过来lr 调到 1e-6训练 200 轮后参数还在原点附近蠕动损失几乎不动。实践里判断学习率是否合适最直接的办法就是打印损失曲线如果损失一路飙升或出现 inf/nan说明学习率太大如果损失下降得像蜗牛爬说明学习率太小如果损失先降后稳且没有剧烈抖动这个数量级基本就能用。调参时可以按 10 倍为单位试探0.1、0.01、0.001找到“开始发散”和“明显收敛”之间的那个量级再细调。这是个祖传的调参方法叫“对数坐标扫描”省时省力。另外现在很多 AI 编程助手也能帮你看训练日志、报错信息但前提是你自己看得懂损失曲线——工具只是帮你翻译判断还得靠你。5.2 特征归一化别让量纲毁了训练前面数据里 x 的取值范围是 0 到 10一切顺利。但如果 x 换成“房价元/m²”这种动辄几万的量纲麻烦就来了。看 w 的梯度公式∂L/∂w (2/n)Σ(error × x)x 越大梯度越大稍微更新一下参数就可能冲出天际。为了抵消这种效应你只能把学习率压到极小训练慢得让人抓狂。解决办法是特征归一化把 x 变换到均值为 0、标准差为 1 的分布(x - mean) / std。归一化之后梯度的大小不再被量纲绑架不同特征对梯度的影响也基本均衡。这条经验在简单线性回归里只是“加快收敛”到了深度学习里就是“能不能收敛”的问题——像图片像素值 0 到 255、词向量上百维这种场景不归一化或不做标准化训练基本跑不动。我见过一个同学在房价数据上怎么调学习率都不收敛归一化之后三分钟解决他后来把这件事写进了自己的实验笔记里。这种坑踩一次就长记性了。5.3 数据划分与随机性别让实验“看答案”另一个常见坑是数据划分。如果只在一整批数据上训练和评估模型其实是在“看答案做题”。正确的做法是从一开始就把数据随机打乱划分出训练集和测试集训练只用训练集最后才用测试集评估。这一步在导师带实验时往往是检查重点测试集只能碰一次碰多了你就在“调”测试集了。很多同学测试集反复用、指标越调越好看最后上线一塌糊涂就是吃了这个亏。随机性也需要管理。训练前设好随机种子np.random.seed 或 torch.manual_seed保证每次实验可复现。否则同一个人、同一段代码、两次跑出来的结果不一样排查问题时你就分不清是代码改了还是运气改了。深度学习项目里有个不成文的规矩哪怕只是临时实验也要把随机种子固定住不然推导结论时缺乏说服力。5.4 训练异常自查清单最后把我踩过的坑整理成一张速查清单遇到问题可以逐条对照损失变 nan优先调小学习率其次检查是否除零、是否数据里有无穷值。损失不降检查梯度是否为 0参数没在更新、特征是否归一化、数据是否打乱。损失下降但参数离真值很远检查噪声是不是太大、样本是不是太少或者数据分布有偏。训练集损失低、测试集损失高就是过拟合对线性回归来说通常是特征太少或数据划分不合理需要回头检查数据流程。手工梯度算出来的和框架算的对不上先检查手写公式的符号再检查是否漏了 2/n 这个系数。Debug 方式是打印中间量的 shape错误往往出在广播上。提示入门阶段遇到损失曲线怪异别急着调参先把“数据长什么样”画出来看一遍。很多问题不是模型不行而是数据本身就长歪了。最后说点个人体会。我每次带新人走深度学习入门都会让他们把线性回归手写三遍第一遍对着公式抄第二遍合上书自己推出梯度公式第三遍用 PyTorch 改写。不是故意折腾人而是这三遍走完训练循环——算损失、求梯度、更新参数——会变成肌肉记忆。后面无论学 CNN、RNN 还是 Transformer你都会发现核心还是这个骨架。把最简单的模型吃透永远是性价比最高的投资。
返回列表