简介:面向深度学习初学者与希望系统掌握 PyTorch 实战的开发者,这份视频课程配套资料提供可对照运行的源代码、课件与实验素材,帮助读者从环境配置、模型搭建到训练调参逐步落地,内容覆盖卷积神经网络、循环神经网络、生成对抗网络与图卷积等典型主题。压缩包共 142 个文件,约 85.9MB,包含 55 个 PDF 课件、39 个 Python 脚本,并配有 PNG 图示、Notebook 交互笔记、CSV 数据以及 XML、CFG、TXT 等工程与说明文件,便于按章节查阅代码、复现实验和整理学习笔记。资源中附有 Anaconda、CUDA 10.0 与国内镜像源配置说明,可减少环境准备阶段的阻碍;代码与课件相互对应,适合跟课练习、对照调试和课后复盘。目前已有 2679 人学习下载,适合需要补齐工程细节、查阅课件要点或系统梳理 PyTorch 建模流程的读者使用。
1. 一份配套源码能省掉多少折腾:PyTorch 深度学习教程的实战定位
很多人学 PyTorch 的第一道坎来自脚本跑不起来,而非反向传播公式本身。这份 PyTorch 深度学习教程的配套源码和 PPT,把入门实战里讲到的模型都落成了可运行的脚本,覆盖卷积神经网络、循环神经网络和生成对抗网络几条主线,幻灯片还能当复习提纲。它适合刚学完 Python 和线性代数、想跑通第一个训练循环的人,也适合带课的人直接拿去课堂演示。网上不少教程的代码和视频对不上号,这份资源最大的价值是代码目录和课程章节一一对应,看到哪一节就翻到对应目录跑一遍,出问题也能定位到具体那几行。下面按工程骨架、核心模块、踩坑记录、二次开发四个层次拆开讲。
2. 工程骨架与技术栈:先看清目录再动手
拿到一份教程代码,最怕上来就python train.py,报错一屏接一屏,连从哪查都不知道。比较稳妥的顺序是先看依赖文件、再看目录分层、最后确认数据往哪放,这三步走完,后面跑脚本心里才有底。
2.1 依赖环境与目录布局
这套 PyTorch 教程代码的主线依赖不复杂:Python 3.8 以上、PyTorch 本体、torchvision 负责数据集和图像变换、numpy 和 matplotlib 做数值与可视化、tqdm 看进度条,处理图片时再加 pillow。我一般会先建独立虚拟环境,避免和系统里已有的包版本打架,尤其是 CUDA 版本这块,装错一次够折腾半天。
# 新建独立环境,避免污染系统 Python python -m venv venv source venv/bin/activate # Windows 下用 venv\Scripts\activate # 先装 PyTorch 和 torchvision,版本按显卡驱动选 pip install torch torchvision # 再补教程里常用的辅助库 pip install numpy matplotlib tqdm pillow安装命令本身没什么玄学,关键在于torch和torchvision的版本要配套。CUDA 11.8 的驱动配了 CPU 版 torch,跑起来不报错但慢得让人怀疑人生,torch.cuda.is_available()一眼就能验证。装完先跑一句python -c "import torch; print(torch.__version__, torch.cuda.is_available())",输出版本号和True再往下走。
目录结构通常是按课程章节切的,方便对照视频找代码:
pytorch-tutorial-code/ ├── chapter02_linear/ # 线性回归、softmax 回归 ├── chapter05_cnn/ # 卷积神经网络图像分类 ├── chapter07_rnn/ # 循环神经网络序列建模 ├── chapter09_gan/ # 生成对抗网络 ├── utils/ # 公共工具:随机种子、日志、可视化 ├── data/ # 数据集存放目录 └── requirements.txtdata/目录一般不放仓库里,脚本第一次运行时会自动下载数据集,比如 CIFAR-10、MNIST 这类公开数据。utils/里通常藏着set_seed、AverageMeter、save_checkpoint这类小工具,读代码时先扫一眼这里,后面看训练脚本会省很多猜测。
2.2 训练脚本的通用骨架
教程里每一章的入口脚本长得都差不多:解析参数、建模型、准备数据、写训练循环、跑验证、存权重。把这套骨架吃透,换数据集、换网络结构都只是替换其中一两个函数。下面这段是精简后的通用结构,实际代码会更长,但主干就是这么几块。
import argparse import torch from torch import nn from torch.utils.data import DataLoader def build_model(cfg): """按配置返回模型,方便后续替换网络结构""" if cfg.arch == "cnn": from models.cnn import SimpleCNN return SimpleCNN(num_classes=cfg.num_classes) raise ValueError(f"unknown arch: {cfg.arch}") def train_one_epoch(model, loader, criterion, optimizer, device): model.train() # 切到训练模式,影响 Dropout/BatchNorm total_loss, total_num = 0.0, 0 for x, y in loader: x, y = x.to(device, non_blocking=True), y.to(device, non_blocking=True) optimizer.zero_grad() # 清空上一轮梯度,漏了会累加 logits = model(x) loss = criterion(logits, y) loss.backward() optimizer.step() total_loss += loss.item() * x.size(0) # 按样本数加权,避免最后一个 batch 偏小 total_num += x.size(0) return total_loss / total_num def main(): parser = argparse.ArgumentParser() parser.add_argument("--arch", default="cnn") parser.add_argument("--batch-size", type=int, default=128) parser.add_argument("--epochs", type=int, default=50) parser.add_argument("--lr", type=float, default=1e-3) parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu") cfg = parser.parse_args() device = torch.device(cfg.device) model = build_model(cfg).to(device) criterion = nn.CrossEntropyLoss() optimizer = torch.optim.Adam(model.parameters(), lr=cfg.lr) # 数据加载和训练循环在此展开几个参数值得单独说。loss.item()会从计算图里取出标量,但只取当前 batch 的平均损失,乘上x.size(0)才是这个 batch 的总损失,最后除以总样本数得到的才是整个 epoch 的真实均值。non_blocking=True只有在pin_memory=True的 DataLoader 配 GPU 时才有意义,CPU 训练时可以去掉。argparse里把--device做成可覆盖的,是为了在 GPU 被占满时能临时退回 CPU 调试,不至于改代码。
2.3 随机种子与可复现性
教程代码跑出来的结果每次不一样,很多人第一反应是模型写错了。深度学习本身有随机性,数据打乱、权重初始化、Dropout 都会引入噪声,但做对比实验时至少要保证同一份代码两次运行结果接近。惯常做法是在入口脚本最前面固定随机种子。
import random import numpy as np import torch def set_seed(seed: int = 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) torch.cuda.manual_seed_all(seed) # 多卡时也要固定 torch.backends.cudnn.deterministic = True # 卷积算法可复现 torch.backends.cudnn.benchmark = False # 关闭自动调优,换确定性deterministic=True会让 cuDNN 只选确定性算法,代价是部分卷积实现变慢;benchmark=False同理,关掉自动选最快卷积算法的行为。训练初期调结构时我会打开这两个开关,等结构定下来、只看最终精度时再关掉换速度。如果只固定torch.manual_seed却忘了cuda.manual_seed_all,多卡环境下仍然会出现结果漂移。
3. 卷积网络模块拆解:从一个图像分类脚本看训练循环
CNN 这一章通常是整套教程里代码量最大、也最值得逐行读的部分。它把数据增强、模型定义、训练验证、学习率调度、权重保存串成了一条完整链路,后面 RNN 和 GAN 章节都是在这条链路上换零件。
3.1 模型定义与 forward 链路
入门教程里的 CNN 一般不会一上来就堆 ResNet,而是用两到三个卷积块把Conv2d → BatchNorm2d → ReLU → MaxPool2d这个组合讲清楚。下面这个结构对 32×32 的输入比较友好,通道数从 3 爬到 64,空间尺寸缩两次。
import torch from torch import nn class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.features = nn.Sequential( nn.Conv2d(3, 32, kernel_size=3, padding=1), # 32x32 -> 32x32 nn.BatchNorm2d(32), nn.ReLU(inplace=True), nn.MaxPool2d(2), # -> 16x16 nn.Conv2d(32, 64, kernel_size=3, padding=1), nn.BatchNorm2d(64), nn.ReLU(inplace=True), nn.MaxPool2d(2), # -> 8x8 ) self.classifier = nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 256), # 尺寸必须和 feature map 对上 nn.ReLU(inplace=True), nn.Dropout(0.5), nn.Linear(256, num_classes), ) def forward(self, x): x = self.features(x) return self.classifier(x)padding=1配kernel_size=3是保持空间尺寸不变的常用组合,两次MaxPool2d(2)把 32 变成 16 再变成 8,所以全连接层输入是64 * 8 * 8。这个数字一旦算错,报错信息会是mat1 and mat2 shapes cannot be multiplied,新手很容易卡在这里。inplace=True的 ReLU 省一点显存,但在需要保留原始激活做可视化的场景下要关掉。Dropout(0.5)放在全连接层前是分类任务的常规操作,卷积层里一般不用 Dropout,靠 BatchNorm 做正则。
3.2 训练循环与验证
验证函数和训练函数最大的区别是model.eval()加torch.no_grad()。前者把 BatchNorm 切到用滑动统计量、把 Dropout 关掉,后者切断计算图省显存。两个都漏掉的话,验证精度会明显偏低,而且显存占用虚高。
@torch.no_grad() def evaluate(model, loader, criterion, device): model.eval() correct, total, loss_sum = 0, 0, 0.0 for x, y in loader: x, y = x.to(device), y.to(device) logits = model(x) loss_sum += criterion(logits, y).item() * x.size(0) pred = logits.argmax(dim=1) # 取分数最大的类别 correct += (pred == y).sum().item() total += x.size(0) return loss_sum / total, correct / totallogits.argmax(dim=1)返回的是每个样本的预测类别索引,(pred == y).sum().item()统计猜对的个数。这里没有做 softmax,因为CrossEntropyLoss内部已经包含 log-softmax,模型输出 logits 就行,多做一次 softmax 反而会让数值不稳定。训练循环里每跑完一个 epoch 调一次evaluate,把 loss 和 acc 记下来,画曲线时能看出过拟合是从第几个 epoch 开始的。
3.3 数据加载与增强
图像分类的精度提升,一半靠数据增强。教程里通常会演示随机裁剪、随机翻转、归一化这套组合,顺序不能乱:先做几何变换,再ToTensor把 PIL 图变成张量,最后Normalize。
from torchvision import datasets, transforms from torch.utils.data import DataLoader train_tf = transforms.Compose([ transforms.RandomCrop(32, padding=4), # 先补边再随机裁,保留边缘信息 transforms.RandomHorizontalFlip(), # 水平翻转,概率默认 0.5 transforms.ToTensor(), # HWC uint8 -> CHW float [0,1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)), # 归一化到 [-1,1] ]) train_set = datasets.CIFAR10(root="data", train=True, download=True, transform=train_tf) train_loader = DataLoader( train_set, batch_size=128, shuffle=True, num_workers=4, pin_memory=True, # 锁页内存,配合 GPU 加速数据传输 )Normalize的均值方差按数据集统计量填,教程里统一用 0.5 是为了演示方便,实际项目里应该用训练集算出来的真实均值和标准差。num_workers=4在 Linux 上通常没问题,Windows 下如果写在if __name__ == "__main__":外面会反复启动子进程,这点后面避坑章节会细说。pin_memory=True只在 GPU 训练时有收益,纯 CPU 环境可以去掉。
3.4 优化器、调度器与权重保存
CNN 章节一般会用 Adam 起步,再展示 SGD + momentum 的对比。学习率调度用余弦退火比较常见,训练后期学习率降下来,精度还能再抬一点。
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50) for epoch in range(50): train_loss = train_one_epoch(model, train_loader, criterion, optimizer, device) val_loss, val_acc = evaluate(model, val_loader, criterion, device) scheduler.step() # 每个 epoch 结束后更新学习率 torch.save({ "epoch": epoch, "model": model.state_dict(), # 只存参数,不存整个对象 "optimizer": optimizer.state_dict(), "scheduler": scheduler.state_dict(), }, f"ckpt/epoch_{epoch}.pt")存state_dict()而不是整个model,好处是加载时不依赖原来的类定义路径,换目录、换机器都不容易出问题。weight_decay=1e-4是 Adam 下的常规起点,太大模型欠拟合,太小又压不住过拟合。T_max=50要和总 epoch 数对齐,设成 10 的话学习率很早就降到接近零,后面几十轮基本白跑。断点续训时把三个state_dict一起恢复,学习率才能接着原来的节奏走。
4. 循环网络与生成模型:RNN 与 GAN 两条线的实现差异
CNN 处理的是网格结构,RNN 和 GAN 处理的是序列和分布,代码骨架相似但训练节奏完全不同。这两章分开看容易,放一起对比反而能看清 PyTorch 里哪些是通用套路、哪些是任务特有的写法。
4.1 RNN 序列建模:hidden state 怎么传
字符级语言模型是教程里最常见的 RNN 示例:输入一段字符序列,预测下一个字符。核心是nn.Embedding把字符索引变成向量,nn.LSTM沿时间步滚动,nn.Linear把 hidden state 映射回词表大小。
class CharRNN(nn.Module): def __init__(self, vocab_size, embed_dim=128, hidden_dim=256, num_layers=2): super().__init__() self.embed = nn.Embedding(vocab_size, embed_dim) self.rnn = nn.LSTM( embed_dim, hidden_dim, num_layers=num_layers, batch_first=True, # 输入形状 (batch, seq, feature) dropout=0.3, # 多层 LSTM 时生效,单层会被忽略 ) self.fc = nn.Linear(hidden_dim, vocab_size) def forward(self, x, hidden=None): x = self.embed(x) # (B, T) -> (B, T, E) out, hidden = self.rnn(x, hidden) # out: (B, T, H) logits = self.fc(out) # (B, T, V) return logits, hiddenbatch_first=True是个容易忽略的参数,默认 LSTM 期望(seq, batch, feature),设成 True 后输入输出都按(batch, seq, feature)排,和 DataLoader 出来的形状一致,少一次 transpose。hidden在训练时通常传None让 PyTorch 自动初始化,做文本生成时才需要把上一步的 hidden 传回来保持上下文。序列太长时不能整个序列一次反传,常见做法是按固定长度切段,每段反传一次,hidden 截断后继续传,也就是常说的 TBPTT,教程里一般会在注释里标出来。
损失函数用CrossEntropyLoss时要注意形状对齐:logits是(B, T, V),标签是(B, T),需要logits.view(-1, vocab_size)和target.view(-1)拉平后再算。
4.2 GAN 对抗训练:生成器与判别器的节奏
GAN 的代码量不大,坑主要在训练循环。生成器和判别器要分开优化,判别器更新时生成器的输出必须detach(),否则梯度会顺着判别器回流到生成器,两边一起动,训练直接乱套。
class Generator(nn.Module): def __init__(self, z_dim=100, img_dim=784): super().__init__() self.net = nn.Sequential( nn.Linear(z_dim, 256), nn.LeakyReLU(0.2, inplace=True), nn.Linear(256, 512), nn.LeakyReLU(0.2, inplace=True), nn.Linear(512, img_dim), nn.Tanh(), # 输出压到 [-1,1],和输入归一化对齐 ) def forward(self, z): return self.net(z)训练循环的骨架如下,判别器和生成器各算一次 loss、各走一次step:
for real, _ in loader: real = real.view(real.size(0), -1).to(device) # (B,1,28,28) -> (B,784) bsz = real.size(0) # ---- 判别器:真样本判 1,假样本判 0 ---- z = torch.randn(bsz, z_dim, device=device) fake = G(z).detach() # 关键:切断生成器梯度 d_loss = criterion(D(real), torch.ones(bsz, 1, device=device)) \ + criterion(D(fake), torch.zeros(bsz, 1, device=device)) optimizer_D.zero_grad() d_loss.backward() optimizer_D.step() # ---- 生成器:骗判别器判 1 ---- z = torch.randn(bsz, z_dim, device=device) g_loss = criterion(D(G(z)), torch.ones(bsz, 1, device=device)) optimizer_G.zero_grad() g_loss.backward() optimizer_G.step()detach()那行是整段代码里最容易漏的,漏了之后判别器 loss 照样下降,但生成器学出来的东西全是噪声,看 loss 曲线根本发现不了,只能靠定期存图肉眼判断。GAN 的优化器一般用 Adam,lr=2e-4,betas=(0.5, 0.999),beta1 调低是为了减少动量带来的震荡。判别器太强时生成器梯度会消失,常见做法是判别器每更新两次、生成器更新一次,或者给判别器加标签平滑。
4.3 三条线的调参侧重点
CNN、RNN、GAN 虽然共用一套 PyTorch API,调参时盯的指标和瓶径完全不同。下面这张表是读教程代码时整理的对比,实际调的时候可以按这个顺序排查。
| 维度 | CNN 分类 | RNN 语言模型 | GAN |
|---|---|---|---|
| 常用优化器 | Adam 起步,后期换 SGD | Adam | Adam,lr=2e-4,betas=(0.5,0.999) |
| 关键监控指标 | 验证集准确率 | 困惑度、生成样本 | D/G loss 平衡、样本质量 |
| 归一化手段 | BatchNorm2d | LayerNorm、Dropout | 谱归一化、权重裁剪 |
| 显存主要占用 | 大 batch + 高分辨率 | 序列长度 × hidden 维度 | 双模型同时驻留显存 |
| 典型失败模式 | 过拟合、学习率过大 | 梯度消失、长依赖学不到 | 模式崩塌、判别器过强 |
CNN 的调参空间主要在数据增强和学习率调度,模型结构定下来后改动收益有限。RNN 的瓶颈在序列长度和 hidden 维度,梯度消失靠 LSTM 的门控缓解,但序列一长还是得上注意力机制。GAN 没有明确的收敛指标,D loss 和 G loss 此消彼长是常态,得靠定期采样看图判断,教程里通常会配一个save_image的工具函数。
5. 常见问题排查:环境、显存、梯度三类高频翻车
教程代码跑不通,八成不是模型写错,而是环境或数据形状对不上。下面这几条是我在复现这类教程时反复遇到的,每条按现象、原因、解决三步写,照着排查能省不少时间。
5.1 torch.cuda.is_available() 返回 False
现象:脚本里写了.to(device),但device一直是cpu,训练速度慢到无法接受,nvidia-smi里也看不到进程。
原因通常有三种:装的是纯 CPU 版 torch;显卡驱动版本低于 PyTorch 要求的 CUDA 版本;虚拟环境里同时存在多个 torch 版本,import torch导入的不是刚装的那个。
解决办法是先确认torch.__version__里带不带+cu后缀,再跑torch.version.cuda看编译时的 CUDA 版本,最后对照nvidia-smi右上角的驱动支持版本。三者对不上就重装对应版本的 torch,不要试图靠改环境变量绕过去。
5.2 训练几个 batch 就显存溢出
现象:报CUDA out of memory,但 batch size 明明设得很小,模型参数量也不大。
原因往往是显存里残留了上一个进程的缓存,或者验证阶段忘了torch.no_grad(),计算图一直挂着不释放。另一种情况是保存了带梯度的中间变量,比如在列表里累积 loss 张量而不是loss.item()。
解决办法:先nvidia-smi看是不是有僵尸进程占着显存,有就kill掉;检查验证函数有没有加@torch.no_grad();把total_loss += loss改成total_loss += loss.item()。这三步走完还爆,再考虑梯度累积或混合精度训练,不要一上来就换小模型。
5.3 loss 卡在某个值不动
现象:训练 loss 前几个 epoch 降一点,之后几乎水平,验证精度也不涨。
原因可能是学习率太大导致在最优点附近震荡,也可能是数据没打乱、每个 batch 都是同一类样本,还可能是标签和输出维度对不上但恰好没报错。GAN 里则是判别器太强,生成器梯度接近零。
解决办法:先把学习率降一个数量级试两轮;确认 DataLoader 的shuffle=True;打印一个 batch 的logits.shape和target.shape对齐;GAN 场景下调低判别器学习率或减少判别器更新频率。loss 不降时不要急着改模型结构,先把这几点排掉。
5.4 DataLoader 多进程卡死或报错
现象:脚本跑到for x, y in loader就停住,或者 Windows 下报RuntimeError: An attempt has been made to start a new process...。
原因在 Windows 和 macOS 上,多进程启动方式和 Linux 不同,num_workers > 0时子进程会重新 import 主模块,如果主逻辑没放在if __name__ == "__main__":下面,就会无限递归启动进程。
解决办法:把训练入口包进if __name__ == "__main__":;Windows 下先把num_workers设为 0 验证逻辑通不通,通了再往上加;如果数据集对象里有不可序列化的成员,也会导致子进程启动失败,检查一下自定义 Dataset 的__init__里有没有打开的文件句柄。
5.5 保存的模型加载时报 key 不匹配
现象:load_state_dict报Missing key(s)或Unexpected key(s),明明存的时候没改结构。
原因通常是保存时用了torch.save(model, path)存整个对象,加载时换到了另一台机器或改了类名;也可能是训练时用了nn.DataParallel,存下来的 key 前面多了module.前缀。
解决办法:统一用state_dict()保存;如果已有带module.前缀的权重,加载前做一次 key 重命名,把module.去掉;结构确实改过的话,用strict=False加载并打印缺失的 key,确认是不是只缺最后那个分类层。
6. 二次开发:把教程代码改成自己的实验脚手架
教程代码能跑通只是起点,真正要用起来得改成能反复做实验的脚手架。我一般先做两件事:把散落在脚本里的超参数抽成配置,再给训练循环加断点续训和日志。
6.1 用配置类收拢超参数
argparse适合命令行覆盖,但参数一多就乱。常见做法是加一个 dataclass 或 YAML 配置,把模型、数据、训练三组参数分开,脚本里只读配置,不写魔法数字。
from dataclasses import dataclass, field @dataclass class TrainConfig: batch_size: int = 128 epochs: int = 50 lr: float = 1e-3 weight_decay: float = 1e-4 device: str = "cuda" ckpt_dir: str = "ckpt" seed: int = 42 @dataclass class DataConfig: root: str = "data" num_classes: int = 10 num_workers: int = 4这样换数据集时只改DataConfig,换模型时只改build_model里的分支,实验记录也好对齐。配置类不替代argparse,可以在argparse解析完之后用命令行值覆盖 dataclass 字段,两者配合用。
6.2 断点续训与日志
断点续训的关键是把模型、优化器、调度器、当前 epoch、最佳精度一起存,恢复时按顺序加载。日志方面至少记录每个 epoch 的训练 loss、验证 loss、验证精度和学习率,后面画曲线、对比实验都靠它。
def save_ckpt(path, model, optimizer, scheduler, epoch, best_acc): torch.save({ "model": model.state_dict(), "optimizer": optimizer.state_dict(), "scheduler": scheduler.state_dict(), "epoch": epoch, "best_acc": best_acc, }, path) def load_ckpt(path, model, optimizer, scheduler, device): ckpt = torch.load(path, map_location=device) model.load_state_dict(ckpt["model"]) optimizer.load_state_dict(ckpt["optimizer"]) scheduler.load_state_dict(ckpt["scheduler"]) return ckpt["epoch"], ckpt["best_acc"]map_location=device不能省,GPU 上存的权重拿到 CPU 机器上加载时,不加这个参数会直接报错。恢复后从epoch + 1开始训练,不要从头再来,否则调度器的步数会和实际 epoch 对不上。
6.3 验证改造是否生效
改完脚手架别急着跑长训练,先用小数据验证链路通不通:把训练集截到几百张、epoch 设成 2、batch size 设成 8,跑一遍看 loss 有没有下降、权重有没有存下来、断点能不能恢复。这一步花不了几分钟,但能排掉大部分形状不匹配和路径问题。验证通过后再放大数据量和 epoch,出问题时也容易判断是数据规模导致的还是代码改动导致的。
我在改这类教程代码时踩过一次坑:为了让日志更详细,在训练循环里把每个 batch 的 loss 都 append 到一个列表里,跑长训练时内存一路涨到把进程撑爆。从那以后我每次加日志都会先确认存的是标量还是张量,长时间训练前强制用小数据跑一遍内存观察。希望这些经验帮到你少走几步弯路。
本文还有配套的精品资源,点击获取