☰
PyTorch回归入门实战:从零搭建模型与训练循环
2026/10/16 18:17:59 网站建设 项目流程

1. 从一个最简单的回归任务说起

很多刚接触深度学习的朋友,打开教程第一眼看到的就是分类任务,手写数字识别、猫狗大战之类的。但我觉得,真正适合拿来入门的第一个模型,其实是回归。原因很简单:回归的输出是一个连续值,你能直观地看到预测值和真实值差了多少,损失曲线长什么样,梯度有没有在正常工作。分类任务里你只能看到一个准确率百分比,中间发生了什么,对新手来说反而更模糊。

这篇文章要聊的就是用PyTorch搭一个最基础的回归模型。不搞花哨的,就是一条直线拟合、一个二次函数拟合这种级别的小项目。适合谁看?如果你已经装好了PyTorch,知道张量是什么,但还没自己完整写过训练循环,那这篇就是给你准备的。如果你连环境都还没配好,建议先把PyTorch装上再回来,后面的内容会更容易消化。

我见过太多人学深度学习的方式是:打开一个开源项目,代码几百行,模型结构复杂得要命,训练脚本里还夹杂着分布式、混合精度、各种回调。看完之后只有一个感觉——每个字都认识,连起来不知道在干嘛。回归任务的好处就在于,它把所有干扰项都去掉了,你面对的就是最核心的三件事:定义模型、定义损失、更新参数。把这三件事搞明白了,后面再去看复杂模型,心里就有底了。

2. 为什么回归是入门PyTorch的最佳切入点

2.1 回归任务的本质:从数据中找映射关系

回归这个词听起来有点学术,说白了就是找规律。给你一堆数据点,让你找一条线或者一个曲线去描述这些点的走向,这就是回归。比如你有一组房屋面积和对应价格的数据,想根据面积预测价格,这就是一个典型的回归问题。输入是面积,输出是价格,两者之间不是简单的线性关系,但可以用一个模型去逼近。

从数学角度看,回归就是在假设输出和输入之间存在某种函数关系,然后用数据去估计这个函数的参数。最简单的形式就是线性回归:y = wx + b。w是权重,b是偏置,这两个就是我们要学的参数。模型训练的过程,就是不断调整w和b,让预测值尽可能接近真实值。

PyTorch做这件事的流程非常清晰:先把数据转成张量,然后定义一个模型(哪怕只是一个线性层),接着定义损失函数衡量预测和真实的差距,最后用优化器根据梯度更新参数。这个流程在所有深度学习任务里都是通用的,回归只是把它简化到了最容易理解的程度。

2.2 PyTorch在回归任务上的核心优势

PyTorch最大的好处是动态计算图。什么意思呢?就是你写代码的时候,跟写普通Python程序几乎一样,该用if用if,该用for用for,不需要提前把整个计算图定义好。这对调试来说太重要了。你可以随时打印中间变量的值,可以在任意位置打断点,可以动态改变模型结构。相比之下,静态图框架在调试时就麻烦得多,你得先把图编译好,然后才能跑,中间想看个变量值还得额外配置。

另一个优势是自动求导。你只需要定义前向传播怎么算,反向传播PyTorch帮你搞定。对于回归任务来说,这意味着你不需要手动推导梯度公式,也不需要写任何跟求导相关的代码。你只需要调用loss.backward(),PyTorch会自动计算所有参数的梯度。这对新手来说极大地降低了门槛,你可以把精力集中在理解模型结构和训练流程上,而不是被数学推导卡住。

还有一点是PyTorch的生态。torch.nn模块提供了各种层和损失函数,torch.optim提供了各种优化器,torch.utils.data提供了数据加载工具。你不需要从零实现这些东西,直接调用就行。但更重要的是,这些模块的源码都是开放的,你随时可以点进去看它到底是怎么实现的。这种透明度对于学习来说非常有价值。

2.3 一个最小可运行示例能带来什么

我始终认为,学习深度学习最快的方式是先跑通一个最小示例,然后在这个基础上不断修改和扩展。你不需要一开始就理解所有细节,先让代码跑起来,看到损失在下降,看到预测结果在变好,这种正反馈会让你有动力继续深入。

