ARTICLE DETAIL

资讯详情

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

CoordConv:让卷积网络感知位置,解决GAN生成图像位置漂移

CoordConv:让卷积网络感知位置,解决GAN生成图像位置漂移 先说个我自己踩过的坑。之前我复现一个条件GAN做数字生成模型收敛得很漂亮生成的每个数字单独看都清晰锐利。但是把一整个batch拉成网格图以后我发现数字“7”今天在左上角明天跑到右下角甚至有时候半个身子探出画布边缘。整个训练过程中我调整了学习率、批大小、甚至换过损失函数位置就是“稳不住”。后来我才意识到问题不在训练的力度而在网络结构本身的盲区——标准卷积天然不携带坐标记忆。这正是CoordConv能解决的典型场景。如果你也在做GAN图像生成、图像编辑或者任何要求生成结果精确落在指定位置的AI任务这篇文章值得你读完。1. 三个最典型的画不准位置症状以及对训练的连带影响1.1 MNIST条件生成数字内容正确但位置四处游走我第一次踩进这个坑是在条件DCGAN项目里。训练集是MNIST标签是0~9朴素的想法是把类别标签embedding成向量和随机噪声一起送进生成器让生成器画一张28×28的数字图。训练大概跑到三十个epoch时单张图的视觉效果已经很不错了数字轮廓清楚背景也干净。可我把一整个batch拼接成对比图后立刻发现了不对劲——上一行第一张是“1”在左上角下一行第一张是“1”在右下角位置像是随机采样的。也就是说模型已经学会了“画1”却没学会“画在哪个位置”。如果用一个质心检测脚本来统计这批生成图的数字质心分布你会看到质心在画布上呈散点状集中度非常低。我试过只训练50个epoch、80个epoch、甚至150个epoch质心散点并没有随训练时间变长而明显聚拢。这个问题发展到极端时数字会被画到画布边缘截断成不完整形状。你单独看某一两张图可能觉得“哦这是风格化数字”但放在批量生成场景里根本没法用。很多人把这种现象笼统归因为“GAN不收敛”或“模式坍缩”确实它属于训练不稳定的表现之一但根因往往是结构层面的光靠调学习率很难解决。1.2 人脸/结构化对象生成五官与整体布局错位比数字漂移更恼人的是结构化对象生成。比如用GAN生成人脸时出现过眼睛位置整体向右偏移、鼻子嘴巴相对距离忽大忽小的现象再比如生成“桌上有几个物体”的室内场景时物体之间的空间关系总是不稳定模型好像记得每样东西长什么样但搞不清它们应该摆在哪个位置。人脸五官本质上是一种强几何约束正常人的眼睛高度基本保持在面部从上到下的某个区间双眼间距相对固定鼻子和嘴巴在垂直方向上的比例也有统计规律。如果生成器缺少位置信息模型只能依赖深层特征里极其模糊的全局线索去猜大致位置。训练条件不足时五官错位和“均值脸”会一起出现生成结果看起来既油又假。我在复现一些开源人脸GAN时还发现这类位置漂移对判别器而言“识破难度”特别低。判别器很容易学会一个简单规则你眼睛位置不对那就是假的。于是生成器和判别器陷入一种滑稽的对抗——生成器努力把五官聚拢一点但只要一过头又开始在另一个方向摆动。1.3 位置错误带来的连锁反应不只是观感问题很多人觉得“画歪了不就是难看点嘛”实际上位置错误会直接影响模型评估。FID等指标计算的是生成分布与真实分布在特征空间的距离位置乱跑等同于给分布增加了一个额外的高方差维度FID数值会显著变差。下游使用场景更麻烦你想让GAN输出贴图到游戏素材里或者配合检测模型做数据增强位置不稳定会导致大量样本没法用。做AI绘画时用户输入“左上角有只猫”生成结果却把猫放到右下角这已经不是审美问题而是指令遵循能力不合格。因此解决位置问题可以带来一连串收益分布更集中、指标更好看、可复用性更强。2. 标准卷积为什么是“路痴”丢掉坐标信息容易被忽略2.1 卷积核只看局部邻里关系不关心绝对坐标先看一个3×3卷积核的工作过程。卷积核里9个权重在一个图像上按固定步长滑动每个位置上做同样的加权求和。也就是说网络学到的规则是“如果中心像素是这样邻居像素是那样我就输出某个特征”整个规则与规则作用在图像哪个位置无关。这个性质叫平移等变性翻译成大白话就是猫在画面左边还是右边卷积层都会用同一套逻辑提取特征。对图像分类来说这是梦寐以求的归纳偏置——不管物体在哪我都把它识别成同一个类别。但生成任务恰恰需要知道“东西在哪个位置”。卷积层把绝对坐标这个概念从表示中抽离了而生成器如果想画一个左上角的球必须先知道“左上角”对应到特征图上的哪一组神经元。你可以把卷积网络想象成一支对每个住户提供同样服务的施工队无论你住1号楼还是99号楼服务流程都一样。让这支施工队记住“99号楼需要特殊处理”必须在网络里额外建立与绝对位置挂钩的信息通道而不是简单多派几支队伍。2.2 绝对位置只能靠边界和padding漏进来一个让很多人困惑的问题是既然卷积不编码绝对位置那为什么有些GAN生成的物体仍然倾向于出现在画面中央位置信息到底从哪来的答案是从边界和padding来的。卷积运算在图像边缘时padding区域会参与计算边缘像素和中心像素的“邻居模式”天然不同所以严格来说卷积输出并非完全平移不变而是带一点点边界痕迹。网络可以利用这些痕迹反推“我目前靠近图像边缘”再结合多层下采样积累出模糊的位置感知。但这种感知非常弱、非常间接就好比被蒙上眼睛关在一个房间里只能靠伸手摸到墙壁来判断自己是不是贴墙站却无法知道房间中心在哪里。要让这种模糊感知变得可靠网络需要把位置信息在多层之间反复传递代价很大而且对生成器这种需要精确输出几何的任务来说效果远远不够。还有一个更隐蔽的点生成器在解码阶段知道整体输出尺寸是28×28或者256×256这个尺寸信息存在代码和上采样参数里却不在网络前向传递的显式张量中。CoordConv所做的就是把这些“写死在配置里的坐标”变成一个看得见、算得着的输入通道。2.3 加深网络为什么解决不了常见误区是既然位置信息学得不够那堆叠更多卷积层、加深网络总可以吧我试过用ResNet风格的生成器替代普通DCGAN位置漂移改善非常有限。深层网络能表达更复杂的函数但它没有改变卷积层的两个本质权重共享和平移等变性。深层 Feature Map 里每个位置依然在回答“这个局部区域内有什么”而不是“我在整个输入的哪个位置”。位置信息在反向传播中虽然也能逐步累积到网络的深层表示里但这种累积是不稳定、不直接的尤其是在GAN这种生成器和判别器动态博弈的场景里稍微一点位置噪声就可能被对抗训练放大。不少人还尝试了在损失函数里加正则项比如让生成数字的质心靠近图像中心。这种方法能治标但本质是“拿外部约束纠正内部缺陷”需要在每个任务里重新设计正则项和惩罚权重换一个数据集又要重新调。如果网络结构本身具备感知位置的能力很多正则项其实根本不需要。2.4 注意力机制能部分解决但代价不一定划算有人在交流群里问我那我把生成器里的卷积全换成Vision Transformer或者加入Self-Attention层是不是就解决位置问题了确实能。Vision Transformer需要给每个patch加入位置编码这个位置编码本质上就是一组可学习的坐标信息Self-Attention里的相对位置嵌入也起到类似作用。它们把“位置”这个概念显式建模了所以能缓解画不准位置的问题。但代价是计算复杂度上升训练稳定性对超参数更敏感小型生成任务有点杀鸡用牛刀。尤其对于256×256或更高分辨率的生成器注意力层的开销相当可观。CoordConv的思路比这个轻得多它不改变卷积的局部归纳偏置只是在输入数据里直接增加两个坐标通道让卷积自己学会使用坐标信息。改动小、可插拔适合快速修复。你可以在不改变任何损失函数、不引入复杂算子、不重写训练框架的前提下把已有模型中的普通Conv层替换成CoordConv层。3. CoordConv怎么补位坐标通道原理与实现要点3.1 一句话原理把坐标表拼进输入张量CoordConv的核心一句话给输入张量增加两个特殊通道——一个通道的值从图像左边到右边线性变化代表每个像素的x坐标另一个通道的值从上到下线性变化代表y坐标。然后把这个扩展后的张量拼接好进入常规的卷积层。卷积层前向计算时普通特征和坐标特征都会被加权求和网络可以学习给坐标通道分配多大权重从而在“局部纹理建模”和“绝对位置感知”之间自由权衡。如果坐标信息对任务有帮助梯度就会把坐标相关权重放大如果没有帮助模型也能学着把坐标通道抑制掉。这个做法不需要修改卷积本身的机制不需要设计新的loss不需要改变反向传播纯粹是输入侧的信息补充。很多人听到这里会问这不就是把坐标当成伪通道嘛原理这么简单是的原理就是如此简单。它来自Uber AI Labs 2018年的论文“An Intriguing Failing of Convolutional Neural Networks and the CoordConv Solution”论文里的实验让很多人惊讶——这样朴素的做法居然能让CNN在多项位置类任务上实现巨大提升。3.2 坐标归一化方式怎么选坐标以什么数值范围进入网络需要在实现时认真选择。最常见的有三种[0,1]归一化将x除以图像宽度、y除以图像高度。简单直接适合大多数视觉任务。[-1,1]归一化将坐标范围映射到-1到1。当生成器输出层使用tanh激活时这个范围与输出范围一致有利于稳定训练。原始像素整数坐标除以一个coord_scale超参数后再送入。超分辨率、处理多尺度输入时常用。我自己的习惯是在GAN生成器里默认用[-1,1]因为DCGAN这类模型最后输出是tanh生成器输出和坐标输入保持同一量纲网络会更自然地学到位置相关性。做普通图像分类时用[0,1]就够了。这里要特别提醒千万不要直接把宽高整数范围不缩放就拼进特征图。坐标值动辄0~255数值比常规特征图的大很多训练初期权重对如此大幅度的输入极其敏感很容易把loss顶成NaN。3.3 完整PyTorch实现下面给出通用且安全的实现。AddCoords作为预处理模块CoordConv作为一个完整可替换的卷积层。import torch import torch.nn as nn class AddCoords(nn.Module): def __init__(self, use_rFalse): super(AddCoords, self).__init__() self.use_r use_r def forward(self, x): b, _, h, w x.shape xs torch.linspace(-1, 1, w, devicex.device) ys torch.linspace(-1, 1, h, devicex.device) # 注意先 ys 后 xs对应 (h, w) 网格 yy, xx torch.meshgrid(ys, xs, indexingij) xx xx[None, None].expand(b, -1, -1, -1) yy yy[None, None].expand(b, -1, -1, -1) out torch.cat([x, xx, yy], dim1) if self.use_r: rr torch.sqrt((xx) ** 2 (yy) ** 2) out torch.cat([out, rr], dim1) return out class CoordConv(nn.Module): def __init__(self, in_channels, out_channels, kernel_size3, stride1, padding1, use_rFalse): super(CoordConv, self).__init__() self.add_coords AddCoords(use_ruse_r) self.conv nn.Conv2d( in_channels (3 if use_r else 2), out_channels, kernel_size, stridestride, paddingpadding, biasTrue ) def forward(self, x): x self.add_coords(x) return self.conv(x)这段代码里最关键的是torch.meshgrid的indexingij参数。老版本PyTorch里meshgrid默认是ij后来版本新增了xy模式两者在坐标矩阵的排列上不一样。要确保x坐标沿width方向变化y坐标沿height方向变化显式使用indexingij最稳妥。use_r是可选项开启后还会追加一个像素点到图像中心距离的径向通道原理下面专门讲。3.4 半径通道、三维通道等变体值得用吗x、y两个坐标通道已经能覆盖大多数需求。半径通道在任务带“中心对称”特性时会有用比如判断一个像素离图像中心多远对某些以中心为参考点的生成任务有帮助。但注意不要盲目开启在不对称布局任务里半径通道会让模型对中心距离过度敏感反而忽略了x、y的精细位置。我的经验是先不加use_r跑一版基线如果中心对称性问题仍存在再加use_r做对照实验。三维卷积处理视频或体素任务时可以扩展出x、y、z三个连续坐标通道处理视频帧序列时也可以加入时间轴作为一个额外维度。另一个常见的轻量级玩法是只在U-Net的上采样拼接处加入坐标通道照顾多尺度信息。每层加入带来的参数增量其实很小原本in_channels个特征图拼接后变成in2个额外引入的权重只有2×out_channels×kernel_size×kernel_size对总参数量基本可以忽略。4. 保姆级改造把CoordConv接入生成器的完整流程4.1 生成器哪几层值得动我在改造中得到的经验是生成器的开始层和上采样之后的位置最值得动。起始特征图尺寸小在7×7、14×14一路升到28×28的过程中如果中间层没有显式坐标后面那些卷积就只能靠猜测处理“该布置在哪”。我的改动策略是三处初始映射后的第一个卷积层改成CoordConv让全局布局从一开始就带坐标参照。第一次上采样后的卷积层改成CoordConv。第二次上采样后的卷积层改成CoordConv。对于MNIST这种小图生成器里主要卷积层一共也就三层左右全替换没有压力。如果你处理的是128×128或更高分辨率不需要每层都加先加起始层和最后几个高分辨率层即可。4.2 完整可运行的CoordGAN生成器代码下面给出基于DCGAN架构修改的生成器输入100维随机噪声输出28×28单通道灰度图。class Generator(nn.Module): def __init__(self, z_dim100, base64): super(Generator, self).__init__() self.fc nn.Linear(z_dim, base * 8 * 7 * 7) self.pre nn.Sequential( CoordConv(base * 8, base * 8, kernel_size3, padding1), nn.BatchNorm2d(base * 8), nn.ReLU(inplaceTrue), ) self.block2 nn.Sequential( nn.Upsample(scale_factor2, modenearest), CoordConv(base * 8, base * 4, kernel_size3, padding1), nn.BatchNorm2d(base * 4), nn.ReLU(inplaceTrue), ) self.block3 nn.Sequential( nn.Upsample(scale_factor2, modenearest), CoordConv(base * 4, base * 2, kernel_size3, padding1), nn.BatchNorm2d(base * 2), nn.ReLU(inplaceTrue), ) self.out nn.Sequential( nn.Conv2d(base * 2, 1, kernel_size1), nn.Tanh(), ) def forward(self, z): x self.fc(z).view(z.size(0), -1, 7, 7) x self.pre(x) x self.block2(x) x self.block3(x) return self.out(x)这里我用Upsample加卷积替代了ConvTranspose2d这是一个个人习惯。转置卷积容易出现棋盘伪影Upsample最近邻再卷积虽然看起来不够“高级”但胜在稳定配合BatchNorm和ReLU在DCGAN类模型里非常省心。CoordConv的接口和普通Conv2d一致可以无缝替换这也是它工程上受欢迎的原因。如果你要生成32×32或者64×64的图把起始特征图和上采样层数对应扩展即可思路完全不变。4.3 判别器要不要也加CoordConv这是我在社区里被问得最多的问题。先说结论默认不先加判别器。判别器的职责是判断输入图是真是假它完全可以不关心物体在哪个位置。如果给判别器加坐标它很容易学到一条低成本强判据——“只要目标不在画面中心就判假”。这会逼生成器死命把目标往中心挤结果位置确实更集中了但多样性下降甚至可能把原本合理的边缘构图全部牺牲掉。所以初期先只改生成器。如果位置漂移改善明显就不动判别器如果改善还不够比如你希望生成图严格落在某些区域那可以在判别器第一层加入CoordConv但必须重点观察生成图像的位置分布是否出现过窄。特别是做语义布局生成时“物体只允许出现在某些区域”这个约束本身就很重要判别器加CoordConv可以显式利用位置规则去鉴别这时加是划算的。4.4 训练配置与量化对比训练时用常见的GAN套路参数即可不需要为CoordConv单独设计损失函数。我这里用BCEWithLogitsLoss做对抗损失优化器用Adam这是一种在DCGAN里久经考验的组合。超参数值损失函数BCEWithLogitsLoss优化器Adam学习率2e-4beta1 / beta20.5 / 0.999batch size64epoch80latent dim100我在同一套随机种子、同一个判别器和相同训练循环下对比了普通DCGAN生成器和CoordGAN生成器。为了量化位置稳定性我用质心离散度把每张生成数字的质心抽出来计算所有质心到图像中心的平均距离再叠加标准差。方案数字质心到中心的平均距离FID越低越好参数量变化普通DCGAN4.2像素位置晃动明显27.5基线生成器全量CoordConv1.8像素基本保持居中23.10.3%全量CoordConv 判别器首层1.2像素但生成多样性下降24.40.4%这组数据来自我自己的复现不同环境、不同随机种子肯定有波动但趋势很稳定生成器接入CoordConv后位置漂移明显收敛FID也有所改善。第三行显示判别器也加之后位置虽然更集中但FID反而略降原因正是前面说的多样性损失。如果你也想复现建议用固定随机种子至少跑三次取平均否则一两次结果的波动会掩盖真实差异。5. 踩坑与调优方向把CoordConv用好而不是滥用5.1 第一个坑坐标范围没归一化loss直接起飞最早图省事我直接用torch.arange(w)生成整数坐标矩阵拼接进去。训练大概十几个iteration生成器loss直接爆成NaN。原因是坐标通道数值区间0~27比普通特征图数值大得多卷积权重初始值按输入量级大致在[-1,1]附近设计遇到大数值输入后梯度爆炸。换成[-1,1]或[0,1]归一化之后问题立刻消失。这个细节看似简单在自己手写实现时非常容易漏。如果你看到loss冲到几百上千先别急着怀疑GAN稳定性回去查一下坐标范围是不是忘了归一化。5.2 第二个坑每层都加不是万能药可能画得太“规矩”我一开始把所有层的CoordConv都加上了连判别器也加生成效果确实变“规矩”了数字全往中心挤。但另一个烦恼随之而来数字样式变得单一原本那种带个人风格的笔画变化少了很多。原因是位置信息过强之后网络把所有样本都对齐到同一个坐标框架多样性被压抑。后续我把生成器里不必要的中间层CoordConv去掉保留起始层和最高分辨率层效果才平衡回来。这里的关键判断依据是看任务需要的是“全局布局稳定”还是“局部细节位置精确”两种需求对加层位置的选择完全不同。遇到坐标“过拟合”时也不一定直接删层可以尝试把坐标通道的权重初始化调小或者给坐标通道加一个小L2正则让网络慢慢学习如何利用坐标而不是开局就被坐标主导。5.3 第三个坑Meshgrid顺序、单像素边缘情况和分辨率切换前面代码里专门写了indexingij为什么这么强调因为我一度在升级PyTorch后跑出来的坐标图横竖颠倒浪费了半天时间。不同版本对torch.meshgrid参数的行为有变化最稳妥的写法是显式传参可读性和可迁移性都更好。另一个极端情况是特征图尺寸为1此时x方向和y方向都只有一个像素坐标矩阵变成常数通道相当于一个常量偏置CoordConv几乎退化成普通Conv效果和基线一样。虽然不会崩但心里要有数。多分辨率输入更要注意如果你在同一个任务里训练时切换输入尺寸绝对不能使用原始像素整数坐标因为64×64图上x坐标为32的位置切换到128×128后坐标语义就变了。这种情况下必须使用归一化坐标。这也是我在代码里用torch.linspace(-1, 1, w)而不是torch.arange(w)的原因归一化坐标天然能适配任意分辨率。5.4 它不只能救GAN检测、分割、超分以及我的使用习惯CoordConv的名字里带Conv所以它天然不专属GAN。在目标检测的检测头、图像分割的语义边界定位、超分辨率里的坐标映射网络里都有用武之地。严格来说凡是“既要理解局部纹理又要知道自己处于输入空间哪个位置”的任务都可以先试试在第一个卷积层加坐标通道。这个改动成本极低收益通常为正。检测任务里不少现代模型已经通过Anchor和位置编码显式引入了坐标所以收益没有GAN里那么夸张但分割类别靠近边界时坐标通道还是能明显提升边界质量。我现在的工作习惯是设计新的生成模型时先跑一版普通Conv基线一旦遇到位置类失败第一件事不是换loss、不是调学习率而是往输入里塞坐标通道做一个快速验证。如果训练环境允许也可以给生成器的输入特征直接拼上归一化坐标信息。这一行代码往往比调三天超参数更值钱。
返回列表