☰
PyTorch训练全流程指南:张量、模型搭建与避坑实践
2026/10/14 1:56:02 网站建设 项目流程

简介:这份 PDF 是 Daniel Voigt Godoy 所著《Deep Learning with PyTorch Step-by-Step》2022 年版(v1.1.1),定位为深度学习初学者的系统性入门教程。全书按难度递进组织:先从神经网络原理、激活函数与损失函数等基础概念入手,再介绍 PyTorch 环境搭建、张量操作与数据管道,随后通过图像分类、自然语言处理等实战案例演示模型训练与评估流程,并延伸至迁移学习、生成对抗网络和注意力机制等高级主题。内容兼顾理论讲解与示例代码,附有目录、FAQ、附录等辅助材料,方便读者按需跳转与查漏补缺。压缩包共 1 个文件,纯 PDF 格式,大小 33.58MB。目前已有 216 人学习/下载,适合具备一定编程基础但尚未系统接触深度学习的读者,在章节化指引下一步步完成从理论到实践的首个项目。

1. 为什么一本 2022 年的 PyTorch 教程,到现在依然值得照做

一份 2022 年发布的 Deep Learning with PyTorch Step-by-Step 教程,放到今天还能不能用?先说结论:能,而且对初学阶段友好得不像是三年前的东西。整理者 ID 为 Godoy,他把深度学习训练流程拆成了从张量、自动求导、模型搭建到训练与可视化的完整链路,每一步都有可运行的代码。这份材料踩在一个很舒服的时间点上:记录的是 PyTorch 稳定版的最佳实践,避开了早期 1.x 的 API 混乱和 2.x 大改带来的文档断层,现在装 2.x 的 PyTorch 也基本兼容。它适合两类人:一类是刚补完 Python 基础、想系统走通一次训练闭环的初学者;另一类是已经跑通过几个脚本、但对梯度、DataLoader 和验证流程缺乏完整认识的从业者。这条路不是最快,但最稳。

2. 张量与自动求导:PyTorch Step-by-Step 的底层逻辑

这套教程一开始没有直接丢出一个神经网络,而是花了大量篇幅讲张量和自动求导。很多人觉得这部分枯燥,跳到后面搭模型,结果遇到梯度不更新、显存暴涨的问题时完全不知道去哪查。其实,训练循环里每一步都在和计算图打交道,只是平时被封装得看不见。把这一步补扎实,后面几乎不会再被 autograd 相关的坑绊倒。

2.1 从张量到计算图:动手之前先看懂 grad_fn

深度学习操作的是多维数组,PyTorch 里叫张量。张量除了存数据,还额外记录两个东西:是否需要梯度(requires_grad)以及这个数据是怎么算出来的(grad_fn)。后者是计算图的入口,也是 Step-by-Step 这份教程里反复出现的概念。

import torch x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y = x * 2 z = y.sum() print(x.grad_fn) # None,叶子节点没有来源 print(y.grad_fn) # <MulBackward0 object ...> print(z.grad_fn) # <SumBackward0 object ...> print(z.grad_fn.next_functions) # 能看到 z 的梯度来源是 y

代码里的逻辑很直白:x 是叶子张量,所以它没有 grad_fn;y 是由 x 乘 2 得到的,grad_fn 记录了这个乘法节点;z 是 y 求和的结果,grad_fn 记录求和节点。通过 next_functions 可以把这条链路逐级往上摸,看到整个前向过程是怎么连接起来的。

requires_grad=True 只应该加在需要求梯度的叶子张量上。中间变量和最终输出会自动带上 grad_fn,不需要手动设置。很多人在准备数据时直接对输入加 requires_grad,完全没必要,模型参数才是需要梯度的那一部分。这份教程把计算图前置来讲,就是希望你在写模型之前先建立“前向构建图、反向填梯度”的直觉,否则后面看 backward 报错信息都会一头雾水。

2.2 最小自动求导演练:标量梯度与向量 Jacobi 矩阵

能真正跑出梯度的最小代码只有三行。这个例子在教程里出现得很早,但值得反复玩。

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True) y = (x ** 2).sum() # y = x0^2 + x1^2 + x2^2 y.backward() # 标量可以直接 backward print(x.grad) # tensor([2., 4., 6.])

输出结果是 [2., 4., 6.],因为 y 对每个 xi 的偏导是 2xi,代入 x 的初始值恰好得到这个结果。backward() 执行时,PyTorch 从 y 出发沿计算图反向传播,把梯度填到叶子张量的 .grad 属性里。整个过程不需要你手动实现链式法则,但你需要知道梯度落到了哪里。

