☰
可视化梯度下降:从损失曲面到PyTorch训练循环的实践指南
2026/10/15 14:45:57 网站建设 项目流程

简介:《Deep Learning with PyTorch Step-by-Step》是Daniel Voigt Godoy撰写的深度学习入门教程,2022年更新至v1.1.1版,适合具备一定编程基础、希望系统掌握PyTorch框架的初学者及转行AI领域的读者。全书以循序渐进的方式组织,从神经网络基本原理、激活函数、损失函数等基础概念讲起,逐步覆盖PyTorch环境配置、数据预处理、模型构建与训练,并通过图像分类、自然语言处理等实战案例演示完整项目流程;同时涉及迁移学习、生成对抗网络(GANs)、注意力机制等进阶主题。书中配有大量可运行的代码示例和清晰图解,帮助读者在理解理论的同时快速上手实践,建立从入门到进阶的完整学习路径。该资源为单个PDF文件,大小约33.58MB,内容完整、目录清晰,便于按章节查阅或系统研读。目前已有216人浏览学习,适合作为自学或教学辅助材料。

1. 这本 PyTorch 入门书为什么先讲可视化梯度下降:从损失曲面到第一个回归模型

拿到《Godoy -- Deep Learning with PyTorch Step-by-Step -- 2022》时,我以为又是一本从 MNIST 分类开始的 PyTorch 入门书。翻到目录才发现路径完全反过来:开篇用整章可视化梯度下降,先在二维损失曲面上讲清楚学习率太大、特征没归一化会出什么状况,之后才进入第一个回归问题。

这本书适合两类人:一类是刚学过 Python、想系统建立深度学习直觉的初学者;另一类是跑过不少模型、但 loss 不降时只能靠玄学调参的熟手。实际翻下来,它最大的价值不是教你某个 API,而是把 Tensor、Autograd、Optimizer 在一次迭代里各自干什么拆得明明白白。下面按配环境、跑案例、踩坑的顺序,把这本书真正值得动手的部分过一遍。

2. 先把环境一次配好:Conda 虚拟环境、PyTorch 与三件可视化工具

2.1 为什么从 Conda 虚拟环境开始而不是直接用 pip

书的 Setup Guide 部分明确推荐 Anaconda 加 Conda 虚拟环境,而不是把依赖直接装进系统 Python。这里有一个很实际的原因:PyTorch 的 CUDA 版和 CPU 版依赖解析差异很大,混在不同项目里很容易把环境搞坏。我一般会给每本深度学习的书单独开一个环境,避免和手头其他项目互相污染。

这本书把环境配置放在正文之前,还专门写了 FAQ。我建议即使是熟手也别跳过这部分,它的配置清单本身就是一张依赖关系表:TorchVision 负责数据集,TensorBoard 负责训练曲线,GraphViz 负责画计算图。理解每个包在后续章节里的角色,比机械执行安装命令更有用。

常见做法是 conda create 一个新环境,激活后装 PyTorch,再补上 Jupyter、TensorBoard、GraphViz。书里提到"如果你的环境已经乱了,重新建一个环境是最快的后悔药",这个建议在实操里确实管用,我至少靠这招救回过两次环境。

2.2 安装步骤与版本选择

书里给了三种跑代码的方式:在线 notebook、临时会话环境、本地安装。我复现时选了本地安装,因为要配合 TensorBoard 和保存训练中间结果。下面是按这本书推荐顺序整理出来的最小安装流程:

conda create -n pytorch_book python=3.9 -y conda activate pytorch_book conda install pytorch torchvision torchaudio cpuonly -c pytorch -y pip install jupyter tensorboard conda install -c conda-forge python-graphviz pip install torchviz

先说版本策略:这本书是 2022 年的 v1.1.1 修订版,示例基于当时的 PyTorch 稳定版本。我安装时没有锁死版本号,直接让包管理器解析到 2.x 系列,书里绝大多数 API(Tensor、nn.Module、optim)在 2.x 下都兼容,只有极少数默认行为有差异,后面避坑章会提到。

