pytorch60分钟教程

tensor_tutorial

Tensors

首先我们对张量下一个定义,它是一个很类似于数组和矩阵的数据结构,
在pytorch中,它作为我们的模型的输入,输出和参数的数据结构存在。进一步地说,张量和numpy的ndarrays很像,但是张量可以在GPUs或者其他设备上加速计算。

Tensors Initialization

张量有多重初始化方式,以下是一些示例:
直接从数据中初始化
(指的就是基本的数据类型吧,不是其他封装好的)

data = [[1,2], [3,4]]
x_data = torch.tensor(data)

从numpy的ndarray中转换

np_array = np.array(data)
x_np = torch.from_numpy(np_array)

也可以从另一个张量初始化

x_ones = torch.ones_like(x_data)  # ones_like()方法用于创建一个形状和类型都和传入张量一样的张量,但是这个张量的所有值都为1
x_rand = torch.rand_like(x_data, dtype=float) # 类似的,rand_like()方法用于创建一个形状和类型都和传入张量一样的张量,但是这个张量的所有值都为任意值
// 还有一点需要说明,在用另一个张量初始化时,如果指明类型和形状,是会覆盖之前的张量的

或者用任意值或者随机值来初始化
这种方式需要指定张量的形状,即shape,shape是一个元组,用圆括号标识

shape = (2, 3)
rand_tensor = torch.rand(shape)
ones_tensor = torch.ones(shape)
zeros_tensor = torch.zeros(shape)

Tensors Attribute

张量的特性包括它们的形状,类型以及它们存储在什么设备上

tensor = torch.rand(3, 4) # 接受一个元组或者多个表示形状的整型都是可以的
print(f"Shape of tensor: {tensor.shape}")		# Shape of tensor: torch.Size([3, 4])
print(f"Datatype of tensor: {tensor.dtype}")	# Datatype of tensor: torch.float32
print(f"Device tensor is stored on: {tensor.device}")	# Device tensor is stored on: cpu
// pytorch自己实现了一些数据类型,叫做tensor.dtype,这样可以更高效地进行一些计算

Tensors Operation

pytorch上有超过一百种的张量操作,包括转置,索引,切片,科学计算,线性代数,随机采样等等,每一种操作都是可以放到GPU上进行加速的,以下是一些常用的张量操作示例

# We move our tensor to the GPU if available
if torch.cuda.is_available():
  tensor = tensor.to('cuda')	# to()方法用来把张量放到gpu上
  print(f"Device tensor is stored on: {tensor.device}")

# Standard numpy-like indexing and slicing
tensor = torch.ones(4, 4)
tensor[:,1] = 0
print(tensor)

# Joining tensors 张量拼接
t1 = torch.cat([tensor, tensor, tensor], dim=1) # 张量会在给定的维度上拼接,要注意区分第0维度和第1维度
print(t1)
"""
几个细节需要说明
1. 我之前一直有点区分不清楚,0和1到底是哪个轴,现在我们从坐标的角度可能好理解一点,对于一个二维的矩阵而言,第一维其实就相当于纵坐标,所以他自然是类似于y轴的概念,这样dim=1自然就是沿着x轴拼接了
2. torch.cat和torch.stack相比,stack方法要求要更高一些,它要求拼接的张量形状必须一致,而cat要求的是除了拼接的这一维,其他的形状一致就好
"""

# Multiplying tensors
print(f"tensor.mul(tensor) \n {tensor.mul(tensor)} \n")	# 这是按元素乘积,通过调用某个张量的方法,来传入另一个参数与他按元素相乘
print(f"tensor * tensor \n {tensor * tensor}")	# 这是备用的按元素乘积运算符

print(f"tensor.matmul(tensor.T) \n {tensor.matmul(tensor.T)} \n")	# 正常的矩阵乘法使用,和上面一样也是tensor的方法
print(f"tensor @ tensor.T \n {tensor @ tensor.T}")	# 备用的矩阵乘法运算符

# In-place operations 原地计算
print(tensor, "\n")
tensor.add_(5)		# 这种方式会占用一些内存,可能因为丧失内存而导致计算错误
print(tensor)

Bridge with NumPy

