拓冰建站拓冰建站
首页 / 资讯中心 / 正文

PyTorch深度学习入门:从环境搭建到完整训练循环的实战指南

1. 从“小土堆”到PyTorch一份写给新手的超详细入门指南如果你刚接触深度学习看到PyTorch这个名字可能会觉得有点高大上再看到网上各种复杂的教程和项目更是容易打退堂鼓。我刚开始学的时候也是这种感觉总觉得门槛很高无从下手。后来我找到了一套被大家亲切称为“小土堆”的PyTorch入门教程它最大的特点就是从零开始把每一个基础概念都掰开揉碎了讲就像把一座看似高不可攀的山分解成了一个个可以轻松爬上去的小土堆。这份笔记就是我在跟着“小土堆”学习时结合自己踩过的坑和后来的实践经验整理的一份超细致的基础学习笔记。它不是官方文档的复述而是一个过来人告诉你哪些地方容易卡住哪些概念需要反复琢磨以及如何把学到的零散知识真正串联起来去解决一个实际问题。无论你是计算机专业的学生还是想转行AI的开发者甚至是好奇的爱好者这份笔记都希望能帮你平稳地迈出PyTorch学习的第一步。2. 环境搭建别在第一步就“翻车”万事开头难对于编程学习来说环境配置往往是第一个“拦路虎”。很多人兴致勃勃地打开教程结果在安装环节就折腾半天热情消磨殆尽。所以我们先把环境这块地基打牢。2.1 安装方式选择Anaconda是“最佳拍档”对于PyTorch新手我强烈推荐使用Anaconda来管理你的Python环境和包。原因很简单深度学习项目依赖的库非常多比如NumPy、SciPy、Matplotlib等而且版本之间可能存在冲突。Anaconda就像一个集装箱可以为不同的项目创建独立的、互不干扰的虚拟环境。你可以在一个环境里装PyTorch 1.8在另一个环境里测试PyTorch 2.0而不会搞乱系统本身的Python。安装好Anaconda后打开命令行Windows叫Anaconda PromptMac/Linux叫终端我们开始创建专属环境。不建议直接在你的基础base环境里安装PyTorch。# 创建一个名为pytorch_env的新环境并指定Python版本为3.8兼容性好 conda create -n pytorch_env python3.8 # 激活这个环境 conda activate pytorch_env激活后你的命令行提示符前面通常会显示(pytorch_env)这表示你后续的所有操作都在这个“集装箱”里进行。2.2 PyTorch安装去官网“抄作业”接下来安装PyTorch本身。绝对不要去网上随便搜一个pip install torch命令就执行因为PyTorch的安装命令需要根据你的操作系统、是否使用GPUCUDA版本来定制。最稳妥的方法是访问 PyTorch官网 。在官网页面上你会看到一个直观的配置选择器PyTorch Build: 选择稳定版Stable。Your OS: 选择你的操作系统Windows/Mac/Linux。Package: 选择Conda如果你用Anaconda或Pip。Language: 选择Python。Compute Platform: 这是关键。如果你有NVIDIA显卡并想用GPU加速训练需要先确认你的显卡驱动和CUDA版本。可以在命令行输入nvidia-smi查看。比如显示CUDA Version: 11.7这里就选择CUDA 11.7。如果你的电脑没有NVIDIA显卡或者暂时不想折腾GPU务必选择CPU。用CPU版本完全不影响你学习基础语法和运行小规模示例先跑起来更重要。选择完毕后网站会生成一行对应的安装命令。例如对于Windows系统、使用Conda包管理器、CUDA 11.7的用户命令可能长这样conda install pytorch torchvision torchaudio pytorch-cuda11.7 -c pytorch -c nvidia把它复制到刚才激活的pytorch_env环境中执行即可。注意安装过程可能会比较慢因为要下载的包很大。请保持网络通畅也可以考虑配置国内的镜像源来加速。安装完成后验证一下。在激活的环境里打开Python解释器命令行输入python然后输入import torch print(torch.__version__) # 查看PyTorch版本 print(torch.cuda.is_available()) # 查看GPU是否可用返回True则成功如果都能正常打印出版本号且第二句根据你的选择返回TrueGPU或FalseCPU那么恭喜你环境搭建成功3. 张量Tensor一切计算的基石PyTorch的核心数据结构是张量Tensor。你可以把它理解为Numpy中ndarray的升级版但最关键的是它能够无缝地在CPU和GPU之间进行转换和计算并且支持自动求导这是神经网络训练的核心。理解张量是理解PyTorch所有操作的基础。3.1 创建张量多种姿势灵活运用创建张量的方式有很多对应不同的初始化需求。import torch # 1. 从数据直接创建 data [[1, 2], [3, 4]] x_data torch.tensor(data) # 最常用的方式从列表、元组等Python数据结构创建 print(f“从数据创建\n{x_data}”) # 2. 从Numpy数组创建 (非常常用) import numpy as np np_array np.array(data) x_np torch.from_numpy(np_array) # 注意这样创建的张量和原Numpy数组共享内存 print(f“从Numpy创建\n{x_np}”) # 3. 继承另一个张量的属性形状、数据类型 x_ones torch.ones_like(x_data) # 创建一个和x_data形状、数据类型全一样的全1张量 print(f“全1张量\n{x_ones}”) x_rand torch.rand_like(x_data, dtypetorch.float) # 指定数据类型创建随机张量 print(f“随机张量\n{x_rand}”) # 4. 使用形状参数创建 shape (2, 3,) # 表示一个2行3列的矩阵 rand_tensor torch.rand(shape) # 均匀随机分布 [0, 1) ones_tensor torch.ones(shape) # 全1 zeros_tensor torch.zeros(shape) # 全0 print(f“形状创建-随机\n{rand_tensor}\n形状创建-全1\n{ones_tensor}”)这里有个极易踩坑的点torch.tensor()和torch.Tensor()的区别。torch.tensor()是一个函数它会根据你传入的数据拷贝一份并推断出合适的数据类型。而torch.Tensor()是类构造函数如果你传入一个列表它也会拷贝数据但默认数据类型是torch.float32。更让人困惑的是直接写torch.FloatTensor([1,2])会创建浮点型张量[1., 2.]。为了避免混乱新手我建议统一使用torch.tensor()它的行为最直观。3.2 张量属性与操作形状、设备与计算张量有几个关键属性在后续操作中会频繁用到。tensor torch.rand(3, 4) # 创建一个3行4列的随机张量 print(f“张量的形状 {tensor.shape}”) # 或 tensor.size() print(f“张量的数据类型 {tensor.dtype}”) # 如 torch.float32 print(f“张量所在的设备 {tensor.device}”) # cpu 或 cuda:0设备Device这个概念非常重要。默认创建的张量在CPU上。如果安装了GPU版本的PyTorch且可用你可以将张量移动到GPU上以加速计算这是深度学习训练速度的关键。# 判断GPU是否可用然后移动张量 if torch.cuda.is_available(): tensor tensor.to(‘cuda’) # 或者 tensor tensor.cuda() print(f“张量现在在{tensor.device}”)张量的操作语法和Numpy非常相似学习成本很低。# 索引和切片 (和Python列表、Numpy几乎一样) tensor torch.ones(4, 4) print(‘第一行’, tensor[0]) # 第一行 print(‘第一列’, tensor[:, 0]) # 第一列 print(‘右下角2x2子矩阵\n’, tensor[2:, 2:]) # 拼接张量 t1 torch.cat([tensor, tensor, tensor], dim1) # 沿维度1列拼接 print(f“沿列拼接后的形状{t1.shape}”) # torch.Size([4, 12]) # 矩阵乘法多种写法 y1 tensor tensor.T # 运算符 y2 tensor.matmul(tensor.T) # .matmul() 方法 y3 torch.rand(4, 4) torch.matmul(tensor, y3, outy3) # 指定输出位置节省内存 # 逐元素乘法注意和矩阵乘法的区别 z1 tensor * tensor # * 运算符是逐元素乘 z2 tensor.mul(tensor) print(f“矩阵乘法结果形状{y1.shape}”) # torch.Size([4, 4]) print(f“逐元素乘结果形状{z1.shape}”) # torch.Size([4, 4])但计算方式不同注意*乘号和torch.mm()/矩阵乘是新手最容易混淆的操作。*是对应位置元素相乘要求两个张量形状完全相同或满足广播规则。而矩阵乘法是行与列的点积要求第一个张量的列数等于第二个张量的行数。务必分清。3.3 张量与Numpy的互转数据处理的桥梁在实际项目中我们经常用Numpy或Pandas做数据加载和预处理然后用PyTorch张量进行模型计算。它们之间的转换必须熟练掌握且要警惕一个大坑内存共享。# 张量 - Numpy数组 (共享内存) t torch.ones(5) n t.numpy() print(f“t: {t}”) # tensor([1., 1., 1., 1., 1.]) print(f“n: {n}”) # array([1., 1., 1., 1., 1.], dtypefloat32) t.add_(1) # 注意带下划线的操作是原地(in-place)操作会修改t自身 print(f“修改t后t: {t}”) # tensor([2., 2., 2., 2., 2.]) print(f“修改t后n: {n}”) # array([2., 2., 2., 2., 2.], dtypefloat32) !!! n也变了 # Numpy数组 - 张量 (共享内存) n np.ones(5) t torch.from_numpy(n) np.add(n, 1, outn) # 修改n print(f“修改n后t: {t}”) # tensor([2., 2., 2., 2., 2.], dtypetorch.float64) !!! t也变了看到吗通过.numpy()和torch.from_numpy()转换得到的新对象与原对象共享底层内存。修改其中一个另一个会同步变化。这有时能提高效率但更多时候是潜在的Bug来源。如果你需要一份独立的拷贝应该这样做t torch.ones(5) n t.detach().cpu().numpy() # 更安全的做法.detach()分离计算图.cpu()确保数据在CPU再转numpy # 或者直接拷贝 n_copy t.numpy().copy() n np.ones(5) t torch.tensor(n) # 使用torch.tensor()会创建数据拷贝4. 自动求导AutogradPyTorch的“灵魂”PyTorch相比其他框架一个巨大的优势就是其动态计算图和自动求导机制它让定义和训练神经网络变得异常简单直观。理解autograd是理解PyTorch如何工作的关键。4.1 计算图与梯度PyTorch中所有用于计算的张量都有一个requires_grad属性。如果设置为TruePyTorch就会跟踪在该张量上执行的所有操作并构建一个动态计算图。当你完成计算后可以反向传播backward()自动计算所有梯度。import torch # 创建一个需要计算梯度的张量 x torch.ones(2, 2, requires_gradTrue) # 设置 requires_gradTrue 来跟踪计算 print(x) # 对x进行一个操作 y x 2 print(y) # 注意y有一个 grad_fn 属性记录了它是由加法创建的 # grad_fn: AddBackward0 object at ... z y * y * 3 out z.mean() # 对z所有元素求平均值得到一个标量 print(z, out) # 现在进行反向传播计算梯度 out.backward() # 等价于 out.backward(torch.tensor(1.)) # 因为out是一个标量所以不需要为backward()传入参数。 # 打印梯度 d(out)/dx print(x.grad)我们来手动验算一下上面的梯度为什么是4.5。 设x是一个元素全为1的矩阵。y x 2-y元素全为3。z y * y * 3-z每个元素是3*3*3 27。out z.mean()-out (27*4)/4 27。 现在求d(out)/dx。根据链式法则d(out)/dx d(out)/dz * dz/dy * dy/dx。out mean(z) sum(z)/4所以d(out)/dz 1/4一个全为0.25的矩阵。z 3*y^2所以dz/dy 6*y。当y3时dz/dy 18。y x 2所以dy/dx 1。 因此d(out)/dx (1/4) * 18 * 1 4.5。这与x.grad的结果一致。4.2 停止梯度跟踪与评估模式在实际应用中我们经常需要冻结一部分网络参数如微调预训练模型或者在模型推理测试时不需要计算梯度以节省内存和计算资源。这时就需要停止梯度跟踪。# 方法1使用 .detach() 获得一个不需要梯度的新张量但共享数据 x torch.randn(3, requires_gradTrue) y x * 2 z y.detach() # z是从y分离出来的不需要梯度但修改z会影响y的数据共享内存 print(y.requires_grad) # True print(z.requires_grad) # False # 方法2使用 torch.no_grad() 上下文管理器块内的计算都不会被跟踪 with torch.no_grad(): w x * 3 print(w.requires_grad) # False # 方法3在模型评估时使用 .eval() 方法并结合 torch.no_grad() model ... # 你的神经网络模型 model.eval() # 将模型设置为评估模式会影响Dropout、BatchNorm等层的行为 with torch.no_grad(): predictions model(test_data) # 此处的forward计算不会构建计算图注意model.eval()和torch.no_grad()通常一起使用但作用不同。model.eval()是告诉模型中的特定层如Dropout、BatchNorm切换到推理阶段的行为模式。torch.no_grad()是关闭自动求导引擎节省内存和计算。在训练循环的验证/测试阶段务必同时使用它们。4.3 梯度累加与清零在训练神经网络时梯度是累加的。这意味着每次调用.backward()计算出的梯度会加到.grad属性中而不是替换它。这样设计是为了方便实现梯度累加用多个小批次模拟一个大批次等技巧。但这也意味着在每次参数更新优化器step之前必须手动将梯度清零否则梯度会越累越大导致训练出错。# 一个简单的训练循环片段 optimizer torch.optim.SGD(model.parameters(), lr0.01) # 优化器 for epoch in range(num_epochs): for data, target in dataloader: optimizer.zero_grad() # 关键一步清零上一轮的梯度 output model(data) loss criterion(output, target) loss.backward() # 反向传播计算梯度梯度会累加到参数的.grad属性 optimizer.step() # 根据梯度更新参数如果你忘记了optimizer.zero_grad()你会发现loss不下降甚至爆炸。这是新手常犯的错误之一。5. 神经网络构建用nn.Module搭建你的模型PyTorch提供了torch.nn模块来帮助我们高效地构建神经网络。其核心是nn.Module类你自定义的任何一个网络层或整个网络都应该继承这个类。5.1 定义你的第一个网络让我们构建一个用于图像分类的简单卷积神经网络CNN。import torch import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 必须调用父类的初始化 # 定义网络层 # 输入图像通道1灰度图输出通道6卷积核大小3x3 self.conv1 nn.Conv2d(1, 6, 3) self.conv2 nn.Conv2d(6, 16, 3) # 全连接层 # 经过两次卷积池化后特征图大小需要计算这里假设输入是32x32 # 第一次卷积后(32-32*0)/1 1 30 - 30x30 # 第一次池化后30/2 15 - 15x15 # 第二次卷积后(15-32*0)/1 1 13 - 13x13 # 第二次池化后13/2 6向下取整 - 6x6 # 所以特征图数量16 * 6 * 6 576 self.fc1 nn.Linear(16 * 6 * 6, 120) # 全连接层输入576维输出120维 self.fc2 nn.Linear(120, 84) self.fc3 nn.Linear(84, 10) # 假设是10分类任务 def forward(self, x): # 定义数据的前向传播路径 # 卷积 - ReLU激活 - 最大池化 x F.max_pool2d(F.relu(self.conv1(x)), 2) # 池化窗口2x2 x F.max_pool2d(F.relu(self.conv2(x)), 2) # 将多维特征图“展平”成一维向量以便输入全连接层 x torch.flatten(x, 1) # 从第1维开始展平第0维是batch_size # 全连接层 - ReLU激活 x F.relu(self.fc1(x)) x F.relu(self.fc2(x)) x self.fc3(x) # 最后一层通常不加激活函数对于分类配合CrossEntropyLoss会内部做Softmax return x # 实例化网络 net Net() print(net)关键点解析__init__中定义的是网络的“组件”层如卷积层、全连接层。这里只是定义了这些层对象还没有指定数据如何流动。forward方法中定义的是数据的“流动路径”。它接收输入x并按照你定义的顺序调用__init__中定义的层最终返回输出。forward才是网络真正的计算逻辑。在forward中我们使用了F.relu和F.max_pool2d。torch.nn.functional通常导入为F包含了许多不带参数的函数式操作如激活函数、池化、损失函数等。你也可以使用nn.ReLU()和nn.MaxPool2d(2)这样的模块化方式将其定义在__init__中。两种方式都可以函数式调用更灵活模块化定义在print(net)时能更清晰地显示网络结构。全连接层nn.Linear的输入维度需要根据前面卷积池化后的特征图大小手动计算。这是一个常见的出错点。更现代的做法是可以先定义一个nn.AdaptiveAvgPool2d(1)层将任意大小的特征图池化为1x1这样输入维度就固定为通道数省去了计算麻烦。5.2 访问模型参数与应用钩子Hooknn.Module会自动跟踪所有在__init__中定义的、属于nn.Module子类的属性比如nn.Conv2d,nn.Linear。你可以方便地访问和遍历它们。# 访问所有参数 params list(net.parameters()) print(f“网络总参数量{len(params)}层”) print(f“第一层卷积的权重形状{params[0].size()}”) # conv1.weight # 访问特定层的参数 print(net.conv1.weight) # 直接通过属性访问 print(net.conv1.bias) # 遍历所有子模块 for name, module in net.named_children(): print(f‘子模块名{name}, 模块类型{module}’) # 遍历所有参数带名字 for name, param in net.named_parameters(): if param.requires_grad: print(f‘参数名{name}, 形状{param.data.size()}’)有时我们想查看网络中某一层的输入或输出用于调试或可视化。这时可以使用钩子Hook。钩子允许你在不修改网络forward代码的情况下插入自定义函数。# 定义一个钩子函数打印该层的输出形状和部分值 def print_activation(module, input, output): print(f‘{module.__class__.__name__} 输出形状{output.shape}’) # print(output) # 谨慎打印数据量可能很大 # 为 conv1 层注册一个前向钩子 handle net.conv1.register_forward_hook(print_activation) # 运行一个前向传播触发钩子 dummy_input torch.randn(1, 1, 32, 32) # (batch, channel, height, width) output net(dummy_input) # 用完记得移除钩子避免内存泄漏 handle.remove()6. 数据加载与处理Dataset与DataLoader模型和算法准备好了数据是下一个关键。PyTorch提供了torch.utils.data.Dataset和DataLoader这两个强大的抽象来高效地处理数据加载、批处理、打乱、多进程读取等问题。6.1 自定义DatasetDataset是一个抽象类代表一个数据集。你需要继承它并实现两个核心方法__len__: 返回数据集的大小。__getitem__: 给定索引idx返回对应的样本数据标签。假设我们有一个简单的图像分类数据集图像文件在data/imgs文件夹标签在一个labels.csv文件里。import os import pandas as pd from torch.utils.data import Dataset from PIL import Image class CustomImageDataset(Dataset): def __init__(self, annotations_file, img_dir, transformNone): 参数 annotations_file (str): 标签文件路径如csv。 img_dir (str): 图像文件夹路径。 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, label关键点transform参数非常重要。它通常是一个由torchvision.transforms组合而成的变换管道负责将PIL图像或Numpy数组转换为PyTorch张量并进行归一化、数据增强等操作。6.2 使用DataLoader进行批处理Dataset每次只返回一个样本。DataLoader则围绕Dataset提供了批量生成、打乱数据、多进程加载等功能。from torch.utils.data import DataLoader from torchvision import transforms # 定义图像变换 transform transforms.Compose([ transforms.Resize((32, 32)), # 调整大小 transforms.RandomHorizontalFlip(), # 随机水平翻转数据增强 transforms.ToTensor(), # 转换为张量并自动将[0,255]像素值缩放到[0.0,1.0] transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)) # 归一化到[-1, 1] ]) # 实例化自定义数据集 dataset CustomImageDataset(annotations_file‘labels.csv’, img_dir‘data/imgs’, transformtransform) # 创建DataLoader dataloader DataLoader(dataset, batch_size4, # 每批4个样本 shuffleTrue, # 每个epoch打乱数据 num_workers2) # 使用2个子进程加载数据 # 在训练循环中使用 for epoch in range(num_epochs): for images, labels in dataloader: # dataloader自动生成批次 # images的形状是 [batch_size, 3, 32, 32] # labels的形状是 [batch_size] # ... 训练代码 ... pass参数详解与避坑batch_size: 根据你的GPU内存调整。太小则训练不稳定且慢太大可能内存溢出。常见值有32, 64, 128。shuffle: 训练集一定要设为True防止模型学习到数据的顺序。验证集和测试集设为False。num_workers: 用于数据加载的子进程数。可以加快数据从磁盘到内存的加载速度。设置太大可能导致内存爆炸或死锁一般设置为CPU核心数或稍小一些。在Windows上有时多进程会有问题如果遇到报错可以尝试设为0。pin_memory: 如果使用GPU将其设为True可以将数据直接锁页内存加速从CPU到GPU的数据传输。6.3 使用TorchVision预置数据集对于常见数据集如MNIST, CIFAR-10, ImageNetPyTorch的TorchVision库提供了现成的Dataset实现极大方便了学习和实验。import torchvision import torchvision.transforms as transforms # 下载并加载CIFAR-10训练集 transform transforms.Compose([ transforms.ToTensor(), transforms.Normalize((0.4914, 0.4822, 0.4465), (0.2470, 0.2435, 0.2616)) # CIFAR-10的均值和标准差 ]) trainset torchvision.datasets.CIFAR10(root‘./data’, trainTrue, downloadTrue, transformtransform) trainloader DataLoader(trainset, batch_size64, shuffleTrue, num_workers2) # 类别名称 classes (‘plane’, ‘car’, ‘bird’, ‘cat’, ‘deer’, ‘dog’, ‘frog’, ‘horse’, ‘ship’, ‘truck’) # 查看一个批次的数据 dataiter iter(trainloader) images, labels next(dataiter) print(f‘图像批次形状{images.shape}’) # torch.Size([64, 3, 32, 32]) print(f‘标签批次形状{labels.shape}’) # torch.Size([64])7. 训练循环完整实现将所有部分串联起来现在我们将环境、张量、自动求导、网络模型、数据加载所有这些知识串联起来写一个完整的、可以跑起来的图像分类模型训练脚本。这是从“知道”到“做到”最关键的一步。7.1 定义模型、损失函数和优化器我们使用一个比之前例子更简单清晰的网络并选择适合分类任务的交叉熵损失和常用的Adam优化器。import torch.nn as nn import torch.nn.functional as F import torch.optim as optim # 定义一个更简单的网络 class SimpleCNN(nn.Module): def __init__(self): super(SimpleCNN, self).__init__() self.conv1 nn.Conv2d(3, 16, 3, padding1) # CIFAR-10是3通道彩色图 self.pool nn.MaxPool2d(2, 2) self.conv2 nn.Conv2d(16, 32, 3, padding1) self.fc1 nn.Linear(32 * 8 * 8, 256) # 经过两次2x2池化32x32 - 16x16 - 8x8 self.fc2 nn.Linear(256, 10) # 10个类别 def forward(self, x): x self.pool(F.relu(self.conv1(x))) x self.pool(F.relu(self.conv2(x))) x torch.flatten(x, 1) # 展平 x F.relu(self.fc1(x)) x self.fc2(x) return x # 初始化网络、损失函数、优化器 device torch.device(‘cuda’ if torch.cuda.is_available() else ‘cpu’) print(f‘使用的设备{device}’) net SimpleCNN().to(device) # 将网络移动到GPU如果可用 criterion nn.CrossEntropyLoss() # 交叉熵损失内部已包含Softmax optimizer optim.Adam(net.parameters(), lr0.001) # Adam优化器学习率0.001选择分析损失函数对于多分类任务nn.CrossEntropyLoss是标准选择。它结合了LogSoftmax和NLLLoss所以网络的最后一层不需要再加Softmax激活函数直接输出原始分数logits即可。这是新手常犯的另一个错误在交叉熵损失前又加了Softmax导致数值不稳定。优化器optim.SGD随机梯度下降是基础但optim.Adam因其自适应学习率特性在大多数情况下能更快收敛且对初始学习率不那么敏感更适合新手。学习率lr是一个超参数0.001是Adam一个不错的起点。7.2 编写训练与验证循环一个完整的训练过程通常包含多个轮次epoch每个epoch内遍历整个训练集并定期在验证集上评估模型性能。def train_one_epoch(model, dataloader, criterion, optimizer, device): “”“训练一个epoch”“” model.train() # 设置为训练模式启用Dropout, BatchNorm更新 running_loss 0.0 correct 0 total 0 for i, data in enumerate(dataloader, 0): # 获取输入数据 inputs, labels data 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() _, predicted torch.max(outputs.data, 1) # 获取预测类别 total labels.size(0) correct (predicted labels).sum().item() # 每100个batch打印一次信息 if i % 100 99: print(f‘[批次 {i 1}] 损失{running_loss / 100:.3f}’) running_loss 0.0 epoch_loss running_loss / len(dataloader) epoch_acc 100 * correct / total return epoch_loss, epoch_acc def validate(model, dataloader, criterion, device): “”“在验证集上评估模型”“” model.eval() # 设置为评估模式关闭Dropout, 固定BatchNorm统计量 val_loss 0.0 correct 0 total 0 with torch.no_grad(): # 关闭梯度计算节省内存和计算 for data in dataloader: images, labels data images, labels images.to(device), labels.to(device) outputs model(images) loss criterion(outputs, labels) val_loss loss.item() _, predicted torch.max(outputs.data, 1) total labels.size(0) correct (predicted labels).sum().item() val_loss / len(dataloader) val_acc 100 * correct / total print(f‘验证集 - 平均损失{val_loss:.3f}, 准确率{val_acc:.2f}%’) return val_loss, val_acc # 假设我们已经有了 trainloader 和 valloader num_epochs 10 for epoch in range(num_epochs): print(f‘\nEpoch {epoch 1}/{num_epochs}’) train_loss, train_acc train_one_epoch(net, trainloader, criterion, optimizer, device) print(f‘训练集 - 平均损失{train_loss:.3f}, 准确率{train_acc:.2f}%’) val_loss, val_acc validate(net, valloader, criterion, device) print(‘训练完成’)核心要点与避坑指南.train()和.eval()模式务必在训练循环开始前调用model.train()在验证/测试前调用model.eval()。这会影响Dropout和BatchNorm等层的行为。在eval模式下Dropout层会让所有神经元通过BatchNorm层会使用训练阶段统计好的全局均值和方差而不是当前批次的统计量。.zero_grad()的位置必须在loss.backward()之前optimizer.step()之后的下一个批次开始前调用。顺序是zero_grad()-forward-loss-backward-step。.item()的使用loss是一个包含单个元素的张量。使用.loss.item()可以将其转换为Python数字方便打印和记录。直接打印loss会打印出整个张量及其计算图信息。设备移动确保模型、输入数据、标签都在同一个设备上CPU或GPU。model.to(device)移动模型data.to(device)移动数据。验证时使用torch.no_grad()这能显著减少内存消耗并加速计算因为不需要保存中间变量用于反向传播。7.3 模型保存与加载训练好的模型需要保存下来以便后续评估、继续训练或部署。# 保存模型的完整状态包括结构和参数 PATH ‘./cifar_net.pth’ torch.save(net.state_dict(), PATH) # 加载模型需要先有相同的网络结构定义 net SimpleCNN() # 重新实例化一个结构相同的网络 net.load_state_dict(torch.load(PATH)) net.to(device) # 记得移动到设备 # 如果要保存整个模型包括结构可以这样但不太推荐因为对代码结构有依赖 # torch.save(net, ‘model.pth’) # model torch.load(‘model.pth’)最佳实践通常只保存模型的状态字典state_dict而不是整个模型对象。state_dict是一个Python字典将每一层映射到其参数张量。这样做更灵活加载时你需要先实例化模型结构然后再加载参数。这确保了你的模型定义代码是版本可控的。这份笔记从最基础的环境搭建到核心的张量和自动求导再到构建网络、加载数据最后完成一个完整的训练循环覆盖了PyTorch入门所需的主要知识点。每个部分我都结合了自己当初学习时遇到的困惑和后来项目中的经验希望能帮你避开那些常见的“坑”。深度学习框架的学习最好的方式就是“动手”。不要只看一定要把代码敲一遍尝试修改参数观察输出变化甚至故意写错一些地方看看报错信息。当你用自己的数据成功训练出第一个模型时这些抽象的概念才会真正变得具体和牢固。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门