参数说明:cpuonly 这个关键字很重要,它告诉 conda 只拉 CPU 版,避免把 CUDA 相关的几百兆依赖装进来;如果你的机器有 NVIDIA 显卡,把 cpuonly 换成 pytorch-cuda=12.1 即可,具体版本以显卡驱动支持为准。python=3.9 是兼容性最稳的选择,3.10 以上也能用,但如果有老版本的 torchviz 依赖,还是 3.9 省心。

也有人问能不能不装 Anaconda,直接 pip install torch?可以,但不建议。这本书的配套代码是大量 notebook,需要 jupyter、matplotlib、torchviz、tensorboard 联动,用 pip 一个个补齐容易遇到版本冲突。我从 pip 路线切回 conda 路线花了半小时,从那以后凡是跟着书做项目,一律先建环境。

2.3 TensorBoard、GraphViz 与 Jupyter:书里依赖的可视化三件套

这本书和市面上其他 PyTorch 教程有一个明显差异:它大量使用图来做教学。第 0 章用损失曲面、梯度向量、等高线图讲梯度下降;讲 Autograd 时用动态计算图展示 forward 和 backward 的路径。这两部分分别依赖 matplotlib、GraphViz 和 torchviz。

包在书里的用途安装方式
torch / torchvision模型构建、训练、数据集加载conda install
tensorboard训练曲线与参数分布可视化pip install
graphviz + torchviz计算图绘制conda install + pip install
jupyternotebook 运行环境pip install

我自己的体会是,如果你跳过这些可视化部分只看文字,会丢掉这本书大约三分之一的信息量。损失曲面里学习率从 0.2 调到 1.0 再到 5.0 的那些图,比任何文字描述都直观。所以环境里一定得装 GraphViz,而且要装对。pip install graphviz 只是装了 Python 绑定,系统里还得有 dot 命令,用 conda install -c conda-forge python-graphviz 一次性解决,是我踩过坑之后的推荐做法。

Jupyter 这边有一个容易忽略的细节:不要在 base 环境里启动 jupyter,然后在 notebook 里手动选内核,那样内核指向可能是错的。正确做法是激活目标环境后再启动:

conda activate pytorch_book jupyter notebook

这样 notebook 默认就跑在当前环境里,import torch 一定成功。另外,书里的 3D 曲面图在部分 Linux 服务器上默认后端显示不了交互图,如果遇到空白窗口,先试着把 matplotlib 后端切换成 TkAgg 或 Agg 再跑,本地 Windows 和 macOS 通常没有这个问题。

3. 把书拆成三条可复现的主线:可视化梯度下降、五步回归、PyTorch 训练循环

3.1 先看见"玄学":损失曲面、学习率与特征尺度

书第 0 章的标题直接叫 Visualizing Gradient Descent。它用最简单的线性模型 y = wx + b 造合成数据,然后画出损失曲面。这章最重要的不是代码,而是几个结论:当学习率偏低时,参数轨迹缓慢接近最优点;当学习率过高时,loss 出现震荡;当特征尺度差异大时,损失曲面变得细长狭窄,梯度下降会走出"之"字形。

这个可视化实验的价值在于,它把过拟合、欠拟合、学习率这些容易变成玄学的概念,变成了肉眼可见的几何形状。我跑完这章之后,再回头看训练日志里的 loss 曲线,判断标准完全不一样了。loss 震荡不再靠猜,而是先去看学习率和特征分布。

书里还专门做了一个"坏特征"实验:把一个特征的数值范围放大十倍,损失曲面立刻被拉成一条峡谷。这个实验直接导向了后面的结论——训练前做标准化不是锦上添花,而是让梯度下降能正常工作的前置条件。

3.2 手动实现五步梯度下降:从随机初始化到参数更新

书在第 1 章用 NumPy 手动实现了一次线性回归的梯度下降,过程明确拆成五步:随机初始化参数、计算预测、计算损失、计算梯度、更新参数。后面不断重复这五步,直到损失收敛。这个骨架其实就是后来所有深度学习训练循环的原型,PyTorch 只是把其中几步自动化了。

