简介:这份资源是《Deep Learning with PyTorch》的官方样本PDF,面向希望入门PyTorch框架、掌握深度学习项目实践的开发者与学习者,尤其适合具备一定Python基础、想通过动手示例理解模型训练全流程的读者。压缩包内仅含1个PDF文件,大小约399KB,内容为英文原版样章,涵盖训练循环构建、训练统计信息收集、tqdm进度报告、DataLoader与Dataset使用,以及一个完整的回归模型项目,包括数据集描述、模型搭建、训练与数据预处理优化等关键环节。目前已有138人学习下载,可作为快速了解该书结构、判断是否深入学习的参考。通过这份样本,读者能提前接触PyTorch动态计算图、GPU加速张量运算与神经网络API等核心概念,并借助回归项目示例理解从数据准备到模型改进的完整思路,为后续系统学习打下基础。
1. 一份只有 32 页的 PyTorch 样章,为什么值得先啃透
拿到deep-learning-with-pytorch-sample.pdf的时候,我第一反应是「就这?」——32 页,目录占了两页,免责声明和版权页又占一页,真正能看的正文其实集中在三个章节:第 8 章「Creating a Training Loop for Your Models」、第 12 章「Project: Building a Regression Model in PyTorch」、第 18 章「Training a PyTorch Model with DataLoader and Dataset」。但恰恰是这三章,把 PyTorch 入门最容易翻车的三个环节全踩了一遍:训练循环怎么写才不丢梯度、回归项目怎么从裸模型调到能看、DataLoader 和 Dataset 怎么配合才不拖慢 GPU。
这份样章不是教程全集,它更像一份「路线图切片」。完整版覆盖了从张量操作、Autograd、多层感知机,到 CNN、LSTM、超参搜索、模型可视化等一整套内容,而样章只抽了其中三段。对已经能跑通nn.Linear但一写训练循环就心里没底的人来说,这三段正好补上「从能跑到跑对」的缺口。它适合两类人:一是刚学完 PyTorch 基础 API、准备做第一个端到端项目的新手;二是写过几轮训练脚本、但每次都要回头翻旧代码的熟手——样章里的训练循环模板和 DataLoader 用法,可以直接当脚手架用。
2. 训练循环的骨架:从零写一个不丢梯度的 loop
2.1 为什么zero_grad必须放在backward之前
样章第 8 章开篇就点了一件事:训练一个深度学习模型,核心元素只有四个——前向传播、计算损失、反向传播、更新参数。听起来像废话,但真正动手写的时候,顺序错一步,模型要么不收敛,要么收敛到一半突然炸掉。
最常见的翻车现场是zero_grad的位置。PyTorch 的梯度是累加的,默认不会自动清零。如果你把optimizer.zero_grad()写在loss.backward()后面,那第一次迭代没问题,第二次迭代的梯度就会叠加上第一次的残留,等效于把学习率悄悄放大了一倍。短跑几轮看不出来,跑上几十个 epoch 后 loss 曲线会突然抖成锯齿。
我一般会固定成这个顺序:
# 训练循环的标准骨架 for epoch in range(num_epochs): model.train() # 切换到训练模式,启用 dropout / BN for batch_idx, (features, targets) in enumerate(train_loader): features = features.to(device) targets = targets.to(device) optimizer.zero_grad() # 1. 清空上一轮梯度 outputs = model(features) # 2. 前向传播 loss = criterion(outputs, targets) # 3. 计算损失 loss.backward() # 4. 反向传播,梯度写入 param.grad optimizer.step() # 5. 按梯度更新参数逻辑说明:zero_grad放在最前面,保证每次backward写入的梯度都是干净的。model.train()不是可有可无的装饰,如果你的网络里有Dropout或BatchNorm,不切模式会让推理行为混进训练,验证集指标会莫名其妙地偏低。
参数说明:device建议在循环外定义一次,比如device = torch.device('cuda' if torch.cuda.is_available() else 'cpu'),循环内只做.to(device),避免每次迭代都重新判断。batch_idx如果不用可以写成_,但保留它方便后面接 tqdm 或手动打印进度。
2.2 用 tqdm 报告进度,但别让它拖慢训练
样章专门用了一节讲 tqdm,这个选择很实在。训练循环最难受的不是写代码,是等结果——尤其是 CPU 上跑一个稍大的全连接网络,几分钟没有任何输出,你会怀疑是不是死循环了。tqdm 解决的就是这个「黑匣子」问题。
但 tqdm 用不好会反过来拖慢训练。我见过有人在每个 batch 里更新一次 tqdm 的set_postfix,还把当前 loss 转成 Python float 再格式化,结果 GPU 利用率从 90% 掉到 60%。原因是频繁的字符串格式化和终端刷新会阻塞主线程。
比较稳的写法是:外层用tqdm包住 epoch,内层只在每 N 个 batch 更新一次 postfix。
from tqdm import tqdm for epoch in tqdm(range(num_epochs), desc="Epoch"): model.train() running_loss = 0.0 for batch_idx, (features, targets) in enumerate(train_loader): features, targets = features.to(device), targets.to(device) optimizer.zero_grad() outputs = model(features) loss = criterion(outputs, targets) loss.backward() optimizer.step() running_loss += loss.item() # 每 50 个 batch 刷新一次显示,减少终端 IO if batch_idx % 50 == 0: tqdm.write(f"Epoch {epoch} | Batch {batch_idx} | Loss {running_loss/(batch_idx+1):.4f}")逻辑说明:loss.item()把单元素张量转成 Python 浮点数,避免把计算图带进累加变量导致显存泄漏。tqdm.write比直接print更安全,它不会打断进度条的渲染。
参数说明:batch_idx % 50里的 50 不是固定值,如果你的数据集小、batch 少,可以改成 10;如果 batch 上千,改成 100 或 200。原则是让终端每秒刷新不超过两次。
提示:如果你在 Jupyter Notebook 里跑,tqdm 的
tqdm.notebook子模块比标准版更稳,但样章里的代码是脚本风格,直接from tqdm import tqdm就行。
3. 回归项目实战:从裸模型到加预处理的完整链路
3.1 数据集加载与模型搭建的最小闭环
样章第 12 章给了一个回归项目的完整流程:描述数据集、搭模型、训练、然后用预处理改善效果。虽然样章没贴出完整数据,但从上下文看,这是一个典型的表格回归任务——特征列是数值型,目标列是连续值。这类任务在工业场景里比图像分类更常见,但网上教程反而少,所以这一章的价值被低估了。
我按样章的思路补一个可复现的最小闭环。假设数据已经读进一个pandas.DataFrame,特征列叫feat_1到feat_n,目标列叫target。
import torch import torch.nn as nn import pandas as pd from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 1. 读数据并划分 df = pd.read_csv("your_regression_data.csv") X = df.drop(columns=["target"]).values.astype("float32") y = df["target"].values.astype("float32").reshape(-1, 1) X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) # 2. 标准化:回归任务里这一步比模型结构更影响结果 scaler = StandardScaler() X_train = scaler.fit_transform(X_train) X_test = scaler.transform(X_test) # 注意:测试集只能用 train 的统计量 # 3. 转成张量 X_train = torch.tensor(X_train, dtype=torch.float32) y_train = torch.tensor(y_train, dtype=torch.float32) X_test = torch.tensor(X_test, dtype=torch.float32) y_test = torch.tensor(y_test, dtype=torch.float32)逻辑说明:StandardScaler的fit_transform只在训练集上调用,测试集用transform。这是回归项目里最容易犯的错之一——如果测试集也fit_transform,等于把测试集的均值方差泄漏进了训练过程,评估指标会虚高。
参数说明:astype("float32")是必须的,PyTorch 默认吃 float32,如果传 float64 会在第一次前向传播时报 dtype 不匹配。reshape(-1, 1)把目标从一维转成二维,因为nn.Linear的输出是(batch, 1),不 reshape 会导致广播错误。
模型部分,样章用的是全连接网络。我一般从两层隐藏层起步:
class RegressionNet(nn.Module): def __init__(self, input_dim): super().__init__() self.net = nn.Sequential( nn.Linear(input_dim, 64), nn.ReLU(), nn.Linear(64, 32), nn.ReLU(), nn.Linear(32, 1) # 回归输出 1 维,不加激活 ) def forward(self, x): return self.net(x) model = RegressionNet(input_dim=X_train.shape[1]) criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=1e-3)逻辑说明:回归任务的输出层不能加Sigmoid或Softmax,否则输出被限制在固定区间,遇到目标值超出范围的数据直接学不动。MSELoss是最常用的回归损失,如果目标有异常值,可以换成nn.L1Loss或nn.SmoothL1Loss。
参数说明:lr=1e-3是 Adam 的常用起点,如果 loss 下降太慢可以调到3e-3,如果震荡就降到1e-4。隐藏层维度 64 和 32 不是金标准,特征维度高的时候第一层可以设成 128 或 256。
3.2 预处理到底改善了什么:一个可对比的实验
样章在「Improving the Model with Preprocessing」一节里强调预处理能提升效果,但没展开说提升在哪。我补一个对比实验的设计,你可以直接套。
| 配置 | 特征处理 | 目标处理 | 预期现象 |
|---|---|---|---|
| 基线 | 无 | 无 | loss 下降慢,容易卡在某个值 |
| 仅特征标准化 | StandardScaler | 无 | 收敛加快,但最终精度提升有限 |
| 特征+目标标准化 | StandardScaler | StandardScaler | 收敛快且稳定,MSE 显著下降 |
| 特征标准化+目标对数变换 | StandardScaler | log1p | 目标偏态严重时效果最好 |
目标标准化容易被忽略。如果目标值范围是 0 到 100000,而特征标准化后范围是 -3 到 3,MSE 的梯度会被大数值主导,模型前期基本在拟合均值。把目标也标准化后,梯度尺度一致,收敛速度会明显不同。
# 目标标准化:训练集 fit,测试集 transform,预测后要逆变换 y_scaler = StandardScaler() y_train_scaled = y_scaler.fit_transform(y_train) y_test_scaled = y_scaler.transform(y_test) # 训练完成后评估时,把预测值逆变换回原始尺度 model.eval() with torch.no_grad(): pred_scaled = model(X_test) pred = y_scaler.inverse_transform(pred_scaled.numpy()) mse = ((pred - y_test.numpy()) ** 2).mean() print(f"Test MSE (original scale): {mse:.4f}")逻辑说明:inverse_transform是必须的,否则你报的 MSE 是标准化尺度下的,和业务方理解的「误差多少」对不上。model.eval()和torch.no_grad()一起用,前者切换 BN/Dropout 行为,后者关闭计算图节省显存。
参数说明:如果目标有零值或负值,log1p不能用,得换成np.sign(y) * np.log1p(np.abs(y))这种带符号的变换。逆变换时对应写np.sign(pred) * np.expm1(np.abs(pred))。
4. DataLoader 与 Dataset:把数据管道从瓶颈里捞出来
4.1 为什么你的 GPU 利用率只有 30%
样章第 18 章讲 DataLoader 和 Dataset,这一章解决的是训练循环之外的问题:数据供给。很多人写完训练循环后发现 GPU 利用率上不去,第一反应是模型太小,其实更常见的原因是 DataLoader 在拖后腿——每个 batch 都要等 CPU 读完文件、做完增强、再拷贝到显存。
DataLoader 的核心参数只有四个需要盯:batch_size、shuffle、num_workers、pin_memory。
from torch.utils.data import Dataset, DataLoader class TabularDataset(Dataset): def __init__(self, features, targets): self.features = features self.targets = targets def __len__(self): return len(self.features) def __getitem__(self, idx): return self.features[idx], self.targets[idx] train_dataset = TabularDataset(X_train, y_train) train_loader = DataLoader( train_dataset, batch_size=64, shuffle=True, # 训练集打乱,验证集/测试集不打乱 num_workers=4, # 子进程数,一般设为 CPU 核数的一半 pin_memory=True # 锁页内存,加速 CPU 到 GPU 的拷贝 )逻辑说明:__getitem__返回的是单个样本,DataLoader 负责拼成 batch。shuffle=True只在训练集开,验证集和测试集开了会导致评估结果不可复现。pin_memory=True在 GPU 训练时几乎总是该开,它让数据放在锁页内存里,to(device)的拷贝会快一截。
参数说明:num_workers不是越大越好。设成 0 表示在主进程里读数据,调试时用这个;设成 4 到 8 是常见区间,超过 CPU 物理核数反而会因为进程切换开销变慢。Windows 上num_workers > 0需要把训练代码放在if __name__ == "__main__":里,否则会无限递归创建子进程。
4.2 Dataset 类的三个易错点
样章里 Dataset 的示例很简洁,但实际写的时候有三个地方容易翻车。
第一,__len__返回的是样本数,不是 batch 数。有人把len(self.features) // batch_size写进去,结果 DataLoader 只遍历了部分数据,训练集越跑越少。
第二,__getitem__里不要做太重的操作。如果每个样本都要读一次磁盘文件,num_workers再大也救不了。常见做法是在__init__里把数据全部读进内存,或者用lmdb、webdataset这类格式做顺序读。
第三,返回的张量 dtype 要统一。如果features是 float64、targets是 float32,拼 batch 的时候 PyTorch 会报错或者隐式转换,隐式转换在 GPU 上会触发同步,拖慢速度。
class TabularDataset(Dataset): def __init__(self, features, targets): # 在初始化时统一 dtype,避免 __getitem__ 里反复转换 self.features = torch.as_tensor(features, dtype=torch.float32) self.targets = torch.as_tensor(targets, dtype=torch.float32) def __len__(self): return self.features.shape[0] # 明确返回样本数 def __getitem__(self, idx): return self.features[idx], self.targets[idx]逻辑说明:torch.as_tensor比torch.tensor更省内存,如果输入已经是张量,它不会复制。在__init__里做 dtype 统一,__getitem__就只剩索引操作,子进程的负担最小。
参数说明:如果数据量特别大,__init__里全读进内存不现实,可以只存文件路径,在__getitem__里读。但这时候num_workers要适当调大,并且考虑用prefetch_factor(PyTorch 1.7+)控制每个 worker 预取的 batch 数。
注意:
pin_memory=True和num_workers > 0同时用时,如果自定义 Dataset 返回的是 Python 对象而不是张量,锁页内存的优势会消失。确保__getitem__返回的是张量或张量元组。
5. 避坑与排查:样章没写但一定会遇到的五个问题
5.1 loss 变成 nan:先查学习率,再查数据
现象:训练几个 batch 后 loss 突然变成nan,之后再也降不下来。
原因:最常见的是学习率太大,导致梯度爆炸;其次是输入数据里有inf或nan,前向传播直接污染整个 batch。
解决:先把学习率降一个数量级试一轮。如果还不行,在__getitem__或数据加载后加一句assert not torch.isnan(features).any(),定位到具体是哪个样本的问题。回归任务里,目标值如果有极端异常值,MSE 损失会产生巨大梯度,可以考虑先做目标截断或换SmoothL1Loss。
5.2 验证集 loss 比训练集低:不一定是好事
现象:训练集 loss 0.5,验证集 loss 0.3,看起来模型泛化很好。
原因:如果验证集没开shuffle而训练集开了,且验证集样本分布更集中,就会出现这种「假象」。另一种可能是 Dropout 在训练时拉高了训练 loss,而验证时关闭了 Dropout。
解决:先确认验证集的shuffle=False,并且model.eval()在验证前调用了。如果两者都正确,再检查验证集是不是太小——小验证集的 loss 波动本来就大,单次结果说明不了问题。
5.3 GPU 显存够但报 OOM:计算图没释放
现象:显存明明有 8G,模型只有几百万参数,但跑几个 batch 就 OOM。
原因:在训练循环里把loss或outputs累加进了 Python 列表,计算图一直挂在显存里没释放。
解决:累加 loss 用loss.item(),不要用loss本身。如果需要在训练中途做验证,验证部分包在with torch.no_grad():里,并且验证完把中间变量显式del掉。
5.4 DataLoader 在 Windows 上卡死
现象:num_workers > 0时程序卡在第一个 batch,没有任何报错。
原因:Windows 的进程启动方式是spawn,子进程会重新导入主模块,如果训练代码不在if __name__ == "__main__":保护下,就会无限递归。
解决:把训练入口包进if __name__ == "__main__":,或者临时把num_workers设成 0 确认是不是这个问题。
5.5 模型保存后加载结果不一致
现象:训练完保存state_dict,重新加载后预测结果和保存前对不上。
原因:保存时模型在train()模式,加载后没有调eval(),Dropout 和 BatchNorm 的行为不一致。
解决:加载后固定加model.eval()。如果保存的是整个模型(torch.save(model))而不是state_dict,还要注意类定义必须和保存时完全一致,否则反序列化会失败。我一般只存state_dict,加载时先实例化模型再load_state_dict。
6. 把样章当脚手架:三个可以直接搬进项目的技巧
样章只有 32 页,但它给的三个章节恰好覆盖了 PyTorch 项目里最常复用的三段代码。我的习惯是把它们拆成三个独立文件:train_loop.py、dataset.py、regression_template.py,新项目直接复制改参数。
第一个技巧是训练循环的「最小可复现版本」。不要一上来就加早停、检查点、学习率调度,先把zero_grad -> forward -> loss -> backward -> step这五步跑通,确认 loss 在降,再加其他功能。样章第 8 章的代码就是这个最小版本,我每次开新项目都会先跑一遍它,确认环境和数据管道没问题。
第二个技巧是 DataLoader 的参数组合。我整理了一个速查表,按场景直接选:
| 场景 | batch_size | num_workers | pin_memory | shuffle |
|---|---|---|---|---|
| CPU 调试 | 16 | 0 | False | True |
| 单 GPU 表格数据 | 64 | 4 | True | True |
| 单 GPU 图像数据 | 32 | 8 | True | True |
| 多 GPU | 按显存调 | 8 | True | True |
| 验证/测试 | 128 | 4 | True | False |
第三个技巧是回归项目的评估闭环。样章只讲到训练,但实际交付时业务方要的是原始尺度上的误差。我习惯在训练脚本最后加一段:
def evaluate(model, X_test, y_test, y_scaler=None): model.eval() with torch.no_grad(): pred = model(X_test) if y_scaler is not None: pred = y_scaler.inverse_transform(pred.numpy()) y_true = y_scaler.inverse_transform(y_test.numpy()) else: pred = pred.numpy() y_true = y_test.numpy() mae = abs(pred - y_true).mean() rmse = ((pred - y_true) ** 2).mean() ** 0.5 return {"MAE": mae, "RMSE": rmse}逻辑说明:inverse_transform把预测和目标都还原到原始尺度,算出来的 MAE 和 RMSE 才能直接写进报告。如果目标做过对数变换,逆变换要用对应的expm1。
参数说明:y_scaler为None时表示没做目标标准化,直接输出原始预测。这个函数不依赖训练时的任何中间变量,可以单独导入到评估脚本里用。
从那以后我每次拿到新的 PyTorch 项目,都会先找一份类似的样章或最小示例,把训练循环、DataLoader、评估这三段跑通,再往里填业务逻辑。这份deep-learning-with-pytorch-sample.pdf的价值不在于它讲了多少,而在于它把最容易被跳过的基础环节单独拎了出来。希望帮到你。
本文还有配套的精品资源,点击获取