笔者最近在研究 pytorch 代码的一些细节实现,在看到 转置卷积 这一概念的实现时,花了很长时间才搞懂。原因有两个:1.网上很多资料都是讲解 转置卷积 这一数学运算的,但是基本没有详细讲 pytorch 转置卷积函数的传入参数是怎么得到结果的。2当你尝试去看源码时却发现,python源码只是定义了接口,真正的计算过程调用了 C++ 库来实现的。
笔者颇花了一番功夫才理解了pytorch 怎么实现的转置卷积,借助本文记录
注:本文不涉及转置卷积的详细原理,请自行查阅其他资料;本文假设读者懂得卷积操作是怎么实现的。
1.背景知识
对于转置卷积,我的理解就是把小矩阵变成大矩阵,用术语来说就是上采样。
这一过程恰恰与卷积1过程相反 ,卷积是把一个大矩阵缩小成小矩阵。
这里你可能会想 我本来有大图像,读入变成矩阵 T T T,为了降低运算量,我通过卷积得到了一个小矩阵 t t t,那我可以再借助转置卷积把 t t t 还原成 T T T吗?
答案是不可以。
我们这里用到的转置卷积只是把 小的
t
e
n
s
o
r
tensor
tensor 变成一个大的
t
e
n
s
o
r
tensor
tensor ,仅此而已。
2.torch.nn.function.conv_transposed() 计算过程
2.1 先熟悉一下(conv_transposed())[]函数
import torch.nn.functional as F
F.conv_transpose1d(input=input_f, weight=kernel_f, bias=bias_f,
stride=1, padding=0, output_padding=0,
groups=2, dilation=1)
也可以自行查看官方文档
需要注意的是 input
和 weight
的形状是有讲究的,
weight
的shape[0]必须是input
的通道数;
weight
的shape[1]必须是输出通道数除以
g
r
o
u
p
group
group 即
C
o
g
r
o
u
p
\frac{C_o}{group}
groupCo;说明我们输出结果的通道数显然是与 group
参数有关;
所以本文重点讲解的参数是 input、 weight 、group。
因为它们三个最直接决定了输出结果
2.2 转置卷积是怎么回事
1. 我们有 输入和卷积核
2. 然后我们要做两件事
- 把输入两端补零,补 L-1 个
- 把卷积核旋转 180°
3. 开始按照正常卷积进行计算
简单通过以上三步我们就得到了转置卷积的结果,如果你想知道为什么这样做,可以查阅更多资料,但是不想了解那么多,每次只要给输入补零,然后旋转卷积核,最后进行计算就行了。
这就结束了?Naive!
上面只是最简单的一维卷积核,而且只是单通道,
多通道时输入通道和输出通道什么关系?
多维卷积怎么做?
且听下面讲解
2.3 group参数 的含义和计算流程
以下是本文用到的实验代码,以下讲解过程只会更改 input_f
、kernel_f
和 group_f
这三个输入参数
import torch
import torch.nn as nn
import torch.nn.functional as F
input_f = torch.randint(low=0, high=2, size=(1, 4, 5))
# kernel_f = torch.randint(low=0, high=3, size=(4, 3, 2))
kernel_f = torch.tensor([[[1, 0],
[1, 0],
[1, 0]],
[[0, 1],
[0, 1],
[0, 1]],
[[1, 0],
[1, 0],
[1, 0]],
[[0, 1],
[0, 1],
[0, 1]]])
bias_f = None if 1 else torch.randint(low=0, high=3, size=(2,))
out = F.conv_transpose1d(input=input_f, weight=kernel_f, bias=bias_f,
stride=1, padding=0, output_padding=0,
groups=2, dilation=1)
print("*******************************************")
print("******************** F ********************")
print("*******************************************\n")
print("========== I n p u t ===========")
print(input_f[0])
print(input_f.shape)
print('\n')
print("========== K e r n e l ==========")
print(kernel_f)
print(kernel_f.shape)
print('\n')
print("========== B i a s ==========")
print(bias_f)
# print(bias.shape)
print('\n')
print("========== O u t p u t ==========")
print("out.shape: ", out.shape)
print("out:")
print(out[0])
2.3.1 input通道和 weight[0] 一一对应
我们看前面的官方文档可以发现 ,pytorch要求我们的 weight
的shape[0]
必须是 input
的输入通道
C
i
C_i
Ci。其实这样做的原因是 计算的第一步会给输入信号每个通道从 weight
按顺序取出一个 weight[i]
,此后的计算就是它们一一进行了。
如何验证呢?
我们可以把除了 weight
input
之外的参数都设成默认
执行如下代码
input_f = torch.randint(low=0, high=2, size=(1, 4, 5))
kernel_f = torch.tensor([[[1, 0],
[1, 0],
[1, 0]],
[[0, 0],
[0, 0],
[0, 0]],
[[0, 0],
[0, 0],
[0, 0]],
[[0, 0],
[0, 0],
[0, 0]]])
out = F.conv_transpose1d(input=input_f, weight=kernel_f)
输出结果:
========== I n p u t ===========
tensor([[0, 1, 0, 0, 1],
[1, 1, 1, 0, 0],
[1, 1, 0, 0, 1],
[0, 0, 0, 1, 1]])
torch.Size([1, 4, 5])
========== K e r n e l ==========
========== I n p u t ===========
tensor([[0, 0, 1, 1, 1],
[1, 0, 1, 0, 0],
[1, 1, 0, 0, 1],
[0, 1, 1, 1, 1]])
torch.Size([1, 4, 5])
========== K e r n e l ==========
tensor([[[1, 0],
[0, 1],
[1, 1]],
[[0, 0],
[0, 0],
[0, 0]],
[[0, 0],
[0, 0],
[0, 0]],
[[0, 0],
[0, 0],
[0, 0]]])
torch.Size([4, 3, 2])
========== O u t p u t ==========
out.shape: torch.Size([1, 3, 6])
out:
tensor([[0, 0, 1, 1, 1, 0],
[0, 0, 0, 1, 1, 1],
[0, 0, 1, 2, 2, 1]])
发现了什么?如果我们按照第2节的转置卷积计算规则验算就会发现,输出结果的三个通道正是 weight[0]
的三个通道(三个行向量)对input[0]
的转置卷积结果!
所以这也就是为什么pytorch要求我们的 weight
的shape[0]
必须是 input
的输入通道
C
i
C_i
Ci!因为它们是一一对应的!
换句话就是每个输入tensor的每个通道从 weight
按顺序取出一个 weight[i]
,此后的计算就是它们一一进行了。
2.3.2 group是把 weight 进行分组
我们刚才讲了 每个 weight[i]
都会与 input[n][i]
进行转置卷积,这样我们就能得到
C
i
C_i
Ci个卷积结果,而且每个卷积结果都是
C
o
g
r
o
u
p
\frac{C_o}{group}
groupCo个通道。
告诉大家,
g
r
o
u
p
group
group 的作用就是说明了这些转置卷积结果如何组合起来变成最终输出。
我总结的规则就是
- 把转置卷积结果分成 g r o u p group group 组。
- 同一组的计算结果叠加
- 不同组的计算结果并联
下面图示讲解 其中 g r o u p group group 设置为 2
按前面讲的一个输入通道对应一个卷积核计算
按
g
r
o
u
p
group
group 参数分组,同组按元素相加,异组并联
最终我们就得到了结果
2.4 conv_transpose1D()其他细节
stride
:默认为1,其他数值会对输入先插零,再当作正常输入进行计算
padding
:默认为0,其他数值会对weight
先两边补零,再当作正常权重进行计算
diliation
:默认为1,其他数值会对weight
先中间插0,再当作正常权重进行计算
3. g r o u p group group代表了什么
为什么本文专注于对 group 这一参数的探讨呢,原因是 group 决定了我们上采样得到的结果是由输入的哪些通道决定的
group=1 :表明所有的计算结果都要叠加,这说明输出的每一通道,都是由所有的输入通道上采样后叠加在一起的。
group= C i C_i Ci :表明所有的计算结果都要并联,这说明输出的每一通道,都是由单个的输入通道上采样后叠加在一起的。
提一句,由于本人是通信专业的,发现深度学习领域的卷积与信号处理领域的卷积不是一回事,信号处理的卷积其实还多一步卷积核反转的过程,然后才是相乘求积分(连续信号)或者相乘累加(离散信号) ↩︎