ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

线性回归在深度学习中的核心作用与手写实现

2026/10/6 4:57:15 拓冰建站 浏览量
线性回归在深度学习中的核心作用与手写实现 线性回归这个词在深度学习火起来的今天听起来像个老古董但你要是真动手写过几个深度学习项目就会发现它是整个神经网络体系里最不该跳过的一块砖。很多人上来就啃卷积神经网络、Transformer结果连最基础的损失函数下降曲线都看不懂问题就出在线性回归这个起点没踩实。这篇东西我想跟你聊透线性回归在深度学习里的真实位置它不是一个过时的统计工具而是理解梯度下降、反向传播、过拟合这些核心概念的绝佳实验场。我会从数学原理、NumPy手写实现、PyTorch实战到调参踩坑完整过一遍不管你是有一定编程基础想转深度学习的初学者还是已经跑过几个项目但总觉得基础不牢的从业者这篇文章都能帮你把这块地基重新夯实。1. 线性回归的本质它到底在解决什么问题1.1 一个例子讲清楚线性回归线性回归解决的是预测连续数值这类问题。举个最直白的例子你想根据房子的面积预测房价。假设你收集了100条真实交易记录横轴是面积纵轴是价格把这些点画在坐标图上你会发现它们大致沿着一条直线分布。线性回归要做的就是找到一条直线让这条直线尽量贴合所有数据点这条直线就是你的预测模型。这条直线的数学形式是 y w·x bw 是权重b 是偏置。拿到一个新房子的面积 x代入公式就能算出预测价格 y。就这么简单。但背后藏着深度学习里最关键的思想模型不是人手工指定的而是通过数据自动学出来的。w 和 b 一开始是随机值模型通过不断看数据、算误差、调整 w 和 b最终收敛到一组让误差最小的参数这个过程就是训练。1.2 线性回归在深度学习体系里的真实定位很多人误以为深度学习就是多层神经网络线性回归这种单层模型根本不算深度学习。这个理解有偏差。严格来说深度学习强调的是用多层结构自动学习特征表示但线性回归恰恰是神经网络的最小单元。你把线性回归的公式 y w·x b 画成计算图输入层一个节点直接连到输出层一个节点这就是一个没有隐藏层、没有激活函数的单层神经网络。换句话说线性回归是深度学习中所有模型的基础组件。多层感知机的每一层本质上都在做线性变换加非线性激活卷积神经网络的卷积核操作核心也是线性加权求和。你把线性回归吃透了再看神经网络里的全连接层、损失函数、梯度更新会发现全是老朋友。所以很多深度学习课程把线性回归放在第一课不是因为它简单而是因为它是理解一切复杂模型的最小可运行范式。1.3 为什么深度学习入门必须亲手写一次线性回归我见过太多人直接调 PyTorch 或 TensorFlow 的现成接口跑 MNIST 手写数字识别跑通了就觉得自己会深度学习了。但真让他解释一下模型是怎么学出来的损失值为什么从大到小变化梯度下降每一步在做什么基本答不上来。这就是基础不牢的典型表现。亲手实现一次线性回归哪怕是最简单的版本你也会被迫面对几个绕不开的问题损失函数怎么定义、梯度怎么算、参数怎么更新、数据要不要标准化、学习率调大了会怎样。这些问题在你看视频教程时觉得都懂真正自己动手写代码跑起来才会发现每一个都是坑。我强烈建议不管你之后要学 CNN 还是 Transformer先把线性回归用 NumPy 手写一遍再用 PyTorch 写一遍两遍对照着看基础就扎实了。2. 数学原理拆解损失函数与梯度下降为什么长这样2.1 目标函数与MSE损失模型要学出好的 w 和 b首先得有一个量化标准预测值和真实值差多少。最常用的量化方式是均方误差 MSE公式是 L (1/n)∑(y_pred - y_true)²就是所有样本预测误差的平方取平均。为什么用平方而不是直接用误差的绝对值两个原因。第一平方操作让正负误差不会相互抵消你不能让一个样本预测高了、另一个预测低了两个误差中和成零误差这显然不合理。第二平方函数是凸函数意味着损失曲面只有一个全局最低点梯度下降能稳定收敛不用担心陷入局部最优。绝对值误差虽然也是凸函数但它在零点不可导梯度计算不方便实际使用中 MSE 是更顺手的默认选择。还有一个细节值得注意MSE 的系数 1/n 或 1/2n 在不同教材里写法不同。PyTorch 的 MSELoss 默认是 1/n而有些推导为了方便求导会写成 1/(2n)本质不影响结果只是让导数表达式里少个系数 2。初学者看到不同资料里公式不一样容易懵知道这个背景就不会被绕晕。2.2 梯度下降的直观理解与推导有了损失函数接下来问题是怎么找到让损失最小的 w 和 b。数学上可以直接求导令导数为零解得解析解也就是最小二乘法的正规方程。但深度学习的场景里样本量巨大、特征维度高求解析解涉及矩阵求逆计算量不可接受而且一旦模型变复杂就不再是线性问题解析解根本不存在。所以实际训练用的是梯度下降法。梯度下降的思想可以类比成下山你站在山顶看不清路但能感觉到脚下哪个方向坡度最陡沿着最陡的方向迈一步反复执行就能走到山谷最低点。这里坡度最陡的方向就是损失函数对参数的梯度导数迈一步的大小就是学习率。参数的更新公式是 w w - 学习率 × 梯度那个负号很关键因为梯度指向损失增大的方向要减掉它才能让损失变小。以 MSE 损失为例对 w 求导的结果是 (2/n)∑x·(y_pred - y_true)对 b 求导的结果是 (2/n)∑(y_pred - y_true)。你注意看这个形式梯度的大小正好由预测误差驱动误差大梯度就大参数更新步子就大预测越准梯度越接近零参数更新越来越细微。这就是学习的本质——误差回传指导参数修正。这个思想贯穿深度学习全程反向传播算法本质上就是利用链式法则高效计算每一层参数的梯度线性回归里你手动推导的这两条梯度公式就是反向传播的最简雏形。2.3 学习率最容易被忽视的关键参数学习率是训练过程中最值得反复体会的超参数没有之一。它直接决定每一步参数更新的幅度。学习率设得太小模型收敛极慢训练几百个 epoch 损失还在缓慢下降浪费计算资源设得太大参数更新步子迈过头损失函数可能不减反增甚至直接震荡发散数值变成 NaN。我自己的经验是线性回归这种简单模型上学习率从 0.01 到 0.1 之间通常能找到合适值但具体多少取决于数据的量级。如果数据没有做标准化特征值动辄上千梯度也会很大学习率就得调得很小才能稳住数据标准化之后特征值基本落在 -1 到 1 之间梯度量级可控学习率的选择余量就大多了。所以调参的第一步往往不是调学习率本身而是先把数据处理好。实践中你还会遇到学习率调度器这个概念。简单场景用固定学习率就够了但更复杂的模型训练里常见做法是训练初期用较大的学习率快速下降后期逐步减小学习率让参数在最优值附近精细收敛。PyTorch 里自带 StepLR、CosineAnnealingLR 这些调度器等你对固定学习率的训练过程有体感之后再去尝试这些工具会容易理解得多。3. 动手实现从NumPy手写到PyTorch实战3.1 数据准备构造一组带噪音的线性数据开始写代码之前先准备数据。为了能验证模型学得对不对最好用已知的真实参数来生成合成数据这样训练完可以直接对比模型学到的 w 和 b 与真实值差多少。假设真实模型是 y 2·x 1我们生成 100 个样本x 在 0 到 10 之间均匀分布y 按照真实公式计算后加上一些高斯噪音模拟现实场景中数据不可避免的随机扰动。噪音的标准差设成 1 左右这样数据点会围绕直线上下波动但趋势仍然清晰可见。import numpy as np np.random.seed(42) x np.linspace(0, 10, 100).reshape(-1, 1) true_w, true_b 2.0, 1.0 y true_w * x true_b np.random.randn(100, 1) * 1.5 print(x.shape, y.shape)这里 reshape 成列向量的操作值得提一句。后面的矩阵运算是按照样本数 × 特征数的组织方式设计的x 变成 100×1 的二维数组才能和 w 做矩阵乘法。很多初学者在这里栽过跟头一维数组直接算也能出结果但广播规则容易把人搞晕尤其进入 PyTorch 之后张量维度问题会频繁出现从一开始就保持维度清晰是个好习惯。3.2 用NumPy从零手写线性回归使用 NumPy 手写线性回归是理解整个深度学习训练流程的最佳方式代码不长但每一行都有意义。核心就四步初始化参数、计算预测值、计算损失、计算梯度并更新参数。import numpy as np def compute_loss(y, y_pred): n len(y) loss np.mean((y_pred - y) ** 2) return loss def train_linear_regression(x, y, lr0.05, epochs500): n, d x.shape w np.random.randn(d, 1) * 0.01 b np.zeros((1, 1)) history [] for epoch in range(epochs): y_pred x w b loss compute_loss(y, y_pred) history.append(loss) grad_w (2 / n) * (x.T (y_pred - y)) grad_b (2 / n) * np.sum(y_pred - y) w w - lr * grad_w b b - lr * grad_b if epoch % 50 0: print(fepoch {epoch}, loss {loss:.6f}) return w, b, history w, b, history train_linear_regression(x, y) print(f学到的参数: w{w[0,0]:.4f}, b{b[0,0]:.4f})这段代码里有两个地方要特别留意。第一个是参数初始化w 用了随机值并乘以 0.01b 直接初始化为 0。随机初始化是必要的如果 w 初始为 0所有样本的预测值相同梯度虽然不为零但容易让训练走弯路而乘 0.01 是为了让初始梯度不会太大训练更稳定。第二个是梯度的矩阵写法x.T (y_pred - y) 这一行同时完成了所有样本的误差加权求和比写 for 循环逐个样本累加高效得多也简洁得多。跑完这段代码你会看到 loss 从几百一路降到个位数甚至更低学到的 w 接近 2、b 接近 1。这个过程虽然简单但你已经完整走了一遍深度学习训练的标准流程前向传播计算预测、计算损失、反向传播求梯度、梯度下降更新参数。后续所有模型无论多复杂核心循环都是这个框架。3.3 用PyTorch实现同样的任务手写版本帮你理解了原理PyTorch 版本则让你看到实际工程中怎么用框架把繁琐的求导过程自动化。框架的价值恰恰在于你只需要定义模型结构和前向计算逻辑反向传播和梯度更新由框架自动完成。import torch import torch.nn as nn import torch.optim as optim x_tensor torch.tensor(x, dtypetorch.float32) y_tensor torch.tensor(y, dtypetorch.float32) class LinearRegressionModel(nn.Module): def __init__(self): super().__init__() self.linear nn.Linear(1, 1) def forward(self, x): return self.linear(x) model LinearRegressionModel() criterion nn.MSELoss() optimizer optim.SGD(model.parameters(), lr0.05) epochs 500 for epoch in range(epochs): y_pred model(x_tensor) loss criterion(y_pred, y_tensor) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 50 0: print(fepoch {epoch}, loss {loss.item():.6f}) w_pytorch model.linear.weight.item() b_pytorch model.linear.bias.item() print(fPyTorch学到的参数: w{w_pytorch:.4f}, b{b_pytorch:.4f})这段代码里藏着几个新概念值得展开讲。第一个是 nn.Linear(1, 1)第一个参数是输入特征维度第二个是输出维度这里都是 1对应一元线性回归。框架自动帮我们初始化了权重和偏置你可以通过 model.linear.weight 访问。第二个是 optimizer.zero_grad() 这行很多初学者第一次写都会忘。PyTorch 的梯度是累加的如果不清零下一次 backward 计算出的梯度会叠加到上一次的梯度上参数更新就会出错。每轮训练开始前手动清零是固定动作。还有一个细节是 loss.backward() 和 optimizer.step() 的分工。backward 负责计算所有参数的梯度并存储在每个参数的 grad 属性里step 根据梯度和学习率真正更新参数。理解这两步的分离你就能明白为什么调试时可以打印参数的 grad 值来检查梯度是否正常这在排查训练问题时非常有用。3.4 两种实现的对比与选择建议手写版本和框架版本跑出来的结果基本一致但体验完全不同。手写版本对理解原理至关重要你会清楚地看到每个参数是怎么被梯度一步步推着走的框架版本则展示了真实项目的写法模型定义、损失函数、优化器三件套是几乎所有 PyTorch 项目的标配。我给初学者的建议是第一次学线性回归两版都要写。先手写跑通了再上 PyTorch对照着看每个环节的对应关系——手写的 loss 计算对应 criterion手写的参数更新公式对应 optimizer.step()。这样以后再接触更复杂的模型时你看 PyTorch 代码就不会觉得黑盒而是知道框架在背后替你完成了哪些事。数据处理方面还有个容易忽略的小问题。NumPy 的数组转成 PyTorch 张量时一定要确认数据类型是 float32因为 PyTorch 的默认浮点类型是 float32如果传入 float64 的数据有时会报类型不匹配的错。用 torch.tensor(x, dtypetorch.float32) 显式指定类型能从源头上避免这类问题。4. 超参数调优与训练技巧让模型真正收敛4.1 数据标准化不做的后果很严重很多教程在讲线性回归时会默认你的数据已经适合直接训练但现实中的数据很少这么友好。假设你要预测房价特征不止面积一个还有卧室数量、房龄这些量级差异会很大面积在几十到几百平方米房龄在 0 到 50 年之间。如果不做标准化梯度更新的步伐会被量级大的特征主导量级小的特征几乎学不到东西。标准化最常用的方法是 Z-score每个特征减去均值再除以标准差转换后数据均值接近 0、标准差接近 1。另一个常用方法是 Min-max 归一化把数据映射到 0 到 1 区间。选择哪种取决于场景如果数据分布近似正态Z-score 更合适如果数据分布比较均匀且没有太多离群点Min-max 归一化更直观。还有一点必须记住标准化参数只能用训练集计算然后直接用同一组均值和标准差去处理验证集和测试集。这是防止信息泄露的关键。如果先用全量数据算了均值标准差再做标准化模型在评估时就已经见过测试集的信息了评估结果会偏乐观不能反映真实泛化能力。这个错误比较隐蔽但后果很实在。4.2 学习率、迭代次数与Batch Size怎么配学习率、迭代次数epoch和 Batch Size 是三个互相纠缠的超参数。线性回归这种小规模问题通常用全量梯度下降也就是每次更新用全部样本计算梯度代码里体现为直接对 100 个样本做矩阵运算。数据规模大了之后全量计算太慢就会引入小批量随机梯度下降每次随机抽一小批样本算梯度。Batch Size 越小更新越频繁收敛路径上的噪声越大越大更新越稳定但单次计算量越大。我实际调参的顺序是先固定 Batch Size比如 32 或 64用默认的 Adam 优化器或 SGD 配一个中等学习率0.01 到 0.1跑一遍观察损失曲线。如果损失一路下降然后趋平说明学习率基本合适如果损失震荡剧烈先把学习率调小十倍再试如果损失下降极慢调大学习率。迭代次数则看损失曲线什么时候开始不再明显下降再往后多训练只会浪费时间甚至引入过拟合风险。这里推荐一个非常实用的工具early stopping在验证集损失连续多个 epoch 不下降时提前终止训练而不是死板地跑完设定的所有 epoch。很多深度学习框架和训练工具都内置了这个机制但线性回归实验里你可以手动实现一次写个连续 10 个 epoch 损失没有改善就 break的逻辑体会一下训练不是跑完就完事而是该停就停的节奏感。4.3 训练过程的监控指标训练时只盯着最终 loss 值是不够的最好把每个 epoch 的损失记录成曲线直观看到下降趋势。最理想的情况是损失平滑下降像滑梯一样常见情况是前几十个 epoch 下降很快后面逐渐变平这很正常因为接近最优解时梯度会越来越小。如果损失曲线像锯齿一样上下剧烈跳动大概率是学习率偏大如果曲线基本是平的可能学习率太小也可能是代码有 bug。除了损失值还有一个被低估的指标学到的参数与真实参数的差距。用合成数据训练线性回归有个额外好处就是你手里有 ground truth可以打印 w 和 b 的收敛过程看它们是不是一步步逼近真实值。这个观察对建立模型到底学到了什么的直觉特别有帮助。你会发现训练早期参数变化快后期变化慢和损失曲线的规律完全对应。实际工程里还会看 R² 分数衡量模型解释了多少数据方差越接近 1 说明拟合效果越好。对线性回归来说R² 可以直观地告诉你模型的好坏程度而不像损失值那样受数据量纲影响不好横向比较。不过看 R² 时要小心它随特征数量增加会虚高所以一般看调整后的 R² 才公平。5. 常见问题与排查技巧实录5.1 损失不下降或震荡怎么办损失完全不下降是最容易让人心态崩溃的情况但绝大多数时候不是模型的问题而是训练设置出了问题。先检查学习率如果是 0.001 甚至更小数据量又不大的话可能训练几百轮也没明显变化试着调大十倍。再检查梯度打印一下参数的 grad 值如果梯度接近零可能是权重初始化太小导致梯度消失或者是数据没处理好梯度计算本身出了问题。最后检查数据确认 x 和 y 的对应关系有没有错位标签有没有混入异常值。损失震荡幅度很大也很常见。一个容易踩的坑是优化器里没调对 momentum 参数或者用的优化器本身对当前学习率太敏感。我的排查思路是把学习率降到原来的十分之一如果震荡明显缓解说明是学习率的问题可以用学习率调度器在训练中动态调整而不是手动改如果震荡仍然剧烈就要怀疑数据本身的问题比如存在极端离群点一个误差巨大的样本会拖拽整个梯度方向。5.2 梯度爆炸与NaN问题训练过程中 loss 突然变成 nan 是新手最容易遇到的噩梦之一。线性回归这种简单模型出现 nan最常见原因是学习率过大导致参数更新幅度过大数值计算溢出。比如数据值很大误差也很大梯度值可能到几千甚至几万学习率 0.1 乘上去参数瞬间跳到离谱的值再算损失时平方就溢出了。排查流程很直接先看 loss 是在哪个 epoch 变成 nan 的往前回溯参数值的变化然后把学习率调小一千倍如果训练恢复正常基本锁定是学习率问题。此外也可以在代码里加梯度裁剪gradient clipping当梯度的模超过设定阈值时按比例缩放到阈值范围内。PyTorch 里一行代码 torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm) 就能搞定复杂模型的训练中这是家常便饭。最后提醒一个细节数据里的 NaN 值。如果你的输入数据本身包含 NaN训练过程也会出现各种诡异表现。建议在数据预处理阶段就检查一下 np.isnan(data).any()有 NaN 要么删除对应样本要么用均值或中位数填充不要带着脏数据直接训练。5.3 欠拟合与过拟合怎么区分线性回归模型结构固定欠拟合通常表现为训练误差和测试误差都很高。这个模型的学习能力就这么强只有线性的表达能力如果数据本身不是线性关系比如真实关系是抛物线你用线性模型去拟合怎么训练都得不到好结果。这种情况不是调参能解决的而是模型容量不够需要换更复杂的模型或者先对特征做非线性变换比如加入 x² 项变成多项式回归。过拟合在线性回归这种简单模型上不明显因为参数少但如果你不断增加特征数量比如给一元数据加上 x²、x³、x⁴ 这些高阶项模型就会开始死记硬背训练数据里的每一个点包括噪声。表现是训练损失极低测试集上表现反而变差。此时可以引入正则化L2 正则化岭回归让大权重受到惩罚L1 正则化Lasso则会让一部分特征的权重变成零相当于自动做特征选择。学会区分这两种情况的价值在于你后续处理任何深度学习模型都会面对同样的判断。看到模型效果不好第一步不是无脑加层或调参而是先判断问题出在容量不足欠拟合还是容量过剩外加数据不够过拟合对症下药才有效率。5.4 问题排查速查表症状可能原因排查动作损失完全不下降学习率过小调大学习率观察前50轮损失变化损失震荡剧烈学习率过大调小学习率或使用学习率调度器损失变成NaN梯度爆炸/数据含NaN调小学习率加梯度裁剪清洗数据训练误差高、测试误差也高欠拟合模型容量不足增加特征复杂度或更换更强模型训练误差低、测试误差高过拟合模型记住了噪声加正则化扩大数据量减少特征学出的参数严重偏离真实值数据未标准化做Z-score标准化重新训练相同的代码两次结果不同随机初始化与数据顺序影响设置随机种子 np.random.seed 和 torch.manual_seed这张表是我从实际带新人的过程中整理的每一条都对应真实踩过的坑。里面最值得强调的还是随机种子问题深度学习中处处有随机性参数初始化、数据打乱顺序、GPU 运算都不完全确定。如果你的实验结果需要复现在代码开头固定所有随机种子是基本操作。这看起来是小事但真到写论文或者做工程交付时一个跑一次一个结果的项目等于没有交付。6. 从线性回归走向深度学习一条清晰的进阶路线6.1 线性回归与单层神经网络的关系线性回归是一个没有隐藏层的神经网络而且没有激活函数。你在 PyTorch 里的实现 LinearRegressionModel和单层全连接网络的唯一区别就是输出有没有经过激活函数。把线性回归的输出接上一个 Sigmoid 函数它就变成了逻辑回归可以解决二分类问题把多个线性回归的输出组合起来加一层非线性激活就变成了多层感知机。理解这层递进关系非常重要。很多新手觉得从线性回归跳到神经网络是一道巨大的坎其实就是一层窗户纸。神经网络每一层做的事情不过是对输入做一次线性变换加上一次非线性变换然后把结果传给下一层。你在线性回归里学会的损失函数、梯度下降、优化器选择在神经网络里一个都不会变变的只是参数更多、计算图更复杂而已。6.2 引入非线性激活函数的意义你可能会问如果只是把多个线性层堆叠在一起模型的能力会变强吗答案是不会。若干个线性变换串联起来数学上仍然等价于一个线性变换因为线性函数复合之后还是线性函数。这就像你把手机相机开了三倍变焦再开三倍变焦效果是九倍变焦但拍摄效果本质上还是在一张直通图像上做缩放没有信息增强。激活函数的作用就是打破这种线性限制。ReLU、Sigmoid、Tanh 这些非线性函数被插在层与层之间让神经网络可以拟合任意复杂的非线性关系。这也是深度学习深的意义所在每一层逐级提取更抽象的特征靠的就是非线性激活的反复叠加。下次看到网络结构里Conv1 - ReLU - Conv2 - ReLU这样的写法你要知道 ReLU 就是那个让网络真正拥有表达能力的角色。6.3 给新手的下一步学习建议学完线性回归之后最自然的进阶路径是逻辑回归和 Softmax 分类它们把线性模型从回归任务扩展到分类任务。然后可以学多层感知机MLP在手写数字识别等小数据集上跑通一个真正的深度模型。之后再根据你的方向选择做视觉的走卷积神经网络做序列数据的走循环神经网络或者直接转向 Transformer 系架构也算水到渠成。每个阶段都建议保持同一个习惯先手写核心逻辑再引入框架。手写会让你的调试能力真正长在自己身上框架只是在成熟项目中提升效率的工具。遇到问题先看损失曲线、看梯度值、看参数变化这种排查思路比背几个 API 有价值得多。我在实际带人过程中的体会是线性回归这个起点值得反复回来复习。你学到反向传播、学到正则化、学到学习率调度再回头看线性回归总会有新的理解。比如当初只在代码里写过 gradient clipping 的一行调用直到训练深层网络遇到真正的梯度爆炸才明白那一行背后救了你多少次。基础概念的复利效应在深度学习这条路上体现得特别明显。建议你把这篇文章里手写版本和 PyTorch 版本的代码都跑通改一改学习率、换一换数据分布、加一点噪声亲手制造几个错误再亲手把它们修好这些经历比任何教程都更能帮你建立对模型训练的直觉。