一个最小回归示例大概长这样:生成一些带噪声的数据点,定义一个线性模型,用均方误差作为损失函数,用随机梯度下降作为优化器,然后写一个训练循环。代码量不大,但涵盖了深度学习的完整流程。跑通之后,你可以尝试改模型结构,把线性层换成多层感知机;可以改损失函数,试试L1损失;可以改优化器,换成Adam;可以改学习率,观察收敛速度的变化。每一次修改都是一次学习。

而且回归任务的结果非常直观。你可以把原始数据点和拟合的直线画在同一张图上,一眼就能看出拟合得好不好。损失曲线也很容易理解,下降就是好事,震荡或者上升就说明有问题。这种直观性对于建立直觉非常有帮助。

3. 核心概念拆解:张量、模型、损失与优化器

3.1 张量操作:回归任务的数据载体

张量是PyTorch里最基本的数据结构,你可以把它理解成多维数组。标量是0维张量,向量是1维张量,矩阵是2维张量,更高维的就是张量。在回归任务里,输入数据通常是2维张量,形状是(样本数, 特征数)。比如你有100个房屋样本,每个样本只有面积一个特征,那输入张量的形状就是(100, 1)。

创建张量的方式有很多种。可以从Python列表创建,可以用torch.zeros或torch.ones创建全零或全一的张量,可以用torch.randn创建服从标准正态分布的随机张量。在回归任务里,我们通常需要自己生成一些模拟数据,这时候torch.randn和torch.linspace就很有用。

张量的运算和NumPy非常像,加减乘除、矩阵乘法、广播机制都支持。但PyTorch张量比NumPy数组多了一个关键属性:requires_grad。如果一个张量的requires_grad=True,PyTorch会追踪所有基于它的操作,构建计算图,以便后续自动求导。在回归任务里,模型的参数需要设置requires_grad=True,而输入数据通常不需要。

还有一个重要的概念是设备。张量可以放在CPU上,也可以放在GPU上。如果要用GPU加速,需要把模型和数据都移到GPU上。对于入门级的回归任务来说,CPU完全够用,数据量小,模型简单,GPU加速的收益不明显。但了解这个概念是必要的,后面处理更大规模的数据时会用到。

3.2 模型定义:从线性层到自定义网络

PyTorch定义模型的标准方式是继承nn.Module类,然后在__init__里定义层,在forward里定义前向传播的逻辑。对于最简单的线性回归,只需要一个nn.Linear层就够了。nn.Linear(in_features, out_features)会创建一个全连接层,内部自动初始化权重和偏置。

但实际做回归任务时,输入特征往往不止一个。比如预测房价,可能用到面积、房间数、楼层、房龄等多个特征。这时候in_features就等于特征数。输出通常是一个值,所以out_features=1。如果要做多输出回归,比如同时预测房价和租金,那out_features就设为2。

nn.Linear的权重初始化默认是均匀分布,范围是[-1/sqrt(in_features), 1/sqrt(in_features)]。这个初始化策略在大多数情况下够用,但如果你发现训练初期损失下降很慢,可以尝试手动初始化,比如用正态分布或者Xavier初始化。不过对于入门级的回归任务,默认初始化通常不会有什么问题。

除了nn.Linear,PyTorch还提供了很多其他层,比如卷积层、循环层、注意力层。但在回归入门阶段,我建议先用线性层把流程跑通,不要急着上复杂结构。等你对训练循环、损失函数、优化器都熟悉了,再去尝试更复杂的模型。

3.3 损失函数:衡量预测与真实的差距

回归任务最常用的损失函数是均方误差,也就是MSE。它的计算方式是把每个样本的预测值和真实值相减,平方,然后求平均。平方的作用有两个:一是让正负误差都变成正数,避免相互抵消;二是放大大误差的惩罚,让模型更关注那些预测得特别离谱的样本。

PyTorch里用nn.MSELoss()就可以创建MSE损失。调用的时候传入预测值和真实值,它会自动计算并返回一个标量张量。这个标量就是损失值,后续调用backward()就是基于这个值来计算梯度的。

除了MSE,回归任务还常用L1损失,也就是平均绝对误差。L1损失对异常值更鲁棒,因为它是线性的,不会像MSE那样放大异常值的影响。但L1损失在零点不可导,可能会给优化带来一些麻烦。实际应用中,MSE用得更多,因为它的数学性质更好,梯度计算更稳定。

还有一种损失叫Smooth L1损失,结合了MSE和L1的优点,在零点附近用MSE,远离零点用L1。PyTorch里也有对应的实现。对于入门来说,先用MSE把流程跑通,等遇到具体问题时再考虑换损失函数。

