☰
《动手学深度学习2.0》PyTorch版:从环境配置到模型迁移的完整避坑指南
2026/10/15 6:49:12 网站建设 项目流程

简介:《动手学深度学习2.0-李沐Pytorch版》是一本面向深度学习初学者与研发人员的实战型教材,要求读者具备一定Python基础。全书从线性代数、微积分、概率与自动微分等预备知识讲起,逐步深入卷积神经网络、循环神经网络与注意力机制等前沿技术,并覆盖监督学习、非监督学习与强化学习等不同范式,最终延伸至计算机视觉与自然语言处理等应用场景。资源包内共1个PDF文件,大小约26.65MB,内容完整、目录结构清晰,便于按章节系统学习与检索。目前已有778人学习下载,适合希望借助PyTorch框架动手实现模型、理解神经网络结构与训练方法的读者,通过书中大量代码示例边学边练,逐步掌握从数据预处理到模型部署的核心技能。

1. 从零复现《动手学深度学习2.0》的最小闭环:为什么有人三天跑通,有人三周还在配环境

很多人第一次打开《动手学深度学习2.0》的在线书,看到“线性回归从零实现”那一节,觉得不过如此,结果卡在第一步——环境。不是 CUDA 版本对不上,就是 torch 装完 import 报 DLL 缺失,再不然就是 jupyter 里画不出图。这本书的定位很明确:它不是一本只讲公式的教材,而是一套“代码即讲义”的工程化课程,PyTorch 版把每个概念都落成可运行的 notebook。它适合两类人:一类是想系统补深度学习基础、但受不了纯理论推导的开发者;另一类是已经会用框架调包、但说不清反向传播到底在算什么的人。真正跑通一个最小闭环——装好环境、跑通第一个从零实现的线性回归、看到 loss 下降曲线——比收藏十个仓库都管用。这一章先把“这本书到底在教什么、你该用什么姿势跟”讲清楚,后面再拆环境、数据、训练循环和踩坑。

2. 环境与工具链:把 PyTorch 版跑起来的最小依赖组合

2.1 为什么推荐 conda 而不是 pip 裸装

《动手学深度学习2.0》的代码依赖三件套:PyTorch、torchvision、d2l。其中 d2l 是这本书配套的轻量工具包,封装了数据加载、绘图和训练循环的常用函数。裸 pip 装 torch 在部分平台上会拉错 CUDA 运行时,导致 import 时报找不到 cudart 之类的动态库。conda 的优势是它会把 CUDA runtime、cuDNN 和 Python 版本一起解析,减少“装完能 import 但一跑就崩”的概率。

我一般会单独建一个环境,不跟其他项目混用,因为这本书的代码对 torch 版本有一定敏感性,混用环境最容易出现“昨天还能跑,今天换了个项目就报错”的玄学问题。

# 创建独立环境,Python 版本选 3.9 或 3.10 都比较稳 conda create -n d2l python=3.10 -y conda activate d2l # 安装 PyTorch,具体 CUDA 版本按自己显卡驱动选 # 如果没有 NVIDIA 显卡,直接用 CPU 版即可,学习阶段完全够用 pip install torch torchvision --index-url https://download.pytorch.org/whl/cu118 # 安装 d2l 工具包 pip install d2l

这段命令的逻辑是:先隔离环境,再装框架,最后装课程配套包。参数上唯一需要你确认的是cu118这个后缀,它对应 CUDA 11.8。如果你不确定驱动支持哪个版本,最省事的做法是先装 CPU 版,把书里的前几章跑通,等确实需要 GPU 加速再换。CPU 版把--index-url那行换成pip install torch torchvision即可。

装完做一次最小验证,别急着开 notebook:

import torch from d2l import torch as d2l # 打印版本,确认没有混装 print(torch.__version__) # 造一个张量,确认计算图能正常跑 x = torch.arange(12, dtype=torch.float32).reshape(3, 4) print(x.sum())

