
1. 从“Hello, Tensor”开始为什么是PyTorch如果你刚开始接触深度学习或者从TensorFlow、Keras等其他框架转过来面对PyTorch的第一感觉可能是它看起来“更Python”。这不是错觉。PyTorch的核心设计哲学之一就是“命令式编程”Imperative Programming或者更直白地说它像写普通Python代码一样写深度学习模型。你执行一个加法操作张量Tensor的值立刻改变你打印一个变量看到的就是它当前的状态。这种直观的、动态的交互体验是PyTorch在研究和快速原型开发领域迅速崛起的关键。对比来看一些早期框架采用“声明式编程”Declarative Programming你需要先定义一个静态的计算图然后再把数据喂进去执行。这种方式在部署时效率很高但在调试和实验阶段就像隔着一层毛玻璃操作不够直接。PyTorch的动态图机制在PyTorch中称为“即时执行”Eager Execution让你可以随时用Python的print、pdb调试器来查看中间变量的值这对于理解模型内部发生了什么、定位Bug至关重要。尤其是在处理变长序列如自然语言处理、或模型结构本身需要根据数据动态变化的场景时动态图的灵活性是无可替代的。所以当你写下import torch时你引入的不仅仅是一个张量计算库更是一套以Python原生思维为核心的深度学习工作流。我们从最基础的单元——张量开始这不仅是数据的容器更是理解PyTorch所有操作的基石。2. 张量Tensor一切皆数据的容器张量是PyTorch中最基本的数据结构你可以把它理解为多维数组。这个“多维”的概念需要明确一下0维张量就是一个标量Scalar比如一个数字torch.tensor(5.0)。1维张量就是一个向量Vector比如torch.tensor([1.0, 2.0, 3.0])。2维张量就是一个矩阵Matrix比如torch.tensor([[1, 2], [3, 4]])。3维及以上张量可以想象成由多个矩阵堆叠起来的立方体甚至更高维度的数据块这在图像批量、通道、高、宽、序列数据批量、序列长度、特征维度中非常常见。创建张量有多种方式每种方式背后都有其适用的场景import torch # 1. 从Python列表或NumPy数组直接创建最常用 data [[1, 2], [3, 4]] x_numpy np.array(data) x_torch_from_list torch.tensor(data) # 注意torch.tensor 总是会拷贝数据 x_torch_from_numpy torch.from_numpy(x_numpy) # 与NumPy数组共享内存修改一个会影响另一个 # 2. 创建特定形状的“占位符”张量初始化时未指定具体值 zeros torch.zeros(2, 3) # 2行3列的全0矩阵 ones torch.ones(2, 3, dtypetorch.float32) # 全1矩阵并指定数据类型为32位浮点 empty torch.empty(3, 4) # 分配内存但不初始化值是不确定的可能是任意值速度最快 rand torch.rand(2, 3) # 元素服从[0, 1)均匀分布 randn torch.randn(2, 3) # 元素服从标准正态分布均值为0方差为1 # 3. 类似已有张量的属性创建 x torch.tensor([[1, 2, 3], [4, 5, 6]]) x_like_zeros torch.zeros_like(x) # 创建一个和x形状、数据类型完全一样的全0张量 x_like_rand torch.rand_like(x, dtypetorch.float) # 创建一个和x形状一样但指定了新数据类型且值随机的张量这里有几个关键细节和容易踩的坑torch.Tensor()vstorch.tensor()torch.Tensor()是类构造函数它接收的是形状shape如torch.Tensor(2, 3)会创建一个未初始化的2x3张量。而torch.tensor()是一个工厂函数它接收的是具体的数据如torch.tensor([1, 2, 3])。在绝大多数情况下推荐使用torch.tensor()因为它的意图更明确能自动推断数据类型。数据类型dtype至关重要张量的运算对数据类型非常敏感。整数类型如torch.int32和浮点类型如torch.float32之间不能直接进行混合运算。模型参数、梯度通常需要torch.float32。在创建张量时可以通过dtype参数指定也可以通过.to()方法或.float()、.int()等方法进行转换。设备device是另一个维度张量可以存在于CPU内存或GPU显存中。torch.tensor([1,2,3], devicecuda)会直接在GPU上创建张量。你也可以通过.to(cuda)或.cuda()将CPU张量移动到GPU。一个常见的错误是试图将位于不同设备上的张量进行运算这会直接导致运行时错误。在写代码时心里要时刻清楚你的张量在哪。张量的属性.shape,.dtype,.device是调试时最先要查看的信息。一个简单的习惯是在构建复杂运算前先print一下关键张量的这些属性能避免很多低级错误。2.1 张量的核心操作索引、切片与变形对张量的操作是模型构建的砖瓦。PyTorch的API设计很大程度上借鉴了NumPy所以如果你熟悉NumPy上手会非常快。索引和切片和Python列表、NumPy数组的规则几乎完全一致。x torch.arange(12).reshape(3, 4) # 创建一个3x4的张量元素从0到11 print(x) # tensor([[ 0, 1, 2, 3], # [ 4, 5, 6, 7], # [ 8, 9, 10, 11]]) print(x[0]) # 第一行: tensor([0, 1, 2, 3]) print(x[:, 1]) # 第二列: tensor([1, 5, 9]) print(x[1:3, 2:]) # 第2到3行第3列到最后一列: tensor([[6, 7], [10, 11]]) print(x[x 5]) # 布尔索引选出所有大于5的元素: tensor([ 6, 7, 8, 9, 10, 11])变形Reshape与维度操作这是处理数据以适应网络层输入输出形状的关键。x torch.arange(12) print(x.shape) # torch.Size([12]) # reshape/view: 改变张量的视图形状但不改变底层数据 y x.reshape(3, 4) # 或 x.view(3, 4) print(y.shape) # torch.Size([3, 4]) # 注意使用view()时要求张量在内存中是连续的(contiguous)否则会报错。reshape()更安全它会自动处理连续性问题。 # 压缩/增加维度 z torch.randn(3, 1, 28, 28) # 一个典型的图像批量批量大小3通道数1高28宽28 print(z.shape) # torch.Size([3, 1, 28, 28]) squeezed z.squeeze(1) # 移除维度1如果该维度大小为1 print(squeezed.shape) # torch.Size([3, 28, 28])去掉了通道维度 unsqueezed squeezed.unsqueeze(1) # 在维度1上增加一个维度 print(unsqueezed.shape) # torch.Size([3, 1, 28, 28])恢复了原状 # 转置与维度交换 a torch.randn(2, 3, 4) b a.permute(2, 0, 1) # 将原来的维度[0,1,2]变为[2,0,1] print(b.shape) # torch.Size([4, 2, 3])注意reshape和view都返回一个新的视图共享底层数据而transpose和permute也是返回视图。这意味着如果你修改了返回的张量原始张量也可能被修改。如果你需要一份真正的、独立内存的拷贝记得使用.clone()。2.2 张量的广播机制让不同形状的张量一起运算广播是PyTorch/NumPy中一个非常强大但也容易出错的特性。它的核心规则是从尾部维度开始逐维度向前比较。如果两个维度相等或其中一个为1或其中一个不存在维度缺失则这两个维度是“兼容”的。在兼容的维度上大小为1的维度会被“拉伸”以匹配另一个张量对应维度的大小。举个例子# 案例1向量加标量 a torch.tensor([1, 2, 3]) # shape (3,) b torch.tensor(10) # shape () 可以看作 (1,) c a b # b被广播为 [10, 10, 10]然后与a相加 print(c) # tensor([11, 12, 13]) # 案例2矩阵加行向量 A torch.ones(2, 3) # shape (2, 3) row torch.tensor([1, 2, 3]) # shape (3,) C A row # row被广播为 [[1,2,3], [1,2,3]]然后相加 print(C) # tensor([[2., 3., 4.], # [2., 3., 4.]]) # 案例3矩阵加列向量需要先变形 A torch.ones(2, 3) # (2,3) col torch.tensor([[1], [2]]) # (2,1) D A col # col被广播为 [[1,1,1], [2,2,2]] print(D) # tensor([[2., 2., 2.], # [3., 3., 3.]])广播机制避免了显式复制数据带来的内存和性能开销让代码更简洁。但如果你不小心也可能导致意想不到的结果。一个调试技巧是在遇到形状不匹配的错误时手动模拟一下广播过程检查每个维度是否兼容。3. 自动微分AutogradPyTorch的“灵魂”动态图带来了灵活性而自动微分Autograd系统则赋予了PyTorch“学习”的能力。它是神经网络反向传播算法得以自动实现的基石。理解Autograd是理解PyTorch训练循环的关键。3.1 计算图与梯度追踪在PyTorch中每个张量都有一个属性requires_grad默认为False。当你将其设置为True时PyTorch就会开始追踪track所有施加于该张量之上的操作并构建一个有向无环图DAG也就是计算图。这个图记录了从输入叶子节点通常是你的模型参数或输入数据到输出损失值的所有计算步骤。import torch # 创建需要计算梯度的张量叶子节点 x torch.tensor([1.0, 2.0], requires_gradTrue) # 假设这是模型参数 w torch.tensor([0.5, -0.5], requires_gradTrue) # 另一个参数 b torch.tensor(0.1, requires_gradTrue) # 偏置 # 前向传播计算 y_pred x * w b # 一个简单的线性运算 loss (y_pred - torch.tensor([2.0, 1.0])).pow(2).sum() # 计算均方误差损失 print(loss) # tensor(5.4100, grad_fnSumBackward0)注意看loss张量它有一个grad_fn属性指向了创建它的反向传播函数SumBackward0。这意味着loss不是叶子节点它是通过计算得到的。而x、w、b是叶子节点它们的grad_fn是None。3.2 反向传播与梯度计算当我们调用loss.backward()时PyTorch会从loss这个节点开始沿着计算图反向传播利用链式法则计算图中所有requires_gradTrue的叶子节点即我们的参数x、w、b关于loss的梯度。计算得到的梯度会累积在这些叶子节点的.grad属性中。# 执行反向传播 loss.backward() # 查看梯度 print(x.grad) # tensor([ 1.8000, -3.6000]) print(w.grad) # tensor([ 2., 4.]) print(b.grad) # tensor(6.)现在x.grad就存储了∂loss/∂x的值。我们可以利用这个梯度来更新参数例如使用梯度下降x x - learning_rate * x.grad。3.3 梯度累积与清零这里有一个至关重要的细节.grad属性是累积的。这意味着每次调用.backward()计算出的梯度会加到累加现有的.grad值上而不是覆盖它。这在某些高级场景如梯度累积模拟更大批量训练中有用但在标准的每个批次更新一次参数的训练循环中这会导致严重错误——你会在上一个批次的梯度上继续累加使得梯度爆炸或更新方向错误。因此在每次参数更新前必须手动将梯度清零# 错误的做法连续两次backward而不清零 loss.backward() print(w.grad) # 假设是 tensor([2., 4.]) loss.backward() # 再次计算同样的loss仅作演示 print(w.grad) # 变成了 tensor([4., 8.])梯度翻倍了 # 正确的训练循环步骤 optimizer torch.optim.SGD([x, w, b], lr0.01) # 优化器后续会讲 for epoch in range(num_epochs): # ... 前向传播计算 loss ... optimizer.zero_grad() # 关键一步将所有被优化参数的梯度清零 loss.backward() # 反向传播计算当前批次的梯度 optimizer.step() # 根据梯度更新参数optimizer.zero_grad()是标准训练循环中必不可少的一环。忘记它是新手常犯的错误之一。3.4 分离计算图与detach()有时我们只需要张量的数值而不希望它参与梯度计算图。例如在评估模型性能时我们计算准确率这个计算不应该影响模型参数的梯度。这时就需要detach()方法。detach()会返回一个与原始张量共享数据但requires_gradFalse的新张量并且它被从当前的计算图中“分离”出来。对它的任何操作都不会被追踪梯度。x torch.tensor([1.0], requires_gradTrue) y x * 2 z y.detach() # z是从计算图中分离出来的张量 print(z.requires_grad) # False # 对z的操作不会影响x的梯度 w z * 3 w.backward() # 这里会报错因为w不是从requires_gradTrue的张量计算得来的 # 正确的用法在不需要梯度的计算分支使用detach另一个常见场景是在生成对抗网络GAN中训练生成器时需要阻止梯度通过判别器传播到生成器这时也会用到detach()。4. 神经网络构建模块torch.nn初探torch.nn模块提供了构建神经网络所需的所有“乐高积木”。它包含定义网络层如线性层、卷积层、循环层的类、各种激活函数、损失函数以及容器如Sequential、ModuleList来组织这些层。4.1 定义你的第一个网络继承nn.Module在PyTorch中自定义神经网络的标准方式是创建一个类并继承torch.nn.Module。你需要做两件事在__init__方法中定义网络的所有层作为类的属性。在forward方法中定义数据如何通过这些层流动即前向传播。import torch.nn as nn import torch.nn.functional as F class SimpleNet(nn.Module): def __init__(self, input_size, hidden_size, num_classes): super(SimpleNet, self).__init__() # 必须调用父类的初始化 # 定义网络层 self.fc1 nn.Linear(input_size, hidden_size) # 全连接层1 self.fc2 nn.Linear(hidden_size, hidden_size) # 全连接层2 self.fc3 nn.Linear(hidden_size, num_classes) # 输出层 # 注意这里没有定义激活函数。我们可以在forward里使用F.relu或者定义nn.ReLU()层。 def forward(self, x): # 定义前向传播路径 out self.fc1(x) out F.relu(out) # 使用函数式API应用ReLU激活 out self.fc2(out) out F.relu(out) out self.fc3(out) # 注意对于分类问题通常最后一层不接激活函数如softmax因为损失函数如CrossEntropyLoss内部会处理。 return out # 实例化网络 model SimpleNet(input_size784, hidden_size128, num_classes10) print(model)nn.Linear是线性层全连接层它的数学形式是y xA^T b。在__init__中定义层时PyTorch会自动将这些层注册为模型的子模块。这意味着model.parameters()能够递归地收集到所有这些层的可学习参数权重和偏置这对于优化器来说是必需的。forward方法定义了数据流的管道。你永远不需要直接调用model.forward(x)。正确的调用方式是output model(x)。PyTorch的nn.Module基类实现了__call__方法它会先调用一些内部的钩子hooks然后再调用你的forward方法。4.2 损失函数与优化器定义了模型结构我们还需要两样东西来训练它衡量预测好坏的损失函数和根据损失梯度更新参数的优化器。损失函数在torch.nn模块中。常见的有nn.MSELoss()均方误差损失用于回归任务。nn.CrossEntropyLoss()交叉熵损失用于多分类任务它内部集成了Softmax所以网络最后一层不需要加Softmax。nn.BCELoss()二分类交叉熵损失用于二分类任务需要搭配Sigmoid激活函数。nn.BCEWithLogitsLoss()结合了Sigmoid和BCELoss数值上更稳定。优化器在torch.optim模块中。它接收需要优化的参数通常是model.parameters()和学习率等超参数。optim.SGD随机梯度下降最基础但依然强大常搭配动量momentum。optim.Adam自适应矩估计目前最流行的优化器对超参数不那么敏感通常作为默认选择。optim.AdamWAdam的改进版修正了权重衰减weight decay的实现在许多任务上表现更好。import torch.optim as optim # 假设我们有模型和一批数据 model SimpleNet(784, 128, 10) criterion nn.CrossEntropyLoss() # 定义损失函数 optimizer optim.Adam(model.parameters(), lr0.001) # 定义优化器 # 模拟一个训练批次 inputs torch.randn(32, 784) # 批量大小32特征维度784 labels torch.randint(0, 10, (32,)) # 32个0-9的随机标签 # 训练循环的一个迭代 optimizer.zero_grad() # 清零梯度 outputs model(inputs) # 前向传播 loss criterion(outputs, labels) # 计算损失 loss.backward() # 反向传播计算梯度 optimizer.step() # 根据梯度更新参数 print(fLoss: {loss.item():.4f})这个zero_grad - forward - loss - backward - step的五步循环就是PyTorch训练的核心骨架。理解并熟练运用它你就掌握了用PyTorch进行模型训练的基本功。4.3 模型的保存与加载训练好的模型需要保存下来以便后续评估、部署或继续训练。PyTorch提供了两种主要方式保存整个模型使用torch.save(model, model.pth)。这种方法会保存模型的结构和所有参数。加载时使用model torch.load(model.pth)。缺点保存的模型与定义模型的源代码类强绑定。如果源代码类发生改变如类名、结构加载可能会失败。仅保存模型状态字典推荐方式。状态字典state_dict是一个Python字典它将每一层映射到其参数张量。# 保存 torch.save(model.state_dict(), model_weights.pth) # 加载 model SimpleNet(784, 128, 10) # 必须先实例化一个结构相同的模型 model.load_state_dict(torch.load(model_weights.pth)) model.eval() # 将模型设置为评估模式影响Dropout、BatchNorm等层的行为这种方式更灵活因为它只关心参数值不关心模型是如何定义的。只要你能重新构建出结构相同的模型就能加载参数。在实际项目中我通常会将模型结构定义在一个单独的.py文件中然后保存和加载状态字典。同时我还会把优化器的状态字典 (optimizer.state_dict()) 以及当前的epoch、损失记录等一起保存以便完美地恢复训练状态。这可以通过保存一个字典来实现checkpoint { epoch: epoch, model_state_dict: model.state_dict(), optimizer_state_dict: optimizer.state_dict(), loss: loss, # ... 其他你想保存的信息 } torch.save(checkpoint, checkpoint.pth)5. 数据处理的基石Dataset与DataLoader真实世界的数据很少是整齐划一的。PyTorch用torch.utils.data.Dataset和DataLoader这两个抽象优雅地解决了数据加载和批处理的问题。5.1 自定义DatasetDataset是一个抽象类代表一个数据集。你需要继承它并实现两个核心方法__len__: 返回数据集的大小。__getitem__: 给定一个索引idx返回对应的数据样本和标签。假设我们有一个图像分类任务图像文件路径和标签存储在一个CSV文件中。from torch.utils.data import Dataset, DataLoader from PIL import Image import pandas as pd import os class CustomImageDataset(Dataset): def __init__(self, annotations_file, img_dir, transformNone): Args: annotations_file (string): CSV文件路径包含 img_name 和 label 列。 img_dir (string): 图像文件所在的目录。 transform (callable, optional): 一个可选的图像变换函数/组合。 self.img_labels pd.read_csv(annotations_file) self.img_dir img_dir self.transform transform def __len__(self): return len(self.img_labels) def __getitem__(self, idx): img_path os.path.join(self.img_dir, self.img_labels.iloc[idx, 0]) image Image.open(img_path).convert(RGB) # 打开图像并转为RGB label self.img_labels.iloc[idx, 1] if self.transform: image self.transform(image) # 应用变换如缩放、裁剪、归一化等 return image, labeltransform参数非常重要。它通常是一个由torchvision.transforms中的变换组合而成的管道负责将原始的PIL图像或NumPy数组转换为PyTorch张量并进行标准化等预处理。5.2 使用DataLoader进行批处理与加速Dataset每次只返回一个样本。DataLoader则围绕Dataset提供了一个迭代器它可以批处理将多个样本组合成一个批次张量。打乱数据在每个epoch开始时随机打乱数据顺序防止模型学习到数据的顺序信息。多进程加载使用多个子进程并行加载数据极大地加速数据准备过程避免训练时GPU等待CPU处理数据I/O瓶颈。from torchvision import transforms # 定义图像变换 transform transforms.Compose([ transforms.Resize((224, 224)), # 调整大小 transforms.RandomHorizontalFlip(), # 数据增强随机水平翻转 transforms.ToTensor(), # 转换为张量并归一化到[0,1] transforms.Normalize(mean[0.485, 0.456, 0.406], std[0.229, 0.224, 0.225]) # 用ImageNet的均值和标准差归一化 ]) # 创建Dataset实例 dataset CustomImageDataset(annotations_filelabels.csv, img_dir./images, transformtransform) # 创建DataLoader dataloader DataLoader(dataset, batch_size32, # 批次大小 shuffleTrue, # 是否在每个epoch打乱数据 num_workers4, # 用于数据加载的子进程数 pin_memoryTrue) # 如果使用GPU将数据锁页内存可以加速到GPU的数据传输 # 在训练循环中使用 for epoch in range(num_epochs): for batch_idx, (images, labels) in enumerate(dataloader): # 现在 images 是一个形状为 [32, 3, 224, 224] 的张量 # labels 是一个形状为 [32] 的张量 # ... 将 images 和 labels 移动到GPU ... # ... 前向传播、计算损失、反向传播、优化 ... pass关于num_workers和pin_memory的实践经验num_workers通常设置为CPU核心数或略少。设置得太高会增加系统开销可能反而变慢。在Windows上有时多进程加载会有问题可以尝试设置为0。pin_memoryTrue当你的数据需要从CPU内存复制到GPU显存时这个选项可以显著提升速度。它告诉DataLoader将加载的数据放在“锁页内存”中这使得GPU的DMA直接内存访问能够更快地获取数据。如果你的训练循环中出现了GPU利用率低比如一直在10%-30%徘徊很可能是数据加载成了瓶颈检查并优化DataLoader的配置增加num_workers, 设置pin_memoryTrue通常是第一步。6. 实战一个完整的图像分类训练流程让我们把上面所有的知识点串联起来构建一个在CIFAR-10数据集上进行图像分类的完整训练脚本。CIFAR-10是一个包含10个类别、6万张32x32彩色图像的小型数据集非常适合入门。6.1 准备数据与模型import torch import torch.nn as nn import torch.optim as optim import torchvision import torchvision.transforms as transforms from torch.utils.data import DataLoader # 1. 定义数据变换 transform_train transforms.Compose([ transforms.RandomCrop(32, padding4), # 随机裁剪数据增强 transforms.RandomHorizontalFlip(), # 随机水平翻转数据增强 transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), # CIFAR-10的均值和标准差 ]) transform_test transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2023, 0.1994, 0.2010)), ]) # 2. 加载数据集 trainset torchvision.datasets.CIFAR10(root./data, trainTrue, downloadTrue, transformtransform_train) trainloader DataLoader(trainset, batch_size128, shuffleTrue, num_workers4, pin_memoryTrue) testset torchvision.datasets.CIFAR10(root./data, trainFalse, downloadTrue, transformtransform_test) testloader DataLoader(testset, batch_size100, shuffleFalse, num_workers4, pin_memoryTrue) classes (plane, car, bird, cat, deer, dog, frog, horse, ship, truck) # 3. 定义一个简单的卷积神经网络CNN class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 32, 3, padding1) # 输入通道3输出通道323x3卷积核 self.pool nn.MaxPool2d(2, 2) # 2x2最大池化 self.conv2 nn.Conv2d(32, 64, 3, padding1) self.fc1 nn.Linear(64 * 8 * 8, 256) # 计算尺寸经过两次池化32x32 - 16x16 - 8x8 self.fc2 nn.Linear(256, 10) self.dropout nn.Dropout(0.5) # Dropout层防止过拟合 def forward(self, x): x self.pool(torch.relu(self.conv1(x))) x self.pool(torch.relu(self.conv2(x))) x torch.flatten(x, 1) # 展平除了批次维度 x self.dropout(torch.relu(self.fc1(x))) x self.fc2(x) return x model SimpleCNN() device torch.device(cuda if torch.cuda.is_available() else cpu) model.to(device) # 将模型移动到GPU如果可用 # 4. 定义损失函数和优化器 criterion nn.CrossEntropyLoss() optimizer optim.Adam(model.parameters(), lr0.001, weight_decay1e-4) # 加入L2正则化权重衰减 scheduler optim.lr_scheduler.StepLR(optimizer, step_size30, gamma0.1) # 学习率调度器每30个epoch乘以0.16.2 训练循环与评估def train(epoch): model.train() # 将模型设置为训练模式启用Dropout等 running_loss 0.0 correct 0 total 0 for batch_idx, (inputs, targets) in enumerate(trainloader): inputs, targets inputs.to(device), targets.to(device) optimizer.zero_grad() outputs model(inputs) loss criterion(outputs, targets) loss.backward() optimizer.step() running_loss loss.item() _, predicted outputs.max(1) total targets.size(0) correct predicted.eq(targets).sum().item() if batch_idx % 100 99: # 每100个batch打印一次 print(fEpoch: {epoch}, Batch: {batch_idx1}, Loss: {running_loss/100:.3f}, Acc: {100.*correct/total:.2f}%) running_loss 0.0 def test(): model.eval() # 将模型设置为评估模式禁用Dropout固定BatchNorm的统计量 test_loss 0 correct 0 total 0 with torch.no_grad(): # 禁用梯度计算节省内存和计算资源 for inputs, targets in testloader: inputs, targets inputs.to(device), targets.to(device) outputs model(inputs) loss criterion(outputs, targets) test_loss loss.item() _, predicted outputs.max(1) total targets.size(0) correct predicted.eq(targets).sum().item() print(fTest Loss: {test_loss/len(testloader):.3f}, Test Acc: {100.*correct/total:.2f}%) return 100.*correct/total # 5. 开始训练 num_epochs 50 best_acc 0.0 for epoch in range(1, num_epochs1): train(epoch) acc test() scheduler.step() # 更新学习率 # 保存最佳模型 if acc best_acc: print(fSaving best model with accuracy {acc:.2f}%) best_acc acc torch.save(model.state_dict(), cifar10_best.pth)这个脚本包含了深度学习训练的所有核心要素数据加载与增强、模型定义、训练循环、验证评估、模型保存以及学习率调度。你可以通过调整网络结构如增加层数、使用更复杂的模块如ResNet块、调整超参数批量大小、学习率、优化器、增加更丰富的数据增强等手段来提升模型性能。7. 调试技巧与常见陷阱即使理解了所有语法在实际编码中依然会遇到各种问题。这里分享几个我踩过坑后总结的调试经验。1. 形状不匹配Shape Error这是最常见的错误。养成随时打印张量形状的习惯。在模型forward函数的关键位置、在数据送入模型之前都print(x.shape)一下。特别是全连接层nn.Linear的输入特征数必须和前一层的输出特征数匹配。卷积神经网络中经过一系列卷积池化后展平flatten前的特征图尺寸需要仔细计算。2. 设备不匹配Device Error确保你的模型、输入数据、标签都在同一个设备上CPU或GPU。一个简单的模式是在代码开头定义device然后对所有张量和模型都调用.to(device)。device torch.device(cuda:0 if torch.cuda.is_available() else cpu) model MyModel().to(device) for data, target in dataloader: data, target data.to(device), target.to(device) # ... 后续计算3. 忘记zero_grad()如前所述这会导致梯度累积训练完全失败。将其作为backward()之前必须的步骤。4. 混淆model.train()和model.eval()model.train()会启用Dropout和BatchNorm的训练模式使用当前批次的统计量进行归一化并应用Dropout。model.eval()则会禁用它们使用训练阶段累积的移动平均统计量进行归一化不应用Dropout。在训练和评估/测试时务必切换正确的模式否则性能会大幅下降。5. 在with torch.no_grad():块外进行验证/测试在评估模型时我们不需要计算梯度。使用with torch.no_grad():上下文管理器可以显著减少内存消耗并加速计算。忘记使用它虽然不会导致错误但会浪费大量资源。6. 数据加载的瓶颈如果GPU利用率很低观察一下CPU使用率。如果CPU某个核心跑满而GPU在等待那很可能是DataLoader的num_workers设置得太小或者数据预处理transform太复杂。尝试增加num_workers使用pin_memoryTrue并检查transform中是否有耗时的操作如复杂的图像增强。对于极耗时的预处理可以考虑离线预处理数据。7. 损失不下降或为NaN学习率太大这是最常见的原因。尝试将学习率降低一个数量级例如从0.01降到0.001。数据没有归一化输入数据的值范围差异巨大如图像像素值0-255会导致梯度爆炸或消失。务必使用transforms.Normalize。网络结构或初始化问题过深或过浅的网络不合适的权重初始化都可能导致训练困难。可以尝试使用nn.init中的方法如nn.init.kaiming_normal_来初始化权重。损失函数用错例如在多分类任务中如果网络输出层用了Sigmoid而损失函数用了CrossEntropyLoss就会出问题。记住CrossEntropyLossLogSoftmaxNLLLoss所以网络最后一层不需要任何激活函数。调试深度学习代码是一个系统工程。从数据管道开始检查确保数据加载正确然后检查前向传播确保形状流正确接着检查损失计算最后检查反向传播和优化步骤。使用torchsummary库可以方便地查看模型各层的输入输出形状是一个很好的辅助工具。