前言:本篇文章是跟随 Pytorch官网60分钟速成教程 来学习。来看看今天能不能速成哈哈哈。
首先要安装pytorch:
javascript
pip3 install torch torchvision
当然首先得有 pip3 和 python 环境,前置的安装在这里就不赘述了,这篇文章还是专注于 PyTorch 入门
一、张量
张量是一种数据结构,类似(多维)数组和矩阵,它将模型的输入、参数、输出的数据结构统一化。
pytorch 中的张量类似于 numpy 中的 narray,两者密不可分,可以相互转化,所以我们在程序一开始可以将这两个包引进来,方便调试
py
import torch
import numpy as np
(一)张量初始化
① torch.tensor(数据)
直接来自数据,数据类型自动推断
javascript
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
print(x_data)

② torch.from_numpy(narray)
从 numpy 数组创建张量
javascript
data = [[1, 2], [3, 4]]
np_array = np.array(data)
x_np = torch.from_numpy(np_array)
print(x_data)

③ torch.ones_like(x_data)
创建一个数据全是1的张量,会继承参数张量x_data的数据类型和形状。
javascript
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
y_data = torch.ones_like(x_data)
print(y_data)

④ torch.rand_like(x_data, dtype=torch.float)
创建一个随机数张量,随机数范围是0~1,形状继承参数张量 x_data ,数据类型需要通过第二个参数指定为浮点数 dtype=torch.float。
javascript
data = [[1, 2], [3, 4]]
x_data = torch.tensor(data)
y_data = torch.rand_like(x_data, dtype=torch.float)
print(y_data)

⑤ 通过shape
shape 用来指定张量的形状,直接 shape = (2, 3,) 定义,不得不说,python比js还灵活。shape = (2, 3,) 表示定义一个2行3列的二维数组,后面用逗号,表示后面还可以任意增加维度
下面的方法分别表示创建指定形状的随机数、数据全为1的张量、数据全为0的张量,第二个参数可以用 dtype=int 指定数据类型为整型
py
shape = (2, 3, )
# 创建随机数张量
rand_tensor = torch.rand(shape)
# 创建数据全为1的张量,数据类型为int
ones_tensor = torch.ones(shape, dtype=int)
# 创建数据全为0的张量
zeros_tensor = torch.zeros(shape)
print(f"Random Tensor: \n {rand_tensor} \n")
print(f"Ones Tensor: \n {ones_tensor} \n")
print(f"Zeros Tensor: \n {zeros_tensor}")

(二)张量属性
形状、数据类型、设备
javascript
tensor = torch.rand(3, 4)
print(f"Shape of tensor: {tensor.shape}")
print(f"Datatype of tensor: {tensor.dtype}")
print(f"Device tensor is stored on: {tensor.device}")

(三)张量运算
上述网页包含一百多种张量运算,下面介绍几种常见的张量运算
① 张量切片
张量切片是用于选中张量的一部分的语法。
python
# 创建一个形状为4*4,数值全是1的张量
tensor = torch.ones(4, 4)
# 全部行的第一列设置为0
tensor[:,1] = 0
print(tensor)

② torch.cat
连接两个张量,不会升高张量的维度
python
tensor1 = torch.ones(4, 4)
tensor2 = torch.zeros(4, 4)
print(torch.cat([tensor1,tensor2]))

③ tensor.mul
张量乘法,就可以看成矩阵乘法。二维还能手算,再往上就难手动模拟了
python
tensor1 = torch.tensor([[1,2],[3,4]])
tensor2 = torch.tensor([[2,1],[2,3]])
print(tensor1.mul(tensor2))

④ 原地操作
带有 _ 的是原地操作,就是直接修改当前的张量
python
tensor = torch.tensor([[1,2],[3,4]])
print(tensor, "\n")
tensor.add_(5)
print(tensor)

⑤ 与NumPy互相转化
数据层面相当于浅拷贝,修改一个会影响另一个
1️⃣ 张量转NumPy数组
python
t = torch.ones(5, dtype=int)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

张量的变化会反映在 NumPy 数组中。
python
t.add_(8)
print(f"t: {t}")
print(f"n: {n}")

2️⃣ NumPy数组转张量
python
n = np.ones(5)
t = torch.from_numpy(n)
print(f"n: {n}")
print(f"t: {t}")

NumPy数组的变化会反映在张量中。
python
np.add(n, 1, out=n)
print(n)
print(t)

