☰
PyTorch基础实操:张量、自动求导与训练循环全解析
2026/10/15 12:12:38 网站建设 项目流程

1. 为什么我推荐从PyTorch入手深度学习框架

先交代个背景。我在深度学习这条路上摸爬滚打也有几年了,早年间用的框架比较老,写模型像写语文作文,先定义计算图、再编译、再喂数据,中间任何一步想调试都恨不得把整个流程重新跑一遍。后来切到PyTorch,最大的感受就是:终于能用写普通Python代码的方式写深度学习模型了。

如果你正处在"理论学了不少、准备动手跑第一个模型"的阶段,PyTorch基本是绕不开的选择。几个关键原因:

  • 动态计算图。PyTorch的计算图是边跑边搭的,你可以像写普通程序一样随意print中间变量、用if分支改变网络结构、在调试器里慢慢看每一步张量的shape和数值。这对于理解和排错太重要了。
  • 与NumPy无缝衔接。Tensor和NumPy数组之间的转换只需要一行代码,对已经熟悉Python数据处理的同学来说几乎没有学习成本。
  • 生态完整。无论是图像分类、目标检测、NLP还是推荐系统,市面上主流预训练模型的PyTorch版本几乎都是更新最快、资料最全的那一份。
  • 社区反馈速度。遇到报错,搜索基本都能找到答案,这是初学者的隐形刚需。

这篇笔记不是PyTorch官方文档的翻译,也不是把某个教程从头抄到尾。我想从一个实际动手做过不少模型的人的角度,把PyTorch最核心的几个基础模块——张量、自动求导、模型搭建、数据加载、训练循环——掰开揉碎讲清楚。每个部分我都会告诉你为什么需要这个东西、底层大概怎么工作、实际用的时候要注意什么。适合刚学完Python和简单机器学习基础、准备进入深度学习的读者,也适合看文档看得一头雾水、想换个角度重新理解的人。

2. 张量(Tensor):模型处理的一切都是它

2.1 从NumPy到Tensor:多出来的三样东西

如果你用过NumPy,那对Tensor应该不会太陌生。Tensor本质上就是一个多维数组,你可以把它想象成一个可以装任意维度数据的"盒子":一维的叫向量,二维的叫矩阵,三维以上就叫张量。图像是三维张量(高度×宽度×通道),一句话是一维序列,一批句子就是二维或三维张量。

但相比NumPy数组,Tensor多了三样关键能力:

  1. 可以放到GPU上计算。这是深度学习能跑得动的基础。一张普通的显卡,算力比CPU高出几十倍不止。
  2. 自动记录计算历史。每个张量会记住自己是通过什么运算得到的,这正是后续自动求导的基础。
  3. 支持自动求导。这是代码能够自动帮你算梯度、做反向传播的核心机制。

来看最基本的创建方式:

import torch # 从列表或NumPy创建 a = torch.tensor([1, 2, 3]) # 一维张量 b = torch.zeros(2, 3) # 2x3全零矩阵 c = torch.randn(3, 3) # 3x3标准正态分布随机数 d = torch.arange(0, 10, 2) # [0, 2, 4, 6, 8]

实际写代码的时候,我更喜欢用torch.zeros、torch.ones、torch.randn这类带初始化函数的创建方式,因为它们顺手,而且语义明确。

2.2 形状(shape)是PyTorch的第一语言

我见过太多初学者在模型跑不通的时候一头雾水,最后排查半天发现是张量形状多了一个维度或者少了一个维度。在PyTorch的世界里,shape就是第一语言。模型每一层的输入输出形状,就是数据在这些层之间流动的"接口契约"。

举一个特别常见的场景。你要处理一批MNIST手写数字图片,每张图是28×28像素,训练时一般一次喂32张图。那么你的输入张量形状就是(32, 1, 28, 28)——32是批次大小,1是通道数(黑白图单通道),后面两个28是宽高。

x = torch.randn(32, 1, 28, 28) print(x.shape) # torch.Size([32, 1, 28, 28])

有一个操作我会反复用,就是打印每一层的输出形状,配合注释把形状变化写清楚:

def forward(self, x): # 输入形状: [batch, 1, 28, 28] x = self.conv1(x) print(x.shape) # 确认卷积后形状变化 x = self.pool(x) print(x.shape) # ...

一旦某个层对不上号,立刻就能定位到是哪一步的形状出了问题。

2.3 设备(device):CPU和GPU的"户口"问题

张量有个隐藏属性叫device,标识它住在CPU上还是GPU上。这个属性虽然不起眼,但没处理好会让程序直接崩掉——CPU上的张量和GPU上的张量不能直接做运算。

x_cpu = torch.randn(3, 3) x_gpu = x_cpu.cuda() # 搬到GPU y_gpu = torch.randn(3, 3, device='cuda') print(x_gpu + y_gpu) # 正常 # print(x_cpu + y_gpu) # 报错!CPU和GPU张量不能直接相加

正确做法是先判断环境里有没有GPU,再决定把张量放哪:

device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') x = x.to(device)

养成模型.to(device)+数据.to(device)的肌肉记忆,能避开很多莫名其妙的设备不匹配报错。

2.4 索引、切片和广播:写起来像NumPy

Tensor的索引切片和NumPy几乎一模一样,从a[0]到a[1:3]再到布尔索引都支持:

x = torch.randn(4, 3) print(x[:, 0]) # 所有行的第一列 print(x[x > 0]) # 布尔索引,选出大于0的元素

再就是广播机制。两个形状不完全一致的张量做运算时,PyTorch会尽力把它们的形状"扩展"到一致再算。比如一个形状是[4, 1]的张量和一个形状是[3]的张量相加,PyTorch会自动把前者沿着第二维扩展成[4, 3],把后者沿着第一维扩展成[4, 3],然后逐元素相加。这个过程符合直觉、用起来省事。

但我必须提醒一点:广播虽然方便,千万不要滥用。我早期写代码图省事,让不同形状的张量直接相加,结果网络结构复杂之后完全不知道哪个维度被偷偷扩展了,调试起来非常痛苦。现在我的习惯是:凡是需要对齐形状的地方,一律显示的unsqueeze或reshape写清楚,宁可多两行代码,也要保证每个张量的形状都是自己心里有数的。

2.5 三个高频操作:reshape、permute、unsqueeze

  • reshape:重新定义张量的形状。比如把28×28的图像拉平成784维向量。
  • permute:交换维度顺序。比如把[batch, height, width, channel]转成[batch, channel, height, width],这在图像处理里太常见了。
  • unsqueeze:在指定位置插入一个大小为1的维度。比如把形状[28, 28]变成[1, 28, 28]。
x = torch.randn(28, 28) x_flat = x.reshape(-1) # 784 x_batch = x.unsqueeze(0) # [1, 28, 28] x_permuted = x.permute(1, 0) # 交换行列

这一小节的内容看似繁琐,但你要知道:深度学习模型里90%的报错都跟形状有关。熟练掌握这几个操作,等于给自己省掉了一半的debug时间。

3. 自动求导(autograd):让反向传播不再神秘

3.1 没有autograd之前,梯度是手算的

早期写神经网络的人有个噩梦:要自己手推每一层的梯度公式,然后一行行代码实现。网络稍微深一点,链式法则一展开就是一堆复杂的乘积求和,极容易出错。PyTorch的autograd模块把这个过程彻底自动化了——你只需要把前向计算写出来,梯度是自动算好的。

它的机制简单说就是:PyTorch在张量背后偷偷建了一张"计算图"。你每做一次运算,图里就新增一条记录,存下运算类型和参与运算的张量。等你要算梯度了,就从最终结果出发,沿着这张图反向走一遍,自动把每个中间变量的梯度算出来。

3.2 requires_grad、grad_fn和backward

只有带requires_grad=True的张量才会被记录进计算图。默认创建的张量这个属性是False,你也不需要给所有张量都打开,只要给"需要求梯度的叶子变量"(一般是模型参数)打开就行。

x = torch.tensor([2.0], requires_grad=True) y = x ** 2 # y = x^2 y.backward() # 自动求导 print(x.grad) # tensor([4.]),因为 y' = 2x = 4

