☰
转置卷积原理详解:输出尺寸计算与PyTorch实战
2026/10/14 13:12:45 网站建设 项目流程

1. 转置卷积到底在做什么:从一次误用说起

很多人第一次接触转置卷积,是在做语义分割或者图像生成任务的时候。当时我手里有一个特征图,尺寸被下采样得很小,想把它恢复到原图分辨率,第一反应就是去找一个“能把图像变大的层”。翻文档看到ConvTranspose2d,参数长得跟普通卷积差不多,就直接拿来用了。结果训练倒是能跑,但生成出来的图像总带着一种奇怪的棋盘格纹理,边界处一格一格的,像马赛克没铺平。后来花了不少时间才搞明白,问题不在数据,也不在损失函数,而在于我对转置卷积的 stride 和 padding 理解是错的。

转置卷积这个名字本身就容易让人误解。它不叫“反卷积”是有原因的,因为它并不是卷积的逆运算。卷积操作在数学上是一个多对一的映射,多个输入位置会贡献到同一个输出位置,这个过程本身是不可逆的,你没法从卷积结果精确还原出原始输入。转置卷积做的事情,是构造一个与卷积“形状上对称”的操作:如果普通卷积把特征图从大变小,转置卷积就把它从小变大。它复用的是同一套权重连接关系,只是方向反过来走。

这篇文章我想把转置卷积这件事从头到尾拆一遍。核心会围绕三个问题展开:转置卷积的输出尺寸到底怎么算,stride 和 padding 在其中扮演什么角色,以及为什么它不该被叫做反卷积。适合已经会用 PyTorch 搭卷积网络、但对转置卷积内部机制还模模糊糊的人。读完之后,你应该能自己推导任意参数下的输出尺寸,也能判断什么时候该用它、什么时候该换别的上采样方式。

2. 转置卷积的核心原理与名称辨析

2.1 为什么它不叫反卷积:从数学本质说起

先把名称这件事说清楚,因为很多 confusion 都来自这里。普通卷积操作,假设输入是 4x4,卷积核 3x3,stride 为 1,不加 padding,输出是 2x2。这个过程可以写成一个矩阵乘法:把输入展平成 16 维向量,输出展平成 4 维向量,中间乘一个 4x16 的稀疏矩阵,这个矩阵的每一行对应一个输出位置,行里的非零元素就是卷积核权重。

那这个 4x16 矩阵的转置是 16x4,用它去乘一个 4 维向量,会得到一个 16 维向量。这就是转置卷积的矩阵形式。注意,这里用的是“转置”,不是“逆”。矩阵的逆要求矩阵是方阵且可逆,而卷积矩阵是长方形的,根本不存在传统意义上的逆。所以转置卷积只是把连接关系转置了,权重值还是原来那些,并没有求逆。

这就解释了为什么它不能精确还原卷积的输入。你拿卷积的输出做转置卷积,得到的不是原来的输入,而是一个和原输入同形状、但数值完全不同的东西。它更像是一种“按照卷积的连接模式反向传播梯度”的操作。事实上,在反向传播里,卷积层求输入梯度时用的正是转置卷积。所以转置卷积在框架里的正式名字是 transposed convolution,有些地方也叫 fractionally strided convolution,但绝不叫 deconvolution。你在论文里看到 deconvolution,除非是信号处理领域的特定语境,否则多半是作者用词不严谨。

2.2 从矩阵乘法理解转置卷积的形状变化

理解了矩阵视角,输出尺寸就好推了。普通卷积的输出尺寸公式是:

out = floor((in + 2*padding - kernel_size) / stride) + 1

转置卷积要做的是把这个关系反过来。已知转置卷积的输入是 in,想求输出 out,那么把上面的公式里的 out 和 in 对调,再解出来:

out = (in - 1) * stride - 2 * padding + kernel_size

这个公式是转置卷积输出尺寸的核心。注意这里没有 floor,因为转置卷积是“放大”操作,它会把每个输入位置按照 stride 展开,再叠加卷积核的贡献。如果算出来带小数,说明参数组合不太合理,PyTorch 会向下取整或者报错,具体取决于版本和参数。

我拿几个具体数字走一遍。假设 in=2,kernel_size=3,stride=1,padding=0,那么 out = (2-1)*1 - 0 + 3 = 4。这正好对应前面 4x4 卷积到 2x2 的逆过程形状。如果 stride=2,padding=0,in=2,kernel=3,那么 out = (2-1)*2 - 0 + 3 = 5。也就是说,stride 为 2 的转置卷积会把每个输入点之间拉开一个空位,再补上卷积核,输出比简单翻倍还要大一点。