Tensors on the CPU and NumPy arrays can share their underlying memory locations, and changing one will change the other.
这是不是意味着它们是一种实现方式?

# Tensor to NumPy array
t = torch.ones(5)
print(f"t: {t}")
n = t.numpy()
print(f"n: {n}")

t.add_(1)
print(f"t: {t}")
print(f"n: {n}")		# 如果改变张量的值,那么array的值也会发生变化,反之亦然

Autograd_tutorial

torch.autograd

在pytorch中,autograd是一个能够帮助神经网络自动记录计算图并且计算梯度的核心模块,它的功能应该和Tensorflow的GradientTape是类似的,都起到一个自动计算梯度的作用,具体的细节是它用户使用动态计算图来记录张量的每一步操作,这样反向传播时他可以利用计算图来计算梯度。
接下来我们举一个例子,来反映他是怎么在一个神经网络中使用的。

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)

prediction = model(data) # forward pass

loss = (prediction - labels).sum()
loss.backward() # backward pass

optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

optim.step() #gradient descent

事实上,上面的代码省略了很多关键的步骤,比如我们是否把每个参数都设置为需要自动计算参数,比如

x = torch.tensor([1.0, 2.0, 3.0], requires_grad=True)	
# require_grad默认值是False,这意味着只有指定为True,pytorch才会追踪这个张量的操作,并自动计算梯度,这对优化计算效率和内存管理很有帮助

另外

loss.backward()

这里只会计算requires_grad设置为True的张量,另外它会计算结果关于其它参数的梯度,并把它们存在每个变量的grad的attribute中,我们可以通过调用grad这个attribute来使用loss关于这个参数的梯度。
关于优化器会优化哪些参数,'model.parameters()'返回一个生成器,它生成模型所有需要优化的参数,在这里我们在优化器注册了模型的所有参数。

optim = torch.optim.SGD(model.parameters(), lr=1e-2, momentum=0.9)

然后我们会使用计算好的梯度来更新参数:

optim.step()

最后,我们需要清零梯度,因为梯度会叠加而非自动覆盖,所以我们需要清零梯度

optim.zero_grad()

autograd是怎么收集梯度的

接下来我们将举一个例子,来解释autograd是怎么收集梯度的

  1. 创建张量
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)

这里创建了两个张量a和b,并把requires_grad设置为True以启用自动求导。

  1. 创建函数
Q = 3*a**3 - b**2

Q是一个关于a和b的向量函数,因为Q是一个向量,像我们平时做深度学习中的loss往往就是一个标量,即标量函数。

  1. 计算梯度
    ∂ Q ∂ a \frac {\partial Q}{\partial a} aQ ∂ Q ∂ a \frac {\partial Q}{\partial a} aQ是可以分别计算的

  2. 调用backward()
    因为这里的Q是一个向量而非标量,所以我们在调用backward()时,还要额外地传一个参数,就是Q关于其自身的梯度,这个梯度和Q的形状一样,如果Q是一个标量我们默认为1。等效地,我们也可以把Q累加为一个标量,这样就不用传额外的梯度,即Q.sum().backward()。这部分怎么去理解呢,实际上还是认为是一个多对一的关系,如果输出向量有多维,可能认为每一个元素在进行不同的任务或者从不同的角度分析输入,也就是说在loss之后我们又有一个真正最后的输出,他反映了一些计算loss的对应关系。

external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
  1. 存储和验证梯度
    计算好的梯度会存储在张量的.grad的attribute中。

Optional Reading - Vector Calculus using autograd

我想我们可以在这里讲一讲有关如何计算损失函数关于参数的梯度计算,实际上在我学习吴恩达老师的深度学习的过程中是手推过矩阵求导的,当时是从一个个元素的求导推广到整个矩阵的情况,但这里我们可以简单解释一下背后的数学原理。