如果这两步都过,说明环境基本可用。如果from d2l import torch as d2l报错,大概率是 d2l 版本和 torch 版本不匹配,优先重装 d2l 而不是降 torch。

2.2 数据加载的三种方式与选择依据

书里从零实现部分会手写data_iter,把特征和标签按批量切分。这是为了让你看清“小批量随机梯度下降”到底怎么取数据。但到了后面章节,代码会切换到d2l.load_data_fashion_mnist这类封装函数。很多人在这里困惑:到底该用哪种?

我的建议是分阶段。前四章(线性回归、softmax、多层感知机)坚持用手写版本,因为这是理解训练循环的关键。从卷积神经网络开始,直接用 d2l 封装,把精力放在网络结构上。下面是一个手写数据迭代器的最小示例:

import torch from torch.utils import data def load_array(data_arrays, batch_size, is_train=True): """构造一个 PyTorch 数据迭代器""" dataset = data.TensorDataset(*data_arrays) # is_train 决定是否打乱顺序,训练集要打乱,测试集不需要 return data.DataLoader(dataset, batch_size, shuffle=is_train) # 假设 features 和 labels 已经准备好 # batch_size 一般取 32 或 64,太小梯度噪声大,太大显存吃紧 batch_size = 32 data_iter = load_array((features, labels), batch_size)

这里TensorDataset把特征和标签按行绑定,DataLoader负责切批和打乱。shuffle参数是新手最容易忽略的点:训练集不打乱,模型可能因为样本顺序固定而收敛变慢甚至震荡。测试集打乱则没有意义,反而增加无谓开销。

2.3 训练循环里三个必须盯住的量

不管哪一章,训练循环的结构都差不多:前向计算、算损失、反向传播、更新参数。但真正决定你能不能判断“跑对了没有”的,是三个量:训练损失、验证损失、学习率。

训练损失持续下降但验证损失先降后升,是过拟合的典型信号。训练损失不降,先查学习率是不是太大,再查数据标签有没有对齐。学习率这个参数,书里常用 0.03、0.1 这类值,但换到自己的数据集上必须重新试。我一般会先跑一个很小的子集,用 0.1、0.03、0.01 各试几十步,看哪个 loss 下降最稳,再放大到全量数据。

# 一个最小训练循环骨架,以线性回归为例 net = torch.nn.Linear(2, 1) loss_fn = torch.nn.MSELoss() trainer = torch.optim.SGD(net.parameters(), lr=0.03) for epoch in range(3): for X, y in data_iter: # 前向 y_hat = net(X) # 算损失,注意 y 的形状要和 y_hat 对齐 loss = loss_fn(y_hat, y.reshape(y_hat.shape)) # 梯度清零,这一步漏了梯度会累加 trainer.zero_grad() # 反向传播 loss.backward() # 更新参数 trainer.step() print(f'epoch {epoch + 1}, loss {loss.item():.6f}')

zero_grad是血泪经验:PyTorch 默认会累加梯度,不清零的话第二次迭代的梯度是前两次之和,loss 会莫名其妙爆炸。y.reshape(y_hat.shape)是为了避免广播导致的形状错位,这种错位不一定报错,但会让损失算错。

3. 从零实现与调包实现:两条路线的分工与切换时机

3.1 为什么书里要你先手写再调包

《动手学深度学习2.0》最容易被低估的设计,是它把同一个模型讲两遍:一遍从零实现,一遍用框架高层 API。线性回归、softmax 回归、多层感知机都是这个套路。很多人觉得手写部分“太啰嗦”,直接跳到调包,结果遇到自定义层或自定义损失时完全不知道从哪下手。

手写部分的价值在于暴露“黑匣子”里的每一步。比如 softmax 回归从零实现时,你会亲手写softmax函数、交叉熵损失、以及带 L2 正则的权重衰减。这些在torch.nn.CrossEntropyLoss里是一行,但只有自己写过,才知道它内部做了 log-sum-exp 的数值稳定处理。

