nn.ConvTranspose2d详解

目录

  • 概述
  • 1.得到新的feature map
    • s=1
    • s>1
  • 2.确定卷积核的值
  • 3.执行卷积操作
  • 引用

概述

  • 该函数是用来进行转置卷积的,它主要做了这几件事:首先,对输入的feature map进行padding操作,得到新的feature map;然后,随机初始化一定尺寸的卷积核;最后,用随机初始化的一定尺寸的卷积核在新的feature map上进行卷积操作。
  • 补充一下,卷积核确实是随机初始的,但是后续可以对卷积核进行单独的修改,比如使用双线性卷积核,这样的话卷积核的参数是固定的,不可以进行学习修改。
  • 下面对上面的过程进行演示。

1.得到新的feature map

  • 分两种情况,s(步长)=1或者>1,分别对应下面的情况。

s=1

nn.ConvTranspose2d详解_第1张图片

  • 如上图所示,当s=1时,对于原feature map不进行插值操作,只进行padding操作,padding_NewSize=(kernel_size - padding - 1),这里的padding是卷积操作时设置的padding值,如果是valid卷积的话padding=0,kernel_size是转置卷积中卷积核的尺寸。下面用 H i n t H_{int} Hint代替原始feature map, H o u t H_{out} Hout代替新的feature map。经过padding之后,再经过卷积核进行卷积操作就得到了 H o u t H_{out} Hout,上图 H o u t = H i n t + 2 ∗ p a d d i n g N e w S i z e = 2 + 4 = 6 H_{out}=H_{int}+2*paddingNewSize=2+4=6 Hout=Hint+2paddingNewSize=2+4=6
  • 其实这种情况还是比较好处理的,直接遵循 k ′ = k , s ′ = s , p ′ = k − p − 1 k^{'}=k,s^{'}=s,p^{'}=k-p-1 k=k,s=s,p=kp1就好了,其中 ′ ' 表示执行转置卷积时的参数,没有 ′ ' 表示是转置卷积前的卷积操作的参数。

s>1

nn.ConvTranspose2d详解_第2张图片

  • 当s>1时,进行插值操作,什么是插值呢?大家看上图, H i n H_{in} Hin的元素之间被分隔开了,具体间隔多少个呢?(s-1)个。也就是说,上图的s=2。插入的元素是0。上图padding的设置为1。其实包括s啊,kernel_size啊,padding啊都是传入该函数的参数,你在调用该函数前肯定得先算好这些参数传入的值才能保证经过转置卷积之后得到的尺寸是你心仪的尺寸,这些尺寸的设置其实不是你需要重点关注的内容,只需要套公式计算就好了。

  • 这里有个疑问,为什么要进行插值操作呢?如果不进行插值操作,那么你步长不是1,可能会跳过一些 H i n H_{in} Hin的元素值,就丢失了一部分信息。

  • 另外这个地方比较麻烦的一点是,不能直接使用s=1时的公式来确定 p ′ , s ′ , k ′ p^{'},s^{'},k^{'} p,s,k了,而是要对输入进行处理,主要分为两步:

  • 1.在输入的每两个像素之间增加s-1个0。

  • 2.在输入的底边和右边加入 ( c + 2 ∗ p − k ) m o d ( s ) (c+2*p-k)mod(s) (c+2pk)mod(s),其中c就是根据第一步中计算出来的转置卷积输出的大小。

  • 我们来看下第1和2步,其实我们都知道卷积公式,那么假设转置卷积的输入i=3,k=3,s=2,p=1,计算转置卷积的输出:
    在这里插入图片描述
    此时c有两种情况,当c=5时, ( c + 2 ∗ p − k ) m o d    s = ( 5 + 2 ∗ 1 − 3 ) m o d    2 = 0 , s − 1 = 1 (c+2*p-k)mod\,\, s=(5+2*1-3)mod \,\, 2=0,s-1=1 (c+2pk)mods=(5+213)mod2=0s1=1,因此对3x3的输入特诊图进行0填充,按第一步填充得到:
    nn.ConvTranspose2d详解_第3张图片
    第二步中 ( c + 2 ∗ p − k ) m o d    s = ( 5 + 2 ∗ 1 − 3 ) m o d    2 = 0 (c+2*p-k)mod\,\, s=(5+2*1-3)mod \,\, 2=0 (c+2pk)mods=(5+213)mod2=0,故无需填充。
    这两步走完了,最后按照 k ′ = k , s ′ = s , p ′ = k − p − 1 得 到 k ′ = 3 , s ′ = 1 , p ′ = k − p − 1 = 1 k^{'}=k,s^{'}=s,p^{'}=k-p-1得到k^{'}=3,s^{'}=1,p^{'}=k-p-1=1 k=k,s=s,p=kp1k=3,s=1,p=kp1=1
    此时 p ′ = 1 p^{'}=1 p=1,再对特征图进行填充:
    nn.ConvTranspose2d详解_第4张图片
    然后按照 k ′ = 3 , s ′ = 1 k^{'}=3,s^{'}=1 k=3,s=1进行卷积得到结果:
    nn.ConvTranspose2d详解_第5张图片

  • 在看若c=6:
    当c=6时, ( c + 2 ∗ p − k ) m o d    s = ( 6 + 2 ∗ 1 − 3 ) m o d    2 = 1 , s − 1 = 1 , 因 此 对 3 × 3 (c+2*p-k)mod\,\, s=(6+2*1-3)mod \,\, 2=1,s-1=1,因此对3 \times 3 (c+2pk)mods=(6+213)mod2=1s1=13×3的输入特诊图进行0填充,按第一步填充得到:
    nn.ConvTranspose2d详解_第6张图片
    第二步中 ( c + 2 ∗ p − k ) m o d    s = ( 6 + 2 ∗ 1 − 3 ) m o d    2 = 1 , (c+2*p-k)mod\,\, s=(6+2*1-3)mod \,\, 2=1, (c+2pk)mods=(6+213)mod2=1因此再在特诊图下面和右边填充一层
    nn.ConvTranspose2d详解_第7张图片
    同样计算出参数 k ′ = 3 , s ′ = 1 , p ′ = 3 − 1 − 1 = 1 k^{'}=3,s^{'}=1,p^{'}=3-1-1=1 k=3,s=1,p=311=1,对特征图进行填充:
    nn.ConvTranspose2d详解_第8张图片
    然后再卷积得到输出:
    nn.ConvTranspose2d详解_第9张图片

  • 那大家可能会问了,如何用参数来决定c到底是几呢?大家可以看到上述c=5和6的区别就是 ( c + 2 ∗ p − k ) m o d    s = 几 (c+2*p-k)mod\,\, s=几 (c+2pk)mods=,我们设置这个值为0就相当于告诉函数我们要c=5,设置=1就相当于告诉函数我们要c=6,决定权在我们自己,只需要告诉函数就好了。而这个值是通过output_padding这个参数来设置的。

  • 上述部分只是举了个例子,给你了一个卷积之后的feature map,它卷积前的大小有很多种可能,计算转置卷积的输出的大小的时候,因为有向下取整操作,所以卷积输出的大小是不确定的,所以我们必须告诉函数选哪个,而output_padding的设置就是为了避免这种不确定性的。

2.确定卷积核的值

  • 卷积核的确定方式主要有两种,一是通过双线性插值固定卷积核的参数,不随着学习过程更新;二是随机初始化,并随着学习过程更新。
  • 其实我个人感觉,后者更好一点,因为特征点对于中心的贡献程度并不一定是线性的。
  • 随机初始化就不说了,而双线性插值固定卷积核是关于x轴和y轴对称的,如下图所示:
    nn.ConvTranspose2d详解_第10张图片
    生成代码:

def bilinear_kernel(in_channels, out_channels, kernel_size):
    factor = (kernel_size + 1) // 2
    if kernel_size % 2 == 1:
        center = factor - 1
    else:
        center = factor - 0.5
    og = np.ogrid[:kernel_size, :kernel_size]
    filt = (1 - abs(og[0] - center) / factor) * \
           (1 - abs(og[1] - center) / factor)
    weight = np.zeros((in_channels, out_channels, kernel_size, kernel_size),
                      dtype='float32')
    weight[range(in_channels), range(out_channels), :, :] = filt
    return torch.from_numpy(np.array(weight))

这个很好理解,因为卷积核执行卷积操作其实就相当于得到了一个 H o u t H_{out} Hout的元素,而卷积核的范围是有限的,它是越处于卷积核中间的元素对于 H o u t H_{out} Hout的元素的值的贡献越高,所以呈现一个圆峰状的卷积核矩阵形状。这是符合常理的,因为卷积核所笼罩的范围相对于 H i n H_{in} Hin的位置正是映射到 H o u t H_{out} Hout的元素相对于 H o u t H_{out} Hout的位置,
如下图所示
nn.ConvTranspose2d详解_第11张图片
上面红框元素相对于 H o u t H_{out} Hout的位置就是下面红框相对于 H i n H_{in} Hin的位置(或者说相对于 H o u t H_{out} Houtpadding之后的矩阵的位置)。上面红框的值由下面红框的值来推测,推测手段就是线性插值方法,所以使用的卷积核是双线性卷积核,就是对卷积核笼罩的像素点进行双线性插值推测得到了对于卷积核笼罩的中心像素点的像素值。

  • 再直观一点讲,假如用H代表下面的padding之后的矩阵,那么问题就变成了已知 H 2 , 2 H_{2,2} H2,2 H 2 , 4 H_{2,4} H2,4 H 2 , 6 H_{2,6} H2,6 H 4 , 2 H_{4,2} H4,2 H 4 , 4 H_{4,4} H4,4 H 4 , 6 H_{4,6} H4,6 H 6 , 2 H_{6,2} H6,2 H 6 , 4 H_{6,4} H6,4 H 6 , 6 H_{6,6} H6,6,然后使用双线性插值推测 H 2 , 2 H_{2,2} H2,2—— H 2 , 6 H_{2,6} H2,6 H 3 , 2 H_{3,2} H3,2—— H 3 , 5 H_{3,5} H3,5 H 4 , 2 H_{4,2} H4,2—— H 4 , 5 H_{4,5} H4,5 H 5 , 2 H_{5,2} H5,2—— H 5 , 5 H_{5,5} H5,5 H 6 , 2 H_{6,2} H6,2—— H 6 , 5 H_{6,5} H6,5的值,其中有些值已经已知了。

3.执行卷积操作

  • 只需要注意下尺寸的问题就好:在这里插入图片描述
    在调用nn.ConvTranspose2d的时候注意参数满足上述公式。其中H_out是原始feature map的尺寸,而H_in是输入图像的尺寸,也就是目标尺寸,想要通过上采样达到的尺寸。

引用

  • https://www.freesion.com/article/53151034051/
  • https://www.cnblogs.com/wanghui-garcia/p/10791328.html
  • https://blog.csdn.net/jiongnima/article/details/78578876?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-6.control&dist_request_id=&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromBaidu-6.control
  • https://www.freesion.com/article/5924530862/
  • 侵删

你可能感兴趣的:(算法,语义分割,深度学习,算法,转置卷积)