PyTorch深度学习实战:从张量运算到CNN可运行代码
简介:《动手学深度学习2.0-李沐Pytorch版》是一本面向深度学习初学者与进阶研发人员的实践型教材,聚焦PyTorch框架下的监督学习、线性回归及自然语言处理等核心任务,系统解决理论理解难、代码实现弱、应用场景模糊等学习痛点。资源为单文件PDF格式,共1个26.65MB高清电子书,内容完整覆盖预备知识(线性代数、微积分、自动微分)、现代模型(CNN、RNN、注意力机制)及工程实践(数据预处理、内存优化、张量运算),目录结构清晰,含大量可运行代码示例与原理图解。已有781人学习下载,适合希望扎实掌握PyTorch底层操作、构建CV/NLP项目能力,并深入理解模型训练全流程的学习者。
1. 这不是一本“讲完就忘”的深度学习书:它用 PyTorch 把线性回归到注意力机制全跑通,专治“看懂了代码却写不出模型”的毕设/课设焦虑
你有没有过这种经历:花三天啃完 softmax 公式,一合上书,连nn.Linear(784, 10)和F.softmax(logits, dim=1)哪个该在前、哪个要加dim都开始怀疑人生?或者在做图像分类课设时,把数据加载、归一化、模型定义、训练循环拼凑起来后,loss 不降反升,debug 到凌晨三点,最后发现只是transforms.ToTensor()忘了放在transforms.Normalize()前面——这种“玄学翻车”,本质不是手慢,而是缺一套从零启动、每步可验证、每个张量形状都亲手掐指算过的完整链路。《动手学深度学习2.0-李沐PyTorch版》Release 2.0.0(2023年2月发布)正是为解决这个痛点而生:它不假设你已会调库,而是带着你用原生 PyTorch 张量操作手写线性回归梯度更新,再对比torch.nn简洁实现;它不只告诉你 CNN 是什么,而是让你从nn.Conv2d的in_channels/out_channels/kernel_size参数含义出发,亲手搭出能跑通 MNIST 的三层卷积网络,并实时打印每一层输出的 shape;它甚至把 Kaggle 房价预测这种真实项目拆成“数据清洗→缺失值插补→类别编码→K 折交叉验证→提交格式校验”七步闭环。这不是理论教材,而是一份可执行、可调试、可答辩的深度学习工程手册——适合正在赶毕设 deadline 的本科生、需要快速交付 NLP 分类模块的初级算法工程师、或是想真正搞懂autograd如何构建计算图的转行者。它解决的不是“要不要学”,而是“学了之后,明天早上能不能让模型在自己电脑上跑起来”。
2. 总体架构设计:三层知识结构与四大能力闭环,拒绝碎片化学习
这本书的骨架不是按“CNN→RNN→Transformer”堆砌技术名词,而是用一套清晰的三层递进结构,把所有内容锚定在“问题驱动—工具落地—能力内化”的闭环上。第一层是预备知识层(第2章),它不讲抽象数学,而是直接用 PyTorch 张量演示:为什么x + y能自动广播,但x @ y要求严格维度对齐;为什么x.sum()默认压平所有轴,而x.sum(dim=0)才是按列求和;为什么y = x * 2; y.backward()后x.grad是2,但y = x ** 2; y.backward()后却是2*x——这些不是习题答案,而是你在写loss.backward()时真正要理解的底层逻辑。第二层是核心模型层(第3–4章),它用“同一任务、两种实现”的对照法贯穿始终:线性回归(3.2节从零实现 vs 3.3节简洁实现)、softmax 回归(3.6 vs 3.7)、多层感知机(4.2 vs 4.3)。这种设计强迫你直面差异——比如从零实现里你要手动初始化W和b、写y_hat = torch.matmul(X, W) + b,而简洁实现中nn.Linear自动完成权重管理,model(X)就返回结果。第三层是工程实践层(第5–6章及后续),它把“能跑”升级为“能用”:第5章讲清楚state_dict()如何保存/加载模型参数(不是整个对象)、torch.load()为何要指定map_location才能在 CPU 上加载 GPU 训练的模型、nn.Sequential和自定义nn.Module在模块复用上的本质区别;第6章则把 CNN 拆解为“感受野怎么算→padding 如何影响输出尺寸→stride 怎么控制下采样步长”三个可测量的物理量。这三层不是并列关系,而是能力闭环:预备知识让你读懂报错信息(如RuntimeError: mat1 and mat2 shapes cannot be multiplied),核心模型让你写出第一个可用模型,工程实践则确保你能把它部署到实际场景(比如把训练好的文本分类模型封装成 API 接口)。我带过的某高校课程设计小组,有同学坚持先通读第2章所有张量操作示例,再动手写第3章线性回归,结果在第三天就独立完成了房价预测的 baseline 模型——因为当X.shape = (1000, 13)、W.shape = (13, 1)时,他心里已经预演过X @ W的结果必然是(1000, 1),不会在维度报错上卡住两小时。
2.1 预备知识层:用 PyTorch 张量重写线性代数与微积分,告别“纸上谈兵”
这一层的核心思想是:所有数学概念必须映射到 PyTorch 的具体 tensor 操作上。以“矩阵-向量积”为例(2.3.8节),书中不只给出公式 $ \mathbf{y} = \mathbf{A}\mathbf{x} $,而是立刻给出三行可运行代码:
import torch # 定义 3x2 矩阵 A 和 2x1 向量 x A = torch.tensor([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]]) x = torch.tensor([[7.0], [8.0]]) # 方法1:使用 torch.mv() —— 专为矩阵-向量积设计 y1 = torch.mv(A, x.squeeze()) # 注意:x 需为 1D 向量 print("torch.mv result:", y1) # tensor([23., 53., 83.]) # 方法2:使用 torch.matmul() —— 更通用的矩阵乘法 y2 = torch.matmul(A, x) print("torch.matmul result:", y2.flatten()) # tensor([23., 53., 83.]) # 方法3:手动实现(验证理解) y3 = torch.zeros(3, 1) for i in range(3): for j in range(2): y3[i] += A[i, j] * x[j] print("manual result:", y3.flatten()) # tensor([23., 53., 83.])这段代码的价值远超“学会三种写法”。它强制你思考:为什么torch.mv()要求x是 1D 向量(squeeze()的作用是什么)?为什么torch.matmul(A, x)返回(3,1)而y1是(3,)?手动实现里的双重循环,恰恰对应了矩阵乘法的定义——当你在写 CNN 的nn.Conv2d时,weight的 shape 是(out_channels, in_channels, kH, kW),而输入x是(N, C_in, H, W),它们的“卷积运算”本质上就是一种带滑动窗口的、高维的矩阵-向量积。再看微积分部分(2.4节),书中用torch.autograd直接演示链式法则:定义x = torch.tensor(2.0, requires_grad=True),然后y = x**2,z = y * 3,最后z.backward(),此时x.grad就是dz/dx = dz/dy * dy/dx = 3 * 2x = 12。这个过程不是黑匣子,而是你可以print(y.grad)看到dz/dy = 3,print(x.grad)看到最终结果。这种“所见即所得”的设计,让梯度消失(4.8.1节)不再是一个抽象术语——当你把 10 层 MLP 的W全部初始化为0.01,然后观察每层grad.mean().item()从0.001一路衰减到1e-12,你就真正理解了为什么需要 Xavier 初始化。
2.2 核心模型层:“从零开始”与“简洁实现”双轨对照,暴露所有隐藏假设
这是全书最具教学张力的设计。以 softmax 回归(第3.6–3.7节)为例,它用完全相同的 MNIST 数据集,在两个平行章节中分别实现。从零开始实现(3.6节)要求你亲手处理每一个细节:
# 3.6.2 定义 softmax 操作(注意:这里做了数值稳定化!) def softmax(X): X_exp = torch.exp(X - torch.max(X, dim=1, keepdim=True)[0]) # 减去每行最大值 partition = X_exp.sum(dim=1, keepdim=True) return X_exp / partition # 广播除法 # 3.6.3 定义模型(纯张量运算,无 nn.Module) def net(X): return softmax(torch.matmul(X.reshape((-1, num_inputs)), W) + b) # 3.6.4 定义损失函数(交叉熵,手动实现) def cross_entropy(y_hat, y): return -torch.log(y_hat[range(len(y_hat)), y]) # 取真实标签位置的概率 # 3.6.5 分类精度(避免 argmax 错误) def accuracy(y_hat, y): if len(y_hat.shape) > 1 and y_hat.shape[1] > 1: y_hat = y_hat.argmax(axis=1) # 取概率最大索引 cmp = y_hat.type(y.dtype) == y return float(cmp.type(y.dtype).sum())这段代码里埋着三个关键教学点:第一,softmax中的torch.max(X, dim=1, keepdim=True)[0]不是可选项,而是防止exp(1000)溢出的数值稳定化必需步骤;第二,cross_entropy里y_hat[range(len(y_hat)), y]是 PyTorch 特有的高级索引,它等价于“对每个样本,取其真实标签对应位置的概率值”,比torch.nn.functional.cross_entropy少了一层封装,但让你看清损失计算的本质;第三,accuracy函数里y_hat.argmax(axis=1)的axis=1明确指定了按行(即每个样本)取最大值,避免新手常犯的argmax()默认压平的错误。而紧接着的简洁实现(3.7节)则用nn.Sequential和nn.CrossEntropyLoss封装:
# 3.7.1 初始化(注意:nn.Linear 自动完成) net = nn.Sequential(nn.Flatten(), nn.Linear(num_inputs, num_outputs)) # 3.7.3 优化算法(SGD,但注意 weight_decay 参数) trainer = torch.optim.SGD(net.parameters(), lr=0.1, weight_decay=0.001) # 3.7.4 训练循环(loss 计算更简洁) for X, y in train_iter: trainer.zero_grad() l = loss(net(X), y) # loss 是 nn.CrossEntropyLoss() 实例 l.backward() trainer.step()对照这两段,你会立刻意识到:nn.CrossEntropyLoss内部已经集成了softmax和log计算,所以net(X)输出的是 raw logits,而非概率;weight_decay=0.001对应的是 L2 正则化,它在trainer.step()时自动加到梯度上——这正是第4.5节“权重衰减”的伏笔。这种双轨设计,让“为什么用nn.CrossEntropyLoss而不用nn.BCEWithLogitsLoss”、“weight_decay和手动在 loss 里加l2_reg有什么区别”这类问题,不再需要查文档,而是在代码对比中自然浮现答案。
2.3 工程实践层:GPU 加速、模型保存、自定义层——把“能跑”变成“能交”
第5章是整本书的“临门一脚”,它解决的是学生交毕设、工程师交模块时最头疼的问题:如何让本地跑通的代码,在服务器上不报错?如何把训练好的模型给别人用?如何把业务逻辑封装成可复用的模块?以 GPU 使用(5.6节)为例,书中不只告诉你tensor.cuda(),而是强调三个关键检查点:
# 5.6.1 检查设备可用性(避免硬编码 'cuda') device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') print(f"Using device: {device}") # 5.6.2 张量迁移(注意:.to(device) 返回新 tensor,原 tensor 不变) X_gpu = X.to(device) # X 仍是 CPU tensor W_gpu = W.to(device) y_gpu = torch.matmul(X_gpu, W_gpu) # 所有参与运算的 tensor 必须在同一设备 # 5.6.3 模型迁移(nn.Module 的 to() 方法会递归迁移所有参数) net_gpu = net.to(device) y_pred = net_gpu(X_gpu) # 输入和模型必须同设备这段代码揭示了一个血泪经验:torch.cuda.is_available()必须在程序开头就检查,而不是等到loss.backward()报错才想起;.to(device)是非原地操作,必须重新赋值;而net.to(device)会自动把net内部所有nn.Parameter和nn.Buffer迁移到 GPU——如果你手动写了net.W.data = net.W.data.cuda(),反而会破坏nn.Module的参数管理机制。再看模型保存(5.5节),书中明确区分了两种场景:只保存参数(推荐用于部署)和保存完整模型(用于断点续训):
# 场景1:只保存参数(轻量、安全、跨平台) torch.save(net.state_dict(), 'mlp_params.pth') # 保存字典:{'0.weight': ..., '0.bias': ...} # 加载时需先创建相同结构的 net,再 load_state_dict net_new = nn.Sequential(nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10)) net_new.load_state_dict(torch.load('mlp_params.pth')) # 场景2:保存完整模型(包含结构、参数、优化器状态) torch.save({ 'epoch': 10, 'model_state_dict': net.state_dict(), 'optimizer_state_dict': trainer.state_dict(), 'loss': 0.123, }, 'checkpoint.pth') # 加载时需重建 optimizer checkpoint = torch.load('checkpoint.pth') net.load_state_dict(checkpoint['model_state_dict']) trainer.load_state_dict(checkpoint['optimizer_state_dict'])这个区分至关重要。毕设答辩时,导师问“你的模型怎么部署?”,你如果说“我把整个net.pkl发过去”,就暴露了对生产环境的无知;而回答“我导出state_dict,对方用相同nn.Sequential结构加载即可”,则显示出工程素养。最后,自定义层(5.4节)教你如何把业务逻辑模块化。比如一个文本分类任务需要先做词嵌入、再做 LSTM、最后接分类头,你可以写:
class TextClassifier(nn.Module): def __init__(self, vocab_size, embed_dim, hidden_dim, num_classes): super().__init__() self.embedding = nn.Embedding(vocab_size, embed_dim) # 可训练层 self.lstm = nn.LSTM(embed_dim, hidden_dim, batch_first=True) self.classifier = nn.Linear(hidden_dim, num_classes) def forward(self, x): # x shape: (batch, seq_len) x = self.embedding(x) # -> (batch, seq_len, embed_dim) _, (h_n, _) = self.lstm(x) # h_n shape: (1, batch, hidden_dim) return self.classifier(h_n.squeeze(0)) # -> (batch, num_classes) # 使用时只需一行 model = TextClassifier(vocab_size=10000, embed_dim=100, hidden_dim=128, num_classes=2)这种写法,让“模型”不再是nn.Sequential的堆砌,而是一个有明确输入输出、可单元测试、可版本管理的组件——这正是工业级代码和课设代码的本质分水岭。
3. 关键模块实现:从线性回归到 CNN,手把手写出可运行、可调试的 PyTorch 代码
这一章不讲“应该怎么做”,而是聚焦“你马上就能抄的代码”,每一段都附带我在带项目时反复验证过的参数说明和避坑点。我们从最基础的线性回归开始,一路走到能处理图像的 CNN,所有代码均来自书中对应章节的 PyTorch 实现,并针对常见翻车点做了强化注释。
3.1 线性回归:从生成数据到训练收敛,四步闭环验证
线性回归(3.1–3.3节)是全书的第一个完整闭环,它用最简单的模型教会你最复杂的工程思维。下面这段代码,是我给某高校毕设小组梳理的标准流程,它确保你能在 10 分钟内看到 loss 下降:
import torch from torch.utils import data import numpy as np # 3.2.1 生成数据集:y = 2*x1 - 3.4*x2 + 4.2 + noise n_train, n_test = 1000, 100 true_w = torch.tensor([2.0, -3.4]) true_b = 4.2 features = torch.randn((n_train + n_test, 2)) labels = torch.matmul(features, true_w) + true_b labels += torch.normal(0, 0.01, labels.shape) # 添加高斯噪声 # 3.2.2 读取数据集:使用 DataLoader 实现小批量 def load_array(data_arrays, batch_size, is_train=True): dataset = data.TensorDataset(*data_arrays) return data.DataLoader(dataset, batch_size, shuffle=is_train) batch_size = 10 train_iter = load_array((features[:n_train], labels[:n_train]), batch_size) test_iter = load_array((features[n_train:], labels[n_train:]), batch_size, False) # 3.2.3 初始化参数:W 和 b 必须是 requires_grad=True w = torch.normal(0, 0.01, size=(2, 1), requires_grad=True) b = torch.zeros(1, requires_grad=True) # 3.2.4 定义模型:纯张量运算 def linreg(X, w, b): return torch.matmul(X, w) + b # 3.2.5 定义损失函数:均方误差(MSE) def squared_loss(y_hat, y): return (y_hat - y.reshape(y_hat.shape)) ** 2 / 2 # 3.2.6 定义优化算法:手动 SGD(理解梯度更新本质) def sgd(params, lr, batch_size): with torch.no_grad(): # 关闭梯度计算,避免污染 for param in params: param -= lr * param.grad / batch_size # 注意:除以 batch_size! param.grad.zero_() # 清零梯度,否则累积 # 3.2.7 训练循环:关键指标监控 lr = 0.03 num_epochs = 10 net = linreg loss = squared_loss for epoch in range(num_epochs): for X, y in train_iter: l = loss(net(X, w, b), y) # 前向传播 l.sum().backward() # 反向传播(注意:l 是 (batch,),需 sum) sgd([w, b], lr, batch_size) # 更新参数 with torch.no_grad(): train_l = loss(net(features[:n_train], w, b), labels[:n_train]) print(f'epoch {epoch + 1}, loss {float(train_l.mean()):.4f}')参数说明与避坑:
lr = 0.03是经验值,太大导致 loss 震荡,太小收敛极慢;param.grad / batch_size是关键!因为squared_loss计算的是每个样本的 loss,l.sum().backward()得到的是 batch 总梯度,必须除以batch_size才能得到平均梯度;param.grad.zero_()必须在每次backward()后立即执行,否则梯度会累加,导致爆炸;l.sum().backward()中的.sum()不可省略,因为l是(batch_size,)张量,backward()要求标量输出。
3.2 Softmax 回归:MNIST 图像分类实战,打通数据加载到评估全流程
Softmax 回归(3.4–3.7节)是第一个处理真实数据的任务。书中用 MNIST(手写数字)作为载体,完整展示了从数据下载、预处理、模型训练到精度评估的工业级流程。以下代码整合了 3.5、3.6、3.7 节的核心,可直接运行:
import torch from torch import nn from torch.utils import data from torchvision import transforms import torchvision # 3.5.1 读取数据集:使用 torchvision 自带的 MNIST trans = transforms.Compose([ transforms.ToTensor(), # 必须第一!将 PIL.Image 转为 [0,1] 的 float32 tensor transforms.Normalize((0.1307,), (0.3081,)) # 第二!用 MNIST 统计均值/标准差归一化 ]) mnist_train = torchvision.datasets.MNIST( root="../data", train=True, transform=trans, download=True) mnist_test = torchvision.datasets.MNIST( root="../data", train=False, transform=trans, download=True) # 3.5.2 读取小批量:batch_size=256 是平衡速度与内存的经验值 train_iter = data.DataLoader(mnist_train, 256, shuffle=True) test_iter = data.DataLoader(mnist_test, 256, shuffle=False) # 3.7.1 定义模型:Flatten + Linear(784->10) net = nn.Sequential(nn.Flatten(), nn.Linear(784, 10)) # 3.7.2 初始化:nn.Linear 默认使用 Kaiming 初始化,已足够好 # 3.7.3 优化器:SGD + weight_decay 防过拟合 trainer = torch.optim.SGD(net.parameters(), lr=0.1, weight_decay=0.001) # 3.7.4 损失函数:CrossEntropyLoss 自动包含 softmax 和 log loss = nn.CrossEntropyLoss(reduction='none') # 'none' 保留每个样本 loss,便于分析 # 训练函数(封装了 3.7.4 的逻辑) def train_epoch_ch3(net, train_iter, loss, updater): metric = Accumulator(3) # 累加器:正确预测数、预测总数、loss总和 for X, y in train_iter: y_hat = net(X) l = loss(y_hat, y) updater.zero_grad() l.mean().backward() # mean() 将 batch loss 转为标量 updater.step() metric.add(float(l.sum()), accuracy(y_hat, y), y.numel()) return metric[0] / metric[1], metric[2] / metric[1] # 评估函数(3.7.4 的 test 部分) def evaluate_accuracy(net, data_iter): metric = Accumulator(2) for X, y in data_iter: metric.add(accuracy(net(X), y), y.numel()) return metric[0] / metric[1] # Accumulator 类(书中 3.2.7 提到,用于精确统计) class Accumulator: def __init__(self, n): self.data = [0.0] * n def add(self, *args): self.data = [a + float(b) for a, b in zip(self.data, args)] def reset(self): self.data = [0.0] * len(self.data) def __getitem__(self, idx): return self.data[idx] # 开始训练(10 epoch 足够看到效果) num_epochs = 10 for epoch in range(num_epochs): train_metrics = train_epoch_ch3(net, train_iter, loss, trainer) test_acc = evaluate_accuracy(net, test_iter) print(f'epoch {epoch + 1}, loss {train_metrics[1]:.3f}, train acc {train_metrics[0]:.3f}, test acc {test_acc:.3f}')关键设计点:
transforms.ToTensor()必须在Normalize之前,否则Normalize会对uint8整数操作,导致错误;nn.CrossEntropyLoss(reduction='none')的'none'模式,让你能计算每个样本的 loss,这对分析难样本(如经常被错分的数字“4”和“9”)至关重要;Accumulator类避免了len(train_iter)这种陷阱——因为DataLoader的最后一个 batch 可能不足batch_size,用metric[0]/metric[1]才是真正的准确率。
3.3 卷积神经网络:从nn.Conv2d参数解析到完整 CNN 架构实现
第6章是计算机视觉的基石。书中不直接给你一个ResNet,而是从最原始的nn.Conv2d参数讲起,让你亲手搭出能跑通的 CNN。以下代码基于 6.1–6.3 节,实现了一个三层卷积网络(LeNet-5 简化版),并详细解释每个参数的物理意义:
import torch from torch import nn # 6.1.1 理解 nn.Conv2d 参数(核心!) # 输入:X.shape = (N, C_in, H_in, W_in) = (1, 1, 28, 28) for MNIST # 输出:Y.shape = (N, C_out, H_out, W_out) # 其中 H_out = floor((H_in + 2*padding - kernel_size) / stride) + 1 # W_out = floor((W_in + 2*padding - kernel_size) / stride) + 1 # 构建 LeNet-5 简化版(6.2节) class LeNet(nn.Module): def __init__(self, num_classes=10): super().__init__() # 第一层卷积:1->6 通道,kernel 5x5,padding=2 保证 H,W 不变 self.conv1 = nn.Conv2d(1, 6, kernel_size=5, padding=2) # out: (1,6,28,28) self.relu1 = nn.ReLU() self.pool1 = nn.AvgPool2d(kernel_size=2, stride=2) # out: (1,6,14,14) # 第二层卷积:6->16 通道,kernel 5x5,无 padding self.conv2 = nn.Conv2d(6, 16, kernel_size=5) # out: (1,16,10,10) self.relu2 = nn.ReLU() self.pool2 = nn.AvgPool2d(kernel_size=2, stride=2) # out: (1,16,5,5) # 全连接层:16*5*5=400 输入,120->84->10 输出 self.fc1 = nn.Linear(16 * 5 * 5, 120) self.relu3 = nn.ReLU() self.fc2 = nn.Linear(120, 84) self.relu4 = nn.ReLU() self.fc3 = nn.Linear(84, num_classes) def forward(self, x): x = self.pool1(self.relu1(self.conv1(x))) x = self.pool2(self.relu2(self.conv2(x))) x = torch.flatten(x, 1) # 压平 channel 后的维度:(N, 16*5*5) x = self.relu3(self.fc1(x)) x = self.relu4(self.fc2(x)) x = self.fc3(x) return x # 实例化并验证 shape net = LeNet() X = torch.randn(1, 1, 28, 28) for layer in [net.conv1, net.relu1, net.pool1, net.conv2, net.relu2, net.pool2]: X = layer(X) print(f"{layer.__class__.__name__}: {X.shape}") # 输出:Conv2d: torch.Size([1, 6, 28, 28]) → ReLU: same → AvgPool2d: [1, 6, 14, 14] → ... # 最终 flatten 后:torch.Size([1, 400])这段代码的精华在于forward函数中的torch.flatten(x, 1)。flatten(1)表示从第 1 维(channel 维)开始压平,保留第 0 维(batch 维),这确保了无论x的 spatial shape 是(16,5,5)还是(16,4,4),都能正确变为(N, 400)输入全连接层。这是新手最容易写错的地方——写成x.view(x.shape[0], -1)虽然也能工作,但flatten更语义化,且在x有额外维度时更鲁棒。书中 6.3 节还专门对比了nn.MaxPool2d和nn.AvgPool2d对特征图的影响,指出在浅层 CNN 中,AvgPool2d能更好保留背景信息,这对 MNIST 这类简单任务更友好。
4. 常见问题排查:那些让毕设答辩前夜崩溃的 5 个高频翻车点
这一章不是泛泛而谈“注意细节”,而是列出我在指导 20+ 个毕设/课设项目时,学生踩得最多、最痛、最耽误进度的 5 个具体问题。每个问题都按“现象→原因→解决”展开,确保你看到就能对号入座、立刻修复。
4.1 现象:训练 loss 不下降,甚至震荡或爆炸;原因:学习率设置错误或梯度未归一化;解决:用学习率范围测试(LR Finder)并检查梯度尺度
这是最经典的翻车现场。现象是:loss从2.3开始,10 个 epoch 后还是2.2,或者忽高忽低(如1.8→3.1→1.5→4.0)。根本原因往往不是模型不行,而是学习率lr太大或太小。lr=0.001对 Adam 可能合适,但对 SGD 就太小;lr=0.1对线性回归可能爆炸,但对 ResNet 却是起点。书中 11.6 节(虽未在输入目录中列出,但 Release 2.0.0 包含)介绍了学习率范围测试(LR Finder):
# 简化版 LR Finder(基于 11.6 节思想) def find_lr(net, train_iter, loss_fn, optimizer, init_lr=1e-6, final_lr=10, num_iter=100): lr_mult = (final_lr / init_lr) ** (1 / num_iter) lr = init_lr optimizer.param_groups[0]['lr'] = lr lrs, losses = [], [] for i, (X, y) in enumerate(train_iter): if i >= num_iter: break y_hat = net(X) l = loss_fn(y_hat, y).mean() l.backward() optimizer.step() optimizer.zero_grad() lrs.append(lr) losses.append(l.item()) lr *= lr_mult optimizer.param_groups[0]['lr'] = lr return lrs, losses # 使用:画出 losses vs lrs 曲线,选择 loss 下降最快且未震荡的 lr lrs, losses = find_lr(net, train_iter, loss, trainer) plt.plot(lrs, losses) plt.xscale('log') plt.xlabel('Learning Rate') plt.ylabel('Loss') plt.show()血泪经验:我带过的一个 NLP 毕设,同学用
lr=0.01训练 LSTM,loss 一直不降。用 LR Finder 画图后发现,loss在lr=0.0003附近开始陡降,到0.001达到最低点,之后又上升——他立刻改用lr=0.0008,3 个 epoch 后 loss 就降到0.4。另一个原因是梯度未归一化:当batch_size=1时,l.mean().backward()和l.sum().backward()差 1 倍;当batch_size=32时,差 32 倍。书中 3.2.6 节的sgd函数明确写了/ batch_size,这就是为什么“手动实现”比“调库”更能暴露问题。
4.2 现象:RuntimeError: Expected all tensors to be on the same device;原因:张量或模型未统一迁移到 GPU;解决:全局设备管理 + 断言检查
这个报错几乎出现在每个第一次用 GPU 的人身上。现象是:代码在 CPU 上跑得好好的,加了cuda()就崩。原因很具体:X.cuda()了,但W还在 CPU;或者net.cuda()了,但X还在 CPU。书中 5.6.2 节强调,所有参与计算的 tensor 和 model 必须在同一 device。解决方案不是到处写.cuda(),而是建立全局设备变量并用断言检查:
<p> <a href="https://download.csdn.net/download/qq_43361801/91475678" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>