做图像生成的朋友应该都有过这种体验普通的生成对抗网络GAN训练完之后你朝潜在空间里随便采样结果一会儿出猫一会儿出狗完全没法控制它输出你想要的内容。比如我想让模型“只生成右转脸的人像”单纯靠调整输入噪声想都别想因为普通GAN根本没有接收指令的入口。2020 ML/DL 课程第 75 讲的重点正是解决这个痛点的条件生成对抗网络Conditional GAN。这一讲没有太多花哨的技巧核心就是把“随机生成”变成“可控生成”让生成结果严格服从我们给定的条件无论是类别标签、文字描述还是另一张图像。这篇文章我会把Conditional GAN的动机、架构、训练目标、典型应用和训练中踩过的坑一次讲透。适合对GAN已经有基础理解、但没系统整理过条件控制细节的同学阅读也适合想快速复现一个text-to-image或pix2pix小项目的人按图索骥。1. 为什么普通GAN玩不转“指定生成”1.1 无条件GAN的生成是“开盲盒”先回顾一下普通GAN的结构生成器G输入随机噪声向量z输出一张伪造图像判别器D输入一张图像输出它认为这张图是真实数据还是生成数据的概率。训练完成以后G确实学会了把z映射到训练集分布附近的点但问题在于z的每个维度并没有语义含义。你试过把z的某个分量从-1调到1观察生成图像的变化吗结果往往是整张脸都在变而不是某个属性在变。因为普通GAN的训练目标只有一个——让生成分布贴近真实分布它没有动力把z的各个维度整理成人能理解的控制项。所以你在采样时会发现这次生成猫下次生成狗运气好还会出现畸形个体。这在一些不需要精细控制的场景下够用比如随机生成人像做数据集扩充。但当你明确想要“戴眼镜的男性”“红色的鸟”“左边有树的街景”时普通GAN就彻底没辙了。问题本质是生成器缺少一个能接收用户意图的条件接口。1.2 给生成加一个“遥控器”Conditional GAN的解决思路非常直接在生成器和判别器的输入端都追加一个条件变量c让生成器变成G(z, c)判别器变成D(x, c)。此时生成器不仅要学会把z映射到看起来真实的图像还要让这张图像满足c的描述。c可以是一个one-hot类别向量比如猫0、狗1也可以是一段文本的嵌入向量甚至可以是一张语义分割图、一幅线稿或一张低分辨率图。条件决定了生成内容的方向噪声z则负责提供同一条件内部的多样性。用生活化的类比来说普通GAN像在漆黑房间里抓彩票你只知道奖池里有各种奖品但不知道抓到什么Conditional GAN给了你一本商品目录你可以指着目录上的某一项说“我要这个”然后再让噪声决定“这个”的具体款式和细节。1.3 条件c能承载哪些信息类型条件变量不是只能用来做类别控制它的表达能力比很多人想象中大得多。我通常把条件c分为四类条件类型表示方式典型任务类别标签one-hot向量或类别嵌入MNIST指定数字生成、指定物体类别生成文本描述预训练模型将句子编码为向量text-to-image按描述生成图片结构化图像语义分割图、边缘图、关键点热图pix2pix图像翻译、图像修复属性向量语义属性向量、人脸表情向量人脸属性编辑、姿态迁移不同条件类型决定网络结构设计的差异。类别标签只需要一个embedding层就能转成向量而文本描述可能需要一个较强的编码器结构化图像则要设计成UNet那样的图像到图像结构。条件c的本质是把“用户意图”编码成模型能消费的向量至于向量怎么得到反而是后面工程上最费功夫的部分。2. 核心架构拆解条件怎么接进生成器与判别器2.1 生成器端的三类条件注入方式最常见的做法是把条件c与噪声z拼接成一个长向量输入生成器的第一层。这个方案最简单我早期做MNIST条件生成时就是直接torch.cat再Linear效果也够用。但它的缺点是条件只在入口出现一次深层特征很快会被噪声和中间变换稀释。稍微进阶一点的做法是把条件嵌入向量在多个尺度上注入比如把条件映射成缩放系数和偏置系数作用在特征图的每个通道上类似StyleGAN的AdaIN机制。你甚至可以把条件向量拼到每一个上采样block的输入里强制模型在生成的不同阶段都“看见”条件。实验下来多层注入对文本条件尤其重要因为一句描述往往对应多个局部属性颜色、姿态、背景单点注入很容易丢失细节。当条件c是一张图像比如语义分割图或低分辨率图时生成器就不能再把条件当向量处理了通常需要把条件图像和当前特征拼在通道维度上或者直接用UNet的跳跃连接把条件结构从编码器送到解码器。这时候生成器更像一个“以条件为主干输入”的修复网络噪声反而退居次要位置。2.2 判别器端的条件处理细节很多人一开始会犯一个错误只把条件c塞给生成器判别器继续用D(x)判断真假。这会导致生成器很快学会忽略条件因为判别器根本没有检查条件匹配的能力它只会逼着生成器“看起来像真实图像”不会逼它“符合指定的类别或描述”。判别器有条件地工作常见的做法也有两种。第一种是拼接把图像特征和条件向量拼接在一起再送去输出层。收敛速度尚可但隐含的问题是图像特征和条件特征可能不在同一语义空间拼接后要让网络自己学习对齐关系。第二种是投影判别器projection discriminator数学形式更优雅先让图像特征和条件嵌入做内积再把结果和无条件logit相加。这个内积项本质上是在度量“图像特征与条件特征的方向一致性”对条件匹配的建模更直接。我在实现时优先用投影判别器并且把条件嵌入层换成可训练的Embedding训练稳定性明显好于简单拼接。2.3 配对数据怎么构造才不会把训练带偏Conditional GAN的训练数据必须是成对的真实图像x要配它对应的真实条件c。这个很容易想到但容易忽略的是负样本怎么构造。判别器的正样本是“真实图像真实条件”。负样本有两种。第一种是“生成图像该生成图像使用的条件”这个所有入门教程都会提第二种是“真实图像错误条件”比如把一张猫的图配上一个“狗”的标签。这一对负样本经常被漏掉。第二类负样本教给判别器一个关键能力检测图像与条件的不匹配。如果没有它判别器只会机械地判断“画质像不像真实照片”生成器就会钻空子生成一张高质量但类别随便的图。实际训练时我会在DataLoader里专门维护一个错误条件池每批数据随机打乱条件分配给真实图像保证错配样本和配对样本数量大致相等。3. 条件GAN的训练目标与流程搭建3.1 目标函数给对抗过程加条件条件GAN的目标函数可以写成min_G max_D V(D, G) E_{(x,c)~p_data}[log D(x, c)] E_{z~p_z, c~p_c}[log(1 - D(G(z, c), c))]对比普通GAN公式几乎没变只是所有出现x和z的地方都带上了条件c。生成器试图让D(G(z, c), c)的输出接近1也就是让伪造图像既真实又匹配条件判别器则试图把“真实图像真实条件”判定为1把“伪造图像条件”以及“真实图像错误条件”判定为0。理解这个公式的关键在于“条件匹配”被纳入了优化目标。判别器判断的不再只是图像来源而是“图像与条件的联合概率”。这也解释了为什么正负样本的构造如此重要判别器能学到什么样的决策边界完全取决于你给它看了什么样的对与错。3.2 生成器为什么要用替代损失理论上生成器要最小化log(1 - D(G(z, c), c))但实操中这个目标的梯度在训练早期非常平缓。原因是判别器刚训练时很容易识别出假图D的输出接近0此时log(1-D)的导数很小生成器拿不到有效梯度训练像原地踏步。更稳定的做法是让生成器最大化log D(G(z, c), c)也就是把“让判别器认为假图是真的”作为目标。这相当于给另一个方向的梯度信号。实现时只需要把生成器损失的真值标签从0翻成1。我在初版代码里老是忘记这一步结果训练十几个epoch生成的都是模糊色块后来改成不饱和损失才正常。如果要更激进地稳定训练最小二乘损失也是一个选择把判别器的输出当作拟合目标而不是概率理论上能缓解梯度消失并让生成样本更接近数据流形。3.3 一个可直接改的PyTorch训练循环下面给一段我在小项目里常用的训练循环骨架只保留核心逻辑for epoch in range(epochs): for x_real, c in dataloader: x_real, c x_real.to(device), c.to(device) # 1) 训练判别器 z torch.randn(x_real.size(0), z_dim).to(device) x_fake G(z, c).detach() real_out D(x_real, c) fake_out D(x_fake, c) wrong_out D(x_real, c[torch.randperm(len(c))]) # 真实图配错误条件 d_loss (F.binary_cross_entropy(real_out, ones) F.binary_cross_entropy(fake_out, zeros) F.binary_cross_entropy(wrong_out, zeros)) / 3 optimizer_D.zero_grad() d_loss.backward() optimizer_D.step() # 2) 训练生成器 z torch.randn(x_real.size(0), z_dim).to(device) x_fake G(z, c) fake_out D(x_fake, c) g_loss F.binary_cross_entropy(fake_out, ones) # 不饱和版本 optimizer_G.zero_grad() g_loss.backward() optimizer_G.step()注意两个地方。第一x_fake在训练判别器时要用detach()阻断梯度否则G的梯度会通过x_fake回传训练顺序会乱掉。第二判别器的平均损失不是简单地两两相加真实图配错误条件这项很容易被网络忽略建议把三个损失项单独打印出来观察如果wrong_out的loss一直不下降说明判别器还没学会识别条件错配后面条件控制大概率失效。3.4 让训练稳定的三个小改动第一标签平滑。把真实标签从1改为0.9左右防止判别器过分自信。第二判别器和生成器用不同的学习率我一般给判别器开lr1e-4生成器开lr2e-4让D稍微“迟钝”一点。第三梯度惩罚。在WGAN-GP的基础上叠加条件输入对判别器输入做插值并计算梯度范数。这些改动不会立即让效果突飞猛进但能显著降低训练过程中的振荡幅度。另一个容易被忽视的点是条件嵌入层的初始化。如果条件向量维度太小比如类别用了4维嵌入生成器很容易把条件信息忽略掉。我常用的经验是类别embedding维度不低于32文本向量维度不低于128图像条件因为本身信息量大所以不用额外升维。4. 值得动手复现的三个应用场景4.1 文本生成图像给一句描述让它画画text-to-image是我认为最能体现Conditional GAN价值的场景。任务输入是一句话比如“一只红色的鸟站在绿色树枝上”输出是符合描述的图像。这里条件c来自文本编码器不再需要人工标注类别模型要理解语言描述与视觉属性的对应关系。实操时最费工夫的是文本编码部分。早期通常用预训练的语言模型把整句话编码成一个向量再接上噪声一起输入生成器。但一句描述包含多个属性时单向量容易丢失空间关系。后来出现跨模态注意力机制让文本中的每个词和图像区域建立对齐这个思路已经是现在的主流方案。对初学者来说我的建议是先别追求复杂结构用预训练编码器拿全局向量做一遍理解条件注入和损失配合后再扩展注意力模块。4.2 图像翻译用分割图生成街景图pix2pix系列是条件GAN在图像翻译上的经典框架。此时条件c是另一张完整图像比如语义分割图每个像素标注“路面”“建筑”“天空”等类别生成目标是对应的街景照片。生成器结构通常会设计成UNet编码器压缩分割图提取结构信息解码器恢复纹理细节中间的跳跃连接把边缘和轮廓信息直接传给解码器所以生成图能保持输入分割图的布局。判别器用PatchGAN不判断整张图而是把图像切成若干个小patch对每个patch判断真假再取平均。这么做的动机是整体结构交给生成器的L1损失和分割图的逐像素对齐来约束判别器只需要聚焦局部纹理是否真实。如果整张图做判别模型容易只关注全局构图而忽略细小纹理PatchGAN则能把精力集中在地面材质、树叶纹理这类局部特征上。4.3 图像修复与属性编辑图像修复任务中条件c是带掩码或缺损的图生成器要做的是在掩码区域内补全内容同时保持补全结果与周边环境的衔接自然。这里条件信息其实就是损坏图像本身网络要理解上下文的语义关系。属性编辑则更接近条件GAN的最朴素形态比如输入一张人脸图条件向量为“戴眼镜”“微笑”生成器输出编辑后的人脸。这些任务有个共同点生成结果和条件之间是“强对齐”关系而不是“弱相关”。类别条件生成时同一类下允许各种姿态和背景变化但图像修复和翻译要求结构上一一对齐。面对强对齐任务单纯靠对抗损失不够通常要叠加感知损失或者L1损失否则模型会为追求局部真实性而偏离条件结构。5. 常见问题与排查技巧实录5.1 典型故障现象与排查速查表训练条件GAN比普通GAN更容易出怪问题因为涉及条件路径的有效性。我把常见现象整理成一个速查表现象可能原因排查建议生成图清晰但完全无视条件条件注入太弱或判别器没使用条件换成多层条件注入改用投影判别器检查错误条件负样本是否加入类别、属性张冠李戴配对数据有问题条件编码维度太小可视化训练数据的x-c配对是否正确增大embedding维度训练初期生成全是噪声生成器用了饱和损失梯度消失换成maximize log D的非饱和目标调大生成器学习率模式崩塌生成样本高度相似判别器过强生成器被压制降低D的学习率增加G每轮更新次数加标签平滑文本条件生成语义不匹配文本编码器太弱或文本特征被噪声淹没使用更强的预训练编码器训练时给文本特征加少量dropout并提高条件权重如果你加入了错误条件负样本但wrong_out的loss一直异常高我建议直接打印生成图片和条件标签手动核对“猫图是不是配了狗标签”。有次我调了一下午发现是数据加载时索引错位条件根本没和图像对齐所有问题都从这里来。5.2 评估环节IS与FID怎么选更靠谱很多同学训练完只看生成图“顺不顺眼”这样太主观。Inception Score用预训练分类网络给生成图分类同时看类别置信度高低和类别多样性分数高意味着图像清晰且多样。但IS有个明显缺陷它不看生成分布与真实分布的距离只评估生成图本身所以更偏向于评价“生成的图作为独立样本有多好”。FID则是把真实图和生成图分别送入Inception网络提取高层特征后计算两个特征分布之间的Wasserstein距离。它直接度量生成分布与真实分布的差异对模式崩塌比IS敏感得多。我用经验建议评估条件GAN时优先看FID而且样本数量不要低于5000张否则统计量不稳定条件生成场景里还可以额外计算一个“条件准确率”用一个简单分类器对生成图分类看分类结果和给定条件是否一致这个指标能直接暴露条件路径是否失效。5.3 几条来自实践的个人心得我自己实践中最深的体会是条件GAN调试的第一顺位一定是条件匹配而不是图像清晰度。生成图模糊一点可以靠调损失改善但如果条件信息没有被生成器接收后面所有优化都白费。所以每改一次网络结构我会先做一个小实验固定条件为某个类别采样多张图肉眼检查这批图是否明显符合该类。如果连这个都过不了就先别上复杂数据集。第二个体会是训练过程要分别记录三部分判别器损失真实配对损失、假图配对损失、错误条件配对损失。正常情况三者应该同步下降如果真实配对损失保持很低而错误条件配对损失不下来意味着判别器偏科了它只顾着识别画质没学会理解条件。第三个体会是关于条件向量维度和注入位置的。条件信息不是越多越好条件维度太高而数据集不大时生成器反而会把条件通道当噪声源。我在小数据集上通常从较低维度开始以每次40%、25%倍数的递增幅度观察效果选定合适维度后再固定。这条经验从MNIST到人脸属性编辑都适用。最后一个小技巧如果启动训练时发现生成器损失一直不怎么动先别急着调学习率。检查一下是不是忘了把真实标签替换到生成器目标里。这个错误我犯的次数最多每次都是神色凝重地调参半小时然后发现代码里依然写着zeros标签。把非饱和目标写对往往比调任何优化器参数都管用。