PT深度学习训练避坑指南:省10小时+成功率翻倍

在跨境电商的汪洋大海中,数据就是咱们的罗盘,而深度学习框架PyTorch,正是咱们提升运营效率、精准触达用户的利器。新媒网跨境获悉,越来越多出海人正借力PyTorch,深挖数据价值,让生意更智能。
PyTorch作为目前增长最快的深度学习框架之一,其“Pythonic”的风格让咱们用起来感觉特别顺手,就像在写普通Python代码一样自然。用它来搭建和训练模型,不仅效率高,据说心情也会好起来,哈哈!
为什么这篇教程值得你花时间一看?
市面上PyTorch教程不少,官方文档也详尽得很。那为啥咱们还要花时间看这篇呢?原因很简单:很多教程要么过于理论,要么跳跃性太强。而这篇,咱们就是从实战出发,一步一个脚印,手把手带你入门,帮你透彻理解PyTorch的核心机制——比如它的自动求导、动态计算图、模型类等等——还会告诉你如何避开一些新手常犯的坑。咱们要的是学完就能上手,干货满满。
这篇教程内容有点长,不过别担心,我已经帮你理好了思路,你可以把它当成一个迷你实战课程,按部就班地学,一个主题一个主题地消化。
内容概览:
- 回归问题实战
- 数据准备与生成
- 梯度下降:核心思想
- Numpy实现线性回归:打基础
- 深入PyTorch世界
- 张量:数据载体
- 数据加载、设备与CUDA:效率关键
- 参数创建:训练的基石
- Autograd:自动求导的魔力
- 动态计算图:灵活应变
- 优化器:省心省力
- 损失函数:衡量好坏
- 模型搭建:结构化思维
- 训练循环:模型成长之路
- 数据集与数据加载器:高效管理数据
- 模型评估:检验成果
- 实战总结与前瞻
一、回归问题实战:从简单开始
很多教程喜欢用花哨的图像分类问题开场,看起来酷炫,但往往容易让咱们新手迷失方向,搞不清PyTorch究竟是怎么运作的。所以,咱们这次先不玩虚的,从一个最简单、最熟悉的线性回归问题入手:只有一个特征 x 的线性回归!这不能再简单了,对吧?
咱们的目标就是找到 a 和 b 这两个参数,让模型能够尽可能准确地预测 y。
$y=a+bx+\epsilon$
二、数据准备与生成:模拟真实场景
在实际业务中,咱们的数据往往是真实的销售额、点击率、转化率等等。这里为了演示方便,咱们先来模拟一些数据。咱们生成100个数据点,x 是咱们的特征,y 是标签。设定 a=1,b=2,再加点随机噪声,这样就模拟出了带有不确定性的真实数据。
接着,咱们要把这些模拟数据分成训练集和验证集。训练集用来“教”模型学习规律,验证集则用来检验模型学得好不好,看看它有没有“作弊”——也就是有没有过拟合。
import numpy as np
# 数据生成,让模型学着预测
np.random.seed(42) # 固定随机种子,保证每次运行结果一致,这是好习惯!
x = np.random.rand(100, 1) # 生成100个x特征值
y = 1 + 2 * x + .1 * np.random.randn(100, 1) # 根据y=1+2x+噪声 生成y标签
# 打乱数据索引,模拟真实数据随机性
idx = np.arange(100)
np.random.shuffle(idx)
# 划分训练集和验证集
train_idx = idx[:80] # 前80个用于训练
val_idx = idx[80:] # 剩下的20个用于验证
# 生成训练集和验证集数据
x_train, y_train = x[train_idx], y[train_idx]
x_val, y_val = x[val_idx], y[val_idx]

