1. 从“PyTorch是什么”说起一个深度学习框架的底层逻辑第一次接触PyTorch的人往往会问一句这不就是又一个深度学习框架吗市面上TensorFlow、Keras、PaddlePaddle那么多了为什么偏偏PyTorch成了学术圈和工业界的“宠儿”我自己的答案是——它把“动态图”这个设计贯彻得足够彻底让你写神经网络像写普通Python代码一样顺手。PyTorch本质上是一套基于Python的数值计算库它的核心任务可以概括成两件事用张量Tensor表示数据用自动求导Autograd记录和计算梯度。这两件事几乎覆盖了深度学习的所有底层需求。什么叫张量你可以把它理解成“能塞进GPU里并行计算的N维数组”比NumPy的ndarray多出来的部分就是它自带梯度追踪能力能自动计算你在训练过程中需要的每一个导数。相比那些“先定义静态计算图再在会话里执行”的老式框架PyTorch采用的是命令式风格边运行边构图。这意味着你在调试模型的时候可以直接打印中间层的输出可以用普通的Python逻辑控制循环和分支打上断点就能查变量的值。我见过太多从其他框架转过来的同学第一次在PyTorch里用调试器追踪张量shape变化都会有一种“这才是人该用的框架”的感觉。这篇文章不是给你背诵API文档而是站在一个常年用PyTorch做实际项目的人视角把这套框架的核心设计、实践经验和踩坑记录一次讲清楚。适合刚入门的初学者建立整体认知也适合已经上手了但总觉得“哪里没理解透”的开发者查漏补缺。读完你会明白三件事PyTorch的动态图机制为什么友好、张量和自动求导为什么是理解一切的关键、以及在实际训练中你一定会遇到的坑和应对方案。2. 核心设计剖析动态图、张量与自动求导是如何协同工作的2.1 Tensor不止是一个多维数组很多教程会把Tensor简单解释成“多维数组”这么说没错但接下来的内容才是重点。Tensor在PyTorch里的地位相当于细胞之于生命体你可以没有模型但一定绕不开Tensor。它的属性包括数据类型dtype、形状shape、设备device和可不可以求导requires_grad这四个属性决定了你后续能对这组数据做什么。先看dtype常见的有float32默认、float64、int64、bool等。深度学习里绝大部分计算都用float32原因在于数值精度和显存占用的平衡。int64通常用于索引、标签等场景比如分类任务的ground truth标签往往是torch.long类型。很多人训练时报错“Expected object of scalar type Long but got scalar type Float”本质上就是没弄明白标签和特征需要不同dtype。再看device这是新手最容易迷惑的地方。Tensor默认创建在CPU上你想用GPU加速就得显式调用.to(cuda)或.cuda()。模型也是一样模型的参数Parameter本质上是Tensor所以模型也必须在GPU上才能让计算在GPU上跑。我看到过最常见的报错场景数据在GPU、模型在CPU前向传播时系统直接抛出一个RuntimeError提示两个Tensor不在同一个device上。记住一个黄金法则数据和模型的设备必须一致要么都在CPU要么都在GPU。最后说requires_grad这是PyTorch区别于NumPy的杀手锏。这个属性告诉Autograd引擎请帮我追踪这个张量的所有运算过程以便之后计算梯度。只有最底层的输入或者需要优化的权重才需要设为True中间结果由PyTorch自动管理。2.2 Autograd引擎反向传播是怎么自动发生的理解Autograd最直观的方式是把它想象成一个“计算过程的录音机”。当你对一个requires_gradTrue的Tensor执行运算时PyTorch会在后台记录下这个运算的类型和输入输出形成一张计算图Computation Graph。这张图是有向无环图前向传播时从输入流转到输出同时把每个节点的运算信息记录下来。当你调用loss.backward()的时候录音机开始倒放从loss这个节点出发沿着计算图反向推导对每个节点应用链式法则把梯度一步步回传到每个requires_gradTrue的叶子节点。执行完毕后这些叶子节点会通过.grad属性拿到对应的梯度值。这里有个极其容易踩的坑梯度是累加的不是覆盖的。每次调用backward()新的梯度会加上旧的梯度而不是替换掉。所以训练循环里必须在反向传播之前把梯度清零最标准写法是optimizer.zero_grad()。我见过一个同学忘了清梯度训练loss不降反升排查了半天最后发现是从上次迭代累积下来的梯度在搞鬼。第二个Autograd的经典坑是原地操作in-place operation比如x * 2这样的写法。这会让计算图的某个节点丢失原始值因为反向传播需要重建前向过程中某个中间状态来计算梯度结果发现那个状态被覆盖了于是报错“a variable needed for gradient computation has been modified by an inplace operation”。避免方法很简单需要保留梯度路径的Tensor用x x * 2而不是x * 2。2.3 动态计算图为什么调试体验优于静态图静态图框架的思路是先完整定义一张计算图然后在会话中反复执行。这张图是“画好之后才能跑”的一旦画完想改结构就得重新画一张。这种方式在部署上很有优势因为图结构确定了你可以做很多全局优化。但在研究阶段代价是调试非常痛苦翻遍文档找不到一个print能打印出中间张量的值。PyTorch的动态图完全相反。它不做“先构图后执行”的两段式而是执行到哪就构图到哪。你写一个for循环循环体里的每个self.fc1(x)操作是等你跑到这一行时才动态加到计算图上的。这就带来两个直接好处。第一个好处是结构灵活。模型里可以出现if条件分支、循环、递归调用反正都是Python原生逻辑怎么方便怎么来。我做过一个自然语言处理项目输入序列长度不等模型需要根据长度选择不同的padding策略、决定是否启用辅助loss分支如果用静态图这套东西会写得很痛苦但在PyTorch里就是几行普通的Python判断。第二个好处是调试太舒服了。你可以在forward函数里任意位置打断点查看每一层的输出shape和数值分布能直接右键跳过某个操作继续执行。这在静态图框架里是不可想象的。很多团队选PyTorch就这一个原因模型出问题时能快速定位迭代效率翻倍。3. 数据加载与模型构建从Dataset到Module的完整链路3.1 Dataset与DataLoader别再手写数据迭代了数据是深度学习的根但大部分人最初都是用“手动切分、for循环取批次”这种方式处理数据我自己刚接触时也这么干过。后来正经用上torch.utils.data.Dataset搭配DataLoader才意识到系统性设计数据流的重要性。Dataset的职责很纯粹定义“样本”和“标签”的存取方式。你要做的就是继承torch.utils.data.Dataset实现两个关键方法__len__返回数据集大小__getitem__接收一个索引并返回对应的数据样本和标签。以图像分类为例__getitem__里通常要做三件事用PIL或cv2读入图像、把图像转成Tensor并归一化到[0,1]区间、返回(图像Tensor, 类别标签)这个元组。DataLoader的职责则是上层调度把多个Dataset样本打包成一个批次、在多进程下预取数据、执行shuffle防止训练顺序偏差。创建时最核心的参数有三个batch_size是每个批次样本数shuffle控制每个epoch是否打乱样本顺序num_workers控制多少个并行的子进程负责加载数据。数据瓶颈经常被人忽视。不少人模型训练慢满世界找优化技巧最后发现显卡利用率只有20%原因是GPU在等CPU把数据送到手上喂不满。num_workers调大可以缓解这个问题但也不是越大越好因为每个worker都会占内存开太多反而内存暴涨。我的经验法则是从num_workers4起步实测GPU利用率上不去了再逐步加。3.2 模型定义的两种姿势nn.Sequential与nn.ModulePyTorch定义模型有两个层次快路子nn.Sequential和通用路子自定义nn.Module子类。nn.Sequential适合直筒型网络输入到输出一路穿过去层与层之间没有分支、没有跳跃连接。比如一个三层的全连接分类器用Sequential写就是三五行代码的事。但它有个局限内部的数据流必须是线性的。如果你要写一个残差网络或者双分支结构Sequential就顶不住了。这时候就需要继承nn.Module自定义类。在__init__里声明所有子模块和可学习参数在forward里定义前向计算流程。PyTorch的nn.Module会对内部所有子模块做注册当你调用.to(device)、.eval()、.train()等方法时会自动广播到所有子模块省去你一个一个管理的麻烦。自定义Module里有几个容易被忽略的细节。一个是参数初始化PyTorch不会自动帮你做合适的初始化默认的初始化策略比如线性层用均匀分布在浅层网络里问题不大但到了深层网络就会导致梯度在传播中消失或爆炸。所以深度学习项目里kaiming_uniform、xavier_uniform这类初始化策略几乎成了标配。另一个细节是forward里不要修改输入张量的requires_grad状态也别对张量做detach()然后重新赋值给原变量这会破坏计算图完整性。3.3 损失函数与优化器选型背后的数学逻辑损失函数是模型的打分标准。回归任务普遍用MSELoss均方误差分类任务用CrossEntropyLoss交叉熵。为什么分类任务用交叉熵而不是直接优化分类准确率因为准确率是阶梯函数不可导梯度为0没法做反向传播。而交叉熵可以提供平滑且有意义的梯度信号引导模型逐步靠近正确答案。PyTorch里的CrossEntropyLoss实际已经包含了Softmax和Log操作所以你模型最后一层不需要额外加Softmax激活直接把logits输入loss即可。优化器则是利用梯度更新参数的引擎。SGD是最朴素的w w - learning_rate * gradient简单但收敛速度在复杂问题上往往不够。Adam类优化器包括AdamW在梯度上做一阶矩和二阶矩的指数加权平均相当于自适应调整每一步的步长收敛快、对学习率不那么敏感。实践中我更常用的组合是前期用Adam快速探索最优区域后期切换到SGD配合合适的学习率精细收敛。这个操作叫“优化器切换”有时能拿到比单一优化器更好的最终精度。定义优化器时把模型的可学习参数传进去optimizer torch.optim.Adam(model.parameters(), lr1e-3)。这里要特别注意的是如果中途往模型里加了新的层或新的Parameter优化器不会自动感知你需要创建一个新的优化器实例或者调用add_param_group手动注册。4. 从零训练一个图像分类模型完整实操流程记录4.1 数据准备我拿一个经典场景来演示用PyTorch训练一个图像分类器。数据集采用模拟项目X包含10个类别每类500张32x32的彩色图像总规模5000张。这类小数据集非常适合用来理解流程训练速度快调试窗口短非常适合初学者完整跑通一遍。先把图像数据组织成标准格式训练集与验证集按照8:2划分图像保存路径结构为/data/train/class_0/xxx.jpg这种形态分类信息一目了然。加载数据时我习惯直接从文件夹创建Datasetfrom torchvision import datasets, transforms train_transforms transforms.Compose([ transforms.RandomHorizontalFlip(), # 随机水平翻转做数据增强 transforms.RandomRotation(10), # 10度内随机旋转 transforms.ToTensor(), # 转成Tensor并归一化到[0,1] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 标准化到[-1,1] ]) train_dataset datasets.ImageFolder(/data/train, transformtrain_transforms) val_dataset datasets.ImageFolder(/data/val, transformtrain_transforms) train_loader torch.utils.data.DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4) val_loader torch.utils.data.DataLoader(val_dataset, batch_size64, shuffleFalse, num_workers4)数据增强这里有个讲究训练集需要增强验证集不需要。因为验证集的目标是评估模型在“真实数据”上的泛化能力做随机翻转和旋转会让评估结果不稳定。很多新手把同一套transform用在训练和验证上结果验证准确率忽高忽低找半天找不到原因。RandomHorizontalFlip和RandomRotation的作用是让模型看到更多样化的输入降低过拟合风险。对小数据集来说简单的数据增强往往比换大模型更有效。Normalize里的两个元组分别是均值和标准差计算方式是对训练集整体统计得到的。如果数据集比较小业界常用的简化处理就是用(0.5, 0.5, 0.5)做均值、方差把数据归一化到[-1, 1]效果通常也不错。4.2 网络结构设计为了能在CPU上快速跑通我用了一个轻量级的卷积网络结构两层卷积 两层全连接。输入是32x32x3的彩色图像第一层卷积用32个3x3卷积核步长为1padding为1输出32个通道的特征图接ReLU和2x2最大池化尺寸降为16x16。第二层卷积用64个3x3卷积核同样padding保持尺寸池化后降到8x8。然后展平为一维向量送入128维的全连接层最后接10维输出。这个结构不算先进但作为基线模型非常适合跑通全流程。深层大模型动不动就要几分钟一个epoch调个bug等半天而这个小网络在CPU上几十秒就能跑完一个epoch调试体验非常舒服。import torch.nn as nn class TinyCNN(nn.Module): def __init__(self, num_classes10): super().__init__() self.features nn.Sequential( nn.Conv2d(3, 32, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), nn.Conv2d(32, 64, kernel_size3, padding1), nn.ReLU(inplaceTrue), nn.MaxPool2d(2), ) self.classifier nn.Sequential( nn.Flatten(), nn.Linear(64 * 8 * 8, 128), nn.ReLU(inplaceTrue), nn.Linear(128, num_classes) ) def forward(self, x): x self.features(x) x self.classifier(x) return x特征提取和分类器分离的设计是惯例原因在于这个结构清晰且易扩展。以后你想换一个更强的特征提取器比如ResNet时只改features部分就行分类头不用动。nn.Flatten()的作用是把二维特征图展平成向量很多教程把展平操作写成x x.view(batch_size, -1)两者效果等价。在实际调试时Flatten()这个名字在模型总结里更直观我推荐用它。4.3 训练循环的标准写法写出训练循环是理解PyTorch精髓的关键一环。这个循环看起来简单但每一行的存在都有理由。我在下面给出完整代码然后逐行拆解。import torch import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model TinyCNN(num_classes10).to(device) criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr1e-3) num_epochs 10 for epoch in range(num_epochs): model.train() # 切换到训练模式 running_loss 0.0 correct 0 total 0 for images, labels in train_loader: images, labels images.to(device), labels.to(device) optimizer.zero_grad() outputs model(images) loss criterion(outputs, labels) loss.backward() optimizer.step() running_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() train_loss running_loss / len(train_dataset) train_acc correct / total # 验证 model.eval() val_loss 0.0 val_correct 0 val_total 0 with torch.no_grad(): for images, labels in val_loader: images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() * images.size(0) _, predicted torch.max(outputs, 1) val_total labels.size(0) val_correct (predicted labels).sum().item() val_loss val_loss / len(val_dataset) val_acc val_correct / val_total print(fEpoch [{epoch1}/{num_epochs}] fTrain Loss: {train_loss:.4f} Train Acc: {train_acc:.4f} fVal Loss: {val_loss:.4f} Val Acc: {val_acc:.4f})第一行model.train()很关键。PyTorch的train()和eval()模式会影响某些层的行为Dropout在训练时随机丢神经元、在评估时全保留BatchNorm在训练时用当前batch的统计量、在评估时用全局累计的滑动均值。忘了切换模式是拿不到准确验证指标的经典原因。optimizer.zero_grad()前面说过了是清除上一次迭代留下的梯度。这个操作必须在backward之前做。如果把zero_grad放在loss.backward()后面累积的梯度会和新梯度相加参数更新方向就错了loss会震荡甚至发散。loss.item()很重要。loss本身是一个带grad_fn的Tensor直接拿去打印或者累加会一直保存着计算图内存越用越多。.item()能把标量Tensor转换成纯Python数值脱离计算图避免内存累积。在统计loss累加时我的习惯是乘上images.size(0)也就是当前batch的样本数最后除以整个数据集的样本数来得到平均loss这个平均是按样本数加权的结果不会被batch_size影响在最后一个不完整batch时依然可靠。验证阶段被torch.no_grad()包裹这个上下文环境告诉Autograd这里的所有运算都别记录计算图别构建反向传播路径。验证不需要梯度所以关掉require_grad的追踪能显著减少内存消耗和计算时间。如果把这一步忽略显存很快就会被验证阶段累积的计算图塞爆。4.4 完整运行后的观察我在CPU上跑完这个训练流程10个epoch大约需要10分钟。第一轮训练准确率大约在35%左右这比随机猜高一些说明模型已经开始学到基本区分特征。到第5轮左右训练准确率能上到90%以上验证准确率在86%上下。到第10轮训练准确率接近100%验证准确率维持在88%到90%之间。这里出现了一个典型信号训练准确率和验证准确率之间的差距开始拉开。这个差距表示的正是模型在记忆训练集特征而不是泛化到通用特征也就是过拟合趋势。小数据集加上相对宽裕的模型容量出现这种现象非常合理。我的处理办法有三板斧。第一增加数据增强强度比如加入ColorJitter调整亮度饱和度、加入RandomCrop配合填充策略。第二加一层Dropout。第三提前终止策略即监控验证集准确率连续若干轮不提高就停止训练。在流程跑通阶段这些优化手段可以先放一边但脑子里必须清楚“训练集高、验证集低”是为什么。4.5 保存与加载模型完整可用的推理流程训练完毕后模型只是一个存于内存中的Python对象进程结束就没了。你需要把它保存到磁盘。PyTorch官方推荐的保存方式有三种适用场景各不相同。torch.save(model, model.pth)这种方式保存整个模型包括结构和权重。缺点很明显加载时依赖模型类定义的源代码如果代码结构变了就加载失败。当你想快速存一个轮次快照做周期性备份时这种写法还能用但我不推荐在生产环境用。torch.save(model.state_dict(), model_weights.pth)是业界主流方式。它只保存模型的可学习参数以字典形式存盘。加载时你需要先构造一个结构相同的模型对象再调用model.load_state_dict(torch.load(model_weights.pth))。这样做的好处是代码重构时能灵活适配只要层名和结构一致就能加载。保存优化器状态也经常被忽略。如果你想从断点处继续训练光有模型权重不够优化器的学习率和Adam的动量状态也得保存。所以做“断点续训”时要保存一个checkpoint字典里面包含模型状态、优化器状态、当前epoch数和最佳验证指标。推理阶段你可以在已有模型结构上加载权重然后调model.eval()进入评估模式再用torch.no_grad()包住推理代码。一步也不能省eval()模式决定BatchNorm和Dropout的表现no_grad()让你在推理时不保留任何计算图。model TinyCNN(num_classes10) model.load_state_dict(torch.load(model_weights.pth, map_locationcpu)) model.eval() from PIL import Image from torchvision import transforms inference_transform transforms.Compose([ transforms.Resize((32, 32)), transforms.ToTensor(), transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) ]) img Image.open(test_img.jpg).convert(RGB) tensor_img inference_transform(img).unsqueeze(0) # 增加batch维度 with torch.no_grad(): logits model(tensor_img) predicted_class torch.argmax(logits, dim1).item()unsqueeze(0)是为了将一张3D的CHW张量变成4D的NCHW张量因为模型始终按batch维度处理输入即使batch大小为1也要保留这个维度。5. 常见问题与排查技巧实录5.1 设备不一致、维度不匹配和dtype错误第一类高频问题就是设备不一致。典型报错长这样RuntimeError: Expected all tensors to be on the same device。排查步骤很固定逐一检查模型、输入数据、标签、loss内部的状态是否都在同一个device上。还有一个小坑是自定义的loss函数里新建了常数张量比如torch.tensor(0.5)这个张量默认在CPU上与GPU上的特征进行运算时也会触发报错。解决办法是用val torch.tensor(0.5, devicex.device)来指定设备。维度不匹配是第二大高频问题。报错信息通常包含“size mismatch”字样比如mat1 and mat2 shapes cannot be multiplied。这个错误的核心原因是前一层输出的通道数与下一层权重矩阵的输入维度对不上。排查时把模型总结打印出来对照每层的输入输出shape逐一确认。尤其是经过卷积、池化、展平之后进入全连接层的维度容易算错。我从不手工计算特征图展平后的尺寸写一次print(model(torch.randn(1, 3, 32, 32)).shape)就能直接看到最终输出形状行不行。dtype错误虽然出现得少但一出现就很迷惑。报错信息是Expected object of scalar type Long but got scalar type Float。原因通常是分类标签没有被转成long类型。在ImageFolder中标签默认是int类型torch的交叉熵期望标签为torch.long。最稳的写法是在数据准备阶段直接做labels labels.long()。5.2 训练loss不降或出现NaNloss不降是最让人头大的问题但绝大多数时候原因其实不复杂。第一个常见原因是学习率过大。参数更新步子太大在损失函数曲面上来回震荡loss当然下不去。处理方式是降低学习率比如从1e-3降到1e-4观察是否有改善。第二个原因是梯度消失尤其在深层网络中反向传播时梯度经过多层连续相乘后会指数级衰减前面几层几乎学不到东西。这时候检查每一层的梯度范数如果发现后层梯度正常但前层梯度接近0就可以考虑加入残差连接或者换用更好的初始化方法。训练过程中出现NaN不是数字时不要慌按顺序排查即可。最常见原因是学习率过大导致loss发散到无穷大减学习率就行。第二个常见原因是loss本身就变成了0或者负数再被log交叉熵里如果模型输出极端自信到正确类别的logit很大loss可能因为数值稳定性出现NaN。第三个是数据里有NaN值比如图像像素异常或标签异常打印数据的统计信息能定位到。最后一个原因是混合精度训练操作不当这在用AMP自动混合精度时需要注意通常是scaler参数没调用对。5.3 显存不足问题排查显存溢出CUDA OutOfMemory在训练大模型时几乎一定遇到过。看到一个常用的排查顺序第一确认是否有梯度累积导致的计算图堆积——忘了detach()、忘记zero_grad()都会让显存快速膨胀。第二把batch_size调小这是最直接的降显存手段。第三检查是否在不需要梯度的验证阶段忘了torch.no_grad()。第四利用torch.cuda.memory_summary()打印内存分配明细定位是哪一个张量把显存撑爆的。调batch_size是牵一发动全身的事batch_size变小时如果你用CrossEntropyLoss这类对batch内的loss直接求平均的损失函数模型受到的学习信号会变弱可能需要适当提高学习率来补偿。我习惯的做法是先用小batch_size跑通前几个step没问题再逐步调大。5.4 数据加载缓慢的训练瓶颈训练速度上不去先看GPU利用率。如果nvidia-smi显示GPU利用率不达标低于90%多半是数据加载在拖后腿。优先级最高的手段是调大num_workers让更多子进程并发读数据。但num_workers过大时内存消耗会上升严重时导致进程被杀需要和内存情况做平衡。对于特别大的数据集还可以配合更底层的优化使用LMDB或TFRecord这类格式做存储用缓存机制把热数据留在内存里或者用torchdata库的流式数据集按需加载。不过在项目早期这些复杂手段没有必要先用好num_workers就能解决大多数问题。6. 实操心得与后续扩展方向6.1 从“跑通”到“跑好”的方法论回顾PyTorch的使用路径我总结出三个阶段。第一阶段是“跑通流程”目标是让代码从数据加载到模型训练再到结果输出全链路顺畅运行这个阶段不需要太多花哨技巧重点在于把框架的基本组件都串起来。第二阶段是“排查问题”数据异常、模型结构错误、训练不收敛这些问题开始出现排查能力决定了你能不能在合理时间内解决它们。第三个阶段是“优化性能”数据管道、模型结构、训练策略都有了优化空间。很多人卡在第一阶段和第二阶段的交界处因为教程里的代码看着都懂换了自己的数据集就不行。我的建议是从手头最小的数据集开始用一个极简模型跑通完整流程再逐步换大数据集和复杂模型。跑通一个流程比理解所有理论更优先有了完整闭环之后再去逐个加深理解。6.2 值得持续学习的几个方向PyTorch生态不止训练模型这么简单整个生态链都值得深入。TorchScript能把PyTorch模型序列化为一种可投入生产环境的中间表示适合部署到服务端。ONNX导出可以让模型在更多推理引擎上运行比如ONNX Runtime。TorchServe则是官方提供的模型服务框架处理REST API请求、模型版本管理和多模型调度。分布式训练也是绕不开的方向。单卡训练到模型太大放不下的时候你会需要DDPDistributed Data Parallel做数据并行或是DeepSpeed这类框架做内存优化。DDP在PyTorch中的使用已经非常成熟一个torch.distributed.launch启动命令加上少量配置代码就能把训练速度提上去。最后是混合精度训练。在支持Tensor Core的GPU上torch.cuda.amp模块可以用较低精度FP16做大部分计算既降低显存占用又能提升速度代价通常是精度上的微小损失。对绝大多数任务来说这个损失可以接受收益却很可观。6.3 最后再分享一个调优小技巧每次训练开始前先取一个很小的数据子集比如4个batch让模型在这个子集上做到训练损失几乎降到0。如果这一步做不到说明代码在数据流向或模型结构上一定有不合理的地方即使训练再长时间也不会有好结果。这个技巧能帮我过滤掉大量无效训练。如果4个batch都能过拟合再换完整数据集跑正式训练大部分问题都能提前暴露。PyTorch是个工具工具的价值在于帮你快速验证想法。框架再强大也代替不了你对数据和问题的理解。我在实际项目里吃过不少亏但每次踩坑后对框架的理解都更深入一层。这篇文章里写的都是我从真实训练过程中沉淀下来的经验希望你在用PyTorch的路上少走几条弯路。