我的做法是:手写版跑通后,立刻用调包版再跑一遍,对比两者的 loss 曲线。如果曲线形状差异很大,说明手写版有 bug,通常是数值稳定性或梯度计算的问题。

3.2 自定义层和自定义训练循环的落地写法

到了卷积和循环网络部分,书里会引入自定义层。比如一个简单的残差块,或者带注意力机制的层。这类代码的通用结构是继承torch.nn.Module,在__init__里定义参数,在forward里写计算逻辑。

import torch from torch import nn class Residual(nn.Module): def __init__(self, input_channels, num_channels, use_1x1conv=False, strides=1): super().__init__() # 主分支:两层 3x3 卷积 self.conv1 = nn.Conv2d(input_channels, num_channels, kernel_size=3, padding=1, stride=strides) self.conv2 = nn.Conv2d(num_channels, num_channels, kernel_size=3, padding=1) # 如果输入输出通道数或尺寸不一致,用 1x1 卷积调整 if use_1x1conv: self.conv3 = nn.Conv2d(input_channels, num_channels, kernel_size=1, stride=strides) else: self.conv3 = None self.bn1 = nn.BatchNorm2d(num_channels) self.bn2 = nn.BatchNorm2d(num_channels) def forward(self, X): Y = torch.relu(self.bn1(self.conv1(X))) Y = self.bn2(self.conv2(Y)) # 残差连接:输入直接加到输出上 if self.conv3: X = self.conv3(X) return torch.relu(Y + X)

这个残差块的关键参数是use_1x1conv和strides。当输入输出通道数不同,或者特征图尺寸因为步幅变小,就需要 1x1 卷积把输入投影到相同形状,否则相加会报维度错误。BatchNorm2d放在卷积之后、激活之前,是这本书的常见顺序,能加速收敛。

自定义训练循环在书里也有模板,核心是把for X, y in data_iter里的四步拆成函数。我一般会保留一个train_epoch函数,把损失累加和准确率统计写进去,这样换数据集时只改数据加载部分。

3.3 用 d2l 的 Trainer 类简化实验管理

书里从 softmax 回归开始引入d2l.Trainer,它把优化器、学习率、批量大小打包在一起。用它的好处是实验代码更短,坏处是隐藏了zero_grad和step的调用时机。如果你已经手写过训练循环,用 Trainer 没问题;如果直接上手 Trainer,建议先看一眼它的源码,知道它在fit里做了什么。

from d2l import torch as d2l # 定义网络、损失、优化器 net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) loss = nn.CrossEntropyLoss() trainer = torch.optim.SGD(net.parameters(), lr=0.1) # 用 d2l 的 fit 函数训练 num_epochs = 10 d2l.train_ch3(net, train_iter, test_iter, loss, num_epochs, trainer)

train_ch3是书里为 softmax 回归写的训练函数,内部会打印每个 epoch 的 loss 和准确率。换到自己的数据时,注意train_iter和test_iter的格式要匹配,否则会在取 batch 时报错。

4. 避坑与排查:跑这本书代码时最常见的五类翻车

4.1 现象:import d2l 报错找不到模块,或 torch 和 d2l 版本冲突

原因通常是 pip 和 conda 混用,或者在一个环境里装了多个 torch 版本。解决方法是先pip list | grep torch看装了几个,再conda list | grep torch对比。如果确实混了,最干净的做法是删掉环境重建。重建时先装 torch,再装 d2l,顺序不要反。

4.2 现象:训练 loss 变成 nan

原因有三类:学习率太大、数据里有 nan 或 inf、损失函数对 log(0) 没有保护。排查顺序是先降学习率到 0.001 试几十步,如果还 nan,检查数据加载部分有没有除以零或空样本。softmax 从零实现时,如果直接对未归一化的输出取 log,很容易出 nan,书里用 log-sum-exp 技巧就是为了避免这个。

4.3 现象:GPU 显存够但跑得比 CPU 还慢