咱们都知道真实的 a=1,b=2。现在,咱们就用这80个训练数据点,通过梯度下降的方法,看看模型能学到多接近真实值的 a 和 b。
三、梯度下降:核心思想,模型“学习”的秘诀
我知道,一提到梯度下降,一些老铁可能头就大了,觉得数学太枯燥。别急,咱们今天不玩深奥的理论,只抓核心,手把手带你理解它的“灵魂”所在。把它想成是爬山下坡,咱们的目标是找到最低点,也就是模型的最佳状态。
梯度下降的整个过程可以拆解为5个基本步骤。
1. 随机初始化:从“蒙”开始
在真实世界里,咱们不可能一开始就知道模型参数应该是什么。所以,训练模型的第一步,就是随机给参数 a 和 b 赋初值,就像盲人摸象一样,先蒙一个起点。
比如,咱们可以从正态分布中随机抽取两个值,作为 a 和 b 的初始值。
$a=0.49671415$
$b=−0.1382643$
2. 模型预测(前向传播):看看“蒙”得有多离谱
有了初始参数,咱们就能用模型和训练数据 x 来做第一次预测了。
$\hat{y_i} = a + bx_i$
当然,这些初始预测值肯定非常离谱。毕竟,一个随机初始化的模型,能做出多准确的预测呢?“那到底有多离谱呢?” 这就需要咱们的“损失”来告诉咱们了。
3. 计算损失(Loss):量化“离谱”的程度
对于回归问题,咱们通常用均方误差(MSE)来衡量损失。简单来说,就是把所有预测值 $\hat{y_i}$ 和真实值 y_i 之间的差值平方后取平均。这个值越高,说明模型预测得越差;值越低,说明预测得越好。如果损失是零,那就意味着模型完美无瑕,但这在实际中几乎不可能。
$MSE = \frac{1}{N}\sum_{i=1}^N(y_i-\hat{y_i})^2 = \frac{1}{N}\sum_{i=1}^N(y_i-a-bx_i)^2$
这里要划个重点:如果咱们用训练集所有的数据点(N个)来计算损失并更新参数,这叫“批量梯度下降”(Batch Gradient Descent)。如果每次只用一个数据点,那就是“随机梯度下降”(Stochastic Gradient Descent)。如果每次用一小批数据(n个),那叫“小批量梯度下降”(Mini-batch Gradient Descent)。咱们这次用的是批量梯度下降。
4. 计算梯度(反向传播):指明“下坡”方向
梯度,其实就是损失函数对每个参数的偏导数。它告诉咱们,如果某个参数稍微变化一点点,损失函数会如何变化。咱们有两个参数 a 和 b,所以要计算两个偏导数。
$\frac{\delta{MSE}}{\delta{a}} = -2\frac{1}{N}\sum_{i=1}^N(y_i-\hat{y_i})$
$\frac{\delta{MSE}}{\delta{b}} = -2\frac{1}{N}\sum_{i=1}^Nx_i(y_i-\hat{y_i})$
梯度的方向和大小非常重要。如果梯度是正的,说明增大这个参数会让损失增大;如果梯度是负的,说明增大这个参数会让损失减小。咱们的目标是减小损失,所以如果梯度为正,咱们就要减小参数;如果梯度为负,咱们就要增大参数。简单来说,就是沿着梯度的反方向走,就能让损失减小。
5. 更新参数:沿着方向“下坡”
最后一步,就是根据计算出的梯度来更新参数。因为咱们要让损失最小化,所以更新方向与梯度方向相反。这里还有一个重要的参数:学习率(用希腊字母 $\eta$ 表示),它是一个乘数因子,控制着咱们每次“下坡”的步子大小。步子太大容易“冲过头”,步子太小又会“磨蹭”很久。
$a = a - \eta\frac{\delta{MSE}}{\delta{a}}$
$b = b - \eta\frac{\delta{MSE}}{\delta{b}}$
学习率的选择是个大学问,这里咱们先不深入,知道它的作用就行。
6. 循环往复:持续“下坡”,直到收敛
现在,咱们用更新后的参数,重新回到步骤1(其实是步骤2:做预测)开始新一轮的计算。这个从预测到计算损失,再到计算梯度、更新参数,完成一轮就算一个“epoch”。批量梯度下降中,一个epoch就是一次参数更新。小批量梯度下降中,一个epoch可能包含多次参数更新。
把这个过程重复几百甚至几千次,模型就会一点点地学习,参数也会不断优化,最终预测效果越来越好,这也就是模型训练的本质。
四、Numpy实现线性回归:打好基础,看清痛点
好啦,理论说了一堆,咱们现在用纯Numpy来实现咱们的线性回归模型和梯度下降过程。等等,不是说好的PyTorch教程吗?没错,但这么做有两个原因:
- 建立框架: 咱们先用Numpy搭好模型的骨架,这个结构在PyTorch里依然适用,能帮你更好地理解流程。
- 感受痛点: 亲手写一遍Numpy版的梯度下降,你就能真切感受到其中的复杂和繁琐,这样再学PyTorch,你才能真正体会到它给咱们带来的便利,才能感受到它的强大!
模型训练,通常有两类初始化:
- 参数/权重初始化: 咱们只有
a和b两个参数,这里随机初始化(第3、4行)。 - 超参数初始化: 比如学习率
lr和训练的轮次n_epochs(第9、11行)。
记住,一定要设置随机种子(np.random.seed(42)),这是保证实验结果可复现性的黄金法则。
对于每个epoch,训练过程包括四个步骤:
- 前向传播: 计算模型的预测值(第15行)。
- 计算损失: 根据预测值和真实标签,计算模型有多“离谱”(第18、20行)。
- 反向传播: 计算每个参数的梯度(第23、24行)。
- 参数更新: 根据梯度和学习率调整参数(第27、28行)。
请留意,如果不是批量梯度下降(就像咱们这个例子),你可能还需要一个内循环,来处理每个数据点(随机梯度下降)或者每批数据点(小批量梯度下降)。小批量梯度下降咱们后面会碰到。
# 初始化参数"a"和"b",随机赋初值
np.random.seed(42) # 同样,固定随机种子
a = np.random.randn(1) # 第3行
b = np.random.randn(1) # 第4行
print("初始化后的参数 a, b:", a, b)
# 设置学习率
lr = 1e-1 # 第9行
# 定义训练轮次(epoch数量)
n_epochs = 1000 # 第11行
for epoch in range(n_epochs):
# 步骤1:计算模型的预测输出(前向传播)
yhat = a + b * x_train # 第15行
# 步骤2:计算模型有多“错”(误差)
error = (y_train - yhat) # 第18行
# 这是回归问题,所以计算均方误差 (MSE) 作为损失
loss = (error ** 2).mean() # 第20行
# 步骤3:计算参数"a"和"b"的梯度
a_grad = -2 * error.mean() # 第23行
b_grad = -2 * (x_train * error).mean() # 第24行
# 步骤4:使用梯度和学习率更新参数
a = a - lr * a_grad # 第27行
b = b - lr * b_grad # 第28行
print("梯度下降后的参数 a, b:", a, b)
# 验证一下:和Scikit-Learn的线性回归结果对比,看咱们计算的是否正确
from sklearn.linear_model import LinearRegression
linr = LinearRegression()
linr.fit(x_train, y_train)
print("Scikit-Learn计算的截距(a), 系数(b):", linr.intercept_, linr.coef_[0])
为了确保咱们的代码没有出错,咱们用大名鼎鼎的Scikit-Learn库来跑一下线性回归,对比一下系数。
# a 和 b 随机初始化后的值
初始化后的参数 a, b: [0.49671415] [-0.1382643]
# 咱们梯度下降1000轮后的 a 和 b
梯度下降后的参数 a, b: [1.02354094] [1.96896411]
# Scikit-Learn 计算出的截距和系数
Scikit-Learn计算的截距(a), 系数(b): [1.02354075] [1.96896447]
可以看到,两者结果精确到小数点后6位都基本一致——这说明咱们用Numpy实现梯度下降的线性回归完全正确!现在,是时候“点燃”PyTorch了!
五、深入PyTorch世界:让模型训练更轻松高效
首先,咱们得聊几个核心概念,这些是PyTorch的基石,搞懂了才能玩转它。在深度学习里,“张量”无处不在,Google的框架都叫TensorFlow了,可见其重要性。那张量到底是个啥?
1. 张量(Tensor):数据的新名片
在Numpy里,咱们有数组(array),有0维(标量)、1维(向量)、2维(矩阵),再往上就是多维数组。广义上说,这些多维数组其实就是张量。所以,为了简单起见,咱们可以把向量和矩阵也统称为张量——从现在开始,除了单个数字叫标量,其他所有数据咱们都称之为张量。
2. 数据加载、设备与CUDA:效率提升的秘密武器
“怎么把Numpy的数组变成PyTorch的张量呢?” 别急,torch.from_numpy() 方法就是干这个的。它会把Numpy数组转换成CPU上的张量。
“但我有高性能GPU啊,想用它来加速!” 没问题,to() 方法就是你的利器。它可以把张量发送到你指定的设备,包括你的GPU(通常表示为 cuda 或 cuda:0)。
“万一我的电脑没GPU,我想让代码自动回退到CPU运行怎么办?” PyTorch也考虑到了这一点,cuda.is_available() 会告诉你有没有可用的GPU,然后你就可以根据情况设置 device 了。此外,你还可以用 float() 方法将张量转换为32位浮点数类型,这也是深度学习常用的精度。
import torch
import torch.optim as optim
import torch.nn as nn
from torchviz import make_dot # 可视化计算图的工具
# 智能判断当前设备:有GPU就用CUDA,没有就用CPU
device = 'cuda' if torch.cuda.is_available() else 'cpu'
print(f"当前使用的设备是: {device}")
# 咱们Numpy的数据现在需要转换成PyTorch的张量,并发送到指定设备
x_train_tensor = torch.from_numpy(x_train).float().to(device)
y_train_tensor = torch.from_numpy(y_train).float().to(device)
# 咱们来看看区别——注意 .type() 方法更实用,它能告诉你张量“住”在哪儿 (设备信息)
print("Numpy数组类型:", type(x_train))
print("PyTorch张量类型:", type(x_train_tensor))
print("PyTorch张量具体类型和所在设备:", x_train_tensor.type())
对比一下变量类型,Numpy数组是 numpy.ndarray,PyTorch张量是 torch.Tensor。但你的张量究竟“住”在CPU还是GPU呢?光看 torch.Tensor 是不知道的。而 x_train_tensor.type() 会清晰地告诉你,比如 torch.cuda.FloatTensor,这就表明它是个GPU张量。
反过来,想把PyTorch张量变回Numpy数组也很简单,用 .numpy() 方法就行。但这里有个小陷阱:
TypeError: can't convert CUDA tensor to numpy. Use Tensor.cpu() to copy the tensor to host memory first.
Numpy可不认识GPU上的张量。你得先用 .cpu() 方法把GPU张量挪回到CPU上,它才能愉快地转换成Numpy数组。
3. 参数创建:模型“智慧”的源泉
那用于数据的张量和用于模型可训练参数/权重的张量有什么区别呢?关键就在于后者需要计算梯度,这样咱们才能根据梯度来更新参数值。requires_grad=True 这个参数就是告诉PyTorch:“嘿,这个张量很重要,请帮我记录它的计算历史,我后面要用它来算梯度!”
你可能觉得,就像处理数据张量一样,先创建一个普通张量,然后用 to(device) 方法把它发送到指定设备就行了,对吧?别急,这里有坑!
# 方法一:先创建普通张量
# 咱们像Numpy里一样随机初始化参数"a"和"b",但这里要特别注意!
# 因为要对这些参数进行梯度下降,所以必须设置 REQUIRES_GRAD = TRUE
a = torch.randn(1, requires_grad=True, dtype=torch.float)
b = torch.randn(1, requires_grad=True, dtype=torch.float)
print("CPU上创建的参数 a, b:", a, b)
这段代码确实创建了两个漂亮的张量作为咱们的参数,也设置了需要计算梯度。但它们是CPU张量。
CPU上创建的参数 a, b: tensor([-0.5531], requires_grad=True) tensor([-0.7314], requires_grad=True)
咱们试试把它送到GPU上:
# 方法二:先创建,再 to(device),但这样会“丢失”梯度信息!
# 如果咱们想在GPU上运行,直接to(device)行不行呢?
a = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
b = torch.randn(1, requires_grad=True, dtype=torch.float).to(device)
print("先requires_grad=True再to(device)的参数 a, b:", a, b)
# 很遗憾,这样不行!to(device)操作会“遮蔽”掉原先的梯度记录,导致梯度信息丢失...
在第二段代码中,咱们尝试了这种“天真”的做法,结果虽然成功地把张量送到了GPU,但却“丢失”了梯度信息。你看 grad_fn=<CopyBackwards>,说明它是个拷贝操作,而不是直接可求导的叶子节点。
先requires_grad=True再to(device)的参数 a, b: tensor([0.5158], device='cuda:0', grad_fn=<CopyBackwards>) tensor([0.0246], device='cuda:0', grad_fn=<CopyBackwards>)
那如果先送到GPU,再设置 requires_grad 呢?
# 方法三:先 to(device),再使用 in-place 操作设置 requires_grad
# 咱们可以先创建普通张量,然后发送到设备(就像处理数据一样)
a = torch.randn(1, dtype=torch.float).to(device)
b = torch.randn(1, dtype=torch.float).to(device)
# 然后再把它们设置为需要计算梯度...
a.requires_grad_()
b.requires_grad_()
print("先to(device)再requires_grad_()的参数 a, b:", a, b)
第三段代码中,咱们先将张量送到设备上,然后再用 requires_grad_() 方法原地设置 requires_grad 为True。
先to(device)再requires_grad_()的参数 a, b: tensor([-0.8915], device='cuda:0', requires_grad=True) tensor([0.3616], device='cuda:0', requires_grad=True)
PyTorch里,所有以 _ 结尾的方法(比如 requires_grad_())都是原地操作(in-place),意味着它们会直接修改底层的变量。
虽然最后一种方法奏效了,但最推荐的做法是:在创建张量时就直接指定设备和 requires_grad!
# 推荐做法:在创建张量时就直接指定设备——最佳实践!
torch.manual_seed(42) # 再次固定随机种子
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print("推荐方式创建的参数 a, b:", a, b)
推荐方式创建的参数 a, b: tensor([0.1940], device='cuda:0', requires_grad=True) tensor([0.1391], device='cuda:0', requires_grad=True)
是不是简单多了?
现在咱们知道了如何创建需要计算梯度的张量,接下来就看看PyTorch是如何处理它们的——这就是“自动求导”的魅力。
六、Autograd:PyTorch的“魔术师”,告别手动求导!
Autograd是PyTorch的自动微分包。有了它,咱们就不用再手动计算偏导数、链式法则那些复杂的数学了,PyTorch会像“魔术师”一样替咱们搞定一切。
那怎么告诉PyTorch去计算所有梯度呢?答案就是 backward() 方法。还记得咱们计算梯度的起点是什么吗?是损失函数!因为咱们要对损失函数求偏导。所以,咱们只需要对表示损失的Python变量调用 loss.backward() 方法就行了。
那实际的梯度值在哪里呢?咱们可以通过张量的 .grad 属性来查看。不过,这里有个细节:.grad 属性会累加梯度!所以,每当咱们用梯度更新完参数后,就得把梯度清零,以免下次计算时混淆。清零的方法就是调用 zero_() 方法。还记得方法名后面带 _ 是什么意思吗?(如果忘了,可以往上翻翻看哦!)
好,现在咱们抛弃手动计算梯度,直接用 backward() 和 zero_() 方法。就这么简单吗?嗯,差不多,但参数更新的时候还有个小“陷阱”!
lr = 1e-1
n_epochs = 1000
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print("Autograd初始化的参数 a, b:", a, b)
for epoch in range(n_epochs):
# 步骤1:前向传播,计算预测值
yhat = a + b * x_train_tensor
# 步骤2:计算损失
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# 告别手动计算梯度了!
# a_grad = -2 * error.mean()
# b_grad = -2 * (x_tensor * error).mean()
# 步骤3:告诉PyTorch,从这个损失开始,给我反向传播,计算所有需要梯度的参数的梯度!
loss.backward()
# 咱们可以看看计算出来的梯度...
# print(a.grad)
# print(b.grad)
# 那参数怎么更新呢?这里有坑!
# 第一次尝试:直接像Numpy那样赋值更新
# AttributeError: 'NoneType' object has no attribute 'zero_'
# a = a - lr * a.grad
# b = b - lr * b.grad
# print(a)
# 第二次尝试:使用in-place操作更新
# RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.
# a -= lr * a.grad
# b -= lr * b.grad
# 第三次尝试:我们需要用torch.no_grad() 来阻止PyTorch的梯度追踪
# 为什么呢?这就涉及到PyTorch的“动态计算图”了...
# 步骤4:在 no_grad() 上下文中更新参数,不让PyTorch追踪这一步
with torch.no_grad():
a -= lr * a.grad
b -= lr * b.grad
# 更新完参数后,别忘了把梯度清零,否则下次计算会累加!
a.grad.zero_()
b.grad.zero_()
print("Autograd梯度下降后的参数 a, b:", a, b)
第一次尝试,咱们如果像Numpy代码那样直接 a = a - lr * a.grad 重新赋值,你会得到一个奇怪的错误 AttributeError: 'NoneType' object has no attribute 'zero_'。这是因为重新赋值后,PyTorch“丢失”了 a 和 b 的梯度计算历史,导致 .grad 属性变成了 None。
第二次尝试,咱们改用Python里常见的原地赋值 a -= lr * a.grad。结果PyTorch又报错了 RuntimeError: a leaf Variable that requires grad has been used in an in-place operation.。这又是怎么回事?!
这其实是PyTorch“好心办坏事”了。PyTorch厉害的地方在于,它会根据每一次涉及到需要计算梯度的张量(或其依赖)的Python操作,动态地构建一个计算图。如果你直接修改了这些张量,它就会“懵了”,不知道怎么追踪梯度了。咱们下一节会详细讲“动态计算图”。
那怎么告诉PyTorch:“退一步海阔天空,让我安静地更新参数,别瞎掺和我的计算图”呢?答案就是 torch.no_grad()。它提供了一个上下文,在这个上下文里的张量操作,PyTorch都不会去构建计算图,也不会追踪梯度。
最终,咱们成功地运行了模型,得到的参数和Numpy版本计算出来的一模一样!
# 第三次尝试:成功!
Autograd梯度下降后的参数 a, b: tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)
七、动态计算图:PyTorch的“思维导图”
“很不幸,没人能告诉你什么是动态计算图,你必须亲自去看。”
—— 墨菲斯
《黑客帝国》是不是很棒?言归正传,我也想让你亲眼看看这个计算图长啥样!PyTorchViz 库和它的 make_dot(variable) 方法能帮咱们轻松可视化与Python变量相关联的计算图。咱们就用最简单的模型:两个(需要计算梯度的)参数张量、预测值、误差和损失。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
下图展示了分别对 yhat、error 和 loss 变量调用 make_dot() 函数后得到的计算图:
咱们来仔细看看这些图的构成:
- 蓝色方框: 这些是咱们的模型参数,也就是那些咱们要求PyTorch计算梯度的张量。
- 灰色方框: 这些是涉及到需要计算梯度的张量或其依赖的Python操作。
- 绿色方框: 和灰色方框类似,但它代表了梯度计算的起始点(假设
backward()方法是从这个变量开始调用的)——梯度计算在图中是从下往上进行的。
如果咱们绘制 error (中间) 和 loss (右边) 变量的计算图,它们和第一个图的区别仅仅是中间步骤(灰色方框)的数量。
再仔细看看最左边 yhat 图的绿色方框:有两支箭指向它,因为它是一个加法操作, a 和 b*x 相加。这很直观对吧?然后看同一张图的灰色方框:它是一个乘法操作 b*x。但只有一个箭头指向它!这个箭头来自咱们的参数 b 对应的蓝色方框。那咱们的数据 x 对应的方框去哪了?答案是:咱们不需要计算 x 的梯度!所以,尽管在计算图中涉及了更多的张量操作,但它只会显示那些需要计算梯度的张量及其依赖关系。
如果咱们把参数 a 的 requires_grad 设置为 False 会怎么样?
a_nograd = torch.randn(1, requires_grad=False, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat2 = a_nograd + b * x_train_tensor
make_dot(yhat2)

不出所料,对应参数 a 的蓝色方框消失了!很简单:不需要梯度,也就不会出现在计算图中。
动态计算图最棒的地方在于它的灵活性。你可以让它变得像你想要的一样复杂。你甚至可以在代码中使用控制流语句(比如 if 语句)来控制梯度的流动(很显然是这样!):-)
下图就展示了这样一个例子。当然,这个计算本身没有任何实际意义,只是为了演示。
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
yhat = a + b * x_train_tensor
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# 如果损失大于0,就额外增加一部分损失——这只是为了演示动态图
if loss > 0:
yhat2 = b * x_train_tensor
error2 = y_train_tensor - yhat2
loss += error2.mean() # 损失 += 额外误差的平均值
make_dot(loss)

