线性回归与PyTorch:梯度下降、损失函数实战入门深度学习
2026/9/24 20:41:37 网站建设 项目流程

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 构造带噪声的线性数据集

真实世界里几乎不存在完美的线性关系,数据总会带点噪声。为了让训练过程贴近实际情况,我们人工制造一份“带噪声的线性数据”,这样既能验证模型的拟合能力,又能直观地观察噪声对结果的影响。

制造数据的方式是:设定真实的 w=2、b=1,然后在线性关系之上叠加服从正态分布的随机噪声:

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_grad=True) b = torch.zeros(1, requires_grad=True) def linear_model(x): return x * w + b

这里的requires_grad=True是告知 PyTorch:这个张量是计算图中的叶子节点,需要计算并跟踪它的梯度。你可以把它理解成给这个变量贴了一个“要算梯度”的标签。

第二种写法是利用 PyTorch 的 nn 模块,更贴近工程实践:

import torch.nn as nn model = nn.Linear(in_features=1, out_features=1)

nn.Linear会在内部自动创建权重和偏置参数,并且已经做了正确的初始化。线性层的 weight 形状是 (out_features, in_features),bias 形状是 (out_features,)。这里输入输出都是 1,所以就是最简单的 y=wx+b 形式。

两种写法各有各的适用场景。第一种适合学习原理,你能看到参数的全部细节;第二种适合实际开发,代码简洁且扩展性好。至于nn.Sequential、多层的写法,本质上就是把多个nn.Linear串起来,大家先把这个单层结构吃透。

4.2 定义损失函数与优化器

损失函数我用nn.MSELoss(),这和我们前面推导的均方误差公式完全一致:

loss_fn = nn.MSELoss() optimizer = torch.optim.SGD(model.parameters(), lr=0.01)

优化器选择的是随机梯度下降(SGD)。这也是一个值得留意的地方:虽然AdamRMSProp这类自适应优化器用起来更“省心”,但做线性回归这种凸优化问题时,SGD 不仅够用,而且能让你更直观地理解梯度下降的本质。Adam 就是在梯度下降的基础上加了动态调整步长的机制,原理是不一样的。

model.parameters()会自动返回模型中所有需要更新的参数,这也是nn.Module比手工定义省心的地方。SGD 的lr=0.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(f'Epoch [{epoch+1}/{epochs}], Loss: {loss.item():.4f}')

很多初学同学会漏掉optimizer.zero_grad()这一步,这是最容易踩的坑。PyTorch 的反向传播会累积梯度到.grad属性中,如果不手动清零,第二次迭代的梯度会和第一次叠加。叠加之后更新方向就跑偏了,模型可能永远无法收敛。

完整训练循环就是一个简化版的深度学习“流水线”:前向传播拿预测结果 → 计算损失衡量好坏 → 反向传播算梯度 → 更新参数来优化好坏。四个步骤缺一不可,顺序也不能乱。

4.4 模型验证与数据可视化

训练完成后,我们最关心两个问题:模型参数是否逼近真实的 w=2、b=1?在验证集上的表现如何?

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, color='red', label='模型预测') plt.plot(x_plot.numpy(), true_w * x_plot.numpy() + true_b, color='green', linestyle='--', label='真实线') plt.legend() plt.xlabel('x') plt.ylabel('y') plt.show()

实测下来,用 lr=0.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 由 2x+1 加噪声生成时,lr=0.01 通常 150-200 轮可以稳定收敛;lr=0.1 就很容易震荡;lr=0.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 这些现代深度学习模型了。

现在的重点,就是先把基础夯实,后面每一步才会走得更稳。

最后分享一点个人心得:带过不少新人之后我发现,愿意花时间把基础模块搞透的人,后面遇到复杂模型时反而学得更快。因为他们看到的不再是一堆抽象的公式和网络结构,而是“数据进来→经过一系列变换→产生损失→反向传播更新”这条主线的不同组合形式。保持对基础原理的好奇心,坚持动手调试,每一步都会成为你之后深度学习路上重要的支点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询