常见原因是数据加载没有用多进程,或者每次迭代都在 CPU 和 GPU 之间来回拷贝小张量。解决方法是把DataLoader的num_workers设为 4 或 8,并把模型和数据在训练开始前就.to(device),不要在循环里反复转移。另外,小批量大小设得太小(比如 1 或 2)也会让 GPU 利用率上不去。

4.4 现象:验证集准确率远低于训练集,但 loss 曲线看起来正常

这通常是数据预处理不一致导致的。训练集做了归一化,验证集没做;或者训练集用了数据增强,验证集用了另一套变换。检查train_iter和test_iter的 transform 是否只差增强部分,归一化的均值和方差必须一致。另一个可能是验证集本身有标签噪声,这种情况只能靠清洗数据解决。

4.5 现象:notebook 里画图不显示,或中文乱码

绘图不显示通常是没加%matplotlib inline,或者 d2l 的绘图函数在非 notebook 环境里需要显式调用plt.show()。中文乱码是字体问题,在绘图前设置plt.rcParams['font.sans-serif'] = ['SimHei']即可,但注意这个字体在部分系统上不存在,换成系统里有的中文字体名。

5. 进阶技巧:把书里的模型迁移到自己的数据集上

5.1 用自定义 Dataset 替换内置数据加载

书里用的 Fashion-MNIST 是固定格式,但你的数据大概率是图片文件夹或 CSV。这时需要写一个自定义Dataset,核心是实现__len__和__getitem__。

from torch.utils.data import Dataset from PIL import Image class MyDataset(Dataset): def __init__(self, file_list, label_list, transform=None): self.file_list = file_list self.label_list = label_list self.transform = transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): # 打开图片并转成 RGB,避免灰度图通道数不匹配 img = Image.open(self.file_list[idx]).convert('RGB') label = self.label_list[idx] if self.transform: img = self.transform(img) return img, label

convert('RGB')是容易漏的一步:如果你的数据里混了灰度图和彩色图,不统一通道数会在卷积层报错。transform一般用torchvision.transforms.Compose组合归一化和尺寸调整。

5.2 迁移学习时冻结层与学习率的配合

书里在“微调”一节讲了怎么加载预训练模型并替换最后一层。实际用时,冻结前面层的参数、只训练分类头,学习率可以设大一点(比如 0.01);如果解冻全部层做微调,学习率要降到 0.001 甚至更低,否则预训练权重会被快速破坏。

import torchvision.models as models from torch import nn # 加载预训练模型 pretrained = models.resnet18(pretrained=True) # 冻结所有参数 for param in pretrained.parameters(): param.requires_grad = False # 替换最后一层,新层的参数默认 requires_grad=True pretrained.fc = nn.Linear(pretrained.fc.in_features, 10) # 只把新层参数传给优化器 optimizer = torch.optim.SGD(pretrained.fc.parameters(), lr=0.01)

这个写法的关键是冻结后只把fc层参数传给优化器。如果直接把pretrained.parameters()传进去,虽然冻结层不会更新,但优化器仍会为它们分配状态,浪费显存。

5.3 用 TensorBoard 看训练过程,比 print 更省心

书里主要用 print 和 matplotlib,但实际项目里 TensorBoard 更方便对比多次实验。装好tensorboard后,在训练循环里加几行记录:

from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/exp1') for epoch in range(num_epochs): # ... 训练代码 ... 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()

启动tensorboard --logdir=runs就能在浏览器里看曲线。runs/exp1这个目录名可以按实验改,方便对比不同学习率或不同网络结构的效果。

5.4 一个我反复用的验证习惯

每次改完数据加载或模型结构,我不会直接跑完整训练,而是先取一个很小的子集(比如 100 个样本),跑 5 到 10 个 epoch,看 loss 能不能降到接近零。如果小数据集都过拟合不了,说明模型或训练循环有 bug,这时候调参没有意义。这个习惯帮我省过很多次“跑了一晚上发现方向错了”的后悔药。另外,随机种子我一般会固定,但不会迷信固定种子后的单次结果,重要结论至少跑三次不同种子看均值。

希望帮到你。

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

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

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

立即咨询