背景知识

  1. 向量值函数:一个函数 f f f 接受一个向量 x ⃗ \vec{x} x 作为输入,并输出另一个向量 y ⃗ \vec{y} y ,形式为 y ⃗ = f ( x ⃗ ) \vec{y} = f(\vec{x}) y =f(x )。这里, x ⃗ \vec{x} x 可能有多个分量(例如 n n n 个), y ⃗ \vec{y} y 也有多个分量(例如 m m m 个)。

  2. 雅可比矩阵:对于向量值函数 f f f,其输出 y ⃗ \vec{y} y 对输入 x ⃗ \vec{x} x 的梯度被称为雅可比矩阵 J J J。矩阵的每个元素 J i j J_{ij} Jij 表示 y ⃗ \vec{y} y 的第 i i i 个分量对 x ⃗ \vec{x} x 的第 j j j 个分量的偏导数:
    J i j = ∂ y i ∂ x j J_{ij} = \frac{\partial y_i}{\partial x_j} Jij=xjyi
    这样,雅可比矩阵 J J J 的形状为 m × n m \times n m×n

向量-雅各比矩阵积

简而言之就是损失关于模型输出的梯度向量,有点像我们前面提到的external_grad,这个向量乘以我们的模型输出关于参数/输入的雅各比矩阵,就得到了损失函数关于参数/输入的梯度。

具体解释

  • 向量-雅可比矩阵积的作用torch.autograd 的核心功能之一就是计算这个向量-雅可比矩阵积。给定一个向量 v ⃗ \vec{v} v ,它计算 J T ⋅ v ⃗ J^T \cdot \vec{v} JTv 。这个结果在机器学习和深度学习中非常有用,因为它可以表示损失函数的梯度。

  • 与损失函数的关系:如果向量 v ⃗ \vec{v} v 是某个标量函数 l = g ( y ⃗ ) l = g(\vec{y}) l=g(y )的梯度,即:

    v ⃗ = ( ∂ l ∂ y 1 , ∂ l ∂ y 2 , … , ∂ l ∂ y m ) T \vec{v} = \left(\frac{\partial l}{\partial y_1}, \frac{\partial l}{\partial y_2}, \ldots, \frac{\partial l}{\partial y_m}\right)^T v =(y1l,y2l,,yml)T

    那么向量-雅可比矩阵积 J T ⋅ v ⃗ J^T \cdot \vec{v} JTv 就是标量函数 l l l 相对于输入 x ⃗ \vec{x} x 的梯度:

    J T ⋅ v ⃗ = ( ∂ l ∂ x 1 , ∂ l ∂ x 2 , … , ∂ l ∂ x n ) T J^T \cdot \vec{v} = \left(\frac{\partial l}{\partial x_1}, \frac{\partial l}{\partial x_2}, \ldots, \frac{\partial l}{\partial x_n}\right)^T JTv =(x1l,x2l,,xnl)T

这个结果利用了链式法则(chain rule):通过计算 y ⃗ \vec{y} y x ⃗ \vec{x} x 的雅可比矩阵 J J J 及其转置,与 v ⃗ \vec{v} v 相乘,我们获得了 l l l x ⃗ \vec{x} x 的梯度。

Computational Graph

Autograd进行自动微分的核心是,他会在前向传播的过程中创建和维护一个DAG(Directed acyclic graph, 有向无环图),这个有向无环图会记录输入输出张量和对张量进行的操作。
一个DAG的例子
整个DAG的节点主要由三部分构成

  1. 叶子节点:输入张量,它们位于图的起点
  2. 根节点:输出张量,它们位于图的终点
  3. 中间节点:它们是一个个的Function对象,用来存储前向操作和反向操作,同时张量的grad_fn会指向这些生成该张量的Function对象,用于在反向传播中计算梯度使用

DAG的使用

  1. 前向传播

    • 从前往后计算结果张量
    • 通过存储操作的梯度函数来构建DAG
  2. 反向传播

    • 通过调用根节点的.backward() 方法启动
    • 计算并累积每个张量的梯度,这些张量的requires_grad=True

DAG的重建

每次.backward()调用后,图都会被重置,这种动态图的构建允许使用控制流语句,并且可以在不同的迭代中改变操作和形状