二、torch.autograd
torch.autograd 是 PyTorch 的自动微分引擎,为神经网络训练提供支持。本节从概念上解释 autograd 如何帮助神经网络进行训练。
(一)背景
神经网络是一系列对输入数据执行的网状的函数的集合。这些函数由参数(由权重和偏置组成)定义,这些参数在 PyTorch 中存储在张量中。
训练神经网络主要有两个步骤:
✬✬✬ ① 正向传播:在正向传播中,神经网络会对于正确的输出数据进行最佳猜测。输入数据经过所有的函数来获得最终猜测。
✬✬✬ ② 反向传播:在反向传播中,神经网络根据其猜测的误差成比例地调整其参数。它通过从输出端向后遍历来实现这一点,收集误差相对于函数参数的导数(梯度),并使用梯度下降法优化参数。
(二)在 PyTorch 中使用
让我们来看一个单独的训练步骤。在这个例子中,我们会加载一个 torchvision 库提供的预训练模型 resnet18。我们会创建一个随机数据张量来展示一个单独的三通道(rgb)、宽高都为64的图片,将其对应的标签初始化为一些随机值。预训练模型中的标签形状为(1,1000)。
python
import torch
from torchvision.models import resnet18, ResNet18_Weights
model = resnet18(weights=ResNet18_Weights.DEFAULT)
data = torch.rand(1, 3, 64, 64)
labels = torch.rand(1, 1000)
下面详细解释一下上述代码
1️⃣ 导入相关的库
python
import torch
from torchvision.models import resnet18, ResNet18_Weights
- torch: PyTorch 深度学习框架核心库
- torchvision.models: PyTorch的计算机视觉模型库
- resnet18: ResNet-18 网络结构(18层深度)
- ResNet18_Weights: ResNet-18 的预训练权重枚举类
2️⃣ 加载预训练的 ResNet-18 模型
python
model = resnet18(weights=ResNet18_Weights.DEFAULT)
weights=ResNet18_Weights.DEFAULT: 使用最新的官方预训练权重
模型特性:
- 输入尺寸: 224×224 的 RGB 图像
- 输出维度: 1000 类(
ImageNet数据集分类) - 预训练: 在
ImageNet数据集上训练过
3️⃣ 创建模拟输入数据
python
data = torch.rand(1, 3, 64, 64)
形状: (1, 3, 64, 64)
1: 批次大小(batch size)- 1张图像
3: 通道数(RGB)- 彩色图像
64: 高度(height)- 64像素
64: 宽度(width)- 64像素
数值: [0, 1) 范围内的随机浮点数
4️⃣ 创建模拟标签
python
labels = torch.rand(1, 1000)
形状: (1, 1000)
1: 批次大小(对应1个样本)
1000: 分类数(ImageNet的1000个类别)
数值: [0, 1) 范围内的随机数
5️⃣ 前向传播
接下来,我们将输入数据通过模型的每一层进行运算,以做出预测。这被称为前向传播。
python
prediction = model(data) # forward pass
6️⃣ 反向传播
然后需要用预测值和标签来计算损失,通过网络反向传播损失。我们通过损失张量调用 .backward() 来进行反向传播,Autograd 随后计算并存储每个模型参数的梯度,并将这些梯度保存在参数的 .grad 属性中。
python
# 计算损失
loss = (prediction - labels).sum()
# 反向传播
loss.backward()
7️⃣ 加载优化器
下一步,我们会加载一个优化器。接下来,我们加载一个优化器,这里选择的是SGD,学习率为0.01,动量为0.9。
我们需要在优化器里注册模型所有的参数。
python
optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
优化器
在机器学习和深度学习中,优化器(Optimizer) 扮演着"导航员"的角色。它的核心作用是通过调整模型的参数,来最小化损失函数(Loss Function)的值,从而让模型的预测结果越来越接近真实值。
简单来说,如果把训练模型比作在深夜下山,损失函数就是"山的高度",而优化器就是带你寻找"山谷(最低点)"的方法和步伐。
核心作用与目标优化器的本质是执行 梯度下降(Gradient Descent) 的变体。其具体工作包括:
更新参数:根据计算出的梯度(Gradient),决定如何修改模型权重 w w w 和偏置 b b b。
控制学习速率:决定每一步走多远(学习率 η \eta η)。寻找最优解:在复杂的参数空间中避开鞍点或局部最小值,寻找全局最优解(或足够好的局部最优解)。
动量借用了物理学的概念,简单来说,动量的作用就是让损失函数更加平滑,在稳定的方向上速度更快。
8️⃣ 启动梯度下降
最后,我们调用 .step() 来启动梯度下降。优化器会根据存储在 .grad 中的梯度来调整每个参数。
python
optim.step() #梯度下降
上述就是训练神经网络的整体流程。
(三)Autograd中的自微分
接下来看一下 autograd 如何收集梯度。下面的代码创建了两个张量 a 和 b,通过 requires_grad=True 表示这两个张量需要计算梯度,这表示这两个张量上的每一步操作都会被追踪。
python
import torch
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
接下来通过对 a 和 b 的运算式创建另一个张量 Q
python
Q = 3*a**3 - b**2
假设 a 和 b 是神经网络的参数,Q 是误差。在神经网络训练中,我们想要误差 Q 关于参数的梯度,即:

当我们在 Q 上调用 .backward() 时,autograd 会计算这些梯度,并将它们存储在相应张量的 .grad 属性中。
我们需要在 Q.backward() 中显式传递一个梯度参数,它是一个向量。梯度是一个与 Q 形状相同的张量,它表示 Q 相对于自身的梯度,即:

等价地,我们也可以将 Q 聚合为一个标量,并隐式地调用 backward,例如 Q.sum().backward()。
python
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
external_grad = torch.tensor([1., 1.])
这一行创建了一个权重张量(通常称为v)。
它的维度必须与 Q 的维度完全一致。
这里设为 [1., 1.],意味着要将Q 中每个元素的梯度等权重地传递回去。
backward(gradient=external_grad)
计算Q关于网络参数的梯度。gradient 参数指定了 Q 中每个元素在反向传播时的"权重"或"初始变化率"。
梯度现在保存在 a.grad 和 b.grad 中,两者权重相同。
python
# 检查梯度是否正确
print(9*a**2 == a.grad)
print(-2*b == b.grad)
在数学中,如果 y = f(x),矩阵y是矩阵x的函数,那么y对x的导数是一个雅各比矩阵。

一般来说,torch.autograd 用于计算向量-雅可比积。也就是说,给定任意向量 𝑣,计算乘积 𝐽转置 ⋅ 𝑣。
如果 v 是一个标量函数 l=g(y) 的梯度

那么根据链式法则,向量雅可比矩阵乘积将是 𝑙 对 𝑥⃗ 的梯度:

也就是说,矩阵y是矩阵x的函数,l是矩阵y的标量函数(即输入一个矩阵y,输出l是一个数值),torch.autograd 计算的是l 对x`的梯度。
(四)计算图
从概念上讲,autograd 会在一个有向无环图 (DAG) 中记录数据(张量)以及所有已执行的操作(以及产生的新张量),该图由 Function 对象组成。在这个DAG中,叶节点是输入张量,根节点是输出张量。通过从根节点到叶节点追踪这个图,你可以使用链式法则自动计算梯度。
在前向传播过程中,autograd 同时做两件事:
- 执行请求的操作以计算结果张量
- 在DAG中维护操作的梯度函数。
反向传播从在DAG根节点上调用.backward()时开始。然后,autograd会: - 从每个梯度函数
.grad_fn计算梯度 - 将其累积到相应张量的
.grad属性中 - 使用链式法则,一直传播到叶张量
以下是我们示例中DAG的可视化表示。在图中,箭头的指向是前向传播的方向,节点表示正向传播中每个操作的反向函数。蓝色的叶结点表示叶张量a和b。

在PyTorch中,DAG(有向无环图)是动态的。需要注意的一个重要事项是,该图是从头开始重新创建的;每次调用.backward()后,autograd都会开始填充一个新图。这正是允许你在模型中使用控制流语句的原因;如果需要,你可以在每次迭代中更改形状、大小和操作。
从有向无环图中移除某些节点或元素的过程 :torch.autograd 会追踪所有配置项requires_grad为True的张量,requires_grad为False的张量,表示不需要梯度,会将其从梯度计算DAG中排除。
python
x = torch.rand(5, 5)
y = torch.rand(5, 5)
z = torch.rand((5, 5), requires_grad=True)
a = x + y
print(f"Does `a` require gradients?: {a.requires_grad}")
b = x + z
print(f"Does `b` require gradients?: {b.requires_grad}")

在神经网络中,不计算梯度的参数通常被称为冻结参数。冻结不需要梯度的参数可以减少自动梯度计算,提高模型的效率。
在微调中,我们通常会冻结模型的大部分参数,而只修改分类器层,以便对新的标签进行预测。让我们通过一个小例子来演示这一点。和之前一样,我们加载一个预训练的resnet18模型,并冻结所有的参数。
python
from torch import nn, optim
model = resnet18(weights=ResNet18_Weights.DEFAULT)
# 冻结所有参数
for param in model.parameters():
param.requires_grad = False
假设我们想在一个包含10个标签的新数据集上微调模型。在 ResNet 中,分类器是最后一个线性层 model.fc。下面创建一个简单的线性层作为我们的分类器。
python
model.fc = nn.Linear(512, 10)
现在模型中除了model.fc,其他的参数都是冻结的,计算梯度全部根据model.fc的权重和偏置来计算
python
# 优化分类器
optimizer = optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)
上面代码尽管我们在优化器中注册了所有参数,但只有分类器的权重和偏置是计算梯度(因此在梯度下降中更新)的参数。
三、神经网络
(一)简介
神经网络可以使用 torch.nn 包构建。
torch.nn 包依赖于 autograd 来定义模型并对其进行微分。一个 nn.Module 包含多个层,以及一个返回输出结果的 forward(input) 方法。
下面是一个对数字图片进行分类的网络:

这是一个简单的前馈网络。它接收输入,将其依次传递通过多个层,然后最终给出输出。
前馈神经网络(Feedforward Neural Network, FNN) 是深度学习中最基础、最古老的结构。它的名字"前馈"形象地描述了数据的流动方向:信号只从输入层流向输出层,没有任何反馈或循环。
神经网路典型的训练过程如下:
- 定义一个包含一些可学习参数(或权重)的神经网络。可学习就是有规律,可以学习其中的规律。
- 遍历输入数据集
- 通过网络处理输入
- 计算损失(输出结果与正确答案的差距有多大)
- 将梯度反向传播到网络的参数中
- 更新网络权重,通常使用一个简单的更新规则:
weight = weight - learning_rate * gradient
(二)定义
python
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 个输出通道, 5x5 平方卷积核
self.conv1 = nn.Conv2d(1, 6, 5)
self.conv2 = nn.Conv2d(6, 16, 5)
# 仿射操作(线性变换): y = Wx + b
# 输入维度为 16 * 5 * 5,输出维度为 120
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 其中 5*5 是来自图像处理后的空间维度
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, input):
# 卷积层 C1: 1 个输入图像通道, 6 个输出通道, 5x5 平方卷积。
# 使用 ReLU 激活函数,并输出大小为 (N, 6, 28, 28) 的张量,
# 其中 N 是批处理大小(Batch Size)。
c1 = F.relu(self.conv1(input))
# 下采样层 S2: 2x2 网格,纯函数式操作。
# 该层没有任何参数,输出一个 (N, 6, 14, 14) 的张量。
s2 = F.max_pool2d(c1, (2, 2))
# 卷积层 C3: 6 个输入通道, 16 个输出通道, 5x5 平方卷积。
# 使用 ReLU 激活函数,并输出一个 (N, 16, 10, 10) 的张量。
c3 = F.relu(self.conv2(s2))
# 下采样层 S4: 2x2 网格,纯函数式操作。
# 该层没有参数,输出一个 (N, 16, 5, 5) 的张量。
s4 = F.max_pool2d(c3, 2)
# 展平操作(Flatten): 纯函数式操作,将多维张量拉平。
# 输出一个 (N, 400) 的张量(16*5*5 = 400)。
s4 = torch.flatten(s4, 1)
# 全连接层 F5: 输入 (N, 400) 张量,
# 输出 (N, 120) 张量,使用 ReLU 激活函数。
f5 = F.relu(self.fc1(s4))
# 全连接层 F6: 输入 (N, 120) 张量,
# 输出 (N, 84) 张量,使用 ReLU 激活函数。
f6 = F.relu(self.fc2(f5))
# 全连接输出层: 输入 (N, 84) 张量,
# 输出 (N, 10) 张量(对应 10 个数字类别)。
output = self.fc3(f6)
return output
net = Net()
print(net)
你只需要定义 forward 函数,反向传播函数(即计算梯度的部分)会自动通过 autograd 为你定义。你可以在 forward 函数中使用任何 Tensor 操作。
模型的可学习的参数会由 net.parameters() 方法返回。
python
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
net.parameters(): 这是一个生成器(Generator),它会遍历模型中所有的参数。list(...): 将生成器转换为列表,方便我们通过索引(如 [0], [1])直接访问特定的参数层。
包含内容: 每一层通常包含两个参数:权重 (weight) 和 偏置 (bias)。

让我们尝试一个随机的32x32输入。注意:该网络 (LeNet) 的预期输入大小为32x32。 要在MNIST数据集上使用此网络,请将数据集中的图像调整大小为32x32。
python
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
torch.randn: 创建一个张量,其元素服从均值为 0、方差为 1 的标准正态分布。(1, 1, 32, 32): 这是张量的维度(Shape),对于处理图像的卷积神经网络,通常遵循 (N, C, H, W) 格式:- 1 (N): 批处理大小 (Batch Size)。即便只有一张图片,也需要这个维度。
- 1 ©: 通道数 (Channels)。对应 conv1 定义的输入通道(黑白图像)。
- 32, 32 (H, W): 图像的高度和宽度。LeNet-5 最初设计的输入尺寸就是 32×32。

将所有参数的梯度缓冲区清零,并使用随机梯度进行反向传播:
python
net.zero_grad()
out.backward(torch.randn(1, 10))
在进一步进行之前,让我们回顾一下你目前为止见过的所有类。
torch.Tensor- 一个支持自动求导操作(如 backward())的多维数组。同时也会保存相对于该张量的梯度。nn.Module- 神经网络模块。封装参数的便捷方式,并提供将其移动到 GPU、导出、加载等辅助功能。nn.Parameter- 一种特殊的 Tensor,当其作为属性分配给一个 Module 时,会自动注册为参数。autograd.Function- 实现自动求导操作的前向和反向定义。每个 Tensor 操作都会创建至少一个 Function 节点,该节点连接到创建 Tensor 的函数并编码其历史记录。
至此已经实现了定义神经网络、处理输入以及调用反向传播。
下面来看一下如何计算损失和更新网络的权重。
(三)损失函数
损失函数接收输入对,计算输出的预测值和真实值的之间的差距。nn包提供了若干个损失函数,其中最简单的是 nn.MSELoss ,这个函数计算的是预测值和真实值之间的均方误差。
例如:
python
# 获取模型的预测结果
output = net(input)
# 创建一个含有10个随机数的向量
target = torch.randn(10)
# 将其形状改为 [1, 10] 计算损失必须保证output和target形状一致 1表示批次,10表示特征维度
target = target.view(1, -1)
# 实例化"均方误差"准则
criterion = nn.MSELoss()
# 将预测值和目标值传入,返回一个包含损失值的张量
loss = criterion(output, target)
print(loss)
loss损失长下面这样

此时得到的 loss 是一个带有梯度信息的 Tensor。在后续的训练步骤中,会调用 loss.backward() 来根据这个误差计算梯度,进而更新网络权重。
现在,如果你沿着反向传播方向追踪损失,使用它的 .grad_fn 属性,你将会看到一个如下的计算图:
python
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> flatten -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
所以,当我们调用loss.backward()时,整个计算图会相对于神经网络的参数进行微分,并且图中所有配置了requires_grad=True的张量的.grad属性所指向的张量都会累积梯度。
为了便于说明,让我们逐步反向传播几个步骤:
python
print(loss.grad_fn) # MSELoss
print(loss.grad_fn.next_functions[0][0]) # Linear
print(loss.grad_fn.next_functions[0][0].next_functions[0][0]) # ReLU

(四)反向传播
反向传播误差需要使用 loss.backward() 方法。不过你需要清除现有的梯度,否则梯度会被累加到现有的梯度上。现在我们将调用loss.backward(),并查看反向传播前后conv1的偏置梯度。
python
net.zero_grad() # 将所有参数的梯度缓冲区清零
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
loss.backward() # 自动求导 从loss损失函数值开始,根据链式法则逆向遍历计算图,计算出的每个参数的梯度,会被存储到参数对应的.grad属性中
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

到目前为止,我们已经了解了如何使用损失函数。下面来看一下如何更新神经网络中的权重。
(五)更新权重
实践中最简单的更新规则是随机梯度下降法(SGD):
python
weight = weight - learning_rate * gradient
我们可以用简单的Python代码实现这一点:
python
learning_rate = 0.01 # 学习率
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
learning_rate = 0.01- 作用:定义学习率。它决定了模型在优化过程中沿着梯度反方向"迈出的步子"有多大。
- 意义:太大可能导致错过最优解(在谷底左右跳动),太小则导致收敛过慢。
for f in net.parameters():net.parameters():这是一个生成器,它会遍历神经网络 net 中所有需要训练的参数(包括权重weights和偏置biases)。
f:代表当前的某一个参数张量(Tensor)。f.data.sub_(f.grad.data * learning_rate)
这是最关键的一行,执行了实际的数学运算:f.grad.data:获取该参数在反向传播(loss.backward())中计算出来的梯度。梯度指明了函数值上升最快的方向。learning_rate:将梯度乘以学习率,得到更新的幅度。.sub_():这是 PyTorch 中的一个原地(in-place)减法操作(带有下划线后缀的方法通常表示直接修改原数据)。- 整体含义:将参数 f 减去"学习率 × 梯度"。因为梯度是上升方向,减去它就是向下降的方向移动,从而降低损失函数的值。
请注意,梯度缓冲区必须使用optimizer.zero_grad()手动设置为零。这是因为梯度会像反向传播部分解释的那样累积。
四、训练分类器
(一)如何处理数据
通常,处理图片、文本、音频和视频数据可以使用标准的python包,可以将数据加载成 numpy 数组。然后你可以把 npmpy 数组转化为 torch.*Tensor。
- 图像使用Pillow, OpenCV包
- 音频使用scipy 和 librosa 的包
- 对于文本,原始 Python 还是 Cython ,或者 NLTK 和 SpaCy 都可以。
特别针对视觉,我们创建了一个名为torchvision的软件包,它包含用于常见数据集(如 ImageNet、CIFAR10、MNIST 等)的数据加载器以及用于图像的数据转换器,即torchvision.datasets和torch.utils.data.DataLoader。
这提供了极大的便利,并避免重复造轮子。
下面我们将使用CIFAR10数据集。它包含以下类别:'飞机'、'汽车'、'鸟'、'猫'、'鹿'、'狗'、'青蛙'、'马'、'船'、'卡车'。CIFAR-10中的图像大小为3x32x32,即大小为32x32像素的3通道彩色图像。

(一)训练一个图像分类器
我们将按以下顺序执行步骤:
- 使用
torchvision加载并标准化 CIFAR10 训练和测试数据集。 - 定义一个卷积神经网络。
- 定义一个损失函数。
- 在训练数据上训练网络。
- 在测试数据上测试网络。
① 加载和归一化CIFAR10
使用 torchvision,可以非常容易地加载 CIFAR10 数据集。
python
import torch
import torchvision
import torchvision.transforms as transforms
torchvision 数据集输出的是范围在 [0, 1] 的 PILImage 图像。我们将它们转换为归一化范围在 [-1, 1] 的 Tensor。
python
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
batch_size = 4
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
shuffle=True, num_workers=2)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
shuffle=False, num_workers=2)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
这段代码是使用 PyTorch 进行深度学习开发时非常典型的数据准备阶段。它主要完成了数据的下载、预处理(标准化)以及加载器的构建。
可以将其分为四个核心部分来讲解:
- 图像预处理 (Transforms)
python
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
这部分定义了对原始图像进行的"流水线"操作:
transforms.ToTensor():
作用一:将 PIL 图像或 NumPy 数组转换为 FloatTensor。
作用二:将像素值从 [0,255] 归一化到 [0.0,1.0] 之间。
transforms.Normalize(...):
执行减均值、除以标准差的操作:output=(input−mean)/std。
这里传入两个元组 (0.5, 0.5, 0.5) 分别对应 RGB 三个通道。
效果:将图像像素值从 [0,1] 范围转换到 [−1,1] 范围。这有助于神经网络在训练时更快收敛。
- 定义批次大小 (Batch Size)
python
batch_size = 4
- 这表示每次网络前向传播和反向传播时,会同时处理 4 张图片。
- 在实际训练中,如果 GPU 显存很大,可以调高这个值(如 32, 64 或 128)提高处理效率。
- 加载数据集 (Datasets)
python
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,
download=True, transform=transform)
testset = torchvision.datasets.CIFAR10(root='./data', train=False,
download=True, transform=transform)
这里使用了 PyTorch 内置的 CIFAR10 数据集:
root='./data': 指定数据下载和存放的路径。train=True/False: 指定是加载训练集(50,000张)还是测试集(10,000张)。download=True: 如果本地没有数据,程序会自动从互联网下载。transform=transform: 应用我们第一步定义的预处理逻辑。
- 数据加载器 (DataLoader)
python
trainloader = torch.utils.data.DataLoader(trainset, batch_size=batch_size,
shuffle=True, num_workers=2)
testloader = torch.utils.data.DataLoader(testset, batch_size=batch_size,
shuffle=False, num_workers=2)
DataLoader 是 PyTorch 中非常强大的工具,负责实际给模型"喂"数据:
shuffle=True: 在每个训练周期(Epoch)开始时打乱数据。这对于训练非常重要,可以防止模型产生"位置依赖"或过拟合特定顺序。num_workers=2: 使用 2 个子进程来并行加载数据。这可以加快数据读取速度,防止 CPU 读取数据成为模型训练的瓶颈。
- 类别定义
python
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
CIFAR10 数据集的标签是 0-9 的整数。这个元组的作用是建立一个索引到名字的映射,方便后续我们将预测结果(数字)转换成人类可读的名称(如"猫"、"狗")。
总结
这段代码执行后,你就拥有了两个迭代器 trainloader 和 testloader。你可以直接通过循环来获取数据:
python
for images, labels in trainloader:
# images 的形状是 [4, 3, 32, 32] -> (batch_size, channels, height, width)
# labels 的内容是 4 个对应的类别索引
...

下面就可以看几个训练图像的效果
python
import matplotlib.pyplot as plt
import numpy as np
# functions to show an image
def imshow(img):
img = img / 2 + 0.5 # unnormalize
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# get some random training images
dataiter = iter(trainloader)
images, labels = next(dataiter)
# show images
imshow(torchvision.utils.make_grid(images))
# print labels
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))
这段代码是使用 PyTorch 进行深度学习(通常是处理 CIFAR-10 等数据集)时,非常典型的数据可视化步骤。它的核心作用是:从训练数据集中随机抽取一组图片,将它们拼接成一张大图展示出来,并打印出对应的类别标签。
- 定义显示函数
imshow
python
def imshow(img):
img = img / 2 + 0.5 # 反归一化 (Unnormalize)
npimg = img.numpy()
# 将 [C, H, W] 转换为 [H, W, C]
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
- 反归一化 (img / 2 + 0.5):在预处理数据时,通常会将图片像素值从 [0,1] 归一化到 [−1,1](均值为 0.5,标准差为 0.5)。为了让图片能正常显示,需要将其还原回 [0,1] 范围。
img.numpy():将 PyTorch 的 Tensor(张量)转换为 NumPy 数组,因为 Matplotlib 绘图库使用的是 NumPy。np.transpose(..., (1, 2, 0)):PyTorch 的图片格式是 [通道数 C, 高度 H, 宽度 W]。Matplotlib 的要求是 [高度 H, 宽度 W, 通道数 C]。此操作将维度顺序重新排列,以便正常显示颜色。
- 获取随机训练图像
python
dataiter = iter(trainloader)
images, labels = next(dataiter)
iter(trainloader):将trainloader(数据加载器)包装成一个迭代器。next(dataiter):从中抓取"一批"(Batch)数据。images包含了这批图片的像素数据,labels包含了这些图片对应的类别索引。
- 展示图片
python
# 将这一批图片拼接成网格
imshow(torchvision.utils.make_grid(images))
torchvision.utils.make_grid(images):这是一个方便的函数,它把 images 里的多张图片(通常是一个 batch,比如 4 张或 64 张)像贴瓷砖一样拼成一张长方形的大图。- 调用前面定义的
imshow函数进行显示。
- 打印标签
python
print(' '.join(f'{classes[labels[j]]:5s}' for j in range(batch_size)))
classes[labels[j]]:labels[j]是类别的数字索引(如 0, 1, 2),通过classes列表找到它对应的名字(如 'cat', 'dog', 'ship')。f'...:5s':格式化字符串,保证每个单词占用 5 个字符宽度,使打印出来的文字能和上面的图片对齐。' '.join(...):将这些名字用空格连接成一行字符串并打印。

② 定义卷积神经网络
复制之前"神经网络"章节中的神经网络,并对其进行修改,使其能够处理3通道图像(而不是像之前定义的那样处理1通道图像)。
python
import torch.nn as nn
import torch.nn.functional as F
class Net(nn.Module):
def __init__(self):
super().__init__()
self.conv1 = nn.Conv2d(3, 6, 5) # 定义卷积层
self.pool = nn.MaxPool2d(2, 2) # 最大池化
self.conv2 = nn.Conv2d(6, 16, 5) # 卷积层
self.fc1 = nn.Linear(16 * 5 * 5, 120) # 全连接层
self.fc2 = nn.Linear(120, 84) # 全连接层
self.fc3 = nn.Linear(84, 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 = F.relu(self.fc2(x)) # 激活函数
x = self.fc3(x) # 全连接
return x # 返回结果
net = Net()
最上面三行是定义网络的固定写法。
③ 定义损失函数和优化器
使用分类交叉熵损失和带momentum的SGD。
- 分类交叉熵损失 (Categorical Cross-Entropy Loss)
它是什么:
这是一种衡量预测概率分布与真实标签概率分布之间"距离"的函数。主要用于多分类任务(如识别手写数字 0-9)。
核心原理:
输入: 神经网络最后一层通过 Softmax 激活函数输出的概率值(所有类别概率之和为 1)。
目标: 拉近模型输出概率与真实 One-hot 标签(正确类别为1,其余为0)的距离。 - 带动量的 SGD (SGD with Momentum)
它是什么:
普通的 SGD(随机梯度下降)在更新参数时只看当前的梯度,容易在沟壑中震荡,或者陷入局部最小值。Momentum(动量) 借鉴了物理学中的惯性概念。
核心原理:
它记录了之前的更新方向。如果当前的梯度方向与之前的更新方向一致,则加大步长;如果不一致,则起到平滑作用。
python
import torch.optim as optim
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
④ 训练网络
事情开始变得有趣起来了。我们只需要遍历我们的数据迭代器,并将输入提供给网络并进行优化。
python
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
# zero the parameter gradients
optimizer.zero_grad()
# forward + backward + optimize
outputs = net(inputs)
loss = criterion(outputs, labels)
loss.backward()
optimizer.step()
# print statistics
running_loss += loss.item()
if i % 2000 == 1999: # print every 2000 mini-batches
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0
print('Finished Training')
这段代码是使用 PyTorch 框架训练神经网络的核心循环(Training Loop)。它展示了模型如何通过接触数据、计算误差并调整参数来学习。
- 外层循环:Epoch(轮次)
python
for epoch in range(2): # loop over the dataset multiple times
- Epoch 代表将整个训练数据集完整地送入神经网络训练一次的过程。
range(2)表示这段代码会将整个数据集训练 2 轮。通常在实际项目中需要更多轮次(如 10、50 或 100),直到模型收敛。
- 内层循环:Batch(批次)
python
for i, data in enumerate(trainloader, 0):
# get the inputs; data is a list of [inputs, labels]
inputs, labels = data
trainloader是一个迭代器,它将数据集切分成许多小份(称为 Mini-batches)。- 直接训练整个数据集会消耗巨大内存,因此我们分批处理。
inputs是图像或特征数据,labels 是这些数据对应的正确标签(真实答案)。
- 核心训练四部曲
这是深度学习最关键的步骤:
第一步:梯度清零
python
optimizer.zero_grad()
- 在 PyTorch 中,梯度是累加的。如果不清零,当前批次的梯度会和上一个批次的梯度叠加。为了正确计算当前批次的更新量,必须先清零。
第二步:前向传播 (Forward Pass)
python
outputs = net(inputs)
- 将数据
inputs输入模型net,得到模型的预测结果outputs。
第三步:计算损失 (Loss) 与反向传播 (Backward Pass)
python
loss = criterion(outputs, labels)
loss.backward()
criterion(损失函数)衡量模型预测值与真实标签之间的差距(误差)。loss.backward()利用链式法则计算损失函数对模型每个参数的梯度(即确定参数该往哪个方向调整,调整多少)。
第四步:更新参数 (Optimize)
python
optimizer.step()
- 优化器(如 SGD 或 Adam)根据刚才计算出的梯度,实际动手修改模型内部的参数(权重),使下一次预测更准确。
- 统计与打印
python
running_loss += loss.item()
if i % 2000 == 1999: # 每 2000 个 mini-batches 打印一次
print(f'[{epoch + 1}, {i + 1:5d}] loss: {running_loss / 2000:.3f}')
running_loss = 0.0
loss.item()获取当前批次的损失数值。- 为了观察模型是否在进步,代码每处理 2000 个批次就打印一次平均损失。
- 注意:如果损失值(loss)在不断下降,说明模型正在有效学习。

保存训练好的模型:
python
PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)
会在根目录中多出一个这个文件

- 测试网络
我们已经对训练数据集进行了两次训练迭代。但是我们需要检查网络是否真正学习到了东西。
我们将通过预测神经网络输出的类别标签,并将其与真实标签进行比对来检查这一点。如果预测正确,我们将把该样本添加到正确预测的列表中。
好的,第一步。让我们先展示测试集中的一张图像,以便熟悉一下。
python
dataiter = iter(testloader)
images, labels = next(dataiter)
# print images
imshow(torchvision.utils.make_grid(images))
print('GroundTruth: ', ' '.join(f'{classes[labels[j]]:5s}' for j in range(4)))

接下来,让我们重新加载我们保存的模型(注意:在这里保存和重新加载模型并不是必要的,这样做只是为了演示如何操作):
python
net = Net()
net.load_state_dict(torch.load(PATH, weights_only=True))
好的,现在让我们看看神经网络认为以上这些例子是什么:
python
outputs = net(images)
输出是10个类别的能量值。一个类别的能量值越高,网络就越认为图像属于该特定类别。所以,让我们获取能量最高的索引:
python
_, predicted = torch.max(outputs, 1)
print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
for j in range(4)))

下面的内容是在GPU上训练,由于俺是小辣鸡macbook就不操作了。
这一篇可太硬核了,虽然说感觉很多云里雾里,但只是不熟悉,多来几遍就好了。B站上炮哥讲的pytorch挺好的,推荐,结合炮哥的视频会更好理解原理。