这里有个容易踩的坑:很多人以为 stride=2 的转置卷积就是把尺寸精确翻倍。实际上只有当 kernel_size = 2*stride 且 padding = kernel_size/2 - 1 这种特定组合下,输出才刚好是输入的整数倍。比如 kernel=4,stride=2,padding=1,in=2,out = (2-1)*2 - 2 + 4 = 4,刚好翻倍。这也是为什么很多生成网络里用 kernel=4、stride=2、padding=1 这组参数,就是为了让上采样倍数干净利落。

2.3 stride 与 padding 在转置卷积里的真实含义

普通卷积里,stride 是卷积核滑动的步长,padding 是在输入边缘补零的圈数。到了转置卷积,这两个参数的含义变得有点绕,但如果你从“反向传播”的角度看就清楚了。

转置卷积的 stride,控制的是输出中相邻输入位置之间的间隔。stride=1 时,每个输入位置对应的输出区域是紧挨着的;stride=2 时,输出里会插入空洞,相当于把输入“撑开”了再卷积。你可以这样想象:先把输入每个元素之间插入 stride-1 个零,然后在外面补 padding 圈零,最后用一个翻转过的卷积核做普通卷积。这个“插零再卷积”的视角,是理解转置卷积最直观的方式。

padding 在转置卷积里起的是“裁剪”作用。因为插零之后特征图变大了,卷积完会更大,padding 决定从输出边缘裁掉多少。所以 padding 越大,输出越小。这跟普通卷积里 padding 越大输出越大的直觉是相反的,第一次接触很容易搞反。

我整理了一个对照表,把常见参数组合下的输出尺寸列出来,方便你直接查:

输入尺寸kernelstridepadding输出尺寸
23104
23205
24214
44218
842116
33216
35227

从表里能看出来,kernel=4、stride=2、padding=1 这组参数在输入为偶数时,输出正好是输入的两倍,非常规整。这也是它在实践中出场率最高的原因。

3. PyTorch 里的转置卷积层实操

3.1 ConvTranspose2d 的参数逐个拆解

PyTorch 里转置卷积层的接口是torch.nn.ConvTranspose2d,签名和Conv2d很像:

nn.ConvTranspose2d( in_channels, out_channels, kernel_size, stride=1, padding=0, output_padding=0, groups=1, bias=True, dilation=1, padding_mode='zeros' )

前三个参数和普通卷积一样,分别是输入通道、输出通道、卷积核大小。重点说后面几个容易出问题的。

stride和padding前面已经讲过,它们共同决定输出尺寸。output_padding是一个比较特殊的存在,它不改变卷积核的覆盖方式,只是在输出的一侧额外补一些像素,用来解决输出尺寸的歧义。举个例子,当 stride=2、kernel=4、padding=1 时,输入 3 会得到输出 6,输入 4 得到输出 8,都是整数倍。但如果输入是 5,公式算出来 out = (5-1)*2 - 2 + 4 = 8,而 5 的两倍是 10,差了 2。这时候如果你希望输出严格是输入的两倍,就可以设 output_padding=1,让输出变成 9 或者 10,具体补在哪一侧由框架决定。这个参数在需要精确控制输出尺寸的场合很有用,比如 U-Net 的 decoder 要和 encoder 的特征图做 skip connection 拼接时,尺寸必须对齐。

dilation是空洞卷积的膨胀率,转置卷积也支持,但实践中用得少,因为转置卷积本身已经够复杂了,再叠 dilation 容易把尺寸算乱。groups是分组卷积,转置卷积同样支持,但一般只在特定架构里出现。

3.2 手算一遍输出尺寸:从公式到代码验证

光看公式容易忘,我习惯用代码验证一遍。下面这段可以直接跑:

import torch import torch.nn as nn # 定义一个转置卷积层 conv_t = nn.ConvTranspose2d( in_channels=3, out_channels=16, kernel_size=4, stride=2, padding=1 ) # 输入一个 3x8x8 的特征图 x = torch.randn(1, 3, 8, 8) y = conv_t(x) print(y.shape) # 输出应该是 1x16x16

