前言
卷积神经网络是一种以图像识别为中心,并且在多个领域得到广泛应用的深度学习方法,如目标检测、图像分割、文本分类等。卷积神经网络于1998年由Yann Lecun提出,在2012年的ImageNet挑战赛中,Alex Krizhevsky凭借深度卷积神经网络AlexNet网络获得远远领先于第二名的成绩,震惊世界。如今卷积神经网络不仅是计算机视觉领域最具有影响力的一类算法,同时在自然语言分类领域也有一定程度的应用。
什么是卷积神经网络?大致可以理解为只要包含了卷积层的神经网络都属于卷积神经网络。
在深度学习的思想提出后,卷积神经网络在计算机视觉等领域取得了快速的应用,有很多基于卷积层、池化层及全连接层的深度卷积神经网络被提出。如1998年提出的LeNet-5,2012年出现的AlexNet网络,2014年提出的GoogLeNet网络和VGG系列的网络,这些网络的提出都用于解决图像领域的问题,针对自然语言分类的问题,2014年开始将CNN网络应用于文本分类,提出了TextCNN网络。
本文主要通过学习刘二大人卷积神经网络课程介绍一下卷积神经网络的相关知识,分为基础篇和高级篇,基础篇主要介绍了卷积层,池化层等概念,高级篇给出了GoogleNet网络的一些讲解。
一、基础篇
1.1卷积层
卷积核(Kernel)
卷积可以看作是输入和卷积核之间的内积运算,是两个实值函数之间的一种数学运算。
如一个1×5×5的输入和一个3×3的卷积核进行卷积,过程如下:
注意这是输入单通道的情况,如果输入有多个通道呢?
给每个通道配一个卷积核,分别计算结果,最后多个结果作加法,得到通道为1的输出。
如果想要输出多个通道呢?
就需要选择多个滤波器,有多少个滤波器,最终就会输出多少个通道,这里注意一下卷积核和滤波器的区别,卷积核是二维的,由宽和高来定义,而滤波器是由宽、高和深度定义的,可以看作是卷积核的集合,只有当输入是一通道时,此时,滤波器深度为1,可以看作二者相同。
如图中所示,从n×w1×h1的输入到m×w2×h2的输出,需要m个大小为n×kernel_w×kernel_h的滤波器(n是滤波器的深度也就等于输入的通道数),把m个滤波器拼成一个张量,即为m×n×kernel_w×kernel_h.
下面是代码示例:
import torch
in_channels,out_channels=5,10
width,height=100,100
kernel_size=3
batch_size=1
input=torch.randn(batch_size,
in_channels,
width,
height)
conv_layer=torch.nn.Conv2d(in_channels,
out_channels,
kernel_size=kernel_size)
output=conv_layer(input)
print(input.shape)
print(output.shape)
print(conv_layer.weight.shape)
输入为5×100×100,经过10个深度为5,kernel_size=3的滤波器卷积,得到输出为10×98×98,滤波器张量为10×5×3×3。
填充(Padding)
对原有特征进行填充,一般默认是填充0,若输入是1×5×5,需要大小一样的输出,如果卷积核是3×3,则需要填充一圈,如果卷积核是5×5,则需要填充两圈。
示例:
import torch
input=[3,4,6,5,7,
2,4,6,8,2,
1,6,7,8,4,
9,7,4,6,2,
3,7,5,4,1]
input=torch.Tensor(input).view(1,1,5,5)
conv_layer=torch.nn.Conv2d(1,1,kernel_size=3,padding=1,bias=False)
kernel=torch.Tensor([1,2,3,4,5,6,7,8,9]).view(1,1,3,3)
conv_layer.weight.data=kernel.data
output=conv_layer(input)
print(output)
步长(Stride)
Stride表示卷积核卷积过程中移动的步长大小,通过增加步长可以有效降低特征图中的宽度和高度。
示例:
import torch
input=[3,4,6,5,7,
2,4,6,8,2,
1,6,7,8,4,
9,7,4,6,2,
3,7,5,4,1]
input=torch.Tensor(input).view(1,1,5,5)
conv_layer=torch.nn.Conv2d(1,1,kernel_size=3,stride=2,bias=False)
kernel=torch.Tensor([1,2,3,4,5,6,7,8,9]).view(1,1,3,3)
conv_layer.weight.data=kernel.data
output=conv_layer(input)
print(output)
其它(Other)
除了上述这些概念,还有空洞卷积、转置卷积与应用于NLP(自然语言处理)任务的二维卷积运算过程等。
空洞卷积可以认为是基于普通的卷积操作的一种变形,在Multi-Scale Context Aggregation by Dilated Convolutions一文中主要用于图像分割。相对于普通卷积而言,空洞卷积通过在卷积核中添加空洞(0)元素,从而增大感受野,获取更多的信息。感受野(receptive field)可以理解为在卷积神经网络中,决定某一层输出结果中一个元素所对应的输入层的区域大小。通俗的解释就是特征映射(feature map)上的一个点对应输入图上的区域大小。示意图如下:
转置卷积的主要作用是将特征图放大恢复到原来的尺寸,其与原有的卷积操作计算方法上并没有差别,而主要区别是在于,转置卷积是卷积的反向过程,即卷积操作的输入作为转置卷积的输出,卷积操作的输出作为转置卷积的输入。转置卷积可以保证在尺寸上做到卷积的反向过程,但是内容上并不能保证完全做到卷积的反向过程。示意图如下:
针对自然语言的词嵌入(Embedding)进行二维卷积,是利用卷积神经网络对自然语言进行分类的关键步骤。在NLP中,由于词嵌入层中每一行都表示一个词语,即每个词语都是由一个向量(词向量)表示的,当提取句子中有利于分类的特征时,需要从词语或字符级别提取,也就是说卷积核的宽度应该覆盖完全单个词向量,即二维卷积的卷积核宽度必须等于词向量的维度。
示意图如下:
注:该部分摘自《PyTorch深度学习入门与实战》
1.2池化层
池化操作的一个重要的目的就是对卷积后得到的特征进行进一步处理(主要是降维),池化层可以起到对数据进一步浓缩的效果,从而缓解计算时内存的压力。池化会选取一定大小区域,将该区域内的像素值使用一个代表元素表示。在PyTorch中,提供了多种池化的类,如果使用平均值代替,称为平均值池化(Average Pooling),如果使用最大值代替则称为最大值池化(Max Pooling),还有重叠池化(Overlapping Pooling)等等。
示例:
代码:
import torch
input=[3,4,6,5,
2,4,6,8,
1,6,7,8,
9,7,4,6,
]
input=torch.Tensor(input).view(1,1,4,4)
print(input)
maxpooling_layer=torch.nn.MaxPool2d(kernel_size=2)
output=maxpooling_layer(input)
print(output)
1.3综合示例
import torch
import torch.nn.functional as F
class Net(torch.nn.Module):
def __init__(self):
super(Net,self).__init__()
'''当创建torch.nn.Conv2d对象时,只需指定kernel_size参数即可。
默认情况下,torch.nn.Conv2d会自动初始化卷积核的权重。后续是模型根据
训练数据,在训练过程中不断更新参数'''
self.conv1=torch.nn.Conv2d(1,10,kernel_size=5)
self.conv2=torch.nn.Conv2d(10,20,kernel_size=5)
self.pooling=torch.nn.MaxPool2d(2)
self.fc=torch.nn.Linear(320,10)
def forward(self,x):
batch_size=x.size(0)
x=self.pooling(F.relu(self.conv1(x)))
x=self.pooling(F.relu(self.conv2(x)))
x=x.view(batch_size,-1) #flatten
x=self.fc(x)
return x
model=Net()
二、高级篇
GoogLeNet
首先要说一下1×1卷积:
和普通的卷积一样,1×1卷积可以改变通道的数量,如果需要输出n个通道,就需要n个这样的由m(输入通道)个1×1卷积拼接起来的滤波器,1×1卷积主要的工作是改变通道,作用是可以减少计算量,如下所示,通过中间层引入一个1×1卷积可以大大减少计算量,可以看到运算量变成了原来的十分之一,大大提高了计算效率。
GoogLeNet网络结构:
不同于以往的串形网络结构,GoogLeNet网络结构比较复杂,
可以看到有很多重复的部分,可以把重复的部分封装起来,来减少代码的冗余,这部分叫做Inception模块,Inception模块包含有四个分支,主要是用来解决构造神经网络时,超参数比较难选的问题,因此把几种卷积都用一下,效果更好的卷积被赋予的权重会更大,自动找到最优卷积的组合。
最后一步是把四个分支得到的结果,沿着通道方向进行拼接(Concatenate):
(注:这里四个分支得到的输出只有通道数可能是不同的,h和w都是相同的,以满足把四个分支得到的结果,沿着通道方向进行拼接)
这就是Inception模块,有了Inception模块就可以进行对GoogLeNet网络的组装了。
下面是应用组建的GoogLeNet网络对MNIST手写数字分类数据集进行的训练和预测:
在这里插入代码片import torch
from torchvision import transforms
from torchvision import datasets
from torch.utils.data import DataLoader
import torch.nn.functional as F
import torch.optim as optim
batch_size=64
transform=transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.1307,),(0.3081))
])
train_dataset=datasets.MNIST(root='../dataset/mnist/',
train=True,
download=True,
transform=transform)
train_loader=DataLoader(train_dataset,
shuffle=True,
batch_size=batch_size)
test_dataset=datasets.MNIST(root='../dataset/mnist/',
train=False,
download=True,
transform=transform)
test_loader=DataLoader(train_dataset,
shuffle=False,
batch_size=batch_size)
import torch
import torch.nn.functional as F
class InceptionA(torch.nn.Module):
def __init__(self,in_channels):
super(InceptionA,self).__init__()
self.branch1x1=torch.nn.Conv2d(in_channels,16,kernel_size=1)
self.branch5x5_1=torch.nn.Conv2d(in_channels,16,kernel_size=1)
self.branch5x5_2=torch.nn.Conv2d(16,24,kernel_size=3,padding=1)
self.branch3x3_1=torch.nn.Conv2d(in_channels,16,kernel_size=1)
self.branch3x3_2=torch.nn.Conv2d(16,24,kernel_size=3,padding=1)
self.branch3x3_3=torch.nn.Conv2d(24,24,kernel_size=3,padding=1)
self.branch_pool=torch.nn.Conv2d(in_channels,24,kernel_size=1)
def forward(self,x):
branch1x1=self.branch1x1(x)
branch5x5=self.branch5x5_1(x)
branch5x5=self.branch5x5_2(branch5x5)
branch3x3=self.branch3x3_1(x)
branch3x3=self.branch3x3_2(branch3x3)
branch3x3=self.branch3x3_3(branch3x3)
branch_pool=F.avg_pool2d(x,kernel_size=3,stride=1,padding=1)
branch_pool=self.branch_pool(branch_pool)
outputs=[branch1x1,branch5x5,branch3x3,branch_pool]
return torch.cat(outputs,dim=1)
class Net(torch.nn.Module):
def __init__(self):
super(Net,self).__init__()
self.conv1=torch.nn.Conv2d(1,10,kernel_size=5)
self.conv2=torch.nn.Conv2d(88,20,kernel_size=5)
self.incep1=InceptionA(in_channels=10)
self.incep2=InceptionA(in_channels=20)
self.mp=torch.nn.MaxPool2d(2)
self.fc=torch.nn.Linear(1408,10)
def forward(self,x):
in_size=x.size(0)
x=F.relu(self.mp(self.conv1(x)))
x=self.incep1(x)
x=F.relu(self.mp(self.conv2(x)))
x=self.incep2(x)
x=x.view(in_size,-1)
x=self.fc(x)
return x
model=Net()
criterion=torch.nn.CrossEntropyLoss()
optimizer=optim.SGD(model.parameters(),lr=0.01,momentum=0.5)
def train(epoch):
running_loss=0.0
for batch_idx,data in enumerate(train_loader,0):
inputs,target=data
optimizer.zero_grad()
#forward+backward+update
outputs=model(inputs)
loss=criterion(outputs,target)
loss.backward()
optimizer.step()
running_loss+=loss.item()
if batch_idx%300==299:
print('[%d,%5d] loss: %.3f' % (epoch +1,batch_idx+1,running_loss/300))
running_loss=0.0
def test():
correct=0
total=0
with torch.no_grad():
for data in test_loader:
images,labels=data
outputs=model(images)
_, predicted=torch.max(outputs.data,dim=1)
total+=labels.size(0)
correct+=(predicted==labels).sum().item()
print('Accuracy on test set:%d %%' % (100*correct/total))
if __name__=='__main__':
for epoch in range(10):
train(epoch)
test()
##最终输出预测正确率为98%