转置卷积核(Transpose Convolution Kernel)是深度学习中用于进行反卷积操作的核心组件之一。虽然有时也被称为 “逆卷积”,但实际上它用于执行上采样操作,而不是数学上的卷积的逆运算。
在深度学习中,转置卷积核常用于图像分割、图像重建、生成对抗网络(GAN)等任务中。它的作用是将输入特征图放大,通常在图像生成过程中用于从低分辨率图像生成高分辨率图像。
再次强调:转置卷积操作并不是真正的卷积的逆运算,因为信息的损失是不可逆的。逆卷积核的权重在训练过程中需要学习,类似于正常卷积操作中的卷积核。
参考资料:
1. 转置卷积参数
在 PyTorch 中,我们可以使用以下 API 进行转置卷积计算:
nn.ConvTranspose2d( in_channels: int, out_channels: int, kernel_size: _size_2_t, stride: _size_2_t = 1, padding: _size_2_t = 0, output_padding: _size_2_t = 0, groups: int = 1, bias: bool = True, dilation: _size_2_t = 1, padding_mode: str = 'zeros', device=None, dtype=None )
参数含义如下:
- in_channels 表示输入图像的通道数
- out_channels 表示输出图像的通道数
- kernel_size 表示每个通道卷积核大小
- stride 表示每个像素点之间填充多少 s-1 个 0
- padding 表示在特征图周围填充 k-p-1 行或列
- bias 表示卷积核的偏置参数是否需要
- padding_mode 在输入数据周围填充的数据,默认填充0
out_channels 也表示包含多少个卷积核,in_channels 表示每个卷积核中包含多少个 kernel_size 大小的卷积核参数。例如:in_channels=5,kernel_size=3,则表示每个卷积核的尺寸为 (5, 3, 3),即:5 个 3×3 的矩阵。
转置卷积计算的过程如下:
- 首先,在输入图像每个像素之间填充 s-1 行或列 0
- 其次,在输入图像周围填充 k-p-1 行或列 0
- 然后,将卷积核参数上下左右翻转
- 最后,使用卷积核对填充后的图像进行正常的卷积操作
需要注意的是,进行转置卷积操作时,padding 和 stride 只参与对输入图像的填充处理,后续只需要对处理后的图像进行正常的步长为 1 的卷积操作。
2. padding 实验
在进行转置卷积操作时,padding 只会影响对输入图像周围的填充。填充行列数为:k – p – 1。接下来我们做实验来检验。
为了能够理解计算过程,我们这里使用 F.conv_transpose2d 函数,并使用固定的输入和卷积核参数,示例代码如下(p=0, k=3):
import torch import torch.nn as nn import torch.nn.functional as F def test01(): p = 0 k = 3 # 固定随机数 torch.manual_seed(0) # 输入数据 inputs = torch.randint(1, 10, size=(1, 1, 3, 3), dtype=torch.float32) # 卷积核参数: (每个卷积核的通道数,输出通道数量,卷积核宽高) weight = torch.randint(1, 5, size=(1, 1, 3, 3), dtype=torch.float32) # 偏置形状要和输出通道数一样 bias = torch.zeros(size=(1,), dtype=torch.float32) outputs = F.conv_transpose2d(inputs, weight, bias, padding=p) print('输出结果:\n', outputs) # 分解动作 def test02(): p = 0 k = 3 # 固定随机数 torch.manual_seed(0) # 输入数据 inputs = torch.randint(1, 10, size=(1, 1, 3, 3), dtype=torch.float32) weight = torch.randint(1, 5, size=(1, 1, k, k), dtype=torch.float32) # 1. 在输入张量的左、右、上、下填充 n 行列 0 inputs = F.pad(inputs, (k - p - 1,) * 4, value=0.0) print('周围填充:\n', inputs) # 2. 卷积核参数向下左右翻转 weight = torch.flip(weight, dims=[2, 3]) print('参数翻转:\n', weight) # 3. 正常进行卷积操作 outputs = F.conv2d(inputs, weight=weight, padding=0, stride=1) print('输出结果:\n', outputs) if __name__ == '__main__': test01() print('-' * 50) test02()
程序输出结果:
输出结果: tensor([[[[ 36., 22., 41., 9., 9.], [ 37., 83., 99., 53., 30.], [ 48., 70., 109., 66., 30.], [ 15., 49., 56., 29., 13.], [ 8., 10., 12., 4., 2.]]]]) -------------------------------------------------- 周围填充: tensor([[[[0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0.], [0., 0., 9., 1., 3., 0., 0.], [0., 0., 7., 8., 7., 0., 0.], [0., 0., 8., 2., 2., 0., 0.], [0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0.]]]]) 参数翻转: tensor([[[[1., 1., 1.], [3., 4., 1.], [3., 2., 4.]]]]) 输出结果: tensor([[[[ 36., 22., 41., 9., 9.], [ 37., 83., 99., 53., 30.], [ 48., 70., 109., 66., 30.], [ 15., 49., 56., 29., 13.], [ 8., 10., 12., 4., 2.]]]])
3. stride 实验
stride 表示每个像素点之间填充多少 s-1 个 0。
import torch import torch.nn as nn import torch.nn.functional as F def test01(): s = 2 p = 0 k = 3 # 固定随机数 torch.manual_seed(0) # 输入数据 inputs = torch.randint(1, 10, size=(1, 1, 3, 3), dtype=torch.float32) # 卷积核参数: (每个卷积核的通道数,输出通道数量,卷积核宽高) weight = torch.randint(1, 5, size=(1, 1, 3, 3), dtype=torch.float32) # 偏置形状要和输出通道数一样 bias = torch.zeros(size=(1,), dtype=torch.float32) outputs = F.conv_transpose2d(inputs, weight, bias, padding=p, stride=s) print('输出结果:\n', outputs) # 分解动作 def test02(): s = 2 p = 0 k = 3 # 固定随机数 torch.manual_seed(0) # 输入数据 inputs = torch.randint(1, 10, size=(1, 1, 3, 3), dtype=torch.float32) weight = torch.randint(1, 5, size=(1, 1, k, k), dtype=torch.float32) # 1. 元素之间填充0 new_inputs = torch.zeros(1, 1, 5, 5) new_inputs[:, :, ::s, ::s] = inputs print('元素填充:\n', new_inputs) # 2. 矩阵周围填充0 inputs = F.pad(new_inputs, (k - p - 1,) * 4, value=0.0) print('周围填充:\n', inputs) # 3. 卷积核参数向下左右翻转 weight = torch.flip(weight, dims=[2, 3]) print('参数翻转:\n', weight) # 4. 正常进行卷积操作 outputs = F.conv2d(inputs, weight=weight, padding=0, stride=1) print('输出结果:\n', outputs) if __name__ == '__main__': test01() print('-' * 50) test02()
程序输出结果:
输出结果: tensor([[[[36., 18., 31., 2., 15., 6., 9.], [ 9., 36., 28., 4., 6., 12., 9.], [37., 23., 63., 17., 56., 17., 24.], [ 7., 28., 29., 32., 31., 28., 21.], [39., 23., 47., 12., 29., 11., 13.], [ 8., 32., 26., 8., 8., 8., 6.], [ 8., 8., 10., 2., 4., 2., 2.]]]]) -------------------------------------------------- 元素填充: tensor([[[[9., 0., 1., 0., 3.], [0., 0., 0., 0., 0.], [7., 0., 8., 0., 7.], [0., 0., 0., 0., 0.], [8., 0., 2., 0., 2.]]]]) 周围填充: tensor([[[[0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 9., 0., 1., 0., 3., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 7., 0., 8., 0., 7., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 8., 0., 2., 0., 2., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.], [0., 0., 0., 0., 0., 0., 0., 0., 0.]]]]) 参数翻转: tensor([[[[1., 1., 1.], [3., 4., 1.], [3., 2., 4.]]]]) 输出结果: tensor([[[[36., 18., 31., 2., 15., 6., 9.], [ 9., 36., 28., 4., 6., 12., 9.], [37., 23., 63., 17., 56., 17., 24.], [ 7., 28., 29., 32., 31., 28., 21.], [39., 23., 47., 12., 29., 11., 13.], [ 8., 32., 26., 8., 8., 8., 6.], [ 8., 8., 10., 2., 4., 2., 2.]]]])