跑出来是torch.Size([1, 16, 16]),和公式算的一致:(8-1)2 - 21 + 4 = 16。你可以把输入改成 7x7 试试,输出会变成 14x14,仍然是两倍。但如果输入是 5x5,输出是 10x10,也是两倍。等等,前面说 5 的两倍是 10,公式算出来是 8,怎么这里又对了?因为 kernel=4、stride=2、padding=1 这组参数下,公式是 out = (in-1)2 - 2 + 4 = 2in,恒等于两倍。我前面举的 5 的例子算错了,实际是 (5-1)*2 - 2 + 4 = 8+2 = 10。所以这组参数确实能保证输出永远是输入的两倍,不管输入是奇数还是偶数。这就是它好用的地方。

再试一组不那么规整的:kernel=3,stride=2,padding=1,输入 5x5。公式给出 out = (5-1)*2 - 2 + 3 = 9。代码验证:

conv_t2 = nn.ConvTranspose2d(3, 16, kernel_size=3, stride=2, padding=1) x2 = torch.randn(1, 3, 5, 5) print(conv_t2(x2).shape) # torch.Size([1, 16, 9, 9])

输出 9x9,不是 10x10。如果你希望它是 10x10,就得加 output_padding=1。这个细节在做特征图拼接时特别关键,尺寸差一个像素,torch.cat就会直接报错。

3.3 权重初始化与棋盘格效应的关系

转置卷积的权重初始化比普通卷积更敏感。普通卷积里,如果初始化不好,最多是收敛慢一点。转置卷积如果初始化不当,很容易在输出里产生周期性的棋盘格纹理。这个现象的根源在于:当 stride 大于 1 时,输出中不同位置被卷积核覆盖的次数不一样。有些位置被覆盖多次,有些只覆盖一次,如果权重没有经过良好校准,这些覆盖次数的差异就会在输出上表现为亮度不均的格子。

解决办法有几个。最直接的是用 kernel_size 能被 stride 整除的组合,比如 kernel=4、stride=2,这样每个输出位置被覆盖的次数是均匀的。另一个办法是初始化时用较小的方差,PyTorch 默认的初始化对转置卷积来说往往偏大。我通常会用nn.init.kaiming_normal_手动初始化,或者干脆在转置卷积后面接一个普通卷积来平滑输出。还有一种做法是用双线性插值上采样加普通卷积替代转置卷积,这样完全避开棋盘格问题,但会损失一些可学习的灵活性。

注意:如果你在训练生成网络时发现输出有规律的网格纹理,先别急着调损失函数,检查一下转置卷积的 kernel 和 stride 是否匹配。这是最常见的原因。

4. 转置卷积的典型应用场景与替代方案

4.1 语义分割里的上采样解码器

语义分割任务里,encoder 通过卷积和池化不断下采样,提取语义特征,但最终输出需要和原图一样大,给每个像素分类。decoder 就要把低分辨率特征图逐步放大。转置卷积在这里是标准配置之一。它的好处是权重可学习,网络能自己决定怎么放大最有利于分割。相比简单的双线性插值,转置卷积多了参数,表达能力更强。

但转置卷积在分割里也有争议。有些工作指出,转置卷积的上采样容易在边界处产生伪影,尤其是当类别边界比较精细时。所以后来很多架构改用“插值上采样 + 普通卷积”的组合,先用双线性插值把尺寸放大,再用 3x3 卷积融合特征。这样既避免了棋盘格,又保留了可学习的参数。我在实际项目里两种都用过,转置卷积在训练数据充足时表现更好,插值方案在小数据集上更稳。

4.2 生成网络里的尺寸放大

生成对抗网络和变分自编码器的 decoder 部分,经常需要从一个小尺寸的 latent 向量逐步生成大图。转置卷积是这里的常客。比如从一个 1x1 或者 4x4 的向量,经过几层转置卷积,变成 64x64 或 256x256 的图像。每一层通常把尺寸翻倍,通道数减半,直到变成 3 通道的 RGB 图。

这里有个经验:生成网络里用转置卷积时,kernel 选 4、stride 选 2、padding 选 1 是最稳的。这组参数保证输出严格翻倍,而且每个输出位置被均匀覆盖,不容易出棋盘格。如果你用 kernel=3、stride=2,输出尺寸是 2*in - 1,不是整数倍,后面拼接或者计算损失时容易出问题。

4.3 什么时候不该用转置卷积

转置卷积不是万能的。如果你的任务只是单纯放大图像,不涉及学习,那双线性插值或者最近邻插值就够了,还更快。如果放大倍数很大,比如从 8x8 直接到 256x256,一层转置卷积的 kernel 要设得很大,参数量爆炸,这时候应该用多层逐步放大,每层翻倍。如果对输出尺寸的精确性要求极高,比如要做像素级对齐,那转置卷积的 output_padding 机制虽然能调,但容易出错,不如用插值加卷积来得直接。

