ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

转置卷积完全指南:原理、图解与PyTorch实践

转置卷积完全指南:原理、图解与PyTorch实践 说实话我一开始看到“转置卷积”这四个字第一反应是“这名字有点劝退”。但接触时间长了就会发现转置卷积在图像生成、语义分割、超分辨率这些方向里几乎是绕不开的底座PyTorch 里叫ConvTranspose2dTensorFlow 里叫Conv2DTranspose老资料里还经常叫“反卷积”。名字一堆但干的事情很聚焦把特征图变大而且这个“变大”的过程是能学习的。本文不会只贴公式我会把我自己理解转置卷积时踩过的坑、试过的实验、总结出来的图解方法都写出来尽量让新手也能在脑子里画出一张清晰的图。这篇博文适合这几类人正在写分类网络但突然碰到需要上采样的模块、做分割或生成模型时对着nn.ConvTranspose2d尺寸算不对、或者只是想在面试前彻底搞懂“转置卷积到底转置了什么”的朋友。我会从零开始把转置卷积的两种等价视角、尺寸公式、参数搭配、工程中的常见坑全部讲透最后还给出一套可以直接拿去用的 PyTorch 验证代码。1. 转置卷积到底在解决什么问题1.1 上采样为什么是深度学习刚需回答转置卷积有什么用之前得先搞清楚一个问题为什么网络需要“上采样”。常规的卷积神经网络从 Conv 到 Pool 再到 Stride一路都在把空间尺寸压小、把通道数拉大。这个过程对分类任务没问题但一旦任务需要输出像素级结果问题就来了。拿语义分割举例输入一张 512×512 的图片经过好几层下采样后特征图可能只有 32×32但最终要输出 512×512 的每个像素类别标签。这一步“从小图放大回大图”就是上采样。图像生成任务更直接GAN 里的生成器输入是一个 100 维的噪声向量经过一系列操作最终要产出一张 64×64 或者 256×256 的图片中间全靠一层层上采样把空间维度撑起来。上采样的实现方式有很多最朴素的是最近邻插值和双线性插值这两种方法简单、快、没有参数但问题是它们不学习放大后的细节完全依赖插值规则没法根据任务自适应。于是就有了可学习的上采样转置卷积以及后来的 PixelShuffle 等方案。转置卷积的核心价值在于它把“放大”这个过程也变成了网络的一部分放大倍率、边缘处理方式、特征填充逻辑都能通过训练数据来调整。1.2 “转置”不是“逆”先把这个误区拆掉我见过太多人把转置卷积直接叫成“反卷积”然后理所应当地认为它是卷积的逆运算输入一张图经过“反卷积”就能无损还原出卷积前的图。这是个非常深、非常常见的误解。转置卷积确实在形状上做了和普通卷积相反的事情普通卷积把大图变小转置卷积把小图变大。但“形状上的反转”不等于“数学上的求逆”。一个卷积操作丢掉的边缘信息、池化丢掉的细节位置转置卷积并不能凭空找回来。它只是在空间维度上执行了一次“和卷积反向”的映射至于映射出来的是什么内容全靠卷积核参数来决定。那“转置”这个词到底怎么来的这个说法来自矩阵运算视角。如果用矩阵形式表示一次普通卷积输出y可以写成y Cx其中C是卷积核展开后形成的稀疏矩阵x是输入。转置卷积做的事情就是在这个矩阵视角下让数据沿着C^T的方向流动。也就是说普通卷积的前向传播在反向传播时误差信号正好是经过C^T传回上一层的。所以转置卷积和卷积之间共享着同一个矩阵的转置关系这就是它名字的由来。后文我会用一个具体例子把C和C^T画出来看懂那个你就彻底明白这个“转置”了。2. 图解转置卷积的两种等价视角理解转置卷积我一直觉得有两条路缺一不可一条是矩阵视角解决“为什么叫转置”的问题另一条是补零加卷积的视角解决“参数到底怎么配”的问题。两条路看着不一样最后算出来的结果完全相同而且可以互相验证。以下所有内容都以 PyTorch 的nn.ConvTranspose2d为基准来说明。2.1 矩阵视角y Cx 与 x C^T y先用最简单的一维例子来讲二维只是它的推广。设输入序列x [x1, x2, x3]卷积核w [w1, w2]stride1没有 padding那么普通卷积这里指深度学习里的互相关不做核翻转的输出长度为3 - 2 1 2y1 w1*x1 w2*x2 y2 w1*x2 w2*x3把它写成矩阵形式[y1] [w1 w2 0 ] [x1] [y2] [0 w1 w2] * [x2] [x3]中间这个矩阵就是C形状是 2×3。注意这里的矩阵非常稀疏每一行只有两个非零元素分别对应卷积核的两个权重。现在把这个矩阵转置一下C^T [w1 0 ] [w2 w1] [0 w2]C^T的形状是 3×2。如果我现在输入一个长度为 2 的向量y [y1, y2]让它经过C^T输出就是一个长度为 3 的向量xx1 w1*y1 x2 w2*y1 w1*y2 x3 w2*y2这不就是一次把 2 个元素变成 3 个元素的操作吗长度 2 进、长度 3 出正好和原来的 3 进 2 出反过来了。二维情况完全同理只不过矩阵更大、更稀疏。普通卷积的输入如果是 4×4经过 3×3 卷积核输出 2×2那么对应的C就是 4×16 的矩阵转置卷积的C^T就是 16×4能把 2×2 的输入映射回 4×4 的输出。需要强调的一点是转置卷积的卷积核并不是直接取自某个固定卷积核的转置它是独立训练出来的参数。C^T这个形式只解释“运算形状为什么反了”在工程里转置卷积的权重和普通卷积一样都是随机初始化再训练的。别把“数学形式上的转置”理解成“权重也要共享转置”。2.2 补零视角插零、补边、普通卷积矩阵视角能解释原理但真正到了调参数阶段我觉得更好用的图景是“补零 普通卷积”。这个视角下转置卷积可以被拆成三个连续步骤每一步都有具体的图像化操作。还是用最开始那个常用配置输入 2×2kernel_size3stride2padding1output_padding1。目标输出是 4×4。第一步内部插零。在输入矩阵的每个相邻元素之间插入stride - 1个零行和列都要插。stride2所以每两个元素之间插入 1 个零输入 2×2 a b c d 内部插零后 3×3 a 0 b 0 0 0 c 0 d第二步边界补零。这一步很多人容易搞反注意了转置卷积里的 padding 并不是简单地在原始输入周围补 padding 圈零而是在内部插零后的图周围补kernel_size - 1 - padding圈零。这里 kernel_size3padding1所以补3 - 1 - 1 1圈零补零后 5×5 0 0 0 0 0 0 a 0 b 0 0 0 0 0 0 0 c 0 d 0 0 0 0 0 0第三步用原始的 3×3 卷积核在这个 5×5 上做步长为 1、无额外 padding 的普通卷积。5×5 经过大小为 3 的卷积核输出是 3×3。样子虽然没画出来但数值已经有了。如果此时配置了 output_padding1等价于再对底部和右侧各扩展一个单位让卷积核能多滑动一行和一列最终输出就变成了 4×4。这里我建议你把“output_padding”理解成“为了做尺寸对齐而在输出侧右下方补出来的额外滑动空间”而不是“在最终输出上直接补一圈零”。这两种理解在多数情况下结论一致但遇到不对称尺寸时后者容易出错。后面我会用代码验证这段过程。2.3 输出尺寸公式推导与记忆方法有了补零视角输出尺寸公式就特别好推了。设输入边长为in转置卷积的参数为stride、padding、kernel_size、output_padding暂时忽略 dilation输出边长公式为out (in - 1) * stride - 2 * padding kernel_size output_padding别死记从补零视角一步步推一下你就能亲眼看见每一块来源。内部插零后边长变成(in - 1) * stride 1补零圈数为kernel_size - 1 - padding左右两侧都补所以边长增加2 * (kernel_size - 1 - padding)。此时待卷积图的边长是(in - 1) * stride 1 2 * (kernel_size - 1 - padding)普通卷积的输出边长等于“输入边长 - kernel_size 1”代入并化简out (in - 1) * stride 1 2 * (kernel_size - 1 - padding) - kernel_size 1 output_padding (in - 1) * stride - 2 * padding kernel_size output_padding以后无论参数怎么组合你都可以在纸上按这个流程推一次而不是背公式。这个推导过程也解释了为什么转置卷积的padding越大输出反而越小padding 越大在插零后图上补的边界零就越少卷积核能滑出的范围就越小。3. 关键参数怎么配stride、padding、output_padding3.1 每个参数在补零视角下的实际作用先说stride。它是“放大倍数”最直接的来源。stride2 时输入元素之间插 1 个零边长从in变成2*in - 1再经过后续卷积总输出基本是输入的 2 倍左右。stride4 时输入元素之间插 3 个零放大趋势更明显。所以做 2 倍上采样一般用 stride24 倍上采样可以叠两层 stride2也可以直接用一层 stride4。再说kernel_size。它在公式里以加法形式出现kernel 越大输出越大。同时它也决定了输出中每个位置“能看到”多少个输入元素的重叠贡献。关于重叠贡献的话题下一章会展开这里只提醒一点kernel 的取值直接影响后续会不会出现棋盘格伪影不能随便图省事选个奇数就行。padding的语义和普通卷积完全相反。普通卷积里的 padding 让输出变大转置卷积里的 padding 让输出变小。要理解这个得回到kernel_size - 1 - padding这个补零圈数公式padding 越大待卷积图周围补的零越少卷积核能滑出去的边缘空间越小。在代码里常见的组合是kernel3, stride2, padding1, output_padding1这套组合专门用来做精确的 2 倍上采样。output_padding的职责只有一个在公式最后微调输出尺寸让它对齐到某个预期值。它并不会增加额外的有效输入信息只是让卷积核在右下角方向多滑出一部分空间。常见设计里2 倍上采样时 output_padding 一般取 1 或 0具体看 kernel 和 padding 的搭配。3.2 常用上采样配置速查表做图像生成或者分割时2 倍、4 倍、8 倍上采样用得最多。我把自己平时验证过的几套配置整理成了一张表各参数以 PyTorch 为准目标放大倍数kernel_sizestridepaddingoutput_padding输入尺寸示例输出尺寸2×32114×48×82×42104×48×82×22004×48×84×34134×416×164×44004×416×16第一套kernel3, stride2, padding1, output_padding1是最常见的在 DCGAN、ResNet 的某些解码结构里能看到类似配置。第二套kernel4, stride2, padding1也很经典DCGAN 生成器里用的就是 kernel4stride2padding1此时 output_padding 不需要额外设置输出正好是 2 倍。第三套kernel2, stride2, padding0, output_padding0是最轻量的精确 2 倍配置参数少计算快但表达能力弱一些通常用在简单生成任务或者叠加其他网络模块的场景。表格里这几个例子你可以拿公式手动验一遍以第一套为例out (4-1)*2 - 2*1 3 1 8正好是 2 倍。3.3 用 PyTorch 实测验证整个流程理论说得再好不如跑一段代码直接验证。下面这段代码把权重全部设为 1输入是一个 2×2 的矩阵这样输出每个位置的数值就等于“它覆盖了多少个有效的输入贡献”。import torch import torch.nn as nn conv_t nn.ConvTranspose2d( in_channels1, out_channels1, kernel_size3, stride2, padding1, output_padding1, biasFalse, ) conv_t.weight.data.fill_(1.0) # 简化验证核全部为1 x torch.arange(4, dtypetorch.float32).reshape(1, 1, 2, 2) print(输入:) print(x[0, 0]) y conv_t(x) print(输出尺寸:, y.shape) print(输出:) print(y[0, 0])输入这个torch.arange(4)就是0 1 2 3输出尺寸应该是[1, 1, 4, 4]。由于权重全为 1输出中每个像素的值就等于前一章“补零视角”里那个 5×5 图上对应 3×3 窗口内非零元素的和。你甚至可以手动算第一行第一个位置窗口覆盖的是左上角周围的零区域如果有有效输入就累加没有就是 0。如果你愿意还可以把conv_t.weight.data改成更复杂的核去观察不同位置受到的影响。这种“固定权重看数值”的小实验我强烈建议刚学的朋友自己做一次比看十篇博客都有用。我也经常拿这种验证方式去检查自己写的自定义网络每一层输出的尺寸对不对效率很高。4. 工程中的常见坑与替代方案4.1 棋盘格效应的成因棋盘格也叫 checkerboard artifacts是转置卷积在图像生成任务里最容易出现的问题。具体表现是生成图像里有一层规律的格子状纹理尤其是低频区域或渐变区域特别明显看起来像下国际象棋的棋盘。原因是转置卷积等价于“插零 普通卷积”后输出不同位置接收到的有效输入信息量是不均匀的。回到 2.1 节那个一维例子转置卷积的输出x1 w1*y1 # 只收到1个输入元素贡献或者说1个窗口 x2 w2*y1 w1*y2 # 收到2个输入元素贡献 x3 w2*y2 # 只收到1个输入元素贡献中间位置得到的贡献多、两端位置得到的贡献少这种不均匀在二维空间里会形成周期性的亮暗差异。当卷积核尺寸不能被 stride 整除时这个问题尤其严重。比如 kernel3、stride2此时 3 不能被 2 整除重叠区域和空白区域交错出现棋盘格肉眼可见。而 kernel4、stride2 这种可整除的组合重叠分布相对均匀伪影就会轻很多。解决棋盘格的思路很明确一是让kernel_size能被stride整除这是最直接的规避手段二是干脆换用别的上采样方式比如“先插值放大 普通卷积”或者用 PixelShuffle。尤其是任务里对图像质量要求高、对伪影零容忍时我不会蛮力去调反卷积参数而是直接切到 PixelShuffle 或者双线性插值加卷积的思路。4.2 输出尺寸对不上自查这几个地方使用转置卷积时最常见的报错或异常就是输出尺寸和预期不一致。我排查过很多次这种问题整理了一个顺序清单第一确认公式里的每个值都写对了。别以为stride2, kernel3, padding1就一定是 2 倍上采样不补output_padding时输出会差 1。例如 4×4 输入kernel3, stride2, padding1out (4-1)*2 - 2 3 7不是 8。第二output_padding不要超过stride和padding的合理范围。它的实际作用是补右下方向的滑动空间如果给得太大输出尺寸会明显超出预期而且多出来的部分基本是无效边缘信息。第三注意dilation。带空洞的转置卷积公式会多一个 dilation 项如果你没有主动设置 dilationPyTorch 默认是 1但如果你从别的框架迁移模型最好显式确认一下这个值。第四编码器和解码器对称时在每一层设计阶段就要算好尺寸。我习惯把尺子写在代码注释里比如“输入 B×64×64输出 B×128×128”然后在网络 forward 里临时 print 每层 shape逐层核对。4.3 转置卷积不是万能的上采样方式虽然转置卷积用起来很顺手但它不是所有上采样问题的唯一解甚至很多时候不是最优解。第一个问题是它对超参数敏感。kernel、stride、padding、output_padding 四者之间的细微变化都会影响输出尺寸和伪影程度。对于不熟悉细节的开发者来说调试成本不低。第二个问题是计算量和显存占用。转置卷积内部要先把输入插零再执行一次普通卷积在一张大特征图上作卷积的代价并不小。解码器如果连续叠加好几层转置卷积显存消耗会明显上升训练速度也会受影响。第三个问题是生成质量。在 GAN 和超分辨率任务里棋盘格伪影经常被人诟病。相比之下PixelShuffle 先把通道维度重新排列成空间维度再通过普通卷积做精细调整不会产生插零造成的周期性贡献不均是目前很多高质量生成模型的优先选择比如超分辨率领域经典模型 ESPCN、SRGAN 的生成器结构。那转置卷积还值得用吗当然值得。它的表达能力强结构简单直接在 DCGAN 里用kernel4, stride2, padding1就能稳定生成图片很多分割网络解码器也在用反卷积。我的建议是如果你做的是通用生成或分割任务且更在意实现简单、调试方便转置卷积没问题如果你做超分辨率或对伪影零容忍优先考虑 PixelShuffle。4.4 常见问题速查表现象可能原因处理建议输出尺寸比预期小 1忘记设置 output_padding根据公式补output_padding1输出尺寸比预期大很多output_padding 设置过大output_padding 取 0 或 1不要超过 stride生成图像有明显格子纹理kernel_size 与 stride 不整除改用可整除组合如 kernel4, stride2输入输出尺寸计算复杂对补零视角不熟把公式写在注释里逐个参数代入手动算显存占用过高反卷积层过多或 kernel 过大减少反卷积层或换 PixelShuffle/插值卷积训练早期输出全是噪声初始化或数值范围问题检查权重初始化必要时加 BatchNorm这几点基本覆盖了我在实际项目里遇到过的绝大多数问题。尤其那个“尺寸差 1”的坑我在一个图像翻译项目里调了整整一下午最后发现就是少写了一个output_padding1。所以现在写网络我第一件事就是把尺寸公式抄在文件头部每个上采样层的参数都先在纸面算好再落代码。最后再分享一个我自己一直用的小技巧。你想观察转置卷积对特征图的影响时不需要一开始就上真实图片直接构造一张全 1 的输入图再把转置卷积权重全部固定为 1看它的输出分布。这个方法能很直观地感受到哪些位置是“贡献重叠区”哪些位置是“空洞区”比单纯盯公式理解快得多。图像生成里那个棋盘格只要你在输出图上画一下权重分布热力图自己就明白了。
返回列表