import numpy as np def forward(x, w, b): return x * w + b def compute_loss(y_true, y_pred): return np.mean((y_true - y_pred) ** 2) # Step 0: 参数初始化 w, b = 0.5, -0.2 lr = 0.1 for epoch in range(200): # Step 1: 用当前参数预测 y_pred = forward(x_train, w, b) # Step 2: 计算均方误差 loss = compute_loss(y_train, y_pred) # Step 3: 对 MSE 手动求偏导 grad_w = np.mean(2 * (y_pred - y_train) * x_train) grad_b = np.mean(2 * (y_pred - y_train)) # Step 4: 沿负梯度方向更新 w -= lr * grad_w b -= lr * grad_b

这段代码里,梯度是利用均方误差的解析导数直接算出来的,没有用任何自动微分。它对理解 PyTorch 的 backward() 很关键。PyTorch 只是替代了 Step 3 的计算方式,整个训练流程并没有变。

参数说明:lr(学习率)决定每步迈多大,0.1 在这个合成数据上是一个比较稳的起点。grad_w 和 grad_b 是损失函数分别对 w 和 b 的偏导。这里有一个书中反复强调的点:如果 x_train 的数值很大,grad_w 会被同步放大,导致更新步过头,loss 不降反升。这就是为什么后面所有实验都要先做特征缩放。

3.3 四个核心抽象:Tensor、Autograd、Optimizer、nn.Module

书从第 1 章后半段开始进入 PyTorch,把四个核心抽象拆开讲。Tensor 是带 requires_grad 属性的数据容器;Autograd 负责在 backward() 时沿动态计算图自动算梯度;Optimizer 封装了更新参数的策略;nn.Module 把参数和 forward 逻辑组合成模型。

书里在讲 Autograd 时专门画了动态计算图。它强调的是:PyTorch 的计算图是每次 forward 时动态构建的,不是先定义再运行。这意味着你可以在同一轮迭代里根据条件改变计算路径,这也是理解 backward 行为的前提。

import torch import torch.nn as nn # 用 nn.Linear 定义一个单层线性模型 model = nn.Linear(1, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.1) loss_fn = nn.MSELoss() # 一次完整的训练迭代 model.train() optimizer.zero_grad() # 清空上一轮累积的梯度 y_pred = model(x_train) # forward loss = loss_fn(y_pred, y_train) loss.backward() # backward optimizer.step() # 更新参数

这里最容易被忽略的是 optimizer.zero_grad()。PyTorch 的 backward() 默认把梯度累加到 .grad 上,而不是覆盖。如果某一轮忘掉 zero_grad,梯度会一直累积,参数更新步长越来越大,loss 曲线会出现断崖式跳变。这本书在讲 Autograd 时专门强调了这个行为,我从那以后写训练循环都固定按 zero_grad、backward、step 的顺序来。

参数说明:nn.MSELoss() 默认对 batch 内所有样本求均值,返回标量,正好满足反向传播的要求。SGD 的三个核心参数是参数集合、学习率、可选 momentum,这里没设 momentum。如果要加,常规值是 0.9,能让收敛更平稳,但书里这个阶段刻意不用,目的是先看清梯度下降本身的行为。

3.4 数据准备、模型配置、训练三步走:把流程整理成工程结构

书在"把一切拼起来"那一节里,把完整训练流程分成三部分:数据准备、模型配置、模型训练。它刻意不把代码写成一个大杂烩,而是像工程一样分阶段组织。我跟着这个结构完整跑完一个回归模型后,最大的感受是:调试模型时先确定是数据问题、模型结构问题还是训练循环问题,比盲目改参数有效得多。

