1. 输入 4 维数据

CNN 中各层间传递的数据是 4 维数据。所谓 4 维数据,比如数据的形状是(10, 1, 28, 28),则它对应 10 个高为 28、长为 28、通道为 1 的数据。用 Python 实现如下:

In [2]: a = np.random.rand(3, 1, 4, 4)

In [4]: a.shape
Out[4]: (3, 1, 4, 4)

In [5]:

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

In [5]: a[0][0]

或者

In [6]: a[0, 0]

2. 基于im2col 的展开

im2col 是一个函数,将输入数据展开以适合滤波器(权重)。如图7-17 所示,对 3 维的输入数据应用 im2col 后,数据转换为 2 维矩阵(正确地讲,是把包含批数量的 4 维数据转换成了2 维数据)。

python 3维卷积 时间特征 python 二维卷积_ide


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

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

python 3维卷积 时间特征 python 二维卷积_卷积_02


在图7-18 中,为了便于观察,将步幅设置得很大,以使滤波器的应用区域不重叠。而在实际的卷积运算中,滤波器的应用区域几乎都是重叠的。

在滤波器的应用区域重叠的情况下,使用 im2col 展开后,展开后的元素个数会多于原方块的元素个数。因此,使用 im2col 的实现存在比普通的实现消耗更多内存的缺点。

但是,汇总成一个大的矩阵进行计算,对计算机的计算颇有益处。比如,在矩阵计算的库(线性代数库)等中,矩阵计算的实现已被高度最优化,可以高速地进行大矩阵的乘法运算。因此,通过归结到矩阵计算
上,可以有效地利用线性代数库。

im2col 这个名称是 image to column 的缩写,翻译过来就是“从图像到矩阵”的意思。CaffeChainer 等深度学习框架中有名为 im2col 的函数,并且在卷积层的实现中,都使用了 im2col

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

如图7-19 所示,基于 im2col 方式的输出结果是 2 维矩阵。因为 CNN 中数据会保存为 4 维数组,所以要将 2 维输出数据转换为合适的形状。以上就是卷积层的实现流程。

python 3维卷积 时间特征 python 二维卷积_卷积_03


im2col 函数代码实现如下:

def im2col(input_data, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    input_data : 由(数据量, 通道, 高, 长)的4维数组构成的输入数据
    filter_h : 滤波器的高
    filter_w : 滤波器的宽
    stride : 步幅
    pad : 填充

    Returns
    -------
    col : 2维数组
    """
    N, C, H, W = input_data.shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1

    img = np.pad(input_data, [(0,0), (0,0), (pad, pad), (pad, pad)], 'constant')
    col = np.zeros((N, C, filter_h, filter_w, out_h, out_w))

    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            col[:, :, y, x, :, :] = img[:, :, y:y_max:stride, x:x_max:stride]

    col = col.transpose(0, 4, 5, 1, 2, 3).reshape(N*out_h*out_w, -1)
    return col

应用示例:

In [8]: x1 = np.random.rand(1, 3, 7, 7)

In [9]: col1 = im2col(x1, 5, 5, stride=1, pad=0)

In [10]: col1.shape
Out[10]: (9, 75)

In [14]: x2 = np.random.rand(10, 3, 7, 7)

In [15]: col2 = im2col(x2, 5, 5, stride=1, pad=0)

In [16]: col2.shape
Out[16]: (90, 75)

In [17]:

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

分别对其应用 im2col 函数,在这两种情形下,第 2 维的元素个数均为 75。这是滤波器(通道为 3、大小为
5 × 5)的元素个数的总和。批大小为1 时,im2col 的结果是(9, 75)。而第 2个例子中批大小为10,所以保存了10 倍的数据,即(90, 75)。

3. 卷积层实现

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

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

其中:

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

是用 im2col 展开输入数据,并用 reshape 将滤波器展开为 2 维数组。然后,计算展开后的矩阵的乘积。

将各个滤波器的方块纵向展开为 1 列。这里通过 reshape(FN,-1) 将参数指定为 -1,这是 reshape 的一个便利的功能。通过在 reshape 时指定为 -1,reshape 函数会自动计算 -1 维度上的元素个数,以使多维数组的元素个数前后一致。比如,(10, 3, 5, 5) 形状的数组的元素个数共有 750 个,指定 reshape(10,-1) 后,就会转换成(10, 75) 形状的数组。

forward 的实现中,最后会将输出大小转换为合适的形状。转换时使用了 NumPytranspose 函数。transpose 会更改多维数组的轴的顺序。如图 7-20 所示,通过指定从 0 开始的索引(编号)序列,就可以更改轴的顺序。

python 3维卷积 时间特征 python 二维卷积_数据_04


以上就是卷积层的 forward 处理的实现。通过使用 im2col 进行展开,接下来是卷积层的反向传播的实现,因为和 Affine 层的实现有很多共通的地方,所以就不再介绍了。但有一点需要注意,在进行卷积层的反向传播时,必须进行 im2col 的逆处理。col2im 代码如下:

def col2im(col, input_shape, filter_h, filter_w, stride=1, pad=0):
    """

    Parameters
    ----------
    col :
    input_shape : 输入数据的形状(例:(10, 1, 28, 28))
    filter_h :
    filter_w
    stride
    pad

    Returns
    -------

    """
    N, C, H, W = input_shape
    out_h = (H + 2*pad - filter_h)//stride + 1
    out_w = (W + 2*pad - filter_w)//stride + 1
    col = col.reshape(N, out_h, out_w, C, filter_h, filter_w).transpose(0, 3, 4, 5, 1, 2)

    img = np.zeros((N, C, H + 2*pad + stride - 1, W + 2*pad + stride - 1))
    for y in range(filter_h):
        y_max = y + stride*out_h
        for x in range(filter_w):
            x_max = x + stride*out_w
            img[:, :, y:y_max:stride, x:x_max:stride] += col[:, :, y, x, :, :]

    return img[:, :, pad:H + pad, pad:W + pad]

除了使用 col2im 这一点,卷积层的反向传播和 Affine 层的实现方式都一样。卷积层反向传播代码如下所示:

class Convolution:
    def __init__(self, W, b, stride=1, pad=0):
        self.W = W
        self.b = b
        self.stride = stride
        self.pad = pad
        
        # 中间数据(backward时使用)
        self.x = None   
        self.col = None
        self.col_W = None
        
        # 权重和偏置参数的梯度
        self.dW = None
        self.db = None

    def forward(self, x):
        FN, C, FH, FW = self.W.shape
        N, C, H, W = x.shape
        out_h = 1 + int((H + 2*self.pad - FH) / self.stride)
        out_w = 1 + int((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)

        self.x = x
        self.col = col
        self.col_W = col_W

        return out

    def backward(self, dout):
        FN, C, FH, FW = self.W.shape
        dout = dout.transpose(0,2,3,1).reshape(-1, FN)

        self.db = np.sum(dout, axis=0)
        self.dW = np.dot(self.col.T, dout)
        self.dW = self.dW.transpose(1, 0).reshape(FN, C, FH, FW)

        dcol = np.dot(dout, self.col_W.T)
        dx = col2im(dcol, self.x.shape, FH, FW, self.stride, self.pad)

        return dx