内容来源:超算习堂 (easyhpc.net)
文章目录
- 01 Tensors
- 环境要求
- 1.1 Tensors
- 1.1.1 直接创建tensor
- 1.1.2 在现有tensor中创建tensor
- 1.1.3 从NumPy中创建tensor
- 1.2 基本运算
- 1.2.1 使用运算符
- 1.2.2 调用方法
- 1.3 CUDA Tensors
- 02 Autograd
- 2.1 Tensor
- 2.2 Gradient
- 03 Neural Network
- 3.1 网络的定义
- 3.2 损失函数
- 3.3 反向传播
- 3.4 更新权重
- 04 Training a Classifier
- 4.1 使用torchvision下载和标准化CIFAR10数据集
- 4.2 定义一个卷积神经网络
- 4.3 定义损失函数以及相应的优化器
- 4.4 在训练集上训练神经网络
- 4.5 在测试集上测试模型性能
- 05 Data Parallelism
- 5.1 初始化参数
- 5.2 创建一个随机的数据集
- 5.3 DataParallel对象
- 5.4 运行模型
01 Tensors
PyTorch是一个基于Python语言的科学计算库,主要的目标是:
- 作为NumPy的替代者,可以有效使用GPU资源;
- 为深度学习研究者提供一个灵活、快速的开发平台。
环境要求
安装PyTorch根据不同的操作系统、语言等有多种安装方式,下面简单介绍Python3语言,基于pip安装PyTorch的流程:
1、安装python,pip:
sudo apt-get update
sudo apt-get install -y python3 python3-pip
2、安装不含CUDA的版本(0.4):
sudo pip3 install http://download.pytorch.org/whl/cpu/torch-0.4.0-cp36-cp36m-linux_x86_64.whl
sudo pip3 install torchvision
另外的安装命令可以查阅 Installing PyTorch
1.1 Tensors
Tensor是PyTorch类似于NumPy中ndarray的数据结构,特别之处在于tensor可以利用GPU进行加速计算。引入依赖包:
from __future__ import print_function
import torch
import numpy
1.1.1 直接创建tensor
创建5×3未初始化矩阵
x = torch.empty(5, 3)
print(x)
创建5×3随机初始化矩阵
x = torch.rand(5, 3)
print(x)
创建全零矩阵,且指定元素类型
x = torch.zeros(5, 3, dtype=torch.long)
print(x)
从列表开始创建tensor
x = torch.tensor([5.2, 1])
print(x)
1.1.2 在现有tensor中创建tensor
从现有的tensor中创建tensor(属性会被复用)
x = x.new_ones(5, 3, dtype=torch.double)
print(x)x = torch.randn_like(x, dtype=torch.float)
print(x)
tensor的形状
print(x.size())
1.1.3 从NumPy中创建tensor
NumPy -> PyTorch
a = np.ones(5)
b = torch.from_numpy(a)
print (b)
PyTorch -> NumPy
a = torch.ones(5)
b = a.numpy()
print (b)
1.2 基本运算
Tensor在运算上支持多种语法,以下例子以加法进行展开
1.2.1 使用运算符
x = torch.ones(5, 3)
y = torch.ones(5, 3)
print (x + y)
1.2.2 调用方法
print (torch.add(x, y))
提供一个输出的tensor
result = torch.empty(5, 3)
torch.add(x, y, out=result)
print (result)
将x
加到y
上
y.add_(x)
print (y)
方法中带_
后缀的,都是in-place
的操作
Pytorch中inplace操作_二十米的博客-CSDN博客
1.3 CUDA Tensors
当设备允许时,tensor对象可以调用.to
方法将数据移动到其它设备上。
# let us run this cell only if CUDA is available
# We will use ``torch.device`` objects to move tensors in and out of GPU
if torch.cuda.is_available():device = torch.device("cuda") # a CUDA device objecty = torch.ones_like(x, device=device) # directly create a tensor on GPUx = x.to(device) # or just use strings ``.to("cuda")``z = x + yprint(z)print(z.to("cpu", torch.double)) # ``.to`` can also change dtype together!
02 Autograd
autograd
包是PyTorch中构建神经网络的核心。
autograd
包提供了在tensor上自动求导的操作,并且它是一个“define-by-run”的框架,也就是说,代码的执行顺序决定了反向传播(backprop)的过程。
2.1 Tensor
torch.Tensor
对象是操作的核心,
- 当你把tensor的
.requires_grad
属性设为True
,那框架就会开始追踪你在tensor上的所有操作; - 当你需要完成计算时,你可以调用
.backward()
方法,然后框架将自动完成梯度的计算; - 在这个tensor上的梯度会累积到
.grad
属性中。
为了停止tensor追踪操作的行为(对内存的消耗),
- 你可以手动调用
.detach()
使得计算历史从tensor中分离出来,同时tensor以后的计算过程也不会遭到记录。 - 除此之外,还可以用
with torch.no_grad():
来包裹代码块,这在验证模型的过程中特别有用,因为有些模型可能会含有可训练的参数(requires_grad=True
),但是显然我们不需要计算它的梯度。
另外一个重要的概念是Function
。
Tensor
和Function
是相互联系的,它们共同建立了一个非循环的计算图;- 其中的每一个变量都有一个
.grad_fn
属性,该属性引用了一个已经创建了Tensor
的Function
。
当Tensor
是一个标量时(只有一个元素),你不需要指定tensor的形状,但是当tensor含有多个元素时则需要显式指定匹配的形状。
创建一个记录计算历史的tensor
import torch
x = torch.ones(2, 2, requires_grad=True)
进行一次运算,查看与之相关的Function
对象
y = x + 1
print (y.grad_fn)
2.2 Gradient
进行更复杂的计算
z = y * y * 3
out = z.mean()
现在来进行计算梯度的实验,计算out
的梯度,即: d ( o u t ) d ( x ) \frac{d(out)}{d(x)} d(x)d(out)
out.backward()
print(x.grad)
计算过程是
o u t = 1 2 × 2 ∑ i z i = 3 4 ∑ i ( x i + 1 ) 2 out = \frac{1}{2 \times 2} \sum_{i} z_i = \frac{3}{4} \sum_{i} (x_i + 1)^2 out=2×21∑izi=43∑i(xi+1)2
∂ o u t ∂ x i = 3 2 ( x i + 1 ) \frac{\partial_{out}}{\partial_{x_i}} = \frac{3}{2}(x_i + 1) ∂xi∂out=23(xi+1)
∂ o u t ∂ x i ∣ x i = 1 = 3 \left. \frac{\partial_{out}}{\partial_{x_i}}\right|_{x_i=1} = 3 ∂xi∂out xi=1=3
使用with torch.no_grad():
可以停止自动求导
print(x.requires_grad)
print((x ** 2).requires_grad)with torch.no_grad():print((x ** 2).requires_grad)
03 Neural Network
PyTorch中神经网络可以通过torch.nn
包进行构建,nn
依赖于autograd
对模型进行定义及其对神经网络的求导。nn.Module
包括了神经网络的层级设计,以及名为一个forward(input)
的调用方法,该方法会返回一个output
对象。
以图像分类网络为例:
这是一个前馈神经网络,网络接受一个输入input
,然后计算结果一层一层往后传递,最终得到输出结果output
。
一个神经网络典型的训练过程应该包括:
- 定义神经网络(应包含可学习的模型参数/权值)
- 将数据集迭代地输入神经网络
- 在神经网络中,逐层地计算输入的数据
- 计算最后的损失(真实值与预测值之间的差距)
- 梯度的反向传播
- 更新网络的参数,常见的更新规则: w e i g h t = w e i g h t − l e a r n i n g _ r a t e ∗ g r a d i e n t weight = weight - learning\_rate * gradient weight=weight−learning_rate∗gradient
3.1 网络的定义
一个网络定义的例子:
import torch
import torch.nn as nn
import torch.nn.functional as F# 这段代码定义了一个神经网络模型类 `Net`,继承自 `nn.Module`。
# 该模型包含两个卷积层和三个全连接层,用于对图像进行分类。
class Net(nn.Module):# 初始化函数def __init__(self):super(Net, self).__init__()# 定义了两个卷积层 `self.conv1` 和 `self.conv2`,# 分别将输入的单通道图像转换为6通道的特征图和16通道的特征图。# 这两个卷积层的卷积核大小均为5x5。self.conv1 = nn.Conv2d(1, 6, 5)self.conv2 = nn.Conv2d(6, 16, 5)# an affine operation: y = Wx + b# 定义了三个全连接层 `self.fc1`、`self.fc2` 和 `self.fc3`,# 分别将输入的特征图转换为120维、84维和10维的向量。# 这三个全连接层的作用是将卷积层提取的特征进行分类。self.fc1 = nn.Linear(16 * 5 * 5, 120)self.fc2 = nn.Linear(120, 84)self.fc3 = nn.Linear(84, 10)# 在前向传播函数 `forward` 中def forward(self, x):# 首先对输入的图像进行卷积操作,并使用 ReLU 激活函数进行非线性变换。# 然后使用 2x2 的最大池化操作对特征图进行降维,以减小模型的参数量。# Max pooling over a (2, 2) windowx = F.max_pool2d(F.relu(self.conv1(x)), (2, 2))# If the size is a square you can only specify a single numberx = 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# 定义了一个辅助函数 `num_flat_features`,用于计算特征图展开后的向量维度。# 这个函数的作用是帮助自动计算全连接层的输入维度。def num_flat_features(self, x):size = x.size()[1:] # all dimensions except the batch dimensionnum_features = 1for s in size:num_features *= sreturn num_featuresnet = Net() # 创建了一个 `Net` 的实例 `net`
print(net) # 输出该模型的结构。
输出结果:
Net((conv1): Conv2d(1, 6, kernel_size=(5, 5), stride=(1, 1))(conv2): Conv2d(6, 16, kernel_size=(5, 5), stride=(1, 1))(fc1): Linear(in_features=400, 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)
)
仅需要定义好forward
函数,求导过程将会自动开始执行。想要获得模型中可学习的参数,可调用net.parameters()
函数:
# 通过 `net.parameters()` 获取神经网络中的所有可学习参数,返回一个包含所有参数的迭代器。然后,将迭代器转换成列表,便于后续操作。
params = list(net.parameters())# 打印出参数的数量,即神经网络中可学习参数的个数。
print(len(params))# 打印出第一个参数的形状,即第一个可学习参数的大小,通常是一个张量。
print(params[0].size()) # conv1's .weight
输出结果:
10
torch.Size([6, 1, 5, 5])
将32×32大小的随机数矩阵作为网络的输入:
# nSamples x nChannels x Height x Width
input = torch.randn(1, 1, 32, 32)
out = net(input)
print(out)
输出结果:
tensor([[-0.0089, -0.0514, 0.0059, 0.1412, -0.1543, 0.0494, -0.0966,-0.1150, -0.0986, -0.1103]])
将所有参数的梯度buffer置零,然后将随机梯度反向传播:
# `net.zero_grad()` 用于清空网络中所有参数的梯度,以便进行新的反向传播计算。
net.zero_grad()# `out.backward(torch.randn(1, 10))` 是对 `out` 进行反向传播计算,并且传入了一个随机张量作为 `out` 关于某些标量的梯度。
# 这里的随机张量相当于是一个虚拟的损失值,用于计算梯度。在实际的训练中,这个随机张量应该被替换成真实的损失值的梯度。
out.backward(torch.randn(1, 10))
3.2 损失函数
损失函数将(预测值,真实值)作为输入,然后计算评估模型与真实情况之间的差距。loss functions列出了nn
包中可用的损失函数。
output = net(input) # torch.Size([1, 10])
target = torch.arange(1, 11) # 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)
这段代码用于计算模型预测结果与目标值之间的均方误差(MSE)损失。具体来说:
-
output = net(input)
:使用神经网络模型net
对输入input
进行预测,得到输出output
。 -
target = torch.arange(1, 11)
:生成一个1到10的序列作为目标值,这里只是为了演示,实际应用中通常是根据具体任务定义的。 -
target = target.view(1, -1)
:将目标值的形状调整为与输出output
相同,即(1, 10)
。在这里,
target
张量原本是一个形状为(10,)
的一维张量,表示模型需要预测的目标值。调用view(1, -1)
方法将其形状调整为(1, 10)
的二维张量,其中第一维大小为1,表示这是一个批次大小为1的输入数据。第二维大小为10,与output
张量的第二维大小相同,表示模型需要预测的10个目标值。需要注意的是,
view()
方法只是改变了张量的形状,而不改变张量中的元素。因此,target
张量中的元素顺序不变,只是被重新排列成了一个二维张量。 -
criterion = nn.MSELoss()
:定义一个均方误差(MSE)损失函数。 -
loss = criterion(output, target)
:计算预测输出output
与目标值target
之间的均方误差损失。
输出结果:
tensor(39.2273)
然后当你调用loss
的.grad_fn
属性时,你可以看到loss
上的计算图:
input -> conv2d -> relu -> maxpool2d -> conv2d -> relu -> maxpool2d-> view -> linear -> relu -> linear -> relu -> linear-> MSELoss-> loss
更具体一点可以一步步往回输出:
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中,每个张量都有一个.grad_fn
属性,代表它的梯度计算函数。对于计算图中的每个节点(操作),都有一个对应的梯度计算函数,这些函数被存储在.grad_fn
属性中。这些梯度计算函数构成了计算图,用于自动求导。
对于上面的代码,loss
是由nn.MSELoss()
计算得到的,因此loss.grad_fn
是一个MSELoss
对象,代表均方误差损失函数。
loss.grad_fn.next_functions
是一个元组,包含了计算图中与该节点相连的下一层节点的梯度计算函数。在这个例子中,loss
的下一层节点是一个Linear
操作,因此loss.grad_fn.next_functions[0][0]
是一个Linear
对象,代表线性变换操作。
同理,loss.grad_fn.next_functions[0][0].next_functions[0][0]
代表的是Linear
操作的下一层节点,即ReLU
操作。
3.3 反向传播
为了实现反向传播,我们需要调用函数loss.backward()
,在调用之前需要清空梯度的buffer,否则梯度会被累加。
net.zero_grad() # zeroes the gradient buffers of all parametersprint('conv1.bias.grad before backward')
print(net.conv1.bias.grad)loss.backward()print('conv1.bias.grad after backward')
print(net.conv1.bias.grad)
这段代码主要是用于梯度计算和反向传播的过程。具体来说:
-
net.zero_grad()
:将模型net
的所有参数的梯度缓冲区清零,以避免梯度累加的影响。梯度累加指在一次迭代中多次计算梯度,然后将这些梯度相加,最后再更新模型参数。这种方法可以在内存有限的情况下,使用更大的batch size,从而加快训练速度。
但是,梯度累加也有一些缺点。首先,它会占用更多的内存,因为需要存储多个梯度。其次,它会导致训练过程中的噪声增加,因为每个梯度都是在不同的batch上计算得到的,这些batch可能存在差异。最重要的是,梯度累加会导致模型更新不稳定,因为多个梯度相加可能会产生较大的梯度值,从而导致模型参数更新过大或不稳定。
因此,为了避免这些问题,我们通常不建议使用梯度累加,而是使用更大的GPU内存或者分布式训练等方法来加速训练。
-
print('conv1.bias.grad before backward')
:输出卷积层conv1
的偏置项bias
的梯度值,此时梯度值为0。 -
loss.backward()
:调用损失函数loss
的backward()
方法进行反向传播,计算模型各参数相对于损失函数的梯度。 -
print('conv1.bias.grad after backward')
:输出卷积层conv1
的偏置项bias
的梯度值,此时梯度值已经被计算出来。 -
print(net.conv1.bias.grad)
:输出卷积层conv1
的偏置项bias
的梯度值,即反向传播计算得到的梯度值。
通过这段代码,我们可以了解到反向传播的过程:在前向传播计算出模型的输出后,根据输出和目标值计算出损失函数的值,然后通过反向传播计算出模型各参数相对于损失函数的梯度,最终利用梯度下降等优化算法来更新模型参数,使得损失函数的值逐渐减小,从而提高模型的预测准确率。
报错解决:RuntimeError: Found dtype Long but expected Float_唐僧爱吃唐僧肉的博客-CSDN博客
输出结果:
conv1.bias.grad before backward
tensor([ 0., 0., 0., 0., 0., 0.])
conv1.bias.grad after backward
tensor([ 0.0501, 0.1040, -0.1200, 0.0833, 0.0081, 0.0120])
3.4 更新权重
在实际场景中,最简单实用的更新规则是随机梯度下降: w e i g h t = w e i g h t − l e a r n i n g r a t e ∗ g r a d i e n t weight = weight - learning_rate * gradient weight=weight−learningrate∗gradient
实现的代码如下:
learning_rate = 0.01
for f in net.parameters():f.data.sub_(f.grad.data * learning_rate) # sub_表示in-place操作,即直接对f.data进行修改
这段代码用于手动更新神经网络的参数,具体来说:
learning_rate = 0.01
:设置学习率为0.01,即每次更新的步长。for f in net.parameters():
:遍历神经网络net
的所有参数。f.data.sub_(f.grad.data * learning_rate)
:使用梯度下降法(Gradient Descent)更新参数,即将参数的值减去学习率乘以其对应的梯度值。这里使用了sub_()
函数实现就地减法操作,即直接在原有的参数值上进行更新。
通过手动更新参数,可以实现自定义的优化算法,而不是使用预定义的优化器(如SGD、Adam等)。但是需要注意的是,手动更新参数需要对参数的梯度进行手动计算,这对于复杂的神经网络来说是非常困难的。因此,通常情况下我们会使用PyTorch提供的优化器来自动更新参数,从而简化优化过程。
sub_()
是Tensor
类中的一个函数,用于将Tensor
对象中的所有元素减去一个给定的标量或另一个Tensor
对象中的元素。
在这里,f
是一个Parameter
对象,它是Tensor
的子类,表示神经网络模型中的可训练参数。f.data
是f
的值,是一个Tensor
对象,它存储了f
的值。f.grad
也是一个Tensor
对象,它存储了f
关于损失函数的梯度值。
需要注意的是,在每次更新参数之后,我们需要手动将梯度缓存清零,以避免梯度累加的影响。这可以通过调用optimizer.zero_grad()来实现。
在实际中,你可能还会用到其它的更新方式,一些常用的更新算法(Nesterov-SGD,Adam,RMSProp等)都已经被预先实现在torch.optim
包中,使用方法如下:
import torch.optim as optim# create your optimizer
optimizer = optim.SGD(net.parameters(), lr=0.01)# in your training loop:
optimizer.zero_grad() # zero the gradient buffers
output = net(input)
loss = criterion(output, target)
loss.backward()
optimizer.step() # Does the update
这段代码演示了使用torch.optim
中的SGD
优化器来更新神经网络模型的参数。具体来说:
optimizer = optim.SGD(net.parameters(), lr=0.01)
:创建一个SGD
优化器,将神经网络模型net
的参数作为待优化参数传递给优化器,并设置学习率为0.01
。optimizer.zero_grad()
:将优化器中所有参数的梯度缓存清零。output = net(input)
:使用神经网络模型net
对输入input
进行预测,得到输出output
。loss = criterion(output, target)
:计算预测输出output
与目标值target
之间的损失。loss.backward()
:计算损失函数对模型参数的导数。optimizer.step()
:根据计算得到的梯度更新模型参数,即执行参数更新操作。
SGD
(Stochastic Gradient Descent)是深度学习中常用的优化算法之一,也是最基础的优化算法之一。它的基本思想是在每个训练步骤中,随机选择一小部分样本计算梯度,通过梯度下降的方式更新模型权重,以最小化损失函数。
在PyTorch中,可以使用optim.SGD
类来实现SGD
优化器。optim.SGD
类的构造函数接受两个参数:
params
:需要优化的参数列表,可以通过net.parameters()
方法获取。lr
:学习率,即每次梯度下降时的步长大小。
在训练过程中,可以通过调用optimizer.zero_grad()
方法来清空所有参数的梯度,然后计算模型输出和损失,调用loss.backward()
方法计算梯度,最后调用optimizer.step()
方法执行一步梯度下降更新模型参数。
04 Training a Classifier
本实验将沿以下步骤展开:
4.1 使用torchvision下载和标准化CIFAR10数据集
Torchvision是PyTorch的一个包,它包含了一些用于计算机视觉任务的实用函数和数据集。它提供了一些预处理图像的工具,可以用于数据增强和训练数据的准备。此外,它还提供了一些经典的计算机视觉模型,如AlexNet、VGG、ResNet和Inception等,可以用于图像分类、目标检测、分割和生成等任务。
可以使用以下代码查看torch和torchvision的版本:
import torch
import torchvisionprint("Torch version:", torch.__version__)
print("Torchvision version:", torchvision.__version__)# 输出结果
# Torch version: 2.0.0
# Torchvision version: 0.15.0
使用torchvision
载入CIFAR10
CIFAR-10是一个常用的图像分类数据集,由10个类别的60000张32x32彩色图像组成,每个类别有6000张图像。这些类别包括飞机、汽车、鸟、猫、鹿、狗、青蛙、马、船和卡车。数据集被分为训练集和测试集,其中训练集包含50000张图像,测试集包含10000张图像。CIFAR-10数据集是计算机视觉领域中最常用的数据集之一,被广泛用于图像分类、目标识别、图像分割等任务的研究和算法评估。CIFAR10数据集的大小约为163 MB。
import torch
import torchvision
import torchvision.transforms as transforms
这段代码是用来导入PyTorch深度学习框架及其相关的库和模块,其中
- torch是PyTorch的核心库,提供了各种张量操作和自动求导功能;
- torchvision是PyTorch的视觉库,提供了图像数据处理、数据加载和预处理等功能;
- transforms是torchvision库中的一个模块,提供了各种图像数据预处理的方法,如缩放、裁剪、旋转、翻转、标准化等。
通过导入这些库和模块,可以方便地进行深度学习任务,尤其是图像分类任务。
torchvision
的数据集输出是PILImage
图像(范围[0,1]),我们需要将其转换为Tensor对象接受的范围([-1,1])
transform = transforms.Compose([transforms.ToTensor(),transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))])trainset = torchvision.datasets.CIFAR10(root='./data', train=True,download=True, transform=transform)
trainloader = torch.utils.data.DataLoader(trainset, batch_size=4,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=4,shuffle=False, num_workers=2)classes = ('plane', 'car', 'bird', 'cat','deer', 'dog', 'frog', 'horse', 'ship', 'truck')
这段代码是在使用PyTorch进行图像分类任务时的数据预处理部分。具体来说,这段代码使用了PyTorch中的torchvision模块来加载CIFAR-10数据集,并对数据集进行了一些预处理操作。
其中,transforms.Compose()
函数用于将多个数据预处理操作组合在一起,这里使用了两个操作:
ToTensor()
将数据转换为张量格式,Normalize()
对数据进行标准化处理,使得数据的均值为0.5,标准差为0.5。
接下来,使用torchvision.datasets.CIFAR10()
函数来加载CIFAR-10数据集,其中train=True
表示加载训练集,train=False
表示加载测试集。同时,通过transform
参数传入上一步定义的数据预处理操作。
最后,使用torch.utils.data.DataLoader()
函数将数据集转化为可迭代的数据加载器,用于训练和测试模型。其中batch_size
参数表示每次迭代加载的数据量,shuffle
参数表示是否打乱数据顺序,num_workers
参数表示使用多少个进程来加载数据。
最后,定义了一个classes
列表,其中包含了CIFAR-10数据集中的10个类别。
结果输出:
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./data\cifar-10-python.tar.gz
100.0%
Extracting ./data\cifar-10-python.tar.gz to ./data
Files already downloaded and verified
在第一次运行时,程序会从指定的链接下载CIFAR-10数据集的压缩文件,并将其保存到本地目录./data
下。下载完成后,程序会自动将压缩文件解压到./data
目录下,然后就可以使用PyTorch提供的数据集类torchvision.datasets.CIFAR10
加载数据集了。
在第二次运行时,因为数据集已经下载并解压到了本地,所以程序会直接跳过下载和解压的步骤,而是使用已经下载好的数据集进行训练和测试。因此,程序会输出"Files already downloaded and verified"。
4.2 定义一个卷积神经网络
# 将上一节实验中神经网络的定义稍作修改(单通道改成3通道)
import torch.nn as nn
import torch.nn.functional as Fclass Net(nn.Module):def __init__(self):super(Net, self).__init__()self.conv1 = nn.Conv2d(3, 6, 5) # 输入通道数为3,输出通道数为6,卷积核大小为5x5self.pool = nn.MaxPool2d(2, 2)self.conv2 = nn.Conv2d(6, 16, 5) # 输入通道数为6,输出通道数为16,卷积核大小为5x5# 最后一层卷积层的输出是一个大小为16x5x5的张量,其中16表示卷积核的数量,5x5表示每个卷积核的输出大小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 = x.view(-1, 16 * 5 * 5) # 将16x5x5的特征图展开成一维向量x = F.relu(self.fc1(x))x = F.relu(self.fc2(x))x = self.fc3(x)return xnet = Net()
4.3 定义损失函数以及相应的优化器
简单起见,我们使用最简单的交叉熵损失函数(Cross-Entropy)和随机梯度下降(SGD)优化模型:
import torch.optim as optimcriterion = nn.CrossEntropyLoss()
optimizer = optim.SGD(net.parameters(), lr=0.001, momentum=0.9)
# 学习率控制了每一次参数更新的幅度
# 动量则是一种加速算法,可以加快模型收敛的速度。
4.4 在训练集上训练神经网络
我们迭代地从trainloader
对象中取数据,然后将数据输入到神经网络中,得到输出,计算损失,不断地优化参数:
# 对数据集进行多次循环训练(每个循环称为一个 epoch)
for epoch in range(2): running_loss = 0.0# 遍历 trainloader 中的每个 batch,对 batch 中的数据进行训练并更新参数。for i, data in enumerate(trainloader, 0):# get the inputs# 遍历训练数据集中的每个 batch,# 每个 batch 包含一个 inputs 和一个 labels。inputs, labels = data# zero the parameter gradients# 将 optimizer 的梯度清零,以避免梯度累加optimizer.zero_grad()# forward + backward + optimizeoutputs = net(inputs)loss = criterion(outputs, labels)loss.backward() # 反向传播误差,计算每个参数的梯度optimizer.step() # 根据梯度下降算法更新参数值# print statistics# 在训练神经网络时用来监控训练过程的损失函数值的变化情况running_loss += loss.item()if i % 2000 == 1999: # print every 2000 mini-batchesprint('[%d, %5d] loss: %.3f' %(epoch + 1, i + 1, running_loss / 2000))running_loss = 0.0print('Finished Training')
在训练神经网络时用来监控训练过程的损失函数值的变化情况:
running_loss += loss.item()
:将当前 batch 的损失函数值加到running_loss
变量中,loss.item()
是获取当前 batch 的损失函数值的方法。if i % 2000 == 1999:
:当当前 batch 的索引值i
是 2000 的倍数减1时,执行下面的代码,即每训练完 2000 个 mini-batches 就输出一次损失函数值。print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000))
:输出当前训练 epoch 和 mini-batch 的索引值以及当前的平均损失函数值,其中%d
表示整数占位符,%5d
表示占 5 个字符的整数占位符,%.3f
表示保留 3 位小数的浮点数占位符。running_loss = 0.0
:将running_loss
变量清零,以便下一次计算平均损失函数值。
结果输出:
[1, 2000] loss: 2.199
[1, 4000] loss: 1.856
[1, 6000] loss: 1.688
[1, 8000] loss: 1.606
[1, 10000] loss: 1.534
[1, 12000] loss: 1.488
[2, 2000] loss: 1.420
[2, 4000] loss: 1.384
[2, 6000] loss: 1.336
[2, 8000] loss: 1.351
[2, 10000] loss: 1.309
[2, 12000] loss: 1.277
Finished Training
报错解决:解决RuntimeError: An attempt has been made to start a new process before…办法 - 知乎 (zhihu.com)
4.5 在测试集上测试模型性能
我们在训练数据集上训练了两轮,现在来验证模型的实际效果。
correct = 0 # 记录正确预测的数量
total = 0 # 记录总共预测的数量
with torch.no_grad(): # 使用`torch.no_grad()`上下文管理器,关闭梯度计算,以减少内存消耗和加速计算for data in testloader:images, labels = dataoutputs = net(images)_, predicted = torch.max(outputs.data, 1) # 获取每个样本在每个类别上的分数,然后选取分数最高的类别作为预测结果total += labels.size(0) # 将当前批次中的样本数量累加到总样本数中correct += (predicted == labels).sum().item()# 在 `(predicted == labels)` 中,`predicted == labels` 的结果是一个由 True 和 False 组成的张量,表示模型预测的结果是否正确。`.sum()` 会将所有 True 的数量加起来,即表示正确的样本数。`.item()` 则将这个数量转化为 Python 中的标量值,方便打印输出。print('Accuracy of the network on the 10000 test images: %d %%' % (100 * correct / total)) # %d %%用于将正确率格式化为整数百分比形式# `%%` 表示输出一个百分号。这是因为在 Python 中,如果要输出一个百分号,需要使用两个百分号来转义。
这段代码是用来计算神经网络在测试集上的准确率的。具体来说,代码中的for
循环遍历了测试集中的所有数据,然后将数据输入到神经网络中进行预测。对于每个预测结果,代码通过torch.max
函数取出最大值及其对应的索引,即预测的标签。然后将预测的标签与真实标签进行比较,如果相同,则认为预测正确,将正确预测的数量累加到correct
变量中。最后,代码计算正确率并输出。
torch.no_grad()
是一个上下文管理器(context manager),用于在执行代码时禁用梯度计算。在这个上下文管理器内部,PyTorch将不会跟踪计算图中的梯度信息,这样可以提高代码的执行效率,特别是在测试模型时,不需要计算梯度,可以加快模型的推理速度。在训练模型时,需要计算梯度,所以不需要使用torch.no_grad()
。
torch.max()
函数返回输入张量中所有元素的最大值和相应的索引。在这里,outputs.data
是网络输出的结果,第二个参数1
指定了在第一个维度上进行最大值计算,即在每个样本的输出结果中找到最大的那个值及其对应的索引。
_
是一个占位符,表示我们不需要使用这个值,只需要获取最大值对应的索引,即predicted
。在 Python 中,通常使用_
表示一个变量的值不需要使用。举个例子,如果
outputs.data
的形状是(batch_size, num_classes)
,那么torch.max(outputs.data, 1)
的返回值是一个元组,包含两个张量:第一个张量是每个样本的最大值,形状是(batch_size,)
;第二个张量是每个样本最大值对应的索引,形状也是(batch_size,)
。predicted
就是这个索引张量。
结果输出:
Accuracy of the network on the 10000 test images: 53 %
53%的准确率对比随机猜一个结果(1/10)还是要好一些的;接下来再看看具体到某一类上,准确率到底表现如何:
class_correct = list(0. for i in range(10)) # 定义一个长度为10的列表,初始化为0,用于存储每个类别的正确分类数
class_total = list(0. for i in range(10)) # 定义一个长度为10的列表,初始化为0,用于存储每个类别的样本总数
with torch.no_grad():for data in testloader:images, labels = dataoutputs = net(images)_, predicted = torch.max(outputs, 1)c = (predicted == labels).squeeze() # 由于张量c的维度可能为1,使用squeeze()函数将数组中维度为1的维度去掉for i in range(4): # 遍历每个batch中的前4个样本# `DataLoader`在每次遍历数据集时都会对数据进行随机打乱,以增加模型的泛化能力# 所以每次获取到的`data`中的数据是随机的,因此range(4)获取的数据也是随机的label = labels[i] # 获取当前样本的标签class_correct[label] += c[i].item() # 如果当前样本被正确分类,则将对应类别的正确分类数加1class_total[label] += 1 # 将对应类别的样本总数加1for i in range(10):print('Accuracy of %5s : %2d %%' % (classes[i], 100 * class_correct[i] / class_total[i]))
在这个代码段中,
(predicted == labels)
是一个维度为1的张量,其中每个元素对应一个测试样本的预测是否正确。然而,在后面的代码中,我们需要将c
与标签label
对应起来,以便在class_correct
和class_total
中更新对应类别的计数器。因此,我们需要将c
从维度为1的张量转换为一个标量,以便在后面的代码中进行索引。这里使用了.squeeze()
函数将维度为1的张量压缩为标量。
data
是由DataLoader
迭代器返回的一个batch数据,而batch
是指一次性输入到神经网络中的一组数据,通常是多个数据样本一起组成的一个张量。在深度学习中,通常我们为了提高训练效率,会将数据集分成若干个batch,每个batch包含多个数据样本,然后将每个batch依次输入到神经网络中进行训练。
每个
data
对应一个batch,而range(4)
是指每个batch中的前4个数据。在这个代码片段中,这个循环只迭代了每个batch中的前4个数据,因为我们只想查看每个类别的前4个预测结果。如果需要查看整个batch的结果,可以将循环范围改为range(batch_size)
,其中batch_size
是batch大小。
输出结果:
Accuracy of plane : 60 %
Accuracy of car : 75 %
Accuracy of bird : 33 %
Accuracy of cat : 50 %
Accuracy of deer : 26 %
Accuracy of dog : 47 %
Accuracy of frog : 54 %
Accuracy of horse : 66 %
Accuracy of ship : 48 %
Accuracy of truck : 70 %
05 Data Parallelism
在PyTorch利用GPU资源其实很便捷,首先指定GPU:
device = torch.device("cuda:0")
然后将model
迁移到GPU上:
model.to(device)
将tensor
也迁移到GPU上:
tensor = tensor.to(device)
最后将模型放到并行环境(DataParallel
)中,
model = nn.DataParallel(model)
以上就是PyTorch中数据并行的核心思路。
5.1 初始化参数
引入依赖包、定义初始化参数:
import torch
import torch.nn as nn
from torch.utils.data import Dataset, DataLoader# Parameters and DataLoaders
input_size = 5
output_size = 2batch_size = 30
data_size = 100# Device
device = torch.device("cuda:0" if torch.cuda.is_available() else "cpu")
5.2 创建一个随机的数据集
简单实现__getitem__
方法即可:
class RandomDataset(Dataset): # RandomDataset类继承了PyTorch中的Dataset类def __init__(self, size, length):self.len = lengthself.data = torch.randn(length, size)def __getitem__(self, index):return self.data[index]def __len__(self):return self.lenrand_loader = DataLoader(dataset=RandomDataset(input_size, 100),batch_size=batch_size, shuffle=True)
init()
方法初始化RandomDataset类的实例。它接受两个参数:size和length。其中,size表示每个数据样本的大小,length表示数据集的大小。在这个方法中,我们使用torch.randn()方法生成一个大小为(length, size)的随机数据集,作为RandomDataset的数据。getitem()
方法用于获取数据集中的一个数据样本。它接受一个index参数,表示要获取的数据样本的索引。在这个方法中,我们直接返回数据集中的第index个数据样本。len()
方法返回数据集的大小。- 最后,我们使用DataLoader类来创建一个名为rand_loader的数据加载器。它接受一个RandomDataset类的实例作为数据集,batch_size参数表示每个mini-batch的大小,shuffle参数表示是否对数据进行洗牌。在这个例子中,我们将数据集洗牌,并将每个mini-batch的大小设置为batch_size=30。
5.3 DataParallel对象
对于这个简单demo,我们的模型中只有一层线性变换,但是DataParallel
是可以应用于任一模型上的(包括CNN、RNN、Capsule Net等)。
建立一个简单线性模型:
class Model(nn.Module):# Our modeldef __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
(如果存在GPU)将模型迁移到GPU上:
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 GPUsmodel = nn.DataParallel(model)model.to(device)
5.4 运行模型
正式运行模型:
for data in rand_loader:input = data.to(device)output = model(input)print("Outside: input size", input.size(),"output_size", output.size())
运行在CPU上时,可以看到:
In Model: input size torch.Size([30, 5]) output size torch.Size([30, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([30, 5]) output size torch.Size([30, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([30, 5]) output size torch.Size([30, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([10, 5]) output size torch.Size([10, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])
运行在2个GPU上时,可以看到:
Let's use 2 GPUs!In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])In Model: input size torch.Size([15, 5]) output size torch.Size([15, 2])
Outside: input size torch.Size([30, 5]) output_size torch.Size([30, 2])In Model: input size torch.Size([5, 5]) output size torch.Size([5, 2])In Model: input size torch.Size([5, 5]) output size torch.Size([5, 2])
Outside: input size torch.Size([10, 5]) output_size torch.Size([10, 2])
因为tensor
的维度从[30, xx]变成了2个GPU上的[15, xx]和[15, xx]。