1. 为什么“pytorch”这个标题值得单独拎出来聊
如果你在技术社区里闲逛,看到有人只甩出一个词——“pytorch”,然后底下跟了一堆讨论,千万别觉得这是水帖。这个词背后藏着的东西,远比“一个深度学习框架”要复杂得多。它可能是一个刚入行的朋友在问“这玩意儿到底怎么上手”,也可能是一个老手在吐槽“版本又变了,之前的代码跑不通了”,甚至可能是一个团队在纠结“新项目到底选它还是选另一个”。
我写这篇东西的起因很简单:过去几年里,我陆陆续续带过不少新人,也帮一些团队做过技术选型的咨询。每次聊到深度学习框架,pytorch总是绕不开的话题。但奇怪的是,网上关于它的内容要么是官方文档的翻译,要么是零散的教程片段,很少有人从一个“实际干活的人”的角度,把它的设计思路、实操细节、踩坑经验串起来讲清楚。所以我想试试,用一篇长文把这件事说明白。
这篇文章适合谁看?如果你是刚接触深度学习的学生或者转行的开发者,它能帮你建立一套完整的认知框架,知道pytorch到底在干什么、为什么这么设计、怎么用才不别扭。如果你已经用了一段时间,但总觉得有些地方“知其然不知其所以然”,那这篇东西可能会帮你打通一些任督二脉。如果你是个团队的技术负责人,正在做框架选型,那里面关于生态、部署、性能的讨论应该也能给你一些参考。
核心关键词我会在行文中自然带出来:动态计算图、自动微分、张量操作、模型训练流程、GPU加速、生态工具链。这些词你肯定不陌生,但我会尽量把它们放在具体的场景里讲,而不是干巴巴地列定义。
2. 从“为什么是它”说起:pytorch的设计哲学与核心思路
2.1 动态图到底解决了什么痛点
要理解pytorch为什么能火起来,得先回到它诞生的那个时间点。在它之前,深度学习框架的主流是“静态图”模式。什么意思呢?就是你得先定义好整个计算流程,编译成一个固定的图,然后才能往里喂数据。这就像你装修房子,必须先把所有水电线路画好、固定死,然后才能开始住人。一旦想改个插座位置,对不起,得把墙砸了重来。
pytorch带来的最大改变就是动态计算图。它让计算图在运行时才构建,你写一行代码,它就执行一行,同时记录下这行操作,方便后面做梯度计算。这听起来好像只是个小改动,但实际用起来差别巨大。最直接的感受就是调试变得像写普通Python代码一样自然。你可以用print打日志,可以用pdb设断点,可以随时改变网络结构,甚至可以在前向传播里加if-else分支——这些在静态图时代都是很麻烦的事情。
我举个实际例子。假设你在做一个自然语言处理的任务,句子的长度是不固定的。用动态图,你可以直接写一个循环,每次处理一个词,根据当前的状态决定下一步怎么走。这种灵活性在处理变长序列、树结构、图结构数据时特别重要。而静态图往往需要你提前把所有可能的情况都考虑进去,用各种占位符和填充来模拟,代码写起来又长又容易出错。
当然,动态图也不是没有代价。早期的pytorch在性能上确实比不过那些静态图框架,因为静态图可以在编译阶段做大量的优化。但后来pytorch引入了TorchScript和torch.compile这些工具,把动态图的灵活性和静态图的性能优势结合了起来。你可以先用动态图快速开发和调试,等模型稳定了,再把它编译成静态图来加速推理。这个思路我觉得特别务实——先让开发者爽,再让机器爽。
2.2 自动微分:把梯度计算这件事彻底自动化
另一个让pytorch脱颖而出的点是它的自动微分系统。在深度学习里,训练模型的核心就是反向传播,也就是计算损失函数对每个参数的梯度,然后根据梯度更新参数。手动推导和实现梯度不仅繁琐,而且特别容易出错。pytorch的autograd引擎把这件事完全自动化了。
它的工作原理其实不复杂:每个张量都有一个requires_grad属性,当你把它设为True时,pytorch就会追踪所有对这个张量的操作,并构建一个计算图。当你调用.backward()时,它就会沿着这个图反向传播,自动计算梯度。整个过程对用户是透明的,你只需要关心前向传播的逻辑,反向传播交给它就行。
但这里有个细节值得注意:pytorch的自动微分是基于“磁带”机制的。每次前向传播都会构建一个新的计算图,反向传播完成后这个图就会被释放。这意味着你不能在反向传播之前修改参与计算的张量,否则会报错。这个设计是为了节省内存,但也要求你在写代码时稍微注意一下顺序。我见过不少新手在这里栽跟头,比如在循环里不小心把中间变量给覆盖了,结果梯度算出来全是错的。
还有一个常见的坑是in-place操作。像x += 1这种原地修改,在自动微分里可能会引发问题,因为它会覆盖掉计算梯度所需的值。pytorch通常会检测到这种情况并报错,但有时候错误信息不太直观,需要你仔细排查。我的建议是,除非你很清楚自己在做什么,否则尽量用x = x + 1这种非原地操作,虽然会多占一点内存,但能避免很多莫名其妙的错误。
2.3 张量:一切计算的基础单元
pytorch里的张量和NumPy的数组很像,但多了两个关键能力:一是可以在GPU上运行,二是支持自动微分。你可以把张量理解成一个多维的表格,里面装着数字。标量是0维张量,向量是1维,矩阵是2维,再往上就是高维张量。深度学习里的所有数据——图片、文本、音频——最终都会被转换成张量来处理。
张量的操作非常丰富,有元素级的加减乘除,有矩阵乘法,有形状变换,有索引和切片。这些操作和NumPy基本一一对应,所以如果你之前用过NumPy,上手会非常快。但pytorch的张量有一些自己的特点,比如view和reshape的区别。view要求张量在内存里是连续的,而reshape会自动处理不连续的情况。如果你不确定,用reshape更保险,但view在连续内存上效率更高。
还有一个经常被忽略的点是张量的设备管理。你可以用.to(device)把张量在CPU和GPU之间搬来搬去。但每次搬运都是有开销的,所以最好一次性把模型和数据都放到同一个设备上,避免在训练循环里频繁搬运。我见过有人把数据放在CPU上,模型放在GPU上,然后在每次前向传播时都搬一次数据,结果训练速度慢了好几倍。这种问题在代码里往往不明显,但性能分析工具一跑就能看出来。
3. 核心细节深挖:那些文档里不会明说的实操要点
3.1 模型定义:从nn.Module开始
在pytorch里定义模型,标准做法是继承nn.Module类。这个类提供了很多便利的功能,比如自动注册参数、管理子模块、切换训练和评估模式等。你需要在__init__方法里定义网络层,在forward方法里定义前向传播的逻辑。
这里有个新手常犯的错误:在__init__里定义层的时候,忘记把它们赋值给self。比如写nn.Linear(10, 5)而不写self.fc = nn.Linear(10, 5)。这样这个层就不会被注册到模型里,训练的时候参数根本不会更新。这个错误很隐蔽,因为代码不会报错,只是模型效果一直上不去。我建议每次定义完模型后,用print(model)看一下结构,确认所有层都在里面。
另一个值得注意的点是forward方法的灵活性。因为pytorch是动态图,你可以在forward里写任何Python代码,包括条件判断、循环、甚至调用其他函数。这给了你很大的自由度,但也意味着你需要自己保证逻辑的正确性。比如在处理变长序列时,你可以根据输入的实际长度来动态调整计算,而不需要像静态图那样用掩码来模拟。
还有一个小技巧:如果你需要在forward里使用一些不参与梯度计算的中间变量,可以用with torch.no_grad():上下文管理器来包裹这部分代码。这样可以减少内存占用,加快计算速度。比如在计算评估指标时,就不需要构建计算图。
3.2 数据加载:Dataset和DataLoader的配合
数据加载是训练流程里很容易被忽视但又特别重要的一环。pytorch提供了Dataset和DataLoader两个类来帮你处理这件事。Dataset负责定义如何获取单个样本,DataLoader负责批量加载、打乱顺序、多进程加速。
自定义Dataset需要实现__len__和__getitem__两个方法。__len__返回数据集的大小,__getitem__根据索引返回一个样本。这里有个性能上的考量:__getitem__里尽量不要做太重的计算,比如读取大文件、做复杂的预处理。这些工作最好提前做好,或者放在DataLoader的collate_fn里批量处理。
DataLoader的num_workers参数控制用多少个子进程来加载数据。设成0表示在主进程里加载,设成大于0的数会启动多个子进程。理论上num_workers越大,数据加载越快,但实际中受限于CPU核心数和磁盘IO速度。我一般会从4开始试,然后根据GPU利用率来调整。如果GPU利用率一直上不去,而CPU占用很高,那可能是数据加载成了瓶颈,可以适当增加num_workers。
还有一个坑是Windows系统上的多进程问题。在Windows上,num_workers大于0时,DataLoader会使用spawn而不是fork来创建子进程,这要求你的代码必须放在if __name__ == '__main__':保护块里,否则会无限递归创建进程。这个坑我踩过好几次,每次都在想“为什么代码在Linux上跑得好好的,到Windows就炸了”。
3.3 训练循环:看似简单,细节最多
训练循环的骨架很简单:前向传播、计算损失、反向传播、更新参数。但每个步骤里都有很多细节可以优化。
前向传播时,要确保模型处于训练模式(model.train()),这样Dropout和BatchNorm层才会按训练时的行为工作。计算损失时,要注意损失函数的输入顺序,比如nn.CrossEntropyLoss期望的输入是未经过Softmax的logits和类别索引,而不是概率值。反向传播前,记得用optimizer.zero_grad()清空之前的梯度,否则梯度会累加。更新参数时,optimizer.step()要在loss.backward()之后调用。
这些步骤看起来都是常识,但我在实际代码审查中经常发现有人漏掉zero_grad(),或者把step()和backward()的顺序搞反。这些错误不会导致程序崩溃,但会让训练结果完全不对。我的建议是,把训练循环写成一个固定的模板,每次新建项目时直接复制,避免手写出错。
还有一个进阶技巧是梯度裁剪。当梯度变得非常大时,参数更新会非常剧烈,导致训练不稳定。torch.nn.utils.clip_grad_norm_可以把梯度的范数限制在一个阈值内。这个技巧在训练RNN和Transformer时特别有用。我一般会把阈值设在1.0到5.0之间,具体取决于模型和任务。
3.4 GPU加速:不只是把模型搬到显卡上
用GPU加速是pytorch的一大卖点,但很多人只是简单地把模型和数据.to('cuda'),然后就觉得万事大吉了。实际上,要充分发挥GPU的性能,还有很多细节要注意。
首先是批大小的选择。批大小太小,GPU的并行计算能力利用不充分;批大小太大,内存可能不够,而且泛化性能可能会下降。我一般会从32或64开始试,然后根据GPU内存和训练效果来调整。如果内存不够,可以用梯度累积来模拟更大的批大小。
其次是混合精度训练。pytorch提供了torch.cuda.amp模块,可以自动把部分计算转换成半精度浮点数,从而减少内存占用、加快计算速度。实测下来,混合精度训练通常能带来1.5到2倍的速度提升,而且对模型精度的影响很小。不过要注意,有些操作对数值精度比较敏感,比如Softmax和LayerNorm,这些地方最好保持全精度。
还有一个容易被忽视的点是数据搬运。每次把数据从CPU搬到GPU都是有开销的,所以最好一次性把整个批次的数据搬过去,而不是一个一个搬。另外,如果数据在CPU上做了预处理,可以考虑把预处理也放到GPU上做,这样能进一步减少CPU和GPU之间的数据传输。
4. 完整实操流程:从零搭建一个图像分类项目
4.1 环境准备与依赖安装
在开始写代码之前,先把环境搭好。我推荐用conda来管理Python环境,因为它在处理科学计算相关的依赖时比较省心。创建一个新的环境,指定Python版本,然后安装pytorch和相关的库。
conda create -n pytorch-demo python=3.10 conda activate pytorch-demo安装pytorch时,要去官网根据你的操作系统、包管理器、CUDA版本选择合适的命令。如果你没有GPU,或者不想折腾CUDA,直接装CPU版本就行。CPU版本虽然训练慢,但用来学习和调试足够了。
# CPU版本 pip install torch torchvision torchaudio # GPU版本(CUDA 11.8) pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118除了pytorch本身,还需要安装一些辅助库:numpy用于数值计算,matplotlib用于画图,tqdm用于显示进度条,tensorboard用于可视化训练过程。这些库都可以用pip直接安装。
注意:安装GPU版本时,要确保你的显卡驱动版本和CUDA版本兼容。如果版本不匹配,pytorch会无法识别GPU,
torch.cuda.is_available()会返回False。这时候需要升级驱动或者换一个CUDA版本。
4.2 数据准备与预处理
我们用一个经典的图像分类数据集来演示。假设数据已经下载好了,放在data/train和data/val两个文件夹里,每个文件夹下面按类别分子文件夹。这种目录结构可以直接用torchvision.datasets.ImageFolder来加载。
import torch from torchvision import datasets, transforms from torch.utils.data import DataLoader # 定义预处理 train_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.RandomHorizontalFlip(), transforms.RandomRotation(10), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) val_transform = transforms.Compose([ transforms.Resize((224, 224)), transforms.ToTensor(), transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]) ]) # 加载数据集 train_dataset = datasets.ImageFolder('data/train', transform=train_transform) val_dataset = datasets.ImageFolder('data/val', transform=val_transform) # 创建DataLoader train_loader = DataLoader(train_dataset, batch_size=32, shuffle=True, num_workers=4) val_loader = DataLoader(val_dataset, batch_size=32, shuffle=False, num_workers=4)这里的Normalize参数是ImageNet数据集的均值和标准差,用预训练模型时通常都用这组值。如果你是从头训练,也可以自己算一下数据集的均值和标准差,但用ImageNet的值通常也没问题。
数据增强是提升模型泛化能力的重要手段。RandomHorizontalFlip和RandomRotation是最常用的两种,它们通过随机变换来增加数据的多样性。但要注意,有些增强操作可能会改变图像的语义,比如对于数字识别任务,水平翻转会把“6”变成“9”,这时候就不能用。所以增强策略要根据具体任务来定。
4.3 模型搭建与初始化
我们用一个预训练的ResNet-18来演示。预训练模型已经在大规模数据集上学习到了通用的特征,我们只需要替换最后的全连接层,让它输出我们需要的类别数。
import torch.nn as nn from torchvision import models def create_model(num_classes, pretrained=True): model = models.resnet18(pretrained=pretrained) # 冻结前面的层 for param in model.parameters(): param.requires_grad = False # 替换最后的全连接层 num_features = model.fc.in_features model.fc = nn.Linear(num_features, num_classes) return model model = create_model(num_classes=10)这里我们把前面的层都冻结了,只训练最后的全连接层。这样做的好处是训练速度快,而且不容易过拟合,特别适合数据量不大的情况。如果你的数据量很大,或者任务和预训练任务差异很大,可以考虑解冻更多的层,甚至全部解冻。
提示:冻结层的时候,记得把模型切换到评估模式(
model.eval())来跑一次前向传播,确认输出维度正确。然后再切换回训练模式(model.train())开始训练。
4.4 损失函数与优化器选择
对于多分类任务,nn.CrossEntropyLoss是最常用的损失函数。它内部结合了LogSoftmax和NLLLoss,数值上更稳定。优化器我一般用Adam或者AdamW,它们对学习率不那么敏感,适合快速实验。如果追求更好的最终效果,可以用SGD加动量,但需要更仔细地调学习率。
import torch.optim as optim criterion = nn.CrossEntropyLoss() optimizer = optim.AdamW(model.fc.parameters(), lr=1e-3, weight_decay=1e-4) scheduler = optim.lr_scheduler.CosineAnnealingLR(optimizer, T_max=10)这里只把model.fc.parameters()传给优化器,因为只有这部分参数需要更新。如果你解冻了更多层,记得把它们的参数也加进去。学习率调度器可以在训练过程中动态调整学习率,CosineAnnealingLR会让学习率按余弦曲线下降,通常能带来更平滑的收敛。
4.5 训练循环与验证
训练循环的代码看起来很长,但结构很固定。我习惯把它写成一个函数,方便复用。
def train_one_epoch(model, loader, criterion, optimizer, device): model.train() running_loss = 0.0 correct = 0 total = 0 for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) optimizer.zero_grad() outputs = model(inputs) loss = criterion(outputs, labels) loss.backward() optimizer.step() running_loss += loss.item() * inputs.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total return epoch_loss, epoch_acc def validate(model, loader, criterion, device): model.eval() running_loss = 0.0 correct = 0 total = 0 with torch.no_grad(): for inputs, labels in loader: inputs, labels = inputs.to(device), labels.to(device) outputs = model(inputs) loss = criterion(outputs, labels) running_loss += loss.item() * inputs.size(0) _, predicted = outputs.max(1) total += labels.size(0) correct += predicted.eq(labels).sum().item() epoch_loss = running_loss / total epoch_acc = correct / total return epoch_loss, epoch_acc验证循环里用torch.no_grad()包裹,这样可以不构建计算图,节省内存和时间。另外记得调用model.eval(),让Dropout和BatchNorm层切换到评估模式。
4.6 模型保存与加载
训练完成后,需要把模型保存下来。pytorch推荐保存state_dict而不是整个模型,因为state_dict只包含参数,不包含模型结构,加载时更灵活。
# 保存 torch.save(model.state_dict(), 'model.pth') # 加载 model = create_model(num_classes=10) model.load_state_dict(torch.load('model.pth')) model.to(device) model.eval()保存的时候还可以把优化器的状态也存下来,方便以后继续训练。但如果你只是要做推理,只存模型参数就够了。
注意:加载模型时,要先创建同样结构的模型,再调用
load_state_dict。如果模型结构不匹配,会报错。所以保存模型时最好把模型定义的代码也一起存下来,或者用版本控制工具管理代码。
5. 常见问题与排查技巧实录
5.1 训练不收敛怎么办
训练不收敛是新手最常遇到的问题。表现是损失不下降,或者震荡得很厉害。排查思路可以按以下顺序来:
首先检查数据。把一批数据可视化出来,看看图片和标签是不是对应的。我遇到过有人把标签搞错了,模型学了半天学了个反向映射。另外检查一下数据预处理,特别是归一化的均值和标准差,如果算错了,输入数据的分布会很奇怪,模型很难学。
然后检查模型。用print(model)看一下结构,确认层都注册了。跑一次前向传播,看看输出维度对不对。如果输出全是同一个值,可能是初始化有问题,或者某层的激活函数把输出压死了。
接着检查损失函数和优化器。损失函数的输入顺序对不对?学习率是不是太大了?我一般会先用一个很小的学习率(比如1e-5)跑几个批次,看看损失有没有下降。如果下降了,再慢慢调大。
最后检查梯度。用torch.autograd.gradcheck或者手动打印梯度,看看有没有梯度消失或爆炸。如果梯度全是0,可能是某处的requires_grad设错了,或者用了detach()把计算图断开了。
5.2 显存不够用的几种解法
显存不够是另一个高频问题。解决方法有几种,按推荐程度排序:
第一种是减小批大小。这是最直接的方法,但可能会影响训练效果。如果不想减小批大小,可以用梯度累积:每次只加载一个小批次,计算梯度但不更新参数,累积几次后再更新。这样等效于更大的批大小,但显存占用不变。
第二种是使用混合精度训练。torch.cuda.amp可以把部分计算转成半精度,显存占用能减少差不多一半。用法很简单,用autocast上下文管理器包裹前向传播,用GradScaler来缩放损失。
第三种是检查有没有不必要的张量占用显存。比如在训练循环里,如果某个中间变量一直没释放,显存会越用越多。可以用del手动删除,或者用torch.cuda.empty_cache()清空缓存。但empty_cache()治标不治本,还是要找到根源。
第四种是换更小的模型,或者用模型并行把模型拆到多张显卡上。这个比较复杂,一般项目用不到。
5.3 多GPU训练的那些坑
多GPU训练能显著加快训练速度,但配置起来有不少坑。pytorch提供了DataParallel和DistributedDataParallel两种方式。DataParallel用起来简单,但效率不高,而且只支持单机多卡。DistributedDataParallel效率高,支持多机多卡,但配置复杂一些。
用DataParallel时,要注意模型的第一层会在主卡上,所以主卡的显存占用会比别的卡多。如果主卡显存不够,可以把损失函数也放到主卡上算。另外,DataParallel在每次前向传播时都会复制模型到其他卡上,开销比较大。
用DistributedDataParallel时,需要用torch.distributed.launch或者torchrun来启动脚本,每个进程控制一张卡。数据加载要用DistributedSampler来保证每个进程拿到不同的数据。保存模型时,只在主进程上保存,避免多个进程同时写文件。
提示:多GPU训练时,学习率通常要按GPU数量线性放大。比如单卡用0.001,4卡就用0.004。但这不是绝对的,还要看批大小有没有相应调整。
5.4 模型部署时的注意事项
训练好的模型最终要部署到生产环境。部署时要注意几点:
第一,把模型切换到评估模式(model.eval()),并且用torch.no_grad()包裹推理代码。这样能关闭Dropout和BatchNorm的训练行为,同时减少内存占用。
第二,考虑用TorchScript把模型编译成静态图。这样可以脱离Python环境运行,速度也更快。用torch.jit.trace或者torch.jit.script来编译,然后保存成.pt文件。
第三,如果部署在服务器上,可以考虑用TorchServe或者ONNX Runtime来提供HTTP接口。TorchServe是官方工具,支持模型版本管理、自动扩缩容等功能。ONNX Runtime跨平台支持更好,适合嵌入到其他系统里。
第四,注意输入输出的预处理和后处理要和训练时保持一致。比如归一化的参数、图像的通道顺序、输出的解码方式,这些细节如果对不上,推理结果会完全错误。
6. 生态工具链:pytorch不只是个框架
6.1 torchvision、torchaudio、torchtext
pytorch的生态里有几个官方维护的领域库,分别针对视觉、音频和文本任务。torchvision提供了常用的图像数据集、模型架构和图像变换操作。torchaudio提供了音频加载、特征提取和预训练模型。torchtext提供了文本处理工具和预训练词向量。
这些库的好处是接口统一,和pytorch核心库配合得很好。比如torchvision.transforms可以和DataLoader无缝衔接,torchvision.models里的预训练模型可以直接加载。但它们的更新频率可能跟不上最新的研究进展,有些新模型可能要等一段时间才会加进去。
6.2 TensorBoard与可视化
训练过程中,可视化能帮你快速发现问题。pytorch集成了TensorBoard,可以记录损失曲线、准确率曲线、权重分布、计算图等。用法很简单,创建一个SummaryWriter,然后在训练循环里调用add_scalar、add_histogram等方法。
from torch.utils.tensorboard import SummaryWriter writer = SummaryWriter('runs/experiment_1') writer.add_scalar('Loss/train', train_loss, epoch) writer.add_scalar('Accuracy/train', train_acc, epoch) writer.close()启动TensorBoard后,在浏览器里就能看到实时的训练曲线。我一般会同时记录训练集和验证集的指标,这样能直观地看到有没有过拟合。如果训练集损失一直下降,验证集损失先降后升,那就是过拟合了,需要加正则化或者早停。
6.3 模型转换与跨平台部署
pytorch训练的模型可以转换成多种格式,方便在不同平台上部署。最常见的是ONNX格式,它是一种开放的模型交换格式,被很多推理引擎支持。用torch.onnx.export可以把pytorch模型转成ONNX。
dummy_input = torch.randn(1, 3, 224, 224) torch.onnx.export(model, dummy_input, 'model.onnx', input_names=['input'], output_names=['output'], dynamic_axes={'input': {0: 'batch_size'}, 'output': {0: 'batch_size'}})转换时要注意指定动态轴,这样模型才能处理不同批大小的输入。另外,有些pytorch的操作在ONNX里没有对应的实现,转换时会报错。这时候需要把那些操作替换成ONNX支持的操作,或者用自定义层来实现。
除了ONNX,还可以转成TensorRT来在NVIDIA显卡上加速推理,或者转成Core ML来在苹果设备上运行。每种格式都有自己的优缺点,选择时要考虑目标平台的硬件和软件环境。
7. 一些个人体会和后续可以折腾的方向
写了这么多,其实还有很多细节没展开。比如分布式训练的具体配置、自定义自动微分函数的写法、量化压缩的技术、模型剪枝的方法等等。每一个话题都够单独写一篇长文。
我个人在实际操作中的体会是,pytorch的学习曲线其实挺友好的,但前提是你得动手写。光看文档和教程,很多坑是遇不到的。我建议新手从一个小项目开始,比如用预训练模型做一个简单的图像分类,把整个流程跑通一遍。遇到问题就去查文档、搜社区、看源码。慢慢地,你就会对它的设计哲学和实现细节有更深的理解。
后续如果想继续深入,可以试试这几个方向:一是研究torch.compile的原理,看看它怎么把动态图编译成高效的静态图;二是学习自定义CUDA算子,把性能瓶颈的部分用C++和CUDA重写;三是探索分布式训练,用多机多卡来训练更大的模型。这些方向都有很多资料可以查,但最重要的还是动手实践。
最后再分享一个小技巧:如果你在复现别人的论文代码时遇到问题,可以先跑通作者提供的预训练模型,确认推理流程没问题,然后再尝试从头训练。这样能把问题范围缩小,更容易定位是数据的问题、模型的问题还是训练策略的问题。这个思路我在很多项目里都用过,屡试不爽。