卷积神经网络(CNN)| 笔记 | 1

卷积层

1
2
3
4
5
6
7
8
9
10
11
输入图片: 5*5*3

卷积核: 3*3*3
- 卷积核有2个: Filter W0, Filter W1

偏置: 1*1*1
- 偏置有2个: Bias b0, Bias b1

卷积结果(Output Volumn): 3*3*2

步长(stride): 2

补充:

输入: \(7*7*3\)\(7*7\) 是因为 pad = 1 (在图片边界行和列都补零,补零的行和的数目是1)

补零作用:提取图片边界的特征。

\(7*7\) 指 图片高h * 宽w

而之所以 \(*3\) 是因为,对于彩色图片,一般采用 RGB 3原色表示,也称 3通道。

Q: 卷积核深度为何设为 3 ?

A: 因为输入 是 3通道,所以卷积核深度 必须与 输入的深度相同。

至于 卷积核宽w,高h 则是可变化的,但是宽高必须相等。

=> w0[:,:,0] * x[:,:,0]蓝色区域矩阵(R通道) + w0[:,:,1] * x[:,:,1]蓝色区域矩阵(G通道)+ w0[:,:,2] * x[:,:,2]蓝色区域矩阵(B通道) + b0(千万不能丢,因为 y = w * x + b)

第一项 => 0 * 1 + 0 * 1 + 0 * 1 + 0 * (-1) + 1 * (-1) + 1 * 0 + 0 * (-1) + 1 * 1 + 1 * 0 = 0

第二项 => 0 * (-1) + 0 * (-1) + 0 * 1 + 0 * (-1) + 0 * 1 + 1 * 0 + 0 * (-1) + 2 * 1 + 2 * 0 = 2

第三项 => 0 * 1 + 0 * 0 + 0 * (-1) + 0 * 0 + 2 * 0 + 2 * 0 + 0 * 1 + 0 * (-1) + 0 * (-1) = 0

卷积核输出o[0,0,0] = > 第一项 + 第二项 + 第三项 + b0 = 0 + 2 + 0 + 1 = 3

全连接层存在的问题

之前介绍的全连接的神经网络中使用了全连接层(Affine层)。

在全连接 层中,相邻层的神经元全部连接在一起,输出的数量可以任意决定。

全连接层存在什么问题呢?

那就是数据的形状被“忽视”了。

比如,输 入数据是图像时,图像通常是高、长、通道方向上的3维形状。

但是,向全 连接层输入时,需要将3维数据拉平为1维数据。

实际上,前面提到的使用 了MNIST数据集的例子中,输入图像就是1通道、高28像素、长28像素 的(1, 28, 28)形状,但却被排成1列,以784个数据的形式输入到最开始的Affine层。

而卷积层可以保持形状不变。

当输入数据是图像时,卷积层会以3维 数据的形式接收输入数据,并同样以3维数据的形式输出至下一层。因此,在CNN中,可以(有可能)正确理解图像等具有形状的数据。

另外,CNN中,

有时将卷积层的输入输出数据称为特征图(feature map)。

其中,卷积层的输入数据称为输入特征图(input feature map),输出 数据称为输出特征图(output feature map)。

本书中将“输入输出数据”和“特征图”作为含义相同的词使用。

卷积运算

卷积层进行的处理就是卷积运算。

卷积运算相当于图像处理中的“滤波 器运算”。在介绍卷积运算时,我们来看一个具体的例子(图7-3)。

如图7-3所示,卷积运算对输入数据应用滤波器。

在这个例子中,输入 数据是有高长方向的形状的数据,滤波器也一样,有高长方向上的维度。

假 设用(height, width)表示数据和滤波器的形状,则在本例中,输入大小是 (4, 4),滤波器大小是(3, 3),输出大小是(2, 2)。另外,有的文献中也会用“核”这个词来表示这里所说的“滤波器”。

​ 对于输入数据,卷积运算以一定间隔滑动滤波器的窗口并应用。这里所 说的窗口是指图7-4中灰色的3 × 3的部分。如图7-4所示,将各个位置上滤波器的元素和输入的对应元素相乘,然后再求和(有时将这个计算称为 乘积累加运算 )。然后,将这个结果保存到输出的对应位置。将这个过程在所有位置都进行一遍,就可以得到卷积运算的输出。

