卷积
卷积这个概念最先在数学中学过,先看看下图卷积神经网络的计算过程。
上图卷积的计算跟数学中的卷积是不一样的。但在深度学习中我们还是称为卷积
CNN的基本结构
图中是一个图形识别的CNN模型。可以看出最左边的船的图像就是我们的输入层,计算机理解为输入若干个矩阵,这点和DNN基本相同。
接着是卷积层(Convolution Layer),这个是CNN特有的,我们后面专门来讲。卷积层的激活函数使用的是ReLU。我们在DNN中介绍过ReLU的激活函数,它其实很简单,就是ReLU(x)=max(0,x)ReLU(x)=max(0,x)。在卷积层后面是池化层(Pooling layer),这个也是CNN特有的,我们后面也会专门来讲。需要注意的是,池化层没有激活函数。
卷积层+池化层的组合可以在隐藏层出现很多次,上图中出现两次。而实际上这个次数是根据模型的需要而来的。当然我们也可以灵活使用使用卷积层+卷积层,或者卷积层+卷积层+池化层的组合,这些在构建模型的时候没有限制。但是最常见的CNN都是若干卷积层+池化层的组合,如上图中的CNN结构。
在若干卷积层+池化层后面是全连接层(Fully Connected Layer, 简称FC),全连接层其实就是我们前面讲的DNN结构,只是输出层使用了Softmax激活函数来做图像识别的分类,这点我们在DNN中也有讲述。
从上面CNN的模型描述可以看出,CNN相对于DNN,比较特殊的是卷积层和池化层,如果我们熟悉DNN,只要把卷积层和池化层的原理搞清楚了,那么搞清楚CNN就容易很多了。
初识卷积
首先,我们去学习卷积层的模型原理,在学习卷积层的模型原理前,我们需要了解什么是卷积,以及CNN中的卷积是什么样子的。
大家学习数学时都有学过卷积的知识,微积分中卷积的表达式为:
S ( t ) = ∫ x ( t − a ) w ( a ) d a S(t) = \int x(t-a)w(a) da S(t)=∫x(t−a)w(a)da
离散形式是:
s ( t ) = ∑ a x ( t − a ) w ( a ) s(t) = \sum\limits_ax(t-a)w(a) s(t)=a∑x(t−a)w(a)
这个式子如果用矩阵表示可以为:
s ( t ) = ( X ∗ W ) ( t ) s(t)=(X*W)(t) s(t)=(X∗W)(t)
其中星号表示卷积。
如果是二维的卷积,则表示式为:
s ( i , j ) = ( X ∗ W ) ( i , j ) = ∑ m ∑ n x ( i − m , j − n ) w ( m , n ) s(i,j)=(X*W)(i,j) = \sum\limits_m \sum\limits_n x(i-m,j-n) w(m,n) s(i,j)=(X∗W)(i,j)=m∑n∑x(i−m,j−n)w(m,n)
在CNN中,虽然我们也是说卷积,但是我们的卷积公式和严格意义数学中的定义稍有不同,比如对于二维的卷积,定义为:
s ( i , j ) = ( X ∗ W ) ( i , j ) = ∑ m ∑ n x ( i + m , j + n ) w ( m , n ) s(i,j)=(X*W)(i,j) = \sum\limits_m \sum\limits_n x(i+m,j+n) w(m,n) s(i,j)=(X∗W)(i,j)=m∑n∑x(i+m,j+n)w(m,n)
这个式子虽然从数学上讲不是严格意义上的卷积,但是大牛们都这么叫了,那么我们也跟着这么叫了。后面讲的CNN的卷积都是指的上面的最后一个式子。
其中,我们叫W为我们的卷积核,而X则为我们的输入。如果X是一个二维输入的矩阵,而W也是一个二维的矩阵。但是如果X是多维张量,那么W也是一个多维的张量。
CNN中的卷积层
有了卷积的基本知识,我们现在来看看CNN中的卷积,假如是对图像卷积,回想我们的上一节的卷积公式,其实就是对输入的图像的不同局部的矩阵和卷积核矩阵各个位置的元素相乘,然后相加得到。
举个例子如下,图中的输入是一个二维的3x4的矩阵,而卷积核是一个2x2的矩阵。这里我们假设卷积是一次移动一个像素来卷积的,那么首先我们对输入的左上角2x2局部和卷积核卷积,即各个位置的元素相乘再相加,得到的输出矩阵S的 S 00 S_{00} S00的元素,值为 a w + b x + e y + f z aw+bx+ey+fz aw+bx+ey+fz。接着我们将输入的局部向右平移一个像素,现在是(b,c,f,g)四个元素构成的矩阵和卷积核来卷积,这样我们得到了输出矩阵S的 S 01 S_{01} S01的元素,同样的方法,我们可以得到输出矩阵S的 S 02 , S 10 , S 11 , S 12 S_{02},S_{10},S_{11},S_{12} S02,S10,S11,S12的元素。
最终我们得到卷积输出的矩阵为一个2x3的矩阵S。
再举一个动态的卷积过程的例子如下:
我们有下面这个绿色的5x5输入矩阵,卷积核是一个下面这个黄色的3x3的矩阵。卷积的步幅是一个像素。则卷积的过程如下面的动图。卷积的结果是一个3x3的矩阵。
上面举的例子都是二维的输入,卷积的过程比较简单,那么如果输入是多维的呢?比如在前面一组卷积层+池化层的输出是3个矩阵,这3个矩阵作为输入呢,那么我们怎么去卷积呢?又比如输入的是对应RGB的彩色图像,即是三个分布对应R,G和B的矩阵呢?
在斯坦福大学的cs231n的课程上,有一个动态的例子,链接在这。建议大家对照着例子中的动图看下面的讲解。
大家打开这个例子可以看到,这里面输入是3个7x7的矩阵。实际上原输入是3个5x5的矩阵。只是在原来的输入周围加上了1的padding,即将周围都填充一圈的0,变成了3个7x7的矩阵。
例子里面使用了两个卷积核,我们先关注于卷积核W0。和上面的例子相比,由于输入是3个7x7的矩阵,或者说是7x7x3的张量,则我们对应的卷积核W0也必须最后一维是3的张量,这里卷积核W0的单个子矩阵维度为3x3。那么卷积核W0实际上是一个3x3x3的张量。同时和上面的例子比,这里的步幅为2,也就是每次卷积后会移动2个像素的位置。
最终的卷积过程和上面的2维矩阵类似,上面是矩阵的卷积,即两个矩阵对应位置的元素相乘后相加。这里是张量的卷积,即两个张量的3个子矩阵卷积后,再把卷积的结果相加后再加上偏倚b。
7x7x3的张量和3x3x3的卷积核张量W0卷积的结果是一个3x3的矩阵。由于我们有两个卷积核W0和W1,因此最后卷积的结果是两个3x3的矩阵。或者说卷积的结果是一个3x3x2的张量。
仔细回味下卷积的过程,输入是7x7x3的张量,卷积核是两个3x3x3的张量。卷积步幅为2,最后得到了输出是3x3x2的张量。如果把上面的卷积过程用数学公式表达出来就是:
s ( i , j ) = ( X ∗ W ) ( i , j ) + b = ∑ k = 1 n _ i n ( X k ∗ W k ) ( i , j ) + b s(i,j)=(X*W)(i,j) + b = \sum\limits_{k=1}^{n\_in}(X_k*W_k)(i,j) +b s(i,j)=(X∗W)(i,j)+b=k=1∑n_in(Xk∗Wk)(i,j)+b
其中, n _ i n n\_in n_in为输入矩阵的个数,或者是张量的最后一维的维数。 X k X_k Xk代表第k个输入矩阵。 W k W_k Wk代表卷积核的第k个子卷积核矩阵。 s ( i , j ) s(i,j) s(i,j)即卷积核W对应的输出矩阵的对应位置元素的值。
通过上面的例子,相信大家对CNN的卷积层的卷积过程有了一定的了解。
对于卷积后的输出,一般会通过ReLU激活函数,将输出的张量中的小于0的位置对应的元素值都变为0。
卷积层输出形状计算
o
u
p
u
t
h
e
i
g
h
t
=
i
n
p
u
t
h
e
i
g
h
t
−
k
+
2
p
s
+
1
ouput_{height} = \frac{input_{height}-k+2p}{s}+1
ouputheight=sinputheight−k+2p+1
k 是卷积核的尺寸(假设长宽相等),p 表示padding的大小,s 表示stride的大小
池化层
相比卷积层的复杂,池化层则要简单的多,所谓的池化,个人理解就是对输入张量的各个子矩阵进行压缩。假如是2x2的池化,那么就将子矩阵的每2x2个元素变成一个元素,如果是3x3的池化,那么就将子矩阵的每3x3个元素变成一个元素,这样输入矩阵的维度就变小了。
要想将输入子矩阵的每nxn个元素变成一个元素,那么需要一个池化标准。常见的池化标准有2个,MAX或者是Average。即取对应区域的最大值或者平均值作为池化后的元素值。
下面这个例子采用取最大值的池化方法。同时采用的是2x2的池化。步幅为2。
首先对红色2x2区域进行池化,由于此2x2区域的最大值为6.那么对应的池化输出位置的值为6,由于步幅为2,此时移动到绿色的位置去进行池化,输出的最大值为8.同样的方法,可以得到黄色区域和蓝色区域的输出值。最终,我们的输入4x4的矩阵在池化后变成了2x2的矩阵。进行了压缩。
1*1卷积核
这是一种特殊的卷积核,这种卷积核主要为了降维和升维的操作。 如下图所示,如果选择2个filter的1*1卷积层,那么数据就从原来的三维降维到2维,右边是升维的操作。
CNN模型结构小结
理解了CNN模型中的卷积层和池化层,就基本理解了CNN的基本原理,后面再去理解CNN模型的前向传播算法和反向传播算法就容易了。下一篇我们就来讨论CNN模型的前向传播算法。
附:代码分析
下面的代码来自tensorflow.keras.layers中的卷积部分,仅看创建模型和调用的代码。
def build(self, input_shape):
input_shape = tensor_shape.TensorShape(input_shape)
input_channel = self._get_input_channel(input_shape)
if input_channel % self.groups != 0:
raise ValueError(
'The number of input channels must be evenly divisible by the number '
'of groups. Received groups={}, but the input has {} channels '
'(full input shape is {}).'.format(self.groups, input_channel,input_shape))
#我们拿图像常用的Con2D为例,kerner_size是输入指定的卷积核的大小,比如(2,2),
#input_channel // self.groups可以忽略groups,该维度是通道数,比如RGB图像是3通道,
#filters指卷积核的个数,该参数指定了输出的最后一维的维度。
#所以kernel_shape的结果就是(2,2,3,filters)
kernel_shape = self.kernel_size + (input_channel // self.groups, self.filters)
#下面是训练参数的声明
self.kernel = self.add_weight(
name='kernel',
shape=kernel_shape,
initializer=self.kernel_initializer,
regularizer=self.kernel_regularizer,
constraint=self.kernel_constraint,
trainable=True,
dtype=self.dtype)
if self.use_bias:
self.bias = self.add_weight(
name='bias',
shape=(self.filters,),
initializer=self.bias_initializer,
regularizer=self.bias_regularizer,
constraint=self.bias_constraint,
trainable=True,
dtype=self.dtype)
else:
self.bias = None
channel_axis = self._get_channel_axis()
self.input_spec = InputSpec(min_ndim=self.rank + 2,
axes={channel_axis: input_channel})
# Convert Keras formats to TF native formats.
if self.padding == 'causal':
tf_padding = 'VALID' # Causal padding handled in `call`.
elif isinstance(self.padding, six.string_types):
tf_padding = self.padding.upper()
else:
tf_padding = self.padding
#膨胀卷积用的
tf_dilations = list(self.dilation_rate)
tf_strides = list(self.strides)
tf_op_name = self.__class__.__name__
if tf_op_name == 'Conv1D':
tf_op_name = 'conv1d' # Backwards compat.
self._convolution_op = functools.partial(
#调用了卷积操作函数
nn_ops.convolution_v2,
strides=tf_strides,
padding=tf_padding,
dilations=tf_dilations,
data_format=self._tf_data_format,
name=tf_op_name)
self.built = True
def call(self, inputs):
if self._is_causal: # Apply causal padding to inputs for Conv1D.
inputs = array_ops.pad(inputs, self._compute_causal_padding(inputs))
#调用卷积操作
outputs = self._convolution_op(inputs, self.kernel)
#偏置
if self.use_bias:
output_rank = outputs.shape.rank
if self.rank == 1 and self._channels_first:
# nn.bias_add does not accept a 1D input tensor.
bias = array_ops.reshape(self.bias, (1, self.filters, 1))
outputs += bias
else:
# Handle multiple batch dimensions.
if output_rank is not None and output_rank > 2 + self.rank:
def _apply_fn(o):
return nn.bias_add(o, self.bias, data_format=self._tf_data_format)
outputs = nn_ops.squeeze_batch_dims(
outputs, _apply_fn, inner_rank=self.rank + 1)
else:
outputs = nn.bias_add(
outputs, self.bias, data_format=self._tf_data_format)
#激活
if self.activation is not None:
return self.activation(outputs)
return outputs
(╯﹏╰)b发现CNN的实现代码可能用的C实现的,没找到相关代码
参考
https://www.cnblogs.com/pinard/p/6483207.html