简介:PDF电子书《Python深度学习:基于PyTorch》是机械工业出版社推出的PyTorch深度学习入门教程,面向零基础或刚入门的开发者与数据科学爱好者,力求通过通俗讲解帮助读者快速建立深度学习知识体系。全书从Numpy基础入手,逐步讲解PyTorch基础操作、张量(Tensor)与自动求导、计算图、神经网络工具箱nn、数据处理工具箱以及可视化工具;随后深入机器学习基础,涵盖监督学习、过拟合、激活函数、损失函数与优化器选择;再进入视觉与文本两大应用方向,介绍卷积神经网络及LeNet、AlexNet、VGG、ResNet等经典模型,同时讲解循环神经网络、LSTM、GRU及词嵌入方法。此外,还专设生成式深度学习章节,逐一拆解VAE、GAN、CGAN、DCGAN的原理与实现;最后以人脸检测识别和迁移学习实战作为综合演练。书中对每个函数和用法都做了细致说明,并配有CIFAR-10分类、股票行情预测、图像生成等可运行案例,便于读者边学边练。资源为单个PDF文件,包体大小21.68MB,适合下载后按章节系统阅读、检索笔记;目前已有5662人学习浏览,内容源于大量编程实战经验,值得作为PyTorch入门与进阶的案头参考。
1. 手上的PDF很多,能跑通的PyTorch项目才是真学懂了
拿到《Python深度学习:基于PyTorch.pdf》这样的资料,很多人的第一反应是收藏、划线、存网盘。但真到动手时才发现:同样的模型结构,别人能训到90%准确率,自己跑出来却一直在60%上下打转;同一个损失函数,换了个数据集就loss爆炸;明明照着代码敲的,却报一堆看不懂的错。这不是你比谁笨,而是深度学习工程的坑根本不在书里——PDF教你的是张量、梯度、反向传播,实战教你的却是显存怎么省、数据怎么喂、训练怎么稳住。
这篇文章的定位很明确:以PyTorch作为深度学习落地的主线,把从环境搭建到模型训练的完整路径拆开揉碎,覆盖数据加载、模型构建、训练循环、断点恢复、性能调优这几个核心环节,再把最常翻车的点单独拉出来讲透。适合刚读完理论但动手就卡壳的初学者,也适合想系统排查自己训练流程的熟手。一句话,这是照着PDF学完之后,真正把代码跑通、把模型练出来的那种笔记。
2. 环境先行:把PyTorch装到能跑GPU,这步别靠感觉
很多初学者在环境上吃的亏,比在模型上吃的亏还多。装PyTorch有两种极端:一种是无脑pip install torch,装成CPU版本后跑大模型慢到怀疑人生;另一种是追求最新版,结果CUDA版本和驱动不匹配,torch.cuda.is_available()永远返回False。我一般建议的做法是:先确定显卡驱动支持的CUDA版本,再反推PyTorch的安装命令,最后做一次完整的GPU验证,一次装对。
2.1 用conda建独立环境:为CUDA、Python、包版本上三重保险
深度学习最忌讳把包装进base环境。不同项目的torch版本、CUDA版本、Python版本互相打架时,你会陷入“装一个包要卸掉另一个包”的死循环。独立环境是后悔药,提前吃。
# 创建Python 3.10环境,名字叫dl_practice conda create -n dl_practice python=3.10 -y conda activate dl_practice # 先确认显卡驱动最高支持到哪个CUDA版本 nvidia-sminvidia-smi右上角的CUDA Version是驱动支持的上限,不是当前已安装的版本。比如显示CUDA 12.1,那你可以放心安装支持CUDA 12.x的PyTorch;如果显示11.8,就只能向下兼容。这个数字决定了后面安装命令里cu118还是cu121的选择,选错了装出来大概率跑不了GPU。
2.2 安装命令怎么选:cu118与cu121的真实区别
PyTorch安装最稳的方式是走官网的install命令生成器,但那个页面给出的命令在国内网络环境下经常拉不动。我习惯手动指定国内源,把选择权握在自己手里。
# 安装CUDA 12.1对应的PyTorch稳定版(含torchvision) pip install torch torchvision --index-url https://download.pytorch.org/whl/cu121 # 如果网速不理想,用清华源做备选 pip install torch torchvision -i https://pypi.tuna.tsinghua.edu.cn/simplecu121和cu118的差别主要在编译时的CUDA Toolkit版本。PyTorch官方针对每个CUDA版本发布了独立的wheel包,cu121支持CUDA 12.1,cu118支持CUDA 11.8。如果你的驱动只支持11.8,装cu121的包也能装上,但运行时可能报错或者默默退回CPU计算,这是最隐蔽的坑。装完后一定要做下面这个验证。
import torch print(torch.__version__) # 确认版本号 print(torch.cuda.is_available()) # 必须为True print(torch.cuda.get_device_name(0)) # 输出显卡型号 print(torch.randn(3, 3).cuda()) # 实际跑一次张量搬运到GPU这四行验证缺一不可。很多人只查torch.cuda.is_available(),返回True就觉得万事大吉,结果模型训练时数据还在CPU上,慢得没法看。最后一行的torch.randn(3, 3).cuda()是真正把张量搬到显存里,这一步能过才说明PyTorch和驱动、CUDA之间是通的。
2.3 装完就崩的三种典型:版本、路径、环境混淆
我见过最多的问题是这三个。第一个是conda环境激活了但pip装到了别的环境里,检查方法很简单:which python和which pip看路径是否指向当前环境,如果指向base或者其他位置,说明PATH有问题。第二个是torch.cuda.is_available()返回True但训练时OOM,在2GB显存的旧卡上尤其常见,这时不要硬上大batch,后面有专门的显存优化方法。第三个是torch和torchvision版本不匹配导致导入报错,解决方式是pip install torchvision --upgrade让它对齐当前torch版本。
环境装好之后,下一步就是数据怎么进模型。这一步我见过太多人栽跟头——不是不会写Dataset,是不知道DataLoader里那些参数到底动了什么,导致训练速度慢、内存占用高、甚至结果不稳。下面这一章是重点。
3. 数据加载与预处理:Dataset写得对不对,直接决定训练能不能收敛
数据是深度学习的起点,但数据加载这个环节在PDF里往往只有几页带过。实际做项目时你会发现,一个写得粗糙的Dataset子类,轻则训练时CPU一直满负荷而GPU在空转,重则数据标签对不上导致模型学了错误映射。PyTorch官方文档给了很多接口,但怎么把接口组织成一套适合自己的数据流水线,需要把原理和参数一起弄明白。
3.1 写一个自己的Dataset:从文件路径到张量的完整链路
最常见的图像分类场景,数据按文件夹存放,一个子文件夹对应一个类别。这时继承torch.utils.data.Dataset是最标准的做法,核心是重写__len__和__getitem__两个方法。
from torch.utils.data import Dataset from PIL import Image import os class ImageFolderDemo(Dataset): def __init__(self, root_dir, transform=None): self.samples = [] # 每一项是 (图片路径, 类别名) self.classes = sorted(os.listdir(root_dir)) for cls_name in self.classes: cls_path = os.path.join(root_dir, cls_name) if not os.path.isdir(cls_path): continue for img_name in os.listdir(cls_path): if img_name.lower().endswith(('.jpg', '.png', '.jpeg')): self.samples.append( (os.path.join(cls_path, img_name), cls_name) ) self.transform = transform def __len__(self): return len(self.samples) def __getitem__(self, idx): img_path, cls_name = self.samples[idx] img = Image.open(img_path).convert('RGB') # 统一转RGB,避免RGBA导致shape不一致 if self.transform: img = self.transform(img) return img, self.classes.index(cls_name) # 返回张量和索引标签这个写法里有几个容易被忽略的工程细节。Image.open(...).convert('RGB')是为了防止数据集中混有RGBA模式的图片,如果漏掉这步,训练到一半会因shape不一致报错。标签返回索引而不是字符串,因为损失函数只接受数值。self.classes排序是为了保证标签的确定性,否则每次运行同一个类别对应不同索引,模型就白训了。
3.2 DataLoader参数:workers、pin_memory、shuffle 为什么省不下来
光是Dataset还不够,DataLoader才是真正驱动数据流动的引擎。很多人只写三行参数就完事,但每个参数背后都对应着一类训练性能问题。
from torch.utils.data import DataLoader from torchvision import transforms transform_ops = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(p=0.5), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) train_loader = DataLoader( train_dataset, batch_size=32, shuffle=True, # 训练必须打乱,防止模型学到样本顺序 num_workers=4, # 数据预处理并行进程数 pin_memory=True, # 将数据锁页,加速CPU到GPU的传输 drop_last=False # 最后一批不满时是否丢弃 )先说shuffle。训练集必须置为True,因为如果不打乱,每个batch内部的样本都来自同一个子文件夹,模型会学到“类别和位置相关”这种假规律;但验证集和测试集必须置为False,保持评估的确定性。num_workers不是越大越好,它受CPU核心数和图片解码速度影响,在Windows上设得过大还容易报DataLoader worker错误,一般取CPU逻辑核心数减一到四即可。pin_memory=True的作用是让GPU能从内存更快地取数据,配合non_blocking=True的数据搬运,训练吞吐能提升不少;drop_last建议设成True用于大规模训练,因为最后一批不满会导致BatchNorm层的统计量抖得厉害。
3.3 数据增强的边界:只会套Compose,等于没调参数
数据增强是PyTorch深度学习里最容易被低估的环节。很多人套用ImageNet的标准化参数mean=[0.485, 0.456, 0.406]然后什么都不调,这对自然图像是合理的,但对医学影像、卫星图、工业缺陷图这种灰度图或特殊分布的数据就不合适了。一个直观的做法是:先不标准化,把归一化前的数据可视化,观察像素分布范围,再决定是否沿用ImageNet参数。灰度图通常用单通道均值,而非三通道的通用值。
增强强度也不是越强越好。RandomHorizontalFlip对左右对称的数据集是好用的,但对文本方向敏感的数据就是灾难。RandomRotation(degrees=30)会让工业零件检测模型把旋转后的错误形状学进特征里。我一般会把增强分成两步:先加对任务语义无影响的(轻微翻转、裁剪、颜色抖动),再用一个小数据集做消融实验,看增强前后验证集到底涨了多少。
4. 模型构建与训练循环:从写网络到跑通一次完整训练
数据就绪后,真正的主战场在模型定义与训练循环。PyTorch的模型定义看起来简单——继承nn.Module、实现forward,但训练循环里藏着梯度清零、反向传播、断点保存、学习率调度这些决定成败的细节。这一章会从一个可运行的最小训练框架展开,讲清每个组件的存在理由和参数含义。
4.1 用nn.Module定义识别模型:forward里别堆张量操作
这里用一个轻量的卷积网络做例子,结构上能复用到大多数中小型图像任务。
import torch.nn as nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super(SimpleCNN, self).__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.ReLU(inplace=True), nn.MaxPool2d(2), ) self.classifier = nn.Sequential( nn.AdaptiveAvgPool2d((1, 1)), nn.Flatten(), nn.Linear(64, num_classes) ) def forward(self, x): x = self.features(x) x = self.classifier(x) return xAdaptiveAvgPool2d((1, 1))是个值得记住的技巧,它让最后的全连接层不需要关心输入图片的原始尺寸,224也好、300也好,都能池化成1x1的特征图。这样模型对不同分辨率输入的适应性变强了。nn.ReLU(inplace=True)能省一点显存,但要注意被复用的输入张量不能被后续操作再引用,否则会拿到被改过的值。forward里尽量只做模块的串联调用,别在中间堆一堆就地操作的张量运算,否则后续想改网络结构时逻辑会很混乱。
4.2 训练循环的标准骨架:zero_grad、backward、step的顺序不能乱
训练循环是PyTorch项目里最不该自己瞎改的代码段。每一步的顺序、每个API的位置,都是无数人用翻车经历换来的。
import torch.optim as optim model = SimpleCNN(num_classes=10).to(device) criterion = nn.CrossEntropyLoss() optimizer = optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-5) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=20) epochs = 50 for epoch in range(epochs): model.train() running_loss = 0.0 for x, y in train_loader: x, y = x.to(device), y.to(device) pred = model(x) loss = criterion(pred, y) optimizer.zero_grad() # 梯度清零,必须在backward之前 loss.backward() torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=5.0) optimizer.step() running_loss += loss.item() * x.size(0) scheduler.step() epoch_loss = running_loss / len(train_loader.dataset) print(f"Epoch {epoch+1:03d} | Loss {epoch_loss:.4f} | LR {scheduler.get_last_lr()[0]:.2e}")三点关键逻辑。optimizer.zero_grad()必须在loss.backward()之前执行,因为PyTorch的梯度是累加的,不清零会把多个batch的梯度混在一起,loss曲线会剧烈震荡。clip_grad_norm_按参数范数裁剪梯度,对深层网络和不稳定的任务几乎是必开项,max_norm=5.0是个较安全的起点,如果loss还爆炸就降到1.0。scheduler.step()放在每个epoch结束后更新学习率,CosineAnnealingLR会让学习率从初始值余弦降到接近0,这种退火方式比固定学习率更容易收敛到平坦的极小值。
4.3 模型保存与断点续训:中途停电,你不想从头再来
训练几十个epoch后断电是最无语的事故。保存断点不是只存权重,而是要存下 optimizer、scheduler、epoch 数,这样恢复后才能无缝接着跑。
# 保存断点 checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'scheduler_state_dict': scheduler.state_dict(), 'loss': epoch_loss, } torch.save(checkpoint, f'checkpoint_epoch_{epoch+1}.pth') # 加载断点续训 checkpoint = torch.load(f'checkpoint_epoch_{epoch+1}.pth', map_location=device) model.load_state_dict(checkpoint['model_state_dict']) optimizer.load_state_dict(checkpoint['optimizer_state_dict']) scheduler.load_state_dict(checkpoint['scheduler_state_dict']) start_epoch = checkpoint['epoch'] + 1这里有个血泪经验:scheduler的状态如果不保存,恢复训练后学习率会从初始值重新开始,前面几十个epoch的退火效果全部作废,模型大概率在错误的步长上震荡。另外,加载模型做推理时用load_state_dict就够,不用恢复optimizer;如果只做推理却保存了完整断点,文件体积会大不少。
5. 训练避坑手记:PyTorch项目里最常见的五个翻车现场
这个标题下的学习路径中,真正劝退人的永远是那些报错信息看不懂、loss曲线不下降、显存莫名不够的问题。以下五条踩坑记录,是按照出现频率排的,每一条都是亲历过的翻车现场。
5.1 明明有GPU却OOM:不是显存小,是batch size和缓存没管好
现象:torch.cuda.is_available()返回True,模型也很小,但一训练就报CUDA out of memory。原因:一是batch size设得太大,激活值把显存吃光了;二是PyTorch的显存缓存机制,训练结束时显存没有完全释放,下一次运行仍然显示占用。解决:先torch.cuda.empty_cache()看释放情况,再把batch size减半,如果瓶颈在输入图片尺寸,把Resize改成小一点的分辨率。还有一个更狠的方案,开启梯度累加,每跑两步或四步(accumulation steps)再更新一次梯度,等价于扩大batch size但不增加显存占用。
accumulation_steps = 4 optimizer.zero_grad() for i, (x, y) in enumerate(train_loader): pred = model(x.to(device)) loss = criterion(pred, y.to(device)) loss = loss / accumulation_steps # 均摊梯度 loss.backward() if (i + 1) % accumulation_steps == 0: optimizer.step() optimizer.zero_grad()5.2 loss变成NaN:学习率太大,或者数据里有脏值
现象:训练到中途,loss突然从1.5跳成nan,之后再也没恢复正常。原因:一般有两个,一个是学习率太大导致梯度更新越过最优点,另一个是输入数据里混入了NaN或无穷大的像素值。解决:先用torch.isnan检查输入数据,排掉脏数据;再把学习率降低一个数量级,比如从1e-3降到1e-4;如果仍然出现nan,检查模型的输出最后是否经过了softmax——如果CrossEntropyLoss你已经不需要手动softmax,再套一层会导致数值不稳定。最后的手段是给模型加权重初始化,比如nn.init.kaiming_normal_。
5.3 多个GPU环境下的device不匹配:一个.to(device)漏了全盘皆输
现象:模型在CPU上能跑,搬到GPU上就报Expected all tensors to be on the same device。原因:一部分张量(比如输入数据)在GPU上,另一部分张量(比如标签或模型参数)还在CPU上。解决:不要只给模型调用.to(device),输入和标签也要在每次batch迭代时搬到GPU。我的习惯是在训练循环开头写一个全局的device = torch.device("cuda" if torch.cuda.is_available() else "cpu"),然后模型、输入、标签三处都调用.to(device),一步不漏。
5.4 验证集的loss在下降但准确率不动:输出层和标签的类别映射错了
现象:训练集loss一直降,验证集准确率却纹丝不动,像一条水平线。原因:类别索引和classes列表的排序不一致,模型学到的输出索引和真实标签对不上。解决:回到Dataset的定义,检查self.classes.index(cls_name)与DataLoader读入的标签是否对应同一个排序逻辑。还有一种隐藏情况是标签从0开始,但有些数据集的标注从1开始,导致模型永远预测不对最后一个类。写一个小脚本,把训练集的前100个样本的标签可视化一遍,确认类别和索引是对的。
5.5 数据加载成了训练瓶颈:GPU利用率忽高忽低
现象:训练时GPU利用率在0%和99%之间反复横跳,训练速度忽快忽慢。原因:num_workers设置过小,GPU等数据的时间比计算的时间还长。解决:逐步加大num_workers,观察GPU利用率的变化;同时把pin_memory=True打开;如果数据集是海量小文件,建议先转成WebDataset或LMDB这种大文件格式,减少随机读盘的IO代价。Windows上num_workers超过8容易报错,最好保持在0到8之间。
6. 模型真的在学吗:用三招确认训练有效,别再白白烧卡
训练结束不代表任务完成。很多项目跑到最后loss下降得很漂亮,但一测试实际效果就露馅。所以最后一招,是教你验证一个训练好的模型到底有没有真正学到东西,并形成自己的检查习惯。
第一招是过拟合单批数据。取训练集里的一个batch,让模型在这一个batch上反复迭代几十次,观察loss是否持续下降到一个极小值。如果连一个batch都拟不上去,说明模型结构或数据管道的存在bug,这时去调超参都是白费功夫。第二招是画梯度范数曲线。在每个step后记录grad_norm = sum(p.grad.norm() for p in model.parameters()),画出范数值随训练的变化,正常情况下是逐步下降的,如果剧烈震荡不收敛,就该调学习率或clip阈值。第三招是把预测结果可视化。分类任务把错分样本排列成网格图,看模型到底犯的是哪些错;检测任务把预测框画回原图,判断是定位不准还是分类不准,这比任何指标都好用。
def check_grad_norm(model): total_norm = 0.0 for p in model.parameters(): if p.grad is not None: total_norm += p.grad.norm().item() ** 2 return total_norm ** 0.5把这行函数插进训练循环里每个step结束之后,前三轮epoch如果梯度范数在1e-5的级别,说明梯度消失了,这时应该换激活函数或加残差连接;如果梯度范数超过了100且持续上升,说明梯度爆炸了,clip值继续调低。
说到底,PyTorch深度学习的本职工作是把PDF上的理论变成一个能用的系统。环境、数据、模型、训练、验证每一个环节都在劝退,但每踩过一个坑,下一次就少烧一张卡。我自己的习惯是,每跑完一个项目,都把遇到过的报错和解决方案记在项目目录的NOTE.md里,时间久了这就是最贴合自己工作流的技术手册。希望帮到你。
本文还有配套的精品资源,点击获取