1. 入门之前先理解PyTorch的设计思路与核心概念1.1 为什么大家现在都在用PyTorchPyTorch入门这个话题我在不同场合聊过很多次。带过的朋友里有刚接触编程的大三学生也有从其他框架转过来的后端工程师大家问的第一个问题往往都是同一个为什么是PyTorch答案其实藏在使用体验里。PyTorch最核心的设计是动态计算图你的模型代码就是普通的Python代码每一行按顺序执行随时可以打印中间张量的shape和数值。这对新手极其友好——你不需要像老派框架那样先定义好一张静态图再往里喂数据调试的时候脑子里不用多绕一层。写过几天之后你会意识到动态图带来的自由感对于学习阶段太重要了改模型结构就像改普通函数一样自然。另外PyTorch的生态在学术界和工业界都足够成熟。随便找个研究方向几乎都能找到用PyTorch实现的开源参考代码。对于入门者来说学的东西马上能用到真实项目里这种正反馈是支撑你走完初期枯燥阶段的重要动力。1.2 张量Tensor所有操作的基本对象刚开始接触PyTorch你最先见到的概念一定是张量Tensor。你可以把它理解成带设备信息的、能自动求导的多维数组。NumPy数组你肯定用过Tensor和它长得几乎一样但多了两个关键能力一个是能放到GPU上做并行计算另一个是能记录运算过程用于自动求导。先看最常见的创建方式import torch # 从列表创建 a torch.tensor([[1, 2], [3, 4]], dtypetorch.float32) # 全零、全一、随机 zeros torch.zeros(2, 3) ones torch.ones(3, 2) randn torch.randn(4, 4) # 和NumPy互转 import numpy as np np_arr np.array([1, 2, 3]) tensor_from_np torch.from_numpy(np_arr) back_to_np tensor_from_np.numpy()有几个细节新手经常会忽略我单独提一下。dtype一定要留意。默认的浮点类型是torch.float32但很多预训练模型和混合精度训练会用float16或bfloat16类型不匹配在后续损失计算或者设备转换时特别容易报错而且报错信息往往不太直观会让你找半天。device这个概念也要尽早建立。CPU和GPU上的张量是两种独立的东西不在同一个显存/内存空间里。你用tensor.cuda()或者tensor.to(cuda)把张量搬到GPU后运算要求两边都在同一个设备上否则会直接报Expected all tensors to be on the same device这类错误。我的习惯是写代码时一开始就定义device torch.device(cuda if torch.cuda.is_available() else cpu)后续统一用.to(device)省掉后面一堆麻烦。reshape、transpose、squeeze、unsqueeze这些操作也是高频使用的。view和reshape都能改变形状区别在于view要求张量在内存中是连续的而reshape会在不连续时自动拷贝。新手在这上面踩坑的次数不少我的建议是拿不准就用reshape语义更安全。1.3 autogradPyTorch的自动求导是怎么工作的很多教程把梯度讲得很神秘其实核心就一句话你定义了前向计算过程PyTorch自动帮你把每个参数的梯度算出来。它靠的是一张计算图。当你用一个requires_gradTrue的张量去执行运算时PyTorch会在后台记录这次运算的信息包括输入、输出、运算类型。这就像在你家里装了一个全程录像的摄像头从卧室走到厨房每一步都被记下来了。前向传播结束后你调用loss.backward()它就从最终的loss出发沿着记录反向走一遍用链式法则算出路径上每个参数的梯度。看一个最经典的线性回归示例比空谈原理有用得多import torch # 准备数据y 2x 1加一点噪声 x torch.linspace(0, 1, 100).reshape(-1, 1) y 2 * x 1 0.1 * torch.randn(x.shape) # 初始化参数 w torch.randn(1, requires_gradTrue) b torch.zeros(1, requires_gradTrue) learning_rate 0.01 for epoch in range(500): # 前向传播 y_pred x * w b loss ((y_pred - y) ** 2).mean() # 反向传播 loss.backward() # 手动更新参数 with torch.no_grad(): w - learning_rate * w.grad b - learning_rate * b.grad # 梯度清零 w.grad.zero_() b.grad.zero_() if epoch % 100 0: print(fepoch {epoch}, loss {loss.item():.4f}, w {w.item():.4f}, b {b.item():.4f})这里面有三个容易被忽略的点。第一loss.backward()计算出的梯度是累加到.grad上的。如果不手动清零下一次backward()会在原有梯度上继续加。这个特性在多机多卡、梯度累积场景里是有用的但对普通训练来说就是坑——你会看到loss忽高忽低还不知道为什么。第二更新参数时必须放在torch.no_grad()上下文里。原因很简单如果不包这一层更新参数的赋值操作本身又会被记录进计算图内存会越滚越大而且梯度链会变得混乱。用优化器的时候这个动作被封装了但里面做的事情就是这个。第三loss.item()会把单元素张量转成Python数值用于打印。直接用loss打印会出现tensor(0.1234, grad_fnMeanBackward0)这种带计算图信息的结果不利于阅读也容易让新手误以为loss是个普通数。2. 环境搭建与第一个模型从线性回归建立训练闭环2.1 安装与版本选择的取舍环境这块说简单也简单说烦也烦。我的建议是新手直接用Anaconda创建独立环境别把包都堆在系统环境里。不同项目对PyTorch版本、Python版本的需求可能互相冲突独立环境是成本最低的隔离方案。conda create -n pytorch_env python3.9 conda activate pytorch_env conda install pytorch torchvision torchaudio -c pytorch如果你没有GPU或者暂时不想处理CUDA驱动的问题直接装CPU版也完全够入门pip install torch torchvision --index-url https://download.pytorch.org/whl/cpu网上很多教程一上来就让你配CUDA其实没必要。CPU版把模型训练跑通、把原理弄懂之后再换GPU版就是换个安装命令的事。反过来如果从一开始就被驱动版本、CUDA版本、cuDNN版本这些事缠住很多人还没开始写模型就先被环境劝退了。关于版本核心原则是不要盲目追求最新版而是选择与你参考项目匹配的稳定版。PyTorch版本迭代节奏快API基本稳定但偶尔有破坏性变更。你复现开源代码时看到import torch报错大概率是版本不匹配。装好之后可以用下面的命令快速验证环境是否正常import torch print(torch.__version__) # 查看版本 print(torch.cuda.is_available()) # 查看CUDA是否可用 print(torch.cuda.get_device_name(0)) # 查看GPU型号如果可用2.2 用nn模块重写模型告别手写参数更新前面线性回归的例子适合理解原理但实际写项目时没人会手动更新参数。PyTorch提供了torch.nn和torch.optim把网络结构、损失函数、优化器都封装好了。以同样的线性回归为例换用nn模块后的写法是import torch import torch.nn as nn # 准备数据 x torch.linspace(0, 1, 100).reshape(-1, 1) y 2 * x 1 0.1 * torch.randn(x.shape) # 定义模型一个线性层输入维度1输出维度1 model nn.Linear(1, 1) # 损失函数均方误差 loss_fn nn.MSELoss() # 优化器随机梯度下降学习率0.01 optimizer torch.optim.SGD(model.parameters(), lr0.01) for epoch in range(500): # 前向传播 y_pred model(x) loss loss_fn(y_pred, y) # 梯度清零 optimizer.zero_grad() # 反向传播 loss.backward() # 参数更新 optimizer.step() if epoch % 100 0: print(fepoch {epoch}, loss {loss.item():.4f})你注意对比一下这里训练循环变成了一个固定的四步模式optimizer.zero_grad()清梯度loss.backward()算梯度optimizer.step()更新参数。顺序不能乱先清零再反向传播最后更新。很多新手把zero_grad写在backward后面梯度累加导致loss震荡排查半天。这就是PyTorch训练的最小闭环。不管你后面用CNN还是Transformer代码骨架都是这一套定义模型、定义损失函数、定义优化器、循环里执行前向传播 - 计算损失 - 清零梯度 - 反向传播 - 更新参数。把这个闭环刻在脑子里PyTorch入门就成功了一大半。3. 用卷积神经网络完成图像分类任务3.1 数据装载Dataset与DataLoader的配合学完线性回归很多人会觉得有点不过瘾毕竟连个像样的智能效果都没看到。接下来我们用经典的图像分类任务——手写数字识别走一遍真实项目的完整流程。手写数字识别用的是MNIST数据集每张图28x28灰度图共10个类别。这个数据集在PyTorch里可以直接下载用来练手非常合适。先看数据准备部分import torch from torch.utils.data import DataLoader from torchvision import datasets, transforms # 数据预处理转成Tensor并归一化 transform transforms.Compose([ transforms.ToTensor(), # 像素值从0~255转到0~1 transforms.Normalize((0.1307,), (0.3081,)) # 标准化用MNIST数据集的统计值 ]) # 下载并加载训练集和测试集 train_dataset datasets.MNIST(root./data, trainTrue, downloadTrue, transformtransform) test_dataset datasets.MNIST(root./data, trainFalse, downloadTrue, transformtransform) # DataLoader负责按批次取数据 train_loader DataLoader(train_dataset, batch_size64, shuffleTrue) test_loader DataLoader(test_dataset, batch_size64, shuffleFalse)transforms.ToTensor()做了两件事把形状从(28, 28)变成(1, 28, 28)顺便加了一个通道维度同时把像素值缩放到[0, 1]区间。标准化那一步则是让数据分布更集中训练更容易收敛。图像的均值和标准差需要在训练集上统计这里直接用了MNIST的标准值是公开约定俗成的。DataLoader里的batch_size和shuffle各有讲究。batch_size64表示每次给模型64张图这个参数影响训练速度和稳定性太小则梯度噪声大、收敛慢太大会把显存撑爆。shuffleTrue是训练集必须的打乱数据顺序能避免模型学到样本顺序里的虚假规律测试集不需要打乱。这里有个新手容易搞混的点Dataset负责取单个样本DataLoader负责批量打包。你自己写Dataset类时只需要实现__len__和__getitem__两个方法DataLoader会负责迭代、打包、并行读取。3.2 搭建一个卷积神经网络图像数据用全连接网络也能做但效果远不如卷积网络。卷积层利用空间局部性用很小的卷积核扫描整张图参数少、效果好。这里设计一个小型CNN结构不复杂跑在CPU上几十秒就能训完一个epochimport torch.nn as nn import torch.nn.functional as F class SimpleCNN(nn.Module): def __init__(self): super().__init__() # 第一个卷积块1通道 - 32通道卷积核3x3 self.conv1 nn.Conv2d(1, 32, kernel_size3, padding1) # 第二个卷积块32通道 - 64通道 self.conv2 nn.Conv2d(32, 64, kernel_size3, padding1) # 池化层 self.pool nn.MaxPool2d(2, 2) # 全连接层经过两次池化后图像尺寸 28 - 14 - 7 # 所以展平后是 64 * 7 * 7 self.fc1 nn.Linear(64 * 7 * 7, 128) self.fc2 nn.Linear(128, 10) def forward(self, x): # x形状: (batch, 1, 28, 28) x self.pool(F.relu(self.conv1(x))) # - (batch, 32, 14, 14) x self.pool(F.relu(self.conv2(x))) # - (batch, 64, 7, 7) x x.view(-1, 64 * 7 * 7) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) # 输出10类分数 return x注意forward里的形状注释这是我这几年带新人时反复强调的习惯。每一层改变张量形状你都要能在心里算出来否则后面改了网络结构全连接层的输入维度错了报错信息会直接让你懵掉。写网络时把每个关键位置的shape注释出来能省掉大量调试时间。view(-1, 64 * 7 * 7)里的-1表示自动推断这个维度即batch大小。手动计算尺寸容易错尤其是改padding、stride之后。我的做法是写一句测试代码用随机的假数据过一遍模型model SimpleCNN() fake_input torch.randn(64, 1, 28, 28) # 模拟一个batch output model(fake_input) print(output.shape) # 应该是 torch.Size([64, 10])如果这一步输出形状不对说明网络内部维度算错了改到对为止再进行训练。这也是个小成本验证大问题的好习惯。3.3 训练与评估完整跑通一次迭代有了数据、模型、优化器和损失函数就可以跑完整训练了。图像分类用的是nn.CrossEntropyLoss()它把softmax和交叉熵损失合在了一起所以你模型最后一层不需要额外接softmax输出的是每个类别的分数即可。优化器我建议从Adam开始它对学习率的敏感性比SGD低对新手友好。import torch.optim as optim device torch.device(cuda if torch.cuda.is_available() else cpu) model SimpleCNN().to(device) loss_fn nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001) def train_one_epoch(model, loader, loss_fn, optimizer, device): model.train() # 训练模式 total_loss 0.0 for images, labels in loader: images, labels images.to(device), labels.to(device) outputs model(images) loss loss_fn(outputs, labels) optimizer.zero_grad() loss.backward() optimizer.step() total_loss loss.item() return total_loss / len(loader) num_epochs 5 for epoch in range(num_epochs): avg_loss train_one_epoch(model, train_loader, loss_fn, optimizer, device) # 每个epoch结束后在测试集上评估 model.eval() # 评估模式 correct 0 total 0 with torch.no_grad(): # 评估时不需要梯度 for images, labels in test_loader: images, labels images.to(device), labels.to(device) outputs model(images) _, predicted torch.max(outputs, 1) total labels.size(0) correct (predicted labels).sum().item() accuracy 100.0 * correct / total print(fepoch {epoch1}, train loss {avg_loss:.4f}, test acc {accuracy:.2f}%)model.train()和model.eval()切换是个必须养成的习惯。很多网络层如Dropout、BatchNorm在训练和推理时的行为不一样不切换会导致结果异常。比如BatchNorm在train模式下用批次统计量在eval模式下用全局统计量忘了切换验证准确率会忽高忽低。torch.max(outputs, 1)返回两个值最大值和对应的索引索引就是预测的类别编号。torch.no_grad()在评估时一定要加因为推理不需要反向传播不关掉的话它会记录计算图白白消耗内存数据量大时可能直接OOM。跑完这5个epoch准确率一般能到99%左右。很多人第一次看到这个数字会很兴奋——这其实就是你从会跑示例到理解训练流程的分水岭。4. 新手必看的常见问题与排查技巧4.1 训练loss不降反升怎么办这是新手遇到最多的问题。loss不降先别急着调模型按照下面的顺序排查第一检查数据。把输入数据的范围打出来看看是不是有大量NaNlabel是不是对得上类别索引。MNIST这种公开数据集没问题但你自己收集数据时清洗环节出错是最常见的坑。第二检查学习率。lr设置太大loss会来回震荡甚至直接发散到NaN太小loss下降得极其缓慢看起来像没在学。一个朴素的排查方法是把lr试着缩小10倍或放大10倍各跑几十步观察变化。Adam的话从1e-3开始是比较稳的。第三检查模型和损失是否匹配。比如用了CrossEntropyLoss却给模型加了sigmoid输出或者多标签分类用了BCEWithLogitsLosslogits输入和target的shape没对齐。这类错误不会报错但loss的数值和收敛行为会很奇怪。第四归一化层和网络的默认初始化在极少数情况下会组合出梯度消失的现象如果前几步loss完全不动可以试试更换初始化方式或者加残差连接。4.2 显存不够OOM的排查方向CUDA out of memory是GPU版最经典的报错。初学者遇到这个第一反应是我的卡不够好但很多时候其实是代码问题。最常见的诱因是训练循环里忘了把计算图释放。如果你没用optimizer.zero_grad()或者在使用loss.backward()时保留了不该保留的中间变量显存占用会一路飙升。另外tensor.item()和tensor.detach().cpu().numpy()的混用如果放在循环里频繁执行也可能不经意间累积张量引用。排查时可以用一行命令看显存占用print(torch.cuda.memory_summary())它会列出每块内存的分配情况。我的建议很简单先确认zero_grad放在正确位置再检查no_grad是否用对了地方最后才考虑降低batch_size。把batch_size从64降到32是最快的降显存手段代价是训练稳定性略降。4.3 数据加载慢到怀疑人生很多人数据集不大但每个epoch都要等很久。这往往不是计算问题而是DataLoader的读取瓶颈。PyTorch提供了num_workers参数用来指定并行加载数据的进程数。默认值是0表示只在主进程里同步加载速度非常慢。Windows上设置num_workers 0需要放在if __name__ __main__:里保护Linux则没有这个限制。train_loader DataLoader(train_dataset, batch_size64, shuffleTrue, num_workers4)还有个小坑如果你的数据预处理里有耗时操作比如读图后做复杂变换尽量用transforms组合并提前处理好存成缓存而不要每次训练都重复计算。对于超大数据集善用内存映射或者直接把预处理后的数据一次性加载进内存速度提升是数量级的。4.4 梯度相关异常的处理经验RuntimeError: gradient computation has been modified by an inplace operation这个报错新手见到基本是一头雾水。它通常源自对requires_grad张量的原地修改比如用了x y或x[0] 1这种操作。PyTorch保存了计算图上的信息用于反向传播遇到原地修改就可能对不上号于是直接报错。解决思路也很简单遇到需要对张量做更新但又不想破坏梯度链的场景用.clone()先复制一份再修改或者把操作放到torch.no_grad()里。平时写代码时养成一个习惯——凡是requires_gradTrue的张量不要用就地赋值的方式修改能避掉绝大多数这类报错。还有个经常被忽略的点是retain_graphTrue。如果你在同一个计算图里调用多次backward()比如某些需要计算二阶导的场景第一次backward后计算图会被释放第二次就会报错。这时需要在第一次调用时加retain_graphTrue。但注意不要滥用它会让计算图一直驻留内存正常训练里不需要用到。5. 跑通之后我建议你接下来做这三件事如果你跟着上面的流程把MNIST跑到了99%左右的准确率说明PyTorch的基础闭环你已经掌握了。接下来怎么深入我的建议是不要急着去追各种花哨的新架构而是先把基础功练扎实。第一件事自己动手改数据集。把MNIST换成Fashion-MNIST你会发现同样的代码、同样的模型准确率会掉到90%左右。这个掉点的过程非常锻炼人——你会被迫去思考数据复杂度对模型容量的影响从而真正理解为什么需要调结构、调超参而不是纸上谈兵。第二件事尝试手动实现一个简单的优化器。Adam的公式不复杂你照着论文里四个更新步骤手写一遍再和optim.Adam的结果对比。这个练习能让你彻底搞清楚momentum、weight_decay这些参数到底在干什么。我周围很多学到一定阶段卡住的人回去补了这个基本功之后对训练过程的理解都上了一个台阶。第三件事学会用工具观察训练过程。把loss曲线画出来把验证集准确率画出来把每个batch的梯度范数打出来。你不需要用什么高级平台一张画布坐标纸或者matplotlib就够。训练不是玄学所有异常都能从这些曲线里看出端倪——loss呈锯齿状说明batch_size太小或学习率偏高loss下降一段后平台期说明模型容量到头了梯度范数突然增大往往预示着数据里有异常样本。这些观察经验比背十篇教程都管用。最后再分享一个我个人的习惯每跑完一个项目把踩坑记录写在一个文本文件里一行一个坑标上当时的环境版本。这个习惯救过我很多次。深度学习框架迭代快你踩过的坑过半年很可能在别的地方又遇到到时候翻翻自己的记录比重新上网查快得多。学习PyTorch这条路最大的障碍从来不是不会写代码而是不知道去哪查问题。把基础弄扎实把排查思路理顺剩下的就是时间问题了。