PyTorch
PyTorch 简介
PyTorch 是一个开源的机器学习库,它具有强大的 GPU 加速功能,方便用户进行深度学习模型的构建和优化,被广泛应用于计算机视觉、自然语言处理等领域。
- Pytorch 是一个基于 Numpy 的科学计算包,向它的使用者提供了两大功能。
- 作为 Numpy 的替代者,向用户提供使用 GPU 强大功能的能力。
- 做为一款深度学习的平台,向用户提供最大的灵活性和速度。
Pytorch 基本语法
学习目标
- 掌握 Pytorch 的基本元素操作。
- 掌握 Pytorch 的基本运算操作。
Pytorch 的基本元素操作
张量
import torch
创建张量
- 创建一个没有初始化的矩阵:
x = torch.empty(5, 3)
print(x)
tensor([[-8.6500e+17, 1.4868e-42, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00],
[ 0.0000e+00, 0.0000e+00, 0.0000e+00]])
当声明一个未初始化的矩阵时,它本身不包含任何确切的值。当创建一个未初始化的矩阵时,分配给矩阵的内存中有什么数值就赋值给了这个矩阵,本质上是毫无意义的数据。
- 创建一个有初始化的矩阵:
x = torch.rand(5, 3)
print(x)
tensor([[0.6634, 0.5685, 0.0430],
[0.0120, 0.4612, 0.3454],
[0.2519, 0.9060, 0.3871],
[0.1392, 0.5882, 0.7890],
[0.0424, 0.9305, 0.7548]])
torch.rand(5, 3)
:
- 生成的是 均匀分布 的随机数。
- 这些随机数的取值范围是 [0, 1),即大于等于 0 且小于 1。
- 所有数值在这个区间内的出现概率是相等的。
torch.randn(5, 3)
:
- 生成的是 标准正态分布(高斯分布) 的随机数。
- 这些随机数的 均值为 0,标准差为 1。
- 大部分数值会集中在 0 附近,离 0 越远的数值出现概率越低。
- 创建一个全零矩阵并可指定数据元素的类型为
long
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
tensor([[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.],
[0., 0., 0.]])
- 直接通过数据创建张量
x5=torch.tensor([2.5,3.5])
print(x5)
tensor([2.5000, 3.5000])
- 通过已有的张量,创建一个相同尺寸的新张量
# 利用news_methods方法得到一个张量
x = x4.new_ones(5, 3, dtype=torch.double)
print(x)
# 利用randn_1ike方法得到相同张量尺寸的一个新张量,并且采用随机初始化来对其赋值
y = torch.randn_like(x, dtype=torch.float)
print(y)
new_ones()
是 PyTorch创建张量的一个方法,它的主要用途是:
- 保持与原张量相同的设备(如 CPU 或 GPU)
- 保持与原张量相同的数据类型(除非显式指定其他类型)
- 快速创建具有相同尺寸的新张量
例如,如果你有一个在 GPU 上的张量,使用new_ones()
创建的新张量也会自动在 GPU 上,无需手动指定设备。
randn_like()
的作用
- 保持形状一致性:新张量的维度与输入张量完全相同。
- 随机初始化:使用正态分布随机填充元素值,适合需要随机权重初始化的场景(如神经网络)。
- 灵活的数据类型控制:可以通过
dtype
参数指定新张量的数据类型(如torch.float32
、torch.double
等)。
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]], dtype=torch.float64)
tensor([[ 0.3335, 0.1902, 0.4215],
[ 1.8911, 0.3185, -1.3238],
[-0.0186, -0.3717, 0.3835],
[-2.3738, 0.7639, -0.4399],
[-2.1564, -1.6112, -0.5454]])
- 得到张量的尺寸:
print(x.size())#torch.Size([5, 3])
print(x.shape)#torch.Size([5, 3])
torch.Size
函数本质上返回的是一个tuple
,因此它支持一切元组的操作。
基本运算
- 加法操作:
x=torch.zeros(5,3)
y=torch.ones(5,3)
z=x+y
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
- 第二种加法方式:
print(torch.add(x, y))
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
- 第三种加法方式:
out=torch.empty(5,3)
w=torch.add(x,y,out=out)
out
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
- 第四种加法方式:
in-place
(原地置换)
y.add_(x)
print(y)
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
注意:
所有in-place
的操作函数都有一个下划线的后缀。 比如x.copy_(y)
,x.add_(y)
,都会直接改变x
的值。
切片、变形、获取数据、转换
- 用类似于 Numpy 的方式对张量进行切片操作:
print(x[:, 1])
tensor([0., 0., 0., 0., 0.])
- 改变张量的形状:
torch.view()
x = torch.randn(4, 4)
# tensor.view()操作需要保证数据元素的总数量不变
y = x.view(16)
# -1代表自动匹配个数
z = x.view(-1, 8)
print(x.size(), y.size(), z.size())
torch.Size([4, 4]) torch.Size([16]) torch.Size([2, 8])
- 如果张量中只有一个元素,可以用item()将值取出,作为一个python number。
import torch
x = torch.randn(1)
y=x.item()
print(x)
print(y)
tensor([0.1349])
0.13487519323825836
- 将 Torch Tensor 转换为 Numpy array
关于Torch Tensor和Numpy array之间的相互转换
Torhc Tensor和Numpy array共享底层的内存空间,因此改变其中一个的值,另一个也会随之被改变
a = torch.ones(5)
print(a)
tensor([1., 1., 1., 1., 1.])
b = a.numpy()
print(b)
[1. 1. 1. 1. 1.]
- 对其中一个进行加法操作,另一个也随之被改变:
a.add_(1)
print(a)
print(b)
tensor([2., 2., 2., 2., 2.])
[2. 2. 2. 2. 2.]
- 将 Numpy array 转换为 Torch Tensor:
import numpy as np
a = np.ones(5)
b = torch.from_numpy(a)
np.add(a, 1, out=a)
print(a)
print(b)
[2. 2. 2. 2. 2.]
tensor([2., 2., 2., 2., 2.], dtype=torch.float64)
注意:
所有在CPU上的Tensors,除了CharTensor,都可以转换为Numpy array并可以反向转换import numpy as np a=np.array([1,2,3]) b=torch.tensor(a) c=torch.from_numpy(a) np.add(a, 1, out=a) print(a,'\n',b,'\n',c)
troch.from_numpy()和torch.tensor()存在的区别就是troch.from_numpy()转换前后的变量是共享内存的,修改一个,另一个也会变。但是torch.tensor()就不会。
[2 3 4] tensor([1, 2, 3], dtype=torch.int32) tensor([2, 3, 4], dtype=torch.int32)
- 关于Cuda Tensor: Tensors可以用.to()方法来将其移动到任意设备上:
# 如果服务器上已经安装了GPU和CUDA
if torch.cuda.is_available(): # 定义一个设备对象,这里指定成CUDA,即使用GPU
device = torch.device("cuda") # 直接在GPU上创建一个Tensor
y = torch.ones_like(x, device=device) # 将在CPU上面的x张量移动到GPU上面
x = x.to(device)
# x和y都在GPU上面,才能支持加法运算
Z = x + y
# 此处的张量z在GPU上面
print(z)
# 也可以将z转移到CPU上面,并同时指定张量元素的数据类型
print(z.to("cpu",torch.double))
输出结果:
tensor([0.6469],device='cuda:0')
tensor([0.6469],dtype=torch.float64)
小节总结
- 学习了什么是 Pytorch。
- Pytorch 是一个基于 Numpy 的科学计算包,作为 Numpy 的替代者,向用户提供使用 GPU 强大功能的能力。做为一款深度学习的平台,向用户提供最大的灵活性和速度。
- 学习了 Pytorch 的基本元素操作。
- 矩阵的初始化:
torch.empty()
torch.rand(n, m)
torch.zeros(n, m, dtype=torch.long)
- 其他若干操作:
x.new_ones(n, m, dtype=torch.double)
torch.randn_like(x, dtype=torch.float)
x.size()
- 矩阵的初始化:
- 学习了 Pytorch 的基本运算操作。
- 加法操作:
x + y
torch.add(x, y)
torch.add(x, y, out=result)
y.add_(x)
- 其他若干操作:
x[:, 1]
torch.view()
x.item()
- 加法操作:
- 学习了 Torch Tensor 和 Numpy Array 之间的相互转换。
- 将 Torch Tensor 转换为 Numpy Array:
b = a.numpy()
- 将 Numpy Array 转换为 Torch Tensor:
b = torch.from_numpy(a)
- 注意:所有在 CPU 上的 Tensor,除了
CharTensor
,都可以转换为 Numpy Array 并可以反向转换。
- 将 Torch Tensor 转换为 Numpy Array:
- 学习了任意的 Tensors 可以用
.to()
方法来将其移动到任意设备上。x = x.to(device)
Pytorch 中的 autograd
学习目标
- 掌握自动求导中的 Tensor 概念和操作。
- 掌握自动求导中的梯度 Gradients 概念和操作。
梯度追踪
- torch.Tensor 是整个 package 中的核心类,如果将属性.requires_grad 设置为 True,它将追踪在这个类上定义的所有操作。当代码要进行反向传播的时候,直接调用.backward () 就可以自动计算所有的梯度。在这个 Tensor 上的所有梯度将被累加进属性.grad 中。
梯度追踪:启用
.requires_grad=True
后,PyTorch会记录在该张量上执行的所有操作,以构建一个计算图(computational graph)。这个计算图在反向传播过程中用于计算梯度。自动求导:当调用
.backward()
方法时,PyTorch会自动计算该张量相对于某些标量值(通常是损失函数)的梯度,并将梯度存储在.grad
属性中。避免追踪非训练参数:对于不需要训练的参数(例如,输入数据、冻结的层),可以设置
requires_grad=False
(默认值)以减少计算图构建的开销。
- 分离计算图:如果想终止一个 Tensor 在计算图中的追踪回溯,只需要执行.detach () 就可以将该 Tensor 从计算图中撤下,在未来的回溯计算中也不会再计算该 Tensor。
- 除了.detach (),如果想终止对计算图的回溯,也就是不再进行方向传播求导数的过程,也可以采用代码块的方式
with torch.no_grad():
,这种方式非常适用于对模型进行预测的时候,因为预测阶段不再需要对梯度进行计算。
x1 = torch.ones(3, 3)
print(x1)
x = torch.ones(2, 2, requires_grad=True)
print(x)
tensor([[1., 1., 1.],
[1., 1., 1.],
[1., 1., 1.]])
tensor([[1., 1.],
[1., 1.]], requires_grad=True)
torch.Function
torch.Function
是与 Tensor
同等重要的核心类,它们共同构成PyTorch的自动微分系统:
-
-
- Tensor:存储数据和梯度信息
- Function:记录在Tensor上执行的操作(前向计算+反向传播规则)
-
- Function类是和Tensor类同等重要的一个核心类,它和Tensor共同构建了一个完整的自动微分系统,每一个Tensor拥有一个.grad_fn属性,代表引用了哪个具体的Function创建了该Tensor.
- 如果某个张量Tensor是用户自定义的,则其对应的grad_fn is None.
核心关系图:
用户自定义Tensor (grad_fn=None)
│
▼
操作(如乘法)
│
▼
生成新Tensor (grad_fn=MulBackward0)
│
▼
计算图节点
│
▼
反向传播时计算梯度
实际工作流程
-
前向传播:
# 自定义操作(自动创建Function实例)
x = torch.tensor([2.0], requires_grad=True)
y = x.exp() # 实际调用:ExpBackward.apply(x)
2.反向传播:
y.backward()
# 实际调用:
# 1. ExpBackward.backward(ctx, grad_output)
# 2. 计算x的梯度:grad_x = grad_output * y
requires_grad=True
的 Tensor 上执行一个加法操作:y = x + 2
print(y)
tensor([4.], grad_fn=<AddBackward0>)
grad_fn
属性:print(x.grad_fn)
print(y.grad_fn)
None
<AddBackward0 object at 0x10db11208>
z = y * y * 3
out = z.mean()
print(z, out)
tensor([[27., 27.],
[27., 27.]], grad_fn=<MulBackward0>) tensor(27., grad_fn=<MeanBackward0>)
.requires_grad_()
:该方法可以原地改变 Tensor 的属性.requires_grad
的值。如果没有主动设定默认为 False。a = torch.randn(2, 2)
a = ((a * 3) / (a - 1))
print(a.requires_grad)
a.requires_grad_(True)
print(a.requires_grad)
b = (a * a).sum()
print(b.grad_fn)
False
True
<SumBackward0 object at 0x7f191afd6be0>
反向传播-计算梯度
.backward()
实现的。x = torch.ones(2, 2, requires_grad=True)
y = x + 2
z = y * y * 3
out = z.mean()
out.backward()
print(x.grad)
tensor([[4.5000, 4.5000],
[4.5000, 4.5000]])
注意:这里有四个梯度值,那是因为最初的x有四个元素值[x11,x12,x21,x22]
requires_grad=True
来执行自动求导,也可以通过代码块的限制来停止自动求导。print(x.requires_grad)
print((x ** 2).requires_grad)
with torch.no_grad():
print((x ** 2).requires_grad)
True
True
False
.detach()
获得一个新的 Tensor,拥有相同的内容但不需要自动求导。print(x.requires_grad)#True
y = x.detach()
print(y.requires_grad)#False
print(x.eq(y).all())#tensor(True)
总结要点
-
计算图构建:
- 每个非叶子节点Tensor的
.grad_fn
指向创建它的Function
Function
对象形成计算图的节点
- 每个非叶子节点Tensor的
-
反向传播机制:
- 调用
.backward()
时,从目标Tensor的.grad_fn
开始 - 链式调用每个
Function
的backward()
方法
- 调用
-
用户自定义:
- 叶子节点Tensor(用户直接创建)的
.grad_fn=None
- 通过继承
Function
可创建自定义微分规则
- 叶子节点Tensor(用户直接创建)的
-
性能优化:
- 合理使用
.detach()
避免不必要的梯度计算 - 对不需要梯度的Tensor设置
requires_grad=False
- 合理使用
关键理解:PyTorch的动态计算图实际上是由
Tensor
和Function
对象组成的双向链表结构,前者存储数据,后者存储操作历史和微分规则。
小节总结
- 学习了
torch.Tensor
类的相关概念:torch.Tensor
是整个 package 中的核心类,若将属性.requires_grad
设为True
,它会追踪自身上定义的所有操作。执行反向传播时,调用.backward()
可自动计算梯度,梯度会累加进.grad
属性。- 执行
.detach()
,能把该Tensor
从计算图撤下,后续回溯计算不再涉及它。 - 用代码块
with torch.no_grad():
,也可终止计算图回溯,常用于模型预测(无需算梯度场景 )。
- 学习了关于
Tensor
的若干操作:torch.ones(n, n, requires_grad=True)
:创建指定尺寸、requires_grad
属性为True
的全 1 张量。x.grad_fn
:查看张量相关的梯度函数,反映张量的运算来源。a.requires_grad_(True)
:原地修改张量的requires_grad
属性为True
。
- 学习了关于
Gradients
的属性:x.grad
:存储张量相关梯度,反向传播后可查看。- 借助
.detach()
能得到新张量,内容与原张量相同,但无需自动求导 。
使用 Pytorch 构建一个神经网络
学习目标
- 掌握用 Pytorch 构建神经网络的基本流程。
- 掌握用 Pytorch 构建神经网络的实现过程。
torch.nn
- 使用 Pytorch 来构建神经网络,主要的工具都在
torch.nn
包中。 nn
依赖于autograd
来定义模型,并对其自动求导。
构建神经网络的典型流程
- 定义一个拥有可学习参数的神经网络
- 遍历训练数据集
- 处理输入数据使其流经神经网络
- 计算损失值
- 将网络参数的梯度进行反向传播
- 以一定的规则更新网络的权重
- 定义神经网络
# 定义一个简单的网络类
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 定义第一层卷积神经网络,输入通道维度=1,输出通道维度=6,卷积核大小3*3
self.conv1 = nn.Conv2d(1, 6, 3)
# 定义第二层卷积神经网络,输入通道维度=6,输出通道维度=16,卷积核大小3*3
self.conv2 = nn.Conv2d(6, 16, 3)
# 定义三层全连接网络
self.fc1 = nn.Linear(16 * 6 * 6, 120)
self.fc2 = nn.Linear(120, 84)
self.fc3 = nn.Linear(84, 10)
def forward(self, x):
# 在(2, 2)的池化窗口下执行最大池化操作
x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))
x = F.max_pool2d(F.relu(self.conv2(x)), 2)
#
x = x.view(-1, self.num_flat_features(x))
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
def num_flat_features(self, x):
# 获取张量 x 除了第0维(batch_size)之外的所有维度大小
# 例如,若 x 的形状是 [10, 16, 6, 6],则 size = [16, 6, 6]
size = x.size()[1:]
# 初始化特征总数
num_features = 1
# 遍历每个维度的大小并累乘
for s in size:
num_features *= s
# 返回展平后的特征总数(如 16*6*6=576)
return num_features
net = Net()
print(net)
num_flat_features 方法的作用是计算张量在 “展平”(flatten)操作后会得到的特征数量在卷积神经网络(CNN)中,数据需要从卷积层的多维张量(如[batch, channels, height, width]
)转换为全连接层所需的一维向量。这个方法就是用来计算转换后的一维向量长度的。具体来说:
- 输入:一个多维张量
x
,通常形状为[batch_size, channels, height, width]
- 输出:除了第 0 维(batch_size)之外的所有维度的元素数量乘积,即展平后的特征总数
Net(
(conv1): Conv2d(1, 6, kernel_size=(3, 3), stride=(1, 1))
(conv2): Conv2d(6, 16, kernel_size=(3, 3), stride=(1, 1))
(fc1): Linear(in_features=576, out_features=120, bias=True)
(fc2): Linear(in_features=120, out_features=84, bias=True)
(fc3): Linear(in_features=84, out_features=10, bias=True)
)
- 查看网络参数:
模型中所有的可训练参数,都可以通过net.parameters()来获得。
params = list(net.parameters())
print(len(params))
print(params[0].size())
10
torch.Size([6, 1, 3, 3])
- 模型输入
注意:
torch.nn
构建的神经网络只支持 mini-batches 的输入,不支持单一样本的输入。- 比如:
nn.Conv2d
需要一个 4D Tensor,形状为(nSamples, nChannels, Height, Width)
。如果你的输入只有单一样本形式,则需要执行input.unsqueeze(0)
,主动将 3D Tensor 扩充成 4D Tensor。
例如图像的输入尺寸为 32*32:
input = torch.randn(1, 1, 32, 32)
output = net(input)
print(output)
tensor([[-0.0602, 0.0468, 0.0291, -0.0197, 0.0039, -0.0759, 0.0545, -0.0386, -0.0700, -0.0408]], grad_fn=<AddmmBackward0>)
net.zero_grad()#梯度清零
output.backward(torch.randn(1, 10))#反向传播计算梯度
1.
net.zero_grad()
的作用在 PyTorch 中,每次调用backward()
时,梯度会累积到已有梯度上(而不是替换)。这在某些场景下是有用的(如梯度累积),但在大多数标准训练迭代中,我们需要在每次反向传播前清空梯度。具体操作:
net.zero_grad()
会将模型中所有可训练参数(如权重和偏置)的梯度缓冲区(即.grad
属性)设置为零。- 等价于手动遍历每个参数并调用
param.grad.zero_()
。为什么需要清零?
- 如果不清零,梯度会不断累加,导致参数更新异常,模型无法收敛。
2.
out.backward(torch.randn(1, 10))
的作用backward()
方法用于执行反向传播,计算损失函数关于每个参数的梯度。注意:
在实际训练中,通常会先计算损失函数(如loss = criterion(out, target)
),然后调用loss.backward()
。此时无需传入参数,因为损失函数的输出是标量。
损失函数
(output, target)
,然后计算出一个数值来评估output
和target
之间的差距大小。torch.nn
中有若干不同的损失函数可供使用,比如nn.MSELoss
就是通过计算均方差损失来评估输入和目标值之间的差距。nn.MSELoss
计算损失的一个例子:output = net(input)
target = torch.randn(10)
# 改变target的形状为二维张量,为了和output匹配
target = target.view(1, -1)
criterion = nn.MSELoss()
loss = criterion(output, target)
print(loss)
tensor(1.1562, grad_fn=<MseLossBackward>)
反向传播
loss.backward()
。loss
反向传播的方向,使用.grad_fn
属性打印,将可以看到一张完整的计算图如下:input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> view -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss
loss.backward()
时,整张计算图将对loss
进行自动求导,所有属性requires_grad=True
的 Tensors 都将参与梯度求导的运算,并将梯度累加到 Tensors 中的.grad
属性中。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
<MseLossBackward object at 0x7fdba3216da0>
<AddmmBackward object at 0x7fdba3216f28>
<AccumulateGrad object at 0x7fdba3216f28>
# Pytorch中执行梯度清零的代码
net.zero_grad()
print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)
# Pytorch中执行反向传播的代码
loss.backward()
print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
conv1.bias.grad before backward
tensor([0., 0., 0., 0., 0., 0.])#新版本显示为None
conv1.bias.grad after backward
tensor([-0.0002, 0.0045, 0.0017, -0.0099, 0.0092, -0.0044])
更新参数
weight = weight - learning_rate * gradient
learning_rate = 0.01
#传统代码需要循环遍历每个参数,逐个更新
for f in net.parameters():
f.data.sub_(f.grad.data * learning_rate)
import torch.optim as optim
# 通过optim创建优化器对象
optimizer = optim.SGD(net.parameters(), lr=0.01)
# 预测值
output = net(input)
# 真实值
target = torch.randn(10)
# 改变target的形状为二维张量,为了和output匹配
target = target.view(1, -1)
# 指定损失函数
criterion = nn.MSELoss()
# 计算损失
loss = criterion(output, target)
# 将优化器执行梯度清零的操作
optimizer.zero_grad()
# 对损失值执行反向传播的操作
loss.backward()
# 参数的更新通过一行标准代码来执行
optimizer.step()
小节总结
- 学习了构建一个神经网络的典型流程:
- 定义一个拥有可学习参数的神经网络
- 遍历训练数据集
- 处理输入数据使其流经神经网络
- 计算损失值
- 将网络参数的梯度进行反向传播
- 以一定的规则更新网络的权重
- 学习了损失函数的定义:
- 采用
torch.nn.MSELoss()
计算均方误差。 - 通过
loss.backward()
进行反向传播计算时,整张计算图将对loss
进行自动求导,所有属性requires_grad=True
的 Tensors 都将参与梯度求导的运算,并将梯度累加到 Tensors 中的.grad
属性中。
- 采用
- 学习了反向传播的计算方法:
- 在 Pytorch 中执行反向传播非常简便,全部的操作就是
loss.backward()
。 - 在执行反向传播之前,要先将梯度清零,否则梯度会在不同的批次数据之间被累加。
net.zero_grad()
loss.backward()
- 在 Pytorch 中执行反向传播非常简便,全部的操作就是
- 学习了参数的更新方法:
- 定义优化器来执行参数的优化与更新,如使用
optim.SGD
优化器,通过optimizer.step()
实现参数更新。
- 定义优化器来执行参数的优化与更新,如使用
使用 Pytorch 构建一个分类器
学习目标
- 了解分类器的任务和数据样式
- 掌握如何用 Pytorch 实现一个分类器
分类器任务
- 构造一个将不同图像进行分类的神经网络分类器,对输入的图片进行判别并完成分类。
- 本案例采用 CIFAR10 数据集作为原始图片数据。
数据集介绍(CIFAR10)
- CIFAR-10数据集5万张训练图像、1万张测试图像、10个类别、每个类别有6k个图像,数据集中每张图片的尺寸是 3×32×32,代表彩色 3 通道。
- 总共有 10 种不同的分类,分别是 "airplane","automobile","bird","cat","deer","dog","frog","horse","ship","truck"。下图列举了10个类,每一类随机展示了10张图片:
数据集加载与预处理
下载数据集并对图片进行调整,因为torchvision
数据集的输出是PILImage
格式,数据域在[0, 1]
。我们将其转换为标准数据域[-1, 1]
的张量格式。
# 导入torchvision来下载数据集
import torchvision
# 导入torchvision.transforms 来格式化数据集
import torchvision.transforms as transforms
# 将数据转换为标准数据域[-1, 1]的张量格式
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 加载训练数据train=True
trainset = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform)
#将训练数据集 trainset 封装成一个可迭代的数据加载器 trainloader,以便在训练模型时高效地获取批量数据。
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,shuffle=True, num_workers=2)
# 加载测试数据train=False
testset = torchvision.datasets.CIFAR10(root='./data', train=False,download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4,shuffle=False, num_workers=2)
# 定义所有目标分类列表
classes = ('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck')
在 PyTorch 中,
DataLoader
是一个数据加载器,用于批量处理、打乱数据并提供多进程加载功能。具体功能解析
批量处理数据(batch_size=4
)
- 每次从
trainset
中加载 4 个样本 组成一个批次(batch)。- 神经网络通常以批次为单位进行训练,这样可以利用 GPU 的并行计算能力,提高训练效率。
随机打乱数据(shuffle=True
)
- 每个训练周期(epoch)开始时,数据会被重新打乱。
- 这有助于模型学习到数据的统计特性,避免因数据顺序导致的学习偏差,提高泛化能力。
多进程数据加载(num_workers=2
)
- 使用 2 个工作进程 并行加载数据。
- 多进程可以加速数据加载过程,特别是当数据预处理(如图像增强)耗时较长时,避免 CPU 成为训练瓶颈。
参数补充说明
batch_size
:批次大小,通常根据 GPU 内存和模型复杂度调整(如 8, 16, 32 等)。shuffle
:
True
:训练时必须打乱数据。False
:验证 / 测试时通常不打乱,方便结果复现。num_workers
:
- 建议设置为 CPU 核心数的一半左右(如 4 核 CPU 设为 2)。
- Windows 系统可能需要在
if __name__ == '__main__':
下使用,避免多进程错误。总结
DataLoader
是 PyTorch 数据处理的核心组件,通过批量、并行和随机化,显著提升训练效率和模型性能。这行代码的作用是将原始数据集转换为适合模型训练的数据流。
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data/cifar
Extracting ./data/cifar-10-python.tar.gz to ./data
Files already downloaded and verified
torch.utils.data.DataLoader()
中的num_workers
设置为 0。展示训练集的图片
# 导入画图包和numpy
import matplotlib.pyplot as plt
import numpy as np
# 构建展示图片的函数
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 从数据加载器 trainloader 中获取一个批次(batch)的训练数据,是深度学习训练过程中的标准数据获取方式。
dataiter = iter(trainloader)#将数据加载器 trainloader 转换为迭代器(iterator),以便逐个获取数据批次。iter() 是 Python 内置函数,用于将可迭代对象(如列表、字典、DataLoader)转换为迭代器。迭代器的特点是可以使用 next() 方法逐个获取元素。
# 从 trainloader 中加载一个批次的数据(4 张图像及其标签,因为 batch_size=4)
images, labels = next(dataiter)#返回一个元组 (images, labels)
# images:形状为 [4, 3, 32, 32] 的张量(假设数据集是 CIFAR-10,包含 4 张 3 通道的 32×32 图像)。
#labels:形状为 [4] 的张量,表示 4 张图像的类别索引(如 [3, 8, 2, 5])。
# 展示图片
imshow(torchvision.utils.make_grid(images))
# 打印标签label
print(''.join('%5s' % classes[labels[j]] for j in range(4)))
npimg = img.numpy()
:
将 PyTorch 张量(Tensor)转换为 NumPy 数组。np.transpose(npimg, (1, 2, 0))
:
调整图像通道顺序。PyTorch 中图像格式为 [通道,高度,宽度](即[C, H, W]
),而 Matplotlib 要求格式为 [高度,宽度,通道](即[H, W, C]
)。(1, 2, 0)
表示将原数组的第 1 维(高度)作为新数组的第 0 维,原第 2 维(宽度)作为新第 1 维,原第 0 维(通道)作为新第 2 维。torchvision.utils.make_grid(images)
:
将一批图像(如images
形状为[4, 3, 32, 32]
,表示 4 张 3 通道的 32×32 图像)拼接成一个大网格图像。
默认参数会自动在图像间添加空格,并返回形状为[3, H, W]
的张量(单张拼接图)。
print(''.join('%5s' % classes[labels[j]] for j in range(4)))
功能
打印图像对应的真实标签名称。详细解释
labels
:
一个包含 4 个元素的张量(如[3, 8, 2, 5]
),表示当前批次中 4 张图像的类别索引。classes[labels[j]]
:
通过索引labels[j]
从类别列表classes
中获取对应的类别名称。例如:classes = ['plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck'] classes[3] # 输出 'cat'
'%5s' % classes[labels[j]]
:
格式化字符串,确保每个类别名称占 5 个字符宽度(右对齐,不足补空格)。''.join(...)
:
将 4 个类别名称拼接成一个字符串并打印。
bird truck cat cat
定义卷积神经网络
Net
类,包含卷积层、池化层和全连接层等结构)class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 定义第一层卷积神经网络,输入通道维度=3,输出通道维度=6,卷积核大小5*5
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
# 定义第二层卷积神经网络,输入通道维度=6,输出通道维度=16,卷积核大小5*5
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):
# 在(2, 2)的池化窗口下执行最大池化操作
x = self.pool(F.relu(self.conv1(x)), (2, 2))
x = self.pool(F.relu(self.conv2(x)), 2)
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
print(net)
定义损失函数和优化器
import torch.optim as optim
#使用交叉熵损失函数(多分类常用)
criterion = nn.CrossEntropyLoss()
#使用随机梯度下降优化方法
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
在训练集上训练模型
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0):#enumerate(...):Python 内置函数,用于同时获取可迭代对象的索引和元素。
# data中包含输入图像张量inputs,标签张量labels
inputs, labels = data
# 首先将优化器梯度归零
optimizer.zero_grad()
# 输入图像张量进网络,得到输出张量outputs
outputs = net(inputs)
# 利用网络的输出outputs和标签labels计算损失值
loss = criterion(outputs, labels)
# 反向传播+参数更新,是标准代码的标准流程
loss.backward()
optimizer.step()
# 打印轮次和损失值
running_loss += loss.item()
if (i + 1) % 2000 == 0:
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000))##由于是2000个批次(每次4张)的梯度累积,所以除以2000
running_loss = 0.0
print('Finished Training')
模型保存
# 首先设定模型的保存路径
PATH='./cifar_net.pth'
# 保存模型的状态字典
torch.save(net.state_dict(),PATH)
注意:模型保存是将模型
net
的参数(状态字典)保存到指定路径PATH
。
net.state_dict()
:state_dict
是 PyTorch 中用于存储模型可学习参数(权重weight
和偏置bias
)的字典对象。
例如,对于之前定义的Net
模型,state_dict
会包含类似以下的键值对:{ 'conv1.weight': 卷积层1的权重张量, 'conv1.bias': 卷积层1的偏置张量, 'conv2.weight': 卷积层2的权重张量, ..., 'fc3.bias': 全连接层3的偏置张量 }
它只保存参数,不包含模型结构本身。torch.save(...)
:
PyTorch 的保存函数,将state_dict
字典序列化(转换为二进制数据)并写入PATH
指定的文件中。
在测试集上测试模型
查看测试集部分样本的真实标签
# 在测试集上测试模型
dataiter = iter(testloader)
images, labels = next(dataiter)
# 展示图片
imshow(torchvision.utils.make_grid(images))
# 打印真实标签
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))

