一、概述
卷积神经网络(Convolutional Neural Network,CNN)是一种专门用于处理具有网格状结构数据的深度学习模型。最初,CNN主要应用于计算机视觉任务,但它的成功启发了在其他领域应用,如自然语言处理等。
卷积神经网络(Convolutional Neural Network)是含有卷积层的神经网络。卷积层的作用就是用来自动学习、提取图像的特征。
CNN网络主要有三部分构成:卷积层、池化层和全连接层构成,其中卷积层负责提取图像中的局部特征;池化层用来大幅降低运算量并特征增强;全连接层类似神经网络的部分,用来输出想要的结果。
二、卷积思想
1、相关概念
Convolution,输入信息与卷积核(滤波器,Filter)的乘积。
卷:从左往右,从上往下;积:乘积,求和
局部连接
-
局部连接可以更好地利用图像中的结构信息,空间距离越相近的像素其相互影响越大。
-
根据局部特征完成目标的可辨识性。
权重共享
-
图像从一个局部区域学习到的信息应用到其他区域。
-
减少参数,降低学习难度。
2、卷积层
1.卷积核
在卷积神经网络中,卷积核是非常重要的,它们被用来提取图像中的特征。
-
卷积核的个数:卷积核(过滤器)的个数决定了其输出特征矩阵的通道数。
-
卷积核的值:卷积核的值是自定义的,根据想要提取的特征来进行设置,后续进行更新。
-
卷积核的大小:常见的卷积核有1×1、3×3、5×5等,一般都是奇数 × 奇数。
2.卷积计算
卷积的过程是将卷积核在图像上进行滑动计算,每次滑动到一个新的位置时,卷积核和图像进行点对点的计算,并将其求和得到一个新的值,然后将这个新的值加入到特征图中,最终得到一个新的特征图。
-
input 表示输入的图像
-
filter 表示卷积核, 也叫做滤波器
-
input 经过 filter 的得到输出为最右侧的图像,该图叫做特征图
卷积的重要性在于它可以将图像中的特征与卷积核进行卷积操作,从而提取出图像中的特征。
可以通过不断调整卷积核的大小、卷积核的值和卷积操作的步长,可以提取出不同尺度和位置的特征。
单特征图卷积输出:
import os
import torch
import torch.nn as nn
from matplotlib import pyplot as plt
def showing(img):
plt.imshow(img)
plt.show()
def test01():
# 获取当前脚本文件所在的目录路径
current_path = os.path.dirname(__file__)
print(current_path)
# 找到该路径下的图像
img_path = os.path.join(current_path,'data','1,png')
# 转换为相对路径
img_path = os.path.relpath(img_path)
# 使用plt读取图片
img = plt.imread(img_path)
# print(img.shape) # # torch.Size([808, 576, 4])
# 创建卷积核
"""
in_channels 输入通道数
out_channels 输出通道数
kernel_size 卷积核大小
stride 步长
padding 填充
bias 布尔值 卷积层是否包含偏置项
"""
conv = nn.Conv2d(
in_channels=4,
out_channels=1,
kernel_size=3,
stride=1,
padding=0,
bias=True
)
# 注意:卷积层对输入的数据有形状要求 [batch, channel, height, width]
# 需要进行形状转换 且转换为张量 HWC->CHW->NCHW
img = torch.tensor(img,dtype=torch.float).permute(2,0,1).unsqueeze(0)
print(img.shape) # torch.Size([1, 4, 808, 576])
# 卷积操作
out = conv(img)
print(img.shape) # torch.Size([1, 1, 808, 576])
# 将BCHW->HWC 便于显示
img = img.squeeze(0).permute(1, 2, 0)
showimg(img.detach().numpy())
多特征图卷积输出:
# 多特征图输出
def test002():
dir = os.path.dirname(__file__)
img = plt.imread(os.path.join(dir, 'data', '1.png'))
# 定义一个多特征图输出的卷积核
conv = nn.Conv2d(
in_channels=4,
out_channels=3, # 三个输出
kernel_size=3,
stride=1,
padding=1)
# 图形要进行变形处理
img = torch.tensor(img).permute(2, 0, 1).unsqueeze(0)
print(img.shape) # torch.Size([1, 4, 808, 576])
# 使用卷积核对图片进行卷积计算
outimg = conv(img)
print(outimg.shape) # torch.Size([1, 3, 808, 576])
# 把图形形状转换回来以方便显示 BCHW->CHW->HWC
outimg = outimg.squeeze(0).permute(1, 2, 0)
print(outimg.shape) # torch.Size([808, 576, 3])
# 显示这三张特征图
for idx in range(outimg.shape[2]):
showing(outimg[:,:,idx].squeeze(-1).detach())
3.边缘填充 padding
通过上面的卷积计算,我们发现最终的特征图比原始图像要小,如果想要保持图像大小不变, 可在原图周围添加padding来实现。
更重要的,边缘填充还更好的保护了图像边缘数据的特征。
4.步长 stride
按照步长为1来移动卷积核,计算特征图如下所示:
如果我们把 Stride 增大为2,也是可以提取特征图的,如下图所示:
stride太小:重复计算较多,计算量大,训练效率降低;
stride太大:会造成信息遗漏,无法有效提炼数据背后的特征;
5.多通道卷积计算
实际中的图像都是多个通道组成的:
就需要:
-
当输入有多个通道(Channel), 例如RGB三通道, 此时要求卷积核需要有相同的通道数。
-
卷积核通道与对应的输入图像通道进行卷积。
-
将每个通道的卷积结果按位相加得到最终的特征图。
(图片中的结果应为 -71)
6.多卷积核卷积计算
实际对图像进行特征提取时, 我们需要使用多个卷积核进行特征提取。这个多个卷积核可以理解为从不同到的视角、不同的角度对图像特征进行提取。
输出特征图的大小计算方式(粗略版,以后会补充):
-
输入图像大小: W x W
-
卷积核大小: F x F
-
Stride: S
-
Padding: P
-
输出图像大小: N x N
输出特征图大小计算:
import torch
import torch.nn as nn
"""卷积输出图大小"""
def test01():
input = torch.randn(1,1,5,5)
# 创建卷积核
conv = nn.Conv2(
in_channels=1,
out_channels=1,
kernel_size=3,
stride=1,
padding=0,
bias=True
)
# 输出图大小(5-3+2*0)/1+1=3
out = conv(input)
print(out.shape) # torch.Size([1, 1, 3, 3])
多卷积核——神经网络构建:
import torch
import torch.nn as nn
import torch.nn.functional as F
class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__()
# 卷积层 隐藏层
self.conv1 = nn.Conv2d(
in_channels=1, # 输入通道数
out_channels=32, # 输出通道数
kernel_size=3, # 卷积核大小
padding=0,
stride=1 # 步长
)
# 输出特征图大小 30*30
self.conv2 = nn.Conv2d(
in_channels=32, # 输入通道数
out_channels=128, # 输出通道数
kernel_size=3, # 卷积核大小
padding=0,
stride=1 # 步长
)
# 输出特征图大小 28*28
self.conv3 = nn.Conv2d(
in_channels=128, # 输入通道数
out_channels=512, # 输出通道数
kernel_size=3, # 卷积核大小
padding=0,
stride=1 # 步长
)
# 输出特征图大小 26*26
# 线性层 输出层
# 分类判断 10分类
self.fc = nn.Linear(512*26*26,10)
def forward(self,x):
# 前向传播
x = F.relu(self.conv1(x))
x = F.relu(self.conv2(x))
x = F.relu(self.conv3(x))
# 输出 NCHW->ND 线性层需要(batch_size, num_features)的输入形式
x = x.view(x.size(0),-1)
x = self.fc(x)
x = nn.Softmax(dim=1)(x)
return x
if __name__ == '__main__':
input = torch.randn(64,1,32,32)
net = MyNet()
out = net(input)
print(out.shape) # torch.Size([64, 10])
# print(out)
7.卷积参数共享
数据是 32×32×3 的图像,用 10 个 5×5×3 的filter来进行卷积操作,所需的权重参数:
-
5×5×3 = 75,表示每个卷积核只需要 75 个参数。
-
10个不同的卷积核,就需要10*75 = 750 个卷积核参数。
-
如果还考虑偏置参数$b$,最终需要 750+10=760 个参数。
全连接层的参数量
import torch
import torch.nn as nn
class MyNet(nn.Module):
def __init__(self):
super(MyNet, self).__init__()
# 隐藏层 线性层
self.linear1 = nn.Linear(60*60*3, 38*38*3)
# 输出层
self.fc = nn.Linear(38*38*3,10)
def forward(self, x):
x = self.linear1(x)
return x
if __name__ == '__main__':
linearmodel = MyNet()
# 获取网络参数
params = linearmodel.named_parameters()
for name,param in params:
print(name,param)
卷积层的参数量
import torch
import torch.nn as nn
class MyConv(nn.Module):
def __init__(self):
super(MyConv,self).__init__()
self.conv1 = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=8, # 输出通道数
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding=0, # 填充
bias=True
)
# 输出层
self.fc = nn.Linear(30*30*8,10)
def forward(self,x):
x = self.conv1(x)
# x = x.view(-1,30*30*8)
x=x.view(x.size(0),-1)
print(x.shape)
x = self.fc(x)
return x
if __name__ == "__main__":
convmodel = MyConv()
input = torch.randn(1,3,64,64)
out = convmodel(input)
print(out.shape)
# params = convmodel.named_parameters()
# for name,param in params:
# print(name,param)
8.卷积计算
import torch
import torch.nn as nn
class MyConv(nn.Module):
def __init__(self):
super(MyConv,self).__init__()
self.conv1 = nn.Conv2d(
in_channels=3, # 输入通道数
out_channels=8, # 输出通道数
kernel_size=3, # 卷积核大小
stride=1, # 步长
padding=0, # 填充
bias=True
)
# 输出层
self.fc = nn.Linear(30*30*8,10)
def forward(self,x):
x = self.conv1(x)
# x = x.view(-1,30*30*8)
x=x.view(x.size(0),-1)
print(x.shape)
x = self.fc(x)
return x
if __name__ == "__main__":
convmodel = MyConv()
input = torch.randn(1,3,64,64)
out = convmodel(input)
print(out.shape)
# params = convmodel.named_parameters()
# for name,param in params:
# print(name,param)