EasyHPC - PyTorch入门教程【笔记】

内容来源:超算习堂 (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

  • TensorFunction是相互联系的,它们共同建立了一个非循环的计算图;
  • 其中的每一个变量都有一个.grad_fn属性,该属性引用了一个已经创建了TensorFunction

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×21izi=43i(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) xiout=23(xi+1)

∂ o u t ∂ x i ∣ x i = 1 = 3 \left. \frac{\partial_{out}}{\partial_{x_i}}\right|_{x_i=1} = 3 xiout 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=weightlearning_rategradient

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():调用损失函数lossbackward()方法进行反向传播,计算模型各参数相对于损失函数的梯度。

  • 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=weightlearningrategradient

实现的代码如下:

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.dataf的值,是一个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')

在训练神经网络时用来监控训练过程的损失函数值的变化情况

  1. running_loss += loss.item():将当前 batch 的损失函数值加到 running_loss 变量中,loss.item() 是获取当前 batch 的损失函数值的方法。
  2. if i % 2000 == 1999::当当前 batch 的索引值 i 是 2000 的倍数减1时,执行下面的代码,即每训练完 2000 个 mini-batches 就输出一次损失函数值
  3. print('[%d, %5d] loss: %.3f' % (epoch + 1, i + 1, running_loss / 2000)):输出当前训练 epoch 和 mini-batch 的索引值以及当前的平均损失函数值,其中 %d 表示整数占位符,%5d 表示占 5 个字符的整数占位符,%.3f 表示保留 3 位小数的浮点数占位符。
  4. 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_correctclass_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]。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/9588.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

实现流程化办公,可了解一下可视化报表开源

当前,实现流程化办公早已成为众多中小企业的发展目标和趋势。可以借助什么样的软件平台实现这一目标?低代码技术平台拥有可视化操作界面、够灵活、易维护等优势特点,在助力企业实现流程化办公、数字化转型方面具有重要的应用价值和推动作用。…

[华为OD] C卷 田忌赛马 DFS 200

题目: 给定两个只包含数字的数组a, b,调整数组a里面数字的顺序,使得尽可能多的a[i] >b[i]。 数组a和b中的数字各不相同。 输出所有可以达到最优结果的a数组的数量 输入描述 输入的第一行是数组a中的数字,其中只包含数字,每…

【0002day】HistCite的使用

HistCite是一个很好的软件,可以找出一个领域中具有代表性的几篇文章。同时使用也比较方便,因而我学习了一下它,并做一下记录。 分析过程 首先,需要选择核心数据库,然后依据关键字进行检索。 检索出来后,将…

Secure Transformer Inference Made Non-interactive

目录 1.概述2.Attention2.1 Matrix multiplication (ciphertext-plaintext).2.2 Matrix multiplication (ciphertext-ciphertext)2.3 Placement of bootstrapping3.SIMD密文压缩和解压缩4.SIMD槽折叠5.实验结果 1.概述 我们提出了NEXUS,这是第一个用于安全变压器推…

变老相机app

变老相机app 在手机上使用“变老相机”app,其中的时光穿梭功能可以生成10岁、20岁、50岁、70岁的照片 目的 得到未来自己的照片,能够更有效地督促我们为老年的自己存款。

记录minio的bug(Object name contains unsupported characters.)

场景是我将后端服务从121.xxx.xxx.xxx服务器上转移到了另一台服务器10.xxx.xxx.xxx 但图片都还在121.xxx.xxx.xxx服务器上,同样我10.xxx.xxx.xxx也安装了minio并且我的后端服务配置的minio地址也是10.xxx.xxx.xxx 此时有一个业务通过minio客户端获取图片&#xf…

矩阵和空间变换理解

矩阵和空间变换 把向量和矩阵相乘看作是空间变换,是其中一种看法 代数角度:向量的一行和矩阵的一列逐项相乘再相加等于新向量的一项 w代表原来坐标轴和新坐标轴之间的变换关系,而a和b体现的是原来向量的关系 矩阵代表的是旧坐标和新坐标之间…

Wireshark明文抓取

目录 原理 配置 1、配置环境变量 2、Wireshark配置 原理 SSLKEYLOGFILE是一个用于记录SSL/TLS会话中使用的密钥的文件。它主要用于调试和分析SSL/TLS协议。当启用了SSLKEYLOGFILE配置,系统会将所有SSL/TLS会话中使用的密钥记录到指定的文件中。这些密钥可以用来…