在计算图中管理张量

  1. 从DAG中排除:
    • requires_grad=False的张量不会被autograd追踪,他们相当于常量参与其他需要记录的张量的梯度计算
    • 如果一个操作的任一输入张量的requires_grad=True,则该操作的输出张量也会有requires_grad=True,以下是一个例子
    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}")
    
    第一个会输出False,而第二个会输出True
  2. 冻结参数
    • 概念上,冻结参数就是不需要计算参数的梯度,它就是我们之前提到迁移学习/微调中经常使用的方法
    • 用法上,大部分模型参数会被冻结,只有最后一层或几层,比如分类层的参数会被更新
      以下是一个冻结和微调神经网络的示例
from torch import nn
from torchvision.models import resnet18, ResNet18_Weights

model = resnet18(weights=ResNet18_Weights.DEFAULT)
for param in model.parameters():
    param.requires_grad = False

model.fc = nn.Linear(512, 10)  # 假设有10个新类别

from torch.optim import SGD

optimizer = SGD(model.parameters(), lr=0.01, momentum=0.9)

细节上,我们需要说明

  1. 在pytorch中所有新创建的nn.module子类(例如nn.Linear层)的参数requires_grad属性都会默认设置为True,这意味这些参数会自动包含在计算图中,并会自动追踪和更新梯度
  2. 即使我们在优化器中注册了模型的所有参数,也不是所有参数都会被更新的,这种选择性梯度计算和参数更有效率

这里我们对有类似功能的torch.no_grad()进行一个补充说明,接下来是一段示例代码

import torch
from torchvision.models import resnet18, ResNet18_Weights

# 加载预训练模型
model = resnet18(weights=ResNet18_Weights.DEFAULT)
model.eval()  # 将模型设置为评估模式

# 创建输入张量
input_tensor = torch.randn(1, 3, 224, 224)

# 在 no_grad 上下文中进行前向传递
with torch.no_grad():
    output = model(input_tensor)

print(output)

我们在torch.no_grad的上下文进行前向传播,以确保不计算和存储梯度,从上面的代码我们也可以看出,这种方式主要在模型验证阶段和测试阶段使用,以确保不更新参数。

Neural_Networks_tutorial

在Pytorch中,神经网络构建的核心是nn.module

  1. nn.module
    • Pytorch中每个神经网络都是nn.module的实例
    • 它包含了一些神经网络层以及forward方法,该方法定义了输入数据如何通过网络层并生成输出
  2. 神经网络的流程
    • 首先需要定义包含可学习参数的网络
    • 迭代输入数据集,将输入数据通过网络进行处理
    • 计算Loss
    • 通过反向传播计算梯度并传播
    • 更新参数

Define the network

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 input image channel, 6 output channels, 5x5 square convolution
        # kernel
        self.conv1 = nn.Conv2d(1, 6, 5)
        self.conv2 = nn.Conv2d(6, 16, 5)
        # an affine operation: y = Wx + b
        self.fc1 = nn.Linear(16 * 5 * 5, 120)  # 5*5 from image dimension 
        self.fc2 = nn.Linear(120, 84)
        self.fc3 = nn.Linear(84, 10)

    def forward(self, input):
        # Convolution layer C1: 1 input image channel, 6 output channels,
        # 5x5 square convolution, it uses RELU activation function, and
        # outputs a Tensor with size (N, 6, 28, 28), where N is the size of the batch
        c1 = F.relu(self.conv1(input))
        # Subsampling layer S2: 2x2 grid, purely functional,
        # this layer does not have any parameter, and outputs a (N, 6, 14, 14) Tensor
        s2 = F.max_pool2d(c1, (2, 2))
        # Convolution layer C3: 6 input channels, 16 output channels,
        # 5x5 square convolution, it uses RELU activation function, and
        # outputs a (N, 16, 10, 10) Tensor
        c3 = F.relu(self.conv2(s2))
        # Subsampling layer S4: 2x2 grid, purely functional,
        # this layer does not have any parameter, and outputs a (N, 16, 5, 5) Tensor
        s4 = F.max_pool2d(c3, 2)
        # Flatten operation: purely functional, outputs a (N, 400) Tensor
        s4 = torch.flatten(s4, 1)
        # Fully connected layer F5: (N, 400) Tensor input,
        # and outputs a (N, 120) Tensor, it uses RELU activation function
        f5 = F.relu(self.fc1(s4))
        # Fully connected layer F6: (N, 120) Tensor input,
        # and outputs a (N, 84) Tensor, it uses RELU activation function
        f6 = F.relu(self.fc2(f5))
        # Gaussian layer OUTPUT: (N, 84) Tensor input, and
        # outputs a (N, 10) Tensor
        output = self.fc3(f6)
        return output