接着踩一个常见雷:对非标量输出直接 backward。

x = torch.tensor([1.0, 2.0], requires_grad=True) y = x ** 2 # 长度为 2 的向量 y.backward() # RuntimeError: grad can be implicitly created only for scalar outputs

原因在于 PyTorch 的 autograd 用的是向量-Jacobian 积,非标量输出必须显式传入一个和 y 同形状的权重向量。改成 y.sum().backward() 或 y.mean().backward() 都能绕开这个限制。实际训练里,损失函数最终都会汇聚成一个标量,所以这个问题在标准流程里很少触发;但你写自定义损失或对中间张量调 backward 时,就会撞上它。

这份教程的做法是先跑最小例程,再解释背后机制。不要嫌它慢——亲眼看到梯度落在 .grad 上,比背十遍反向传播公式都管用。学习门槛从数学公式转移到了“跑一下、改一下、看一下”,这是它对新手的核心价值。

2.3 环境怎么搭:Python 版本与安装通道

教程是 2022 年的,代码基于当时稳定版 PyTorch 的 API 写的。现在装 2.x 完全没问题,但要留意 Python 版本不能太新。我一般建议用 conda 建一个 Python 3.9 的独立环境,避免和系统其他项目互相污染依赖。

conda create -n pystep python=3.9 -y conda activate pystep pip install torch torchvision matplotlib tensorboard

安装命令看着简单,背后有个关键参数:torch 版本和 CUDA 版本的对应关系。如果机器有 NVIDIA 显卡,需要按官方提供的 index-url 安装对应 CUDA 版本的 wheel,常见做法是去 PyTorch 官网选配置后复制那一条 install 命令。CUDA 小版本不匹配时,torch.cuda.is_available() 会返回 False,而这一项在教程后续的 GPU 训练中几乎是第一个检查点。

torchvision 在教程进入图像分类时会用到,matplotlib 用来画训练前后的对比图,tensorboard 则是第 4 章监听训练曲线的主力。额外提醒一句:不要装 nightly 版本,教学材料的时间戳和 nightly 的 API 可能有差异,到时候文档对不上,很难判断是代码问题还是版本问题。

3. 用 nn.Module 搭模型:线性层、损失函数与优化器的组织方式

第 2 章把计算图讲清楚后,教程才进入模型搭建。这一步的核心不是背网络结构,而是理解 nn.Module 怎么把参数、前向逻辑和优化器串起来。理解了这一点,后面不管是换网络结构还是改训练方式,都会顺手很多。

3.1 从手写线性回归到 nn.Linear:边界在哪

教程第一个完整模型选了线性回归,并且先用手写参数的方式实现了一遍。

import torch class ManualLinear(torch.nn.Module): def __init__(self): super().__init__() self.w = torch.nn.Parameter(torch.randn(1)) self.b = torch.nn.Parameter(torch.zeros(1)) def forward(self, x): return x * self.w + self.b

Parameter 在这里的作用是告诉 PyTorch“这是可训练参数”。任何赋值给 Module 属性的 Parameter 都会被自动注册,之后通过 model.parameters() 就能拿到完整参数列表,优化器也靠这个接口工作。如果不用 Parameter 而是普通张量,参数不会被注册,optimizer 拿到的是空列表,训练时权重纹丝不动。

手写一遍之后,教程会切换到 nn.Linear(1, 1) 一行替代,效果等价。两者的边界在于:手写方式适合你理解 forward 里的乘法加偏置发生了什么;实际项目里我一般直接用 nn.Linear,它内部还处理了权重初始化,少操心一件细节。更复杂的结构会用到 Sequential:

model = torch.nn.Sequential( torch.nn.Linear(1, 8), torch.nn.ReLU(), torch.nn.Linear(8, 1) )

Sequential 适合层与层之间单向串联的简单网络。两个 Linear 中间的 ReLU 是非线性激活函数,没有它的话两层线性叠加还是线性,模型的表达能力不会比单层强。这份教程刻意先做线性回归再做带隐藏层的 MLP,就是让你亲眼看到“加深一层后效果发生了什么变化”。

3.2 损失函数与优化器选型:MSE、CrossEntropy 与 SGD/Adam

任务类型决定损失函数,优化器决定更新策略。教程把二者放在一起讲,因为它们共同决定了训练是否收敛。下面这张表是我在实际项目里的默认起点,和教程的推荐基本一致。

