Pytorch实现卷积、Depthwise Convolution、分组卷积、动态卷积和转置卷积、反卷积、全卷积、空洞卷积、可变形卷积、深度可分离卷积等操作

底层是用img2col实现的,但是如果想用pytorch来实现,可以试试torch.unfold这个函数,也可以硬写

torch.unfold

torch.unfold可以按照指定维度,以一定的间隔将原始张量进行分片(slicing),然后返回重整后的张量。先以二维矩阵展示其用法

>>> a = torch.arange(16).view(4, 4)
>>> a
tensor([[ 0,  1,  2,  3],
        [ 4,  5,  6,  7],
        [ 8,  9, 10, 11],
        [12, 13, 14, 15]])
>>> b = a.unfold(0, 3, 1)           # 按照行,以每3个元素,跨步为1进行展开
>>> b
tensor([[[ 0,  4,  8],
         [ 1,  5,  9],
         [ 2,  6, 10],
         [ 3,  7, 11]],

        [[ 4,  8, 12],
         [ 5,  9, 13],
         [ 6, 10, 14],
         [ 7, 11, 15]]])
>>> b.shape
torch.Size([2, 4, 3])
>>> c = b.unfold(1, 3, 1)           # 对b按照列以每3个元素跨步为1进行展开
>>> c                               # 注意此时c即为3x3滑动窗展开结果
tensor([[[[ 0,  1,  2],
          [ 4,  5,  6],
          [ 8,  9, 10]],

         [[ 1,  2,  3],
          [ 5,  6,  7],
          [ 9, 10, 11]]],


        [[[ 4,  5,  6],
          [ 8,  9, 10],
          [12, 13, 14]],

         [[ 5,  6,  7],
          [ 9, 10, 11],
          [13, 14, 15]]]])
>>> c.shape
torch.Size([2, 2, 3, 3])

这里看出b是a按照行,以每3个元素,跨步为1进行展开;而c是对b按照列以每3个元素跨步为1进行展开,注意此时c即为3×3滑动窗展开结果。这里c的前两维2×2,表示的是卷积输出结果的大小;而后面3×3即是为了完成卷积运算所需要的滑动窗内输入元素。因此卷积结果就是让卷积核权重与2×2个3×3的滑动窗元素相乘并加和得到。

而CNN中完整的卷积是对于两个4维张量进行操作。其中输入 X 尺寸大小为 N × C × H × W N\times C\times H \times W N×C×H×W,分别代表了批样本量(batch size),输入通道数(channel),输入长和宽。而卷积核尺寸大小为 D × C × K × K D\times C\times K\times K D×C×K×K,分别代表输出通道数,输入通道数,和卷积核尺寸(kernel size)。 而在每个输出通道上由 C × K × K C\times K\times K C×K×K 的单个卷积核与 C × H × W C\times H\times W C×H×W 输入通道分别进行二维卷积,再累加在一起形成一个二维的输出。
下面函数就实现了以上逻辑:

import torch
import torch.nn as nn

def conv2d_unfold(x, weight, stride, pad):
    n, c, h_in, w_in = x.shape
    d, c, k, j = weight.shape
    x_pad = torch.zeros(n, c, h_in+2*pad, w_in+2*pad)   # 对输入进行补零操作
    if pad>0:
        x_pad[:, :, pad:-pad, pad:-pad] = x
    else:
        x_pad = x
    x_pad = x_pad.unfold(2, k, stride)
    x_pad = x_pad.unfold(3, j, stride)        # 按照滑动窗展开
    out = torch.einsum(                          # 按照滑动窗相乘,
        'nchwkj,dckj->ndhw',                    # 并将所有输入通道卷积结果累加
        x_pad, weight)
    return out

def conv2d_for(x, weight, stride, pad):
    n, c, h_in, w_in = x.shape
    d, c, kh, kw = weight.shape
    x_pad = torch.zeros(n, c, h_in+2*pad, w_in+2*pad)   # 对输入进行补零操作
    if pad>0:
        x_pad[:, :, pad:-pad, pad:-pad] = x
    else:
        x_pad = x

    h_out,w_out = (h_in+2*pad-kh)//stride+1,(w_in+2*pad-kw)//stride+1
    out = torch.zeros(n,d,h_out,w_out)
    for ni in range(n):
        for di in range(d):
            for hi in range(h_out):
                for wi in range(w_out):
                    for ci in range(c):
                        for offh in range(kh):
                            for offw in range(kw):
                                tmp = x_pad[ni][ci][offh+hi*stride][offw+wi*stride]*weight[di][ci][offh][offw]
                                out[ni][di][hi][wi] += tmp
    return out