net = Net()
print(net)
  1. __init__方法中,创建了一些torch.nn.module的子类实例,并把这些层对象作为模型类的属性
  2. forward实际调用这些层进行前向计算,将输入数据转化成输出
  3. 最后创建了一个Net类的实例,以上层对象会被初始化并准备在forward方法中使用
  4. 我们只用定义forward方法即可,反向传播的过程会被自动记录并计算
  5. 可学习参数存放在实例的parameter方法中,即net.parameters()
  6. 关于参数的部分其实要聊聊,后面的全连接层没什么好说的,就是权重矩阵 W W W和偏置 b b b,每一层会有这两个可学习的参数。在前面的卷积神经网络中,其实我一直有一些误解,比如在检测边缘或者特定的卷积神经网络中,我们的卷积核是人为设定好的,不可训练的,但其实在一般的神经网络中,卷积核作为一个矩阵是可学习的参数,他通过监督学习自适应地学习应该有的特征,同时在每一次卷积核和原图的卷积计算后,我们可以添加一个偏置,这样一个卷积层也是两个可学习的参数,所以如果我们通过如下的代码,打印参数的数量,我们会得到10的输出结果
params = list(net.parameters())
print(len(params))
print(params[0].size())  # conv1's .weight

关于torch.nn的一些其他的注意事项

  1. torch.nn模块主要设计用于处理小批量(mini-batches)的数据,而不是单个样本,小批量处理在深度学习中非常常见,因为它可以有效利用GPU并稳定梯度计算
    • 例如nn.Conv2d层的输入通常是一个四维张量,表示为[nSamples, nChannels, Height, Width]。这里nSamples是批量大小,nChannels是通道数,HeightWidth是图像的高度和宽度。
    • 另外,如果我们只有一个样本,则需要添加一个虚拟的批次维度,可以使用input.squeeze(0)来增加一个批次维度,使输入适应torch.nn的需求
  2. 核心类和概念回顾
    • torch.Tensor:多维数组,支持自动求导(autograd)操作,例如backward()Tensor对象还可以保存相对于自身的梯度,存在.grad中。
    • nn.Module:神经网络模块,是封装模型参数的便捷方式。提供了在 GPU 上移动参数、导出、加载模型等辅助功能。
    • nn.Parameter:是一种特殊的Tensor,当它被分配给Module的属性时,会自动注册为参数。这意味着它会参与梯度计算和优化过程。
    • autograd.Function:实现了自动求导操作的前向和反向定义。每个Tensor操作都会至少创建一个Function 节点,这个节点连接到创建该 Tensor的函数,并编码其历史。

Loss Function

损失函数的定义和方法我想我已经太熟悉了,我们用一个示例讲讲细节:

output = net(input)
target = torch.randn(10)  # a dummy target, for example
target = target.view(1, -1)  # make it the same shape as output
criterion = nn.MSELoss()

loss = criterion(output, target)
print(loss)

有一些细节需要说明

  1. nn.MSELoss是一个继承于nn.Module的损失函数类,它定义了如何计算均方误差(Mean Square Error)
  2. criterion = nn.MSELoss是实例化这个类,创建了一个criterion对象,用来计算具体的损失

关于计算图的分析

  1. 前向传播
    input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
    -> flatten -> linear -> relu -> linear -> relu -> linear
    -> MSELoss
    -> loss
  2. 反向传播
    当调用 loss.backward() 时,PyTorch 将计算损失相对于计算图中所有requires_grad=True参数的梯度。
    这些梯度累积到每个参数的 .grad 属性中。
  3. 追踪grad_fn
    grad_fn 属性指向了创建该张量的 Function 对象,这是计算图的一部分。通过 .grad_fn 和其 next_functions 属性,我们可以追踪计算图中各个步骤的反向传播路径。next_functions 列表包含了与当前操作相关的前一个操作的 grad_fn,从而形成了整个计算图的链条。
    比如
    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
    
    会打印这样的结果
    <MseLossBackward0 object at 0x7871c20a1cf0>
    <AddmmBackward0 object at 0x7871c250c5e0>
    <AccumulateGrad object at 0x7872a05ebd90>
    
    这些代码行展示了如何追踪计算图中的节点,从而理解梯度是如何从损失函数向前传播到每个网络参数的。在训练过程中,PyTorch自动处理这种追踪和计算梯度的过程,但理解这个过程有助于调试和优化神经网络模型。

