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是怎么收集梯度的
- 创建张量
a = torch.tensor([2., 3.], requires_grad=True)
b = torch.tensor([6., 4.], requires_grad=True)
这里创建了两个张量a和b,并把requires_grad设置为True以启用自动求导。
- 创建函数
Q = 3*a**3 - b**2
Q是一个关于a和b的向量函数,因为Q是一个向量,像我们平时做深度学习中的loss往往就是一个标量,即标量函数。
-
计算梯度
∂ Q ∂ a \frac {\partial Q}{\partial a} ∂a∂Q和 ∂ Q ∂ a \frac {\partial Q}{\partial a} ∂a∂Q是可以分别计算的 -
调用
backward()
因为这里的Q是一个向量而非标量,所以我们在调用backward()时,还要额外地传一个参数,就是Q关于其自身的梯度,这个梯度和Q的形状一样,如果Q是一个标量我们默认为1。等效地,我们也可以把Q累加为一个标量,这样就不用传额外的梯度,即Q.sum().backward()
。这部分怎么去理解呢,实际上还是认为是一个多对一的关系,如果输出向量有多维,可能认为每一个元素在进行不同的任务或者从不同的角度分析输入,也就是说在loss之后我们又有一个真正最后的输出,他反映了一些计算loss的对应关系。
external_grad = torch.tensor([1., 1.])
Q.backward(gradient=external_grad)
- 存储和验证梯度
计算好的梯度会存储在张量的.grad的attribute中。
Optional Reading - Vector Calculus using autograd
我想我们可以在这里讲一讲有关如何计算损失函数关于参数的梯度计算,实际上在我学习吴恩达老师的深度学习的过程中是手推过矩阵求导的,当时是从一个个元素的求导推广到整个矩阵的情况,但这里我们可以简单解释一下背后的数学原理。
背景知识
-
向量值函数:一个函数 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 个)。
-
雅可比矩阵:对于向量值函数 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=∂xj∂yi
这样,雅可比矩阵 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} JT⋅v。这个结果在机器学习和深度学习中非常有用,因为它可以表示损失函数的梯度。 -
与损失函数的关系:如果向量 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=(∂y1∂l,∂y2∂l,…,∂ym∂l)T
那么向量-雅可比矩阵积 J T ⋅ v ⃗ J^T \cdot \vec{v} JT⋅v 就是标量函数 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 JT⋅v=(∂x1∂l,∂x2∂l,…,∂xn∂l)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的节点主要由三部分构成
- 叶子节点:输入张量,它们位于图的起点
- 根节点:输出张量,它们位于图的终点
- 中间节点:它们是一个个的
Function
对象,用来存储前向操作和反向操作,同时张量的grad_fn
会指向这些生成该张量的Function对象,用于在反向传播中计算梯度使用
DAG的使用
-
前向传播
- 从前往后计算结果张量
- 通过存储操作的梯度函数来构建DAG
-
反向传播
- 通过调用根节点的
.backward()
方法启动 - 计算并累积每个张量的梯度,这些张量的
requires_grad=True
- 通过调用根节点的
DAG的重建
每次.backward()
调用后,图都会被重置,这种动态图的构建允许使用控制流语句,并且可以在不同的迭代中改变操作和形状
在计算图中管理张量
- 从DAG中排除:
requires_grad=False
的张量不会被autograd追踪,他们相当于常量参与其他需要记录的张量的梯度计算- 如果一个操作的任一输入张量的
requires_grad=True
,则该操作的输出张量也会有requires_grad=True
,以下是一个例子
第一个会输出False,而第二个会输出Truex = 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}")
- 冻结参数
- 概念上,冻结参数就是不需要计算参数的梯度,它就是我们之前提到迁移学习/微调中经常使用的方法
- 用法上,大部分模型参数会被冻结,只有最后一层或几层,比如分类层的参数会被更新
以下是一个冻结和微调神经网络的示例
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)
细节上,我们需要说明
- 在pytorch中所有新创建的
nn.module
子类(例如nn.Linear
层)的参数requires_grad
属性都会默认设置为True
,这意味这些参数会自动包含在计算图中,并会自动追踪和更新梯度 - 即使我们在优化器中注册了模型的所有参数,也不是所有参数都会被更新的,这种选择性梯度计算和参数更有效率
这里我们对有类似功能的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
:
nn.module
- Pytorch中每个神经网络都是
nn.module
的实例 - 它包含了一些神经网络层以及
forward
方法,该方法定义了输入数据如何通过网络层并生成输出
- Pytorch中每个神经网络都是
- 神经网络的流程
- 首先需要定义包含可学习参数的网络
- 迭代输入数据集,将输入数据通过网络进行处理
- 计算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)
- 在
__init__
方法中,创建了一些torch.nn.module
的子类实例,并把这些层对象作为模型类的属性 forward
实际调用这些层进行前向计算,将输入数据转化成输出- 最后创建了一个Net类的实例,以上层对象会被初始化并准备在
forward
方法中使用 - 我们只用定义
forward
方法即可,反向传播的过程会被自动记录并计算 - 可学习参数存放在实例的parameter方法中,即
net.parameters()
- 关于参数的部分其实要聊聊,后面的全连接层没什么好说的,就是权重矩阵 W W W和偏置 b b b,每一层会有这两个可学习的参数。在前面的卷积神经网络中,其实我一直有一些误解,比如在检测边缘或者特定的卷积神经网络中,我们的卷积核是人为设定好的,不可训练的,但其实在一般的神经网络中,卷积核作为一个矩阵是可学习的参数,他通过监督学习自适应地学习应该有的特征,同时在每一次卷积核和原图的卷积计算后,我们可以添加一个偏置,这样一个卷积层也是两个可学习的参数,所以如果我们通过如下的代码,打印参数的数量,我们会得到10的输出结果
params = list(net.parameters())
print(len(params))
print(params[0].size()) # conv1's .weight
关于torch.nn
的一些其他的注意事项
torch.nn
模块主要设计用于处理小批量(mini-batches)的数据,而不是单个样本,小批量处理在深度学习中非常常见,因为它可以有效利用GPU并稳定梯度计算- 例如
nn.Conv2d
层的输入通常是一个四维张量,表示为[nSamples, nChannels, Height, Width]
。这里nSamples
是批量大小,nChannels
是通道数,Height
和Width
是图像的高度和宽度。 - 另外,如果我们只有一个样本,则需要添加一个虚拟的批次维度,可以使用
input.squeeze(0)
来增加一个批次维度,使输入适应torch.nn
的需求
- 例如
- 核心类和概念回顾
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)
有一些细节需要说明
nn.MSELoss
是一个继承于nn.Module
的损失函数类,它定义了如何计算均方误差(Mean Square Error)criterion = nn.MSELoss
是实例化这个类,创建了一个criterion
对象,用来计算具体的损失
关于计算图的分析
- 前向传播
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d
-> flatten -> linear -> relu -> linear -> relu -> linear
-> MSELoss
-> loss - 反向传播
当调用 loss.backward() 时,PyTorch 将计算损失相对于计算图中所有requires_grad=True
参数的梯度。
这些梯度累积到每个参数的.grad
属性中。 - 追踪
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
这些代码行展示了如何追踪计算图中的节点,从而理解梯度是如何从损失函数向前传播到每个网络参数的。在训练过程中,PyTorch自动处理这种追踪和计算梯度的过程,但理解这个过程有助于调试和优化神经网络模型。<MseLossBackward0 object at 0x7871c20a1cf0> <AddmmBackward0 object at 0x7871c250c5e0> <AccumulateGrad object at 0x7872a05ebd90>
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模块常见的几个功能做一个说明:
- 数据预处理:在把图像文件输入到模型之前,transforms可以提供一些预处理的操作,如裁剪,旋转,缩放等
- 数据增强:为了避免过拟合,提升样本的多样性,transforms提供了旋转,翻转,颜色抖动等随机变换来生成新的样本
- 数据标准化:为了提升训练的速度和模型的稳定性,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.5x−0.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)
,先区分一下outputs
和outputs.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
- 将模型和张量分配到 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上
- 你可以使用
- 使用
DataParallel
进行多 GPU 运算- 默认情况下,PyTorch 只会使用一个 GPU。然而,为了利用多个 GPU,可以使用
torch.nn.DataParallel
model = nn.DataParallel(model)
DataParallel
允许你在多个 GPU 上并行地执行模型的前向和反向传播运算。这种并行处理可以显著加快训练和推理过程,特别是对于大型模型或数据集。
- 默认情况下,PyTorch 只会使用一个 GPU。然而,为了利用多个 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上即可