手撸神经网络系列之——卷积运算的正向传播(二)
本文仍然讲解卷积运算的正向传播方法,前一篇文章中我们用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循环的卷积方法即将正式展开,我将继续以尽可能简单的方式,努力介绍这种优雅高效的方法,文体两开花,弘扬深度学习精神,希望大家多多关注!