3.4 优化器:让参数朝着正确的方向更新

优化器的作用是根据梯度来更新模型参数。最基础的优化器是随机梯度下降,也就是SGD。它的更新规则很简单:参数 = 参数 - 学习率 * 梯度。学习率控制每次更新的步长,太大容易震荡,太小收敛慢。

PyTorch里用torch.optim.SGD来创建SGD优化器,需要传入模型参数和学习率。除了SGD,还有Adam、RMSprop、Adagrad等优化器。Adam结合了动量和自适应学习率,在大多数任务上表现都不错,而且对学习率不那么敏感。对于入门级的回归任务,我通常建议先用Adam,学习率设0.01或0.001,看看效果。

优化器的使用流程是固定的:在训练循环里,先调用optimizer.zero_grad()清空梯度,然后计算损失,调用loss.backward()计算梯度,最后调用optimizer.step()更新参数。这个顺序不能乱,尤其是zero_grad()必须在backward()之前调用,否则梯度会累加。

学习率的设置是一个经验活。太大容易震荡,太小收敛慢。对于回归任务,如果数据已经标准化过,学习率设0.01通常是个不错的起点。如果没标准化,可能需要更小的学习率。你可以尝试几个不同的值,观察损失曲线的变化,找到最合适的那个。

4. 完整实操:从零搭建一个回归模型

4.1 数据准备:生成模拟数据与真实数据加载

先来看怎么生成模拟数据。假设我们要拟合一个二次函数y = 2x^2 + 3x + 1,加上一些噪声。用PyTorch可以这样写:

import torch import torch.nn as nn import torch.optim as optim # 生成数据 torch.manual_seed(42) x = torch.linspace(-3, 3, 200).unsqueeze(1) # 形状(200, 1) y = 2 * x**2 + 3 * x + 1 + torch.randn(x.size()) * 2 # 加噪声

这里用linspace生成-3到3之间的200个点,unsqueeze(1)把形状从(200,)变成(200, 1),因为模型期望输入是2维的。y是在真实函数值上加了一些高斯噪声,模拟真实世界的数据不完美。

如果是真实数据,通常需要做标准化。标准化的目的是让数据分布更接近标准正态分布,这样梯度下降更容易收敛。标准化的公式是:(x - mean) / std。对于回归任务,输入和输出都建议标准化。输入标准化帮助模型更快收敛,输出标准化让损失值在合理范围内,避免梯度过大或过小。

数据加载方面,如果数据量不大,直接全部放进张量里做全批量训练就行。如果数据量大,需要用DataLoader分批加载。DataLoader可以打乱数据、并行加载、自动分批,用起来很方便。但对于入门级的回归任务,全批量训练足够了,代码更简单,更容易理解。

4.2 模型搭建:一个线性层够不够用

对于二次函数拟合,一个线性层肯定不够,因为线性层只能拟合直线。我们需要至少一个隐藏层,加上非线性激活函数。最简单的方案是:线性层(1, 10) -> ReLU -> 线性层(10, 1)。这样模型就有能力拟合非线性关系了。

class RegressionModel(nn.Module): def __init__(self): super().__init__() self.net = nn.Sequential( nn.Linear(1, 10), nn.ReLU(), nn.Linear(10, 1) ) def forward(self, x): return self.net(x)

nn.Sequential是一个容器,把层按顺序串起来,forward里直接调用就行。隐藏层大小设为10是个经验值,太小拟合能力不足,太大容易过拟合。对于这个简单任务,10足够了。

如果你要拟合的是线性关系,那一个nn.Linear(1, 1)就够了。但实际任务中,输入和输出的关系往往是非线性的,所以至少加一个隐藏层和激活函数。激活函数除了ReLU,还可以用Tanh、Sigmoid、LeakyReLU等。ReLU计算简单,梯度不会饱和,是默认选择。

4.3 训练循环:每一步到底在做什么

训练循环的代码看起来简单,但每一步都有明确的目的:

model = RegressionModel() criterion = nn.MSELoss() optimizer = optim.Adam(model.parameters(), lr=0.01) for epoch in range(1000): # 前向传播 y_pred = model(x) loss = criterion(y_pred, y) # 反向传播 optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 100 == 0: print(f'Epoch {epoch}, Loss: {loss.item():.4f}')

