卷积实现之im2col算法

卷积详解之im2col算法

  这篇文章是需要和卷积的实现原理一起来看的,两篇文章一个比较宏观,一个比较微观,在卷积的实现原理中我多次提到了im2col算法,这个算法顾名思义,就是把图像转化为列向量,但是这个转换不是整张图的转换,事实上转换出来的图像还是二维的,只是把卷积在特征图的感受野区域给转换成一维的向量。
  我们熟悉卷积都知道,感受野区域是个二维的区域,这二维的区域从存储上不是连续的,这样就不便于计算。im2col算法完成的就是通过矩阵乘法来完成卷积核和感受野的对应相乘,这样计算起来就方便多了。
在这里插入图片描述
  我们可以看到上图中的滑动窗口原理。卷积核在特征图上滑过的一个个窗口,我们称之为感受野,卷积核与感受野发生的运算是对应相乘再相加。比如我们第一次得到的结果就是 1 1 + 1 0 + 1 1 + 0 0 + 1 1 + 1 0 + 0 1 + 0 0 + 1 1 = 4 1*1+1*0+1*1+0*0+1*1+1*0+0*1+0*0+1*1=4 ,这个公式我们可以将它想象成 α 1 w T α 1 = [ 1 , 1 , 1 , 0 , 1 , 1 , 0 , 0 , 1 ] , w = [ 1 , 0 , 1 , 0 , 1 , 0 , 1 , 0 , 1 ] \alpha_1w^T, \alpha_1=[1,1,1,0,1,1,0,0,1],w=[1,0,1,0,1,0,1,0,1] 。可以看到卷积核一共滑过了9个感受野,每个感受野都是和这个卷积核做运算。如果我们把卷积核一行一行的拼成矩阵,那么乘卷积核的结果就是卷积的结果了。结果虽然对了,不过这里把二维的卷积结果变成了一维的向量,最后还需要再转换回去。
在这里插入图片描述
  介绍了初步的概念,我们还应当注意这个时候我们把特征图的矩阵是放在右元的位置。如果这个时候还有输入通道数 C i n C_{in} ,那么就依次排开,对于同一个感受野,所有的通道都放在一行,左元共同的维度展开为一行,右元共同的维度展开为一列。如果卷积核还有O个输出通道,则应该排成一行。
在这里插入图片描述
  其中特征图的上标代表输入的第i个通道,一共C个。下标代表第i个输出,一共 o u t s e i z e = H N W N outseize=H_N*W_N 个。
卷积核的上标代表输出的第i个通道,一共O个。通过这张图大家可以轻易的明白,如果特征图作为矩阵乘法的左元,则 C i n , K h , K w C_{in},K_h,K_w 一同展开为一列,矩阵的行数就是感受野的个数,也是输出的特征图的cell个数。
在这里插入图片描述
  此时的卷积核不同输入通道的也需要转到同一列上去。
在这里插入图片描述
  除了这种情况,还有特征图作为矩阵右元的情况,我们依然是把 C i n , K h , K w C_{in},K_h,K_w 这个维度展开,作为右元,自然这里就要转为一列,而把输出的数目作为一行。卷积核就能把不同的输入通道展开为一行。原理都是一样的。最后展开的特征图维度也就是 C i n K h K w , H N H W C_{in}*K_h*K_w,H_N*H_W 的。
在这里插入图片描述
  然后就是最后一点问题,访问局部性的问题,C语言系的语言都是对数组按照行优先存储,同一行的数据有局部性,如果将特征图的感受野展开为行的时候能够更好的利用局部性。但是这个问题不是主要问题,因为按列展开的时候,也是可以把数据一行一行的写进转换后的矩阵的,这个就需要更加细致的分析,这里就不赘述了。然后就是一般对矩阵进行转置也是需要开销的,所以避免尽量使用转置,这个需要通过合理的设置维度顺序。
  网上很多解释也是让人看了迷之疑惑,明明是展开为列的框架非要给你讲解成展开为行,这个样子,自己实现的话就非常容易踩坑了。下面贴出我自己用numpy实现的代码,首先说一句,我不是从C++底层实现的,如果需要自己看实现,推荐看一下caffe的源码,网上C++实现讲解的比较多,但是还是要注意区分,不要踩坑。代码很简单,对于文章有疑问的同学欢迎讨论。
  我的两种实现一种是基于NCHW通道的,卷积核都是和图像通道保持一致是 ( C o u t , C i n , K h , K w ) (C_{out},C_{in},K_h,K_w) 的,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通道的,卷积核形状是 ( C o u t , K h , K w , C i n ) (C_{out},K_h,K_w,C_{in}) 的,事实上我在这里要对卷积核转置,如果和tf中的实现一致 ( K h , K w , C i n , C o u t ) (K_h,K_w,C_{in},C_{out}) ,则不需要转置。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)
发布了36 篇原创文章 · 获赞 4 · 访问量 47万+

猜你喜欢

转载自blog.csdn.net/m0_38065572/article/details/104709433