☰
转置卷积尺寸计算:stride、padding与output_padding避坑
2026/10/28 14:48:58 网站建设 项目流程

转置卷积这个算子,我见过太多人第一次上手就卡在形状报错上——明明公式在论文里看着挺简单,真落到代码里,stride=2, padding=1到底该配多大的output_padding,输出是 2 倍还是 2 倍减一,很多时候全靠试。我自己最早做语义分割的 decoder 时,就是靠反复 print shape 硬凑出来的,直到把尺寸公式从头推了一遍,才算真正搞明白这三个参数各自在管什么。这篇内容就围绕转置卷积(ConvTranspose)的尺寸计算展开,把stride、padding、output_padding的作用机制、通用公式推导、参数反推方法、以及实际落地时的踩坑记录一次讲透。

适合谁来读:写过 CNN、见过ConvTranspose2d但没细究过形状逻辑的同学;正在做超分辨率、语义分割、生成模型 decoder、或者任何需要上采样的模块,被尺寸对不上折磨过的工程师;以及想从梯度角度理解“转置”二字到底指什么的读者。文中给的公式、代码和配置表都可以直接抄,验证脚本跑一遍,你对这三个参数的直觉基本就建立了。

1. 先把概念摆正:转置卷积到底在算什么

1.1 从卷积的反向传播说起

很多人对转置卷积的第一印象是“卷积的逆运算”,这个理解方向对了一半,但容易带偏。严格讲,转置卷积并不是卷积的数学逆,它算的是卷积操作对输入求导时的那一步线性变换。想清楚这件事,后面所有尺寸问题都会变得顺理成章。

设想一个普通卷积层,输入x形状是 4×4,卷积核 3×3,步长 1,无填充,输出y是 2×2。前向计算可以写成一个矩阵乘法y = W·x,其中W是一个 4×16 的稀疏矩阵,由卷积核的元素按位置排布而成。反向传播时,我们要把上游传来的梯度g(形状 2×2,拉平成 4 维)映射回输入的梯度空间,用的就是W的转置Wᵀ,乘出来是一个 16 维向量,reshape 回 4×4。

转置卷积这个名字,直接来源就是这里的Wᵀ。它的参数矩阵正是普通卷积参数矩阵的转置,所以形状上是“反过来”的:小图进、大图出。但它不是精确逆运算,因为Wᵀ·W一般不等于单位阵,信息在卷积过程中已经丢了。这一点非常关键——它解释了为什么输出尺寸不是简单地把输入尺寸乘回去,而是需要output_padding这种“补丁参数”来消除多义性。

1.2 为什么叫“转置”而不是“反卷积”

行业里还有个更唬人的叫法——反卷积(Deconvolution)。这个叫法在数学上其实是不严谨的,反卷积指的是已知卷积结果反推原始输入,属于病态问题,需要额外的正则约束,跟神经网络里的转置卷积不是一回事。但它流传太广,很多老论文和老代码里都这么写,导致新人一搜就被带偏。

我更愿意把它理解成一种带可学习权重的上采样方式:先在输入元素之间插入零,再用一个普通卷积核去做滑动窗口,最后按需裁剪边缘。这个视角的好处是,你不需要任何矩阵推导就能算清楚输出尺寸,只要数一数“插零之后序列有多长、核滑了几次”就行。下一节的公式推导我用的就是这个思路,工程上更实用。

顺便说一句,如果你看到某个框架里写作deconv、upconv、fractionally-strided conv,基本都指同一个东西。名字混乱是这个算子最劝退的地方之一,先把名分理清楚,后面看文档就不会迷糊。

1.3 它和插值上采样、PixelShuffle 的分工

转置卷积不是唯一的上采样方案,我把它和另外两个常见做法的差别列一下,方便你在项目里做选型判断:

方案是否可学习典型放大倍数主要问题
最近邻 / 双线性插值否任意无参数,表达力弱,边缘偏平滑
转置卷积是2/4/8 等整数倍核不能被步长整除时易出棋盘格
亚像素卷积(PixelShuffle)是整数倍(通道换空间)通道数要乘 r²,显存占用高

转置卷积的优势是端到端可学习,且参数少:一个 4×4 的核,2 倍上采样只需要 16 个权重(不含偏置),比“插值 + 普通卷积”的组合省一些参数。它的短板也很明显,就是输出像素之间的重叠不均匀,容易产生规律性的网格伪影,这个在第 4 节会详细展开。

2. 尺寸公式推导:stride 与 padding 各自管什么

2.1 一维手推:从 1 个元素到 3 个元素