# Data Preparation from sklearn.model_selection import train_test_split import torch x_all = torch.linspace(-1, 1, 100).reshape(-1, 1) y_all = 2 * x_all + 1 + 0.1 * torch.randn_like(x_all) x_tr, x_val, y_tr, y_val = train_test_split( x_all, y_all, test_size=0.2, random_state=42 ) # Model Configuration model = nn.Linear(1, 1) optimizer = torch.optim.SGD(model.parameters(), lr=0.1) loss_fn = nn.MSELoss() # Model Training def train_one_epoch(model, loss_fn, optimizer, x, y): model.train() optimizer.zero_grad() loss = loss_fn(model(x), y) loss.backward() optimizer.step() return loss.item() for epoch in range(100): train_loss = train_one_epoch(model, loss_fn, optimizer, x_tr, y_tr) if epoch % 10 == 0: print(f"epoch {epoch:3d} loss {train_loss:.4f}")

这里用了一个常见做法:把"一个 epoch 的训练"封装成函数。这样做的好处是后面引入验证集时,只需要再写一个验证函数,训练代码完全不用动。这本书虽然没有直接给出一套工程模板,但它把数据准备、模型配置、模型训练分节写,天然引导读者走向这种组织方式。

后面书里开始进入更复杂的内容,从图像分类、迁移学习一路讲到注意力机制相关的现代架构。但核心训练循环仍然是这三段式,只是模型和数据处理变得更复杂。基础阶段把这三段吃透,后面都是在里面填新模块。

4. 复现这本书的高频翻车点:五条踩坑记录

4.1 环境与设备问题:内核错位、import 失败、device 不一致

下面这些坑不是书本身的错误,而是从书里代码迁移到真实环境时最容易出的问题。书里每个 notebook 都是独立完整的,但在你自己的项目里复制粘贴时,环境状态、设备状态和梯度状态都不会自动重置。

坑一:装完环境后 import torch 找不到模块

现象:conda activate pytorch_book 后,在终端 import torch 正常,但打开 Jupyter 后 import torch 直接报 ModuleNotFoundError。

原因:Jupyter 是在 base 环境里启动的,内核指向的是 base 环境而不是 pytorch_book 环境。很多初学者习惯先打开 Jupyter,再在里面切换内核,但内核列表里根本没有新环境,导致 import 失败。

解决:先激活目标环境再启动 Jupyter,或者手动给当前环境注册内核。

conda activate pytorch_book python -m ipykernel install --user --name pytorch_book --display-name "PyTorch Book"

这条命令把当前 Python 环境注册成 Jupyter 内核,以后再启动 Jupyter,内核列表里就能直接选到它。我遇到这个问题时卡了将近二十分钟,最后发现只是启动顺序错了。

坑二:GPU 上训练时提示 device 不一致

现象:模型和数据都调用了 .to(device),但仍然报错,提示所有张量不在同一个设备上。

原因:通常不是模型漏了 .to(device),而是损失计算里有一个操作数没跟上。比如输入 x 转到 GPU 了,标签 y 还留在 CPU;或者某个中间变量用了 .cpu(),后续计算又把它和 GPU 张量混在一起。

解决:把所有参与前向传播的对象统一迁移,并打印一次设备位置做确认。

device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = model.to(device) x_tr = x_tr.to(device) y_tr = y_tr.to(device)

4.2 训练逻辑问题:梯度累积、特征尺度、计算图被截断

坑三:loss 刚开始下降,几十轮之后突然爆到一个很大的值

现象:训练到第 30 轮左右,loss 从 0.03 突然跳到非常大,之后一直不恢复。

原因:反向传播的梯度是累加的,漏掉了 optimizer.zero_grad()。这个坑在手动梯度下降部分不会出现,因为手动更新是直接覆盖;但切到 PyTorch 的 optimizer 后,梯度默认累加,忘掉清零就会累积成非常大的更新步。

解决:养成固定顺序,zero_grad()、backward()、step()。最可靠的方法是把这三行写进一个函数里固定复用,不要每次手敲。我后来检查自己的代码,凡是在循环里临时拼这三行的,基本都犯过错。

注意:backward() 的梯度累加行为是设计如此,不是 bug。PyTorch 这么做是为了支持梯度累积训练,但初学阶段几乎用不到,忘了清零就是纯粹的坑。

