全部文章

PyTorch

PyTorch 简介

PyTorch 是一个开源的机器学习库,它具有强大的 GPU 加速功能,方便用户进行深度学习模型的构建和优化,被广泛应用于计算机视觉自然语言处理等领域。

  • Pytorch 是一个基于 Numpy 的科学计算包,向它的使用者提供了两大功能。
    • 作为 Numpy 的替代者,向用户提供使用 GPU 强大功能的能力。
    • 做为一款深度学习的平台,向用户提供最大的灵活性和速度。

Pytorch 基本语法

学习目标

  • 掌握 Pytorch 的基本元素操作。
  • 掌握 Pytorch 的基本运算操作。

Pytorch 的基本元素操作

张量

张量(Tensor)的概念类似于 Numpy 中的 ndarray 数据结构,最大的区别在于 Tensor 可以利用 GPU 的加速功能。
我们使用 Pytorch 的时候,常规步骤是先将 torch 引用进来,如下所示:
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]])
  1. torch.rand(5, 3)
    • 生成的是 均匀分布 的随机数。
    • 这些随机数的取值范围是 [0, 1),即大于等于 0 且小于 1。
    • 所有数值在这个区间内的出现概率是相等的。
  2. 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创建张量的一个方法,它的主要用途是:
  1. 保持与原张量相同的设备(如 CPU 或 GPU)
  2. 保持与原张量相同的数据类型(除非显式指定其他类型)
  3. 快速创建具有相同尺寸的新张量
例如,如果你有一个在 GPU 上的张量,使用 new_ones() 创建的新张量也会自动在 GPU 上,无需手动指定设备。

randn_like() 的作用

  1. 保持形状一致性:新张量的维度与输入张量完全相同。
  2. 随机初始化:使用正态分布随机填充元素值,适合需要随机权重初始化的场景(如神经网络)。
  3. 灵活的数据类型控制:可以通过 dtype 参数指定新张量的数据类型(如 torch.float32torch.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 并可以反向转换。
  • 学习了任意的 Tensors 可以用.to()方法来将其移动到任意设备上。
    • x = x.to(device)

Pytorch 中的 autograd

学习目标

  • 掌握自动求导中的 Tensor 概念和操作。
  • 掌握自动求导中的梯度 Gradients 概念和操作。
在整个 Pytorch 框架中,所有的神经网络本质上都是一个 autograd package(自动求导工具包)。
autograd package 提供了一个对 Tensors 上所有的操作进行自动微分的功能。

梯度追踪

  • torch.Tensor 是整个 package 中的核心类,如果将属性.requires_grad 设置为 True,它将追踪在这个类上定义的所有操作。当代码要进行反向传播的时候,直接调用.backward () 就可以自动计算所有的梯度。在这个 Tensor 上的所有梯度将被累加进属性.grad 中。
  1. ​梯度追踪​​:启用.requires_grad=True后,PyTorch会记录在该张量上执行的所有操作,以构建一个计算图(computational graph)。这个计算图在反向传播过程中用于计算梯度。

  2. ​自动求导​​:当调用.backward()方法时,PyTorch会自动计算该张量相对于某些标量值(通常是损失函数)的梯度,并将梯度存储在.grad属性中。

  3. ​避免追踪非训练参数​​:对于不需要训练的参数(例如,输入数据、冻结的层),可以设置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的自动微分系统:

      1. ​Tensor​​:存储数据和梯度信息
      2. ​Function​​:记录在Tensor上执行的操作(前向计算+反向传播规则)
  • Function类是和Tensor类同等重要的一个核心类,它和Tensor共同构建了一个完整的自动微分系统,每一个Tensor拥有一个.grad_fn属性,代表引用了哪个具体的Function创建了该Tensor.
  • 如果某个张量Tensor是用户自定义的,则其对应的grad_fn is None.

核心关系图:

用户自定义Tensor (grad_fn=None)
       │
       ▼
  操作(如乘法)
       │
       ▼
生成新Tensor (grad_fn=MulBackward0)
       │
       ▼
  计算图节点
       │
       ▼
反向传播时计算梯度

实际工作流程

  1. ​前向传播​​:

# 自定义操作(自动创建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>)
打印 Tensor 的grad_fn属性:
print(x.grad_fn)
print(y.grad_fn)
输出结果:
None
<AddBackward0 object at 0x10db11208>
在 Tensor 上执行更复杂的操作:
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>

反向传播-计算梯度

在 Pytorch 中,反向传播是依靠.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)

总结要点

  1. ​计算图构建​​:

    • 每个非叶子节点Tensor的.grad_fn指向创建它的Function
    • Function对象形成计算图的节点
  2. ​反向传播机制​​:

    • 调用.backward()时,从目标Tensor的.grad_fn开始
    • 链式调用每个Functionbackward()方法
  3. ​用户自定义​​:

    • 叶子节点Tensor(用户直接创建)的.grad_fn=None
    • 通过继承Function可创建自定义微分规则
  4. ​性能优化​​:

    • 合理使用.detach()避免不必要的梯度计算
    • 对不需要梯度的Tensor设置requires_grad=False

​关键理解​​:PyTorch的动态计算图实际上是由TensorFunction对象组成的双向链表结构,前者存储数据,后者存储操作历史和微分规则。

小节总结

  • 学习了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来定义模型,并对其自动求导。

构建神经网络的典型流程

  1. 定义一个拥有可学习参数的神经网络
  2. 遍历训练数据集
  3. 处理输入数据使其流经神经网络
  4. 计算损失值
  5. 将网络参数的梯度进行反向传播
  6. 以一定的规则更新网络的权重
  • 定义神经网络
# 定义一个简单的网络类
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),然后计算出一个数值来评估outputtarget之间的差距大小。
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>)

反向传播

在 Pytorch 中执行反向传播非常简便,全部的操作就是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])

更新参数

更新参数最简单的算法就是 SGD(随机梯度下降)。
具体的算法公式表达式为:weight = weight - learning_rate * gradient
首先用传统的 Python 代码来实现 SGD 如下:
learning_rate = 0.01
#传统代码需要循环遍历每个参数,逐个更新
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)
使用 Pytorch 官方推荐的标准代码如下:
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()
  • 学习了参数的更新方法:
    • 定义优化器来执行参数的优化与更新,如使用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 是一个数据加载器,用于批量处理、打乱数据并提供多进程加载功能。

具体功能解析

  1. 批量处理数据batch_size=4
    • 每次从 trainset 中加载 4 个样本 组成一个批次(batch)。
    • 神经网络通常以批次为单位进行训练,这样可以利用 GPU 的并行计算能力,提高训练效率。
  2. 随机打乱数据shuffle=True
    • 每个训练周期(epoch)开始时,数据会被重新打乱。
    • 这有助于模型学习到数据的统计特性,避免因数据顺序导致的学习偏差,提高泛化能力。
  3. 多进程数据加载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
注意:
如果你是在 Windows 系统下运行上述代码,并且出现报错信息 "BrokenPipeError",可以尝试将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 上训练模型

为了真正利用 Pytorch 中 Tensor 的优秀属性,加速模型的训练,我们可以将训练过程转移到 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)
 

 

 

 

补充内容

在CPU设备中执行Pytorch代码

数据集下载如果是在linux系统进行,则下载完之后会有一个数据压缩包和一个解压的数据文件夹:
 

 

posted @ 2025-07-09 23:02  指尖下的世界  阅读(5)  评论(0)    收藏  举报