详解卷积层的前向传播与反向传播

未经允许请勿随意转载,如需使用文中图片,请留言或联系博主。

前向传播的底层实现

卷积操作是卷积神经网络中的基础操作,其基本原理非常简单,即是用固定大小的卷积核以固定大小的步长在输入图像上滑动。每滑动一次,就将卷积核与对应位置的特征图作内积运算(相乘再相加)。有时,为了维持输出特征图的大小不变,会在输入特征图的周围补0。这里,我给出输入特征图为三维的情况下的卷积操作示意:

上图中仅给出了卷积核个数为1的情况,因而输出的特征图的通道个数为1。当卷积核个数为k时,输出特征图的通道个数即为k。

为了使用程序实现上述的卷积运算,最简单的做法就是模仿卷积的运算过程,将卷积核依次滑过输入特征图的位置。但该实现方式的效率较低,需要多重循环。那么,如何对这一过程进行加速?

使用矩阵乘法实现卷积层运算

卷积层的基本运算是卷积核组和输入特征图的局部区域做内积,即把卷积核组和输入特征图的局部区域拉伸为向量,再做内积运算。而矩阵乘法也可以看做是两个向量做内积运算,那么,是否可以将卷积运算转换为矩阵乘法?

为了达到这一目的,需要依次进行如下的操作:

将输入特征图拉伸为列向量

从上面的示意图中可以看出,卷积核是不变的,每一次改变的只是输入特征图的局部区域。因而,可以提前将每一个局部区域拉伸为矩阵中的列向量(按照逐行、逐通道的方式);

将卷积核拉伸为行向量

同时,将每一个卷积核拉伸为矩阵中的行向量。这里,假设将输入特征图拉伸得到的特征图称为输入矩阵,将卷积核拉伸得到的特征图称为权重矩阵。这一操作的示意图如下:

上图以$4\times4$的输入和$2\times2$大小的卷积核、步长为2、不填充为例。

执行矩阵乘法

将卷积核和输入特征图都拉伸为矩阵之后,就可以使用矩阵运算来完成卷积操作。使用矩阵乘法得到行向量之后,再进行reshape操作即可得到最终的输出特征图。

深度学习框架中的实现

以caffe中的im2col_cpu函数为例,该函数的功能就是将输入特征图转换为输入矩阵。

在实际的卷积运算中,会加上padding、stride和dialation(空洞卷积)等操作,caffe的实现也兼容了这些操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
template <typename Dtype>
void im2col_cpu(const Dtype* data_im, const int channels,
const int height, const int width, const int kernel_h, const int kernel_w,
const int pad_h, const int pad_w,
const int stride_h, const int stride_w,
const int dilation_h, const int dilation_w,
Dtype* data_col) {
const int output_h = (height + 2 * pad_h -
(dilation_h * (kernel_h - 1) + 1)) / stride_h + 1;
const int output_w = (width + 2 * pad_w -
(dilation_w * (kernel_w - 1) + 1)) / stride_w + 1;
const int channel_size = height * width;
for (int channel = channels; channel--; data_im += channel_size) {
for (int kernel_row = 0; kernel_row < kernel_h; kernel_row++) {
for (int kernel_col = 0; kernel_col < kernel_w; kernel_col++) {
int input_row = -pad_h + kernel_row * dilation_h;
for (int output_rows = output_h; output_rows; output_rows--) {
if (!is_a_ge_zero_and_a_lt_b(input_row, height)) {
for (int output_cols = output_w; output_cols; output_cols--) {
*(data_col++) = 0;
}
} else {
int input_col = -pad_w + kernel_col * dilation_w;
for (int output_col = output_w; output_col; output_col--) {
if (is_a_ge_zero_and_a_lt_b(input_col, width)) {
*(data_col++) = data_im[input_row * width + input_col];
} else {
*(data_col++) = 0;
}
input_col += stride_w;
}
}
input_row += stride_h;
}
}
}
}
}

