卷积详解之im2col算法
这篇文章是需要和卷积的实现原理一起来看的,两篇文章一个比较宏观,一个比较微观,在卷积的实现原理中我多次提到了im2col算法,这个算法顾名思义,就是把图像转化为列向量,但是这个转换不是整张图的转换,事实上转换出来的图像还是二维的,只是把卷积在特征图的感受野区域给转换成一维的向量。
我们熟悉卷积都知道,感受野区域是个二维的区域,这二维的区域从存储上不是连续的,这样就不便于计算。im2col算法完成的就是通过矩阵乘法来完成卷积核和感受野的对应相乘,这样计算起来就方便多了。
我们可以看到上图中的滑动窗口原理。卷积核在特征图上滑过的一个个窗口,我们称之为感受野,卷积核与感受野发生的运算是对应相乘再相加。比如我们第一次得到的结果就是
,这个公式我们可以将它想象成
。可以看到卷积核一共滑过了9个感受野,每个感受野都是和这个卷积核做运算。如果我们把卷积核一行一行的拼成矩阵,那么乘卷积核的结果就是卷积的结果了。结果虽然对了,不过这里把二维的卷积结果变成了一维的向量,最后还需要再转换回去。
介绍了初步的概念,我们还应当注意这个时候我们把特征图的矩阵是放在右元的位置。如果这个时候还有输入通道数
,那么就依次排开,对于同一个感受野,所有的通道都放在一行,左元共同的维度展开为一行,右元共同的维度展开为一列。如果卷积核还有O个输出通道,则应该排成一行。
其中特征图的上标代表输入的第i个通道,一共C个。下标代表第i个输出,一共
个。
卷积核的上标代表输出的第i个通道,一共O个。通过这张图大家可以轻易的明白,如果特征图作为矩阵乘法的左元,则
一同展开为一列,矩阵的行数就是感受野的个数,也是输出的特征图的cell个数。
此时的卷积核不同输入通道的也需要转到同一列上去。
除了这种情况,还有特征图作为矩阵右元的情况,我们依然是把
这个维度展开,作为右元,自然这里就要转为一列,而把输出的数目作为一行。卷积核就能把不同的输入通道展开为一行。原理都是一样的。最后展开的特征图维度也就是
的。
然后就是最后一点问题,访问局部性的问题,C语言系的语言都是对数组按照行优先存储,同一行的数据有局部性,如果将特征图的感受野展开为行的时候能够更好的利用局部性。但是这个问题不是主要问题,因为按列展开的时候,也是可以把数据一行一行的写进转换后的矩阵的,这个就需要更加细致的分析,这里就不赘述了。然后就是一般对矩阵进行转置也是需要开销的,所以避免尽量使用转置,这个需要通过合理的设置维度顺序。
网上很多解释也是让人看了迷之疑惑,明明是展开为列的框架非要给你讲解成展开为行,这个样子,自己实现的话就非常容易踩坑了。下面贴出我自己用numpy实现的代码,首先说一句,我不是从C++底层实现的,如果需要自己看实现,推荐看一下caffe的源码,网上C++实现讲解的比较多,但是还是要注意区分,不要踩坑。代码很简单,对于文章有疑问的同学欢迎讨论。
我的两种实现一种是基于NCHW通道的,卷积核都是和图像通道保持一致是
的,NCHW通道代码:
def im2col(img, ksize, stride,padding='same'):
'''
:param img: 4D array N,FH,FW,C_{in}
:param ksize: tuple (kh,kw)
:param stride:
:param padding:
:return:
'''
kh,kw=ksize
if padding=='same':
p1=kh//2
p2=kw//2
img=np.pad(img,((0,0),(0,0),(p1,p1),(p2,p2),),'constant')
N,C,H,W=img.shape
out_h=(H-kh)//stride+1
out_w=(W-kw)//stride+1
outsize=out_w*out_h
col=np.empty((N,C,kw*kh,outsize,))
for y in range(out_h):
y_start= y * stride
y_end= y_start + kh
for x in range(out_w):
x_start= x * stride
x_end= x_start + kw
col[:,:,0:,y*out_w+x]= img[:, :, y_start:y_end, x_start:x_end].reshape(N, C, kh * kw)
return col.reshape(N,-1,outsize)
def conv(X,W,stride=1,padding='same'):
'''
:param X: 4D array N,C_{in},FH,FW
:param W: 4D array C_{out},C_{in},kh,kw
:param stride:
:param padding:
:return: 4D array N,F,H,C_{out}
'''
KN,c,kh,kw=W.shape
N,C,FH,FW=X.shape
assert(c==C),"The feature map channel must equal the filter channel."
col=im2col(X,(kh,kw),stride,padding)
print(col)
z=np.dot(W.reshape(KN,-1),col).transpose((1,0,2))
out_h=FH//stride
return z.reshape(N,KN,out_h,-1)
另一种是基于NHWC通道的,卷积核形状是 的,事实上我在这里要对卷积核转置,如果和tf中的实现一致 ,则不需要转置。NHWC通道的实现。
def im2col(img, ksize, stride,padding='same'):
'''
:param img: 4D array N,FH,FW,C_{in}
:param ksize: tuple (kh,kw)
:param stride:
:return:
'''
kh,kw=ksize
if padding=='same':
p1=kh//2
p2=kw//2
img=np.pad(img,((0,0),(p1,p1),(p2,p2),(0,0)),'constant')
N,H,W,C=img.shape
out_h=(H-kh)//stride+1
out_w=(W-kw)//stride+1
col=np.empty((N*out_h*out_w,kw*kh*C))
outsize=out_w*out_h
for y in range(out_h):
y_min=y*stride
y_max=y_min+kh
y_start=y*out_w
for x in range(out_w):
x_start= x * stride
x_end= x_start + kw
col[y_start+x::outsize,:]= img[:, y_min:y_max, x_start:x_end, :].reshape(N, -1)
return col
def conv(X,W,stride=1,padding='same'):
'''
:param X: 4D array N,FH,FW,C_{in}
:param W: 4D array C_{out},kh,kw,C_{in}
:param stride:
:param padding:
:return: 4D array N,F,H,C_{out}
'''
FN,kh,kw,c=W.shape
N,FH,FW,C=X.shape
if padding=='same':
out_h=FH//stride
col=im2col(X,(kh,kw),stride)
z=np.dot(col,W.reshape(FN,-1).T)
return z.reshape(N,out_h,-1,FN)