每个张量都有一个grad_fn属性,记录它是怎么算出来的。y的grad_fn是PowBackward0(表示是幂运算的结果),x因为是叶子节点,grad_fn为None。调试时我经常靠grad_fn判断一个张量是不是模型参数直接算出来的中间结果。

3.3 梯度累积的陷阱:backward之后到底要不要手动清零

这是初学者最容易踩的坑,没有之一。默认情况下,.backward()不会清空张量的梯度,而是把新算出来的梯度累加到已有的梯度上。

optimizer.zero_grad() # 1. 清空上一轮梯度 loss.backward() # 2. 计算本轮梯度 optimizer.step() # 3. 更新参数

标准的训练循环里这三步缺一不可。很多人写了训练代码,loss明明在下降,参数却纹丝不动,八成就是忘了optimizer.zero_grad(),梯度一直在累加,把更新方向搞得乱七八糟。我的习惯是:在训练循环里,zero_grad永远放在当前batch的前面一句,而不是放在上一轮batch的末尾。这样即使某个batch出错了提前break,下一个batch也不会被上一轮的残留梯度污染。

3.4 多个loss相加后的backward

有时模型会有多个损失项,比如一个任务同时要分类和回归,你可能会写loss = loss_cls + 0.5 * loss_reg。这种情况下直接对loss调一次backward()是没有问题的,因为PyTorch会沿着计算图把梯度同时传给所有出现过的参数。

我见过有同学写出这样的代码:

loss_cls.backward() loss_reg.backward()

如果这两个loss共享了一部分网络层,那这两个调用会把梯度分别累加上去,等于无意中把那一部分参数的梯度放大了两倍。所以正确做法是:先把多个loss按权重合成为一个总loss,再对总loss做一次backward。除非你有特殊理由要分步更新参数,否则不要对多个loss分别调用backward。

3.5 gradient detach:当你不想让它继续回传的时候

有些训练技巧里,你希望某个分支的计算不被梯度影响,比如对抗训练、GAN里要固定住一部分网络,或者你想把某个中间结果当常数用。方法是.detach():

z = x.detach() # z和x共享数据,但z不会记录任何计算历史

这个操作的意思是:我要这个数值,不要它参与后续的梯度计算了。实际用它可以把一个不需要回传梯度的分支和主分支隔离开来,避免梯度错误地传播到不该更新的参数上。

4. 用 nn.Module 搭模型:从手写网络结构开始

4.1 为什么一定要用nn.Module

理论上你可以只用Tensor和autograd从零写一个神经网络,就是自己定义权重、自己算输出、自己写更新规则。但实际项目里没人这么干,因为:

  • 手动管理权重参数太容易遗漏;
  • 各层初始化、激活函数、损失函数、优化器的实现都很成熟,没必要重复造轮子;
  • nn.Module自带一套参数注册和整理的机制,model.parameters()一条命令就能拿到模型所有可训练参数,给优化器用。

nn.Module可以理解为"深度学习模型的积木框架":你把各个层当成积木塞进去,框架帮你统一管理参数、设备迁移、训练/评估模式切换等所有琐碎事务。

4.2 两种最常用的建模型方式

第一种,用nn.Sequential搭简单的线性堆叠网络,适合前馈模型:

import torch.nn as nn model = nn.Sequential( nn.Linear(784, 256), nn.ReLU(), nn.Linear(256, 10) )

第二种,继承nn.Module定义子类,适合更灵活的结构。关键要记住两点:在__init__里定义所有子层,在forward里定义前向计算逻辑。

class MLP(nn.Module): def __init__(self, in_dim=784, hidden_dim=256, out_dim=10): super().__init__() self.fc1 = nn.Linear(in_dim, hidden_dim) self.relu = nn.ReLU() self.fc2 = nn.Linear(hidden_dim, out_dim) def forward(self, x): x = self.fc1(x) x = self.relu(x) x = self.fc2(x) return x model = MLP()

我这里只写了线性层,实际你可以替换成nn.Conv2d、nn.LSTM、nn.TransformerEncoderLayer等等,套路完全一样。

4.3 forward做了什么,为什么它决定了"灵活度"