还有一个场景要避开转置卷积:当你的输入特征图里有大量零值或者稀疏区域时,转置卷积会把零也放大,输出里会出现大块空白。这时候插值方法反而更自然。

5. 常见问题与排查技巧实录

5.1 输出尺寸对不上怎么办

这是最高频的问题。你搭了一个 encoder-decoder,想把 encoder 某一层的输出和 decoder 某一层的输出拼接,结果torch.cat报错,说尺寸不匹配。排查步骤是这样的:

第一步,把 encoder 和 decoder 每一层的输入输出尺寸都打印出来。不要凭感觉算,直接跑一遍前向传播,在每一层后面加 print。

第二步,找到尺寸不匹配的那一层,用公式反推。转置卷积的输出公式是 out = (in-1)stride - 2padding + kernel + output_padding。把已知的 in、stride、padding、kernel 代进去,看算出来的 out 和你期望的差多少。

第三步,如果差 1 个像素,优先调 output_padding。如果差得多,说明 stride 或者 kernel 选错了,需要重新设计这一层的参数。

我整理了一个速查表,把常见的不匹配情况和对应解法列出来:

现象可能原因解法
输出比期望小 1output_padding 没设设 output_padding=1
输出比期望小很多stride 或 kernel 不匹配重新选 kernel=2*stride
输出尺寸是奇数,期望偶数输入尺寸是奇数调整输入或加 output_padding
拼接时报通道数不对通道数没对齐检查 out_channels 设置

5.2 棋盘格效应的排查与消除

棋盘格效应表现为输出图像上有规律的明暗格子,周期和 stride 相关。排查方法很简单:用一张全 1 的输入跑一遍转置卷积,看输出是不是均匀的。如果输出有起伏,那就是棋盘格。

消除办法按优先级排:第一,换 kernel=4、stride=2、padding=1 的组合;第二,如果换不了,在转置卷积后面接一个 3x3 普通卷积,让卷积来平滑那些不均匀;第三,改用插值上采样加卷积;第四,调整权重初始化,用更小的方差。

我个人的经验是,第一种办法能解决八成以上的棋盘格问题。剩下两成里,大部分是权重初始化的问题,少数是数据本身就有周期性模式,被转置卷积放大了。

5.3 转置卷积和普通卷积的参数对照

很多人学转置卷积时,脑子里没有一张清晰的对照表,导致参数总是设错。我把两者的关键区别整理如下:

维度普通卷积 Conv2d转置卷积 ConvTranspose2d
尺寸变化通常变小通常变大
stride 含义卷积核滑动步长输出中插入的空洞间隔
padding 含义输入边缘补零输出边缘裁剪
输出公式floor((in+2p-k)/s)+1(in-1)*s-2p+k+op
权重共享是是
是否可逆否否
常见用途特征提取上采样、生成

这张表建议存下来,每次设参数时对照看一眼,能省很多调试时间。

5.4 一个容易忽略的细节:bias 的影响

转置卷积的 bias 默认是开的。在大多数情况下这没问题,但如果你在转置卷积后面接了 BatchNorm,bias 就是多余的,因为 BatchNorm 会减掉均值。这时候应该设 bias=False,减少一点参数量,也避免不必要的数值干扰。这个细节很小,但在复现论文时,如果论文里明确说了 no bias,你忘了设,结果可能会有细微差异。

另外,当 output_padding 大于 0 时,bias 会加在补出来的那些像素上,可能导致输出边缘的数值分布和中间不一致。如果对边缘质量敏感,可以考虑关掉 bias,用后续的归一化层来补偿。

6. 从零实现一个转置卷积的完整案例

6.1 任务设定与网络结构设计

我拿一个简单的图像重建任务来演示。输入是一张 32x32 的灰度图,先经过三层普通卷积下采样到 4x4,再经过三层转置卷积上采样回 32x32,中间不加池化,用 stride=2 的卷积代替下采样。这个结构很像一个小型 autoencoder,适合用来观察转置卷积的行为。

网络结构如下:

  • encoder:Conv2d(1, 16, 3, stride=2, padding=1) → ReLU → Conv2d(16, 32, 3, stride=2, padding=1) → ReLU → Conv2d(32, 64, 3, stride=2, padding=1) → ReLU
  • decoder:ConvTranspose2d(64, 32, 4, stride=2, padding=1) → ReLU → ConvTranspose2d(32, 16, 4, stride=2, padding=1) → ReLU → ConvTranspose2d(16, 1, 4, stride=2, padding=1) → Sigmoid

