使用卷积神经网络
一、卷积神经网络
- 在前面的单元中,我们已经学习了如何使用类定义来定义一个多层神经网络,但是这些网络是通用的,并不专门用于计算机视觉任务。在这个单元,我们将学习卷积神经网络(CNNs) ,这是专门为计算机视觉设计的。
- 计算机视觉不同于一般的分类,因为当我们试图在图片中找到一个特定的物体时,我们扫描图像寻找一些特定的模式和它们的组合。例如,当我们寻找一只猫的时候,我们首先可能会寻找水平的线条,这些线条可以形成胡须,然后某些胡须的组合可以告诉我们这实际上是一只猫的图片。某些图案的相对位置和存在是重要的,而不是它们在图像上的确切位置。
- 为了提取模式,我们将使用卷积滤波器的概念。但是首先,让我们加载前面单元中定义的所有依赖项和函数。
无法raw.githubusercontent.com访问报错办法:
- 首先下载wget工具,然后将exe文件放入同目录下
- 若出现无法访问对应网站的报错,则
- 找到hosts文件,并添加199.232.68.133 raw.githubusercontent.com保存便可
- 无法保存则需修改hosts的访问权限,去掉只读
- 修改之后仍报错可能需要科学上网
# 实在不行就直接在此目录下新建一个名为pytorchcv的py文件,内容如下:
# Script file to hide implementation details for PyTorch computer vision module
import builtins
import torch
import torch.nn as nn
from torch.utils import data
import torchvision
from torchvision.transforms import ToTensor
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import glob
import os
import zipfile
default_device = 'cuda' if torch.cuda.is_available() else 'cpu'
def load_mnist(batch_size=64):
builtins.data_train = torchvision.datasets.MNIST('./data',
download=True,train=True,transform=ToTensor())
builtins.data_test = torchvision.datasets.MNIST('./data',
download=True,train=False,transform=ToTensor())
builtins.train_loader = torch.utils.data.DataLoader(data_train,batch_size=batch_size)
builtins.test_loader = torch.utils.data.DataLoader(data_test,batch_size=batch_size)
def train_epoch(net,dataloader,lr=0.01,optimizer=None,loss_fn = nn.NLLLoss()):
optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)
net.train()
total_loss,acc,count = 0,0,0
for features,labels in dataloader:
optimizer.zero_grad()
lbls = labels.to(default_device)
out = net(features.to(default_device))
loss = loss_fn(out,lbls) #cross_entropy(out,labels)
loss.backward()
optimizer.step()
total_loss+=loss
_,predicted = torch.max(out,1)
acc+=(predicted==lbls).sum()
count+=len(labels)
return total_loss.item()/count, acc.item()/count
def validate(net, dataloader,loss_fn=nn.NLLLoss()):
net.eval()
count,acc,loss = 0,0,0
with torch.no_grad():
for features,labels in dataloader:
lbls = labels.to(default_device)
out = net(features.to(default_device))
loss += loss_fn(out,lbls)
pred = torch.max(out,1)[1]
acc += (pred==lbls).sum()
count += len(labels)
return loss.item()/count, acc.item()/count
def train(net,train_loader,test_loader,optimizer=None,lr=0.01,epochs=10,loss_fn=nn.NLLLoss()):
optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)
res = { 'train_loss' : [], 'train_acc': [], 'val_loss': [], 'val_acc': []}
for ep in range(epochs):
tl,ta = train_epoch(net,train_loader,optimizer=optimizer,lr=lr,loss_fn=loss_fn)
vl,va = validate(net,test_loader,loss_fn=loss_fn)
print(f"Epoch {ep:2}, Train acc={ta:.3f}, Val acc={va:.3f}, Train loss={tl:.3f}, Val loss={vl:.3f}")
res['train_loss'].append(tl)
res['train_acc'].append(ta)
res['val_loss'].append(vl)
res['val_acc'].append(va)
return res
def train_long(net,train_loader,test_loader,epochs=5,lr=0.01,optimizer=None,loss_fn = nn.NLLLoss(),print_freq=10):
optimizer = optimizer or torch.optim.Adam(net.parameters(),lr=lr)
for epoch in range(epochs):
net.train()
total_loss,acc,count = 0,0,0
for i, (features,labels) in enumerate(train_loader):
lbls = labels.to(default_device)
optimizer.zero_grad()
out = net(features.to(default_device))
loss = loss_fn(out,lbls)
loss.backward()
optimizer.step()
total_loss+=loss
_,predicted = torch.max(out,1)
acc+=(predicted==lbls).sum()
count+=len(labels)
if i%print_freq==0:
print("Epoch {}, minibatch {}: train acc = {}, train loss = {}".format(epoch,i,acc.item()/count,total_loss.item()/count))
vl,va = validate(net,test_loader,loss_fn)
print("Epoch {} done, validation acc = {}, validation loss = {}".format(epoch,va,vl))
def plot_results(hist):
plt.figure(figsize=(15,5))
plt.subplot(121)
plt.plot(hist['train_acc'], label='Training acc')
plt.plot(hist['val_acc'], label='Validation acc')
plt.legend()
plt.subplot(122)
plt.plot(hist['train_loss'], label='Training loss')
plt.plot(hist['val_loss'], label='Validation loss')
plt.legend()
def plot_convolution(t,title=''):
with torch.no_grad():
c = nn.Conv2d(kernel_size=(3,3),out_channels=1,in_channels=1)
c.weight.copy_(t)
fig, ax = plt.subplots(2,6,figsize=(8,3))
fig.suptitle(title,fontsize=16)
for i in range(5):
im = data_train[i][0]
ax[0][i].imshow(im[0])
ax[1][i].imshow(c(im.unsqueeze(0))[0][0])
ax[0][i].axis('off')
ax[1][i].axis('off')
ax[0,5].imshow(t)
ax[0,5].axis('off')
ax[1,5].axis('off')
#plt.tight_layout()
plt.show()
def display_dataset(dataset, n=10,classes=None):
fig,ax = plt.subplots(1,n,figsize=(15,3))
mn = min([dataset[i][0].min() for i in range(n)])
mx = max([dataset[i][0].max() for i in range(n)])
for i in range(n):
ax[i].imshow(np.transpose((dataset[i][0]-mn)/(mx-mn),(1,2,0)))
ax[i].axis('off')
if classes:
ax[i].set_title(classes[dataset[i][1]])
def check_image(fn):
try:
im = Image.open(fn)
im.verify()
return True
except:
return False
def check_image_dir(path):
for fn in glob.glob(path):
if not check_image(fn):
print("Corrupt image: {}".format(fn))
os.remove(fn)
def common_transform():
std_normalize = torchvision.transforms.Normalize(mean=[0.485, 0.456, 0.406],
std=[0.229, 0.224, 0.225])
trans = torchvision.transforms.Compose([
torchvision.transforms.Resize(256),
torchvision.transforms.CenterCrop(224),
torchvision.transforms.ToTensor(),
std_normalize])
return trans
def load_cats_dogs_dataset():
if not os.path.exists('data/PetImages'):
with zipfile.ZipFile('data/kagglecatsanddogs_3367a.zip', 'r') as zip_ref:
zip_ref.extractall('data')
check_image_dir('data/PetImages/Cat/*.jpg')
check_image_dir('data/PetImages/Dog/*.jpg')
dataset = torchvision.datasets.ImageFolder('data/PetImages',transform=common_transform())
trainset, testset = torch.utils.data.random_split(dataset,[20000,len(dataset)-20000])
trainloader = torch.utils.data.DataLoader(trainset,batch_size=32)
testloader = torch.utils.data.DataLoader(trainset,batch_size=32)
return dataset, trainloader, testloader
!wget https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
--2022-05-15 07:56:02-- https://raw.githubusercontent.com/MicrosoftDocs/pytorchfundamentals/main/computer-vision-pytorch/pytorchcv.py
Resolving raw.githubusercontent.com (raw.githubusercontent.com)... 199.232.68.133
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)|199.232.68.133|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 6371 (6.2K) [text/plain]
Saving to: 'pytorchcv.py'
0K ...... 100% 1.41M=0.004s
2022-05-15 07:56:03 (1.41 MB/s) - 'pytorchcv.py' saved [6371/6371]
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)
1. 卷积滤波器
卷积滤波器是一种小窗口,可以覆盖图像的每个像素并计算相邻像素的加权平均数。
它们由权重系数矩阵定义。让我们看看在 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')
2. 垂直滤波器
第一个过滤器被称为垂直边缘过滤器,它由以下矩阵定义:
当垂直滤波器经过相对统一(变化不大)的像素字段时,所有值加起来为0。然而,当它在图像中遇到一个垂直边缘(其实也就是边缘像素的左右灰度值相差巨大)时,就会产生很高的尖峰值(有正有负,对于前景垂直边缘而言,左边缘更亮一些,想想这是为什么?)。这就是为什么在上面的图像中,你可以看到垂直边缘由较高和较低的像素值显示,而水平边缘是 平均值。
3. 水平滤波器
当我们使用水平边缘过滤器时,会发生相反的事情——水平线被放大,垂直线被平均化。由以下矩阵定义:,同理,我们会看到前景水平边缘的上边缘会更亮
- 注意: 如果对尺寸为28 × 28 的图像应用3 × 3滤波器,由于滤波器不跨越图像边界,图像尺寸将变为26 × 26 。然而,在某些情况下,我们可能希望保持图像的大小不变,在这种情况下,图像的所有边际都填充了零(背景值)。也就是滤波器不会覆盖到整幅图像的阵列之外,因此无法处理边际像素,所以我们在各边际填充了背景像素值,填充的宽度是由滤波器尺寸来决定的。3 * 3 则只需要填充一个像素宽度,5 * 5 则填充两个,etc。
在经典的计算机视觉中,对图像采用多个滤波器生成特征,然后利用机器学习算法构建分类器。然而,在深度学习中,我们构造的网络学习最佳卷积过滤器来解决分类问题。
二、卷积层
卷积层是使用 nn. Conv2d 结构定义的。我们需要指定以下内容:
- in_channels:输入通道的数目。在我们的例子中,我们处理的是一个灰度图像,因此输入通道的数目是1。彩色图像有三个通道(RGB)。
- out_channels:需要使用的滤波器数量。我们将使用9种不同的过滤器,这将给网络提供机会来探索哪些过滤器最适合我们的场景。
- kernel_size : 卷积核大小,也就是是滑动窗口的大小。通常使用3x3或5x5的过滤器。滤波器尺寸的选择通常是通过实验来选择的,也就是通过试验不同的滤波器尺寸并比较最终的精度进而敲定大小。
最简单的 CNN 将包含一个卷积层。给定输入大小28x28,在应用9个5x5滤波器之后,我们将得到一个9x24x24的张量(空间大小更小,因为只有24个位置可以将5的滑动间隔放入28个像素, 没有进行填充,上下左右都空出了 2 个像素宽度没有扫描)。每个滤波器的结果由图像中的不同通道表示(因此第一个维度9对应于滤波器的数量)。
经过卷积后,将9x24x24张量压缩成一个大小为5184的向量,然后添加线性层,生成10个类。我们还在图层之间使用了 relu 激活函数(实现非线性模拟)。
class OneConv(nn.Module):
def __init__(self):
super(OneConv, self).__init__()
# 使用nn.Con2d方法来定义2维卷积层
self.conv = nn.Conv2d(in_channels=1,out_channels=9,kernel_size=(5,5))
# 将得到的out_channels个张量压缩成一个向量
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 -- --
├─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个5 × 5的滤波器,因此参数数目为9 × 5 × 5 + 9 = 234 (后面的9代表了9个滤波器各自偏差)。尽管我们在上面的讨论中忽略了这一点,但卷积滤波器也有偏差。我们网络的大多数参数来自最后的稠密层。
hist = train(net,train_loader,test_loader,epochs=5)
plot_results(hist)
Epoch 0, Train acc=0.945, Val acc=0.976, Train loss=0.001, Val loss=0.001
Epoch 1, Train acc=0.978, Val acc=0.979, Train loss=0.001, Val loss=0.001
Epoch 2, Train acc=0.984, Val acc=0.976, Train loss=0.000, Val loss=0.001
Epoch 3, Train acc=0.988, Val acc=0.977, Train loss=0.000, Val loss=0.001
Epoch 4, Train acc=0.987, Val acc=0.978, Train loss=0.000, Val loss=0.001
可见,在将图像经过卷积层处理之后,我们的模型达到了更高的精度,也更快。这是之前的全连接网络不可比拟的。
我们也可以实例化已训练好的卷积层的参数,并弄清楚到底发生了什么:
fig,ax = plt.subplots(1,9)
with torch.no_grad():
p = next(net.conv.parameters())
for i,x in enumerate(p):
ax[i].imshow(x.detach().cpu()[0,...])
ax[i].axis('off')
三、提示
卷积层允许我们从图像中提取特定的图像模式,因此最终的分类器是基于这些特征之上的。然而,通过多个卷积层的叠加,我们可以使用同样的方法提取特征空间中的模式。我们将在下一单元学习多层卷积网络。
四、错误总结
1. 卷积层是什么?
卷积层是一个使用小窗口(卷积核)在输入图像上面滑动以提取图像模式的层。并非是在密集层之前规范化并准备图像的图像预处理层(卷积层自己并不会提供图像,也不会对图像进行规范化!,这应该是输入层的工作)
2. 卷积层参数的计算方式
卷积层的参数数量相当小,它不依赖于图像的分辨率!在本例中,我们使用了9个5 × 5的滤波器,因此参数数目为9 × 5 × 5 + 9 = 234 (后面的9代表了9个滤波器各自偏差)。
3. 卷积层的结果张量尺寸如何计算?
首先输出张量是与输入图像通道无关的,因此张量的第一维大小应该和滤波器个数保持一致,另外张量的长宽应该由卷积核、图像长宽一起决定。
例如,如果输入图像的大小为 3x200x200,则在应用带有 16 个筛选器的 5x5 卷积层后,张量的大小是多少?
由于卷积核不会覆盖到图像边际外,所以上下左右各缺少2个像素宽度,则得张量大小为16 * 196 * 196