任务类型常用损失输出层激活优化器起点
回归MSELoss无SGD(lr=0.01) 或 Adam(lr=0.001)
二分类BCEWithLogitsLoss无Adam(lr=0.001)
多分类CrossEntropyLoss无(损失内部做 softmax)Adam(lr=0.001)

多分类这里有个高频误用:CrossEntropyLoss 内部已经包含 softmax 计算,模型输出层不需要再接 softmax,否则梯度会变得不稳定。教程里专门提了这一点,因为很多人习惯在最后一层加 softmax“把输出变成概率”,跟损失函数叠加后反而出问题。

优化器选型上,SGD 加动量适合从头理解梯度更新过程,Adam 适合快速收敛。我一般先用 SGD 跑通流程,再换 Adam 对比,看哪个收敛更稳。

optimizer = torch.optim.SGD(model.parameters(), lr=0.01, momentum=0.9) # 或 optimizer = torch.optim.Adam(model.parameters(), lr=0.001)

model.parameters() 必须传给优化器,优化器只更新其中 requires_grad=True 的参数。学习率没有万能值,但 0.01 是 SGD 常见起点,0.001 是 Adam 常见起点。如果 loss 完全不降,先检查学习率数量级,再检查数据和标签是否对得上,这两个方向覆盖了大部分训练不收敛的场景。

3.3 一个可复用的最小训练循环骨架

模型、损失、优化器都准备好了,接下来是最小训练循环。这个骨架会贯穿整份教程,后续所有变体都只是在这个框架上加东西。

def train_one_epoch(model, loader, loss_fn, optimizer): model.train() total_loss = 0.0 for xb, yb in loader: optimizer.zero_grad() # 清空上一步的梯度 pred = model(xb) # 前向计算 loss = loss_fn(pred, yb) # 计算损失 loss.backward() # 反向传播,把梯度填到参数上 optimizer.step() # 用梯度更新参数 total_loss += loss.item() return total_loss / len(loader)

每个参数都有明确职责:model.train() 让 Dropout 和 BatchNorm 进入训练行为;optimizer.zero_grad() 必须放在 forward 之前,否则上一步的梯度会累积到这一步,loss 曲线会出现奇怪的震荡;loss.item() 是把损失值从计算图里取出变成普通数字,避免整张图一直在内存里堆积。

这个循环常见的翻车点是漏掉 zero_grad,或者把它放在 backward 之后,造成梯度根本没更新。遇到 loss 不降,先检查这三行,再查形状,最后才去怀疑学习率。这套排查顺序能覆盖掉大部分训练启动阶段的异常。

4. 把训练跑完整:Dataset 加载、验证循环与 TensorBoard 监听

模型能在单个 batch 上跑通只是第一步。真正的训练流程需要把数据组织好、每个 epoch 结束后做一次验证、再用可视化工具观察曲线。教程到这里才把“深度学习训练闭环”完整展示出来,前几章都是在为这个闭环铺垫。

4.1 用 Dataset 与 DataLoader 喂数据:三条必改参数

教程从自带数据集切到自定义 Dataset,这段代码是后续所有数据加载的基础。

class MyDataset(torch.utils.data.Dataset): def __init__(self, x, y): self.x, self.y = x, y def __len__(self): return len(self.x) def __getitem__(self, idx): return self.x[idx], self.y[idx] loader = torch.utils.data.DataLoader( MyDataset(x, y), batch_size=32, shuffle=True, num_workers=2 )

Dataset 只要实现三个方法:len返回样本总数,getitem按索引返回一个样本对。DataLoader 负责把单个样本打包成 batch。里面有三个参数需要认真调:batch_size 影响收敛速度和显存占用,太大容易 OOM,太小收敛慢;shuffle 在训练集里必须设置为 True,否则每个 epoch 内样本顺序固定,模型可能学到顺序而不是特征;num_workers 决定数据加载用的子进程数,在 Windows 上经常需要设为 0,或者把构建 DataLoader 的代码放进 ifname== "main" 里,否则会触发多进程递归报错。

教程在这里强调了一个易漏点:不要在getitem里做重量级的读取或预处理。这个函数每个 epoch 会被调用整个数据集那么多次,里面做重活会直接把训练拖慢。常见做法是提前把所有数据加载到内存,getitem只做索引和类型转换。

4.2 训练/验证双循环怎么写才不是“假训练”