注意 encoder 里每次 stride=2 的卷积,输出尺寸是 floor((32+2-3)/2)+1 = 16,再 8,再 4。decoder 里每次转置卷积,输出尺寸是 (4-1)*2-2+4 = 8,再 16,再 32。尺寸完全对齐,不需要 output_padding。

6.2 关键代码与尺寸验证

import torch import torch.nn as nn class SmallAutoencoder(nn.Module): def __init__(self): super().__init__() self.encoder = nn.Sequential( nn.Conv2d(1, 16, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(16, 32, 3, stride=2, padding=1), nn.ReLU(), nn.Conv2d(32, 64, 3, stride=2, padding=1), nn.ReLU() ) self.decoder = nn.Sequential( nn.ConvTranspose2d(64, 32, 4, stride=2, padding=1), nn.ReLU(), nn.ConvTranspose2d(32, 16, 4, stride=2, padding=1), nn.ReLU(), nn.ConvTranspose2d(16, 1, 4, stride=2, padding=1), nn.Sigmoid() ) def forward(self, x): z = self.encoder(x) return self.decoder(z) model = SmallAutoencoder() x = torch.randn(1, 1, 32, 32) y = model(x) print(y.shape) # torch.Size([1, 1, 32, 32])

跑出来尺寸完全一致。你可以把每一层的输出打印出来,确认 encoder 是 32→16→8→4,decoder 是 4→8→16→32。这个验证步骤不能省,尤其是当你改参数的时候,跑一遍比手算快得多。

6.3 训练中的观察与调参经验

这个网络用 MSE 损失训练,优化器用 Adam,学习率 1e-3。训练几十个 epoch 后,重建图像会逐渐清晰。我在训练过程中观察到几个现象:

第一,如果 decoder 的转置卷积用默认初始化,前几个 epoch 的输出会有明显的棋盘格,随着训练进行逐渐减弱,但不会完全消失。换成 kernel=4、stride=2、padding=1 后,棋盘格从一开始就很轻微。

第二,decoder 最后一层用 Sigmoid 是必要的,因为输入图像归一化到了 [0,1]。如果不用 Sigmoid,输出会有负值,MSE 损失会很大,收敛慢。

第三,学习率不能太大。转置卷积的梯度比普通卷积更容易爆炸,尤其是当 stride 大于 1 时。我试过 1e-2 的学习率,损失直接飞了。1e-3 比较稳,1e-4 收敛太慢。

第四,batch size 对棋盘格有影响。batch 太小的时候,BatchNorm 的统计量不准,棋盘格更明显。如果用了 BatchNorm,batch size 至少 16 以上。

提示:如果你在训练中发现损失突然变成 NaN,先检查转置卷积的权重有没有出现极端值。转置卷积的梯度计算涉及大量重叠区域的累加,数值稳定性比普通卷积差一些。

7. 转置卷积的扩展思考与个人体会

转置卷积这个操作,刚接触时觉得别扭,用熟了之后会发现它其实很优雅。它把卷积的连接关系反过来用,不引入新的权重概念,却能实现上采样。这种“复用”的思路在深度学习里很常见,比如循环神经网络里的权重共享、注意力机制里的键值复用,本质上都是同一套参数在不同方向上发挥作用。

我在实际项目里用转置卷积最多的场景是图像修复和超分辨率。这两个任务都需要从低分辨率或者破损的图像恢复出高分辨率结果,转置卷积的可学习上采样比固定插值更有优势。但我也踩过不少坑,最大的一个是在做多尺度特征融合时,不同尺度的转置卷积输出尺寸没对齐,导致拼接失败。后来我养成了一个习惯:每加一个转置卷积层,先写一行代码验证输出尺寸,确认无误再往下搭。这个习惯帮我省了很多调试时间。

还有一个体会是,转置卷积的参数不要设得太随意。kernel、stride、padding 这三个数不是独立的,它们共同决定输出尺寸和覆盖均匀性。最稳的组合是 kernel=2*stride、padding=kernel/2-1,这样输出严格翻倍,覆盖也均匀。如果你看到有人用 kernel=3、stride=2,那多半是没仔细算过尺寸,后面大概率要加 output_padding 来补救。

最后分享一个小技巧:如果你想快速判断一个转置卷积层会不会产生棋盘格,可以用全 1 输入跑一遍,然后看输出的均值和方差。如果方差接近 0,说明覆盖均匀,不会有棋盘格;如果方差明显大于 0,那就要小心了。这个方法比肉眼观察输出图像快得多,也更客观。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询