先抛开二维和 batch 维度,用一维序列把逻辑走一遍,这是我最推荐的理解路径。假设输入只有 1 个元素(长度 1),卷积核长度 3,步长 1,填充 0。按“插零 + 滑窗”的思路:输入长度 1,步长 1 意味着元素之间不插零,序列还是长度 1;再在两侧各补k-1-p = 3-1-0 = 2个零,序列变成长度 5;用长度 3 的核做 valid 卷积,输出长度是5 - 3 + 1 = 3。所以 1 个元素变成了 3 个元素,这是转置卷积最朴素的形态。

把输入换成 2 个元素,步长仍是 1,核 3,填充 0:序列长度 2,两侧各补 2 个零变成 6,valid 卷积输出6 - 3 + 1 = 4。规律浮现了:步长为 1 时,每增加一个输入元素,输出只增加 1 个,因为卷积核在滑动时是连续覆盖的,相邻位置的输出高度重叠。

真正产生“放大”效果的是步长大于 1 的情况。取输入长度 2,步长 2,核 3,填充 0:先在两个元素之间插入s-1 = 1个零,序列变成[x0, 0, x1],长度 3;两侧各补k-1-p = 2个零,长度 7;valid 卷积输出7 - 3 + 1 = 5。也就是说,输入长度 2、步长 2 的情况下,输出是 5,接近翻倍。这就是“转置卷积能放大”的物理来源:放大倍数由步长决定,而输出的精确长度由核大小和填充共同微调。

2.2 通用公式与“等价步长”视角

把上面的推导一般化,插零后的序列长度是(in - 1) × s + 1,两侧补零后是(in - 1) × s + 1 + 2 × (k - 1 - p),valid 卷积输出就是再减去k加1:

out = (in - 1) × s + 1 + 2(k - 1 - p) - k + 1 = (in - 1) × s + k - 2p

加入空洞率d后,等效核长度变成d × (k - 1) + 1,把它替换进上式,再补上output_padding,就得到框架文档里那个标准式子:

out = (in − 1) × stride − 2 × padding + dilation × (kernel_size − 1) + output_padding + 1

这条式子和普通卷积的尺寸公式长得像,但含义完全不同。普通卷积里stride是“跳着取”,输出是缩小的;转置卷积里stride是“插零间隔”,输出是放大的。我在笔记里给它起了个名字叫等价步长:你可以把转置卷积近似看成“先做 1/s 步长的卷积”,所以它有时被叫作分数步长卷积。

注意:dilation > 1时不要用“核大小 × 步长”这种朴素直觉估算,必须套完整公式,空洞率会让等效核长度快速膨胀。

2.3 output_padding:专治 floor 丢像素

为什么会有output_padding这个反直觉的参数?根源在于普通卷积的尺寸计算里有向下取整。普通卷积的输出是:

out_conv = floor((in + 2p − d(k−1) − 1) / s) + 1

那个floor意味着多解:不同的输入尺寸经过同样的卷积,可能得到同一个输出尺寸。反过来做转置卷积时,你就无法从输出尺寸唯一反推出输入尺寸,差的那一个像素就是“丢失的余数”。

举个具体例子。输入 6,核 3,步长 2,填充 1,走普通卷积:floor((6 + 2 − 3)/2) + 1 = floor(2.5) + 1 = 3。现在拿 3 做输入,用同样的核、步长、填充做转置卷积,套公式:(3−1)×2 − 2 + 3 − 1 + 0 + 1 = 5。想恢复到 6,还差 1,把output_padding设为 1,结果正好是 6。这就是它的全部职责:把 floor 扔掉的那个余数补回来。

有个约束必须记住:output_padding必须小于stride或dilation中的至少一个,否则框架直接报错。原理也好理解——如果允许它大于等于步长,就等价于整块多出一圈,那不如直接改padding或kernel_size了。实际使用中,output_padding的取值几乎永远是 0 或stride - 1,中间值很少出现。

3. 参数组合实操:从公式到代码验证

3.1 写一个自动对拍脚本

光看公式容易记混,我习惯用一个脚本把公式结果和框架实际输出逐一对拍,跑一遍比看十遍文档管用。核心思路是把四元组参数穷举一遍,比较解析值和真实张量形状,任何不一致都直接打印出来。

import itertools import torch import torch.nn as nn def formula(inp, k, s, p, op, d=1): return (inp - 1) * s - 2 * p + d * (k - 1) + op + 1 mismatch = [] for inp, k, s, p in itertools.product([1, 2, 3, 5, 7, 8], [2, 3, 4, 5], [1, 2, 3], [0, 1, 2]): for op in range(min(s, 2)): try: layer = nn.ConvTranspose2d(1, 1, k, stride=s, padding=p, output_padding=op, bias=False) x = torch.randn(1, 1, inp, inp) real = layer(x).shape[-1] expect = formula(inp, k, s, p, op) if real != expect: mismatch.append((inp, k, s, p, op, expect, real)) except RuntimeError as e: pass # 非法参数组合,跳过 print("不一致数量:", len(mismatch)) for row in mismatch[:10]: print(row)

