前言
人每天处理的信号中,有超过70%的是视觉信号,所以视觉问题,包括分类,检测,分割、风格转换等等占了深度学习任务中的很大部分。而卷积神经网络是计算机视觉领域当之无愧的霸主。卷积神经网络是稀疏连接,并且权值共享的,参数比全连接要少非常多,所以完完全全可以用图像全像素作为输入,并且它比全连接网络更容易训练,且能做得更深。另外,卷积神经网络,浅层卷积提取简单特征,深层卷积提取复杂特征,还有感受野等设计都在一定程度上受大脑视觉皮层结构的启发1 ,所以它比较适合视觉任务。这篇笔记主要学习卷积神经网络的核心——卷积操作和Pytorch的卷积层。本笔记的知识框架主要来源于深度之眼,并依此作了内容的丰富拓展,拓展内容主要源自对torch文档的翻译,对孙玉林等著的PyTorch深度学习入门与实战的参考和自己的粗浅理解,所用数据来源于网络。发现有人在其他平台照搬笔者笔记,不仅不注明出处,有甚者更将其作为收费文章,因此笔者将在文中任意位置插入识别标志。
张量转图片可视化见:深度之眼Pytorch打卡(十):Pytorch数据预处理——数据统一与数据增强(上)
全连接网络部件见:深度之眼Pytorch打卡(十三):Pytorch全连接神经网络部件——线性层、非线性激活层与Dropout层(即全连接层、常用激活函数与失活 )
卷进操作(convolution)
第一次接触到卷积是在信号与系统中,把一个信号送入一个系统,在时域内输出信号就是输入信号与系统函数的卷积。它其实那就是一个相乘和相加操作,即线性运算,那里的卷积有四个步骤:翻转,移位,相乘和相加。每一次的移位都会有一个相乘求和结果,它的大小可以衡量此位置翻转后的系统函数与输入信号的相似性,数值越大,在该位置信号与系统函数的相似性就越高。深度学习或者是图像处理里的卷积,类似于信号处理里相关,没有翻转的操作,只有移位和相乘相加,它的输出结果是卷积核或者滤波器与其覆盖区域的相似性,激活值越大,代表覆盖区域与卷积核或者滤波器有越相近的特征。Pytorch
的卷积操作,有一维卷积,二维卷积和三维卷积和空洞卷积等。其中二维卷积,尤其是二维卷积中的多通道卷积最为常用,因为图像本身就是二维多通道的。
一维卷积示意图如图1所示,其摘自国外的一篇文章:Reading Minds with Deep Learning,卷积核在一个维度上,即一条线移动。图中非常明确的展现了一维卷积的过程——移位,相乘,相加。每次移位的元素个数称为步长stride
,图中stride=1
,由于没有拓展两端即Padding=0
,所以卷积后的输出长度变短了。
输出尺寸w1
与输入尺寸w
,卷积核大小f
,拓展大小p
和步长s
之间的关系如式(1),图中输入尺度w=6
,卷积核尺度f=3
,步长s=1
,拓展宽度p=0
,带入即可的w1=4
。
二维单通道的示意图如图2所示,其摘自国外的一篇文章:Convolutional Neural Networks - Basics,卷积核在两个维度上,即平面上移动,图中用的3*3的卷积核,熟悉图像处理的朋友应该能发现它是检测水平线的Sobel算子。图中展现的也是二维卷积核在二维输入上移位、相乘相加的过程。步长、拓展和输出计算同一维卷积,由图可见,其stride=1
,Padding=0
。
二维多通道卷积示意图如图3所示,其摘自这里,虽然它的输入和卷积核都是多维的,但它只是二维单通道卷积的拓展,本质上还是二维卷积。从图中可以看出输入通道数为3
,所以对应的卷积核通道数也为3
,即两者应该同深度。图中只有1
个卷积核,所以输出的通道数是1
,或者输出特征图数是1
,又或者输出深度是1
。对于这样的多通道卷积,它的操作过程与单通道一样,只是输出是卷积核各通道分别与输入的对应通道做卷积,然后将各通道卷积输出相加,再加上偏置的结果。 图中stride=1
,Padding=1
。
三维单通道卷积示意图如图4所示,其摘自国外的一篇博客:3D Convolutional Networks for Traffic Forecasting。三维卷积平常见的很少,卷积核在一个三维空间上移动,每移动一次都进行一次相乘相加操作。它与二维多通道卷积很像,尤其是它的输入和卷积核深度都是3
时连运算过程本质上都是一样,但二维卷积不论有多少个通道,卷积核都只是在一个平面上移动,而不是三维卷积中的空间上移动。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
空洞卷积示意图如图5所示,其摘自国外的一篇博客:An Introduction to different Types of Convolutions in Deep Learning。空洞卷积的卷积核中某些特定位置值被置成了0
,即空洞。空洞卷积可以提升感受野,毕竟图中用3*3
卷积核的参数就有了5*5
大小卷积核的感受野,虽然可能会丢失一些细节信息,但是每一个输出的确关联到了输入中更大的范围。空洞卷积多应用与分割任务。
卷积神经网络稀疏连接示意图如图6所示,由卷积过程,我们可以知道,一个输出神经元只与卷积核覆盖区域的若干输入神经元相连(有运算关系),而不是像全连接网络那样与全部的输入神经元相连,所以卷积神经网络神经元与神经元之间的连接是稀疏的。另外,由于特征的位置不变性,所以检测同一个特征时无论它出现在图像或者特征图的哪一个位置,用来检测提取它的卷积核都是一样的,所以输出同一幅特征图时是共用一组权值的,即同一个卷积核。这两个特点使得卷积神经网络可以做得比较深,而且更容易训练。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
Pytorch卷积层(conv layer)
Pytorch的卷积层主要有nn.Conv1d()、nn.Conv2d(),nn.Conv3d()等,它们的操作与参数都是相同的,所以下面只单独学习最最常用的二维卷积nn.Conv2d()
。
CLASS torch.nn.Conv2d(in_channels: int,
out_channels: int,
kernel_size: Union[int, Tuple[int, int]],
stride: Union[int, Tuple[int, int]] = 1,
padding: Union[int, Tuple[int, int]] = 0,
dilation: Union[int, Tuple[int, int]] = 1,
groups: int = 1,
bias: bool = True,
padding_mode: str = 'zeros')
# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
in_channels: 输入通道数,即输入图像或者特征图的深度,如RGB图像是3,灰度图像是1,和图3中输入深度3。它决定了卷积核的通道数或者深度。
out_channels: 输出通道数,即输出特征图的个数或者深度,如图3中输出通道数为1
。它决定了卷积核的个数,由权值共享可知一个特征图对应一个卷积核。
kernel_size:卷积核尺寸,卷积核宽高。
padding: 拓展边缘的宽度,二元组分别代表拓展的宽度和高度值。主要用于维持卷积前后的尺寸不变或者加强边缘信息的利用。如图7,通过padding
来维持卷积前后尺寸不变,其摘自国外的一篇博客:An Introduction to different Types of Convolutions in Deep Learning。
(CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
dilation:膨胀,用于控制卷积核两个元素之间的间隔,默认为1
,代表正常的卷积。为2
或者大于2
时便是如图5所示的那种空洞卷积。由于在卷积核中间填充了空洞,等效于卷积核变大了,所以输出尺寸表达式(1)中应该在kernal_size处做修改,得到式(2)和(3)。
输出输入,一般都会让宽高一样,而且两个维度上的padding,stride,kernal_size等一般也是相同的,即式(2)与(3)的输出值相同。当前面所述有一个不同时,式(2)与式(3)的值都会有不同。
groups: 分组卷积,默认为1
,即不分组。经典的卷积神经网络AlexNet
的groups就是2
,如图8所示,图源。当时Hinton他们是为了充分利用他们手上的两块GPU才做的分组,现在分组卷积变成了一种方法。分组卷积常用在轻量型高效网络中,因为它用少量的参数量和运算量就能生成大量的特征图。
bias: 是否要偏置。
padding_mode: 填充模式,为字符串,包括填充0,镜像,复制等。'zeros', 'reflect', 'replicate' or 'circular'
。
nn.Conv1d()、nn.Conv2d(),nn.Conv3d()都继承于_ConvNd
,而_ConvNd
又继承于Module
,所以它们像上一篇笔记中所述的线性层那样,是Pytorch神经网络模型的模块,且是不可再分的最小模块。类Conv2d()
中,也是主要由__init__
和forward
方法构成,源码如下。
class Conv2d(_ConvNd):
def __init__(self, in_channels, out_channels, kernel_size, stride=1,
padding=0, dilation=1, groups=1,
bias=True, padding_mode='zeros'):
kernel_size = _pair(kernel_size)
stride = _pair(stride)
padding = _pair(padding)
dilation = _pair(dilation)
super(Conv2d, self).__init__(
in_channels, out_channels, kernel_size, stride, padding, dilation,
False, _pair(0), groups, bias, padding_mode)
def conv2d_forward(self, input, weight):
if self.padding_mode == 'circular':
expanded_padding = ((self.padding[1] + 1) // 2, self.padding[1] // 2,
(self.padding[0] + 1) // 2, self.padding[0] // 2)
return F.conv2d(F.pad(input, expanded_padding, mode='circular'),
weight, self.bias, self.stride,
_pair(0), self.dilation, self.groups)
return F.conv2d(input, weight, self.bias, self.stride,
self.padding, self.dilation, self.groups)
def forward(self, input):
return self.conv2d_forward(input, self.weight)
# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
F.conv2d不能直接进入,它是由C++写的,我们只能看到接口,源码大概在这里。笔者没有理清源码的思路,所以另查了资料来间接搞清楚实现过程。
卷积的实现并不是像上文说的那样移位、相乘再相加,那样太费时了,而是通过矩阵乘法的方式高效完成的,如图9所示,图改自国外的一篇文章:A Comprehensive Introduction to Different Types of Convolutions in Deep Learning。把卷积核转成稀疏矩阵C,再把输入flatten成一维张量,两者内积再resize
便得到了卷积结果。
稀疏矩阵C(Sparse matrix C)可以理解成是由若干被flatten
成一维张量的卷积核(kernel)移位叠放而成的,叠放层数等于卷积输出的元素个数,如图9中的2X2=4
。在flatten
卷积核前,要先把它拓展到与input
相同的尺寸,往右边和下边拓展,用零填充。卷积核填充与flatten
过程,如图10所示。
把卷积核kernel
填充并且flatten后变成1x16
的张量kernel_flatten
。稀疏矩阵C的第一行,将kernel_flatten
向右移0*stride
位后放入,第二行,将kernel_flatten
向右移1*stride
位后放入,前端空白处补0,后端超过的去掉。以此往下…,第n行,将kernel_flatten
向右移(n-1)*stride
位后放入。当满足条件:移位步数(n-1)*stride
加上卷积核的宽Wk
等于Input的宽Wi
的时候(即卷积核在滑动过程中换行点在稀疏矩阵中的反映),则下一行,将kernel_flatten
移位s*k*Wi
位后放入,其中s
为在高维度上的步长,k
满足上述条件的次数,并以该行为第一行重复上述步骤。当行数总和等于输出元素个数时,稀疏矩阵C就形成了。(以上是笔者个人理解,有不对之处,望指出)
通过上述方法,很容易得到图11中的稀疏矩阵C,并且可以透过C观察出宽和高维度上的stride
都等于1
。稀疏矩阵C中的每一行与Input的内积,都一一对应卷积操作中的一个移位相乘相加。
图像处理中的很多操作都是通过卷积运算来完成的,以下用一个简单的边缘检测算子sobel
来学习卷积层的使用,检测水平和竖直方向边缘算子见图12。将这两个算子填入卷积层的权值张量中,不设置偏置,代码如下。
main.py
import torch
import torch.nn as nn
from PIL import Image
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
from tools.transform_inverse import transform_inverse
pil_img = Image.open('data/lenna.jpg').convert('L')
img = transforms.ToTensor()(pil_img)
c = img.size()[0]
h = img.size()[1]
w = img.size()[2]
input_img = torch.reshape(img, [1, c, h, w]) # 转换成4维,[batch_size, c, h, w]
print(input_img.size())
conv1 = nn.Conv2d(1, 2, (3, 3), bias=False) # 实例化
conv1.weight.data[0] = torch.tensor([[1, 2, 1],
[0, 0, 0],
[-1, -2, -1]]) # 水平边缘的sobel算子
conv1.weight.data[1] = torch.tensor([[-1, 0, 1],
[-2, 0, 2],
[-1, 0, 1]]) # 竖直边缘的sobel算子
# print(conv1.weight.data)
out_img = conv1(input_img) # 输出两张特征图
print(out_img.size())
out_img = torch.squeeze(out_img)
out_img = out_img[0]+out_img[1] # 两个边缘特征图相加
out_pil_img = transform_inverse(torch.reshape(out_img, [1, out_img.size()[0], out_img.size()[1]]), None)
plt.figure(0)
ax = plt.subplot(1, 2, 1)
ax.set_title('input picture')
ax.imshow(pil_img, cmap='gray')
ax = plt.subplot(1, 2, 2)
ax.set_title('output picture')
ax.imshow(out_pil_img, cmap='gray')
plt.show()
# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
transform_inverse.py
,该函数详细解释见这篇笔记
import torch
import numpy as np
from PIL import Image
from torchvision import transforms
def transform_inverse(img, transform):
# 将tensor转换成pil数据
if 'Normalize' in str(transform):
Normalize_trans = list(filter(lambda x: isinstance(x, transforms.Normalize), transform.transforms))
m = torch.tensor(Normalize_trans[0].mean, dtype=img.dtype, device=img.device)
s = torch.tensor(Normalize_trans[0].std, dtype=img.dtype, device=img.device)
img.mul_(torch.reshape(s, [-1, 1, 1])).add_(torch.reshape(m, [-1, 1, 1])) # 需要调整形状才能通道对应
img = torch.transpose(img, dim0=0, dim1=1) # C H W ->H C W
img = torch.transpose(img, dim0=1, dim1=2) # H C W ->H W C
if img.requires_grad:
img = img.detach().numpy()
else:
img = np.array(img)*255 # 去归一化
if img.shape[2] == 3:
img = Image.fromarray(img.astype('uint8')).convert('RGB') # 转换成PIL RGB图像
elif img.shape[2] == 1:
img = Image.fromarray(img.astype('uint8').squeeze(), 'L') # (1, H, W)->(H, W)
else:
print('Invalid img format')
return img
# (CSDN意疏原创笔记:https://blog.csdn.net/sinat_35907936/article/details/107833112)
结果:
torch.Size([1, 1, 440, 440])
torch.Size([1, 2, 438, 438])
参考
[16] Hubel DH, Wiesel TN. Receptive fields, binocular interaction and functional architecture in the cat’s visual cortex[J]. Journal of Physiology,1962,160(1): 106-154. ↩︎