坑四:手动梯度下降时 w 和 b 更新幅度不一致,收敛很慢

现象:同样的学习率,b 很快就接近最优值,w 却一直微调,或者反过来。

原因:特征 x 的数值范围远超 1,导致 loss 对 w 的梯度被放大;而 b 的梯度只由残差决定,两者不在一个量级。这就是书里专门讲特征缩放的原因。

解决:把特征缩放到均值为 0、标准差为 1。书里推荐使用标准化的做法,我在合成数据实验里手动做也够用:

mean = x_train.mean() std = x_train.std() x_train_norm = (x_train - mean) / std x_val_norm = (x_val - mean) / std

注意验证集要使用训练集算出来的均值和标准差,不能拿验证集自己的统计量重新算,否则验证结果会有偏差。

坑五:backward() 报错,提示某张量不需要梯度

现象:loss.backward() 抛出异常,提示张量不需要梯度。

原因:模型参数设置了 requires_grad=True,但计算图里有一步被截断了。最常见的情况是从一个 .detach() 后的向量继续计算 loss,或者输入数据本身被误当成了可训练参数。

解决:检查模型参数是否真的挂在计算图上。我一般用 model.parameters() 逐个打印 requires_grad 属性,排查哪个参数被 detach 掉了;如果是输入数据不该参与梯度,重点检查 forward 代码里有没有残留的 detach()。这类问题在从书里代码复制到自己项目时尤其容易出现,因为书里每个 notebook 是独立干净的,你粘过来的代码可能带着前面实验的遗留操作。

5. 读完之后可以马上做的三件事:把书里示例整理成自己的训练模板

5.1 固定训练与验证两个阶段

书里最后一个核心流程是数据准备、模型配置、模型训练的三段式。我读完后的第一件事,就是在此基础上补了一个验证函数,把两个阶段的差异固定下来。

def train_step(model, loss_fn, optimizer, x, y): model.train() optimizer.zero_grad() loss = loss_fn(model(x), y) loss.backward() optimizer.step() return loss.item() def validate_step(model, loss_fn, x, y): model.eval() with torch.no_grad(): loss = loss_fn(model(x), y) return loss.item()

两个函数的核心差别只有两处:model.train() 与 model.eval(),以及是否用 torch.no_grad() 关闭梯度追踪。训练阶段需要计算梯度来更新参数;验证阶段不需要梯度,反而要关掉以减少内存占用。eval() 还会影响 Dropout 和 BatchNorm 的行为,这也是为什么验证阶段必须显式切换,不能只靠 no_grad()。

5.2 用 state_dict 保存与加载模型

这类教程容易忽略但实际很实用的技巧,是用 state_dict 保存模型参数:

torch.save(model.state_dict(), "model.pt") model.load_state_dict(torch.load("model.pt"))

state_dict 只存参数和 buffer,不依赖完整对象结构。读这本书之前,我一直把整个 model 对象直接保存,换环境后经常因为类定义不一致而读不回来。state_dict 是纯参数文件,只要模型结构一致,任何环境都能加载,这是我后来所有项目的默认做法。

5.3 让 TensorBoard 记录每次实验

书里环境配置阶段就装好了 TensorBoard,但训练部分的代码主要用 print 打日志。我读到后面开始自己加曲线记录,发现提前把接口留好,后面调参省很多事。给之前的训练循环加两行就能记录:

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter("runs/regression_lr0.1") # 在每个 epoch 后 writer.add_scalar("train_loss", train_loss, epoch)

TensorBoard 记录的 loss 曲线和打印出来的数字不一样,它能看到整段趋势,包括训练初期是否震荡、后期是否进入平台区。从那以后我每次新开一个实验,第一件事就是先建好 SummaryWriter,哪怕只是顺手记一条 loss 曲线,也比事后对着控制台滚动日志猜过程强。这个习惯一直保留到现在,希望帮到你。

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

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

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

立即咨询