八、优化器(Optimizer):让参数更新更省心!
到目前为止,咱们都是手动根据计算出的梯度来更新参数。对于只有两个参数的模型来说,这可能还行。但如果咱们的模型有成千上万个参数,甚至更多,那手动更新简直就是噩梦!
这时,PyTorch的优化器就派上大用场了!像SGD(随机梯度下降)、Adam等都是常见的优化器。优化器会接收咱们需要更新的参数、学习率(可能还有其他超参数),然后通过它的 step() 方法来执行参数更新。
更棒的是,有了优化器,咱们也不需要再一个个地手动清零梯度了。只需要调用优化器的 zero_grad() 方法,所有参数的梯度就都清零了!
在下面的代码中,咱们创建一个随机梯度下降(SGD)优化器来更新参数 a 和 b。别被它的名字迷惑了,如果咱们在每次更新时都使用了所有的训练数据,那么即使优化器名叫SGD,它执行的其实也是批量梯度下降。
torch.manual_seed(42)
a = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
b = torch.randn(1, requires_grad=True, dtype=torch.float, device=device)
print("优化器初始化参数 a, b:", a, b)
lr = 1e-1
n_epochs = 1000
# 定义一个SGD优化器来更新参数
# 咱们把需要优化的参数 [a, b] 传给优化器,并指定学习率
optimizer = optim.SGD([a, b], lr=lr)
for epoch in range(n_epochs):
# 步骤1:前向传播,计算预测值
yhat = a + b * x_train_tensor
# 步骤2:计算损失
error = y_train_tensor - yhat
loss = (error ** 2).mean()
# 步骤3:反向传播,计算梯度
loss.backward()
# 告别手动更新参数了!
# with torch.no_grad():
# a -= lr * a.grad
# b -= lr * b.grad
# 步骤4:使用优化器来更新参数!一行代码搞定所有参数的更新
optimizer.step()
# 告别手动清零梯度了!
# a.grad.zero_()
# b.grad.zero_()
# 优化器自带清零方法,更方便
optimizer.zero_grad()
print("优化器梯度下降后的参数 a, b:", a, b)
咱们来看看优化前后的两个参数 a 和 b,确保一切正常:
# 优化器初始化参数 a, b
优化器初始化参数 a, b: tensor([0.1940], device='cuda:0', requires_grad=True) tensor([0.1391], device='cuda:0', requires_grad=True)
# 优化器梯度下降后的 a, b
优化器梯度下降后的参数 a, b: tensor([1.0235], device='cuda:0', requires_grad=True) tensor([1.9690], device='cuda:0', requires_grad=True)
太棒了!咱们把优化过程也给优化了,效率再次提升!接下来,咱们看看损失函数的处理。
九、损失函数(Loss):模型好坏的“裁判”
现在,咱们来处理损失函数的计算。不出所料,PyTorch再次为咱们提供了全面的支持。根据不同的任务,我们可以选择多种损失函数。因为咱们做的是回归问题,所以使用的是均方误差(MSE)损失。
【风险前瞻与时效提醒】
各位跨境实战的老铁们,学到这里,咱们已经掌握了PyTorch的核心操作。但技术迭代飞快,PyTorch框架本身也在不断更新。在实际业务中,务必关注官方文档的最新动态,确保代码兼容性与安全性。同时,数据合规性是跨境业务的生命线,即使是模拟数据,也要培养数据隐私和伦理意识。本教程基于当前主流版本(2026年),但理解其核心思想和实战方法,将帮助大家以不变应万变,驾驭未来挑战。新媒网跨境认为,持续学习和适应变化,是每一位跨境人的核心竞争力。
新媒网(公号: 新媒网跨境发布),是一个专业的跨境电商、游戏、支付、贸易和广告社区平台,为百万跨境人传递最新的海外淘金精准资讯情报。
本文来源:新媒网 https://nmedialink.com/posts/pytorch-dl-pitfalls-save-10h-boost-success.html


粤公网安备 44011302004783号 