BackProp

在进行反向传播之前我们应该先清空梯度,因为如果不清空梯度会累积而非替换,所以在进行反向传播,计算梯度之前我们肯定要先对梯度清零,对误差进行反向传播我们需要调用误差的backward()方法,即loss.backward(),下面是一段示例:

net.zero_grad()     # zeroes the gradient buffers of all parameters

print('conv1.bias.grad before backward')
print(net.conv1.bias.grad)

loss.backward()

print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)

Update the weights

最简单的更新规则就是我们的SGD(Stochastic Gradient Descend, 随机梯度下降),它的更新规则如下

weight = weight - learning_rate * gradient

用python代码实现如下

learning_rate = 0.01
for f in net.parameters():
    f.data.sub_(f.grad.data * learning_rate)

由于实际训练过程中可能需要使用多种优化算法,如 SGD、Nesterov-SGD、Adam、RMSProp 等,手动编写更新规则代码既麻烦又容易出错。为此,PyTorch 提供了torch.optim模块,该模块实现了常用的优化算法,并简化了使用过程,如

import torch.optim as optim

# 创建一个 SGD 优化器
optimizer = optim.SGD(net.parameters(), lr=0.01)

optimizer.zero_grad()   # 清空梯度缓存
output = net(input)     # 前向传播,计算输出
loss = criterion(output, target)  # 计算损失
loss.backward()         # 反向传播,计算梯度
optimizer.step()        # 执行一步梯度下降,更新参数

让我在最后讲讲SGD,普通的梯度下降算法可能是把整个输入拼成一个大张量,然后整个地计算loss,计算梯度,最后更新参数,但是这相当于每次更新参数的时候,都要在整个数据集上计算,非常耗时,所以SGD的随机并非真正的随机,而是它拿小批量的数据计算梯度,并把它作为整个数据集的梯度,并更新一次参数,这样每一批次的计算都会更新一次参数,计算更快同时收敛也更快,但是坏处也显而易见,这种方式初期可能下降很快,但是后期不容易收敛到一个最低点,这也是后面很多优化器提出的原因。

cifar10_tutorial

首先我需要说明,面对不同类型的数据,我们可以用一些专门的package来处理它们,而对于图像问题,我们有一个package叫做torchvision,它提供了一些常用的图像数据集的加载器,可以自动下载,加载,预处理数据,使我们免于重复编写一样样板代码来实现这些功能,torchvision.datasets and torch.utils.data.DataLoader是两个关键的模块,主要实现加载和处理数据的功能。
为了实现这样一个分类器,我们需要实现以下几个步骤:

Load and normalize CIFAR10

import torch
import torchvision
import torchvision.transforms as transforms

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')

需要专门说明是transforms这个模块,首先我们从torchvision得到的输出是PILImage格式的数据,它们的大小都是[0,1]之间的,进一步地我们用transforms模块进行预处理,这里先对transforms模块常见的几个功能做一个说明:

  1. 数据预处理:在把图像文件输入到模型之前,transforms可以提供一些预处理的操作,如裁剪,旋转,缩放等
  2. 数据增强:为了避免过拟合,提升样本的多样性,transforms提供了旋转,翻转,颜色抖动等随机变换来生成新的样本
  3. 数据标准化:为了提升训练的速度和模型的稳定性,transforms可以把图像数据归一化到特定的均值和标准差

