终于到了卷积神经网络了。卷积作为一个在数学中非常重要的概念,在深度学习中也有着举足轻重的作用。一般来说,在深度学习中卷积最常用于对图片数据进行处理。通过卷积,我们能够对输入图像进行特定的数字信号处理从而提取出其特征。卷积、池化和全连接等众多层共同构成的网络共同构成卷积神经网络。
卷积之所以称为卷积,就算在图像卷积中有非常生动的“滑动窗口”的可视化过程,但是其本质依旧遵循如下数学公式:
g ( x , y ) = w ∗ f ( x , y ) = ∑ d x = − a a ∑ d y = − b b w ( d x , d y ) f ( x − d x , y − d y ) g(x,y)=w * f(x,y)=\sum_{dx=-a}^{a}\sum_{dy=-b}^{b}w(dx,dy)f(x-dx,y-dy) g(x,y)=w∗f(x,y)=dx=−a∑ady=−b∑bw(dx,dy)f(x−dx,y−dy)
其中 w w w为滤波器的卷积核, f f f为原图像, g g g为卷积后图像。
图像由像素点构成,本质上是一个由数值构成的二维(灰度)或三维(多通道图像)矩阵,卷积核也同样是一个矩阵,因此这个卷积过程也是一个矩阵的运算过程。当前最经典的可视化理解就是滑动窗口的卷积运算过程:
显然,滑动计算的方式,是一种便于理解卷积运算的方式,但并不是对于卷积的高效计算方法。 以前使用C++语言写过一个卷积的小demo,就是使用这种滑动计算的方式,其效率非常非常低下。而且,如果这么滑动,就算计算出来了,但是求导和反向传播怎么办呢?
对此我们采用下图的处理方式:
左边的矩阵为卷积核在输入尺寸上的展开,因为卷积核必然小于等于输入大小,因此,在相差的部分插入0(原卷积核位置都没有,不需要和当前位置的原图相乘,因此该位置乘以0,不影响求和结果),中间的矩阵为输入的一维展开,右边的矩阵即为输出结果。按照(原图长-卷积核长+1)*(原图宽-卷积核宽+1)的结果重新构建输出图像的矩阵即可。
这种方法在前向传播中,由于其为单纯矩阵运算,可以很方便地运用CPU或GPU进行矩阵快速计算(GPU更快,所以在这块用GPU加速非常明显),在反向传播过程中,由于矩阵存在便于构建计算图,反向更新也变得非常高效。
在torch中,我们有如下算子:
这几个算子的计算,在卷积这一过程中的运算本质是相同的,这些分为三个大类:普通卷积、转置卷积和延迟初始化的卷积。
同时,我们不难发现其中有conv1d、conv2d、conv3d,这三个算子有什么不同呢?
这里面的d为dimension,即维度,不难理解这三个对应的就是一维、二维、三维卷积。数据的维度决定了其适用的卷积,如一维的声音、电压值等;二维的灰度图;三维的3D模型。
这些算子都帮我们写好了,直接使用即可。比如前面hw4我们做的图片卷积。
这部分我们将在后面加入了步长和填充后一起给出,因为目前这部分还没有介绍,卷积算子还不能很完善。
为了计算和优化,我们需要确定卷积的所有参数。这边的参数我们定义为可训练的参数。以二维卷积为例,我们假设输入通道数为 n i n n_{in} nin,输出通道数为 n o u t n_{out} nout,卷积核大小为 h × w h \times w h×w,则显然可得参数总量 p a r a m s t o t a l = ( h × w × n i n + 1 ) × n o u t params_{total} = (h \times w \times n_{in} + 1) \times n_{out} paramstotal=(h×w×nin+1)×nout,其中的加一是因为有偏置值的存在。
计算量的产生就出现在前面的卷积过程中,设特征图长宽为 w f , h f w_{f},h_{f} wf,hf。显然易得计算总量 C t o t a l = ( ( n i n × h × w ) + ( n i n × h × w − 1 ) + 1 ) × n o u t × w f × h f C_{total}=((n_{in}\times h \times w) + (n_{in} \times h \times w -1) + 1) \times n_{out} \times w_{f} \times h_{f} Ctotal=((nin×h×w)+(nin×h×w−1)+1)×nout×wf×hf
资料来源:网页链接
在卷积神经网络中,感受野(Receptive Field)是指特征图上的某个点能看到的输入图像的区域,即特征图上的点是由输入图像中感受野大小区域的计算得到的。通俗点的解释是,特征图上一点,相对于原图的大小,也是卷积神经网络特征所能看到输入图像的区域。
感受野的作用:
1、小尺寸的卷积代替大尺寸的卷积,可减少网络参数、增加网络深度、扩大感受野(例如:3 个 3 x 3 的卷积层的叠加可以替代7*7的卷积),网络深度越深感受野越大性能越好;
2、对于分类任务来说,最后一层特征图的感受野大小要大于等于输入图像大小,否则分类性能会不理想;
3、对于目标检测任务来说,若感受野很小,目标尺寸很大,或者目标尺寸很小,感受野很大,模型收敛困难,会严重影响检测性能;所以一般检测网络anchor的大小的获取都要依赖不同层的特征图,因为不同层次的特征图,其感受野大小不同,这样检测网络才会适应不同尺寸的目标。
有时候图片特别大(比如拿8K摄像机拍一张马路照片)但是局部信息都比较少且过渡平滑,没必要花费很多时间在计算整张很大图片的卷积计算上,于是我们需要调整卷积的步长。步长是卷积核在原图上滑动一次的跨度。如步长为1就是卷积核计算一次后向右移动一个像素,步长为2则是移动两个像素。
下面的图片就是当步长为2的卷积过程:
为了方便保持图像大小不变,或者保留边缘特征,我们可以在图像的边缘增加一圈围着的、不会改变卷积结果的“0”。通过设置零填充配合步长,能够得到良好的、和原图尺寸成对应缩放关系的特征图。如上图padding=1时,如果步长为1,则输出与原图相同大小,步长为2则图片大小减小一半。
Pytorch内置的二维卷积已经包含了步长和零填充的参数:当不进行设置时,默认步长为1且不进行零填充。其中还有非常多的参数:
class SimpleConv2d(torch.nn.Module):
def __init__(self, kernel_size, stride = 1, padding = 0):
super(SimpleConv2d, self).__init__()
self.kernel_size = kernel_size if isinstance(kernel_size,tuple) else (kernel_size, kernel_size)
self.stride = stride
self.padding = padding
self.kernel = torch.randn(self.kernel_size, requires_grad = True)
def forward(self, x):
_w, _h = x.shape
kernel = np.zeros(_w, _h)
kernel = kernel2matrix(kernel, kernel_w, kernel_h) #
x = image2column(x, self.kernel_size, self.stride, self.padding) # 图片变换
return torch.mm(kernel, x)
img = plt.imread(r'C:\Users\Lupnis\Downloads\小豆泥.jpg') / 255.
fig,*ax = plt.subplots(1,2,figsize=(20,10))
ax[0][0].imshow(img,cmap='gray')
ax[0][0].set_xticks([])
ax[0][0].set_yticks([])
ax[0][0].set_title('original image')
img_kernel = torch.tensor([[[
[ -1., -1., -1.],
[ -1., 8., -1.],
[ -1., -1., -1.]
]]],dtype=torch.float).expand(3,1,3,3)
img_new = torch.conv2d(torch.tensor(img,dtype=torch.float).permute(2,0,1).unsqueeze(0),img_kernel,groups=3)
img_new = torch.clamp(img_new,0.,1.).detach().squeeze(0).permute(1,2,0)
ax[0][1].imshow(img_new)
ax[0][1].set_xticks([])
ax[0][1].set_yticks([])
ax[0][1].set_title('outline')
通过本次实验,我们了解到了卷积的一些基础知识,通过前面hw4的知识铺垫,我们接下来的学习将会轻松很多。