​ 在全连接的神经网络中,除了权重参数,还存在偏置。CNN中,滤波器的参数就对应之前的权重。并且,CNN中也存在偏置。图7-3的卷积运算 的例子一直展示到了应用滤波器的阶段。包含偏置的卷积运算的处理流如图7-5所示。

如图7-5所示,向应用了滤波器的数据加上了偏置。

偏置通常只有1个 (1 × 1)(本例中,相对于应用了滤波器的4个数据,偏置只有1个),这个值会被加到应用了滤波器的所有元素上。

填充

在进行卷积层的处理之前,

有时要向输入数据的周围填入固定的数据(比 如0等),这称为 填充(padding)

是卷积运算中经常会用到的处理。

比如, 在图7-6的例子中,

对大小为(4, 4)的输入数据应用了幅度为1的填充。“幅度为1的填充”是指用幅度为1像素的0填充周围。

如图7-6所示,

通过填充,大小为(4, 4)的输入数据变成了(6, 6)的形状。

然后,应用大小为(3, 3)的滤波器,生成了大小为(4, 4)的输出数据。

这个例 子中将填充设成了1,不过填充的值也可以设置成2、3等任意的整数。

在图7-5 的例子中,如果将填充设为2,则输入数据的大小变为(8, 8);如果将填充设为3,则大小变为(10, 10)。

因此最后,输出 (4, 4)

注意

使用填充主要是为了调整输出的大小。

比如,对大小为(4, 4)的输入 数据应用(3, 3)的滤波器时,输出大小变为(2, 2),相当于输出大小 比输入大小缩小了2个元素。

这在反复进行多次卷积运算的深度网络中会成为问题。

为什么呢?因为如果每次进行卷积运算都会缩小 空间,那么在某个时刻输出大小就有可能变为1,导致无法再应用 卷积运算。

为了避免出现这样的情况,就要使用填充。

在刚才的例 子中,将填充的幅度设为1,那么相对于输入大小(4, 4),输出大小 也保持为原来的(4, 4)。

因此,卷积运算就可以在保持空间大小不变的情况下将数据传给下一层。

步幅

应用滤波器的位置间隔称为 步幅(stride)

之前的例子中步幅都是1,如 果将步幅设为2,则如图7-7所示,应用滤波器的窗口的间隔变为2个元素。

在图7-7的例子中,

对输入大小为(7, 7)的数据,以步幅2应用了滤波器。

通过将步幅设为2,输出大小变为(3, 3)。像这样,步幅可以指定应用滤波器的间隔。

综上,增大步幅后,输出大小会变小。而增大填充后,输出大小会变大。

如果将这样的关系写成算式,会如何呢?

接下来,我们看一下对于填充和步幅,如何计算输出大小。

这里,假设输入大小为 \((H,W)\),滤波器大小为 \((FH,FW)\),输出大小为 \((OH,OW)\),填充为P,步幅为S。

此时,输出大小可通过式(7.1)进行计算。

现在,我们使用这个算式,试着做几个计算。

例1:图7-6的例子

输入大小:(4, 4);填充:1;步幅:1;滤波器大小:(3, 3)

例2:图7-7的例子

输入大小:(7, 7);填充:0;步幅:2;滤波器大小:(3, 3)

例3

输入大小:(28, 31);填充:2;步幅:3;滤波器大小:(5, 5)

3维数据的卷积运算

之前的卷积运算的例子都是以有高、长方向的2维形状为对象的。

但是, 图像是3维数据,除了高、长方向之外,还需要处理通道方向。

这里,我们按照与之前相同的顺序,看一下对加上了通道方向的3维数据进行卷积运算的例子。

图7-8是卷积运算的例子,图7-9是计算顺序。

这里以3通道的数据为例, 展示了卷积运算的结果。

和2维数据时(图7-3的例子)相比,可以发现纵深 方向(通道方向)上特征图增加了。

通道方向上有多个特征图时,会按通道进行输入数据和滤波器的卷积运算,并将结果相加,从而得到输出。

需要注意的是,在3维数据的卷积运算中,输入数据和滤波器的通道数 要设为相同的值。

在这个例子中,输入数据和滤波器的通道数一致,均为3。

滤波器大小可以设定为任意值(不过,每个通道的滤波器大小要全部相同)。

这个例子中滤波器大小为(3, 3),但也可以设定为(2, 2)、(1, 1)、(5, 5)等任 意值。