在上述代码中有几点需要注意的地方:

  • 在进行padding操作时,并不是真正地在输入特征图的四周填充0,而是按照计算得到的坐标进行判断,如果坐标不在[0, height]范围内,则向输入矩阵的对应位置填充0;

  • 在填充输入矩阵时,填充顺序是逐行填充,而不是将一个局部区域拉伸为列向量后再拉伸下一个局部区域。这一操作逻辑和输入特征图的内存存放顺序有关,输入特征图在内存中是按行存放的;

  • 上述代码中,计算输入特征图中对应坐标的代码比较巧妙:

    1
    2
    int input_row = -pad_h + kernel_row * dilation_h;
    int input_col = -pad_w + kernel_col * dilation_w;

    仅使用这两句代码就实现了padding和空洞卷积操作。

反向传播的底层实现

在深度神经网络中,反向传播实际上就是误差的传播。在使用损失函数计算得到损失之后,为了计算损失关于各层输出的误差,需要经过如下的三步(如下图所示):

  • 损失经过损失函数反向传播到输出层(最后一层),得到输出层的误差(未经过激活函数),对应图中绿色虚线;
  • 经过激活函数反向传播,得到关于当前层的纯输出的误差,对应途中蓝色虚线;
  • 经过当前层(例如卷积)反向传播,得到关于当前层的输入的误差,对应途中红色虚线。

以上三步迭代进行,就可以一直将误差反向传播到每一层。

那么,本节所要讨论的卷积层的反向传播就属于红色虚线所对应的部分。

二维输入的反向传播

为了得到卷积层的反向传播的计算方式,需要首先搞明白当前层的输入特征图中的每一个元素都与当前层的误差的那些部分有关。以下图为例:

上图的上半部分为卷积层的前向传播。在前向传播的过程中,输入特征图中三个位置的元素需要特别注意:

  • a:该元素仅与输出特征图中的一个元素有关,即d1。那么,在反向传播过程中,a也只会接受来自于d1的误差,即:

  • b:该元素与输出特征图中的两个位置的元素有关,即d1d2。那么,在反向传播过程中,b会同时接受来自于d1d2的误差,即:

  • e:该元素与输出特征图中的四个位置的元素有关,即d1d2d3d4。那么,在反向传播过程中,e会同时接受来自于这四个值的误差,即:

观察以上几个元素的误差的计算方式,我们会发现一个有趣的现象,即输入特征图中各个位置的元素的误差实际上等于上图中下半部分的卷积运算(最后一个周围补0的特征图中心的几个值为方向传播到当前层的误差)。该卷积运算是通过如下方式得到的:

  • 首先在反向传播到当前层的误差的周围补0;
  • 接着将卷积核旋转180度;
  • 将旋转后的卷积核与补0后的误差执行卷积运算,则可得到关于当前层的输入的误差。

三维输入的反向传播

了解了二维输入的反向传播,要得到三维输入的反向传播就相对简单一些。以下图为例:

对于输入特征图中的每一个通道,其误差会来自于输出特征图的误差的每一个通道。因为在前向传播过程中,输入特征图的每一个通道会被所有卷积核(上图中为2个)卷积后融合到输出特征图的每一个通道中。

那么,为了得到输入特征图每一个位置的误差,则只需要对每一个输入通道对应的输出特征图的所有通道的误差反向传播后相加即可。

误差关于权重的导数

为了简单起见,这里仅以输入为二维张量,卷积核个数为1的情况为例。对于输入为三维张量,卷积核个位大于1的情况,只需要求得各个通道的参数的导数,再在通道维度进行拼接即可。

在上图中,卷积的步长均为1。首先观察上图中的上半部分的前向传播过程,卷积核中的每一个权重都会对输出特征图的所有位置产生影响。因而,在反向传播过程中,每一个权重会接受来自于输出特征图的所有误差。对于权重w1而言,其导数为输入特征图左上角的元素(即红色框)与输出特征图误差的卷积。

这样一来,为了得到卷积核中所有位置的导数,只需要将输出特征图的误差作为卷积核,以步长1和输入特征图进行卷积即可。如上图中下半部分所示。

那么,如果前向传播过程中的卷积步长不为1呢?只需要在将输出特征图的误差和输入特征图进行卷积时,在输出特征图的误差的相邻两个元素之间填充0即可(类似于空洞卷积)。

误差关于偏置的导数

误差关于偏置的导数就很简单了。对于每一个卷积核,设定一个偏置。一个偏置对应输出特征图中的一个通道,该输出特征图的通道中的每一个元素都与该偏置相关。如下图所示:

因而,误差关于每一个偏置的导数就等于其对应的输出特征图的通道的误差之和。

参考