回到代码的相关部分,transforms.Compose是把几个操作合并了,分别是把PIL图像数据或者Ndarray转化成功张量,同时把[0,255]归一化到[0,1](这一点不太确定),另外把每个通道的数值,用0.5的均值和0.5的标准差归一化,即
x − 0.5 0.5 = x n e w \frac{x-0.5}{0.5} = x_{new} 0.5x0.5=xnew
把[0,1]的数值映射到[-1,1]上
后面是包括加载数据集和把数据集加载到内存中的一些操作

  • batch_size = 4:设置每个批次的样本数量为 4。
  • torchvision.datasets.CIFAR10:加载 CIFAR-10 数据集。
    • root=‘./data’:数据集的根目录。
    • train=True:加载训练数据集。
    • download=True:如果数据集不存在,自动下载。
    • transform=transform:应用前面定义的图像转换操作。
  • torch.utils.data.DataLoader:用于将数据集加载到内存中,并支持批量加载、随机打乱等操作。
    • trainloader:用于加载训练数据。
    • testloader:用于加载测试数据。
    • shuffle=True:在每个 epoch 之前打乱训练数据,提高模型的泛化能力。
    • num_workers=2:使用两个子进程加载数据,可以加速数据加载过程。

Define a Convolutional Neural Network

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) # flatten all dimensions except batch
        x = F.relu(self.fc1(x))
        x = F.relu(self.fc2(x))
        x = self.fc3(x)
        return x


net = Net()

Define a Loss function and optimizer

import torch.optim as optim

criterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)

Train the Network

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')

然后我们还可以把模型的参数保存下来,主要是模型的权重,偏置以及缓冲区内的一些数据,而不是保存整个模型,这使得我们可以在重新加载模型时更灵活地修改或扩展模型架构。

PATH = './cifar_net.pth'
torch.save(net.state_dict(), PATH)

需要补充说明的是,我们使用状态字典这样的结构来存储模型参数的,它存储了模型的所有参数和缓冲区(buffers)。这些参数和缓冲区通常是以张量的形式存储的,包括模型中的可训练参数(如卷积层的权重)和不可训练参数(如均值和方差)。

Test the network on the test data

# 先看一下测试集上的数据
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)))

# 加载我们之前保存的参数
net = Net()
net.load_state_dict(torch.load(PATH))

# 获取测试集上的输出
outputs = net(images)

# 查看在测试集上具体的输出分类情况
_, predicted = torch.max(outputs, 1)

print('Predicted: ', ' '.join(f'{classes[predicted[j]]:5s}'
                              for j in range(4))) 

# 可以进一步计算分类的Accuracy来评估模型性能
correct = 0
total = 0
# since we're not training, we don't need to calculate the gradients for our outputs
with torch.no_grad():
    for data in testloader:
        images, labels = data
        # calculate outputs by running images through the network
        outputs = net(images)
        # the class with the highest energy is what we choose as prediction
        _, predicted = torch.max(outputs.data, 1)
        total += labels.size(0)
        correct += (predicted == labels).sum().item()

print(f'Accuracy of the network on the 10000 test images: {100 * correct // total} %')

# 因为cifar10数据一共有十类,我们可以分别看看这10类上的准确率如何
# prepare to count predictions for each class
correct_pred = {classname: 0 for classname in classes}
total_pred = {classname: 0 for classname in classes}

# again no gradients needed
with torch.no_grad():
    for data in testloader:
        images, labels = data
        outputs = net(images)
        _, predictions = torch.max(outputs, 1)
        # collect the correct predictions for each class
        for label, prediction in zip(labels, predictions):
            if label == prediction:
                correct_pred[classes[label]] += 1
            total_pred[classes[label]] += 1


# print accuracy for each class
for classname, correct_count in correct_pred.items():
    accuracy = 100 * float(correct_count) / total_pred[classname]
    print(f'Accuracy for class: {classname:5s} is {accuracy:.1f} %')