if __name__ == '__main__':

    n,c,h,w = 2,3,4,4
    d,i,j = 1,3,3
    x = torch.arange(n*c*h*w).reshape(n,c,h,w).to(torch.float32)
    weight = torch.arange(d*c*i*j).reshape(d,c,i,j).to(torch.float32)
    bias = torch.arange(d*c*i*j).reshape(d,c,i,j).to(torch.float32)
    stride = 1
    pad = 1
    print(conv2d_unfold(x,weight,stride,pad))
    print(conv2d_for(x,weight,stride,pad))
    conv2d = nn.Conv2d(in_channels=c, out_channels=d, kernel_size=i, stride=1, padding=1,bias=False)
    print(conv2d(x))
    '''
    输出是:
    tensor([[[[ 4521.,  6753.,  7014.,  4635.],
              [ 6858., 10197., 10548.,  6939.],
              [ 7830., 11601., 11952.,  7839.],
              [ 5007.,  7383.,  7590.,  4953.]]],
    
    
            [[[13161., 19281., 19542., 12699.],
              [18522., 27045., 27396., 17739.],
              [19494., 28449., 28800., 18639.],
              [11919., 17319., 17526., 11289.]]]])
    tensor([[[[ 4521.,  6753.,  7014.,  4635.],
              [ 6858., 10197., 10548.,  6939.],
              [ 7830., 11601., 11952.,  7839.],
              [ 5007.,  7383.,  7590.,  4953.]]],
    
    
            [[[13161., 19281., 19542., 12699.],
              [18522., 27045., 27396., 17739.],
              [19494., 28449., 28800., 18639.],
              [11919., 17319., 17526., 11289.]]]])
    tensor([[[[17.7317, 13.2532, 13.5966,  5.0182],
              [19.7582, 14.7422, 15.1810,  9.2396],
              [21.7877, 16.4971, 16.9358, 11.0396],
              [ 5.7740,  3.9739,  3.9210, -0.8523]]],
    
    
            [[[45.2875, 29.7362, 30.0796, 14.0081],
              [44.1122, 35.8005, 36.2392, 30.8389],
              [46.1417, 37.5553, 37.9940, 32.6389],
              [ 4.8742,  1.4374,  1.3846, -1.4356]]]],
           grad_fn=<ConvolutionBackward0>)
    '''

上面代码’nchwkj,dckj->ndhw’中,hw是过了两次unfolder的结果,刚好是输出卷积特征图的hwkj是卷积核的hw,dc是卷积核的输出通道和输入通道

einsum这个函数,输出有几维±>之前重叠的维数,就是写几重循环。例如nchwkj,dckj->ndhw,输出有ndhw这4维,输入有ckj这3维重叠,所以就是写7重循环把上面的给展开
在这里插入图片描述
https://github.com/arogozhnikov/einops ,里面将einsum实现了扩展,可以玩转多种复杂张量操作,有空可以专门另写一篇好好聊聊。

深度卷积(Depthwise Convolution)

所谓Depthwise Convolution,就是在进行卷积的时候,只是各个通道对应相乘,而不进行累加求和。那么实现起来,只需要改动torch.einsum的求和指标即可

out = torch.einsum('nchwkj,ckj->nchw', x_pad, weight)

此时的权重shape也变成了 C × 1 × K × K C\times 1\times K\times K C×1×K×K

分组卷积(Group Convolution)

分组卷积就是在进行卷积运算时,输入通道不是全部参与计算,而是分割开来成为几组,每组内部进行正常卷积。那实现起来,就需要对输入和权重进行一下reshape,都变成多组形式,然后每个组内进行相乘累加求和。新的改造代码如下