GroundTruth: cat ship ship plane
加载模型并对测试图片进行预测
# 首先实例化模型的类对象
net = Net()
# 加载训练阶段保存好的模型的状态字典
PATH = './cifar_net.pth' # 假设模型保存路径为该地址
net.load_state_dict(torch.load(PATH))
# 利用模型对图片进行预测
outputs = net(images)
# 共有10个类别,采用模型计算出的概率最大的作为预测的类别
_, predicted = torch.max(outputs, 1)
# 打印预测标签的结果
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
Predicted: cat ship ship plane
计算模型在全部测试集上的准确率
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
分类别计算准确率
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
for i in range(10):
print('Accuracy of %5s : %2d %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))
在 GPU 上训练模型
定义设备
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
cuda:0
将模型和数据转移到 GPU
# 将模型转移到GPU上
net.to(device)
# 在训练循环中,将输入的图片张量和标签张量转移到GPU上
inputs, labels = data[0].to(device), data[1].to(device)
分类器完整代码示例
# 导入若干工具包
import torch
import torch.nn as nn
import torch.nn.functional as F
# 导入torchvision来下载数据集
import torchvision
# 导入torchvision.transforms 来格式化数据集
import torchvision.transforms as transforms
# 查看数据
# 导入画图包和numpy
import matplotlib.pyplot as plt
import numpy as np
import torch.optim as optim
def main():
# 定义卷积神经网络类
class Net(nn.Module):
def __init__(self):
super(Net, self).__init__()
# 定义第一层卷积神经网络,输入通道维度=3,输出通道维度=6,卷积核大小3*3
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2, 2)
# 定义第二层卷积神经网络,输入通道维度=6,输出通道维度=16,卷积核大小3*3
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):
# 在(2, 2)的池化窗口下执行最大池化操作
x = self.pool(F.relu(self.conv1(x)))
x = self.pool(F.relu(self.conv2(x)))
x = x.view(-1, 16 * 5 * 5)
x = F.relu(self.fc1(x))
x = F.relu(self.fc2(x))
x = self.fc3(x)
return x
net = Net()
print(net)
# 获取数据
# 将数据转换为标准数据域[-1, 1]的张量格式
transform = transforms.Compose(
[transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])
# 加载训练数据train=True
trainset = torchvision.datasets.CIFAR10(root='./data', train=True, download=True, transform=transform)
# 将训练数据集 trainset 封装成一个可迭代的数据加载器 trainloader,以便在训练模型时高效地获取批量数据。
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4, shuffle=True, num_workers=2)
# 加载测试数据train=False
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=4, shuffle=False, num_workers=2)
# 定义所有目标分类列表
classes = ('plane', 'car', 'bird', 'cat', 'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
# 构建展示图片的函数
def imshow(img):
img = img / 2 + 0.5
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
plt.show()
# 从数据加载器 trainloader 中获取一个批次(batch)的训练数据,是深度学习训练过程中的标准数据获取方式。
dataiter = iter(
trainloader) # 将数据加载器 trainloader 转换为迭代器(iterator),以便逐个获取数据批次。iter() 是 Python 内置函数,用于将可迭代对象(如列表、字典、DataLoader)转换为迭代器。迭代器的特点是可以使用 next() 方法逐个获取元素。
# 从 trainloader 中加载一个批次的数据(4 张图像及其标签,因为 batch_size=4)
images, labels = next(dataiter) # 返回一个元组 (images, labels)
# images:形状为 [4, 3, 32, 32] 的张量(假设数据集是 CIFAR-10,包含 4 张 3 通道的 32×32 图像)。
# labels:形状为 [4] 的张量,表示 4 张图像的类别索引(如 [3, 8, 2, 5])。
#
# 展示图片
imshow(torchvision.utils.make_grid(images))
# 打印标签label
print(''.join('%5s' % classes[labels[j]] for j in range(4)))
# 定义损失函数和优化器
criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 在 GPU 上训练模型
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
print(device)
# 将模型转移到GPU上
net.to(device)
# 验证模型是否在GPU上
print(next(net.parameters()).device) # 应该输出 "cuda:0"
# 在训练集上训练模型
for epoch in range(2): # loop over the dataset multiple times
running_loss = 0.0
for i, data in enumerate(trainloader, 0): # enumerate(...):Python 内置函数,用于同时获取可迭代对象的索引和元素。
# data中包含输入图像张量inputs,标签张量labels
inputs, labels = data
# 在训练循环中,将输入的图片张量和标签张量转移到GPU上
# 更清晰的写法:直接解包后转移
inputs, labels = inputs.to(device), labels.to(device)
# 首先将优化器梯度归零
optimizer.zero_grad()
# 输入图像张量进网络,得到输出张量outputs
outputs = net(inputs)
# 利用网络的输出outputs和标签labels计算损失值
loss = criterion(outputs, labels)
# 反向传播+参数更新,是标准代码的标准流程
loss.backward()
optimizer.step()
# 打印轮次和损失值
running_loss += loss.item()
if (i + 1) % 2000 == 0:
print('[%d, %5d] loss: %.3f' %
(epoch + 1, i + 1, running_loss / 2000)) # 由于是2000个样本的梯度累积,所以除以2000
running_loss = 0.0
print('Finished Training')
# 保存模型:
# 首先设定模型的保存路径
PATH = './cifar_net.pth'
# 保存模型的状态字典
torch.save(net.state_dict(), PATH) # 将模型 net 的参数(状态字典)保存到指定路径 PATH。
# 在测试集上测试模型
dataiter = iter(testloader)
images, labels = next(dataiter)
# 展示图片
imshow(torchvision.utils.make_grid(images))
# 打印真实标签
print('GroundTruth: ', ' '.join('%5s' % classes[labels[j]] for j in range(4)))
# 加载模型并对测试图片进行预测
# 首先实例化模型的类对象
net = Net()
# 加载训练阶段保存好的模型的状态字典
PATH = './cifar_net.pth' # 假设模型保存路径为该地址
net.load_state_dict(torch.load(PATH))
# 利用模型对图片进行预测
outputs = net(images)
# 共有10个类别,采用模型计算出的概率最大的作为预测的类别
_, predicted = torch.max(outputs, 1)
# 打印预测标签的结果
print('Predicted: ', ' '.join('%5s' % classes[predicted[j]] for j in range(4)))
# 计算模型在全部测试集上的准确率
correct = 0
total = 0
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs.data, 1)
total += labels.size(0)
correct += (predicted == labels).sum().item()
print('Accuracy of the network on the 10000 test images: %d %%' % (
100 * correct / total))
# 分类别计算准确率
class_correct = list(0. for i in range(10))
class_total = list(0. for i in range(10))
with torch.no_grad():
for data in testloader:
images, labels = data
outputs = net(images)
_, predicted = torch.max(outputs, 1)
c = (predicted == labels).squeeze()
for i in range(4):
label = labels[i]
class_correct[label] += c[i].item()
class_total[label] += 1
for i in range(10):
print('Accuracy of %5s : %2d %%' % (
classes[i], 100 * class_correct[i] / class_total[i]))
if __name__ == '__main__':
main()
1.为什么需要
if __name__ == '__main__'
?多进程数据加载的限制
你的代码中使用了num_workers=2
,这意味着 PyTorch 会使用多进程来加速数据加载。在 Windows 和某些 Unix 系统(如 macOS)上,Python 的多进程模块multiprocessing
使用spawn
模式创建子进程:
spawn
模式:子进程会重新导入主模块(即你的 Python 脚本),并从头执行代码。- 问题:如果没有
if __name__ == '__main__'
保护,子进程会重复执行主模块中的所有代码,包括创建新的子进程,导致无限递归(或死锁)。
2.将关键代码封装在函数中(main())使结构更清晰
小节总结
- 学习了分类器的任务和数据样式:
- 分类器任务是将不同图像进行分类,对输入的图片进行判别并完成分类。
- 采用 CIFAR10 数据集作为原始图片数据,该数据集拥有 10 个类别的 3×32×32 彩色图片。
- 学习了训练分类器的步骤:
- 使用
torchvision
下载 CIFAR10 数据集。 - 定义卷积神经网络。
- 定义损失函数(如交叉熵损失函数)和优化器(如 SGD)。
- 在训练集上训练模型,通过多轮迭代更新网络参数。
- 在测试集上测试模型,评估模型分类性能。
- 使用
- 学习了在 GPU 上训练模型:
- 首先定义设备,根据 CUDA 是否可用选择 GPU 或 CPU:
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
。 - 将模型转移到 GPU 上:
net.to(device)
。 - 在迭代训练过程中,每一步都将图片和标签张量转移到 GPU 上:
inputs, labels = data[0].to(device), data[1].to(device)
。
- 首先定义设备,根据 CUDA 是否可用选择 GPU 或 CPU:
补充内容
在CPU设备中执行Pytorch代码
