本文仍然讲解卷积运算的正向传播方法,前一篇文章中我们用for循环将卷积运算实现了一下,重点关注的是卷积的运算过程,而并未对其进行优化,本文将引入一种相对比较容易想到的卷积优化方法——img2col。

本系列全部代码见下面仓库:

如有算法或实现方式上的问题,请各位大佬轻喷+指正!

img2col方法介绍

“img2col太简单了,我早就会了!”→直接进入卷积正向传播的第三篇文章

img2col的目的是将繁杂的卷积过程转化为一次矩阵乘法运算,从而大大降低运算量,为了减少语言上的琐碎描述,下面直接以图片来展示过程,先祭出前面文章里卷积过程的动图:


注意到,卷积核每次在图像上移动时,都会与对应区域做一个内积运算(元素间相乘再求和),这一过程与矩阵乘列向量的过程十分类似,因此就有了以下操作:


上面演示的过程中,卷积核按顺序遍历图像一遍,每一次覆盖的数据区域展开为一行,并将所有的行拼接为一个矩阵,这就是img2col的核心步骤。

在得到右侧绿色的矩阵后,我们将卷积核展开为一条列向量,并与绿色矩阵进行右乘:


最后将结果reshape回$3\times 3$,即可得到卷积结果:


以上,即是使用img2col进行卷积运算的思路。

实际操作时,我们会有多个卷积核,同时我们还注意到,每个卷积核对同一幅图的卷积过程是完全独立的,因此我们可以将多个卷积核展开的列进行“横向”拼接,同样形成一个矩阵,这样操作,极大地提高了卷积运算的并行化效果。


代码

首先是img2col函数:

def img2col(data, H_out, W_out, kh, kw, stride):
    """
    :param data: 输入数据
    :param H_out: 卷积结果的H
    :param W_out: 卷积结果的W
    :param kh: 卷积核的h
    :param kw: 卷积核的w
    :param stride: 卷积步长
    :return: img2col操作得到的矩阵
    """
    B, C, H, W = data.shape
    out_mat = np.zeros((B, H_out * W_out, C * kh * kw))
    for b in range(B):
        for h in range(H_out):
            for w in range(W_out):
                out_mat[b, h * W_out + w] = data[b, :, h * stride: h * stride + kh, w * stride: w * stride + kw].flatten()
    return out_mat

卷积过程:

B = 2  # batchsize
C_in = 3  # channels_in
C_out = 5  # channels_out
H = 4  # Height of image
W = 5  # Width of image
kh = kw = 2  # kernel size
stride = 1  # stride for convolution
data = np.random.rand(B, C_in, H, W)  # 随机生成被卷积的数据
kernels = np.random.rand(C_out, C_in, kh, kw)  # 随机生成C_out个卷积核,写在一个张量里
# 计算卷积结果的长宽
H_out = (H - kh) // stride + 1
W_out = (W - kw) // stride + 1

flatten_data = img2col(data, H_out, W_out, kh, kw, stride)
flatten_kernels = np.reshape(kernels, (C_out, -1)).T
result = flatten_data.dot(flatten_kernels).swapaxes(-1, -2).reshape(B, C_out, H_out, W_out)

该操作好处是在for循环中避免了矩阵运算,并且减少了一重for。代码具体的细节我就懒得讲了,各位可以自己琢磨一下,实现一下img2col过程。

不过这个方法仍然不够优雅,后续的文章里,一种不需要(在Python中)写for循环的卷积方法即将正式展开,我将继续以尽可能简单的方式,努力介绍这种优雅高效的方法,文体两开花,弘扬深度学习精神,希望大家多多关注!