有几个细节需要说明
1.torch.max的用法:在我们的代码中,我们频繁使用了_, predicted = torch.max(outputs.data, 1),先区分一下outputsoutputs.data吧,简而言之outputs由于由一些记录了梯度的张量计算而来,所以它的requires_grad = True,而data是不带梯度的(其实这种方式也是不安全的,使用outputs.detach才能把张量从计算图中剥离出来,避免无意间修改数值导致破坏计算图)。继续说回他的用法,outputs的形状一般为[batch_size, num_classes],而后面的1指的是axis=1,指的是在每一行上找到最大值,即对于每一个样本概率最大的类别,这个函数会返回两个值,分别是values和indices,它们都是[batch_size]的shape,我们实际上关注的是类别而非具体的概率,所以我们用占位符替代values,而关注具体的indices。
2. 打印模型的正确率大约是 55 % 55\% 55%,这相比瞎猜的 10 % 10\% 10%,已经算是很大的提高了。
3. 按照10个种类的正确率打印下来,我们会得到这样的结果

Accuracy for class: plane is 51.1 %
Accuracy for class: car   is 63.4 %
Accuracy for class: bird  is 60.2 %
Accuracy for class: cat   is 29.7 %
Accuracy for class: deer  is 39.5 %
Accuracy for class: dog   is 48.4 %
Accuracy for class: frog  is 64.0 %
Accuracy for class: horse is 52.3 %
Accuracy for class: ship  is 81.6 %
Accuracy for class: truck is 58.3 %

可能是因为,样本不均衡,或者猫这类生物特征比较复杂,分类难度大等多种因素。

Data_parallel_tutorial

Data Parallelism

  1. 将模型和张量分配到 GPU
    • 你可以使用model.to(device)PyTorch模型加载到 GPU,其中device通常指定为"cuda:0",表示第一个 GPU,如
    device = torch.device("cuda:0")
    model.to(device)
    
    • 同样地,可以用tensor.to(device)将张量移动到 GPU。这种操作会返回一个新的在 GPU 上的张量副本,而不会修改原始张量。
    mytensor = my_tensor.to(device)
    
    • 需要说明的是,先把模型移到GPU上和先把张量移到GPU上没有严格的顺序要求,但是推荐先把模型移到GPU上
  2. 使用DataParallel进行多 GPU 运算
    • 默认情况下,PyTorch 只会使用一个 GPU。然而,为了利用多个 GPU,可以使用torch.nn.DataParallel
    model = nn.DataParallel(model)
    
    • DataParallel允许你在多个 GPU 上并行地执行模型的前向和反向传播运算。这种并行处理可以显著加快训练和推理过程,特别是对于大型模型或数据集。

Imports and parameters

import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader

# Parameters and DataLoaders
input_size = 5
output_size = 2

batch_size = 30
data_size = 100

Device

device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")

Dummy Dataset(随机数据集)

class RandomDataset(Dataset):

    def __init__(self, size, length):
        self.len = length
        self.data = torch.randn(length, size)

    def __getitem__(self, index):
        return self.data[index]

    def __len__(self):
        return self.len

rand_loader = DataLoader(dataset=RandomDataset(input_size, data_size),
                         batch_size=batch_size, shuffle=True)

主要实现__init__,__getitem____len__方法,分别用来初始化数据集类,获取数据集的单个样本,获取数据集大小,这对dataloader迭代加载数据很有帮助。

Simple Model

尽管并行处理可以在很多更复杂的模型,比如CNN,RNN等上运行,这里我们就只举一个非常简单的模型

class Model(nn.Module):
    # Our model

    def __init__(self, input_size, output_size):
        super(Model, self).__init__()
        self.fc = nn.Linear(input_size, output_size)

    def forward(self, input):
        output = self.fc(input)
        print("\tIn Model: input size", input.size(),
              "output size", output.size())

        return output

没什么太多的细节要说,可以说一下super()这个内置函数,他可以调用父类的方法,比如

super(subclass, self).method()

# 在python3中,可以忽略子类名,自动推断父类,避免多重继承的错误
super().method()

这样做的好处是,当父类的内容更新时,子类如果调用了父类的__init__()方法,也会自动更新。

Create Model and DataParallel

model = Model(input_size, output_size)
if torch.cuda.device_count() > 1:
	print("Let's use", torch.cuda.device_count(), "GPUs!")
	# dim = 0 [30, xxx] -> [10, ...], [10, ...], [10, ...] on 3 GPUs
 	model = nn.DataParallel(model)

model.to(device)

实例化模型后,调用model = nn.DataParallel(model)并行化,再把model放在GPU上即可

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值