
带过深度学习新人之后我有一个很深的体会第一课选什么往往决定了后续学习的“手感”。我几乎每次都会从“简单线性回归”讲起这个看起来过于基础的模型其实正好是理解整个深度学习训练闭环的最佳入口。这篇文章就围绕线性回归来写把模型设计、损失函数、梯度下降、PyTorch实现这些核心概念一次性讲透。适合刚接触深度学习的读者也适合那些想把基础概念给别人讲明白的人。1. 为什么学深度学习要先啃下线性回归1.1 线性回归在深度学习里的位置很多人刚接触深度学习时会觉得很神秘以为一上来就要搭卷积神经网络、跑大模型。实际上几乎所有经典的入门学习路径都会从线性回归开始。不少教材包括那本被大家称为“鱼书”的《深度学习入门基于Python的理论与实现》开篇也是围绕这种最朴素的模型来讲。原因很简单线性回归虽然是统计学的老古董但它麻雀虽小五脏俱全。一个线性回归模型可以等价地看成“单个神经元、没有激活函数、没有隐藏层”的神经网络。你在 PyTorch 里写一行nn.Linear(1, 1)得到的结构本质上就是线性回归。后续要学的 softmax 回归、多层感知机、卷积神经网络、Transformer都是在这个基础骨架上不断加东西。换个角度说线性回归是深度学习所有流程的最小可运行样本。如果你能把线性回归的训练过程完整理解清楚那么再去看复杂模型会发现它们只是在“模型结构”上变复杂了但整体流程还是那一套前向传播算输出算损失反向传播算梯度优化器更新参数。1.2 训练闭环模型、损失、优化三件套在整个深度学习体系中有三个东西贯穿始终模型、损失函数、优化算法。线性回归恰好是这三者的最简组合。模型y_pred w * x b只有两个参数需要学。损失函数均方误差衡量预测值和真实值差多远。优化算法梯度下降根据损失对参数的偏导方向去调整参数。用一个生活化例子来说你想预测一间房子的价格影响因素先只看面积。那模型就是一条直线价格 斜率 × 面积 截距。训练目标就是找到合适的斜率和截距让这条直线尽量贴合已知的“面积-价格”数据点。损失函数告诉你当前直线整体偏了多少梯度下降则告诉你怎么去旋转、平移这条直线才能减少偏差。这个“预测—对比—调整—再预测”的循环就是深度学习中说的训练闭环。很多初学者在跑第一个深度学习项目时代码里看见optimizer.zero_grad()、loss.backward()、optimizer.step()这三行会发懵。如果先把线性回归手写一遍你会发现这三行其实对应的是“清空梯度、反向传播计算梯度、按梯度更新参数”每一步都看得见摸得着。1.3 从线性回归到神经网络的半步距离也有人问线性回归这么简单离真正的深度学习是不是太远了其实不远。把单特征的线性回归推广到多特征就是y w1*x1 w2*x2 ... wn*xn b写成矩阵形式就是y Xw b。这在神经网络里就是一个全连接层Linear层只是没有激活函数。如果在这个输出后面加一个 sigmoid 函数就变成了逻辑回归可以用来做二分类再加一层隐藏层和激活函数就成了多层感知机。也就是说线性回归和真正意义上的神经网络之间差的只是“非线性激活函数”和“更多层结构”。把线性回归的原理搞清楚后面这些都是在同一个框架下做加法。2. 简单线性回归的核心原理拆解2.1 数学表达与网络结构怎么对应单特征线性回归的数学表达式是y_pred w * x b其中x是输入特征y_pred是预测值w是权重斜率b是偏置截距。训练的目标就是找到一组w和b让预测值尽可能接近真实值。把式子稍微变形就能看出它和神经网络的关系y_pred w * x b可以看成输入x进入一个神经元先做线性变换w*x b然后直接输出。没有激活函数没有多个神经元所以它是一个最朴素的“单神经元网络”。在 PyTorch 里nn.Linear(1, 1)就是干这件事的第一个参数是输入维度第二个参数是输出维度。这里输入是1维特征输出是1维预测值所以就是nn.Linear(1, 1)。如果你用多特征做线性回归比如面积、房龄、楼层三个特征预测房价那就改成nn.Linear(3, 1)。权重w和偏置b的意义也值得多说一句w决定了输入特征对输出的影响力度和方向b则是在没有任何输入时模型的基准输出。在二维平面上w是直线的斜率b是直线与 y 轴的交点。2.2 损失函数为什么选均方误差有了模型还需要一把“尺子”来量预测得准不准。最常用的尺子是均方误差Mean Squared ErrorMSEloss (1/n) * Σ(y_pred_i - y_true_i)^2也就是把所有样本的预测误差先平方再取平均。为什么是平方而不是直接用误差的绝对值第一平方让大误差被放大得更明显。如果某个样本差得离谱它的平方项会占据损失的主要部分这样优化器就会优先去修正那些误差最大的样本。第二平方函数是光滑可导的而且处处可导这对基于梯度的优化方法非常友好。绝对值函数在零点不可导用起来会麻烦不少。第三均方误差对应的优化问题是凸问题只有一个全局最小值不会陷入局部最优。这里顺便提一个容易被忽视的点MSE 对异常值非常敏感。如果数据里有几个极端噪声点它们的巨大误差会主导梯度方向把直线带偏。所以现实中做回归任务时有时会用 Huber Loss 这类对异常值更鲁棒的损失函数。但作为入门先用 MSE 理解核心逻辑就够了。2.3 梯度下降模型学习的发动机模型有了损失函数有了接下来就是最核心的“学习”环节。深度学习里的“学习”不是玄学本质上是求解一个优化问题找到一组参数让损失函数最小。梯度下降的思路特别直观。损失函数关于参数的梯度指明了“上升最快”的方向。那要让损失变小就往相反方向走一步。参数更新公式可以写成w w - lr * ∂loss/∂wb b - lr * ∂loss/∂b其中lr是学习率也就是每次更新参数的步长。我用下山来类比你站在山上某处目标是走到山谷最低点。你看不清整座山的形状只能感受脚下的坡度。最陡的方向就是梯度方向但那是上山的方向所以你要朝反方向迈步。每一步迈多大由学习率决定。步子太大可能直接跨过山谷跳到对面山坡来回震荡步子太小虽然走得稳但要很久才能到谷底。在线性回归这个例子中损失函数对w和b的偏导数可以手推出来∂loss/∂w (2/n) * Σ x_i * (y_pred_i - y_true_i)∂loss/∂b (2/n) * Σ (y_pred_i - y_true_i)看这个式子你会发现梯度的方向和误差(y_pred - y_true)直接相关。如果预测值普遍偏高梯度就会是正数更新时w和b会减小让预测值往回拉。这就是“学习”在数学上最直白的体现。2.4 正规方程线性回归的另一条路讲到线性回归往往会提到另一个方法正规方程。它的思路是直接对损失函数求导并令导数为零一步算出最优参数w (X^T X)^(-1) X^T y这个方法在样本量不大、特征维度不高时非常好用不需要迭代一次就算出解析解。但在深度学习中我们几乎不用正规方程原因有两个一是当特征维度很高时矩阵求逆的计算量非常大甚至不可行二是深度学习模型的损失函数几乎都不是凸函数根本没有正规方程这种“一步到位”的解。但理解正规方程的存在能帮你更好地理解梯度下降。线性回归正因为是凸优化问题梯度下降不管从哪个初始点出发最终都能收敛到同一个最优解。深度学习中复杂的非凸问题做不到这一点所以初始值的设置才会那么重要。3. 手写一个简单线性回归从零到拟合3.1 先造一份“带噪声的直线”数据理论讲再多不如动手跑一遍。我用一个最简单的例子开始假设真实关系是y 2x 1然后在这个关系上叠加一些随机噪声。为什么加噪声因为真实世界的数据几乎不可能是完美直线总是有测量误差、随机扰动。通过拟合带噪声的数据才更接近实战场景。import numpy as np import matplotlib.pyplot as plt np.random.seed(42) X np.linspace(-2, 2, 100).reshape(-1, 1) true_w, true_b 2.0, 1.0 y true_w * X true_b np.random.normal(scale0.3, sizeX.shape) plt.scatter(X, y, s10) plt.xlabel(x) plt.ylabel(y) plt.title(Synthetic Linear Data with Noise) plt.show()这段代码做了三件事生成 100 个在 -2 到 2 之间均匀分布的特征x根据y 2x 1计算真实输出叠加均值为 0、标准差为 0.3 的高斯噪声。画出来的散点图会大致沿着一条斜向上的直线分布但点不会严格落在直线上。3.2 用 NumPy 手动实现梯度下降现在不用任何深度学习框架只用 NumPy 来实现梯度下降。这一步非常关键因为你能亲眼看到参数从随机值慢慢逼近真实值的过程。w 0.0 b 0.0 lr 0.1 epochs 300 for epoch in range(epochs): y_pred w * X b loss np.mean((y_pred - y) ** 2) grad_w np.mean(2 * X * (y_pred - y)) grad_b np.mean(2 * (y_pred - y)) w - lr * grad_w b - lr * grad_b if epoch % 50 0: print(fepoch {epoch}, loss {loss:.4f}, w {w:.4f}, b {b:.4f})运行这段代码输出会类似epoch 0, loss 5.3452, w 0.4790, b 0.2475 epoch 50, loss 0.1208, w 1.8923, b 0.9687 epoch 100, loss 0.0901, w 1.9815, b 0.9914 epoch 150, loss 0.0897, w 1.9968, b 0.9969 epoch 200, loss 0.0896, w 2.0013, b 0.9991 epoch 250, loss 0.0896, w 2.0026, b 0.9999 epoch 300, loss 0.0896, w 2.0030, b 1.0002从结果可以看到w从 0 慢慢爬到接近 2b从 0 爬到接近 1。损失从一开始的 5.35 一路降到 0.09 附近之后基本不再明显下降说明这条直线已经尽量贴合数据了。为什么损失最后停在 0.0896 而不是 0因为数据里叠加了标准差 0.3 的噪声这部分噪声本质上是不可预测的模型学不到。损失收敛到噪声方差附近说明已经把这个线性关系拟合到位了。这里还要提醒一句epochs和lr对结果影响很大。上面用了lr0.1、epochs300训练过程比较平稳。如果把学习率改成 0.5损失曲线可能剧烈震荡改成 0.001训练速度会肉眼可见地慢下来。这一块我在下一章会详细展开。3.3 用 PyTorch 实现同样的事情手写梯度下降之后再看 PyTorch 代码就轻松多了。框架帮你做了自动求导和参数更新但你要清楚每一行在干什么。import torch import torch.nn as nn X_t torch.tensor(X, dtypetorch.float32) y_t torch.tensor(y, dtypetorch.float32) model nn.Linear(1, 1) loss_fn nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.01) for epoch in range(300): optimizer.zero_grad() y_pred model(X_t) loss loss_fn(y_pred, y_t) loss.backward() optimizer.step() if epoch % 50 0: print(fepoch {epoch}, loss {loss.item():.4f}) w_hat model.weight.item() b_hat model.bias.item() print(fTrained w: {w_hat:.4f}, b: {b_hat:.4f})这段代码和手写版本是一一对应的nn.Linear(1, 1)定义了模型结构对应y_pred w * x b。nn.MSELoss()就是均方误差损失函数。torch.optim.SGD(model.parameters(), lr0.01)是随机梯度下降优化器。optimizer.zero_grad()清空上一步残留的梯度这一步不能省。loss.backward()自动计算所有参数对损失的梯度。optimizer.step()根据梯度和学习率更新参数。实际运行时有个细节PyTorch 默认会对模型参数随机初始化所以第一次迭代的初始w、b不一定为 0但训练趋势和 NumPy 实现是相同的。如果你想把初始权重设成 0可以手动赋值with torch.no_grad(): model.weight.fill_(0.0) model.bias.fill_(0.0)3.4 训练结果的检查点训练完成后除了看参数值是否接近真实值还要学会看拟合效果。一个最简单的方法是画图把原始数据散点、训练得到的回归直线画在同一张图上。plt.scatter(X, y, s10, labeldata) plt.plot(X, w * X b, colorred, linewidth2, labellearned line) plt.xlabel(x) plt.ylabel(y) plt.legend() plt.show()如果直线从数据点中间穿过没有明显偏离说明拟合是健康的。如果直线离数据点特别远那就要检查代码或者训练过程了。这个过程看似简单却是以后所有深度学习项目里的基本检查动作画损失曲线、画预测结果而不是只盯着最终的数字看。4. 实操中常见的问题与排查技巧4.1 学习率不当loss 直接飞了线性回归虽然简单但该踩的坑一个不少。最常见的就是学习率设置不当导致不收敛。我在实际带新人的过程中经常看到有人把学习率设成 1.0结果就是损失不降反升甚至直接变成nan。学习率太大时参数更新一步跨过了最优点反而跑到了损失更大的位置然后梯度方向继续乱跳。这就是“步子太大扯着裆”。学习率太小时参数更新像蜗牛爬训练几百轮后w可能才从 0 挪到 0.5离真实值 2 还很远。建议的做法是先用一套默认值比如lr0.01观察损失曲线。然后按数量级试常见候选0.001、0.003、0.01、0.03、0.1。如果发现某个学习率下损失在下降但速度偏慢就增大一点如果发现损失震荡或变大就减小一点。4.2 特征尺度差异大收敛慢半拍数据特征往往存在量级差异。比如预测房价时面积可能是几十到几百而房龄可能是 1 到 30。这两个特征的数值范围相差很大如果直接丢进模型梯度下降在更新参数时会“偏心”数值大的特征对应的梯度变化剧烈数值小的特征更新很慢整个训练过程会又慢又不稳定。解决办法是把特征做归一化或标准化。最常见的是标准化x_standard (x - mean(x)) / std(x)处理之后特征的均值变成 0方差变成 1梯度下降在各方向上的步长就比较均衡了。在简单线性回归实验中如果x的范围是 -2 到 2那其实还好但如果你拿真实数据直接跑这一步几乎一定要做。4.3 训练集和验证集的“分家”问题另一个容易忽略的重要问题是数据划分。很多人做线性回归实验时直接拿全部数据训练然后说模型拟合得很好。这其实是拿“开卷考试的题目”当练习成绩一旦换一批新数据效果可能立刻打折扣。正确做法是把数据分成训练集和验证集或测试集。训练集用来调参数验证集用来评估模型泛化能力。即使是简单的线性回归也要养成这个习惯。具体操作可以这样做from sklearn.model_selection import train_test_split X_train, X_val, y_train, y_val train_test_split(X, y, test_size0.2, random_state42)然后只在X_train上训练在X_val上算验证损失。你会发现训练损失和验证损失之间的差异能直接告诉你模型是欠拟合还是过拟合。对于简单线性回归欠拟合更常见但如果后续加入高阶多项式特征过拟合问题也会跑出来。4.4 一个速查表我整理了一份简单的排查速查表实战中可以直接对照现象可能原因处理方式loss 非常大或变成 NaN学习率过大调低学习率按 0.1、0.01、0.001 逐档尝试loss 缓慢下降但最终不理想学习率太小或特征未归一化调大学习率对特征做标准化训练损失低验证损失高过拟合减少特征维度、增加数据或引入正则化训练损失和验证损失都很高欠拟合或模型结构不对检查特征是否包含足够信息考虑更复杂模型训练过程中 loss 震荡学习率偏大或数据噪声太大降低学习率检查是否存在异常值参数不收敛到合理值初始化不当或数据有误手动检查数据分布尝试将权重初始化为 0这个表适用面很广不只是线性回归很多深度学习练小模型时都可以参考。说到最后分享一个我自己的体会很多初学者看到一个复杂的深度学习项目时会觉得恐惧但内心只要有一套“训练闭环”的底子情绪就会稳很多。线性回归就是帮你建立这套底子的最佳工具。最后再补一个小技巧在训练线性回归时把w和b初始化为 0 是完全没问题的不像深层网络那样需要随机初始化来打破对称性。趁这个阶段多画两条曲线一条看损失怎么下降一条看回归直线怎么逼近散点你对“梯度下降到底在干嘛”就会产生很直观的肌肉记忆。之后再去学 softmax 回归、多层感知机节奏会顺非常多。