跑完如果输出是 0,说明公式和实现完全一致,你就可以放心用它做尺寸预估了。脚本里跳过非法组合是必要的,因为output_padding ≥ stride的情况框架会抛异常,不处理的话遍历会中断。我习惯把它存成一个check_shape.py放在项目scripts/目录下,改网络结构之前先跑一遍,比调试时抓瞎强。

3.2 四组高频配置的推演

实际项目里 90% 的转置卷积配置都落在下面这几组里,我把推导过程摊开写,方便你对照自己的场景。

目标kernelstridepaddingoutput_padding输出尺寸
精确 2 倍42102×in
精确 2 倍32112×in
精确 2 倍22002×in
与 3×3 卷积对称还原3110in

以第一行为例,代入公式:(in−1)×2 − 2×1 + (4−1) + 0 + 1 = 2in − 2 − 2 + 3 + 1 = 2in,干净利落。第三行更简单:(in−1)×2 − 0 + (2−1) + 0 + 1 = 2in − 2 + 2 = 2in,这也是 U 型网络 decoder 里最常见的一种写法,参数最省。

第四行值得单独说。kernel=3, stride=1, padding=1的转置卷积,输出尺寸等于输入尺寸,看起来毫无用处,但它在特征融合路径里非常常见——用来把通道数降下来,同时保持分辨率不变,作用和 1×1 卷积类似但感受野不同。我自己在 decoder 的跳跃连接融合处就常用它,比先上采样再 1×1 卷积少一步。

3.3 从目标尺寸反推参数的步骤

更多时候我们面对的是反向问题:已知输入尺寸和目标输出尺寸,要确定参数。我的做法分三步,基本不会失手。

第一步,先定放大倍数。用round(out / in)估算,得到大致的stride。注意这一步不能用整除,因为output_padding会影响结果,必须留出余量。

第二步,选一个“好看”的核大小。工程上我优先选kernel = 2 × stride,这个组合下padding = stride / 2能凑出整倍数,且核能被步长整除,棋盘格风险最低。

第三步,解一元方程定padding和output_padding。把公式移项:

padding = ((in − 1) × s + d(k − 1) + op + 1 − out) / 2

先令op = 0代进去算padding,如果算出非整数,就把op加 1 再算一次。因为padding必须是整数,而op只能取 0 到stride−1,通常试一两次就能命中。举例:输入 13,目标输出 26,取k=4, s=2, d=1。代入op=0:padding = ((12)×2 + 3 + 1 − 26)/2 = (24 + 4 − 26)/2 = 1,整数,直接确定padding=1, op=0。

再看一个不整除的例子:输入 12,目标输出 25(这种奇数目标在恢复原始分辨率时很常见)。同样k=4, s=2,op=0时padding = (22 + 4 − 25)/2 = 0.5,不是整数;op=1时padding = (22 + 5 − 25)/2 = 1,命中。所以配padding=1, output_padding=1,输出 25 正好。

提示:反推时先固定kernel = 2 × stride,能大幅提高一次命中的概率。如果目标尺寸和输入尺寸不是整数倍关系,说明这个上采样层级本身设计得别扭,建议回头检查整体结构,而不是硬凑参数。

4. 踩坑实录与排查清单

4.1 尺寸对不上的三种典型错法

错法一:把普通卷积的公式套到转置卷积上。这是最高频的错误。普通卷积里padding越大输出越小,转置卷积里padding越大输出也越小,方向一致,但系数不同——普通卷积减2p,转置卷积也减2p,可stride的作用方向完全相反。我见过有同事在代码注释里写“stride 越大输出越小”,结果整个 decoder 的尺寸全错。

错法二:encoder 和 decoder 不对称。假设 encoder 用了kernel=3, stride=2, padding=1的普通卷积,输入 32 得到floor((32+2−3)/2)+1 = 16。decoder 如果直接用kernel=3, stride=2, padding=1, output_padding=0的转置卷积还原,算出来是(16−1)×2 − 2 + 3 − 1 + 0 + 1 = 31,少了 1 个像素。正确做法是把output_padding设成 1,或者把 encoder 的padding改成 0。这个 1 像素的差异在拼接跳跃连接时会直接报错,非常显眼但也非常容易修。

错法三:忘记dilation的影响。只要 decoder 里用了空洞卷积,核的等效长度就不是k,公式里d(k−1)那一项容易被漏掉。一次我在做多尺度感受野的模块,d=2, k=3的等效核是 5,按 3 算出来的尺寸全错了一圈。