前向传播就是让数据流过模型,得到预测值。然后计算损失,衡量预测和真实的差距。zero_grad()清空上一轮的梯度,因为PyTorch默认会累加梯度。backward()计算当前损失对所有参数的梯度。step()根据梯度更新参数。

这个顺序是固定的,不能乱。特别是zero_grad(),如果忘了调用,梯度会一直累加,导致更新方向错误。我见过不少新手在这里踩坑,损失不下降反而上升,排查半天才发现是忘了清空梯度。

训练轮数设多少?这个没有标准答案。对于这个简单任务,1000轮足够了。你可以观察损失曲线,如果损失已经平稳了,再训练也不会有明显改善。如果损失还在下降,可以继续训练。实际项目中通常会用早停法,当验证集损失不再下降时停止训练。

4.4 结果可视化:用matplotlib看拟合效果

训练完之后,把原始数据点和模型预测的曲线画在一起,直观地看拟合效果:

import matplotlib.pyplot as plt model.eval() with torch.no_grad(): y_pred = model(x) plt.scatter(x.numpy(), y.numpy(), s=10, label='True Data') plt.plot(x.numpy(), y_pred.numpy(), 'r-', linewidth=2, label='Prediction') plt.legend() plt.show()

model.eval()把模型切换到评估模式,主要是影响Dropout和BatchNorm层的行为。with torch.no_grad()关闭梯度计算,节省内存和计算资源。这两个操作在推理阶段是标准做法。

从图上你能直观地看到模型有没有抓住数据的整体趋势。如果预测曲线和真实数据点贴合得很好,说明模型学到了正确的映射关系。如果偏差很大,可能需要调整模型结构、学习率或者训练轮数。

除了拟合曲线,还可以画损失曲线,观察训练过程中损失的变化。损失曲线通常用对数坐标,因为损失值可能跨越好几个数量级。如果损失曲线平滑下降然后趋于平稳,说明训练正常。如果震荡剧烈,可能是学习率太大。如果一直不下降,可能是模型结构有问题或者学习率太小。

5. 踩坑实录与常见问题排查

5.1 损失不下降的几种可能原因

损失不下降是新手最常遇到的问题。根据我的经验,原因通常有这几个:

学习率太大。这是最常见的原因。学习率太大时,参数更新步长过大,损失会在最小值附近震荡甚至发散。解决办法是减小学习率,比如从0.01降到0.001或0.0001。你可以尝试几个不同的值,观察损失曲线的变化。

学习率太小。这个相对少见,但也会发生。学习率太小时,参数更新非常缓慢,损失下降得很慢,看起来像是不下降。解决办法是增大学习率,或者换用Adam这种自适应优化器。

模型结构不合理。如果模型太简单,拟合能力不足,损失降到一定程度就降不下去了。比如用线性模型拟合二次函数,无论怎么训练,损失都有一个下限。解决办法是增加模型复杂度,加隐藏层或者增加隐藏层大小。

数据没有标准化。如果输入数据的范围很大,比如在0到10000之间,梯度可能会非常大,导致训练不稳定。解决办法是对数据进行标准化,让均值为0,标准差为1。

梯度消失或爆炸。深层网络中容易出现这个问题,但即使是浅层网络,如果激活函数选择不当,也可能出现。解决办法是换激活函数,比如用ReLU代替Sigmoid,或者加BatchNorm层。

5.2 过拟合与欠拟合的判断与处理

过拟合是指模型在训练集上表现很好,但在验证集上表现很差。欠拟合是指模型在训练集和验证集上表现都不好。判断方法是把数据分成训练集和验证集,分别计算损失。

对于回归任务,如果训练损失持续下降但验证损失开始上升,说明过拟合了。解决办法有:增加数据量、减小模型复杂度、加正则化(L1或L2)、加Dropout层、早停。

如果训练损失和验证损失都居高不下,说明欠拟合了。解决办法有:增加模型复杂度、增加训练轮数、减小正则化强度、检查数据是否有问题。

对于入门级的回归任务,数据量通常不大,模型也很简单,过拟合和欠拟合都不太严重。但了解这些概念是必要的,后面处理真实任务时会经常遇到。

5.3 梯度相关问题的排查思路

梯度问题是深度学习中最难排查的一类问题。常见的有梯度消失和梯度爆炸。

