卷积层和汇聚层通常会减少下采样输入图像的空间维度(高和宽)
- 卷积通常来说不会增大输入的高和宽,要么保持高和宽不变,要么会将高宽减半,很少会有卷积将高宽变大的
- 可以通过 padding 来增加高和宽,但是如果 padding 得比较多的话,因为填充的都是 0 ,所以最终的结果也是 0 ,因此无法有效地利用 padding 来增加高宽
- 如果输入和输出图像的空间维度相同,会便于以像素级分类的语义分割:输出像素所处的通道维可以保有输入像素在同一位置上的分类结果(为实现输入和输出的空间维度的一致,在空间维度被卷积神经网络层缩小后,可以使用转置卷积增加上采样中间层特征图的空间维度)
语义分割的问题在于需要对输入进行像素级别的输出,但是卷积通过不断地减小高宽,不利于像素级别的输出,所以需要另外一种卷积能够将高宽变大
- 转置卷积可以用来增大输入的高和宽
工作原理
- 转置卷积的工作原理有点类似于卷积
- 假设有一个 2×2 的输入和一个 2×2 的核
- 这个核会在输入上以步幅为 1 进行滑动且没有填充,对于输入的每一个元素,它会跟核上的每一个元素按元素做乘法,然后逐次写回到对应的位置(写回到一个更大的矩阵中,除了写回的位置,其他元素初始化为 0 )
- 这样的话,输入有多少个元素,就会得到多少个乘积的结果(比输入更大的矩阵),最后将这些结果按元素位置进行相加,最终就能够得到输出了
- 上图中的 stride 为 1 (输入的相邻元素跟核按元素乘积的结果写回到更大的矩阵的对应位置时相隔 1 列),stride 如果特别大的话就能够将输出的高宽变得特别大,达到成倍地增加高宽的目的
转置卷积和卷积的异同
1、工作原理
- 常规卷积是输入跟核进行按元素的乘法并相加,最后得到对应位置的值,通过核在输入上进行滑动从而得到输出
- 转置卷积是输入的单个元素跟核按元素做乘法但不相加,保持核的大小,然后按元素写回到一个更大的矩阵的对应位置(输入的每个元素会生成与核大小相同的矩阵写入到一个更大的矩阵的对应位置,所以输出的高宽相对于输入来讲是变大的)
2、输入输出
- 常规卷积通过卷积核 “减少” 输入元素
- 转置卷积通过卷积核 “广播” 输入元素,从而产生大于输入的输出
3、填充
- 常规卷积中将填充应用于输入(如果将高和宽两侧的填充数指定为 1 时,常规卷积的输入中将增加第一和最后的行和列)
- 转置卷积中将填充应用于输出(如果将高和宽两侧的填充数指定为 1 时,转置卷积的输出中将删除第一和最后的行和列)
4、步幅
- 常规卷积中,步幅所指定的是卷积核在输入上的滑动距离
- 转置卷积中,步幅所指定的是卷积核每次运算结果写回到中间结果(输出)矩阵中对应位置的滑动距离,以下两张图是卷积核为 2 × 2 ,步幅分别为 1 和 2 的转置卷积运算
- 上图运算中转置卷积的步幅为 1
- 上图运算中转置卷积的步幅为 2
5、多通道
- 对于多输入和输出通道,转置卷积与常规卷积以相同的方式运作
为什么称之为“转置”
对于卷积 Y = X * W
- " * "代表卷积
- 可以对 W 构造一个 V (V 是一个比较大的向量),使得卷积等价于矩阵乘法 Y‘ = VX’
- 这里的 Y‘, X’ 是 Y, X 对应的向量版本(将 Y, X 通过逐行连结拉成向量)
- 如果 X’ 是一个长为 m 的向量,Y‘ 是一个长为 n 的向量,则 V 就是一个 n×m 的矩阵
转置卷积同样可以对 W 构造一个 V ,则等价于 Y‘ = VTX'
- 按照上面的假设 VT 就是一个 m×n ,则 X’ 就是一个长为 n 的向量,Y‘ 就是一个长为 m 的向量,X 和 Y 的向量发生了交换
- 从 V 变成了 VT 所以叫做转置卷积
所以如果卷积将输入从(h,w)变成了(h',w'),则同样超参数(kernel size, stride, padding)的转置卷积则从(h',w')变成(h,w)
总结
1、与通过卷积核减少输入元素的常规卷积相反,转置卷积通过卷积核广播输入元素,从而产生形状大于输入的输出
2、如果我们将 X 输入卷积层 f 来获得输出 Y = f(X) 并创造一个与 f 有相同的超参数、但输出通道数是 X 中通道数的转置卷积层 g ,那么 g(Y) 的形状将与 X 相同。
3、可以使用矩阵乘法来实现卷积。转置卷积层能够交换卷积层的正向传播函数和反向传播函数
代码:
import torch
from torch import nn
from d2l import torch as d2l
# 实现基本的转置卷积运算
def trans_conv(X, K):
h, w = K.shape # 卷积核的宽、高
Y = torch.zeros((X.shape[0] + h -1, X.shape[1] + w - 1)) # 正常的卷积后尺寸为(X.shape[0] - h + 1, X.shape[1] - w + 1)
for i in range(X.shape[0]):
for j in range(X.shape[1]):
Y[i:i + h, j:j + w] += X[i, j] * K # 按元素乘法,加回到自己矩阵
return Y
# 验证上述实现输出
X = torch.tensor([[0.0,1.0],[2.0,3.0]])
K = torch.tensor([[0.0,1.0],[2.0,3.0]])
trans_conv(X,K)
# 使用高级API获得相同的结果
X, K = X.reshape(1,1,2,2), K.reshape(1,1,2,2)
tconv = nn.ConvTranspose2d(1,1,kernel_size=2,bias=False) # 输入通道数为1,输出通道数为1
tconv.weight.data = K
tconv(X)
# 填充、步幅和多通道
# 填充在输出上,padding=1,之前输出3x3,现在上下左右都填充了1,那就剩下中心那个元素了
# 填充为1,就是把输出最外面的一圈当作填充
tconv = nn.ConvTranspose2d(1,1,kernel_size=2,padding=1,bias=False)
tconv.weight.data = K
tconv(X)
tconv = nn.ConvTranspose2d(1,1,kernel_size=2,stride=2,bias=False)
tconv.weight.data = K
tconv(X)
# 多通道
X = torch.rand(size=(1,10,16,16))
conv = nn.Conv2d(10,20,kernel_size=5,padding=2,stride=3)
tconv = nn.ConvTranspose2d(20,10,kernel_size=5,padding=2,stride=3)
tconv(conv(X)).shape == X.shape
tensor([[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]])
tensor([[[[ 0., 0., 1.],
[ 0., 4., 6.],
[ 4., 12., 9.]]]], grad_fn=<SlowConvTranspose2DBackward0>)
tensor([[[[4.]]]], grad_fn=<SlowConvTranspose2DBackward0>)
tensor([[[[0., 0., 0., 1.],
[0., 0., 2., 3.],
[0., 2., 0., 3.],
[4., 6., 6., 9.]]]], grad_fn=<SlowConvTranspose2DBackward0>)
True