排查顺序我固定成三步:先单独 print 每层的in/out形状,确认是哪一层开始偏的;再把这层的四个参数代入公式手算一遍;最后检查这一层的output_padding是否小于stride。九成问题在前两步就能定位。

4.2 棋盘格伪影:不只是视觉问题

转置卷积输出的规律性网格,本质上来自核覆盖不均匀。当kernel_size不能被stride整除时,某些输出位置被核覆盖的次数多,某些位置少,权重叠加的结果就会出现周期性明暗差异。这也是为什么我强调优先选kernel = 2 × stride:2×2 的核配步长 2、4×4 的核配步长 2,每个输出位置被覆盖的次数完全一致,伪影自然消失。

除了核大小,还有一个隐蔽的诱因是初始化。如果核权重初始化得不好,即使覆盖次数均匀,边界位置和内部位置的数值分布也会有系统性偏差。我的做法是在 decoder 最后一层之前加一个轻量的归一化,让分布更均匀,实测下来伪影会明显减轻。

如果结构上实在没法让核整除步长,替代方案有两个:一是换成“插值上采样 + 普通卷积”的两步走,插值负责放大、卷积负责学习,伪影基本没有,代价是多一点计算;二是用通道重排的方式把通道维度的信息搬到空间维度,完全没有重叠计算,但要求通道数能开方。这三种方案我在不同项目里都用过,选择依据主要是放大倍数是否固定、以及对推理速度的要求。

4.3 速查表与自检流程

把常见现象和对应处理整理成一张表,出问题时先查表再动手改:

现象最可能原因处理方式
形状报错,差 1 像素output_padding缺失设为stride − 1再算
形状报错,差较多核大小或步长选错用第 3.3 节反推法重算
输出出现规则网格核不能被步长整除改用kernel = 2 × stride
边界数值异常偏大填充方式与核不匹配检查padding是否超过k/2
参数量远超预期通道数在转置卷积里没降先降通道再上采样

自检流程我总结成四句话:先定倍数、再定核、后解方程、最后对拍。这四步走完,参数基本不会错。对拍那一步别省,尤其是改动了 encoder 结构之后,decoder 的参数很可能需要同步调整,人眼很难保证两次推导完全一致。

5. 工程落地的一些取舍

5.1 上采样方案选型对比

在真实项目里,转置卷积很少是唯一的选择,更多时候是和别的方案混用。我一般的判断逻辑是这样的:如果放大倍数是 2 或 4 这种整数,且对推理速度敏感,优先转置卷积;如果放大倍数不固定、需要任意的目标分辨率,插值上采样更灵活;如果显存紧张、通道数本来就很宽,通道重排方案的性价比更高。

还有一个实践细节容易被忽略:转置卷积的bias要不要开。如果后面紧跟归一化层,bias是冗余的,关掉能省一点参数,也能避免归一化前出现常数偏移。我在 decoder 里习惯把bias=False写死,除非这一层后面直接接输出。

5.2 跨框架参数对应关系

不同框架对同一套参数的定义基本一致,但命名和默认值有差别,迁移模型时容易踩坑。有的框架把output_padding叫output_padding,有的叫adj或者干脆不暴露这个参数而是让你手动裁剪输出。移植的时候我习惯先把一层的输入输出尺寸在两边各跑一遍小张量,形状对上了再继续,比读文档快。

另外注意默认值差异:dilation一般默认 1,output_padding默认 0,这两个默认值在大多数场景下是对的,但也正因为是默认值,出问题时最容易被忽略。我在排查形状问题的第一步永远是显式打印这四个参数,而不是相信默认值。

5.3 我常用的参数模板

最后给几个我反复用过、可以直接抄的配置。2 倍上采样、通道减半:kernel=4, stride=2, padding=1, output_padding=0,输入C通道输出C//2通道,尺寸精确翻倍。2 倍上采样、省参数版:kernel=2, stride=2, padding=0, output_padding=0。同分辨率特征融合:kernel=3, stride=1, padding=1, output_padding=0。奇数尺寸恢复:kernel=3, stride=2, padding=1, output_padding=1。

我个人在实际操作中的体会是,转置卷积的参数从来不是“算出来”的,而是“先定结构、再解方程、最后对拍验证”三步走出来的。公式记不住没关系,把第 2.2 节那条式子贴在代码注释里,每次改结构之前花三十秒代一遍,比事后花半小时 debug 划算得多。真正容易出问题的从来不是公式本身,而是 encoder 和 decoder 之间那一个像素的默契——它不写在任何文档里,只能靠你对公式的理解去补齐。

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

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

立即咨询