只跑训练循环很容易被表面的 loss 下降欺骗。过拟合发生时,训练 loss 会一路走低,但验证 loss 可能早已掉头向上。教程在每个 epoch 结束后插入一次验证,代码是一个独立的 evaluate 函数。

def evaluate(model, loader, loss_fn): model.eval() total_loss, correct, total = 0.0, 0, 0 with torch.no_grad(): for xb, yb in loader: pred = model(xb) total_loss += loss_fn(pred, yb).item() correct += (pred.argmax(dim=1) == yb).sum().item() total += yb.size(0) return total_loss / len(loader), correct / total

两个细节值得单独说明:model.eval() 切换的是 BatchNorm 和 Dropout 的行为,它和 torch.no_grad() 是两回事——前者管模型状态,后者管计算图构建。验证时既不需要梯度,也不需要 Dropout 的随机性,所以两个都要用。argmax 的作用是把模型输出的概率分布变成预测类别,再和真实标签比较计算准确率。

所谓“假训练”,就是训练循环和验证循环割裂——只在训练集上调参,拿训练 loss 当评估指标。真正要看的始终是验证 loss。如果验证 loss 在某一个 epoch 后开始回升,就该考虑提前停止、降低学习率或增加正则化,而这一步必须靠双循环才能暴露出来。

4.3 用 TensorBoard 看曲线:过拟合是“看”出来的

数值看多了会麻木,曲线一眼就能看出问题。教程引入 TensorBoard 做监听,核心代码只有几行。

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("runs/exp1") for epoch in range(epochs): train_loss = train_one_epoch(model, loader, loss_fn, optimizer) val_loss, val_acc = evaluate(model, val_loader, loss_fn) writer.add_scalar("loss/train", train_loss, epoch) writer.add_scalar("loss/val", val_loss, epoch) writer.add_scalar("acc/val", val_acc, epoch) writer.close()

add_scalar 的三个参数分别是标签、数值、step。step 用 epoch 就够了,不需要精细到 batch 级。训练完成后在终端执行 tensorboard --logdir runs,浏览器打开本地端口就能看到实时滚动的曲线。

判断曲线的一些经验:train loss 和 val loss 都停留在高位不下降,是欠拟合,需要加大模型容量或降低学习率重新跑;train loss 持续下降而 val loss 先降后升,是过拟合的经典形态;val loss 来回剧烈震荡,可能是学习率偏高或 batch size 偏小。教程里说 TensorBoard 不是必须的,也可以把指标写进 CSV 用 matplotlib 画,但实时滚动带来的反馈速度是事后画图比不了的。训练中途发现问题并停下来调参,比跑完再分析省太多时间。

5. PyTorch 训练避坑指南:五个高频翻车现场

这一章是教程之外我踩过的坑和帮别人排查时的经验汇总。每一条都按照现象、原因、解决的顺序写,方便你遇到问题时对照定位。

5.1 默认开启 grad 导致显存暴涨

现象:训练跑了一段时间后报 CUDA out of memory,但模型本身很小。

原因:最常见的是验证循环或推理阶段没有包在 torch.no_grad() 里。只要不在 no_grad 上下文里,每次前向都会构建计算图并保存中间张量,显存自然持续累积。另一种情况是输入数据在外部被手动设了 requires_grad=True,导致整条链路都在建图。

解决:推理和验证统一放进 with torch.no_grad() 块里;输入数据不要加 requires_grad。检查方法很简单,把验证代码里那一行 print(pred.requires_grad) 打出来,看到 True 就是这里的问题。

5.2 模型或数据没有 .to(device):CPU/GPU 隐式互拷

现象:报错信息里出现 expected device cuda:0 but got cpu,或者训练速度极慢但没报错。

原因:模型在 GPU 上,数据还在 CPU 上,PyTorch 尝试隐式转换失败;反过来也一样。最麻烦的是第二种——如果模型在 CPU 而数据在 GPU,有时不会直接报错,而是默默以龟速训练。

解决:在训练脚本开头固定设备,之后统一转移。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) for xb, yb in loader: xb, yb = xb.to(device), yb.to(device)

没有数据的设备转换,是我见过最多的新手错误。写一个统一函数处理 batch 的搬运,能省去后续所有重复代码。

5.3 训练/验证模式切换:Dropout 与 BatchNorm 的翻车现场

现象:训练和验证曲线都正常,模型导出后做推理,结果波动很大,同一份输入每次输出都不一样。

