1. 为什么一个“pytorch”标题值得单独写一篇
看到“pytorch”这个词,很多人第一反应是“这不就是个深度学习框架吗,有什么好写的”。但如果你真正在一线做过项目,就会发现事情远没有这么简单。我见过太多人,装完环境跑通一个MNIST就觉得自己会PyTorch了,结果一上真实项目,数据加载慢得像蜗牛、显存莫名其妙爆掉、训练loss震荡到怀疑人生。问题出在哪?出在对这个框架的理解只停留在“调包”层面,没有吃透它的设计哲学和运行机制。
这篇文章我想聊的,不是那种官方文档式的API罗列,而是从一个实际使用者的角度,把PyTorch从“能跑”到“跑得好”之间那些坑和技巧讲清楚。核心关键词就一个:PyTorch。它是什么?一句话,它是一个基于动态计算图的深度学习框架,用Python写起来像写普通代码一样自然。它能做什么?从图像分类、目标检测,到自然语言处理、语音合成,几乎覆盖了当前主流的深度学习任务。解决了什么问题?解决了早期静态图框架调试困难、灵活性差的问题,让研究者能像调试普通Python程序一样调试神经网络。
适合谁看?如果你刚接触深度学习,想找一个能快速上手的框架,这篇能帮你少走弯路;如果你已经用过一段时间,但总觉得模型训练效率不高、代码组织混乱,这篇里的实操细节和避坑经验应该对你有用。我不打算讲太多数学推导,重点放在“怎么用”“为什么这么用”“遇到问题怎么查”这三件事上。
2. 核心设计思路拆解:动态图到底香在哪
2.1 从“先定义后运行”到“边定义边运行”
要理解PyTorch为什么受欢迎,得先搞清楚它和早期静态图框架的根本区别。静态图的思路是“先搭好整个计算流程,再喂数据运行”,就像你先画好一张完整的工厂流水线图纸,然后开机生产。好处是图纸固定后可以深度优化,坏处是调试的时候你只能看到最终产品,中间哪一步出了问题很难定位。
PyTorch走的是动态图路线,计算图是随着代码执行实时构建的。你写一行y = x * 2,它立刻就算出结果,同时把这一步记录到图里。这意味着你可以用普通的Python调试工具,比如print、pdb,随时查看中间变量的值。我刚开始从静态图转过来的时候,最直观的感受就是“终于不用猜了”。以前调一个自定义层,输出不对只能靠经验猜是哪一步维度错了,现在直接打印出来一看便知。
这种“边定义边运行”的模式,带来的最大好处是灵活性。比如处理变长序列的RNN,或者需要根据条件动态选择分支的模型,用静态图写起来非常别扭,而在PyTorch里就是普通的if-else和for循环。这也是为什么学术界大量论文的官方实现都选择PyTorch,因为研究者需要快速试错,动态图能让他们把精力放在想法上,而不是和框架搏斗。
2.2 张量:一切计算的基本单位
PyTorch里最核心的数据结构是张量,你可以把它理解成一个多维数组,但比NumPy的数组多了两个关键能力:一是可以在GPU上运行,二是支持自动求导。这两个能力是深度学习训练的基石。
先说说GPU加速。深度学习的计算量主要来自矩阵乘法和卷积,这些操作在CPU上串行执行很慢,而GPU有成千上万个核心可以并行处理。PyTorch里把张量搬到GPU上只需要一行.cuda()或者.to(device),之后所有基于这个张量的运算都会自动在GPU上执行。我实测过一个简单的矩阵乘法,在CPU上要几秒,搬到GPU上瞬间完成,差距是数量级的。
再说自动求导。训练神经网络的核心是反向传播,也就是根据损失函数对每个参数的梯度来更新参数。手动推导和实现梯度不仅繁琐而且容易出错。PyTorch的autograd机制会自动记录所有涉及张量的运算,构建计算图,然后在调用.backward()时自动计算梯度。你只需要定义前向传播,反向传播框架帮你搞定。这个机制的设计非常巧妙,它让代码看起来就像普通的数学运算,但背后已经悄悄把梯度计算图建好了。
2.3 模块化设计:nn.Module的妙处
PyTorch用nn.Module来组织神经网络。任何自定义的模型、层、甚至损失函数,都可以继承这个类。它的核心作用是管理参数和子模块。当你把一个nn.Linear赋值给self.fc时,nn.Module会自动把这个层的权重和偏置注册到模型的参数列表里,之后调用model.parameters()就能拿到所有需要更新的参数,传给优化器即可。
这种设计的好处是层次清晰。一个复杂的模型可以拆成多个子模块,每个子模块负责一部分功能,最后在forward函数里把它们串起来。我见过一些新手把所有层都平铺在__init__里,forward里写一长串,结果想改个结构或者复用某部分时非常痛苦。合理的做法是把可复用的部分封装成独立的nn.Module,比如一个残差块、一个注意力模块,这样代码可读性和可维护性都会好很多。
注意:
nn.Module的forward函数定义的是前向传播逻辑,但不要直接调用model.forward(x),而应该用model(x)。因为model(x)会触发__call__方法,里面除了调用forward,还会处理一些钩子函数和状态管理,直接调forward会跳过这些逻辑。
3. 环境搭建与第一个可复现的训练流程
3.1 安装这件事,版本匹配比什么都重要
PyTorch的安装说简单也简单,一行命令的事,但说坑多也是真的多。最大的坑就是版本匹配。你的Python版本、CUDA版本、PyTorch版本、显卡驱动版本,这四者之间必须兼容,否则就是各种报错。
我的建议是,先确定你的显卡驱动支持的最高CUDA版本。在命令行输入nvidia-smi,右上角会显示“CUDA Version”,这个是你驱动支持的最高版本,不是你必须安装的版本。然后去PyTorch官网的安装页面,选择对应的CUDA版本,它会生成安装命令。如果你没有GPU或者不想折腾CUDA,直接装CPU版本就行,学习阶段完全够用。
另一个建议是用虚拟环境。不管是conda还是venv,一定要把PyTorch装在一个独立的环境里。我踩过的坑是,在base环境里装了PyTorch,后来装别的包时把依赖冲突了,导致PyTorch莫名其妙用不了,排查了半天。用虚拟环境可以隔离依赖,出问题了直接删掉重建,成本很低。
# 创建一个新的conda环境,指定Python版本 conda create -n pytorch_env python=3.10 conda activate pytorch_env # 安装PyTorch(以CUDA 11.8为例,具体命令以官网为准) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118安装完成后,验证一下是否成功:
import torch print(torch.__version__) print(torch.cuda.is_available()) # 如果有GPU,应该输出True如果torch.cuda.is_available()返回False,但你有GPU,大概率是版本不匹配。这时候先检查CUDA版本和PyTorch版本是否对应,再检查显卡驱动是否太旧。
3.2 数据加载:别让IO成为训练瓶颈
很多人训练慢,第一反应是模型太大或者GPU不行,但实际上数据加载往往是隐藏的瓶颈。PyTorch提供了Dataset和DataLoader两个类来处理数据。Dataset负责定义“怎么读一条数据”,DataLoader负责“怎么批量读、怎么打乱、怎么并行”。
自定义Dataset需要实现三个方法:__init__、__len__和__getitem__。__getitem__是核心,它接收一个索引,返回一条数据和对应的标签。这里有个关键点:不要在__getitem__里做太重的预处理,比如大图像的解码、复杂的增强。因为这些操作是在DataLoader的工作进程里执行的,如果太慢,GPU就会等数据,利用率上不去。
正确的做法是把轻量的预处理放在__getitem__里,比如归一化、简单的随机裁剪。如果数据量不大,可以提前把整个数据集预处理成张量存到内存里;如果数据量大,可以考虑用LMDB或者HDF5格式存储,读取速度比零散的小文件快很多。
DataLoader的num_workers参数控制并行加载的进程数。设置成CPU核心数通常是个不错的选择,但也不是越大越好。我实测过,num_workers从0增加到4,训练速度提升明显;但从4增加到8,提升就很小了,甚至因为进程间通信开销变大而变慢。建议从4开始试,根据实际情况调整。另外,pin_memory=True可以加速CPU到GPU的数据传输,尤其是当num_workers大于0时,效果比较明显。
from torch.utils.data import Dataset, DataLoader class MyDataset(Dataset): def __init__(self, data_list, transform=None): self.data_list = data_list self.transform = transform def __len__(self): return len(self.data_list) def __getitem__(self, idx): img_path, label = self.data_list[idx] # 这里用PIL读取图片,实际项目中可能是numpy数组或其他格式 image = Image.open(img_path).convert('RGB') if self.transform: image = self.transform(image) return image, label dataloader = DataLoader( dataset, batch_size=32, shuffle=True, num_workers=4, pin_memory=True )3.3 训练循环:把每一步都写清楚
PyTorch的训练循环非常直观,基本就是“前向传播、计算损失、反向传播、更新参数”这四步。但就是这四步,新手容易漏掉一些关键操作。
首先是梯度清零。PyTorch的梯度是累加的,也就是说如果你不在每个batch开始时调用optimizer.zero_grad(),梯度会一直累加,导致更新方向错误。这个坑我见过太多人踩,表现就是loss不下降或者震荡得厉害。记住,zero_grad()要放在loss.backward()之前。
其次是模式切换。训练时用model.train(),验证和测试时用model.eval()。这两个方法会影响Dropout和BatchNorm层的行为。训练时Dropout随机丢弃神经元,BatchNorm用当前batch的统计量;验证时Dropout不生效,BatchNorm用训练阶段累积的全局统计量。如果忘了切换,验证结果会很不稳定。
还有一个细节是损失函数的reduction参数。默认是'mean',也就是对一个batch里所有样本的损失取平均。如果你做的是多任务学习或者需要对不同样本加权,可能需要改成'none',然后自己手动处理。这个参数不搞清楚,loss的量级可能会和预期差很多。
model.train() for epoch in range(num_epochs): for images, labels in dataloader: images = images.to(device) labels = labels.to(device) optimizer.zero_grad() outputs = model(images) loss = criterion(outputs, labels) loss.backward() optimizer.step() # 每个epoch结束后做验证 model.eval() with torch.no_grad(): for images, labels in val_dataloader: images = images.to(device) labels = labels.to(device) outputs = model(images) # 计算验证指标 model.train()提示:验证阶段一定要用
with torch.no_grad():包起来。因为验证不需要反向传播,不构建计算图可以节省大量显存,也能加快速度。我见过有人验证时忘了加这个,结果显存爆了,还以为是模型太大。
4. 进阶实操:让训练更快更稳的几个关键点
4.1 学习率调度:别用一个固定值跑到底
学习率是训练神经网络最重要的超参数之一。设大了,loss震荡甚至发散;设小了,收敛慢得让人着急。更关键的是,固定学习率往往不是最优的。训练初期需要较大的学习率快速下降,训练后期需要较小的学习率精细调整。
PyTorch在torch.optim.lr_scheduler里提供了多种调度策略。最常用的是StepLR和CosineAnnealingLR。StepLR每隔固定epoch数把学习率乘以一个衰减系数,简单直接。CosineAnnealingLR按照余弦曲线从初始值降到最小值,曲线平滑,在很多任务上表现更好。
我个人的经验是,如果不知道选哪个,先用CosineAnnealingLR试试,通常不会太差。另外还有一个技巧是warmup,也就是训练最开始几轮用很小的学习率线性增加到初始值,然后再开始正常衰减。这对大batch训练或者Transformer类模型特别有用,可以避免初期梯度不稳定导致的震荡。
from torch.optim.lr_scheduler import CosineAnnealingLR optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) scheduler = CosineAnnealingLR(optimizer, T_max=num_epochs, eta_min=1e-6) for epoch in range(num_epochs): # 训练代码... scheduler.step()4.2 混合精度训练:省显存还能提速
如果你的显卡支持Tensor Core(比如Volta架构及以后的NVIDIA显卡),强烈建议开启混合精度训练。它的核心思想是:前向传播和反向传播用16位浮点数(FP16),参数更新用32位浮点数(FP32)。这样既能利用FP16计算快的优势,又能保持FP32的数值稳定性。
PyTorch从1.6版本开始内置了torch.cuda.amp模块,用起来非常简单。只需要在训练循环里加几行代码,就能实现自动混合精度。我实测下来,在支持Tensor Core的显卡上,训练速度能提升30%到50%,显存占用也能减少不少。
from torch.cuda.amp import autocast, GradScaler scaler = GradScaler() for images, labels in dataloader: images = images.to(device) labels = labels.to(device) optimizer.zero_grad() with autocast(): outputs = model(images) loss = criterion(outputs, labels) scaler.scale(loss).backward() scaler.step(optimizer) scaler.update()这里GradScaler的作用是动态调整损失缩放因子,防止FP16下梯度下溢。因为FP16能表示的最小正数比FP32大,小梯度直接变成0了,缩放一下就能保留住。这个机制是自动的,你不需要手动调。
注意:混合精度训练不是万能的。有些操作在FP16下数值不稳定,比如某些归一化层或者大数相加。如果开启后loss变成NaN,先检查是不是某些层需要强制用FP32。PyTorch的
autocast会自动处理大部分情况,但自定义操作可能需要手动指定精度。
4.3 模型保存与加载:别只存state_dict
训练完模型要保存,这个操作看似简单,但里面也有讲究。PyTorch有两种保存方式:一种是保存整个模型对象torch.save(model, path),另一种是只保存参数字典torch.save(model.state_dict(), path)。
我强烈推荐只保存state_dict。原因是保存整个模型会把模型类的定义也序列化进去,如果之后你改了模型代码,加载旧模型可能会报错。而state_dict只包含参数张量,和模型结构解耦,只要加载时模型结构一致就能用。另外,state_dict文件更小,加载也更快。
加载的时候,先实例化模型,再用load_state_dict加载参数。如果是在不同设备之间迁移,比如在GPU上训练、在CPU上推理,加载时加个map_location参数就行。
# 保存 torch.save(model.state_dict(), 'model.pth') # 加载 model = MyModel() model.load_state_dict(torch.load('model.pth', map_location='cpu')) model.eval()还有一个实用技巧是保存检查点,除了模型参数,还把优化器状态、当前epoch、最佳指标等一起存下来。这样训练中断后可以接着训,不用从头开始。对于大模型长训练来说,这个习惯能省很多时间。
checkpoint = { 'epoch': epoch, 'model_state_dict': model.state_dict(), 'optimizer_state_dict': optimizer.state_dict(), 'best_acc': best_acc, } torch.save(checkpoint, 'checkpoint.pth')5. 常见问题与排查技巧实录
5.1 显存不够用,先别急着换卡
显存不足是训练深度学习模型时最常见的问题之一。很多人一遇到CUDA out of memory就想着换显卡,但其实大部分情况下可以通过调整代码解决。
第一步是减小batch size。这是最直接有效的方法,batch size减半,显存占用大概也能减半。但要注意,batch size太小会影响BatchNorm的统计量准确性,也可能导致梯度噪声变大。如果减到很小还是不够,可以考虑用梯度累积:用小的batch size跑多次前向和反向,把梯度累加起来,再一次性更新参数。这样等效于大batch训练,但显存占用小。
第二步是检查有没有不必要的计算图保留。比如在验证阶段忘了加torch.no_grad(),或者在循环里把loss存到列表里导致计算图一直不释放。还有一个隐蔽的坑是,在训练循环里用total_loss += loss而不是total_loss += loss.item(),前者会保留计算图,后者只取数值。这个细节不注意,显存会随着训练轮数增长而爆掉。
第三步是用混合精度训练,前面已经讲过,FP16比FP32省一半显存。如果还不行,可以考虑梯度检查点技术,用计算时间换显存空间,但实现起来稍微复杂一些。
| 问题现象 | 可能原因 | 排查方法 | 解决方案 |
|---|---|---|---|
| 训练初期显存正常,后期爆掉 | 计算图未释放 | 检查是否用loss.item()累加loss | 改用.item()或detach() |
| 验证时显存比训练还高 | 未禁用梯度 | 检查验证代码是否在no_grad上下文 | 加with torch.no_grad(): |
| batch size减半后仍不够 | 模型本身太大 | 打印各层输出维度 | 减小模型或使用梯度检查点 |
| 多卡训练显存不均 | 数据分布不均 | 检查各卡batch分配 | 用DistributedDataParallel |
5.2 loss不下降,从这五个方向查
loss不下降或者震荡,是训练中最让人头疼的问题。根据我的经验,排查顺序可以按下面来。
先看数据。标签对不对?有没有标错?输入归一化了吗?我遇到过一次,图像像素值没归一化到0到1之间,导致输入数值很大,梯度爆炸,loss直接变成NaN。还有一次是标签编码错了,把类别索引和one-hot搞混了,模型学了半天学了个寂寞。数据问题是最隐蔽也最致命的,建议训练前先可视化几条样本,确认输入和标签的对应关系没问题。
再看学习率。学习率太大,loss会震荡甚至发散;太小,loss下降极慢。可以做个简单的实验:用几个不同的学习率各跑几百步,看loss曲线。通常能找到一个大致的合理范围。另外,如果用了自适应优化器如Adam,初始学习率一般设1e-3或1e-4,比SGD的0.1要小。
然后看模型结构。层数太深可能导致梯度消失,激活函数选错也可能有问题。比如输出层用Softmax做多分类,但损失函数用的是CrossEntropyLoss,而CrossEntropyLoss内部已经包含了Softmax,再手动加Softmax就重复了,会导致梯度计算错误。这个坑很常见,记住:用CrossEntropyLoss时,模型输出不要加Softmax。
接着看初始化。权重初始化太大会导致梯度爆炸,太小会导致梯度消失。PyTorch的默认初始化通常够用,但如果你自定义了层,最好显式初始化。比如用nn.init.kaiming_normal_对ReLU激活的层做初始化。
最后看优化器。不同优化器适合不同场景。SGD with momentum适合图像分类,Adam适合大多数情况,RMSprop在RNN上表现不错。如果换了优化器loss突然正常了,说明之前的选择不合适。
5.3 训练速度慢,逐项排查瓶颈
训练速度慢,可能的原因很多,需要逐项排查。我一般按下面的顺序来。
先确认GPU利用率。在命令行用nvidia-smi -l 1实时查看GPU利用率。如果利用率很低,比如只有20%到30%,说明GPU在等数据,瓶颈在数据加载。这时候增加num_workers、把数据预处理简化、或者把数据提前加载到内存,都能改善。
如果GPU利用率很高但训练还是慢,那可能是模型计算量本身大。这时候可以考虑用更高效的模型结构、减少输入分辨率、或者用混合精度训练。另外,检查一下有没有不必要的同步操作,比如在训练循环里频繁打印日志或者调用.cpu()把张量搬回CPU,这些都会打断GPU的流水线。
还有一个容易被忽略的点是数据格式。如果输入是NHWC格式而GPU更喜欢NCHW,或者数据类型不是FP32/FP16,都可能影响速度。PyTorch默认用NCHW,一般不需要改,但如果你从其他框架迁移过来,要注意格式转换。
实操心得:我习惯在训练脚本里加一个简单的计时器,记录每个epoch的数据加载时间和模型计算时间。这样一眼就能看出瓶颈在哪。如果数据加载占了总时间的30%以上,那就该优化数据管道了。
5.4 模型不收敛的隐蔽原因
有些问题更隐蔽,不是简单的学习率或数据问题。比如BatchNorm的momentum设得太大,导致统计量更新太快,训练不稳定。默认值是0.1,如果batch size很小,可以适当调小,比如0.01。
还有权重衰减的设置。权重衰减是正则化手段,但设得太大模型欠拟合,太小又起不到正则化效果。通常1e-4到1e-2之间比较常见。如果用了Adam,注意Adam的权重衰减实现和SGD不同,PyTorch的Adam默认是L2正则,不是真正的权重衰减。如果想用真正的权重衰减,可以用AdamW优化器。
另外,随机种子也会影响结果。深度学习训练有一定的随机性,不同的随机种子可能导致结果差异很大。如果一次训练不收敛,可以换个种子再试。但更重要的是,如果多次训练结果都不好,那就不是种子的问题,需要从数据和模型上找原因。
6. 从单卡到多卡:规模上去之后的新问题
6.1 DataParallel和DistributedDataParallel怎么选
当单卡训练太慢,需要多卡并行时,PyTorch提供了两种方案:DataParallel和DistributedDataParallel。DataParallel用起来简单,一行代码model = nn.DataParallel(model)就行,但它有个致命缺点:主卡负载高。因为所有卡的输出都要汇总到主卡计算loss,主卡的显存和计算压力比别的卡大,导致负载不均衡。而且DataParallel是单进程多线程,受Python GIL限制,效率不高。
DistributedDataParallel是官方推荐的多卡方案。它是多进程的,每张卡一个进程,各自独立计算梯度,然后通过通信操作同步梯度。负载均衡好,效率高,而且支持多机多卡。缺点是配置稍微复杂一些,需要设置进程组、用DistributedSampler分配数据。
我的建议是:如果只是临时用两张卡跑个小实验,DataParallel凑合能用;如果是正式训练或者卡数超过2张,直接上DistributedDataParallel,虽然配置麻烦点,但省心。
# DistributedDataParallel的启动方式(用torchrun) # torchrun --nproc_per_node=4 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 = MyModel().to(local_rank) model = DDP(model, device_ids=[local_rank]) # 数据要用DistributedSampler sampler = DistributedSampler(dataset) dataloader = DataLoader(dataset, sampler=sampler, batch_size=32)6.2 梯度同步与通信开销
多卡训练的核心是梯度同步。每张卡用自己的数据算出梯度后,需要把所有卡的梯度平均一下,保证每张卡上的模型参数保持一致。这个过程涉及卡间通信,是有开销的。
通信开销和模型参数量成正比。模型越大,同步一次梯度的时间越长。如果通信时间超过了计算时间,多卡加速比就会下降,甚至可能比单卡还慢。这就是所谓的通信瓶颈。
缓解通信瓶颈的方法有几个。一是用梯度累积,减少同步频率。比如每4个batch同步一次梯度,通信开销就降到四分之一。二是用更快的通信后端,NVIDIA显卡用NCCL,CPU用Gloo。三是重叠通信和计算,PyTorch的DDP默认会尽量把梯度同步和反向传播重叠起来,但需要确保模型结构合理,不要让某一层特别大导致同步等待。
还有一个经验是,batch size要随卡数增加。如果单卡batch size是32,4卡训练时总batch size最好设成128。因为多卡训练时每张卡还是处理32条数据,但梯度是4张卡平均的,等效于128的大batch。如果不同步增加学习率,收敛可能会变慢。通常学习率也要按比例放大,但具体放大多少需要实验。
7. 我踩过的那些坑和最后的小技巧
7.1 几个让我印象深刻的翻车现场
第一个坑是在Jupyter Notebook里训练模型。Notebook适合做探索性实验,但不适合长时间训练。因为Notebook的kernel可能会因为各种原因断开,一断训练就白跑了。而且Notebook里变量一直驻留内存,跑几个实验后显存就满了。我的建议是,探索阶段用Notebook,正式训练一定写成.py脚本,用命令行跑,配合检查点保存。
第二个坑是忘了设随机种子。有一次复现一个实验结果,怎么都复现不出来,折腾了一天才发现是没设种子,每次初始化不一样。PyTorch、NumPy、Python的random都要设种子,而且如果用了CUDA,还要设CUDA的种子。虽然完全确定性很难保证,但至少能减少随机性带来的差异。
import torch import numpy as np import random def set_seed(seed=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第三个坑是数据增强过度。数据增强是防止过拟合的好手段,但增强太狠会让模型学到错误的东西。比如做数字分类,把数字旋转180度,那6就变成9了,标签还是6,模型就懵了。增强策略要和任务匹配,不能盲目套用。
7.2 几个能提升效率的小习惯
第一个习惯是用torchinfo打印模型结构。torchinfo可以显示每一层的输出维度和参数量,比手动打印清晰得多。尤其是排查维度不匹配的问题时,一眼就能看出哪一层出了问题。
第二个习惯是用TensorBoard记录训练过程。PyTorch集成了TensorBoard,可以记录loss曲线、学习率变化、权重分布等。训练时开着TensorBoard,能实时看到训练状态,比盯着命令行输出直观多了。
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/experiment_1') for epoch in range(num_epochs): # 训练... writer.add_scalar('Loss/train', train_loss, epoch) writer.add_scalar('Loss/val', val_loss, epoch) writer.add_scalar('LR', optimizer.param_groups[0]['lr'], epoch) writer.close()第三个习惯是把配置和代码分离。用argparse或者配置文件管理超参数,不要硬编码在代码里。这样改参数不用动代码,也方便记录每次实验用了什么配置。我见过有人把学习率写在代码里,改一次跑一次,最后自己都忘了哪个结果对应哪个学习率。
7.3 后续可以继续深入的方向
PyTorch的生态很丰富,除了核心框架,还有很多值得探索的周边。比如torchvision提供了大量预训练模型和图像变换工具,torchaudio处理音频,torchtext处理文本。如果做部署,可以了解TorchScript和ONNX导出,把模型从Python环境里解放出来,用C++或者别的语言推理。
还有一个方向是自定义算子。PyTorch允许你用C++和CUDA写自定义的层和操作,通过torch.utils.cpp_extension编译成扩展模块。这在实现一些特殊操作或者优化性能时很有用。不过这个门槛稍高,建议先把Python层面的东西吃透再考虑。
最后,PyTorch的版本更新很快,新功能层出不穷。我的建议是关注官方博客和Release Notes,但不要盲目追新。生产环境里,稳定比新功能重要。升级版本前先在测试环境验证,确认没有兼容性问题再上。
最后分享一个小技巧:如果你在调试一个复杂的模型,可以在
forward函数里加一些断言,检查张量的形状和数值范围。比如assert x.shape[1] == expected_channels,assert not torch.isnan(x).any()。这些断言在训练时几乎不增加开销,但能在问题发生的早期就捕获到,比等到loss变成NaN再回头查要高效得多。