跟旧式框架需要先定义好完整的计算图再执行不同,PyTorch的forward就是一段普通Python代码。这意味着:

  • 你可以用if-else给不同输入走不同路径;
  • 你可以在中间层随便打印、调试、修改张量;
  • 你可以把同一个层在不同位置调用多次(权重共享)。

这种灵活性在日常实验中非常实用。比如我调试一个网络时,经常在forward里临时加一行print(x.shape),确认形状没问题再删掉,整个过程不用改任何别的东西。

def forward(self, x): x = self.conv1(x) print('after conv1:', x.shape) # 临时调试 x = torch.relu(x) x = self.pool(x) return x

4.4 用model.parameters()做自查

有个检查模型定义是否正确的通用办法:

total_params = sum(p.numel() for p in model.parameters()) print(f'总参数数量: {total_params}')

每层参数量心里有数之后,模型大小、显存占用基本可以估算个大概。如果发现参数量过于庞大或者小得离谱,八成是网络结构定义漏了或错了。需要精确到每个层的参数名和shape,用:

for name, param in model.named_parameters(): print(name, param.shape)

这个输出在写论文、做实验记录或者跟同事讨论模型方案的时候非常有用。

4.5 model.train()和model.eval()到底切换了什么

这两个方法很多人会忽略,但它们直接改变了某些层的运行行为。

  • model.train():把模型切成训练模式。这个时候BatchNorm层会使用当前batch的均值和方差来归一化,Dropout层会随机丢弃一部分神经元。
  • model.eval():把模型切成推理模式。BatchNorm使用训练阶段累计的全局统计量,Dropout不再丢弃任何神经元。

踩过的一个典型坑是:模型预测阶段忘了切回model.eval(),导致每次跑出来的结果都不一样。因为Dropout仍然在随机丢神经元,每次前向传播的结果自然不可复现。记住:训练前调model.train(),验证/推理前一定调model.eval()。

另外还得提醒一下,model.eval()并不会阻断梯度计算。如果只是想快速算一下模型输出、不关心梯度,可以配合with torch.no_grad():使用,这个后面训练循环里会详细说。

5. 数据管线:Dataset和DataLoader的配合逻辑

5.1 为什么不能直接手动循环喂数据

很多初学者嫌麻烦,把数据读进来之后用普通的for循环一个batch一个batch地手动切,然后塞给模型。这样做在小数据上问题不大,真实项目里马上会遇到:批量加载乱序、预处理逻辑散落各处、多进程加速没法用、batch大小调整牵一发动全身。

PyTorch给了一套标准流程:Dataset负责定义"一条样本长什么样",DataLoader负责从Dataset里批量取数据、打乱顺序、多进程加载。

5.2 用Dataset封装自己的数据格式

一个最小可用的Dataset只需要实现两个方法:__len__返回数据量,__getitem__按下标返回一条样本。下面是一个图像分类数据的例子:

from torch.utils.data import Dataset class ImageDataset(Dataset): def __init__(self, image_paths, labels, transform=None): self.image_paths = image_paths self.labels = labels self.transform = transform def __len__(self): return len(self.image_paths) def __getitem__(self, idx): img = load_image(self.image_paths[idx]) # 自己实现加载函数 label = self.labels[idx] if self.transform: img = self.transform(img) return img, label

__getitem__返回的是一条样本,一般是一个元组(输入, 标签),后续所有数据增强、归一化都可以放在这里做。在真实工程里,数据预处理放在__getitem__里能保证每条样本都经过相同的预处理流程,逻辑非常集中。

5.3 DataLoader的参数:batch_size、shuffle和num_workers

