做深度学习项目这几年,PyTorch 几乎成了我每天的默认选项。从最早跑个简单的图像分类,到后来折腾分布式训练、模型上线,PyTorch这套框架的灵活和透明,帮我把大量注意力从"框架怎么用"挪到了"模型怎么设计"上。这篇文章不打算给你念文档,我以自己从入门到落地的一系列实操经验为线索,把 PyTorch 里真正影响项目成败的机制、流程和坑位拆开讲。无论你是刚装好环境想跑通第一个模型的新手,还是已经在项目里被显存、速度、复现问题折磨过一阵的开发者,这里的内容应该都有对应你需要的那一块拼图。
1. 为什么是 PyTorch:从一次项目选型说起
先从一个真实场景聊起。某次接了个图像分类的模拟项目,数据集大概几十万张图,要求快速迭代模型结构,还要在两周内给出一个能演示的版本。当时摆在我面前的选择其实不少,有静态图风格的框架,也有 PyTorch。最后我选了 PyTorch,核心原因就一句话:调试成本最低。
1.1 框架之争的真正焦点
很多人纠结框架时喜欢比性能、比部署生态,但对我来说,真正的分水岭在开发体验。静态图框架的思路是先把整个计算图定义好,再交给编译器统一优化执行。听起来高效,可一旦中间某个张量的形状不对、数值异常,调试起来就像在黑盒子里捞针。PyTorch 走的是命令式风格,每一行代码执行时就是真实计算,中间结果随手就能 print 出来看,这在我们排查 NaN 损失、梯度爆炸这类问题上省下的时间,远超那点理论上的性能差距。
当然,静态图框架在极端优化场景下有优势。但你也得意识到,那种优势是"部署期"的,是拿"开发期"的灵活性换的。对于绝大多数业务项目,模型结构隔三差五要改,训练策略说调就调,PyTorch 这种"写着不别扭、跑错了立即可见"的开发体验,才是团队效率的保证。
1.2 动态图的实用价值
动态图是 PyTorch 最被称道的特性之一。用大白话说,就是你每次 forward,代码都是真的按顺序在跑,控制流的 if/else、for 循环都可以直接写在模型里,不需要把整个网络结构提前固化成一张静态图。
举个项目里常见的例子:处理变长序列时,每个 batch 里的样本长度不一样,PyTorch 里可以直接在 forward 里用循环遍历每个时间步,或者根据实际长度做 mask。这在静态图框架里往往需要专门的 API 绕来绕去。再比如输出维度依赖输入维度的情况,PyTorch 里你甚至可以在 forward 里临时计算输入尺寸再决定全连接层怎么接。这种自由度,让我在设计一些非标准结构(比如结构搜索、注意力变体)时几乎感觉不到框架的边界。
注意:动态图也有代价——每次迭代都要重新构建计算图,会有额外开销。但对大多数中小规模模型来说,这个开销完全在可接受范围内。真到了大规模分布式训练阶段,还有
torch.compile这种手段可以把动态图编译成优化后的静态执行方式,后面我会单独讲。
1.3 生态与社区的现实考量
选框架不是选信仰,是选生态能不能帮你省时间。PyTorch 的生态覆盖了几乎每一个细分方向:图像有 torchvision,文本有 transformers,图神经网络有 PyG,强化学习有各种基于它的库。遇到问题去搜索,十有八九能找到别人用 PyTorch 踩过的同一个坑,这种"站在前人肩膀上"的感觉,在项目时间紧张时太重要了。
另外,PyTorch 背后有庞大的学术和工业用户群体,新论文的官方实现大部分首先用 PyTorch 发布。这意味着你在复现最新模型时,很少需要自己把别人的思路翻译成另一套框架的写法。社区活跃度带来的不仅是资料多,更是人才密度高——招人、交流、外包协作,大家的基础语言一致,沟通成本直线下降。
2. 核心机制深度拆解:从张量到自动微分
如果你只用过 PyTorch 的高级 API,没搞懂它底层的三大核心机制,遇到稍复杂的问题就会寸步难行。这里我把张量、自动微分和计算图内存管理三个概念掰开讲透,这是所有 PyTorch 代码的根基。
2.1 张量:一切操作的基石
张量(Tensor)本质上就是一个多维数组,但它比普通的 NumPy 数组多了一个能力:自动跟踪运算并支持在 GPU 上计算。你甚至可以把一个张量当成"自带计算历史的数组"。
初始化张量的方式很多,但项目里最常用的就那么几个。从数据转换:
import torch import numpy as np # NumPy 转 Tensor np_array = np.random.randn(3, 4) tensor = torch.from_numpy(np_array)创建特殊张量:
# 全零、全一、单位阵 zeros = torch.zeros(2, 3) ones = torch.ones(2, 3) eye = torch.eye(3) # 均匀分布和正态分布初始化 uniform = torch.rand(2, 3) # [0,1) 均匀分布 normal = torch.randn(2, 3) # 标准正态分布项目里最容易犯的错就在设备管理上。CPU 张量和 GPU 张量不能直接混合运算,很多人一上来就报 "Expected all tensors to be on the same device" 的错。我的习惯是定义设备变量,然后统一搬移:
device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model = MyModel().to(device) data = data.to(device)这不是什么高深技巧,但能帮你逃过至少一半的新手崩溃时刻。
2.2 自动微分:反向传播是怎么"免费"实现的
Python 生态里能算矩阵乘法的库不少,但 PyTorch 真正不可替代的,是把反向传播这个过程自动化了。你只需要定义前向计算,框架会根据计算历史自动构建反向传播路径,算出每个参数对损失的梯度。
这个机制依赖每个张量的requires_grad属性和grad_fn。当一个张量设置了requires_grad=True,之后所有由它参与运算得到的张量,都会自动记录 grad_fn,指向生成它的运算节点。最后调用.backward()时,PyTorch 沿着这条链从后往前计算梯度,存到叶子节点的.grad里。
import torch x = torch.tensor([2.0], requires_grad=True) y = x ** 2 + 3 * x + 1 y.backward() print(x.grad) # tensor([7.]), 导数 2*x + 3 = 7手动算一遍验证:y = x^2 + 3x + 1 对 x 求导是 2x + 3,在 x=2 时为 7,和输出完全一致。这就是自动微分做的事。
需要特别注意的是:requires_grad像病毒一样会传播。如果你的输入数据不小心带上了梯度需求,反向传播时就会把梯度传播到数据本身,导致显存翻倍、而且几乎肯定不是你想要的。所以项目里输入数据的标准写法是:
input_tensor = torch.tensor(data, requires_grad=False)验证集和测试集更是要放在torch.no_grad()上下文里,否则梯度计算会让你的验证过程白白多消耗大量显存和时间。
2.3 计算图与内存管理
PyTorch 的计算图是动态构建的,每次 forward 都是新图。这张图上不光记录了数据流向,还保存了中间激活值(activations),这些中间值是为了在反向传播时计算梯度而必须保留的。
这意味着一个直接的后果:显存占用主要来自中间激活值,而不是模型参数。一个只有几百万参数的小模型,可能因输入分辨率大、batch 大,在 forward 过程中产生十几 GB 的中间张量,直接显存爆炸。理解了这点,你就知道为什么增大 batch 会那么快地撑爆显存——不只是数据量大了,是每一层输出的特征图都变大了。
内存管理上,PyTorch 的缓存分配器会缓存已经释放的显存块,留给后续使用。所以你在程序里看到的任务管理器显存占用,往往不会因为删掉几个张量就立刻降下去。判断自己有没有内存泄漏,要看显存峰值是否随迭代不断攀升,而不是单次不下降。排查泄漏时,我常用torch.cuda.memory_summary()或nvidia-smi对比 GPU 显存曲线。
3. 从零搭建一个完整训练流程
理论说再多,不如把一套完整流程跑通。这一节我按项目里真实的组织方式,从数据加载、模型构建、训练循环到保存恢复,一步步带你搭一个可以复用的基础框架。
3.1 数据加载:DataLoader 的正确使用姿势
一个高质量的数据流水线,对训练速度的影响能到 3 到 5 倍。很多人训练模型慢,不是 GPU 不行,而是数据供给跟不上,GPU 一直在空等。
PyTorch 的标准做法是自定义一个 Dataset,并配合 DataLoader 使用。Dataset 负责"给我第 i 个样本",DataLoader 负责"批量取、打乱顺序、多进程预取"。一个典型的图像分类 Dataset 长这样:
from torch.utils.data import Dataset, DataLoader from PIL import Image import os class ImageFolderDataset(Dataset): def __init__(self, root_dir, transform=None): self.samples = [] self.transform = transform # 这里假设 root_dir 下每个子文件夹是一个类别 classes = sorted(os.listdir(root_dir)) self.class_to_idx = {cls: i for i, cls in enumerate(classes)} for cls in classes: cls_dir = os.path.join(root_dir, cls) for fname in os.listdir(cls_dir): self.samples.append((os.path.join(cls_dir, fname), self.class_to_idx[cls])) def __len__(self): return len(self.samples) def __getitem__(self, idx): path, label = self.samples[idx] img = Image.open(path).convert("RGB") if self.transform: img = self.transform(img) return img, label然后构建 DataLoader 时,num_workers这个参数几乎决定了你的数据加载能不能跑满磁盘和 CPU。根据我测试的经验,在普通机器上设为 4 到 8 能明显提升吞吐;设置太高反而会因为进程间通信和调度开销导致性能下降。pin_memory=True在 GPU 训练时也建议打开,它能借助锁页内存加速 CPU 到 GPU 的数据拷贝。
train_loader = DataLoader( train_dataset, batch_size=64, shuffle=True, num_workers=4, pin_memory=True )数据增强(transform)的正确姿势也同样关键。增强操作应在 Dataset 内、即每个 epoch 独立进行,这样才能保证每个 epoch 看到的样本略有不同,天然起到正则化作用。你要是把增强提前在数据预处理阶段做完了,模型的泛化能力提升就打了折扣。
3.2 模型构建:nn.Module 的组织方式
写模型时,我强烈建议拆成两个层次:nn.Module负责定义可学习参数和计算逻辑,nn.Sequential或自定义 forward 负责组织计算流程。一个好的模型代码是可读的——看到__init__里的层定义,基本能脑补出 forward 的结构。
import torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self, num_classes=10): super().__init__() self.conv1 = nn.Conv2d(3, 16, kernel_size=3, padding=1) self.bn1 = nn.BatchNorm2d(16) self.pool = nn.MaxPool2d(2, 2) self.conv2 = nn.Conv2d(16, 32, kernel_size=3, padding=1) self.bn2 = nn.BatchNorm2d(32) self.fc = nn.Linear(32 * 8 * 8, num_classes) def forward(self, x): x = self.pool(F.relu(self.bn1(self.conv1(x)))) x = self.pool(F.relu(self.bn2(self.conv2(x)))) x = x.view(x.size(0), -1) x = self.fc(x) return x有几个细节值得留意。BatchNorm 层在训练和推理模式下行为不同:训练时用当前 batch 的均值方差,推理时用累积的 running stats。PyTorch 通过model.train()和model.eval()切换,忘记切换推理模式,是验证时结果诡异的最常见原因之一。另外,view和reshape容易搞混,view 要求张量在内存里连续,否则会报错,不连续时用reshape更稳妥。
关于参数初始化,PyTorch 各层自带默认初始化,大多数时候可以直接用。但如果你发现训练初期损失下降异常慢,可以显式做 Xavier 或 Kaiming 初始化。比如:
def init_weights(m): if isinstance(m, nn.Linear): nn.init.xavier_uniform_(m.weight) if m.bias is not None: nn.init.zeros_(m.bias) model.apply(init_weights)3.3 训练循环、验证与 checkpoint
训练循环的标准结构大家可能都见过,但写得好不好,差别全在细节:
def train_one_epoch(model, loader, optimizer, criterion, device): model.train() total_loss = 0.0 correct = 0 total = 0 for images, labels in loader: images, labels = images.to(device), labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() total_loss += loss.item() * images.size(0) _, preds = torch.max(outputs, 1) correct += (preds == labels).sum().item() total += labels.size(0) return total_loss / total, correct / total三个最需要注意的细节:
optimizer.zero_grad()必须在backward()之前。PyTorch 的梯度是累积的,不手动清零的话,每个 batch 的梯度会叠加,相当于不断累积全局梯度,后果就是损失曲线乱跳。- 损失要取
.item()而不是直接累加 tensor。.item()把标量张量转成 Python 数,不保留计算图,否则 1000 个 batch 下来你就累积了 1000 张计算图,内存直接爆炸。 - 每个 epoch 结束要跑验证。验证循环应该在
model.eval()模式下,并且整体包在torch.no_grad()里,这样既保证 BatchNorm 行为正确,又不浪费显存去记录中间激活。
Checkpoint 的保存不止存模型权重,最优做法是把 optimizer、epoch、学习率调度器状态一起存进字典。这样就算训练中断,也能从断点无损恢复。我一般这样写:
checkpoint = { "epoch": epoch, "model_state_dict": model.state_dict(), "optimizer_state_dict": optimizer.state_dict(), "scheduler_state_dict": scheduler.state_dict(), "best_acc": best_acc, } torch.save(checkpoint, f"checkpoint_epoch{epoch}.pth")加载时再用model.load_state_dict(torch.load(path)["model_state_dict"])分别取回。特别提醒一个坑:state_dict里权重是绑定到模型结构上的,你如果把模型结构定义的顺序搞乱,加载时会报 mismatch 错误。保存模型就用state_dict,别乱搞自定义格式。
3.4 优化器与学习率调度
优化器选型上,Adam 永远是快速出效果的首选,SGD + momentum 则在充分调优后往往能取得更好泛化性能。我个人的经验准则是:项目初期用 Adam 快速验证模型能收敛,稳定后再考虑切换 SGD 拉一把泛化。
学习率是最敏感的超参数,没有之一。每次改模型结构、换数据集,原来好用的学习率很可能直接失效——损失变成 NaN 或者根本不下降。第一个排查动作永远是减学习率。设多低?我的习惯是从 1e-3 开始,不好使就按 0.1 倍往下降,直到曲线稳定。
optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=50)CosineAnnealing 是我最常用的调度策略:学习率先保持高位探索,后按余弦曲线平滑降到低位,比起指数衰减,它在后期更不容易震荡。也有项目喜欢ReduceLROnPlateau,它根据验证损失自动降学习率,省心。注意 scheduler 必须放在每个 epoch 结束后调用scheduler.step(),不同调度器触发的时机不完全一样,看文档确认。
4. 踩坑实录与排查技巧
这一节的价值,来自于我这些年踩过的一堆实打实的坑。每个问题背后都有一整段"盯着屏幕发呆"的回忆,我把诊断思路和解决路径一起写出来,方便你对照排查。
4.1 显存不足:不只是"减小 batch"这么简单
显存不足大概是 PyTorch 用户遇到最多的报错:CUDA out of memory。很多人的第一反应是减小 batch size——这确实能缓解,但不是万能的。有几个更精细的手段值得按顺序试:
- 减小输入尺寸:很多数据集默认分辨率是模型公开训练时用的一半甚至四分之一,精度损失可能远小于你的预期,但显存收益是几何级别的。
- 关闭梯度计算:验证和推理时务必包在
torch.no_grad()里。这是最大也最容易被忽略的显存黑洞。 - 用梯度累积模拟大 batch:当你想用大 batch 但显存不够时,可以多个小 batch 累积梯度后再更新一次参数。注意:要等累积到目标步数后再执行
optimizer.step(),并且每步仍然要zero_grad()。 - 使用混合精度:半精度浮点显存占用直接减半,显存翻倍效果立竿见影。
还有个排查技巧:用torch.cuda.max_memory_allocated()看程序峰值显存,用torch.cuda.reset_peak_memory_stats()重置后局部观察某个环节到底吃了多少显存。这比盯着 nvidia-smi 来回对比准确多了。
4.2 梯度爆炸与消失:损失变成 NaN 的元凶
NaN 是另一种让人崩溃的体验。有一次我训练一个深层网络,前面 10 个 epoch 好好的,突然 loss 变成 NaN,而且永远回不来了。查了数据、查了学习率都没问题,最后定位到是某一层激活值过大导致梯度爆炸。
处理梯度爆炸的标准组件是梯度裁剪:
torch.nn.utils.clip_grad_norm_(model.parameters(), max_norm=1.0)放在backward()之后、optimizer.step()之前。这个操作用大白话说就是:如果所有参数的梯度向量总范数超过了阈值,就等比例缩回去。这不会改变梯度方向,只限制"步长",所以对最终收敛影响很小,但对稳定性帮助巨大。
梯度消失则更隐蔽,往往表现为 loss 在训练很久后还纹丝不动。常见诱因包括:激活函数饱和(比如深层 sigmoid)、初始化不当、网络太深没有残差连接。排查时你可以打印各层梯度范数:
for name, param in model.named_parameters(): if param.grad is not None: print(name, param.grad.norm().item())哪一层梯度变成 0,消失点就定位在哪一层。
4.3 训练结果不可复现:随机源被遗忘
"前两天跑 85 个点,今天重跑怎么变成 84.5?"——这类问题几乎人人都遇到过。PyTorch 里至少有四个随机源:PyTorch 本身、Python 的 random、NumPy、以及 CUDA 的非确定性操作。
要尽量复现,需要做四件事:
import torch, numpy as np, random torch.manual_seed(42) np.random.seed(42) random.seed(42) torch.cuda.manual_seed_all(42) torch.backends.cudnn.deterministic = True torch.backends.cudnn.benchmark = False但我也得坦白:某些 CUDA 算子(比如 atomics 类型的求和)在并行执行时天然是乱序的,结果存在微小差异。所以别追求逐位复现,设定好随机种子 + 固定数据加载顺序,跑三次结果稳定在一个小范围内,就算复现成功。
4.4 多 GPU 训练的常见误区
上多卡之前,先想清楚一个问题:多进程并行(DistributedDataParallel)和单进程多卡(DataParallel)根本不是一回事。DataParallel 写起来简单,但同步开销大、负载不均,在 4 卡以上时性能越来越差;正经做法是DistributedDataParallel,每个进程管一块卡,通过后端通信做梯度同步。
torchrun --nproc_per_node=4 train.py在 train.py 里初始化:
import torch.distributed as dist from torch.nn.parallel import DistributedDataParallel as DDP dist.init_process_group(backend="nccl") local_rank = int(os.environ["LOCAL_RANK"]) torch.cuda.set_device(local_rank) model = DDP(model.to(local_rank), device_ids=[local_rank])坑点集中在数据加载上。每个进程都要有自己的 DataLoader,并且给每个进程分不同的数据切片,否则 4 个进程读同样的数据,等于每轮只喂了 1/4 的数据量,模型反复学同一批样本。DistributedSampler 就是干这个的。还有就是 batch size 在多卡下的口径:4 卡时你设置的 batch_size 是每张卡上的,真正的一次优化步骤的全局 batch 是 batch_size × 卡数,这直接影响学习率的合理区间,很多人忽略了这个倍率关系导致训练不稳。
5. 性能优化与工程化落地经验
模型能跑通只是第一步。真正到项目交付阶段,训练速度和部署稳定性才是最现实的需求。这里说的优化,不是玄学调参,而是有明确收益的技术手段。
5.1 torch.compile 与算子融合
PyTorch 2.x 引入的torch.compile是我认为近几年框架层面最有价值的改进之一。它通过 TorchDynamo 把 Python 字节码翻译成计算图,再交给后端编译器做算子融合和内存优化,最终生成高效的执行代码。
使用方式极其简单,在现有模型上加一行就够了:
model = torch.compile(model)对于中等规模的 CV 模型,我实测的加速通常在 1.2 到 1.8 倍之间,无需修改任何训练逻辑。但要注意:compile 在第一次调用时需要额外编译时间,而且某些 Python 动态性太强的代码(比如在 forward 里频繁改 Python list)可能触发回退到 eager 模式,那样加速效果就没了。建议在项目稳定、准备长训之前打开,别在频繁改模型的阶段开,编译缓存会反复失效。
5.2 混合精度训练:又快又省显存
混合精度的原理是:用 FP16 跑 forward 和 backward,但用 FP32 保存权重主副本,避免精度损失累积。现代 GPU 上,FP16 的矩阵乘算力往往是 FP32 的两倍以上,所以混合精度不仅是省显存,是硬件层面真正的加速。
PyTorch 的自动混合精度用起来非常标准:
from torch.cuda.amp import autocast, GradScaler scaler = torch.cuda.amp.GradScaler() for images, labels in loader: optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()有几个关键点务必记住。autocast只包 forward 和 loss 计算,backward放在外面没关系,但千万不能把optimizer.step()放进 autocast 上下文里。GradScaler 的作用是防止 loss 太小导致 FP16 梯度下溢,它会先把 loss 放大,step 前再缩回去。如果 loss 连续出现 inf/NaN,scaler 会自动降低缩放因子,给训练自动兜底。
混合精度在 NVIDIA 显卡上从 Pascal 架构开始就有硬件支持,用显存余额紧张的项目尤其受益。注意:FP16 对数据里的大数值很敏感,所以预处理时建议把输入范围做归一化到 -1 到 1 或 0 到 1 之间,能显著降低溢出概率。
5.3 部署导出:从训练模型到线上服务
训练完成不等于项目完成。把 PyTorch 模型部署到生产环境,我踩过的最大一个坑是:训练时什么都对,一导出就报错,或者轨迹不兼容。
PyTorch 官方推荐的导出格式是 TorchScript。如果你的模型代码比较标准,可以直接 trace 导出:
dummy_input = torch.randn(1, 3, 224, 224).to(device) model.eval() traced_model = torch.jit.trace(model, dummy_input) traced_model.save("model.pt")但 trace 有个硬伤——它只"看"到了你 input 走的那条路径。如果 forward 里有条件分支,喂给 trace 的 dummy input 只执了一条分支,其他分支不会被固化到 exported 图里。生产环境一旦走了没见过的分支,直接报错。所以 trace 之前,要么保证 forward 里没有动态分支,要么用torch.jit.script把模型整体脚本化,不过 script 对 Python 语法的支持有一定限制。
另一个更现代的方案是用 ONNX 导出:
torch.onnx.export( model, dummy_input, "model.onnx", opset_version=17, input_names=["input"], output_names=["output"], dynamic_axes={"input": {0: "batch_size"}, "output": {0: "batch_size"}} )dynamic_axes让 batch 维度可变,否则导出的模型只能支持固定 batch 推理,这在线上服务里非常不灵活。导出 ONNX 后,我可以选择 ONNX Runtime 做 CPU 推理优化,或者转到 TensorRT 做 GPU 推理加速——ONNX 作为中间交换格式,最大的价值是让训练框架和部署框架解耦。
部署前还有一件事经常被忽略:BatchNorm 层的状态。导出前必须model.eval(),否则 BN 层的 running stats 还是训练状态,线上推理结果可能差得离谱。这个问题在导出时没有报错提示,是最阴的暗坑之一。
6. 项目经验之外的几个小建议
写到这,正文该说的机制和流程都已经说得差不多了,最后再分享几个我在实际项目中反复用到的体会,都是代码以外的东西,但往往决定了一个项目是顺利交付还是被迫延期。
第一个体会是把"能复现"当成默认要求,而不是事后补救。我见过的很多深度学习项目,代码里全是别人的痕迹,随机种子不设、数据增强不固定、模型结构边写边改。跑出来的结果好,大家开心;跑出来的结果差,谁也不知道是数据问题、网络问题还是实现问题。从我自己的经验看,项目启动第一天就把种子固定、实验记录表格建好、每次改动的配置存进 checkpoint 文件名,后面排查问题会省下大量时间。
第二个体会是不要迷信公开榜单上的精度。某个模型在公开数据集上效果好,不一定同样适配你的业务数据分布。我印象很深的一次,把一个在公开数据集上表现突出的模型迁移到业务场景,结果精度远低于简单得多的基线。问题不是模型,是数据分布差异。所以在引入新模型之前,先用小规模的快速实验验证它在你的数据上真的有效,再投入资源做完整训练,是我现在项目管理的标准动作。
第三个体会是,调试深度学习模型,本质上是在调试"数据流",而不是在调试"代码"。报错信息只告诉你在哪一行炸了,但真正的问题往往在那一行之前——某个维度被广播错了,某个张量在某个分支里没有搬到 GPU,或者某个数据集返回的标签类型和预期不一致。所以我现在排查 bug 的速度,和"我在任何怀疑点打印 tensor 的 shape、dtype、device、min/max"的速度成正比。PyTorch 透明到这种程度,你已经没有任何理由靠猜来定位问题。
就先聊到这。PyTorch 是个工具箱,不是目的本身。希望这些踩坑记录和实操经验,能让你把更多时间花在真正重要的算法和业务问题上,而不是和框架较劲。