转置卷积这个层尺寸对不上几乎是每个做上采样、做分割、做超分的人都躲不过的一道坎。我见过太多人在ConvTranspose2d前面反复改padding改到训练跑通了却不知道输出为什么是这个数也见过有人在 U-Net 跳连拼接时对着张量形状报错发愣最后只能靠 crop 硬凑。问题的根子其实特别简单转置卷积的输出尺寸不是乘个 stride就完事它由stride、padding、output_padding三个参数共同决定而这三者里最容易被误解的就是output_padding——它不是在输出上补零它甚至不改变卷积核的覆盖方式它只是用来消解一正一反两次变换中的尺寸歧义。这篇内容会把转置卷积的尺寸账从头算一遍先讲清楚它到底在算什么再把三个参数各自的职责拆开然后给出手算例子、可复现的验证代码、以及我在实际调试中攒下来的一套排查顺序。适合刚接触上采样层的新手也适合用了很久但一直被output_padding绕住的中级读者。1. 先把尺寸账算清楚转置卷积为何总在形状上报错1.1 形状对不上从来不是框架的锅绝大多数形状报错都发生在两个地方一是编码器和解码器做跳连拼接的时候二是把解码结果和原始输入做逐像素损失计算的时候。表面上看是解码器输出少了一行一列实际上是你脑子里那套转置卷积就是把尺寸乘回去的直觉在骗你。真正发生的事情是转置卷积并不会去记住它对应的那次正卷积的输入尺寸。它是一个独立的、有自己参数的层你给它一堆特征图它按自己的stride、padding、kernel_size算出一个新尺寸。这个新尺寸和当年正卷积输入的那个尺寸能不能对上纯粹取决于你有没有把参数配对选好。配不上就得靠output_padding把差值补回来或者靠crop把多出来的切掉。这就解释了一个很常见的困惑同样一套结构输入尺寸是 224 的时候能跑换成 225 就崩了。因为正卷积里的floor取整把 225 的余数吃掉了而反向的转置卷积并不知道这件事它只能还原出一个理论值两者就差那么一格。这个差值不是 bug是取整操作留下的信息损失output_padding就是专门用来填这个坑的。1.2 正卷积的尺寸公式是所有计算的锚点想算明白转置卷积先把正卷积的公式背下来因为转置卷积的公式本质上是这个公式的反解。带上膨胀系数dilation的完整形式是W_out floor( (W_in 2 * padding - dilation * (kernel_size - 1) - 1) / stride ) 1这个公式里每一项都在干一件具体的事 2 * padding是给输入四周补零把可滑动的范围撑大- dilation * (kernel_size - 1)是膨胀之后卷积核实际占用的跨度注意不是kernel_size除以stride是因为步长大于 1 时卷积核会跳着走能落脚的位置变少最后的 1是第一个位置本身也算一次。floor就是那个罪魁祸首它把除不尽的余数直接扔掉导致多个不同的输入尺寸可能映射到同一个输出尺寸。举个最直观的W_in 7, K 3, S 2, P 1代入得floor((7 2 - 3) / 2) 1 3 1 4。换成W_in 8得floor((8 2 - 3) / 2) 1 3 1 4。两个不同的输入输出完全一样。你现在应该能理解为啥转置卷积会猜不准了——它拿到的是 4要还原成 7 还是 8没有额外信息它就无从判断。output_padding就是那个额外信息。1.3 为什么它叫转置而不是反卷积这个名字的混乱程度值得单独说一下。很多资料管它叫 deconvolution反卷积这个叫法其实不准确甚至有害。严格意义上的反卷积是卷积运算的数学逆运算要求卷积矩阵可逆而实际用的卷积核几乎是不可逆的——信息已经丢了你不可能真的还原回去。转置卷积这个名字来自矩阵视角把正卷积写成矩阵乘法y C x其中C是一个由卷积核元素排布出来的稀疏矩阵尺寸是输出维度 × 输入维度。那么C的转置C^T尺寸就是输入维度 × 输出维度用它去乘y就能得到一个长度为原本输入维度的向量。这个运算是完全良定义的只不过它还原出来的不是原始信号而是在最小二乘意义下最接近的东西。所以更准确的理解是转置卷积是卷积的反向传播过程本身。在自动求导的语境里C^T y恰好就是y C x对x的梯度。这意味着一个转置卷积层的前向计算和一个正卷积层的反向传播在数学上是同一件事。这个认识特别有用——它解释了为什么转置卷积的可学习参数数量和同规格的正卷积完全一样都是C_in × C_out × K × K也解释了为什么它的输出会呈现那种补零后卷积的稀疏结构。顺便提一句正因为这个等价关系转置卷积的权重初始化和正卷积一样讲究。框架默认的初始化一般是kaiming_uniform_对于很多任务够用但如果你的解码器训练一开始就 loss 炸掉先去看看权重的方差范围别急着怀疑学习率。2. 三种视角看同一件事转置卷积到底怎么算2.1 视角一矩阵转置与梯度反传把特征图展平成向量卷积操作就是一个稀疏矩阵乘法。假设输入长度 4、卷积核长度 3、步长 1、无填充正卷积输出长度是 2。对应的矩阵C长这样每列对应该位置卷积核的覆盖C [w0 w1 w2 0 0 ] [ 0 w0 w1 w2 0 ] [ 0 0 w0 w1 w2 ]这是 3×5 的矩阵输出 3 个位置输入 5 个位置包含填充。转置之后变成 5×3拿它去乘一个长度为 3 的向量就得到长度为 5 的输出。这个乘法做的事情就是把每个输出位置上的权重撒回它原本覆盖的输入位置被多个位置覆盖的地方就累加。这个视角最直接的价值是它告诉你转置卷积的输出里某些位置是多个贡献叠加的某些位置只有一个贡献。当stride 1时叠加次数在空间上不均匀这就是后面要讲的棋盘格伪影的根源。另外它也说明了一件事——转置卷积不是插值它没有做任何平滑只是把值搬回去并相加重合区域自然变亮数值变大。2.2 视角二插零之后再做一个普通卷积这是工程实现里最常用的等价形式也是最容易在代码里验证的。步骤拆成三步第一步在输入的每两个相邻元素之间插入stride - 1个零。输入长度W_in插完之后长度是(W_in - 1) * stride 1。第二步在插零序列的两端各补kernel_size - 1 - padding个零。注意这里的补零数量和正卷积的padding是反着来的正卷积里padding越大输出越小转置卷积里padding越大输出越小但映射到中间这一步就是补零越少。第三步用翻转后的卷积核做一次步长为 1 的普通卷积。把三步的尺寸合起来算插零后(W_in - 1) * S 1两端补零加2 * (K - 1 - P)做长度K、步长 1 的卷积会再减掉K - 1最终W_out (W_in - 1) * S 1 2 * (K - 1 - P) - (K - 1) (W_in - 1) * S - 2 * P K跟官方公式比正好少了output_padding这一项。所以output_padding在这个视角下的含义就非常清楚了它相当于在插零序列的右端和下端再多补几个零把输出撑长。它不参与卷积核的覆盖计算只是把结果的长度往上顶。这里有个细节必须提醒kernel_size - 1 - padding可能是个负数。当padding大于kernel_size - 1时这一步实际上是裁剪而不是补零框架内部不会真的构造负数 padding而是用col2im这类展开-折叠的方式直接算出结果。所以你在写等价实现的时候如果照抄补零的思路会直接报错得加个分支判断。还有一点容易忽略这一步用的卷积核是翻转过的。因为数学定义里的卷积带翻转而深度学习框架实现的是互相关不带翻转。转置卷积作为卷积矩阵的转置等价于用翻转核做互相关。如果你在验证代码里忘了flip结果会跟框架输出差一个镜像数值对不上但形状是对的特别容易误判成实现有问题。我在文末的验证脚本里会显式处理这一点。2.3 视角三从梯度计算直接推第三种视角最程序员不管什么卷积我只关心它对输入求导是什么样。假设正卷积y conv(x)那么dy/dx这个雅可比矩阵的形状就是转置卷积在干的事。具体到反向传播的实现正卷积在前向时会缓存col2im展开后的列矩阵反向时用权重矩阵的转置去乘梯度再折叠回输入形状。这个乘权重转置 折叠的组合就是转置卷积的前向。所以你在任何一个框架里看ConvTranspose的实现基本都能看到col2im、gemm、im2col这些底层调用的影子。这个视角的实用价值在于性能判断转置卷积的计算量并不比插值 卷积小甚至更大因为插零之后大量乘的是 0但框架并不会跳过这些零。在移动端或者对延迟敏感的场景里同样效果下最近邻插值 普通卷积往往比裸的转置卷积快原因就在这。当然具体数值要看硬件和实现不能一概而论我的建议是先用 profiler 实测别凭直觉优化。3. stride、padding、output_padding 三个参数的真实职责3.1 stride只负责放大倍数不负责精确尺寸很多人以为stride 2就是尺寸翻倍在kernel_size 2, padding 0, output_padding 0这一个特例下确实成立。但只要参数稍有变化就不成立了。代入公式看W_out (W_in - 1) * S - 2 * P K OP。当S 2, K 3, P 1, OP 0时W_out 2 * W_in - 2 - 2 3 2 * W_in - 1是翻倍再减一不是翻倍。当S 2, K 4, P 1, OP 0时W_out 2 * W_in - 2 - 2 4 2 * W_in才是精确翻倍。这个差异在深网络里会累积。如果你有 5 层下采样、5 层上采样每层差 1最后差 5 个像素跳连拼接时就得裁掉边缘。所以设计网络时要么把K、P、S配成能整除的组合常用的做法是K 2S、P K/2 - 1之类要么就老老实实每层都做裁剪对齐。stride的另一个副作用是它对输出覆盖均匀性的影响。S 2, K 3这种配置相邻输出位置被卷积核覆盖的次数会在 1 和 2 之间交替这就是棋盘格伪影的数学来源。而S 2, K 4或者S 2, K 2时覆盖次数是均匀的伪影会轻很多。这一条在后面第 6 章会展开。3.2 padding沿用的是正卷积的语义作用方向相反padding这个名字在转置卷积里最容易被误解。它在正卷积里是给输入四周补零在转置卷积里它描述的是同一次变换在正方向上的补零量作用在反方向上就变成了从输出里裁掉多少。用公式感受一下padding每增加 1转置卷积的输出就减少 2左右各一。这和正卷积里padding 每增加 1输出增加 2完全对称。所以你可以这么记padding在转置卷积里是减法项它让输出变小。它存在的意义是保证正反两次变换尺寸一致。考虑一个正卷积K 3, S 2如果不加padding输入 7 会变成floor((7 - 3)/2) 1 3少了 4 个位置信息损失严重。加P 1之后变成floor((7 2 - 3)/2) 1 4边界信息保住了。对应的转置卷积用同样的P 1才能把 4 还原回 7。框架层面通常还会对padding加一个上限约束一般要求它不超过卷积核尺寸的一半具体阈值和是否强制以所用版本为准有的版本是按dilation放宽的。这个限制的存在是因为padding一旦过大中间那步补零就变成裁剪语义上虽然还算通但实现路径会绕容易出现边界处理不一致的情况。实际写代码时尽量把padding控制在kernel_size // 2以内能避开很多玄学问题。3.3 output_padding一个只为消歧义而生的补丁参数这是三个参数里唯一没有物理直觉的那个。前面已经推过它相当于在插零序列右端补零把输出长度撑大OP个位置。但为什么要撑大因为正卷积的floor把信息扔了。看这个例子正卷积W_in 8, K 3, S 2, P 1输出是floor((8 2 - 3)/2) 1 4。现在用转置卷积从 4 还原(4 - 1) * 2 - 2 3 7少了 1。这个 1 去哪了就是被floor扔掉的那半个位置。你在output_padding里填 1输出就变成 8对上了。但注意这多出来的 1 个位置不是凭空补了一个零在输出末尾它的数值是实打实算出来的——只不过它对应的是插零序列最右端那个新补的零参与卷积的结果。换句话说output_padding让输出多长出一格这一格的数值由卷积核最右侧的元素决定。关键约束output_padding必须小于stride并且按 PyTorch 的文档也必须小于dilation。原因很直白——如果它能大于等于stride那说明你在正方向的卷积里丢掉的位置超过了一整个步长这种情况下尺寸歧义已经不是补一格能解决的而是参数配置本身有问题了。给一个超大OP虽然能把尺寸凑对但对应的正卷积根本不存在语义上说不通框架也会直接报错拦你。还有一个高频误解得澄清output_padding不会在输出上额外增加零值像素它只是把输出撑长多出来的位置正常参与计算。所以如果你在一层转置卷积之后看到输出边缘有几个明显偏小或者偏灰的值那不是OP补的零那是边界效应——卷积核覆盖不全导致的属于另一类问题。4. 手把手算从公式到可复现的数值例子4.1 公式汇总与符号约定把要用的公式集中列一下后面所有例子都基于这套约定。符号W_in转置卷积输入尺寸W_out输出尺寸K卷积核尺寸S步长P填充OP输出填充D膨胀系数。运算尺寸公式正卷积W_out floor((W_in 2P - D(K-1) - 1) / S) 1转置卷积W_out (W_in - 1) * S - 2P D(K-1) OP 1插零等价形式W_out (W_in - 1) * S - 2P D(K-1) 1 OP反解 OPOP W_target - [ (W_enc - 1) * S - 2P D(K-1) 1 ]当D 1默认情况时转置卷积公式可以简化为更好记的形式W_out (W_in - 1) * S - 2 * P K OP这个简化版就是日常手算的主力只要不涉及膨胀卷积用它就够了。4.2 六个典型例子的手算过程光看公式没感觉直接上数。下面六个例子覆盖了精确翻倍差一格需要 OPOP 有歧义几种典型情况建议拿纸笔跟着推一遍。#W_inKSPOPW_out对应正卷积验证1431006floor((6-3)/1)1 42432009floor((9-3)/2)1 43432107floor((72-3)/2)1 44432118floor((82-3)/2)1 457421016floor((162-4)/2)1 8不符需 OP68321116floor((162-3)/2)1 8逐条拆解一下。例 1(4-1)*1 - 0 3 0 6。步长为 1 时输出比输入大K - 1 - 2P这是最朴素的边缘扩散效果。输入 4 个点每个点被一个 3 长度的核覆盖边缘延伸出去 1 格总共 6 格。例 2(4-1)*2 - 0 3 0 9。S 2把间隔撑开插零后序列长度是3*21 7再做K 3、无填充的卷积输出7 3 - 1 9。反推正卷积floor((9-3)/2)1 4正好回到起点说明这组参数是无损配对的。例 3(4-1)*2 - 2 3 0 7。加了P 1之后输出从 9 降到 7验证了P每加 1 输出减 2。反推floor((72-3)/2)1 4也对得上。例 4在例 3 基础上OP 1输出 8。这时候有意思了反推floor((82-3)/2)1 floor(7/2)1 31 4还是 4。也就是说7 和 8 两个不同的输出尺寸都能反推回输入 4。这就是前面说的尺寸歧义——单看正卷积的结果你无法判断反方向该输出 7 还是 8。OP的作用就是让你显式声明我要 8。例 5(7-1)*2 - 2 4 0 16。用K 4, S 2, P 1反推floor((16 2 - 4)/2)1 floor(14/2)1 8得到 8 而不是 7。差 1说明从 7 出发的正卷积输出是floor((72-4)/2)1 3而转置卷积输入应该是 3 而不是 7。这个例子提醒一件事转置卷积的输入永远是上一层的输出不是你脑子里想的那个原始尺寸。手算时一定先确认W_in是谁。例 6(8-1)*2 - 2 3 1 16。这是最典型的U 型结构对齐场景编码端把 8 压到 4解码端要还原回 8就必须OP 1否则只能得到 7。如果你在跳连拼接时报8 vs 7八成就是这里少填了一个OP。4.3 output_padding 的合法区间与反解代码实际写模型时很少有人真的手算通常是我要输出 H帮我算 OP 该填多少。写成函数更省事def required_output_padding(w_target, w_in, k, s, p, d1): 已知转置卷积输入 w_in 和期望输出 w_target反解 output_padding。 base (w_in - 1) * s - 2 * p d * (k - 1) 1 op w_target - base upper min(s, d) if d 1 else s if op 0: raise ValueError(f输出太小base{base}, target{w_target}需要减小 padding 或增大 kernel) if op upper: raise ValueError(foutput_padding{op} 超出合法范围 [0, {upper - 1}]) return op这个函数有两个地方值得注意。第一上界取min(s, d)因为框架的约束是两个条件都要满足。第二op 0的分支不是填个负数就行output_padding不支持负值遇到这种情况只能改padding或者改用裁剪方案。实际调试里op 0出现的频率不低通常意味着你的kernel选小了或者padding选大了。还有一个反向检查函数更实用——拿到一层转置卷积的参数直接算输出跟上一层的尺寸比对def deconv_out_size(w_in, k, s, p, op0, d1): return (w_in - 1) * s - 2 * p d * (k - 1) op 1 def check_pair(w_enc_in, k, s, p, d1): 给定编码端输入尺寸算出解码端应该输出多少以及需要的 OP w_enc_out (w_enc_in 2 * p - d * (k - 1) - 1) // s 1 base (w_enc_out - 1) * s - 2 * p d * (k - 1) 1 op w_enc_in - base return w_enc_out, op用check_pair(8, 3, 2, 1)跑一下返回(4, 1)跟例 6 完全一致。这个函数我在搭解码器的时候基本每个 block 都会跑一遍比训练跑起来再报错省太多时间。4.4 三大框架参数语义对照跨框架移植的时候最容易在这里翻车因为同一个效果在不同框架里的参数名和默认行为差别不小。项目PyTorchConvTranspose2dTensorFlowConv2DTranspose说明步长strideint 或 tuplestrides语义一致填充padding整数对称paddingsame/validTF 不支持任意数值填充输出尺寸控制output_paddingoutput_shape显式指定思路不同膨胀dilationdilation_rate语义一致默认填充0valid默认值不同易踩坑TF 的做法是让你直接给output_shape框架自己推padding好处是直观坏处是你得自己保证这个 shape 和正方向能对上否则框架不会拦你训练到一半才发现尺寸偏了。PyTorch 的做法是把OP暴露出来约束更明确但要求你自己算。我个人更习惯后者因为错误能在构造时就暴露。另外 TF 的paddingsame在stride 1时输出尺寸是输入 × stride这个行为和 PyTorch 里K 2S, P K/2 - 1的组合等价需要配合OP。移植的时候别只改函数名把尺寸算一遍再跑。5. 代码落地与验证写完就跑跑完就测5.1 PyTorch 下的最小验证脚本理论推完一定要用代码对一遍否则很容易在某个符号上想反。下面这段是我常用的验证脚本逻辑是先做一次正卷积再用转置卷积还原检查尺寸和数值。import torch import torch.nn as nn torch.manual_seed(0) # 编码8 - 4 enc nn.Conv2d(1, 1, kernel_size3, stride2, padding1, biasFalse) # 解码4 - 8必须带 output_padding1 dec nn.ConvTranspose2d(1, 1, kernel_size3, stride2, padding1, output_padding1, biasFalse) x torch.randn(1, 1, 8, 8) feat enc(x) print(编码后:, feat.shape) # torch.Size([1, 1, 4, 4]) # 把解码器权重设成正卷积权重的转置关系方便观察 with torch.no_grad(): dec.weight.copy_(enc.weight) y dec(feat) print(解码后:, y.shape) # torch.Size([1, 1, 8, 8]) # 不带 output_padding 的对照 dec2 nn.ConvTranspose2d(1, 1, kernel_size3, stride2, padding1, biasFalse) print(不带 OP:, dec2(feat).shape) # torch.Size([1, 1, 7, 7])跑出来8 - 4 - 8OP 1就是那个必需的补丁。把OP去掉就变成 7形状直接对不上。这段代码的价值在于它把为什么需要 OP变成了可观测的事实而不是公式上的一个符号。顺带说一句dec.weight.copy_(enc.weight)这行只是为了让通道关系对应转置卷积的权重形状是(in_channels, out_channels, K, K)正卷积是(out_channels, in_channels, K, K)同通道数下形状恰好一样可以直接拷。真正训练的时候当然不会这么设这里只是为了观察。5.2 用插值加卷积复现同一结果想真正理解内部发生了什么用插零 翻转核卷积手写一遍最有效。下面是可运行的等价实现import torch import torch.nn.functional as F def deconv_manual(x, weight, stride, padding, output_padding0): b, c_in, h, w x.shape c_out, _, kh, kw weight.shape # 1. 元素之间插零 hh (h - 1) * stride 1 ww (w - 1) * stride 1 z x.new_zeros(b, c_in, hh, ww) z[:, :, ::stride, ::stride] x # 2. 右/下端补 output_padding 个零 if output_padding 0: z F.pad(z, (0, output_padding, 0, output_padding)) # 3. 翻转卷积核数学卷积 vs 互相关之差 w_flip torch.flip(weight, dims[2, 3]) # 4. 补零量 K - 1 - padding可能为负 pad_h kh - 1 - padding pad_w kw - 1 - padding if pad_h 0 and pad_w 0: out F.conv2d(z, w_flip, padding(pad_h, pad_w)) else: # 需要裁剪的路径 out F.conv2d(z, w_flip, padding0) cut_h, cut_w -pad_h, -pad_w out out[:, :, cut_h:out.shape[2] - cut_h, cut_w:out.shape[3] - cut_w] return out拿它跟官方实现对一下x torch.randn(1, 3, 5, 5) m nn.ConvTranspose2d(3, 4, kernel_size3, stride2, padding1, output_padding1, biasFalse) ref m(x) mine deconv_manual(x, m.weight, stride2, padding1, output_padding1) print(torch.allclose(ref, mine, atol1e-6)) # Trueallclose输出 True 的那一刻这三个参数之间的关系基本就刻在脑子里的。我第一次跑通这段的时候才真正意识到output_padding在中间那步就是右端补零跟输出末尾补零完全是两回事——前者会参与卷积计算后者不会。5.3 输出尺寸自检函数工程里更实用的做法是把尺寸检查做成断言在模型构造阶段就跑一遍。这套思路我在好几个项目里都用过能挡掉绝大部分低级错误def assert_shapes(layer, in_shape, expect_shape): b, c, h, w in_shape dummy torch.zeros(1, c, h, w) out layer(dummy) got tuple(out.shape[2:]) assert got tuple(expect_shape), f尺寸不符: got {got}, expect {expect_shape} print(fOK: {in_shape[2:]} - {got})搭解码器的时候每加一层就assert_shapes(dec_layer, (1, c, 8, 8), (16, 16))一次。看起来啰嗦但相比训练跑了两小时在拼接处崩掉这几行断言的性价比高得多。尤其是当你的网络根据输入尺寸动态计算层数或者支持多种输入分辨率的时候这个检查几乎是必需的。另外补一个经验如果你用的是paddingsame类型的封装或者自己写了根据输入自动推OP的模块一定要在多种输入尺寸下测比如 224、225、256、257 各跑一遍。取整误差在偶数尺寸上往往看不见一到奇数就冒出来。6. 踩坑实录棋盘格伪影、梯度稀疏与选型建议6.1 棋盘格伪影是怎么来的怎么压下去训练出来的图放大后能看到规则的网格状纹理这是转置卷积最著名的问题。成因在 2.1 节已经埋下伏笔当stride 1且kernel_size不能被stride整除时输出图上不同位置被卷积核覆盖的次数不均匀。具体说S 2, K 3的情况下输出位置被覆盖的次数在 1 和 2 之间交替形成周期为 2 的条纹S 4, K 3更糟覆盖次数在 1 和 2 之间以周期 4 循环视觉上就是明显的网格。而S 2, K 4时每个位置都被覆盖 2 次均匀伪影基本消失。三种压制手段按我自己的使用频率排序第一种把核尺寸改成 stride 的整数倍。S 2就用K 2或K 4S 4就用K 4。这是成本最低的做法改个参数就行代价是K 2时感受野偏小可能需要多堆几层。第二种用插值上采样 普通卷积替代。最近邻或双线性把尺寸放大再用一个K 3, S 1, P 1的普通卷积做特征变换。这条路线的好处是上采样本身是平滑的没有覆盖不均的问题代价是多了一次插值的计算参数上多了一个可选的上采样方式。第三种用像素重排pixel shuffle替代。先用普通卷积把通道数扩到C × S²再重排成空间维度。这种做法在超分领域很常见效果通常比转置卷积干净代价是通道数膨胀带来的显存压力。我的实际取舍是对画质敏感的任务图像生成、超分优先用第二或第三种对速度敏感、伪影不明显的任务分割、检测的特征融合继续用转置卷积但把K设成S的倍数。别指望靠后处理去伪影那是治标。6.2 训练不稳定的几个隐藏原因除了伪影转置卷积还有几个容易忽略的坑。梯度稀疏。插零的视角说明了一件事中间那步有大量元素是 0反向传播时这些位置的梯度也是 0。如果你的stride很大比如 8、16零的比例会非常高梯度信号稀薄收敛会明显变慢。这时候插值 卷积的方案反而梯度更稠密。这一条在我调大倍率超分的时候体会特别深同样层数下换成插值方案前几个 epoch 的 loss 下降明显更干脆。初始化尺度。转置卷积默认的初始化方式和正卷积一样但它的输出是多个位置叠加的结果叠加次数不均会导致输出的方差在空间上不一致。如果解码器层数多这种不一致会累积。缓解办法是把权重初始化得保守一些比如手动乘个 0.5 到 0.8 的系数或者在每个转置卷积后面接 BN / LN 把尺度拉回来。输出端的边界效应。前面提过output_padding撑出来的那一格和padding裁掉的边界数值上是不完整的。如果损失函数对边界敏感比如逐像素 L1边界区域会贡献偏大的梯度训练久了边缘会出现异常。稳妥的做法是在算损失时把边界若干像素 mask 掉或者干脆让输入尺寸是 2 的幂次从根上避免边界不齐。6.3 什么时候不该用转置卷积最后说点反向的话。转置卷积不是上采样的唯一解甚至不是默认最优解。如果你的任务只是把特征图放大然后接几层卷积那我更推荐插值 卷积代码更短、行为更好预测、伪影更少。如果你的任务是生成式的、需要模型自己学上采样方式转置卷积的可学习参数才有价值。如果你的放大倍数是 2 的幂次且很大比如 8 倍、16 倍一层转置卷积不如拆成多层小倍率堆叠8 2 × 2 × 2三层的效果通常比一层stride 8好伪影和梯度稀疏两个问题都能缓解。如果你的显存或者延迟卡得很紧转置卷积的插零计算是纯浪费插值 卷积在同等效果下通常更快。这一条我建议你自己用 profiler 测不同硬件上的结论可能相反别信网上的一句话结论。还有一个容易被忽略的场景当你的解码器需要和多尺度特征做融合时每层的输出尺寸必须精确已知。这种情况下我更倾向于用显式指定输出尺寸的写法自己封装一层内部算OP而不是在每个ConvTranspose2d里手填数字。手填的数字改一次输入分辨率就要全改一遍维护成本太高而且极容易漏改。我自己现在写解码器的习惯是先定一个target_size列表倒着从输入尺寸推每一层该多大然后在构建层的时候自动算OP最后用 5.3 那个assert_shapes全跑一遍。这套流程跑下来形状相关的报错基本绝迹了。剩下要操心的就是伪影和收敛那两个问题至少是有明确排查方向的。