再强调一下,通道数只能设定为和输入数据的通道数相同的值(本例中为3)。

结合方块思考

将数据和滤波器结合长方体的方块来考虑,3维数据的卷积运算会很容易理解。

方块是如图7-10所示的3维长方体。把3维数据表示为多维数组 时,书写顺序为(channel, height, width)。

比如,通道数为C、高度为H、 长度为W的数据的形状可以写成(C, H,W)。

滤波器也一样,要按(channel, height, width)的顺序书写。

比如,通道数为C、滤波器高度为FH(FilterHeight)、长度为FW(Filter Width)时,可以写成(C, FH, FW)。

在这个例子中,数据输出是1张特征图。

所谓1张特征图,换句话说, 就是通道数为1的特征图。

那么,如果要在通道方向上也拥有多个卷积运算的输出,该怎么做呢?

为此,就需要用到多个滤波器(权重)。用图表示的话, 如图7-11所示。

图7-11中,通过应用FN个滤波器,输出特征图也生成了FN个。

如果 将这FN个特征图汇集在一起,就得到了形状为(FN, OH,OW)的方块。

将这个方块传给下一层,就是CNN的处理流。

如图7-11所示,关于卷积运算的滤波器,也必须考虑滤波器的数 量。

因此,作为4维数据,滤波器的权重数据要按(output_channel, input_ channel, height, width)的顺序书写。

比如,通道数为3、大小为5 × 5的滤波器有20个时,可以写成(20, 3, 5, 5)。

卷积运算中(和全连接层一样)存在偏置。

在图7-11的例子中,如果进 一步追加偏置的加法运算处理,则结果如下面的图7-12所示。

图7-12中,每个通道只有一个偏置。

这里,偏置的形状是(FN, 1, 1), 滤波器的输出结果的形状是(FN, OH,OW)。

这两个方块相加时,要对滤波 器的输出结果(FN, OH,OW)按通道加上相同的偏置值。

另外,不同形状的方块相加时,可以基于NumPy的广播功能轻松实现(1.5.5节)。

批处理

神经网络的处理中进行了将输入数据打包的批处理。

之前的全连接神经 网络的实现也对应了批处理,通过批处理,能够实现处理的高效化和学习时对mini-batch的对应。

我们希望卷积运算也同样对应批处理。

为此,需要将在各层间传递的数 据保存为4维数据。

具体地讲,就是按(batch_num, channel, height, width) 的顺序保存数据。

比如,将图7-12中的处理改成对N个数据进行批处理时,数据的形状如图7-13所示。

图7-13的批处理版的数据流中,在各个数据的开头添加了批用的维度。

像这样,数据作为4维的形状在各层间传递。

这里需要注意的是,网络间传递的是4维数据,对这N个数据进行了卷积运算。

也就是说,批处理将N次的处理汇总成了1次进行。

池化层

池化是缩小高、长方向上的空间的运算。

比如,如图7-14所示,进行将 2 × 2的区域集约成1个元素的处理,缩小空间大小。

图7-14的例子是按步幅2进行2 × 2的Max池化时的处理顺序。

“Max 池化”是获取最大值的运算,“2 × 2”表示目标区域的大小。

如图所示,从 2 × 2的区域中取出最大的元素

此外,这个例子中将步幅设为了2,所以 2 × 2的窗口的移动间隔为2个元素。

另外,一般来说,池化的窗口大小会 和步幅设定成相同的值。

比如,3 × 3的窗口的步幅会设为3,4 × 4的窗口的步幅会设为4等。

注意:

除了Max池化之外,还有Average池化等。

相对于Max池化是从 目标区域中取出最大值,Average池化则是计算目标区域的平均值。

在图像识别领域,主要使用Max池化。

因此,本书中说到“池化层”时,指的是Max池化。

池化层的特征

池化层有以下特征。

  1. 没有要学习的参数

    池化层和卷积层不同,没有要学习的参数。

    池化只是从目标区域中取最大值(或者平均值),所以不存在要学习的参数。

  2. 通道数不发生变化

    经过池化运算,输入数据和输出数据的通道数不会发生变化。

    如图7-15 所示,计算是按通道独立进行的。

  3. 对微小的位置变化具有鲁棒性(健壮)

    输入数据发生微小偏差时,池化仍会返回相同的结果。

    因此,池化对 输入数据的微小偏差具有鲁棒性。

    比如,3 × 3的池化的情况下,如图 7-16所示,池化会吸收输入数据的偏差(根据数据的不同,结果有可能不一致)。