梯度消失是指梯度在反向传播过程中越来越小,导致浅层参数几乎不更新。表现是损失下降非常慢,或者完全不下降。解决办法包括:使用ReLU激活函数、使用BatchNorm、使用残差连接、减小网络深度。

梯度爆炸是指梯度变得非常大,导致参数更新步长过大,损失震荡或发散。解决办法包括:梯度裁剪、减小学习率、使用BatchNorm、调整权重初始化。

排查梯度问题的一个实用技巧是打印梯度范数。在训练循环里加一行代码,打印每层参数的梯度范数,观察它们的变化。如果梯度范数接近0,说明梯度消失;如果梯度范数非常大,说明梯度爆炸。

for name, param in model.named_parameters(): if param.grad is not None: print(f'{name} grad norm: {param.grad.norm().item():.6f}')

这个技巧在调试复杂模型时特别有用,能帮你快速定位问题出在哪一层。

5.4 常见问题速查表

问题现象可能原因排查方法解决办法
损失不下降学习率太大观察损失曲线是否震荡减小学习率
损失不下降学习率太小观察损失下降速度增大学习率或换Adam
损失不下降模型太简单检查模型结构增加隐藏层或神经元
损失震荡学习率太大打印梯度范数减小学习率
损失发散梯度爆炸打印梯度范数梯度裁剪、减小学习率
训练损失低但验证损失高过拟合对比训练和验证损失加正则化、早停、增数据
训练和验证损失都高欠拟合检查模型复杂度增加模型复杂度
预测值全是同一个数模型崩溃检查输出层调整初始化、减小学习率

6. 从回归入门到更复杂的任务

6.1 回归任务的变体与扩展

掌握了基础回归之后,你可以尝试一些变体。多输出回归是其中一个,比如同时预测多个目标值。实现方式很简单,把输出层的out_features改成目标数量就行。损失函数还是MSE,它会自动对所有输出求平均。

另一个变体是分位数回归。普通回归预测的是条件均值,分位数回归预测的是条件分位数。比如预测房价的10%分位数、50%分位数、90%分位数,这样你能得到一个预测区间,而不仅仅是一个点估计。PyTorch里可以通过自定义损失函数来实现分位数回归。

还有一种是贝叶斯回归,它不仅给出预测值,还给出预测的不确定性。实现方式是在模型输出均值和方差,然后用负对数似然作为损失函数。这样模型不仅能告诉你预测值是多少,还能告诉你对这个预测有多自信。

6.2 从回归到分类的平滑过渡

回归和分类看起来是两类任务,但本质上都是找映射关系,区别只在于输出空间不同。回归的输出是连续值,分类的输出是离散类别。从回归过渡到分类,主要变化在损失函数和输出层。

分类任务通常用交叉熵损失,输出层用Softmax把logits转换成概率分布。模型结构上,分类任务和回归任务可以完全一样,只是最后一层的输出维度等于类别数。训练循环也几乎一样,只是损失函数的调用方式略有不同。

如果你已经理解了回归的训练流程,切换到分类任务时,只需要改两三个地方:损失函数从MSE换成CrossEntropyLoss,输出维度从1换成类别数,评估指标从MSE换成准确率。其他部分基本不变。

6.3 后续学习路径建议

回归入门之后,我建议按这个顺序继续深入:先学分类任务,理解交叉熵损失和Softmax;然后学卷积神经网络,处理图像数据;接着学循环神经网络和Transformer,处理序列数据;最后学生成模型,比如GAN和扩散模型。

每个阶段都建议先跑通一个最小示例,然后在这个基础上修改和扩展。不要一上来就追求复杂模型,先把基础流程跑熟。PyTorch的官方教程和文档是最好的学习资源,里面的示例代码质量很高,而且持续更新。

另外,多动手写代码比多看教程重要得多。看教程的时候觉得什么都懂了,自己写的时候才发现到处都是问题。遇到问题不要怕,调试的过程就是学习的过程。我到现在还经常遇到各种奇怪的报错,排查问题的能力就是在一次次踩坑中练出来的。

最后分享一个小技巧:养成记录实验的习惯。每次调整超参数或者修改模型结构,都记录下来,包括改了什么、结果如何。这样你才能积累经验,知道什么改动有效,什么改动无效。我见过很多人调参全靠感觉,调了半天也不知道哪个参数起了作用。有记录的话,你就能快速定位到有效的改动,效率会高很多。

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

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

立即咨询