from torch.utils.data import DataLoader dataloader = DataLoader( dataset, batch_size=64, shuffle=True, # 训练时打乱顺序 num_workers=4, # 用4个子进程并行取数据 drop_last=True # 最后不满一个batch时丢弃 )

这里面的关键参数值得一个个说:

  • shuffle=True:训练时打乱,防止模型学到数据顺序中的虚假模式。验证和测试时设False,保证评估结果可复现。
  • num_workers:并行加载数据的子进程数。在Windows上如果设为大于0,必须把训练主逻辑放到if __name__ == '__main__':里,否则多进程会递归地把整个脚本又执行一遍,常见的报错就是"RuntimeError: An attempt has been made to start a new process before the current process has finished its bootstrapping phase"。
  • drop_last:数据量不能被batch_size整除时需要决定最后那部分残片如何处理。训练时我一般设True,避免一个非常小的batch在BatchNorm里产生抖动。验证时设False,宁可最后一批少一点也要把所有数据都看一遍。

5.4 collate_fn:当样本需要特殊打包的时候

DataLoader默认把一组样本打包成batch的方式是"按第0维堆叠",要求所有样本的形状完全一致。但现实中有许多数据不是一个规则的张量:NLP里句子长度不一样、目标检测里每个图片的标注框数量不一样。此时就需要自定义collate_fn,告诉你"怎么把这一批样本拼成一个batch"。

def collate_fn(batch): images = torch.stack([item[0] for item in batch]) labels = torch.tensor([item[1] for item in batch]) return images, labels dataloader = DataLoader(dataset, batch_size=32, collate_fn=collate_fn)

一个更实际的自定义collate_fn:处理不同长度的文本时,会先确认这批样本里最长的句子,然后把其他句子pad到同样长度。所以collate_fn不仅是"拼数据",还可以兼职"动态padding"。

5.5 数据加载的异常排查思路

如果你的训练速度很慢,先看两个地方。第一是num_workers是否为0,如果是0,代表所有数据加载都在主进程里串行执行,CPU忙不过来的时候GPU只能干等。第二是每轮__getitem__里是不是做了过重的重复计算(比如反复读磁盘、反复解码图片),如果图片特别大,建议先把数据预处理一次缓存到内存或磁盘上,__getitem__里只做轻量操作。

6. 训练循环:损失、优化器和梯度更新的闭环

6.1 一次完整的训练循环长什么样

很多人看PyTorch代码,最想知道的就是"为什么训练要按这个顺序写"。我们先看一个完整的最小可训练代码,再逐步拆解:

import torch import torch.nn as nn from torch.utils.data import DataLoader # 模型、数据、优化器 model = MLP() device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') model.to(device) dataloader = DataLoader(train_dataset, batch_size=64, shuffle=True) optimizer = torch.optim.Adam(model.parameters(), lr=1e-3) criterion = nn.CrossEntropyLoss() # 训练循环 for epoch in range(5): model.train() total_loss = 0 for batch_x, batch_y in dataloader: batch_x = batch_x.to(device) batch_y = batch_y.to(device) optimizer.zero_grad() # 第1步:清空梯度 output = model(batch_x) # 第2步:前向传播 loss = criterion(output, batch_y) # 第3步:计算损失 loss.backward() # 第4步:反向传播 optimizer.step() # 第5步:更新参数 total_loss += loss.item() print(f'Epoch {epoch}, loss: {total_loss / len(dataloader):.4f}')

这个循环的顺序不能乱。核心逻辑是:先清空上一轮的梯度,再算预测和损失,然后反向传播计算当前梯度,最后用优化器根据梯度更新参数。每一步都依赖上一步的结果,所以顺序错一步整个训练就废了。

6.2 loss.item() 与 loss.backward() 的关系

loss是一个张量,里面记录着计算历史。loss.item()把它转换成Python数值,用于打印日志、计算指标;loss.backward()则是沿着计算图回传梯度。

这里有个实用经验:打印loss的时候一定要用.item()而不是直接print(loss),否则你会看到一大堆带grad_fn的张量描述信息,既占屏幕又影响日志可读性。另外,如果你对一个已经是item()取出来的浮点数做后续运算,它就不会再累积计算图了,日志和指标计算更干净。

6.3 优化器怎么选:SGD、Adam、AdamW

模型训练选优化器,有点像挑交通工具——没有绝对最好的,只有合不合适。

  • SGD:最基础。需要自己调学习率、动量和weight decay,练久了调参功底会很扎实。收敛慢,但泛化效果往往不错。
  • Adam:最省心。自适应学习率,训练初期收敛快,小数据集和调参时间有限的时候强烈推荐。
  • AdamW:Adam的改进版,weight decay实现方式更正确。现在训练Transformer类模型几乎默认用AdamW。

我个人的习惯是:第一次跑通或者做小规模实验,直接用Adam或AdamW,把关注点放在数据处理和模型结构上;做大实验追求更好效果时再切回SGD配合精细的学习率/动量调度。不是Adam就一定比SGD好,这个后面单独开一篇说。

6.4 损失函数:CrossEntropyLoss的输入是什么

新手最容易懵的损失函数是交叉熵。用nn.CrossEntropyLoss时,模型的原始输出不需要手动经过softmax,因为:

  • CrossEntropyLoss内部整合了LogSoftmax + NLLLoss;
  • 输入是模型的logits(未经过激活的分数),形状是[batch_size, num_classes];
  • 标签是类别索引,形状是[batch_size],每个值是0到num_classes - 1的整数。
criterion = nn.CrossEntropyLoss() output = model(batch_x) # [64, 10] loss = criterion(output, batch_y) # batch_y: [64],整数标签

经常有同学在模型最后接了一层softmax,然后传给CrossEntropyLoss,这会导致损失计算时把softmax又做了一遍,数值和梯度都是错的。除非你用NLLLoss,否则不要在输出层主动softmax之后再接CrossEntropyLoss。

6.5 验证与推理:torch.no_grad()的威力

验证集不需要计算梯度,因为不更新参数。但如果你不做任何处理,PyTorch依然会把验证过程的前向传播记录进计算图,白白消耗大量内存和时间。用torch.no_grad()包起来就能完全跳过梯度记录:

model.eval() with torch.no_grad(): val_output = model(val_x) val_loss = criterion(val_output, val_y)

这句代码非常实用,它让验证和推理速度翻倍,而且显存占用大幅下降。我见过不写no_grad的人训练小模型没感觉,一旦换成大模型或大批次验证,显存立刻溢出。

6.6 学习率与batch_size的联动关系

batch_size和学习率不是独立变量。直观理解:batch越大,一次参数更新的梯度信息越"平均",方差越小,方向越可信,所以可以承受更大的学习率。如果batch变大但学习率不变,收敛速度会肉眼可见变慢。

我实验时一般遵守一个粗略经验:batch翻一倍,学习率也翻一倍。比如batch为32用学习率1e-3,batch为64就调到2e-3。当然这只是起步参考值,最终还是要跑实验确认。这个细节初学时不注意,等到训练又慢又不收敛才想起来调,往往已经浪费了很多时间。

7. 我在实战里踩过的PyTorch基础坑

7.1 dtype不匹配:tensor(1.0)和tensor(1)之间的隐性问题

你创建了一个浮点张量,不小心和整数张量做运算,PyTorch在很多情况下会直接报错或者自动转换,但有时自动转换后的行为跟你想的不一样。比如:

x = torch.tensor([1, 2, 3]) # int64 y = torch.tensor([1.5, 2.5, 3.5]) # float32 # z = x + y # 可能直接报错,因为dtype不同

深度学习模型里,几乎所有的输入数据、模型参数、标签都要是浮点型,唯独标签做损失计算时可以是整数。我早期就被一个"整数张量参与矩阵乘法"的问题折磨过半天。现在写代码会在关键位置强制指定dtype:

x = torch.tensor(data, dtype=torch.float32) y = torch.tensor(labels, dtype=torch.long)

7.2 既然nonzero梯度消失了怎么办

模型参数设了requires_grad=True,但训练后参数一直不更新,查看梯度全是None。这类问题按两步排查:

  1. 确认这个参数是否被某条路径用到了前向计算里。如果网络里定义了某个层但forward里没调用它,那它就是"孤儿参数",没有参与计算图,自然没有梯度。
  2. 如果参与了计算但梯度为None,检查有没有对这个参数做过.detach(),或者在它参与的路径上有过torch.no_grad()包裹。

用命名参数打印可以快速定位问题:

for name, param in model.named_parameters(): grad_norm = param.grad.norm() if param.grad is not None else None print(name, grad_norm)

7.3 爱因斯坦求和约定式的广播混乱

两个形状为[2, 3]和[3, 2]的张量做矩阵乘法,要用torch.mm或@。两个形状为[2, 3]和[3]的张量做逐元素运算,要明确广播方向。深度学习中矩阵乘法和逐元素乘法的使用非常频繁,早期我经常把*(逐元素乘)和@(矩阵乘)搞混,导致网络输出完全错误。

一个明确的规则:

  • x * y:逐元素相乘,要求广播后形状一致;
  • x @ y:标准的矩阵乘法;
  • torch.matmul(x, y):行为更灵活,可根据维度自动选择逐元素乘法或矩阵乘法,但为可读性建议显式区分。

7.4 in-place操作:为什么y = y + 1和y += 1不一样

PyTorch的自动求导引擎要求在计算图中,每个张量的"前世今生"都是可追踪的。如果用原地操作(比如tensor.add_(1)或x[0] = 1)修改了一个参与过运算的叶子张量,梯度计算的链条就被弄断了,报错一般是"a leaf Variable that requires grad has been used in an in-place operation"。

我的建议就一条话:凡是需要梯度回传的张量,一律不要做原地修改。新结果就用y = y + 1写在新的变量里,不要图省事写y += 1。

7.5 模型没有收敛,先别急着调参

训练跑了几个epoch发现loss不降或乱跳,很多人第一反应是调整网络结构、换优化器。我的排错顺序是:

  1. 先检查数据:标签有没有错位?数据有没有归一化?有没有对全0或全1这种无意义数据?
  2. 检查损失:第一次batch的loss是多少?如果跟随机初始化模型应有的损失差得太多,大概率是代码逻辑有问题。
  3. 用小数据过拟合测试:拿几个样本训练,如果loss都降不下去,模型代码多半有bug;如果loss能降到接近0,说明模型结构没问题,问题在数据或训练配置上。

这个小技巧帮我排查了无数个"奇怪"的bug。调参之前先确保代码没错,否则后面全是在错误代码上做无用功。

7.6 随机种子:复现实验的隐形门槛

深度学习的初始化、数据打乱都有随机性。如果完全不设置随机种子,同一个代码跑两次结果很可能不同。做实验要有可复现性,所以才需要固定随机种子:

import random import numpy as np import torch def set_seed(seed=42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)

需要注意,即使设置了种子,由于某些GPU算子并行计算的不可确定性,结果仍然可能有一点点浮动。所以论文级别的复现还需要设置torch.backends.cudnn.deterministic = True,代价是速度轻微下降。实验记录时把种子、框架版本、GPU型号一起记下来,是维护工程素养的好习惯。

8. 把基础打牢之后,你会遇见什么

说句实在话,PyTorch基础这部分内容本身并不难,难度在于它牵涉的模块和概念太多了——张量、自动求导、模型组网、数据管线、优化器和损失函数,每一块单独拎出来都不算复杂,合在一起就是一个五脏俱全的深度学习训练系统。

我的建议是:拿到一篇自己感兴趣的模型代码,不要光看,把代码逐行敲一遍,跑通一次,然后试着改动几处(换激活函数、改层数、调batch大小),观察loss和精度的变化。这个过程比看十遍文档都管用。

接下来你可以沿着几条线继续往下走:如果做图像,去学卷积网络的结构设计、图像增强、迁移学习;如果做文本,去学嵌入层、循环网络或Transformer;如果做通用系统,去学数据并行、分布式训练和模型部署。

一个可以马上动手的小任务

如果你看完这篇笔记想检验一下自己是否真的掌握了,我推荐一个经典小任务:用PyTorch在MNIST数据集上训练一个两层的全连接网络,把准确率推到95%以上。这个任务会逼着你走完全套流程:加载数据、定义Dataset、构建模型、设置优化器和损失函数、写训练循环、做验证评估。等你把这个流程跑通一遍,后续再接触卷积、循环网络、注意力机制,就只剩下"往框架里加新积木"的问题了。

最后分享一个我在训练过程中形成的习惯:训练脚本里始终保留一个保存和加载模型权重的代码片段,每个epoch结束都存一次。这样即使某个实验跑了很久然后崩了,也能从最近的检查点恢复,不至于前功尽弃。细节上的稳定,才是长期做实验的底气。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询