原因:推理时没有调用 model.eval()。Dropout 层在训练模式会随机关闭神经元,如果推理时还开着,每次前向都在用不同的子网络,结果自然不稳定。BatchNorm 更隐蔽,训练模式用的是当前 batch 的统计量,eval 模式才用全局累积的均值方差。

解决:验证和推理前必须加 model.eval(),再配合 torch.no_grad()。这个坑我在一次部署前遇到过,模型导出后预测结果几乎随机,最后定位到就是少了这一行。现在我把 model.eval() 写进所有推理脚本的第一行,形成肌肉记忆。

5.4 梯度被清零或 requires_grad 意外丢失

现象:loss 完全不下降,打印参数的 .grad 全是 None,或权重更新停在一个值上不动。

原因:optimizer.zero_grad() 被放在了 loss.backward() 之后,等于把刚算好的梯度立刻抹掉。另一个常见原因是中途对参数或特征做了 detach(),切断了反向传播路径,requires_grad 变成了 False。

解决:把 zero_grad() 固定放在 forward 之前,和 backward 之间隔开整个前向过程。想冻结某层参数时,用 param.requires_grad_(False) 而不是 detach(),前者保留参数身份,后者会彻底移出计算图。判断方法:跑一个 batch 后检查 model.parameters() 里是否还有 requires_grad=True 的项,以及每项 grad 是否为 None。

5.5 Python 版本与 PyTorch 版本不匹配的玄学错误

现象:pip 安装 torch 成功后,import torch 直接报错,显示 DLL load failed 或非法指令。

原因:Python 3.12 和新版 torch 之间偶发 ABI 兼容问题,或者装的是某个不匹配的预编译包。这类问题最容易被误判成代码问题,实际上在 import 阶段就挂了,完全没走到代码。

解决:用 conda 新建 Python 3.9 或 3.10 环境,再重新安装 PyTorch,优先从官方配置页生成的命令安装。装好后先跑一句 torch.cuda.is_available() 验证 CUDA 是否可用,再跑一句 print(torch.version) 确认版本,最后才打开训练脚本。环境问题优先级最高,因为它会污染对后续所有错误的判断。

6. 把模型固化下来:保存、导出与最后的验证习惯

训练完成后最后一步是把模型保存下来,供后续推理或部署使用。这个环节看起来简单,但保存姿势不对,换环境或改代码后模型可能直接加载失败。

6.1 保存与加载的推荐姿势

推荐只保存 state_dict,也就是参数字典,而不是整个模型对象。

torch.save(model.state_dict(), "model.pt") # 加载时先实例化同类模型,再灌入参数 model = LinearNet() model.load_state_dict(torch.load("model.pt")) model.eval()

state_dict 只存参数,体积小,跨代码版本兼容好;保存整个 model 用的是 pickle,代码结构一变就容易反序列化失败。加载后调用 model.eval() 是为了切到推理模式,避免 Dropout 和 BatchNorm 的行为干扰输出。

6.2 用脚本化或 ONNX 导出做推理验证

跨框架部署时,我一般用 torch.jit.trace 把模型固化成 TorchScript,或者导出 ONNX。

example_input = torch.randn(1, 1) traced = torch.jit.trace(model, example_input) traced.save("model_jit.pt")

trace 用示例输入跑一次前向,记录实际执行路径。它的问题是:遇到依赖数据值的控制流(比如 if x > 0 这种分支)会失效,因为 trace 只记录固定路径。这时需要改用 torch.script。如果目标是跨语言调用,可以导出 ONNX:torch.onnx.export(model, example_input, "model.onnx")。无论哪种方式,导出后都要跑一次推理,和原始模型对比输出是否一致。

6.3 我的收尾习惯

之前有一次,训练指标挺好,导出后推理效果却很奇怪。查了半天,最后定位到是验证模式没切回来,Dropout 在推理时还在随机丢弃神经元。那之后我给自己定了一个固定流程:先 load_state_dict 验证能复现训练指标,再 model.eval() 去验证集上抽 30 个样本人工过一眼,最后才导出 JIT 或 ONNX。

如果你正在跟着这份教程敲代码,把前五章的流程完整走完,模型达到可用水平后,导出前先写一个五行的 smoke test:固定随机种子、喂同一批输入、对比导出前后的输出差。这一步能省掉大量部署期的“玄学”报错,也能逼着你把整个训练流程真正搞清楚。希望帮到你。

本文还有配套的精品资源,点击获取

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

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

立即咨询