ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

线性回归与PyTorch:梯度下降、损失函数实战入门深度学习

线性回归与PyTorch:梯度下降、损失函数实战入门深度学习 1. 从零开始理解线性回归为什么它是深度学习的第一课很多刚接触深度学习的同学一上来就看CNN、Transformer结果被各种网络结构、注意力机制搞得晕头转向。我自己带过不少新人几乎每次都会劝他们先别急着追热点老老实实把线性回归吃透。原因很直接线性回归虽然结构简单但它完整覆盖了深度学习训练流程中的所有核心环节——模型定义、损失函数、梯度下降、反向传播、参数更新一个都不少。你学会了线性回归的训练过程就等于掌握了神经网络训练的“最小闭环”。后面学多层感知机、CNN、RNN本质上都是在跟这个闭环打交道只是换着花样设计模型结构和损失函数而已。用 PyTorch 实现线性回归还有一个额外好处你可以非常直观地看到张量tensor是如何流动的自动求导autograd是怎么工作的以及模型的参数是怎么一步步被优化到位的。这些基本功打扎实了后续学任何复杂模型都会顺畅很多。这篇文章我会从三个方面展开先讲清楚线性回归背后的数学原理和损失函数设计逻辑然后用 PyTorch 手写一遍完整的实现流程从数据准备到模型训练再到结果可视化最后分享几个我带队时经常遇到的坑和排查经验直接帮你绕开弯路。2. 深入拆解线性回归的核心原理2.1 线性模型到底建模的是什么线性回归要解决的问题说起来很简单给出一组输入特征 x 和对应的目标值 y我们要找到一组权重 w 和偏置 b使得 ŷ wx b 这条直线或超平面能够最好地拟合已有的数据点。这里的关键词是“最好”。什么算最好通常我们用**均方误差Mean Squared Error, MSE**来衡量预测值和真实值之间的差距MSE (1/n) * Σ(yᵢ - ŷᵢ)²这个公式的字面意思很好理解就是算每个样本预测值和真实值差值的平方然后取平均。为什么要用平方而不是绝对值这里有两个原因值得展开说。第一平方放大了大误差的惩罚力度。预测偏差 2 个单位时平方是 4偏差 10 个单位时平方是 100。这样模型会优先去“纠正”那些错得离谱的样本符合我们优化模型的直觉——先把大毛病改掉。第二平方误差在数学上处处可导而且导数是连续的。这对梯度下降算法极其关键因为我们需要通过求导获得梯度方向如果使用绝对值误差L1 loss在零点处不可导梯度方向会不稳定影响训练收敛。2.2 梯度下降模型学习的核心引擎有了损失函数接下来就是如何最小化它。这就是梯度下降Gradient Descent出场的时候了。用一个生活化的例子来解释想象你站在一座山腰上四周浓雾弥漫看不清整座山的地形你要怎么走到山谷最低点最靠谱的策略是感受脚下哪边坡度最陡然后往那个方向迈一步反复执行这个动作直到脚下几乎是平地为止。梯度下降就是干这件事的。损失函数是一个“地形”模型参数 w 和 b 决定了你在山上的位置损失值就是当前的海拔。我们计算损失函数对参数的偏导数梯度它指向的是上升最快的方向所以我们反着走也就是往梯度的反方向更新参数。更新公式是这样的w ← w - η * ∂L/∂w b ← b - η * ∂L/∂b这里的 η 就是学习率learning rate它决定了每一步迈多大。学习率太大步子迈过了头可能会越过最低点甚至震荡发散学习率太小步子太碎走了很久还在半山腰上磨蹭。这是整个训练过程中调参最频繁、也最容易出问题的地方后文我会专门展开。2.3 为什么 PyTorch 能自动帮我们做梯度更新以前用纯 Python 写梯度下降你还得自己手动推导偏导数公式参数一多就头疼。PyTorch 的核心贡献是实现了**自动求导autograd**机制。它的工作方式有点像一个“记账系统”你对张量做的每一项运算它都会记录在计算图里。定义好 loss 之后你只需要调用loss.backward()PyTorch 会根据链式法则自动把梯度传播回每一个参与运算的参数张量上你要做的就是读取参数对应的.grad属性然后拿它去更新参数。这一点极其重要因为真实项目里的模型动辄上百万参数手动算导数根本不现实。PyTorch 把最繁琐的数学部分自动化了让你能把精力集中到模型设计、数据处理这些更有价值的环节上去。3. 环境准备与数据生成3.1 安装 PyTorch 并检查 GPU 可用性做线性回归其实用 CPU 就完全够了但如果你打算后续深入深度学习那建议从一开始就装 GPU 版本的 PyTorch。安装方式很简单去 PyTorch 官网选好你的系统环境和 CUDA 版本它会生成对应的安装命令。装完之后有一个非常重要的验证步骤import torch print(torch.__version__) print(torch.cuda.is_available())如果torch.cuda.is_available()返回True说明 GPU 可用。False也不影响线性回归的运行但后续训练大模型时就得排查一下安装源、驱动或 CUDA 版本的问题了。3.2 构造带噪声的线性数据集真实世界里几乎不存在完美的线性关系数据总会带点噪声。为了让训练过程贴近实际情况我们人工制造一份“带噪声的线性数据”这样既能验证模型的拟合能力又能直观地观察噪声对结果的影响。制造数据的方式是设定真实的 w2、b1然后在线性关系之上叠加服从正态分布的随机噪声import torch torch.manual_seed(42) x torch.linspace(0, 10, 100).reshape(-1, 1) true_w 2.0 true_b 1.0 noise torch.randn_like(x) * 0.5 y true_w * x true_b noise这段代码里torch.manual_seed(42)的作用很关键——它会固定随机数生成器的种子让每次生成的数据完全一致。这样你复现代码时得到的训练效果和我这里是完全可比的排查问题也能排除“数据不同”这个变量。噪声标准差选了 0.5这个取值比较适中既能让数据看起来“有散落感”又不至于掩盖掉线性的主体趋势。太小的噪声学起来毫无挑战太大的噪声会让模型学到错误规律。3.3 划分训练集和验证集在正规的机器学习流程中数据要划分为训练集和验证集。训练集用于更新模型参数验证集用于评估模型在没见过的新数据上的表现防止过拟合。这里的数据量不大我用 8:2 的比例划分indices torch.randperm(len(x)) train_idx, valid_idx indices[:80], indices[80:] x_train, y_train x[train_idx], y[train_idx] x_valid, y_valid x[valid_idx], y[valid_idx]torch.randperm会生成随机排列的索引这样打乱数据是为了避免数据本身存在顺序结构而影响训练效果。虽然这份数据是按 x 从小到大排列的理论上对训练影响不大但养成打乱的习惯在真实项目中非常重要。4. 用 PyTorch 实现线性回归的完整步骤4.1 搭建模型两种写法的对比第一种写法是纯手工定义参数最直观地展示模型本质w torch.randn(1, requires_gradTrue) b torch.zeros(1, requires_gradTrue) def linear_model(x): return x * w b这里的requires_gradTrue是告知 PyTorch这个张量是计算图中的叶子节点需要计算并跟踪它的梯度。你可以把它理解成给这个变量贴了一个“要算梯度”的标签。第二种写法是利用 PyTorch 的 nn 模块更贴近工程实践import torch.nn as nn model nn.Linear(in_features1, out_features1)nn.Linear会在内部自动创建权重和偏置参数并且已经做了正确的初始化。线性层的 weight 形状是 (out_features, in_features)bias 形状是 (out_features,)。这里输入输出都是 1所以就是最简单的 ywxb 形式。两种写法各有各的适用场景。第一种适合学习原理你能看到参数的全部细节第二种适合实际开发代码简洁且扩展性好。至于nn.Sequential、多层的写法本质上就是把多个nn.Linear串起来大家先把这个单层结构吃透。4.2 定义损失函数与优化器损失函数我用nn.MSELoss()这和我们前面推导的均方误差公式完全一致loss_fn nn.MSELoss() optimizer torch.optim.SGD(model.parameters(), lr0.01)优化器选择的是随机梯度下降SGD。这也是一个值得留意的地方虽然Adam、RMSProp这类自适应优化器用起来更“省心”但做线性回归这种凸优化问题时SGD 不仅够用而且能让你更直观地理解梯度下降的本质。Adam 就是在梯度下降的基础上加了动态调整步长的机制原理是不一样的。model.parameters()会自动返回模型中所有需要更新的参数这也是nn.Module比手工定义省心的地方。SGD 的lr0.01是我在这个数据尺度下的常用起点后面我会专门聊学习率怎么调。4.3 完整训练循环这是整个文章最核心的代码请务必把每一行的作用都搞清楚epochs 200 for epoch in range(epochs): # 前向传播输入 x得到预测值 y_pred model(x_train) # 计算损失 loss loss_fn(y_pred, y_train) # 梯度清零先清空上一次的梯度 optimizer.zero_grad() # 反向传播计算当前所有参数的梯度 loss.backward() # 更新参数执行一步梯度下降 optimizer.step() if (epoch 1) % 20 0: print(fEpoch [{epoch1}/{epochs}], Loss: {loss.item():.4f})很多初学同学会漏掉optimizer.zero_grad()这一步这是最容易踩的坑。PyTorch 的反向传播会累积梯度到.grad属性中如果不手动清零第二次迭代的梯度会和第一次叠加。叠加之后更新方向就跑偏了模型可能永远无法收敛。完整训练循环就是一个简化版的深度学习“流水线”前向传播拿预测结果 → 计算损失衡量好坏 → 反向传播算梯度 → 更新参数来优化好坏。四个步骤缺一不可顺序也不能乱。4.4 模型验证与数据可视化训练完成后我们最关心两个问题模型参数是否逼近真实的 w2、b1在验证集上的表现如何with torch.no_grad(): w_learned model.weight.item() b_learned model.bias.item() valid_pred model(x_valid) valid_loss loss_fn(valid_pred, y_valid) print(f学到的 w: {w_learned:.4f}, 真实的 w: {true_w}) print(f学到的 b: {b_learned:.4f}, 真实的 b: {true_b}) print(f验证集损失: {valid_loss.item():.4f})torch.no_grad()是一个上下文管理器告诉 PyTorch 这里面的操作不需要构建计算图和计算梯度。推理阶段用它能省内存、提速同时防止不小心污染梯度状态。可视化部分建议把训练数据的散点、真实线、模型预测线画在同一张图上。你会看到三条线中模型预测线和真实线几乎重合这就是训练成功的直观信号。用 matplotlib 就能轻松实现import matplotlib.pyplot as plt plt.scatter(x_train.numpy(), y_train.numpy(), label训练数据) x_plot torch.linspace(0, 10, 100).reshape(-1, 1) y_plot model(x_plot).detach().numpy() plt.plot(x_plot.numpy(), y_plot, colorred, label模型预测) plt.plot(x_plot.numpy(), true_w * x_plot.numpy() true_b, colorgreen, linestyle--, label真实线) plt.legend() plt.xlabel(x) plt.ylabel(y) plt.show()实测下来用 lr0.01 训练 200 轮学到的 w 通常在 1.98 左右b 在 1.1 左右和真实值的偏差在正常范围内因为数据集里本身加入了噪声模型不可能也不应该完美还原真实参数。4.5 训练过程监控损失下降曲线打印每一轮的损失值只能看个大概更好的做法是把损失记录下来画成曲线图。这样你能直观地看到模型何时收敛是否有震荡是否存在欠拟合或过拟合的苗头loss_history [] for epoch in range(epochs): y_pred model(x_train) loss loss_fn(y_pred, y_train) optimizer.zero_grad() loss.backward() optimizer.step() loss_history.append(loss.item()) plt.plot(loss_history) plt.xlabel(Epoch) plt.ylabel(MSE Loss) plt.title(训练损失下降曲线) plt.show()一个健康的损失曲线应该是前期快速下降然后逐渐平缓呈“滑梯状”。如果你的曲线是水平直线说明学习率太小或者模型有 bug如果曲线上下剧烈震荡通常是学习率偏大了。5. 实操中的高频问题与排查经验5.1 特征标准化x 和 y 的量级会影响学习率设置这是初学阶段最容易忽略的问题。如果你的特征 x 取值范围在 [0, 10000]目标值 y 在 [100000, 1000000]NaN 风险会急剧增加因为大批量梯度更新时较大的数值可能导致梯度爆炸。出现这种情况时训练 loss 会打印出nan模型参数也会变成nan。原因就是梯度数值超过了浮点数能表达的上限。排查手段一句话总结如果你的输入特征跨了几个数量级先做标准化归一化到均值 0、方差 1或者缩放到 [0,1]再训练就好多了。线性回归本身是线性变换标准化后再训练得到的参数转换回原空间是非常容易的所以不必担心标准化影响结果的可解释性。我实践中最常用的方式x_mean, x_std x.mean(), x.std() x_normalized (x - x_mean) / x_std标准化之后的特征分布变得“友善”了许多模型训练时收敛速度会明显加快。5.2 学习率的选择策略学习率是训练循环中最关键的“超参数”hyperparameter。给几个具体数值做参考当 x 在 [0,10] 范围内、y 由 2x1 加噪声生成时lr0.01 通常 150-200 轮可以稳定收敛lr0.1 就很容易震荡lr0.001 则收敛很慢1000 轮也未必到位。如果你不确定该用什么学习率我分享一个实战经验从大到小试先用 0.1 看一下是否震荡再降到 0.01 看是否稳定下降。如果 0.001 才稳定说明你的数据可能需要标准化处理而不是单纯换学习率。5.3 梯度清零的作用过深解释前面提到optimizer.zero_grad()是清空累积梯度。为了加深理解我再补充一个场景假设你忘记调用它训练过程中梯度的值会上一次迭代和这一次迭代累加。累加到一定程度参数更新方向就不完全由当前样本决定而是混了历史信息而且这个“历史信息”不断被持续累积越积越偏模型基本就废了。如果你发现 loss 呈现陡增或异常上涨的趋势第一反应就检查代码里有没有optimizer.zero_grad()。5.4 模型评估时要记得关闭梯度跟踪训练阶段需要跟踪梯度以更新参数但模型评估阶段不需要反向传播继续跟踪梯度只会白白消耗内存和算力。而如果你在评估时不小心把模型输出和损失引入了训练循环之外的计算图还可能出现难以察觉的“隐藏 bug”——梯度的链路变得很长后续某个操作莫名其妙报错。所以我在所有推理、评估的代码块外面都会习惯性地写上with torch.no_grad(): # 所有评估代码放这里 ...5.5 随机种子让实验“可复现”深度学习里有三处地方涉及随机性数据生成、模型初始化、优化器内部的随机行为如 shuffle。为了让实验可以反复调试且结果可比较养成固定随机种子的习惯至关重要。通常在脚本开头统一设置import random import numpy as np import torch random.seed(42) np.random.seed(42) torch.manual_seed(42)如果使用 GPU可能还需要设置torch.cuda.manual_seed_all(42)设置完随机种子后同参数、同数据的实验结果是可复现的这给后续排查问题省了太多事。5.6 数据形状问题nn.Linear期望输入形状为(batch_size, in_features)二维张量。很多新手直接把一维张量torch.linspace传进去会得到维度不匹配的报错。解决办法是在构造时就reshape(-1, 1)。如果用的是读入的外部数据建议打印x_train.shape检查一下。形状不匹配的报错信息通常很长但核心信息往往藏在最后几行先读懂报错的最后一行再动手修代码这个顺序很重要。6. 从线性回归到更广阔的世界走完整个线性回归实现你具备的不只是“会调 PyTorch 接口”的能力。你知道了模型是怎么训练的梯度下降是如何运转的学习率对收敛效果的影响有多大以及为什么要做数据标准化。这些认知在后续的机器学习模型、神经网络模型中全部通用而且不会过时。接着往下走你只需要做三件扩展就能自然过渡到多层感知机MLP再进一步过渡到深度学习第一把单层线性模型换成多层的组合中间插入非线性激活函数模型表达能力大幅提升这就接近神经网络的雏形了。第二把均方误差换成交叉熵损失函数把线性输出换成 softmax 概率分布这就走向了分类任务的世界。第三把全连接结构换成卷积层或注意力机制就逐步靠近 CNN、Transformer 这些现代深度学习模型了。现在的重点就是先把基础夯实后面每一步才会走得更稳。最后分享一点个人心得带过不少新人之后我发现愿意花时间把基础模块搞透的人后面遇到复杂模型时反而学得更快。因为他们看到的不再是一堆抽象的公式和网络结构而是“数据进来→经过一系列变换→产生损失→反向传播更新”这条主线的不同组合形式。保持对基础原理的好奇心坚持动手调试每一步都会成为你之后深度学习路上重要的支点。
返回列表