def group_conv2d(x, weight, bias, stride, pad, groups): 
    n, c, h_in, w_in = x.shape
    d, c_g, k, j = weight.shape
    assert c // groups == c_g                                     # 保证分组之后通道相同
    x_pad = torch.zeros(n, c, h_in+2*pad, w_in+2*pad)   # 对输入进行补零操作
    if pad>0:
        x_pad[:, :, pad:-pad, pad:-pad] = x
    else:
        x_pad = x

    x_pad = x_pad.unfold(2, k, stride)
    x_pad = x_pad.unfold(3, j, stride)                            # 按照滑动窗展开
    h_pad, w_pad = x_pad.size(2), x_pad.size(3)
    x_pad = x_pad.reshape(n, groups, -1, h_pad, w_pad, k, j)      # 对输入按照通道分组
    weight = weight.reshape(groups, -1, c_g, k, j)                      # 对权重按照输出通道分组
    out = torch.einsum(                                  # 按照滑动窗相乘,
        'ngchwkj,gdckj->ngdhw',                      # 并将所有输入通道卷积结果累加
        x_pad, weight)
    out = out.reshape(n, d, out.size(3), out.size(4))                     # 再重新reshape成完整输出
    out = out + bias.view(1, -1, 1, 1)                                          # 添加偏置值
    return out

动态卷积(Dynamic Convolution)

所谓动态卷积,本质思想就是让一个batch的输入内部各个单独张量,可以有不同的权重值进行卷积操作。 现在的常规卷积是共享的。那实现这种动态卷积最大难点是什么呢?为了提高并行度与速度,就不可能按照for循环每一个输入与不同的权重相乘。所以也是有很多工作想办法去解决这个问题。比如NeurIPS 2019的这篇文章
https://proceedings.neurips.cc/paper/2019/file/f2201f5191c4e92cc5af043eebfd0946-Paper.pdf

就提出CondConv,先根据输入产生一组gate,然后在已有的一组权重上进行加权,从而实现动态调节产生动态权重。具体如下图

在这里插入图片描述
左侧是CondConv示意图,右侧是他们论证这个等效于Mixture of Experts,就是先计算多组卷积结果,再用gate融合。但是相比MoE,CondConv只需要执行整体的一次卷积即可。

那具体怎么实现呢?有一种实现方式,是把N个Batch按照channel维度堆叠起来,然后做group convolution,再拆分开来。那按照我们上面的实现思路,CondConv实现起来非常直观。我们忽略掉产生gate动态加权权重的部分,假设已经产生了 N\times D\times C\times K\times K 的权重,那么只需要更改一行torch.einsum代码即可

out = torch.einsum('nchwkj,ndckj->ndhw', x_pad, weight)

以上摘录自 https://www.zhihu.com/tardis/zm/art/349683405?source_id=1003

深度可分离卷积(Depthwise separable convolution)

来自于MobileNet V1,顾名思义,来自于分离+Depthwise Convolution 的操作:
在这里插入图片描述

转置卷积=反卷积(为了上采样,语义分割用)

细节可以参考 FCN 全卷积网络和转置卷积,反卷积是上采样(unsampling) 的一种方式,论文作者在实验之后发现反卷积相较于其他上采样方式例如 bilinear upsampling 效率更高,所以采用了这种方式。关于反卷积的解释借鉴了这一篇: https://medium.com/activating-robotic-minds/up-sampling-with-transposed-convolution-9ae4f2df52d0, 英文OK的小伙伴推荐看原文,讲的很通透。

全卷积

FCN 的基本结构很简单,就是全部由卷积层组成的网络。用于图像分类的网络一般结构是"卷积-池化-卷积-池化-全连接",其中卷积和全连接层是有参数的,池化则没有参数。论文作者认为全连接层让目标的位置信息消失了,只保留了语义信息,因此将全连接操作更换为卷积操作可以同时保留位置信息及语义信息,达到给每个像素分类的目的。网络的基本结构如下:
在这里插入图片描述

空洞卷积(为了上采样,语义分割用)

如何不通过池化等下采样操作就能扩大感受野呢?空洞卷积应运而生。顾名思义,空洞卷积就是在标准的卷积核中注入“空洞”,以增加卷积核的感受野。空洞卷积引入了扩张率(dilation rate)这个超参数来指定相邻采样点之间的间隔,扩张率为了的空洞卷积。

可变形卷积

可变形卷积在卷积核的每个采样点上添加一个可学习的偏移量(offset),让采样点不再局限于规则的网格点。
在这里插入图片描述
适应物体在不同图片中出现的复杂几何形变(如尺度形态、非刚性形变等),一直是物体识别领域的难点,可变形卷积网络给出了一个可行的解决方案。它可以端到端地学习几何形变的偏移量,不需要额外的监督信息,并且只增加了少许计算量,最终却能带来效果的显著提升。

全文搜集自:

  1. https://www.zhihu.com/tardis/zm/art/349683405?source_id=1003
  2. 葫芦书
  3. https://zhuanlan.zhihu.com/p/92134485
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值