资讯动态

PyTorch深度学习入门:张量、自动求导与模型训练实战

发布时间:2026/10/13 20:45:38 来源:尧图企业网站定制
做深度学习的人大概都绕不开PyTorch。哪怕你平时用的是TensorFlow或者Paddle遇到论文复现、快速验证想法、甚至只是跑别人的开源项目PyTorch几乎都是首选。我自己第一次接触PyTorch是在复现一篇生成模型的论文时当时照着教程敲了几十行代码突然发现“前向传播写起来就跟普通Python计算一样”反向传播却全都自动算完了。那一刻的体验跟之前手动推公式再一行行写梯度相比完全不是一个量级。这篇笔记是“深度学习笔记”系列的第二篇聚焦PyTorch基础。零基础可以把它当成入门路线图有基础但一直用别的框架的也可以当速查手册来翻。说白了PyTorch的核心就三件事张量Tensor、自动求导Autograd、网络搭建torch.nn。把这三件事的底层逻辑搞清楚再去看任何模型代码你都不会觉得陌生。1. 为什么选择PyTorch框架选型的底层逻辑1.1 动态计算图带来的灵活性深度学习框架一个很关键的区别是计算图的构建方式。TensorFlow早期版本属于静态图你先画好一张计算图再把数据灌进去执行。而PyTorch走的是动态图路线每次前向传播时在运行过程中同步构建计算图。也就是说你的Python代码跑了几行计算图就搭了几行代码跑到哪里图就建到哪里。这个特性对入门者友好得多。比如在写模型时你可以在forward函数里随便加if条件判断、for循环也可以中途用print打印某个中间变量的大小这些操作完全不会有框架层面的限制。换到静态图环境这些事往往得靠特殊算子才能实现调试起来非常折腾。我记得有一次实现一个变长序列的模型输入长度不定需要先排序再切分最后再动态合并。在PyTorch里就是纯Python逻辑改起来非常直观如果在静态图里做同样的操作得构造数据流依赖图工作量完全不在一个层级。所以对我个人来说动态图意味着思考方式可以保持“用Python的方式思考”而不是“用框架的方式思考”。1.2 与Python生态的自然融合PyTorch几乎把所有数据结构都定义成了张量但它没有把用户关在“自己的一亩三分地”里。它与NumPy的互转非常顺畅Tensor.cpu().numpy()就能转过去torch.from_numpy()就能转回来。这带来一个巨大的便利你可以用庞大的Python生态来处理数据、做可视化、写前后处理逻辑只把真正需要计算性能的部分放在PyTorch里跑。调试体验也是很多人的共同感受。因为动态图和Python的执行过程是绑定的你可以在IDE里给某个张量运算行打断点查看这一刻的shape、数值范围、梯度是否产生异常。这种“眼见为实”的调试办法对排查NaN、维度不匹配这类问题很有效果。我自己排查过一次loss不下降的问题就是在断点环境下逐个层看输出均值最后发现是某个卷积层的初始化方式不对激活输出被压到了饱和区。1.3 这套笔记怎么帮你入门后续章节的安排其实是按“跑通一条完整训练链路”的顺序来的先搭环境再认识张量然后弄明白自动求导接着处理数据、搭模型、写训练循环、保存模型最后给出一份踩坑清单。每一节我都会尽量带上“为什么这么做”——为什么DataLoader里常用batch_size32为什么每次迭代都要optimizer.zero_grad()你把这些问题理解了才算真的掌握了PyTorch而不只是复制粘贴代码。建议你阅读的时候把代码自己动手敲一遍不要觉得“我看懂了”就跳过。深度学习里的很多问题是运行时才暴露的小到维度对不上大到梯度爆炸亲手经历一次比看十篇教程都管用。2. 环境准备从零搭一套可用的PyTorch2.1 用conda管理你的Python环境我不建议把PyTorch直接装到系统默认的Python里尤其是你还同时在做其他Python开发的时候。不同项目依赖的Python版本、包版本很容易互相打架搞到最后升级一个包把另一个项目弄崩了极其难受。我用的是conda环境管理操作很简单。创建一个新环境并激活它conda create -n ptorch python3.10 conda activate ptorchPython版本不要贪新选一个PyTorch稳定支持的版本即可。我写这篇笔记时用的Python 3.10主流的PyTorch版本都支持得很好。接下来装基础依赖pip install numpy matplotlib这两个包不是PyTorch必需的但做实验大概率用得到建议一并装了。2.2 安装PyTorch并验证GPU是否可用PyTorch的安装方式建议按官方命令来用pip还是conda都行。我自己习惯用pip因为版本更新及时安装速度也快。pip install torch这里先不指定版本装的是与该Python环境兼容的稳定版本。如果你确实对CUDA有要求希望装对应CUDA编译版本的PyTorch可以指定安装源和版本号。但先说明一个事实把torch装上CPU也能跑真正影响你是否用GPU的是torch有没有安装对应CUDA版本。验证方法如下import torch print(PyTorch版本:, torch.__version__) print(CUDA是否可用:, torch.cuda.is_available()) if torch.cuda.is_available(): print(GPU型号:, torch.cuda.get_device_name(0))如果你的机器装了合适的显卡驱动和CUDAtorch.cuda.is_available()会输出True如果输出False说明当前安装的是CPU版本或者CUDA驱动没配对。第一次安装环境的朋友看到False先不要着急先用CPU跑通后续笔记不影响基础学习。注意一个环境里不要同时装CPU版本和CUDA版本。我第一次就被这个问题坑了——当时为了图快在同一个环境里安装了两次结果torch.cuda.is_available()一直是False后来卸载干净重装才正常。装PyTorch之前最好先明确自己的需求再装对应版本。2.3 环境管理的几条实操经验环境建多了之后很容易忘记哪个环境装了哪些包。我常用的排查命令是conda env list # 列出所有环境 conda list -n ptorch # 查看指定环境下的包如果发现环境不对劲直接整个环境删掉重建也不心疼。毕竟环境本身没什么“资产”代码和数据集不放在环境里就好。另外我强烈建议把项目依赖写进requirements.txt或environment.yml。这样哪怕你三个月后再回来复现一个命令就能重建环境不用靠脑子记。我踩过最深的坑就是环境崩了之后靠聊天记录一个个找回包之后凡是正经项目我都会把依赖清单提交到仓库里。3. 张量PyTorch里的“数组Plus”3.1 创建张量的几种常用方式张量Tensor是PyTorch里最核心的数据结构。你可以把它理解成支持GPU计算、支持自动求导的“超强NumPy数组”。先看一段常用的创建代码import torch # 从Python列表创建 a torch.tensor([1, 2, 3]) b torch.tensor([[1.0, 2.0], [3.0, 4.0]]) # 全0和全1张量 zeros torch.zeros(3, 4) ones torch.ones(2, 3) # 随机张量 rand torch.randn(3, 3) # 标准正态分布 rand_uniform torch.rand(2, 2) # 均匀分布[0, 1) # 等差数列 arange torch.arange(0, 10, 2) # 0, 2, 4, 6, 8你会发现这些API和NumPy长得非常像这也是为什么从NumPy转过来的朋友会觉得亲切。张量有dtype属性比如torch.float32、torch.int64不同dtype会影响内存占用和计算精度。做深度学习时默认应该尽量使用浮点类型。CPU上支持多种dtypeGPU上算子也是但混用类型在进行梯度反传时容易出现“数据类型不匹配”的报错看到这类报错先查dtype。3.2 形状变换view、reshape与维度操作张量的形状决定了它能否和另一个张量做运算。模型训练中九成以上的维度报错都出在对shape不够敏感。PyTorch提供了一整套形状操作x torch.randn(2, 3, 4) # view只改变形状不改变内存布局 y x.view(6, 4) # reshape类似view但更灵活必要时自动处理连续性 z x.reshape(2, 12) # 维度交换 x_t x.transpose(0, 2) # 从(2,3,4)变成(4,3,2) # 增加或删除维度大小为1的轴 x_unsqueeze x.unsqueeze(0) # (1, 2, 3, 4) x_squeeze x_unsqueeze.squeeze(0) # 变回(2,3,4)view和reshape的区别值得多说一句。view要求张量在内存中连续如果遇到transpose、切片等操作产生的非连续张量view可能报错reshape就更宽容。我在处理一批从图片加载来的数据时经常先做transpose再做view此时用view会报“RuntimeError: view size is not compatible”换成reshape就没问题。不过如果张量已经连续reshape也不会复制多余数据二者差别不大。3.3 广播机制不同形状张量也能直接运算广播是NumPy和PyTorch都有的一套张量运算规则。简单说当两个张量形状不完全相同但满足特定条件时系统会自动把维度“补齐”再运算。规则是从尾部维度开始对齐每个维度要么相等要么其中一个为1要么其中一个不存在。举个例子a torch.ones(3, 1) # 形状 (3, 1) b torch.ones(4) # 形状 (4,)a和b做加法时a先变成(3,4)b先变成(1,4)最终结果是形状(3,4)的张量。这个机制非常方便让我在写偏置项时可以直接用一个形状为(batch, 1)的向量加上形状为(batch, feature)的矩阵不需要手动复制。但广播也容易掩盖bug。如果我希望两个形状不匹配的张量直接报错我会先手动检查.shape甚至用assert。比如assert a.shape[-1] b.shape[-1], f末尾维度不一致: {a.shape}, {b.shape}3.4 与NumPy互转的隐秘坑张量和NumPy数组互转时有一个必须记住的细节在CPU上torch.from_numpy()和某个Tensor的.numpy()有时会共享内存。也就是说你在PyTorch这边改了数据NumPy那边也会变反过来也一样。arr np.array([1, 2, 3]) t torch.from_numpy(arr) t[0] 100 print(arr) # 输出 [100 2 3]数组也被改了这个特性在做数据预处理时容易踩坑。如果不想数据被悄悄修改用clone()显式复制t torch.from_numpy(arr).clone()把Tensor转NumPy时同理如果后续会同时使用两边的值最好.copy()一下避免后续操作互相影响。4. 自动求导让反向传播自动发生的秘密4.1 requires_grad与计算图自动求导是深度学习框架的灵魂。PyTorch里只要某个张量设置了requires_gradTrue框架就会开始记录你在它身上做过的所有运算这些运算组成一张“计算图”。图上的每个节点都是一个操作每个操作两端都连着输入输出张量。x torch.tensor(3.0, requires_gradTrue) y x ** 2此时y代表一个计算节点。y是2次幂运算的结果所以y有一个grad_fn属性指向的是产生它的那个操作。x没有grad_fn因为x是我们自己创建的叶子节点不是某个运算的产物。当你需要求梯度时调用y.backward()PyTorch会从y出发沿计算图反向走一遍逐个求出各叶子节点的梯度并把结果存到对应张量的.grad属性里。y.backward() print(x.grad) # tensor(6.)这个例子中y对x的导数是2*xx3所以梯度是6。就这么简单。但自动求导背后其实有很多细节值得研究比如计算图只在反向传播时“走”一遍之后默认会释放保证内存不被占用。所以在训练循环里每次前向传播都会重建计算图这也是动态框架的特点。4.2 backward()之后的常见误区第一次写训练循环的我犯过一个典型错误没有在反向传播前清零梯度。for epoch in range(5): prediction model(x) loss criterion(prediction, target) loss.backward() # 忘了 optimizer.zero_grad() optimizer.step()这段代码跑起来loss会越来越大。原因是PyTorch的梯度是“累积”的也就是说每个张量的.grad在每一步反向传播后不会自动归零而是不断累加。如果不手动清零上一轮的梯度会叠加到这一轮上再经过优化器更新参数参数更新的量就完全偏离了预期。所以标准训练循环里一定在backward()之前调用optimizer.zero_grad()这一步不要省略。4.3 detach()切断梯度传递的利器有时候你不希望某一个运算参与梯度计算。比如在生成对抗网络中更新生成器时不需要让判别器的梯度回传到生成器更新又比如你想取出张量的数值做日志而不希望它把梯度计算带出来。这时用detach()value tensor.detach() # 返回一个新张量它与原计算图断开了我在写监控指标时经常用loss.item()本身就不带梯度但如果需要对中间变量做复杂处理比如记录原始值就先用.detach()再转NumPy避免无意中保存了计算图引用导致内存只增不减。如果只是取一个Python数字loss.item()比loss.detach()更轻量。二者都不会参与梯度计算但item()只适用于标量而detach()适合任意形状的张量。5. 数据加载Dataset与DataLoader的正确用法5.1 自定义Dataset的标准模板训练深度学习模型数据加载是第一步。PyTorch推荐的做法是自定义Dataset类然后交给DataLoader去管理批量加载。自定义Dataset类必须实现三个方法init、len__和__getitem。__init__里读取文件列表__len__返回样本数量__getitem__负责取出一组样本并加工。from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, file_list, transformNone): self.file_list file_list self.transform transform def __len__(self): return len(self.file_list) def __getitem__(self, idx): # 这里根据idx读取文件做预处理后返回 sample load_file(self.file_list[idx]) if self.transform: sample self.transform(sample) return sample需要注意__getitem__返回的是“一个样本”而不是一个batch。batch的组装由DataLoader完成。DataLoader拿到多个样本后会把它们堆叠成批量张量。这就要求你的每个样本形状要一致否则批量堆叠时会报错。如果样本形状确实不一致比如变长序列就需要在collate_fn函数里做特殊处理。5.2 DataLoader的关键参数解读DataLoader最常用的参数有batch_size、shuffle和num_workers。我逐个说下实战感受batch_size决定每次喂给模型的样本个数。影响的是显存占用和梯度稳定性。太大容易显存溢出太小训练震荡。常用值从16到128不等需要根据机器情况调整。shuffle训练集一般设为True让每个epoch中的样本顺序不同避免模型记住固定顺序。验证集和测试集设为False。num_workers开启多个进程来预加载数据减小CPU与GPU之间数据搬运的等待时间。Windows环境有时会遇到多进程问题如果是初学先设成0也没关系跑通主力逻辑要紧。我做过一个对比同样的数据读取逻辑num_workers从0调到4之后每个epoch的训练时间大约缩短了30%。但这不是绝对的如果读取本身很快多进程开销反而更大。如果你发现自己GPU的利用率不高而CPU又很闲优先考虑增加num_workers。5.3 数据预处理的标准化操作torchvision.transforms是图像处理中最常用的工具包。一个典型的流程是from torchvision import transforms transform transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), # 把PIL图片转成Tensor并归一化到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) ])Normalize这一行看着简单但实际很关键。mean和std可以简单地理解为数据分布的中心和离散程度。把图像归一化到均值为0、方差为1附近有助于模型更快收敛。很多从零手写数据加载的同学会跳过Normalize结果就是训练初期loss波动特别大收敛也很慢。我第一次带着NumPy风格直接喂原始像素值[0,255]loss跌得又慢又不稳定加了Normalize之后第一个epoch的loss下降速度明显快了。6. 构建模型从nn.Module走向更复杂的结构6.1 最简单的线性模型长什么样PyTorch定义模型的标准方式是继承torch.nn.Module并实现forward方法。以最简单的线性回归模型为例import torch.nn as nn class LinearModel(nn.Module): def __init__(self): super().__init__() self.linear nn.Linear(in_features2, out_features1) def forward(self, x): return self.linear(x)__init__里定义网络的各个层forward里定义数据如何流过这些层。这里有个很容易被新手忽略的点父类的__init__一定要调用也就是super().init()。如果不调用模型内部的很多注册机制不会生效你会遇到各种奇怪的属性错误。另外如果你在子模块中注册了多个层它们的参数会被自动收集到model.parameters()里。所以优化器可以直接拿model.parameters()来初始化不需要自己手动去收集每个参数。6.2 搭建一个简单的竞赛级卷积网络图像分类入门时LeNet是一个很好的参考。简化版本如下class SimpleCNN(nn.Module): def __init__(self): super().__init__() self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) self.pool nn.MaxPool2d(2, 2) self.fc1 nn.Linear(64 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) self.relu nn.ReLU() def forward(self, x): x self.pool(self.relu(self.conv1(x))) x self.pool(self.relu(self.conv2(x))) x x.view(x.size(0), -1) x self.relu(self.fc1(x)) x self.fc2(x) return x这段代码里x.view(x.size(0), -1)是典型的展平操作。它把形状为(batch, 64, 7, 7)的特征图变换成(batch, 6477)也就是把每个通道上的空间信息都拼成一个长向量然后接全连接层。这个操作本身不改变batch大小只改变单个样本的表示方式。在搭建完整模型时你可以把卷积、激活、池化写成一层层的也可以灵活组合多个子模块。PyTorch没有强制你必须按某种模板组织网络结构。这一点对论文复现特别友好因为很多论文里的连接方式并不常规。6.3 参数初始化的意义和常用做法模型在没有得到充分训练之前参数初始化影响着模型是否能够正常收敛。PyTorch中的全连接层和卷积层默认会使用特定的初始化方式但很多场景下我们还会手动指定初始化。一个常见的做法def init_weights(m): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) nn.init.zeros_(m.bias) model.apply(init_weights)xavier初始化是让权重方差和输入输出维度相匹配的一种方案能有效缓解梯度消失或梯度爆炸。如果是使用ReLU作为激活函数的网络kaiming初始化更常见。这些名词初看有点吓人实际上核心思想就是“让每层输出的数值尺度保持在合理范围”。你可以先记住这两种初始化方式后面碰到收敛问题时再回来研究。7. 训练循环让模型真正学到东西7.1 损失函数怎么选才不别扭损失函数衡量模型预测和真实值之间的差距。常见的选择nn.MSELoss()均方误差适合回归任务。比如预测房价、温度等连续数值。nn.CrossEntropyLoss()交叉熵损失适合多分类任务。PyTorch里它接收的是未经过softmax的原始logits所以你的模型最后一层不需要手动加softmax。有一个容易搞混的地方CrossEntropyLoss在PyTorch内部已经帮你做了softmax运算如果你在模型输出端再接了个nn.Softmax()反而会双重算softmax不仅多耗算力还可能让数值变得不稳定。很多时候“效果怎么变差了”的根源就在这里。7.2 优化器的选型与学习率训练神经网络靠优化器来更新参数。最常用的是SGD和Adam。SGD是带动量的随机梯度下降收敛稳定但需要你认真调学习率。Adam在SGD基础上有了自适应调节学习率的能力不够的话还能用AdamW。实验阶段我通常先上Adam快速把模型结构验证一遍等要追求最优效果时再换成SGD配合余弦退火之类的学习率调度。学习率的设置很关键。学习率太大loss会直接变成NaN或者来回震荡学习率太小模型训练几个epoch后loss下降肉眼可见地慢。一个常用的技巧是先试小数据集上的快速过拟合实验把学习率调到“能快速过拟合”的量级再放回完整数据集训练。比如分类模型常见的初始学习率可以设置在0.001到0.01之间。7.3 完整训练循环模板与逐段拆解下面是一个最基础的分类训练循环建议把它当成肌肉记忆model SimpleCNN() criterion nn.CrossEntropyLoss() optimizer torch.optim.Adam(model.parameters(), lr1e-3) num_epochs 10 for epoch in range(num_epochs): model.train() running_loss 0.0 for inputs, labels in train_loader: optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() avg_loss running_loss / len(train_loader) print(fEpoch [{epoch1}/{num_epochs}] Loss: {avg_loss:.4f})逐段拆解一下model.train()把模型切换到训练模式。如果模型里有Dropout、BatchNorm这类层它们在不同模式下行为不同。训练模式会启用Dropout和基于当前batch的BatchNorm统计所以训练必须带上这一行。optimizer.zero_grad()清空上一轮计算好的梯度。之前也提过不清空梯度会累积导致参数更新混乱。outputs model(inputs)前向传播走一遍网络得到预测结果。loss criterion(outputs, labels)计算损失值。此时loss是一个带有计算图连接关系的张量。loss.backward()从loss反向传播自动求出所有模型参数的梯度。这时模型里各个参数.grad都会被更新。optimizer.step()根据梯度和优化器设置更新模型参数。我在初学阶段总觉得顺序无所谓后来踩了几次坑才明白这几步的顺序是有逻辑依存的。zero_grad必须在backward之前否则梯度会累加step必须在backward之后否则你更新用的是上一轮的旧梯度。顺序不要指望“颠倒也可以”这是框架机制决定的。7.4 验证集与模型评估的模式切换训练完一个epoch通常要在验证集上看一下模型表现不能只盯着训练loss。验证阶段不需要反向传播因此可以显式关闭梯度计算既省内存也避免误操作。model.eval() with torch.no_grad(): total_correct 0 total_samples 0 for inputs, labels in val_loader: outputs model(inputs) preds outputs.argmax(dim1) total_correct (preds labels).sum().item() total_samples labels.size(0) val_acc total_correct / total_samples print(fVal Acc: {val_acc:.4f})model.eval()和torch.no_grad()是两回事。eval()影响的是模块行为尤其是BatchNorm和Dropoutno_grad()影响的是计算图构建。前者是“模型状态变了”后者是“不需要记录梯度了”。两者经常配合使用但严格说不是一回事没有互相替代的关系。每次我在验证集上评估完要接着继续训练时千万不要忘记把模型切回model.train()。我第一次忘了切回来结果下一个epoch的训练loss奇低验证准确率也高得离谱后来发现是Dropout被关闭了相当于用“全量模型”在训练数据上跑自然看着“很好”但泛化性并没提升。8. 模型保存与加载别在最后一步翻车8.1 推荐只保存state_dict模型训练完自然想把结果存下来。很多初学者第一次会直接保存整个模型torch.save(model, model.pth)这种做法的缺点是模型结构以序列化对象的形式保存受代码类定义版本影响很大。如果类定义改了路径或者换了Python版本加载时很容易报错。更稳妥的做法是只保存模型的参数state_dicttorch.save(model.state_dict(), model_weights.pth)加载时先构建出与训练时一样的网络结构再load_state_dictmodel SimpleCNN() model.load_state_dict(torch.load(model_weights.pth)) model.eval()load_state_dict需要键完全匹配。如果你的模型结构变了即使只是某层名字改了一下加载时也会报错或出现missing key。遇到这种问题先检查同名层的shape是否一致。8.2 完整检查点不止保存模型参数在训练时间很长的情况下只保存权重还不够。如果训练中途断电或者跑挂你想从断点续训除了权重还需要保存epoch、优化器状态、损失值等。这时用完整checkpointcheckpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: avg_loss, } torch.save(checkpoint, checkpoint.pt)加载时checkpoint torch.load(checkpoint.pt) model.load_state_dict(checkpoint[model_state_dict]) optimizer.load_state_dict(checkpoint[optimizer_state_dict]) start_epoch checkpoint[epoch] 1优化器的state_dict里保存了动量、学习率调度器的历史状态等信息。如果只恢复模型权重而不恢复优化器状态恢复后的训练表现可能与断开前有明显差异尤其是使用SGD和Adam这类有内部状态的优化器。8.3 设备相关的注意事项如果模型在CPU上训练但测试时机器有GPU直接用torch.load(model_weights.pth)加载可能会把张量默认加载到CPU上这与模型当前的device不匹配。好在PyTorch提供了map_location参数device torch.device(cuda if torch.cuda.is_available() else cpu) model.load_state_dict(torch.load(model_weights.pth, map_locationdevice))如果你是“CPU上保存、GPU上加载”或“GPU上保存、CPU上加载”这个参数就非常关键。我个人习惯是在保存时不做设备相关的操作加载时才通过map_location适配当前环境这样代码对不同机器的兼容性好很多。9. 踩坑记录与调试建议那些文档里查不到的经验9.1 维度匹配模型报错第一大户几乎每个初学者都会遇到这种报错RuntimeError: size mismatch, m1: [64 x 100], m2: [128 x 10]这通常发生在全连接层。其中一个常见原因是卷积层输出展平后的维度与fc1定义时指定的输入维度不一致。我在写自定义网络时特别喜欢先用一段假数据测试模型的forward是否通畅dummy_input torch.randn(1, 3, 224, 224) out model(dummy_input) print(out.shape)如果out.shape与预期不符立刻就能发现“哪里宽了”“哪里窄了”而不是等到训练时才被报错打断。多花一分钟做这个测试比训练时反复试错省太多了。9.2 loss变成NaN或突然爆炸loss变成NaN是训练过程中最让人头疼的问题之一。常见原因包括学习率过大、数据里有异常值、梯度爆炸等。我的排查顺序是这样的先看学习率把学习率调小两个数量级测试一下很多NaN都是学习率过大导致的。检查数据里是否存在NaN或无穷值尤其是输入特征没有做归一化时。检查损失是否计算了log(0)之类的操作交叉熵中常见需要在样本类别极度不均衡时注意。一个很有用的临时拦截措施是在训练循环里判断loss是否有效if not torch.isfinite(loss): print(Loss is not finite, stopping training) break虽然这不能解决问题但至少能帮你定位是在哪个epoch、哪一步开始变坏的。9.3 验证效果与训练效果差异过大如果模型在训练集上准确率极高验证集上却很差那基本是过拟合。应对办法是增加数据增强、增加Dropout强度、减少模型容量、使用权重衰减weight decay。但还有一种情况容易被忽略如果训练时忘记model.train()和model.eval()正确切换验证时的Dropout和BatchNorm行为不正确也会导致验证曲线异常。先检查模式切换是否做到位再考虑调模型复杂度。另外一个容易忽略的坑是数据泄漏。有一次我在做数据增强时不小心把验证集也做了随机增强结果验证集指标每次都在波动模型根本没有得到真实评估。之后我严格要求自己只有训练集才做随机增强验证集和测试集只做确定型预处理。9.4 训练速度慢的排查思路变量很多不只是GPU好坏。常见的原因有数据加载成为瓶颈。可以通过减少num_workers0的对比实验来判断如果改为0后反而变快说明多进程设置得不合适。GPU利用率不高。可以打印一下每个step的时间重点关注前向、反向以及数据搬运时间的占比。模型结构本身太大或者batch_size设得过大。排查这类问题没有捷径。最好的办法是“控制变量法”一次只改一个参数记录训练速度变化。我习惯写一个简单的计时装饰器把每一段耗时都打出来问题出在哪一目了然。说到底PyTorch基础部分的难点不在于概念有多难理解而在于细节是否到位。我自己是从“处处报错”走过来的很多错误现在看起来都特别幼稚但当时的慌张却记忆犹新。等你真正把张量、自动求导、模型搭建、训练循环这几块练熟了后续再接触更复杂的模型架构、分布式训练、混合精度你会发现本质上都是在这些基础能力上做加法。如果你在看完这篇笔记后能独立跑通一个简单的图像分类模型那学习PyTorch的第一步就算真正落地了。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