卷积层和池化层的实现

前面我们详细介绍了卷积层和池化层,本节我们就用Python来实现这 两个层。

和第5章一样,也给进行实现的类赋予forward和backward方法,并使其可以作为模块使用。

大家可能会感觉卷积层和池化层的实现很复杂,但实际上,通过使用某 种技巧,就可以很轻松地实现。

本节将介绍这种技巧,将问题简化,然后再进行卷积层的实现。

4维数组

如前所述,CNN中各层间传递的数据是4维数据。

所谓4维数据,比如 数据的形状是__(10, 1, 28, 28)__,则它对应__10个高为28、长为28、通道为1__的数据。

用Python来实现的话,如下所示。

1
2
3
4
5
6
7
import numpy as np

x = np.random.rand(10, 1, 28, 28) # 随机生成数据

x.shape

# (10, 1, 28, 28)

如果要访问第1个数据的第1个通道的空间数据,可以写成下面这样。

1
x[0, 0] # 或者x[0][0]
1
2
3
x[0, 0].shape

# (28, 28)

像这样,CNN中处理的是4维数据,因此卷积运算的实现看上去会很复杂,但是通过使用下面要介绍的im2col这个技巧,问题就会变得很简单。

基于im2col的展开

如果老老实实地实现卷积运算,估计要重复好几层的for语句。

这样的实现有点麻烦,而且,NumPy中存在使用for语句后处理变慢的缺点(NumPy 中,访问元素时最好不要用for语句)。

这里,我们不使用for语句,而是使用im2col这个便利的函数进行简单的实现。

im2col是一个函数,将输入数据展开以适合滤波器(权重)。

如图7-17所示, 对3维的输入数据应用im2col后,数据转换为2维矩阵(正确地讲,是把包含批数量的4维数据转换成了2维数据)。

im2col会把输入数据展开以适合滤波器(权重)。

具体地说,如图7-18所示, 对于输入数据,将应用滤波器的区域(3维方块)横向展开为1列。

im2col会在所有应用滤波器的地方进行这个展开处理。

在图7-18中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。

而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。

在 滤波器的应用区域重叠的情况下,使用im2col展开后,展开后的元素个数会 多于原方块的元素个数。

因此,使用im2col的实现存在比普通的实现消耗更多内存的缺点。

但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。

比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。

因此,通过归结到矩阵计算上,可以有效地利用线性代数库。

注:

im2col这个名称是“image to column”的缩写,翻译过来就是“从 图像到矩阵”的意思。

Caffe、Chainer等深度学习框架中有名为im2col的函数,并且在卷积层的实现中,都使用了im2col。

使用im2col展开输入数据后,之后就只需将卷积层的滤波器(权重)纵 向展开为1列,并计算2个矩阵的乘积即可(参照图7-19)。这和全连接层的Affine层进行的处理基本相同。

如图7-19所示,基于im2col方式的输出结果是2维矩阵。

因为CNN中 数据会保存为4维数组,所以要将2维输出数据转换为合适的形状。

以上就是卷积层的实现流程。

卷积层的实现

im2col会考虑滤波器大小、步幅、填充,将输入数据展开为2维数组。现在, 我们来实际使用一下这个im2col。

1
2
3
4
5
import sys, os 
sys.path.append(os.pardir)
from common.util import im2col

print('完成')
1
2
3
4
x1 = np.random.rand(1, 3, 7, 7) 
col1 = im2col(x1, 5, 5, stride=1, pad=0)

print(col1.shape) # (9, 75)
1
2
3
4
x2 = np.random.rand(10, 3, 7, 7) # 10个数据 
col2 = im2col(x2, 5, 5, stride=1, pad=0)

print(col2.shape) # (90, 75)

这里举了两个例子。

第一个是批大小为1、通道为3的7 × 7的数据,第二个的批大小为10,数据形状和第一个相同。

分别对其应用im2col函数,在 这两种情形下,第2维的元素个数均为75。

这是滤波器(通道为3、大小为 5 × 5)的元素个数的总和。

批大小为1时,im2col的结果是(9,75)。

而第2个例子中批大小为10,所以保存了10倍的数据,即(90,75)。

现在使用im2col来实现卷积层。这里我们将卷积层实现为名为Convolution 的类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Convolution:

def __init__(self, w, b, stride = 1, pad = 0):
self.w = w
self.b = b
self.stride = stride
self.pad = pad

def forward(self, x):
FN, C, FH, FW = self.w.shape
N, C, H, W = x.shape
out_h = int(1 + (H + 2*self.pad - FH) / self.stride)
out_w = int(1 + (W + 2*self.pad - FW) / self.stride)

col = im2col(x, FH, FW, self.stride, self.pad)
# 滤波器的展开
col_W = self.w.reshape(FN, -1).T
out = np.dot(col, col_W) + self.b

out = out.reshape(N, out_h, out_w, -1).transpose(0, 3, 1, 2)

return out

卷积层的初始化方法将滤波器(权重)、偏置、步幅、填充作为参数接收。
滤波器是(FN, C, FH, FW)的4维形状。
另外,FN、C、FH、FW分别是Filter Number(滤波器数量)、Channel、Filter Height、Filter Width的缩写。

这里用粗体字表示Convolution层的实现中的重要部分。

在这些粗体字 部分,用im2col展开输入数据,并用reshape将滤波器展开为2维数组。

然后,计算展开后的矩阵的乘积。

展开滤波器的部分(代码段中的粗体字)如图7-19所示,将各个滤波器 的方块纵向展开为1列。

这里通过reshape(FN,-1)将参数指定为-1,这是 reshape的一个便利的功能。

通过在reshape时指定为-1,reshape函数会自 动计算-1维度上的元素个数,以使多维数组的元素个数前后一致。

比如, (10, 3, 5, 5)形状的数组的元素个数共有750个,指定reshape(10,-1)后,就会转换成(10, 75)形状的数组。

forward的实现中,最后会将输出大小转换为合适的形状。

转换时使用了 NumPy的transpose函数。

transpose会更改多维数组的轴的顺序。

如图7-20所示,通过指定从0开始的索引(编号)序列,就可以更改轴的顺序。

以上就是卷积层的forward处理的实现。

通过使用im2col进行展开,基 本上可以像实现全连接层的Affine层一样来实现(5.6节)。

接下来是卷积层 的反向传播的实现,因为和Affine层的实现有很多共通的地方,所以就不再 介绍了。

但有一点需要注意,在进行卷积层的反向传播时,必须进行im2col的逆处理。

这可以使用本书提供的col2im函数(col2im的实现在common/util.py中)来进行。

除了使用col2im这一点,卷积层的反向传播和Affine层的实现方式都一样。

卷积层的反向传播的实现在common/layer.py中,有兴趣的读者可以参考。

池化层的实现

池化层的实现和卷积层相同,都使用im2col展开输入数据。

不过,池化 的情况下,在通道方向上是独立的,这一点和卷积层不同。

具体地讲,如图7-21所示,池化的应用区域按通道单独展开。

像这样展开之后,只需对展开的矩阵求__各行的最大值__,并转换为合适的 形状即可(图7-22)。

上面就是池化层的forward处理的实现流程。下面来看一下Python的实现示例。

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
class Pooling:

def __init__(self, pool_h, pool_w, stride = 1, pad = 0):
self.pool_h = pool_h
self.pool_w = pool_w
self.stride = stride
self.pad = pad


def forward(self, x):
N, C, H, W = x.shape
out_h = int(1 + (H - self.pool_h) / self.stride)
out_w = int(1 + (W - self.pool_w) / self.stride)

# 展开(1)
col = im2col(x, self.pool_h, self.pool_w, self.stride, self.pad)
col = col.reshape(-1, self.pool_h*self.pool_w)

# 最大值(2)
out = np.max(col, axis = 1)

# 转换(3)
out = out.reshape(N, out_h, out_w, C).transpose(0, 3, 1, 2)

return out

如图7-22所示,池化层的实现按下面3个阶段进行。

  1. 展开输入数据。

  2. 求各行的最大值。

  3. 转换为合适的输出大小。

    各阶段的实现都很简单,只有一两行代码。

补充:

最大值的计算可以使用NumPy的np.max方法。

np.max可以指定 axis参数,并在这个参数指定的各个轴方向上求最大值。

比如,如 果写成np.max(x, axis=1),就可以在输入x的第1维的各个轴方向上求最大值。

补充

全连接:

相邻层的所有神经元之间都有连接,这称为全连接(fully-connected)

参考

感谢帮助!