专栏链接:https://blog.csdn.net/qq_33345365/category_12578430.html
初次编辑:2024/2/15;最后编辑:2024/3/8
参考网站-微软教程:https://learn.microsoft.com/en-us/training/modules/intro-computer-vision-pytorch
更多的内容可以参考本作者其他专栏:
Pytorch基础:https://blog.csdn.net/qq_33345365/category_12591348.html
Pytorch NLP基础:https://blog.csdn.net/qq_33345365/category_12597850.html
CNN
上一部分描述了如何使用类定义来定义多层神经网络,但是这些网络是通用的,而不是专门用于计算机视觉任务的。在本部分,将描述卷积神经网络(CNN),它是专门为计算机视觉设计的。
计算机视觉不同于一般的分类,因为当我们试图在图片中找到某个物体时,我们是在扫描图像,寻找一些特定的**模式**(Pattern)及其组合。例如,在寻找一只猫时,我们首先可能会寻找水平线,这些水平线可以形成胡须,然后胡须的某些组合可以告诉我们这实际上是一只猫的照片。某些图案的相对位置和存在是重要的,而不是它们在图像上的确切位置。
为了提取模式,我们将使用卷积过滤器(convolutional filters)的概念。
准备工作与加载库
创建一个空的python文件,确保上一部分使用的辅助python文件pytorchcv.py在同一目录下。
加载库,我们使用修改builtins的方式来修改全局变量从而加载数据集(load_mnist)
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)
卷积过滤器
卷积滤波器类似一个窗口,运行在图像的每个像素上,并计算相邻像素的加权平均值。它们由权重系数矩阵定义。让我们看看在MNIST手写数字上应用两种不同卷积滤波器的例子:
plot_convolution(torch.tensor([[-1.,0.,1.],[-1.,0.,1.],[-1.,0.,1.]]),'Vertical edge filter')
plot_convolution(torch.tensor([[-1.,-1.,-1.],[0.,0.,0.],[1.,1.,1.]]),'Horizontal edge filter')
plot_convolution函数在pytorchcv.py中,具体含义后面叙述,第一个过滤器称为垂直边缘过滤器,由下式矩阵定义:
A = ( − 1 0 1 − 1 0 1 − 1 0 1 ) A = \begin{pmatrix} -1 & 0 & 1 \\ -1 & 0 & 1 \\ -1 & 0 & 1 \end{pmatrix} A= −1−1−1000111
当这个过滤器经过相对均匀的像素场时,所有值加起来等于0。但是,当它遇到图像中的垂直边缘时,会产生很高的尖峰值。这就是为什么在上面的图像中,可以看到垂直边缘由高值和低值表示,而水平边缘是平均的。当应用水平边缘过滤器时,会发生相反的事情-水平线被放大,垂直线被平均掉。
如果对大小为28×28的图像应用3×3过滤器-图像的大小将变为26×26,因为过滤器不会越过图像边界。然而,在某些情况下,可能希望保持图像的大小相同,在这种情况下,图像的每一边都填充0。
在经典的计算机视觉中,对图像应用多个过滤器来生成特征,然后由机器学习算法使用这些特征来构建分类器。然而,深度学习中通过构建学习最佳卷积过滤器的网络来解决分类问题。
为了做到这一点,我们引入了卷积层(convolutional layers)。
卷积层
卷积层是用nn.Conv2d构造来定义的。我们需要指定以下内容:
- In_channels:输入通道数。在此处理的是灰度图像,因此输入通道数为1。彩色图像有3个通道(RGB)。
- Out_channels:要使用的过滤器数量。此处为9种不同的过滤器,这将给网络提供大量的机会来探索哪种过滤器最适合。
- Kernel_size:滑动窗口的大小。通常为3x3或5x5。过滤器尺寸的选择通常是通过实验来选择的,即通过尝试不同的过滤器尺寸并比较结果的准确性。
最简单的CNN将包含一个卷积层。给定输入大小为28x28,在应用了9个5x5过滤器之后,将最终得到一个9x24x24的张量。这里,每个过滤器的结果由图像中的不同通道表示(因此第一个维度9对应于过滤器的数量)。
卷积后,张量平铺成一个大小为5184(9x24x24)的向量,然后添加线性层,输出长度为10,对应10个数字。我们还在层与层之间使用relu激活函数。
class OneConv(nn.Module):
def __init__(self):
super(OneConv, self).__init__()
self.conv = nn.Conv2d(in_channels=1,out_channels=9,kernel_size=(5,5))
self.flatten = nn.Flatten()
self.fc = nn.Linear(5184,10)
def forward(self, x):
x = nn.functional.relu(self.conv(x))
x = self.flatten(x)
x = nn.functional.log_softmax(self.fc(x),dim=1)
return x
net = OneConv()
summary(net,input_size=(1,1,28,28))
输出:
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
OneConv [1, 10] --
├─Conv2d: 1-1 [1, 9, 24, 24] 234
├─Flatten: 1-2 [1, 5184] --
├─Linear: 1-3 [1, 10] 51,850
==========================================================================================
Total params: 52,084
Trainable params: 52,084
Non-trainable params: 0
Total mult-adds (M): 0.19
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.04
Params size (MB): 0.21
Estimated Total Size (MB): 0.25
==========================================================================================
这个网络包含大约50k个可训练参数,而在全连接的多层网络中大约有80k个可训练参数。这使得我们即使在较小的数据集上也能获得很好的结果,因为卷积网络的泛化效果要好得多。
注意,卷积层的参数数量相当少,而且它不依赖于图像的分辨率。卷积层有 9 个 5x5 的滤波器,因此权重的数量为 9 * 5 * 5 = 225。加上 9 个bias,总的参数数为 225 + 9 = 234。每个过滤器有一个bias。简单来说,bias就像一个调节旋钮,可以控制滤波器输出的整体偏移量。它可以帮助模型更好地学习数据中的模式,即使这些模式稍微偏离了滤波器的权重所表示的模式。
训练
与上一单元的全连接网络相比,我们能够实现更高的准确性,并且更快。
hist = train(net,train_loader,test_loader,epochs=5)
plot_results(hist)
# Epoch 0, Train acc=0.943, Val acc=0.967, Train loss=0.001, Val loss=0.001
# Epoch 1, Train acc=0.976, Val acc=0.977, Train loss=0.001, Val loss=0.001
# Epoch 2, Train acc=0.983, Val acc=0.976, Train loss=0.000, Val loss=0.001
# Epoch 3, Train acc=0.988, Val acc=0.974, Train loss=0.000, Val loss=0.001
# Epoch 4, Train acc=0.990, Val acc=0.973, Train loss=0.000, Val loss=0.001
卷积层允许我们从图像中提取某些图像模式,因此最终的分类器是基于这些特征之上的。然而,我们可以使用相同的方法在特征空间中提取模式,通过在第一个卷积层的顶部堆叠另一个卷积层。我们将在下一章学习多层卷积网络。
总代码如下所示:
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)
plot_convolution(torch.tensor([[-1., 0., 1.], [-1., 0., 1.], [-1., 0., 1.]]), 'Vertical edge filter')
plot_convolution(torch.tensor([[-1., -1., -1.], [0., 0., 0.], [1., 1., 1.]]), 'Horizontal edge filter')
class OneConv(nn.Module):
def __init__(self):
super(OneConv, self).__init__()
self.conv = nn.Conv2d(in_channels=1, out_channels=9, kernel_size=(5, 5))
self.flatten = nn.Flatten()
self.fc = nn.Linear(5184, 10)
def forward(self, x):
x = nn.functional.relu(self.conv(x))
x = self.flatten(x)
x = nn.functional.log_softmax(self.fc(x), dim=1)
return x
net = OneConv()
summary(net, input_size=(1, 1, 28, 28))
hist = train(net, train_loader, test_loader, epochs=5)
plot_results(hist)
plt.show()
多层CNN
在上一单元中,我们学习了可以从图像中提取模式的卷积过滤器。对于我们的MNIST分类器,我们使用了9个5x5过滤器,得到9x24x24张量。
我们可以使用相同的卷积思想来提取图像中的高级模式。例如,数字8和9的圆边可以由许多较小的笔画组成。为了识别这些模式,我们可以在第一层的结果之上构建另一层卷积过滤器。
准备工作
创建新文件multilayer_CNN.py并加载类
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)
池化层Pooling layers
提取模式的层次结构:第一个卷积层寻找基本模式,比如水平线或垂直线。在它们之上的下一层卷积层寻找更高级的模式,比如原始形状。更多的卷积层可以将这些形状组合成图像的某些部分,直到分类的最终对象。
这样做时,我们还需要应用一个技巧:减小图像的空间大小。一旦我们检测到滑动窗口中有一个水平笔画,那么它发生在哪个像素就不那么重要了。因此,我们可以“缩小”图像的大小,可以使用下面其中一个池化层来完成:
- Average Pooling:取一个滑动窗口(例如,2x2像素)并计算窗口内值的平均值;
- Max Pooling:用最大值替换窗口。最大池化背后的思想是检测滑动窗口内某个模式的存在。
因此,在一个典型的CNN中,它将由几个卷积层组成,在它们之间有池化层来降低图像的维度。我们还会增加过滤器的数量,因为随着模式变得更高级,我们需要寻找更多可能的有趣组合。
由于空间维度的减少和特征/过滤器维度的增加,这种结构也被称为金字塔结构(pyramid architecture)。
一个使用双层CNN的例子:
class MultiLayerCNN(nn.Module):
def __init__(self):
super(MultiLayerCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 10, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(10, 20, 5)
self.fc = nn.Linear(320, 10)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv1(x)))
x = self.pool(nn.functional.relu(self.conv2(x)))
x = x.view(-1, 320)
x = nn.functional.log_softmax(self.fc(x), dim=1)
return x
net = MultiLayerCNN()
summary(net, input_size=(1, 1, 28, 28))
输出:
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
MultiLayerCNN [1, 10] --
├─Conv2d: 1-1 [1, 10, 24, 24] 260
├─MaxPool2d: 1-2 [1, 10, 12, 12] --
├─Conv2d: 1-3 [1, 20, 8, 8] 5,020
├─MaxPool2d: 1-4 [1, 20, 4, 4] --
├─Linear: 1-5 [1, 10] 3,210
==========================================================================================
Total params: 8,490
Trainable params: 8,490
Non-trainable params: 0
Total mult-adds (M): 0.47
==========================================================================================
Input size (MB): 0.00
Forward/backward pass size (MB): 0.06
Params size (MB): 0.03
Estimated Total Size (MB): 0.09
==========================================================================================
关于这个定义需要注意几点:
- 代码没有使用Flatten层,而是使用view函数将前向函数中的张量扁平化,view函数类似于numpy中的重塑函数。因为扁平化层没有可训练的权重,所以我们不需要在我们的类中创建一个单独的层实例——我们可以只使用torch.nn.functional命名空间中的函数。
- 模型中只使用了池化层的一个实例,也是因为它不包含任何可训练的参数,因此一个实例可以有效地重用。
- 可训练参数的数量(~8.5K)明显小于之前的情况(感知机为80K,单层CNN为50K)。这是因为卷积层通常只有很少的参数,与输入图像大小无关。此外,由于池化,在应用最终线性层之前,图像的维数显着降低。少量的参数对我们的模型有积极的影响,因为它有助于防止过拟合,即使在较小的数据集大小。
hist = train(net,train_loader,test_loader,epochs=5)
# Epoch 0, Train acc=0.949, Val acc=0.980, Train loss=0.001, Val loss=0.000
多层CNN能够获得更高的精度,并且更快—只需1或2个epoch。这意味着复杂的网络架构需要更少的数据来弄清楚正在发生什么,并从图像中提取通用模式。
multilayer_CNN.py的代码如下:
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
load_mnist(batch_size=128)
class MultiLayerCNN(nn.Module):
def __init__(self):
super(MultiLayerCNN, self).__init__()
self.conv1 = nn.Conv2d(1, 10, 5)
self.pool = nn.MaxPool2d(2, 2)
self.conv2 = nn.Conv2d(10, 20, 5)
self.fc = nn.Linear(320, 10)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv1(x)))
x = self.pool(nn.functional.relu(self.conv2(x)))
x = x.view(-1, 320)
x = nn.functional.log_softmax(self.fc(x), dim=1)
return x
net = MultiLayerCNN()
summary(net, input_size=(1, 1, 28, 28))
hist = train(net, train_loader, test_loader, epochs=5)
使用来自CIFAR-10数据集的真实图像
下面探索更高级的图片数据集,称为CIFAR-10。它包含60k个32x32彩色图像,分为10类。
首先从网上下载数据并解压缩,放到python文件所在的data文件夹下,网站https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz
准备与展示数据集
创建新文件cifar.py,加入以下代码展示数据集
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
transform = torchvision.transforms.Compose(
[torchvision.transforms.ToTensor(),
torchvision.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=14, shuffle=True)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=14, shuffle=False)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
display_dataset(trainset, classes=classes)
plt.show()
一个著名的CIFAR-10架构被称为LeNet。它遵循与我们上面概述的相同的原则。由于所有图像都是彩色的,输入张量大小为3×32×32,并且5×5卷积滤波器也跨颜色维度应用-这意味着卷积核矩阵的大小为3×5×5。
此处还对这个模型做了一个简化——不使用log_softmax作为输出激活函数,而只返回最后一个完全连接层的输出。在这种情况下,使用CrossEntropyLoss损失函数来优化模型。
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.conv3 = nn.Conv2d(16,120,5)
self.flat = nn.Flatten()
self.fc1 = nn.Linear(120,64)
self.fc2 = nn.Linear(64,10)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv1(x)))
x = self.pool(nn.functional.relu(self.conv2(x)))
x = nn.functional.relu(self.conv3(x))
x = self.flat(x)
x = nn.functional.relu(self.fc1(x))
x = self.fc2(x)
return x
net = LeNet()
summary(net,input_size=(1,3,32,32))
输出:
==========================================================================================
Layer (type:depth-idx) Output Shape Param #
==========================================================================================
LeNet [1, 10] --
├─Conv2d: 1-1 [1, 6, 28, 28] 456
├─MaxPool2d: 1-2 [1, 6, 14, 14] --
├─Conv2d: 1-3 [1, 16, 10, 10] 2,416
├─MaxPool2d: 1-4 [1, 16, 5, 5] --
├─Conv2d: 1-5 [1, 120, 1, 1] 48,120
├─Flatten: 1-6 [1, 120] --
├─Linear: 1-7 [1, 64] 7,744
├─Linear: 1-8 [1, 10] 650
==========================================================================================
Total params: 59,386
Trainable params: 59,386
Non-trainable params: 0
Total mult-adds (M): 0.66
==========================================================================================
Input size (MB): 0.01
Forward/backward pass size (MB): 0.05
Params size (MB): 0.24
Estimated Total Size (MB): 0.30
==========================================================================================
训练
正确地训练这个网络将花费大量的时间,最好是在支持gpu的计算上完成。
opt = torch.optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
hist = train(net, trainloader, testloader, epochs=3, optimizer=opt, loss_fn=nn.CrossEntropyLoss())
# Epoch 0, Train acc=0.261, Val acc=0.388, Train loss=0.143, Val loss=0.121
# Epoch 1, Train acc=0.437, Val acc=0.491, Train loss=0.110, Val loss=0.101
# Epoch 2, Train acc=0.508, Val acc=0.522, Train loss=0.097, Val loss=0.094
通过3次训练,我们所能达到的准确度似乎并不高。然而,请记住,盲目猜测只能给我们10%的准确率,而且我们的问题实际上比MNIST数字分类要困难得多。在如此短的训练时间内获得50%以上的准确率似乎是一个很好的成就。
cifar.py代码如下:
import torch
import torch.nn as nn
import torchvision
import matplotlib.pyplot as plt
from torchinfo import summary
import numpy as np
from pytorchcv import load_mnist, train, plot_results, plot_convolution, display_dataset
transform = torchvision.transforms.Compose(
[torchvision.transforms.ToTensor(),
torchvision.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=14, shuffle=True)
testset = torchvision.datasets.CIFAR10(root='./data', train=False, download=True, transform=transform)
testloader = torch.utils.data.DataLoader(testset, batch_size=14, shuffle=False)
classes = ('plane', 'car', 'bird', 'cat',
'deer', 'dog', 'frog', 'horse', 'ship', 'truck')
display_dataset(trainset, classes=classes)
# plt.show() # 展示数据集
class LeNet(nn.Module):
def __init__(self):
super(LeNet, self).__init__()
self.conv1 = nn.Conv2d(3, 6, 5)
self.pool = nn.MaxPool2d(2)
self.conv2 = nn.Conv2d(6, 16, 5)
self.conv3 = nn.Conv2d(16, 120, 5)
self.flat = nn.Flatten()
self.fc1 = nn.Linear(120, 64)
self.fc2 = nn.Linear(64, 10)
def forward(self, x):
x = self.pool(nn.functional.relu(self.conv1(x)))
x = self.pool(nn.functional.relu(self.conv2(x)))
x = nn.functional.relu(self.conv3(x))
x = self.flat(x)
x = nn.functional.relu(self.fc1(x))
x = self.fc2(x)
return x
net = LeNet()
summary(net, input_size=(1, 3, 32, 32))
opt = torch.optim.SGD(net.parameters(),lr=0.001,momentum=0.9)
hist = train(net, trainloader, testloader, epochs=3, optimizer=opt, loss_fn=nn.CrossEntropyLoss())