一、概述
输入的图像都很大,使用全连接网络的话,计算的代价较高,图像也很难保留原本特征。
卷积神经网络 (Convolutional Neural Network,CNN )是一种专门用于处理具有网格状结构数据的深度学习模型。主要应用于计算机视觉任务,但它的成功启发了在其他领域应用,如自然语言处理等。
CNN网络主要有三部分构成:卷积层、池化层和全连接层构成;卷积层负责提取图像中的局部特征;池化层用来大幅降低运算量并特征增强;全连接层类似神经网络的部分,用来输出想要的结果。
1、全连接网络处理图像缺陷
参数结束了巨大:假设使用 100*1000的输入,神经元存在1000*1000,那么其中需要使用权重参数达到 10的十二次方。
表达能力:全连接无法利用好图片像素的空间特性,降低了学习效率
2、卷积思想
分为两个部分:卷和积 ; 卷 表示从左上 开始到右上,最后到右下的循环过程;积 表示将每次卷的内容进行乘积求和。
2.1、局部连接
没有全连接方式,空间距离越近的像素相互影响越大;局部特征完成目标的识别
2.2、权重共享
从一块区域学习的信息应用到其他区域;减少参数量,降低学习难度和计算量
二、 卷积层 Convolutional
1、卷积核
一个矩阵,用于提取图像的特征(根据数值内容处理为具体特征意义的数值)
卷积核(过滤器)的个数决定了卷积层输出特征矩阵的通道数;卷积核的大小一般设置为奇数形式的n*n,少数出现n*m(n决定行的卷积,m决定列的卷积)
2、卷积计算
原图像:5*5 的矩阵,经过边缘填充变成7*7的矩阵; 卷积核:3*3的矩阵; 特征图: 5*5的矩阵
卷积计算过程就是将卷积核放在图片信息上移动计算,每次达到下一个位置时,重合区域点对点相乘后全部相加,这个值就是特征图的一个格子数据,图像遍历完成后,所有的结果拼接变为特征图。
如下图,左上角的区域对于卷积核位置相乘相加:
API:
nn.Conv2d
参数:in_channels= int 图像信息的通道数
out_channels= int 输出特征图的通道数
kernel_size= int 或 tuple 卷积核的大小
stride= int 或 tuple 步长默认1,表示行或列的移动距离
padding= int,默认0 填充,保障边缘信息被提取
dilation= int 或 tuple 默认1,表示无间隔,若为1表示每次卷积核映射图像数据时,点位要行列都间隔1个格子。
groups 分组
bias= 默认True,需要偏置
padding_mode= str 填充模式,默认zero填充
device= 设备
dtype= 数据类型
代码:
python
import torch
import torch.nn as nn
from matplotlib import pyplot as plt
def test001():
img = plt.imread('./data/1.jpg')
img = torch.tensor(img, dtype=torch.float32).permute(2,0,1).unsqueeze(0)
# 创建卷积核
conv = nn.Conv2d(in_channels=3, # 与输入特征图的通道数相同
out_channels=3, # 决定输出特征图的通道数
kernel_size = 3, # 卷积核的大小
padding=1 # 填充周边
)
img_c = conv(img)
img_c = img_c.permute(0,2,3,1).squeeze(0)
plt.imshow(img_c.detach().numpy())
plt.show()
def test002():
input = torch.randn(10,2,5,6)
# 创建卷积核
conv = nn.Conv2d(in_channels=2, # 与输入特征图的通道数相同
out_channels=3, # 决定输出特征图的通道数
kernel_size = (3,5), # 卷积核的大小
padding=0, # 填充周边
stride=1,
bias=True
)
output = conv(input)
# n = (w-f+2p)/s+1 --> H: (5-3+2*0)/1+1=3 W: (6-5+2*0)/1+1=2
# w 通道大小
# f 卷积核大小
# p 填充大小
# s 步长大小
print(output.shape)
if __name__ == '__main__':
test001()
3、卷积计算的底层表示
4、padding 边缘填充
每次卷积都会对图像矩阵的行列数降低,若想要保持图像大小不变,则需要填充边缘,一般设置padding值为 (图像矩阵大小 - 卷积核大小)/2
如卷积计算的动图,就是padding设置为1 也就是图像矩阵大小减去卷积核大小(5-3)/2 的值
5、stride 步长
stride太小:重复计算较多,计算量大,训练效率降低;
stride太大:会造成信息遗漏,无法有效提炼数据背后的特征;
步长为1:
步长为2:
6、多通道计算
输入通道多时(图片一般为三通道或四通道),需要卷积核的 in_channels 等于输入通道数(卷积核的通道数与输入通道数 一致 ;卷积核的个数与输出通道数一致);
7、多卷积核计算
从不同到的视角、不同的角度对图像特征进行提取。
8、特征图大小
计算方式:
若行列并不相等(图像、卷积核等),那么就需要单独计算:
9、参数共享
若数据为 32*32*3 的图像,使用了10*5*5的卷积核进行卷积操作(只需要考虑图像的通道了,图像大小不重要,因为参数共享),那么其需要的权重参数为
- 5*5*3 =75 表示每一个卷积核所需要的参数
- 10*75=750 表示全部卷积核所需要的参数
- 若考虑偏置系数,则需要加上每个卷积核一个偏置系数,总共为760个参数
若是选择使用全连接,那么所有的连接都需要使用权重参数(图像大小):32*32 * 3* 10*5*5 +10*5*5 = 768,250
三、池化层 Pooling
1、意义
降低数据维度,减小模型,提高计算速度;主要用于对卷积层处理后的特征图进行下采样操作。
2、计算方式
分为了 最大池化 maxpolling 和 平均池化 avgpooling
2.1、最大池化
选取映射内容中最大的数值
2.2、平均池化
所有映射值的平均值
3、步长 stride
步长为1和步长为2 的对比
4、边缘填充 padding
5、多通道计算
对每个输入通道分别池化,而不是像卷积层那样将各个通道的输入相加。
池化层的输出和输入的通道数保持相等。
6、API
nn.MaxPool2d
参数:kernel_size = int 或 tuple 池化核,于卷积核一样
stride= int 或 tuple ,步长
padding= int 或 tuple ,填充
dilation= int 或 tuple ,膨胀
return_indices= 默认false,不返回取值对于下标
ceil_mode= 默认false,向上取整
python
import torch
import torch.nn as nn
def test01():
torch.manual_seed(1)
# 输入数据
input = torch.randint(0,255,(1,64,224,224))
print("输入数据:\n",input[0][0][:6,:6])
# 池化核
pool = nn.MaxPool2d(kernel_size=2,# 池化核大小
stride=2,
return_indices=True)
# 对数据池化
out,return_indices = pool(input)
print("下标:\n",return_indices[0][0][:6,:6])
print("输出结果:\n",out.shape)
if __name__ == '__main__':
test01()
四、卷积扩展
1、反卷积
因为一般情况下,使用卷积会造成缩小,所以需要使用填充对图像大小调整,称为反卷积
2、空洞卷积(膨胀)
使用参数 dilation 控制膨胀程度,1表示不膨胀,2表示膨胀一格距离。
python
import torch
import torch.nn as nn
def test003():
torch.manual_seed(1)
# 输入数据
x = torch.randn(1,1,7,7)
# 创建一个卷积核
conv = nn.Conv2d(in_channels=1,
out_channels=1,
kernel_size=3,
stride=1,
dilation=2,
)
out = conv(x)
print(out.shape)
print(out)
if __name__ == '__main__':
test003()
3、可分离卷积
3.1、空间可分离卷积
将卷积核拆分为两个独立的核计算(3*3 --> 3*1 + 1*3 ),拆分后计算量比标准卷积更少。
如图:5*5 的数据矩阵需要使用3*3的卷积核计算,先使用3*1的卷积核计算得到3*5得数据,在使用1*3得卷积和得到3*3得数据结果。
python
import torch
import torch.nn as nn
# def test004():
class nornalModel(nn.Module):
def __init__(self):
super(nornalModel, self).__init__()
self.conv1 = nn.Conv2d(in_channels=8,
out_channels=8,
kernel_size=3,
stride=1,
padding=0,
bias=False)
def forward(self, x):
self.conv1.weight.data.fill_(1)
x = self.conv1(x)
return x
class waveModel(nn.Module):
def __init__(self):
super(waveModel, self).__init__()
self.conv1 = nn.Conv2d(
in_channels=8,
out_channels=8,
kernel_size=(3,1),
stride=1,
padding=0,
bias=False
)
self.conv2 = nn.Conv2d(
in_channels=8,
out_channels=8,
kernel_size=(1,3),
stride=1,
padding=0,
bias=False
)
def forward(self,x):
self.conv1.weight.data.fill_(1)
self.conv2.weight.data.fill_(1)
x = self.conv1(x)
x = self.conv2(x)
return x
if __name__ == '__main__':
torch.manual_seed(1)
input = torch.randn(1,8,5,5)
model1 = nornalModel()
for name, param in model1.named_parameters():
print(name, param.shape)
torch.manual_seed(1)
input = torch.randn(1,8,5,5)
model2 = waveModel()
for name, param in model2.named_parameters():
print(name, param.shape)
# conv1.weight torch.Size([8, 8, 3, 3])
# conv1.weight torch.Size([8, 8, 3, 1])
# conv2.weight torch.Size([8, 8, 1, 3])
3.2、深度可分离卷积
在空间分离基础上加入通道分离,使用参数 groups进行划分(要求输入通道和输出通道都能被groups设定整数值整除);也就是使用卷积核得不同通道处理输入输出得不同通道。
python
import torch
import torch.nn as nn
class Net1(nn.Module):
def __init__(self):
super(Net1, self).__init__()
self.conv1 = nn.Conv2d(
in_channels=8,
out_channels=8,
kernel_size=3,
stride=1,
bias=False
)
def forward(self, x):
return self.conv1(x)
class deepmovemodel(nn.Module):
def __init__(self):
super(deepmovemodel, self).__init__()
self.conv1 = nn.Conv2d(
in_channels=8,
out_channels=8,
kernel_size=3,
stride=1,
bias=False,
groups=8 # 卷积核数量等于通道数
)
self.conv2 = nn.Conv2d(
in_channels=8,
out_channels=8,
kernel_size=1,
stride=1,
bias=False
)
def forward(self, x):
x = self.conv1(x)
torch.ones_like(self.conv2.weight.data)
x = self.conv2(x)
return x
if __name__ == '__main__':
torch.manual_seed(1)
input = torch.randn(1,8,5,5)
model1 = Net1()
for name, param in model1.named_parameters():
print(name,param.size())
model2 = deepmovemodel()
for name, param in model2.named_parameters():
print(name,param.size())
# conv1.weight torch.Size([8, 8, 3, 3])
# conv1.weight torch.Size([8, 1, 3, 3])
# conv2.weight torch.Size([8, 8, 1, 1])
3.3、扁平卷积
等同空间可分离卷积,如3*3得卷积核被拆分为3个1*1得卷积核,分别计算通道、宽度、高度三个方面内容。
3.4、分组卷积
等同深度可分离卷积。
AlexNet论文中最先提出来的概念,当时主要为了解决GPU显存不足问题。
卷积核被分成不同的组,每组负责对相应的输入层进行卷积计算,最后再进行合并。
3.5、感受野
理解为视野范围。
卷积操作从左到右为5*5矩阵经过3*3的卷积核卷积后得到3*3的矩阵,在经过3*3的卷积核卷积后得到1*1的矩阵,效果等同于5*5矩阵直接经过5*5的卷积核卷积操作。
感受野就是逆向的结果,两个3*3的卷积核且步长为1,感受野为5*5;同理,三个3*3的卷积核,感受野就为7*7,四个3*3为9*9(步长为1)
感受野的作用:假设原本输入大小为 h × w × C,并且使用了C个卷积核,那么标准的一个7*7卷积核就需要 c* (h*w*c) = 49 ,使用三个3*3的卷积核,那么其感受野也还是7*7,但是参数就变成了 3*C*(3*3*C) = 27
五、卷积神经网络案例
1、模型结构
输入:(1,32,32)通道为1,宽高为32的数据
卷积层1:输入1 个通道(等于输入数据的通道数),输出6个通道(卷积核个数),卷积核大小为3*3;
特征图1 :数据32*32,卷积核3*3,步长1,填充0:(32-3+2*0)/1 +1 =30,所以为6个30*30的特征图
池化层1:使用6个2*2的池化核进行处理
特征图2 :30*30池化后(2*2,步长为2)结果为其一半 15*15
卷积层2:输入6个通道,输出16个通道,卷积核为3*3
特征图3 :15*15卷积后得到13*13 的大小;16个13*13
池化层2:使用16个2*2的池化核进行处理
特征图4 :13*13池化后(2*2,步长为2)结果为其一半(向下取整,因为步长为2,所以取不到最右边和最下边的数据); 16个6*6
全连接1:输入需要16*6*6=576 维,输出为 120维
全连接2:输入120维,输出为 84维
全连接3:输入84维,输出为 10维
2、使用代码展现网络模型
python
import torch
import torch.nn as nn
class numberModee(nn.Module):
def __init__(self):
super(numberModee, self).__init__()
self.C1 = nn.Sequential(
nn.Conv2d(
in_channels=1, # 输入通道
out_channels=6, # 输出通道
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding=0 # 填充
),
nn.ReLU()
)
self.S2 = nn.MaxPool2d(kernel_size=2,
stride=2,
padding=0)
self.C3 = nn.Sequential(
nn.Conv2d(
in_channels=6,
out_channels=16,
kernel_size=3,
stride=1,
padding=0),
nn.ReLU()
)
self.S4 = nn.MaxPool2d(kernel_size=2,
stride=2,
padding=0)
self.C5 = nn.Sequential(
nn.Linear(16*6*6, 120),
nn.ReLU()
)
self.F6 = nn.Sequential(
nn.Linear(120, 84),
nn.ReLU()
)
self.OUTPUT = nn.Sequential(
nn.Linear(84, 10),
nn.Softmax(dim=1)
)
def forward(self, x):
x = self.C1(x)
x = self.S2(x)
x = self.C3(x)
x = self.S4(x)
x = x.view(x.size(0),-1)
x = self.C5(x)
x = self.F6(x)
x = self.OUTPUT(x)
return x
if __name__ == '__main__':
input = torch.randn(1, 1, 32, 32)
print(input)
print(input.shape)
net = numberModee()
out = net(input)
# 6 @ 30*30
# 6 @ 15*15
# 16 @ 13*13
# 16 @ 6*6
# 120
# 84
# 10
print(out)
print(out.shape)