Pytorch深度学习入门 | 系列(二)
☀️教程:B站我是土堆
7 神经网络的基础结构
关于神经网络的工具主要在torch.nn(Neural NetWork)包下面
7.1 神经网络的基本骨架nn.Module
nn.Module在Containers包下,这个包下有神经网络的骨架Module类,想要搭建任何神经网络都要继承这个类,除了Containers剩下的包都是往神经网络中填充的东西
在我们自己创建神经网络时,除了要继承 nn.Module
类外,还要重写 __init__()
和实现 forward()
两个函数,代码示例:
import torch
import torch.nn as nn
class JChuanNetWork(nn.Module):
def __init__(self) -> None:
super().__init__()
def forward(self, input):
output = input + 1
return output
if __name__ == '__main__':
jchuan_net = JChuanNetWork()
x = torch.tensor(1.0)
output = jchuan_net(x)
print(output)
当定义一个自己的神经网络类,并继承nn.Module
时,需要实现该类的forward
方法。这是因为nn.Module
类中的forward
方法是一个抽象方法,需要用户自己定义模型的前向传播过程。当实例化这个神经网络并传入一个输入x
时,实际上就是在调用该神经网络的forward
方法。这是因为nn.Module
类中实现了__call__
方法,而这个方法会调用forward
方法。所以,当你调用jchuan_net(x)
时,实际上是在执行jchuan_net.forward(x)
。
7.2 卷积层
在torch.nn下面有一个torch.nn.functional包,这个包里面的东西和torch.nn中的是对应的,只不过torch.nn中对torch.nn.functional的东西进行了进一步的封装,使它们更方便我们的使用,我们先使用torch.nn.functional中的卷积函数来介绍一下卷积操作。
这个conv2d函数可以帮我们完成卷积操作,它有很多参数,其中最主要的是input
和weight
,分别表示输入的数据和kernel(卷积核),接下来我们介绍用法:
import torch
import torch.nn.functional as F
# torch.tensor() 函数是 PyTorch 中用于创建张量(tensors)的主要方法之一 可以帮我们将数据转换为tensor数据类型
# 创建一个5 * 5 的张量作为输入
input = torch.tensor([[1, 2, 0, 3, 1],
[0, 1, 2, 3, 1],
[1, 2, 1, 0, 0],
[5, 2, 3, 1, 1],
[2, 1, 0, 1, 1]])
# 创建一个3 * 3的张量作为卷积核
kernel = torch.tensor([[1, 2, 1],
[0, 1, 0],
[2, 1, 0]])
# 由于conv2d函数接收的input和weight(kernel)参数的tensor shape都是四维的
# 我们需要对tensor进行reshape
# input 参数的shape必须是(minibatch, in_channels, iH, iW)
input = torch.reshape(input, (1, 1, 5, 5))
# weight 参数的shape必须是(out_channels, in_channels/groups, kH, kW)
kernel = torch.reshape(kernel, (1, 1, 3, 3))
print(input.shape)
print(kernel.shape)
# 调用F中的conv2d函数进行卷积操作
output = F.conv2d(input, kernel, stride=1)
print(output)
# 调整stride参数为 2 也就是步长为 2
output2 = F.conv2d(input, kernel, stride=2)
print(output2)
# 调整padding参数为 1 也就是周围填充一圈 0
output3 = F.conv2d(input, kernel, padding=1)
print(output3)
可以看到结果如下:
接下来,我们返回来看一下torch.nn中的卷积层类:
torch.nn.Conv2d类是支持平面图像进行卷积操作的类,它的参数如下:
- in_channels:输入图像的通道数
- out_channels:通过卷积之后,产生的输出的通道数,可以理解为卷积核的数量,假如in_channels设置为1,out_channels设置为2时,输入图像会分别经过两个卷积核来得到两个输出图像,这两个输出图像组合成一个2通道的输出
- kernel_size:卷积核的大小
- stride:卷积的步长
- padding:在边缘进行填充的层数,默认为0
- padding_mode:以什么方式填充,一般选择填充0
- dilation:空洞卷积
- groups:指定输入通道和输出通道之间的连接模式,默认为1,表示每个输入通道都连接到每个输出通道
- bias:给卷积后的输出加一个偏置
代码示例:
import torch
import torchvision
import torch.nn as nn
from torch.nn import Conv2d
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
transform = torchvision.transforms.Compose([
torchvision.transforms.ToTensor()
])
datasets = torchvision.datasets.CIFAR10(root="./dataset", train=False, transform=transform, download=True)
dataloader = DataLoader(dataset=datasets, batch_size=64)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
# 定义一个卷积层
self.conv1 = Conv2d(in_channels=3, out_channels=6, kernel_size=3, stride=1, padding=0)
def forward(self, input):
output = self.conv1(input)
return output
tudui = TuDui()
writer = SummaryWriter("./logs_conv2d")
step = 0
for data in dataloader:
imgs, targets = data
output = tudui(imgs)
print(imgs.shape) # torch.Size([64, 3, 32, 32])
print(output.shape) # torch.Size([64, 6, 30, 30])
writer.add_images("input", imgs, step)
# 由于我们得到的输出是6个通道的,不能添加到TensorBoard中,所以我们先进行一个reshape
# 我们想让shape从[64, 6, 30, 30] -> [xxx, 3, 30, 30],我们可以指定第一个参数为-1
# 这样它会根据后面的值来自动计算
output = torch.reshape(output, (-1, 3, 30, 30))
writer.add_images("output", output, step)
step = step + 1
writer.close()
这段代码搭建了一个小的神经网络,内容是对CIFAR10数据集进行了卷积操作,我们可以从控制台看到,输入的图像是(64,3, 32, 32)的,经过卷积之后变为了(64, 6, 30, 30),我们还将输入和输出图片放到TensorBoard中进行了查看,结果如下:
7.3 最大池化层
最大池化是池化的一种,也是我们经常使用的进行下采样的工具,在保留主要特征的同时减少参数来加快计算速度等等,其操作就是在池化核对应区域中选取最大值,在torch.nn中也有最大池化层,我们常用的是torch.nn.MaxPool2d:
它的参数如下:
- kernel_size:池化核的大小
- stride:池化的步长
- padding:在边缘进行填充的层数,默认为0
- dilation:空洞卷积,即在进行池化的时候,池化区域是否是隔几个值对应一个,看官网有GIF说明:https://github.com/vdumoulin/conv_arithmetic/blob/master/README.md
- return_indices:是否返回记录最大值的索引数组
- ceil_mode:是否采用向上取整,默认是False即向下取整
来看一个简单的代码示例:
import torch
import torch.nn as nn
# 若我们希望生成的张量中的数据是浮点数,可以设置dtype
input = torch.tensor([[1, 2, 0, 3, 1],
[0, 1, 2, 3, 1],
[1, 2, 1, 0, 0],
[5, 2, 3, 1, 1],
[2, 1, 0, 1, 1]], dtype=torch.float32)
# 第一个数-1表示由reshape函数自动计算batch_size
input = torch.reshape(input, (-1, 1, 5, 5))
print(input.shape)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
self.maxpool1 = nn.MaxPool2d(kernel_size=3, ceil_mode=True)
def forward(self, input):
output = self.maxpool1(input)
return output
tudui = TuDui()
output = tudui(input)
print(output)
这里简单演示了一下最大池化的操作,注意最大池化不能对整数型数据进行处理,所以我们把生成的张量转换成了float类型。输出的结果为:
可以看到,它是保留了每个区域的最大值,接下来我们对图片进行最大池化来观察一下结果:
import torch
import torchvision
import torch.nn as nn
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True)
dataloader = DataLoader(dataset=dataset, batch_size=64)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
self.maxpool1 = nn.MaxPool2d(kernel_size=3, ceil_mode=True)
def forward(self, input):
output = self.maxpool1(input)
return output
tudui = TuDui()
writer = SummaryWriter("logs_maxpool")
step = 0
for data in dataloader:
imgs, targets = data
writer.add_images("input", imgs, step)
output = tudui(imgs)
writer.add_images("output", output, step)
print("step:{}".format(step))
step = step + 1
writer.close()
在TensorBoard中观察结果,可以看到,output的图片比input的图片模糊了很多,这就是最大池化造成的:
7.4 非线性激活
非线性激活在神经网络中的作用主要是引入非线性变换,即引入非线性的因素,从而使神经网络能够学习更加复杂的函数关系。神经网络的每一层都包含权重和偏置项(bias),通过线性变换(加权和)将输入转换为输出。如果没有非线性激活函数,多个线性层的组合仍然只会产生线性变换,限制了网络的表示能力。
这里拿ReLU激活函数来举例:
- inplace:是否对原来的输入变量进行替换,如果为True则直接改变输入变量的值,如果为False则将返回一个新的值,而输入变量的值不变
此函数表示当输入大于等于0时等于本身,小于0的时候等于0的,其图像为:
来看一个简单的代码示例:
import torch
import torchvision
import torch.nn as nn
input = torch.tensor([[1, -0.5],
[-1, 3]])
input = torch.reshape(input, (-1, 1, 2, 2))
print(input.shape)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
# inplace设置为False表示不对原来的input进行替换而返回一个新的值
self.relu1 = nn.ReLU(inplace=False)
def forward(self, input):
output = self.relu1(input)
return output
tudui = TuDui()
output = tudui(input)
print(output)
这里使用了Relu非线性激活函数,可以看到控制台输出的结果,当值小于0时都被截断为0了:
接下来我们使用Sigmod激活函数来对图像进行处理,观察一下结果:
import torch
import torch.nn as nn
import torchvision.datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(), download=True)
dataloader = DataLoader(dataset=dataset, batch_size=64)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
# inplace设置为False表示不对原来的input进行替换而返回一个新的值
self.sigmod1 = nn.Sigmoid()
def forward(self, input):
output = self.sigmod1(input)
return output
tudui = TuDui()
writer = SummaryWriter("logs_sigmod")
step = 0
for data in dataloader:
imgs, targets = data
writer.add_images("input", imgs, step)
output = tudui(imgs)
writer.add_images("output", output, step)
step = step + 1
writer.close()
打开TensorBoard查看结果:
在TensorBoard可以对比看出经过Sigmod激活函数后图片的效果
Sigmod激活函数:
7.5 线性层及其他层介绍
线性层是一种常见的神经网络层,它将输入张量与权重矩阵相乘,并加上一个可学习的偏置向量。
线性层中的torch.nn.Linear
是PyTorch中用于定义线性变换(也称为全连接层或仿射层)的类,其数学公式表示为:
其中,input是输入张量,weight是权重矩阵,bias是偏置向量,out是输出张量。
官方文档中的介绍是:
- in_features:输入的样本数量,见下图红框圈起来的节点
- out_features:输出的样本数量,见下图蓝框圈起来的节点
- bias:是否设置偏置
接下来代码演示一下线性层的作用:
import torch
import torch.nn as nn
import torchvision.datasets
from torch.utils.data import DataLoader
from torch.utils.tensorboard import SummaryWriter
dataset = torchvision.datasets.CIFAR10("./dataset", train=False, transform=torchvision.transforms.ToTensor(),
download=True)
dataloader = DataLoader(dataset=dataset, batch_size=64)
class TuDui(nn.Module):
def __init__(self) -> None:
super().__init__()
# 输入为196608 输出为10
self.linear1 = nn.Linear(196608, 10, bias=True)
def forward(self, input):
output = self.linear1(input)
return output
tudui = TuDui()
for data in dataloader:
imgs, targets = data
# 我们输入要是一维的 所以我们可以用flatten函数把输入图像展开成一行
# 使用reshape也可以 torch.reshape(1, 1, 1, -1) 不过太麻烦了
input = torch.flatten(imgs)
print(input.shape) # torch.Size([196608])
output = tudui(input)
print(output.shape)
可以看到,经过线性层之后,输出图像的大小都变为了1 × 10:
正则化层:
引入正则化层后可以加快神经网络的训练速度:
特殊的神经网络层:
下面框出的层,都是已经研究出来的在特定任务下效果非常好的层(就是论文提出的层结构,Pytorch把它们做成现成的层供我们使用了)
其他的层就不再介绍,等使用到的时候可以关注官方文档。
8 神经网络的搭建
8.1 搭建小实战和Sequential的使用
本节将搭建一个比较简单的网络模型,来解决CIFAR10数据集分类的问题,并顺便介绍Containers包下的Sequetial的使用。
我们将按照下面图片的一个CIFAR10 的神经网络模型结构来搭建:
我们在最大池化层时,已经给出了kernel_size并且输入的尺寸大小按照这个kernel_size来计算和输出的尺寸大小是一致的,也就是说不需要设置别的参数了。但是在卷积层时,按照32 × 32大小的输入来看,经过卷积后的输出应该是尺寸变小的,但是这里却没有变,说明我们要设置其他的参数来保证尺寸不变,我们可以根据官网文档中给出的公式来计算出各个参数需要设置的值:
根据神经网络结构的图片,我们可以知道kernel_size为5、Hin为32、Hout为32,这里没有说空洞卷积,所以dilation默认为1,这样我们可以推算一下padding参数和stride参数:
将已知的值带入到上面公式中可以得到下面这个方程:[(27 + 2*padding)+ 1] / stride = 32
下面,我们假设stride为2,那么得到的padding值会很大,非常不合理,所以设置stride为1,解方程得到padding的值为2,这样我们就得到了做卷积时所有参数的值。这样就可以编写代码如下:
import torch
import torch.nn as nn
class CIFAR10Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.conv1 = nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2)
self.maxpool1 = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2)
self.maxpool2 = nn.MaxPool2d(2)
self.conv3 = nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2)
self.maxpool3 = nn.MaxPool2d(2)
self.flatten1 = nn.Flatten()
self.linear1 = nn.Linear(in_features=1024, out_features=64)
self.linear2 = nn.Linear(in_features=64, out_features=10)
def forward(self, input):
input = self.conv1(input)
input = self.maxpool1(input)
input = self.conv2(input)
input = self.maxpool2(input)
input = self.conv3(input)
input = self.maxpool3(input)
input = self.flatten1(input)
input = self.linear1(input)
output = self.linear2(input)
return output
cifar10_net = CIFAR10Net()
# torch.ones指定生成的张量中的值全为1
input = torch.ones((64, 3, 32, 32))
output = cifar10_net(input)
print(output.shape) # torch.Size([64, 10]) 表示64张图片 每个图片都变成了10
Sequential的使用:从上面的代码可以发现,forward函数中的代码写的太繁琐了,我们可以使用Containers包下的Sequential,可以将我们的各个层打包成一个模型,作用类似transforms.Composes,修改后的代码如下:
import torch
import torch.nn as nn
from torch.nn import Sequential
class CIFAR10Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.model1 = Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(in_features=1024, out_features=64),
nn.Linear(in_features=64, out_features=10)
)
def forward(self, input):
output = self.model1(input)
return output
cifar10_net = CIFAR10Net()
print(cifar10_net)
# torch.ones指定生成的张量中的值全为1
input = torch.ones((64, 3, 32, 32))
output = cifar10_net(input)
print(output.shape) # torch.Size([64, 10]) 表示64张图片 每个图片都变成了10
我们可以输出一下该神经网络,可以发现在控制台会有该网络的结构,并且在Sequential包装之后,还会对每个层进行标号:
除了在控制台打印我们的神经网络来查看网络结构以外,还可以使用TensorBoard来可视化的展示我们的神经网络结构(计算图),代码如下:
writer = SummaryWriter("logs_cifar10net")
# add_graph函数,我们要传入我们的神经网络对象以及传入我们的模拟输入数据input
writer.add_graph(cifar10_net, input)
writer.close()
打开TensorBoard观察结果,可以看到有Graph这一栏,里面有我们的神经网络结构,点开可以查看里面的细节:
8.2 损失函数与反向传播
torch.nn中有Loss Functions这个包,里面有我们需要的损失函数,损失函数的作用可以简单的理解为:
- 计算实际输出和目标之间的差距
- 为我们更新输出提供一定的依据(反向传播)
下面我们拿最简单的损失函数torch.nn.L1Loss和MSELoss来做一个演示:
编写代码如下,注意要求的tensor值为浮点数:
import torch
inputs = torch.tensor([1, 2, 3], dtype=torch.float32)
inputs = torch.reshape(inputs, (1, 1, 1, 3))
targets = torch.tensor([1, 2, 5], dtype=torch.float32)
targets = torch.reshape(targets, (1, 1, 1, 3))
loss_l1 = torch.nn.L1Loss()
result_l1 = loss_l1(inputs, targets)
print(result_l1) # tensor(0.6667)
loss_mse = torch.nn.MSELoss()
result_mse = loss_mse(inputs, targets)
print(result_mse) # tensor(1.3333)
可以看到L1Loss的计算方式是:[(1-1)+(2-2)+(5-3)]/3 = 0.666...
,MSELoss的计算方式是:[(1-1)^2+(2-2)^2+(5-3)^2]/3 = 1.3333...
在分类问题中最常用的函数还是交叉熵损失函数,Pytorch中是在torch.nn.CrossEntropyLoss
:
此交叉熵损失函数可以按照下面图片手写的计算过程来理解(此过程是按照我自己的理解写的):
下面我们来简单的使用代码举个例子:
import torch
outputs = torch.tensor([0.1, 0.2, 0.3])
targets = torch.tensor([1])
print(outputs.shape)
outputs = torch.reshape(outputs, (1, 3))
loss_cross = torch.nn.CrossEntropyLoss()
result = loss_cross(outputs, targets)
print(result) # tensor(1.1019)
注意:这里的log是以e为底的,所以其计算公式可以化简为:
我们可以将此损失函数放到之前搭建的CIFAR10Net神经网络中来使用:
import torch
import torch.nn as nn
import torchvision.datasets
from torch.nn import Sequential
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor(), download=True)
dataloader = DataLoader(dataset, 1)
class CIFAR10Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.model1 = Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(in_features=1024, out_features=64),
nn.Linear(in_features=64, out_features=10)
)
def forward(self, input):
output = self.model1(input)
return output
# 定义交叉熵损失函数
loss = nn.CrossEntropyLoss()
cifar10_net = CIFAR10Net()
print(cifar10_net)
for data in dataloader:
imgs, targets = data
output = cifar10_net(imgs)
print("output:{}".format(output))
print("targets:{}".format(targets))
# 计算交叉熵损失 得出的就是误差值
result_loss = loss(output, targets)
print("loss:{}".format(result_loss))
查看控制台,可以看到,我们对每张图片都计算交叉熵损失,可以理解为误差。
光计算出loss是没有用的,我们要利用计算出的loss来进行反向传播(计算各个参数的梯度grad)从而更新参数,代码如下:
# 定义交叉熵损失函数
loss = nn.CrossEntropyLoss()
cifar10_net = CIFAR10Net()
print(cifar10_net)
for data in dataloader:
imgs, targets = data
output = cifar10_net(imgs)
print("output:{}".format(output))
print("targets:{}".format(targets))
# 计算交叉熵损失 得出的就是误差值
result_loss = loss(output, targets)
# 反向传播计算各个参数的梯度
result_loss.backward()
print("loss:{}".format(result_loss))
注意:我们是对计算出的result_loss进行反向传播的,而不是loss函数
反向传播后,我们可以打断点来观察cifar10_net中的参数:
在还未执行反向传播的时候,查看我们神经网络中第一个卷积层中的权重参数,其梯度目前为None,但我们执行下一步反向传播时,可以看到,梯度已经计算出来了:
所以,这里的梯度只有经过反向传播计算之后才会有。
后面,我们会利用优化器,优化器就是根据这些梯度来对我们神经网络中的一些参数进行更新,最终达到降低loss的目的。
8.3 优化器
优化器可以根据梯度对神经网络中的参数进行调整,在PyTorch中,torch.optim
是一个用于实现各种优化算法的模块。优化器的作用是通过调整模型参数来最小化(或最大化)定义的损失函数。
在torch.optim
模块中有许多的优化器算法,我们拿SGD(随机梯度下降)算法来举例一下:
在优化器中,最重要的参数有两个:
- params:神经网络的所有参数
- lr:(learning rate)学习率,也就是我们梯度下降的速率
接下来,我们使用代码演示一下如何使用:
import torch
import torch.nn as nn
import torchvision.datasets
from torch.nn import Sequential
from torch.utils.data import DataLoader
dataset = torchvision.datasets.CIFAR10("./dataset", False, torchvision.transforms.ToTensor(), download=True)
dataloader = DataLoader(dataset, 1)
class CIFAR10Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.model1 = Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(in_features=1024, out_features=64),
nn.Linear(in_features=64, out_features=10)
)
def forward(self, input):
output = self.model1(input)
return output
loss = nn.CrossEntropyLoss()
cifar10_net = CIFAR10Net()
# 定义一个优化器 放入神经网络的参数 设置学习率
optim = torch.optim.SGD(cifar10_net.parameters(), lr=0.01)
# 我们在训练神经网络的时候,将数据集遍历一遍来更新参数是远远不够的
# 我们需要设置epoch来指定训练的轮次 假设我们训练20轮
for epoch in range(20):
# 定义一个每一轮的总loss
running_loss = 0.0
for data in dataloader:
imgs, targets = data
output = cifar10_net(imgs)
result_loss = loss(output, targets)
# 在每次根据梯度更新完参数以后 需要将梯度清零 防止影响下一轮的训练
optim.zero_grad()
result_loss.backward()
# 计算出梯度后 调用优化器对参数进行更新
optim.step()
running_loss += result_loss
# 输出running_loss 观察loss有没有下降
print(running_loss)
我们观察控制台,可以看到每一轮的loss值在逐渐下降:
在我们实际训练神经网络模型的时候,我们的epoch会是成千上万次的训练,这里只是展示一下用法。
8.4 Pytorch提供的现有的网络模型
Pytorch给我们提供了一些已有的网络模型,我们可以去使用,或者去修改网络模型。这里我们主要讲解torchvision包下已有的模型 torchvision.models
中关于分类的模型 :
可以看到,Pytorch提供了很多现成的models,我们这里拿最常用的VGG来讲解:
VGG神经网络有好多个版本,其中最常用的是VGG16或VGG19,其使用也很简单,我们先来看一下这两个参数:
- pretrained:是否设置为预训练好的模型,这里预训练好的模型是在ImageNet中训练的
- progress:是否显示下载的进度条
我们想使用的话也很简单,代码如下:
import torch
import torchvision
# 由于ImageNet数据集太大 下载要140+G 所以我们不使用了
# train_dataset = torchvision.datasets.ImageNet("./dataset_imgage_net", split='train')
# 加载vgg16模型
vgg16_false = torchvision.models.vgg16(pretrained=False)
# 设置下载目录为D盘
torch.hub.set_dir("D:\\DeepLearning\\Pytorch_models")
# 加载已经训练好的vgg16模型 已经训练好的模型要下载
vgg16_true = torchvision.models.vgg16(pretrained=True)
注意:由于vgg16模型的下载默认会下载到C盘,我们可以通过torch.hub.set_dir
来更改下载位置
控制台输出的网络结构如下:
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(1): ReLU(inplace=True)
(2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(3): ReLU(inplace=True)
(4): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(5): Conv2d(64, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(6): ReLU(inplace=True)
(7): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(8): ReLU(inplace=True)
(9): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(10): Conv2d(128, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(11): ReLU(inplace=True)
(12): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(13): ReLU(inplace=True)
(14): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(15): ReLU(inplace=True)
(16): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(17): Conv2d(256, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(18): ReLU(inplace=True)
(19): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(20): ReLU(inplace=True)
(21): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(22): ReLU(inplace=True)
(23): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
(24): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(25): ReLU(inplace=True)
(26): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(27): ReLU(inplace=True)
(28): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
(29): ReLU(inplace=True)
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
(1): ReLU(inplace=True)
(2): Dropout(p=0.5, inplace=False)
(3): Linear(in_features=4096, out_features=4096, bias=True)
(4): ReLU(inplace=True)
(5): Dropout(p=0.5, inplace=False)
(6): Linear(in_features=4096, out_features=1000, bias=True)
)
)
已经训练好的模型和未训练的模型的参数是完全不同的,一般我们可以把VGG16作为前置的网络结构,来利用VGG16提取一些特殊的特征,在VGG16后面加一些网络结构来完成我们的任务。
8.5 现有网络模型的修改
由于ImageNet数据集太大,我们无法下载,所以不能验证VGG16,但是我们可以修改VGG16来应用到CIFAR10数据集上。
从上面VGG16的网络结构可以知道,最后一层是线性层(全连接层),输出的out_featrues为1000,是因为ImageNet是一个1000类的分类任务,而CIFAR10是10分类任务,所以我们可以修改最后一层的out_featrues为10,或者最后再加一个输入为1000输出为10的线性层,代码如下:
import torch
import torchvision
# 由于ImageNet数据集太大 下载要140+G 所以我们不使用了
# train_dataset = torchvision.datasets.ImageNet("./dataset_imgage_net", split='train')
# 加载vgg16模型
vgg16_false = torchvision.models.vgg16(pretrained=False)
# 设置下载目录为D盘
torch.hub.set_dir("D:\\DeepLearning\\Pytorch_models")
# 加载已经训练好的vgg16模型 已经训练好的模型要下载
vgg16_true = torchvision.models.vgg16(pretrained=True)
# (1)我们可以在vgg16_true神经网络的最后再加一个线性层
# 我们可以选择加在哪一层里面 如果是vgg16_true.add_module()的话 那么会加在最外层
# 如果是vgg16_true.classifier.add_module()的话 那么会加在classifier层的最下面
vgg16_true.classifier.add_module("add_linear", torch.nn.Linear(1000, 10))
print(vgg16_true)
# (2)我们可以修改vgg16_false神经网络的最后一层
vgg16_false.classifier[6] = torch.nn.Linear(4096, 10)
print(vgg16_false)
可以看到控制台的输出,我们的网络结构已经发生了改变:
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
......
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
......
(6): Linear(in_features=4096, out_features=1000, bias=True)
(add_linear): Linear(in_features=1000, out_features=10, bias=True)
)
)
VGG(
(features): Sequential(
(0): Conv2d(3, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1))
......
(30): MaxPool2d(kernel_size=2, stride=2, padding=0, dilation=1, ceil_mode=False)
)
(avgpool): AdaptiveAvgPool2d(output_size=(7, 7))
(classifier): Sequential(
(0): Linear(in_features=25088, out_features=4096, bias=True)
......
(6): Linear(in_features=4096, out_features=10, bias=True)
)
)
8.6 网络模型的保存与读取
Pytorch中网络模型的保存和读取有两种方式,一个是保存模型+参数,一个是只保存参数。我们用代码来演示一下:
import torch
import torchvision
vgg16 = torchvision.models.vgg16(pretrained=False)
# 保存方式一:模型 + 参数
torch.save(vgg16, "vgg16_method1.pth")
# 保存方式二:参数
torch.save(vgg16.state_dict(), "vgg16_method2.pth")
保存之后,在项目文件夹下会出现我们保存的模型:
接下来,编写读取模型的代码:
import torch
import torchvision
# 读取方式一:对应保存方式一
model = torch.load("vgg16_method1.pth")
print(model)
# 读取方式二:对应保存方式二
vgg16 = torchvision.models.vgg16(pretrained=False)
vgg16.load_state_dict(torch.load("vgg16_method2.pth"))
print(vgg16)
注意:保存读取方式一会有一些小陷阱,当我们要加载我们自己定义的网络模型的时候,必须在加载的文件中声明我们定义的类,不然加载不出来,例如:
model_save.py文件中:
class NetWork(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.conv1 = torch.nn.Conv2d(3, 64, 3)
def forward(self, input):
output = self.conv1(input)
return output
model = NetWork()
torch.save(model, "model_method1.pth")
model_load.py文件中:
model = torch.load("model_method1.pth")
这样加载我们保存的网络模型会报错,我们必须将代码修改为:
model_load.py文件中:
class NetWork(torch.nn.Module):
def __init__(self) -> None:
super().__init__()
self.conv1 = torch.nn.Conv2d(3, 64, 3)
def forward(self, input):
output = self.conv1(input)
return output
model = torch.load("model_method1.pth")
或者引入我们定义的网络模型:
from model_save import *
model = torch.load("model_method1.pth")
print(model)
这样就可以使用方法一来加载我们自定义的网络模型了,不过官方推荐的保存和加载方式是第二种方法,减少了要保存的文件的大小。
9 神经网络的训练
9.1 完整的模型训练套路
我们以CIFAR10数据集为例,来介绍一下完整的模型训练套路。代码如下:
model.py文件:
from torch import nn
from torch.nn import Sequential
class CIFAR10Net(nn.Module):
def __init__(self) -> None:
super().__init__()
self.model1 = Sequential(
nn.Conv2d(in_channels=3, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=32, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Conv2d(in_channels=32, out_channels=64, kernel_size=5, stride=1, padding=2),
nn.MaxPool2d(2),
nn.Flatten(),
nn.Linear(in_features=1024, out_features=64),
nn.Linear(in_features=64, out_features=10)
)
def forward(self, input):
output = self.model1(input)
return output
train.py文件:
import torch
import torch.nn as nn
import torchvision
from torch.utils.tensorboard import SummaryWriter
from model import *
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor
# 1. 准备数据集
train_data = torchvision.datasets.CIFAR10("../dataset", train=True, transform=ToTensor(), download=True)
test_data = torchvision.datasets.CIFAR10("../dataset", train=False, transform=ToTensor(), download=True)
# 查看数据集的数量(长度)
# train_data_size = len(train_data)
# test_data_size = len(test_data)
# print("训练数据集的长度是:{}".format(train_data_size))
# print("测试数据集的长度是:{}".format(test_data_size))
# 2. 利用 DataLoader来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 4. 创建网络模型
cifar10_net = CIFAR10Net()
# 5. 创建损失函数
loss_fn = nn.CrossEntropyLoss()
# 6. 创建优化器
learning_rate = 1e-2
optimizer = torch.optim.SGD(cifar10_net.parameters(), lr=learning_rate)
# 7. 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 10
# 可选1 添加TensorBoard
writer = SummaryWriter("./logs_cifar10_net")
for i in range(epoch):
print("-----------第{}轮训练开始-----------".format(i + 1))
# 8. 训练步骤开始
for data in train_dataloader:
imgs, targets = data
outputs = cifar10_net(imgs)
# 计算loss值
loss = loss_fn(outputs, targets)
# 优化器优化模型
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step += 1
if total_train_step % 50 == 0:
print("训练次数:{}, Loss:{}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)
# 9. 测试步骤开始
total_test_loss = 0
# torch.no_grad() 是一个上下文管理器,用于禁用梯度计算 当进入with后的代码块时,Pytorch会停止跟踪梯度
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
outputs = cifar10_net(imgs)
loss = loss_fn(outputs, targets)
total_test_loss += loss.item()
print("整体测试集上的Loss:{}".format(total_test_loss))
total_test_step += 1
writer.add_scalar("test_loss", total_test_loss, total_test_step)
# 10. 保存每一轮我们训练的模型
torch.save(cifar10_net, "train_model/cifar10_net_{}.pth".format(i+1))
writer.close()
我们首先看控制台,控制台打印了我们的训练次数以及对应次数的loss和每一轮训练后测试集的loss:
我们再看项目文件所在目录,已经创建了logs_cifar10_net文件夹来存放事件文件和train_model文件夹来存放每一轮训练后的模型:
最后,我们观察TensorBoard中解析事件文件绘制的train_loss和test_loss:
实际上,我们在分类问题中,最后只得到模型在测试集上的Loss还不够,我们想知道这个模型在测试集上预测的正确率,即accuracy,我们如何得到正确率呢?
分类问题中每一个batch_size经过神经网络后得到的是一列数值,例如二分类问题中,设定batch_size为2,经过神经网络的outputs假设为[0.1, 0.2]
[0.3, 0.4]
,每一个行中的值可以理解为该图片是这个类别的的“概率”,我们可以将outputs中的每一行通过Argmax变为该行最大值的索引位置(即预测的类别),然后将预测的类别与targets做比较,相等为1,不等为0,最后将所有结果相加除以batch_size的大小,即得到测试集这组数据预测的正确率,代码如下:
import torch
outputs = torch.tensor([[0.1, 0.2],
[0.3, 0.4]])
# argmax 参数dim为0是从上到下看 为1是从左到右看
# 作用:返回所在行/列最大值的索引位置
print(outputs.argmax(1)) # tensor([1, 1])
preds = outputs.argmax(1)
targets = torch.tensor([0, 1])
print(preds == targets) # tensor([False, True])
# 计算有多少个True
print((preds == targets).sum()) # tensor(1)
# 计算正确率
print((preds == targets).sum() / len(outputs)) # tensor(0.5000)
了解了如何计算accuracy后,我们修改train.py文件中的代码:
import torch
import torch.nn as nn
import torchvision
from torch.utils.tensorboard import SummaryWriter
from model import *
from torch.utils.data import DataLoader
from torchvision.transforms import ToTensor
# 1. 准备数据集
train_data = torchvision.datasets.CIFAR10("../dataset", train=True, transform=ToTensor(), download=True)
test_data = torchvision.datasets.CIFAR10("../dataset", train=False, transform=ToTensor(), download=True)
# 查看数据集的数量(长度)
train_data_size = len(train_data)
test_data_size = len(test_data)
# print("训练数据集的长度是:{}".format(train_data_size))
# print("测试数据集的长度是:{}".format(test_data_size))
# 2. 利用 DataLoader来加载数据集
train_dataloader = DataLoader(train_data, batch_size=64)
test_dataloader = DataLoader(test_data, batch_size=64)
# 4. 创建网络模型
cifar10_net = CIFAR10Net()
# 5. 创建损失函数
loss_fn = nn.CrossEntropyLoss()
# 6. 创建优化器
learning_rate = 1e-2
optimizer = torch.optim.SGD(cifar10_net.parameters(), lr=learning_rate)
# 7. 设置训练网络的一些参数
# 记录训练的次数
total_train_step = 0
# 记录测试的次数
total_test_step = 0
# 训练的轮数
epoch = 10
# 可选1 添加TensorBoard
writer = SummaryWriter("./logs_cifar10_net")
for i in range(epoch):
print("-----------第{}轮训练开始-----------".format(i + 1))
# 8. 训练步骤开始
# 这个将神经网络变为train模式只对部分层有作用 例如Dropout层
# 如果没有这些层 则写不写无所谓
cifar10_net.train()
for data in train_dataloader:
imgs, targets = data
outputs = cifar10_net(imgs)
# 计算loss值
loss = loss_fn(outputs, targets)
# 优化器优化模型
optimizer.zero_grad()
loss.backward()
optimizer.step()
total_train_step += 1
if total_train_step % 50 == 0:
print("训练次数:{}, Loss:{}".format(total_train_step, loss.item()))
writer.add_scalar("train_loss", loss.item(), total_train_step)
# 9. 测试步骤开始
# 这个将神经网络变为eval模式只对部分层有作用 例如Dropout层
# 如果没有这些层 则写不写无所谓
cifar10_net.eval()
total_test_loss = 0
total_accuracy = 0
# torch.no_grad() 是一个上下文管理器,用于禁用梯度计算 当进入with后的代码块时,Pytorch会停止跟踪梯度
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
outputs = cifar10_net(imgs)
loss = loss_fn(outputs, targets)
total_test_loss += loss.item()
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
print("整体测试集上的Loss:{}".format(total_test_loss))
print("整体测试集上的accuracy:{}".format(total_accuracy / test_data_size))
total_test_step += 1
writer.add_scalar("test_loss", total_test_loss, total_test_step)
writer.add_scalar("test_accuracy", total_accuracy, total_test_step)
# 10. 保存每一轮我们训练的模型
torch.save(cifar10_net, "train_model/cifar10_net_{}.pth".format(i + 1))
writer.close()
我们加上accuracy后,控制台会输出每一轮测试集在神经网络中的预测结果正确率,并且我们还把正确率写入事件文件中以供我们在TensorBoard中查看:
完整的模型训练套路就介绍完毕了,在之后我们需要写神经网络模型并训练的时候,可以参考上面的代码来编写。
9.2 利用GPU进行模型训练
我们想要使用GPU进行训练的话,有两种实现方法。
第一种方法:调用网络模型、数据(输入,标注)、损失函数的cuda()函数即可实现,主要代码展示如下:
......
# 4. 创建网络模型
cifar10_net = CIFAR10Net()
# 判断cuda是否可用 可用则使用GPU 不可用则使用CPU
if torch.cuda.is_available():
cifar10_net = cifar10_net.cuda()
# 5. 创建损失函数
loss_fn = nn.CrossEntropyLoss()
# 判断cuda是否可用 可用则使用GPU 不可用则使用CPU
if torch.cuda.is_available():
loss_fn = loss_fn.cuda()
......
for i in range(epoch):
cifar10_net.train()
for data in train_dataloader:
imgs, targets = data
# 判断cuda是否可用 可用则使用GPU 不可用则使用CPU
if torch.cuda.is_available():
imgs = imgs.cuda()
targets = targets.cuda()
outputs = cifar10_net(imgs)
# 计算loss值
loss = loss_fn(outputs, targets)
......
......
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
# 判断cuda是否可用 可用则使用GPU 不可用则使用CPU
if torch.cuda.is_available():
imgs = imgs.cuda()
targets = targets.cuda()
outputs = cifar10_net(imgs)
loss = loss_fn(outputs, targets)
total_test_loss += loss.item()
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
这样如果我们的电脑有GPU,就可以利用GPU进行神经网络的训练了。
第二种方法:调用网络模型、数据(输入,标注)、损失函数的to()函数并定义我们的device即可实现,主要代码展示如下:
......
# 首先定义训练的设备
# device = torch.device("cup") # 调用的cpu进行训练
# device = torch.device("cuda:0") # 调用第一张显卡进行训练
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
......
# 4. 创建网络模型
cifar10_net = CIFAR10Net()
# 使用to方法指定设备进行训练
cifar10_net = cifar10_net.to(device)
# 5. 创建损失函数
loss_fn = nn.CrossEntropyLoss()
# 使用to方法指定设备进行训练
loss_fn = loss_fn.to(device)
......
cifar10_net.train()
for data in train_dataloader:
imgs, targets = data
# 使用to方法指定设备进行训练
imgs = imgs.to(device)
targets = targets.to(device)
outputs = cifar10_net(imgs)
# 计算loss值
loss = loss_fn(outputs, targets)
......
......
with torch.no_grad():
for data in test_dataloader:
imgs, targets = data
# 使用to方法指定设备进行训练
imgs = imgs.to(device)
targets = targets.to(device)
outputs = cifar10_net(imgs)
loss = loss_fn(outputs, targets)
total_test_loss += loss.item()
accuracy = (outputs.argmax(1) == targets).sum()
total_accuracy += accuracy
......
这样我们也可以完成利用GPU进行训练的操作。实际上,方式二是我们常用的一种方式,并且在调用to()函数的时候,除了数据之外,我们也可以不重新赋值给原来的模型或损失函数。只有数据中的图片和标注需要重新赋值回去。
9.3 完整的模型验证(测试)套路
利用已经训练好的模型,给它提供输入,加载我们训练好的网络模型进行预测,代码举例如下:
import torch
import torchvision
from model import *
from PIL import Image
# 导入我们训练用的数据集 这里导入数据集只是为了使用里面的类别数组classes 方便我们输出预测结果
dataset = torchvision.datasets.CIFAR10("../dataset")
# 导入我们要测试的图片
image_path = "./test_images/test_dog.jpg"
image = Image.open(image_path).convert("RGB")
# 将图片调整为神经网络的输入格式
transforms = torchvision.transforms.Compose([
torchvision.transforms.Resize((32, 32)),
torchvision.transforms.ToTensor()
])
image_tensor = transforms(image)
image_tensor = torch.reshape(image_tensor, (1, 3, 32, 32))
print(image_tensor.shape)
# 加载我们训练好的神经网络
model = CIFAR10Net()
model.load_state_dict(torch.load("./train_model/cifar10_net_30.pth"))
model.eval()
with torch.no_grad():
output = model(image_tensor)
result_index = output.argmax(1)
print(result_index)
print("预测的结果是:{}".format(dataset.classes[result_index]))
我们给神经网络传入的图片为:
观察控制台的输出,可以看到,已经成功的预测出来图像的类别:
注意:在将数据放入model中进行计算之前,最好加上model.eval()
和with torch.no_grad():
这两行代码。
当我们的电脑配置不高,不能进行神经网络的训练时,可以在Colab上训练神经网络,每个账号每周都会有免费的时长以供我们使用谷歌的服务器进行神经网络的训练,训练完成后可以将保存的网络模型加载到本地来进行使用。
到此为止,Pytorch入门系列就结束了,虽然学习了很多内容,也了解了神经网络训练的完整套路等等内容,但是会发现在看GitHub中优秀的开源项目时,代码还是很难理解。这时不用怕,先看懂大致的框架就可以,复杂的项目可能只是在每一步骤时有些复杂的操作而已。