day04—java基础之方法详解

方法概述 方法是具有特定功能的代码集合,由于我们是把重复的代码写到方法当中,以后要用直接调用方法即可,不需要再写一遍了。代码相当于只写了一遍。所以可以提高代码的复用性。如果要修改代码,我们也只要修改一处即可&#xff0…

Qt常用基础控件总结

一、按钮部件 按钮部件共同特性 Qt 用于描述按钮部件的类、继承关系、各按钮的名称和样式,如下图: 助记符:使用字符"&“可在为按钮指定文本标签时设置快捷键,在&之后的字符将作为快捷键。比如 “A&BC” 则 Alt+B 将成为该按钮的快捷键,使用”&&qu…

springcloud整合网关(springcloud-gateway)

pom引入依赖 <dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!-- 服务注册 --><dependency><groupId>com.alibaba.cloud</groupId&…

Linux增加硬盘分区并挂载(各个云平台操作)

第一部分&#xff0c;增加硬盘 1.购买硬盘并选择云服务器 输入lsblk 命令后即可看到刚刚添加的硬盘了 vdb就是新添加的硬盘名称了 第二部分 对硬盘进行分区处理 然后对新建磁盘进行分区 输入命令fdisk /dev/vdb 输入lsblk -f 命令查看刚刚建好的分区(看到多余的sdc不用在意…

设计模式Java实现-工厂模式

✨这里是第七人格的博客✨小七&#xff0c;欢迎您的到来~✨ &#x1f345;系列专栏&#xff1a;设计模式&#x1f345; ✈️本篇内容: 工厂模式✈️ &#x1f371;本篇收录完整代码地址&#xff1a;https://gitee.com/diqirenge/design-pattern &#x1f371; 楔子 记得刚…

WAGO系统 远程代码执行漏洞复现(CVE-2023-1698)

0x01 产品简介 WAGO是一家专业从事电气互连、自动化和接口电子技术的公司。 0x02 漏洞概述 在 WAGO 的多个产品中,一个漏洞允许未经身份验证的远程攻击者创建新用户并更改设备配置,可导致远程RCE、拒绝服务等使整个系统受损。 0x03 影响范围 751-9301 Compact Controll…

2W,3KVDC隔离 定电压输入,稳压单、双路输出DC-DC模块电源——TPI-2W 系列

TPI-2W系列产品是专门针对PCB上需要与输入电源隔离的电源应用场合而设计的。该产品适用于&#xff1a;1&#xff09;输入电源的电压变化≤5%&#xff1b;2&#xff09;输入输出之间要求隔离电压≥3000VDC&#xff1b;3&#xff09;对输出电压稳定和输出纹波噪声要求高.

达梦数据刷盘测试

达梦数据库为了保证数据故障恢复的一致性&#xff0c;REDO 日志的刷盘必须在数据页刷盘之前进行。 下面我们通过测试来验证是不是这样 执行我们事先准备的SHELL脚本 可以看到第一次strings文件没有输出&#xff0c;说明刚写的数据在数据库的BUFFER缓冲区内&#xff0c;还没有刷…

Screeps工程化之配置化

目录 前言一、抽取配置项二、读取配置项 前言 Screeps中所有代码都会在一个tick&#xff08;游戏内的世间&#xff09;内执行完成&#xff0c;想要做到代码的高度复用&#xff0c;和隔离各个房间creep的行为就需要将部分代码进行配置化&#xff0c;本文仅为作者本人的游戏思路…

机器学习算法 - 逻辑回归

逻辑回归是一种广泛应用于统计学和机器学习领域的回归分析方法&#xff0c;主要用于处理二分类问题。它的目的是找到一个最佳拟合模型来预测一个事件的发生概率。以下是逻辑回归的一些核心要点&#xff1a; 基本概念 输出&#xff1a;逻辑回归模型的输出是一个介于0和1之间的…

翻译《The Old New Thing》 - The performance cost of reading a registry key

The performance cost of reading a registry key - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20060222-11/?p32193 Raymond Chen 2006年2月22日 读取注册表键的性能成本 注册表是一个方便的场所&#xff0c;它以统一且多线程安全的方式…