ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

GAN、DCGAN、ACGAN全解析:从对抗原理到图像生成实战

GAN、DCGAN、ACGAN全解析:从对抗原理到图像生成实战 第一次听说生成对抗网络这个概念时我的第一反应是这是认真的吗。两个神经网络放在一起互相对抗一个造伪一个识伪到最后造假的那个不仅没被抓反而越练越精甚至能生成难以分辨的图像。这套逻辑放在十年前大多数人都觉得只存在于设想里但2014年Ian Goodfellow把它变成了现实。我从GAN入门到DCGAN、ACGAN一路学下来最大的感受是这三个模型难度层层递进但核心思想始终是一条线——让生成器在判别器的逼迫下逼近真实数据分布。这篇文章是我整理的学习笔记从直觉、数学原理、架构设计到复现踩坑一次性覆盖GAN、DCGAN、ACGAN三兄弟的完整脉络适合刚接触生成模型的读者照着学。1. 为什么对抗能生成数据先建立直觉再碰公式1.1 伪造者与鉴定师的故事理解GAN最顺滑的方式是先放弃数学把它想成一场伪造者与鉴定师的博弈。假设你是一个制造假画的伪造者目标是画出足以骗过专家的作品专家的工作则是分辨眼前这幅画到底是真迹还是赝品。一开始你的画很粗糙专家一眼就能识破。但每次被识破之后你都会从专家的判断中学到哪里露出了马脚于是下一幅画就更精细一点。与此同时专家也会不断见到更逼真的赝品被迫提高自己的鉴别能力。两人就这样互相较劲、一起进步。直到某一天你的画和真迹没有任何肉眼可见的差别专家只能靠抛硬币来决定真假——这就是GAN追求的最终状态。在这个故事里伪造者就是生成器GeneratorG专家就是判别器DiscriminatorD。生成器接收一个随机噪声向量把它加工成一张图像判别器接收一张图像可能是真图也可能是生成器伪造的图输出一个0到1之间的概率表示这张图是真实的概率。训练的过程就是让这两个角色持续对抗、交替提升。1.2 两个角色的数学职责把故事翻译成数学语言生成器和判别器各自有一个明确的职责。生成器 G(z) 做的事是输入一个从简单分布中采样的噪声 z常见做法是100维的标准正态分布输出一个与真实数据同形状的伪样本 G(z)。这里的本质是学习一个从低维噪声空间到高维数据空间的映射相当于在数据分布中找到一张可导航的地图。判别器 D(x) 做的事是输入一个样本 x输出一个标量 D(x) 表示该样本来自真实数据分布的概率。当 x 来自真实数据时D(x) 希望接近1当 x 来自生成器时D(x) 希望接近0。所以判别器实际上是在训练一个二分类器只不过它的训练数据是真图假图的混合流。有一点很多人初学时容易忽略生成器并不直接看到真实图片它的唯一信息来源是判别器给出的判分以及这个分数对自身参数的梯度。也就是说生成器是通过欺骗判别器这个间接目标来学会生成图像的而不是通过逐像素对比真实图。这一点决定了后面所有训练技巧的出发点。1.3 理想状态与纳什均衡当训练真正收敛时我们期望达到一个纳什均衡生成器已经强到能生成让判别器无法区分的样本判别器对任何输入的输出都稳定在0.5——它确实是在瞎猜了。但我必须在这里先泼一盆冷水在实际训练中这种完美均衡几乎从未出现过。更常见的情况是生成器生成的样本在肉眼看来已经相当不错而判别器仍然能从一些细微统计特征上找到破绽。实践中我们通常看样本质量而不是等D收敛到0.5来判断模型好坏。还有一个数学上的隐患当判别器真的输出0.5时它处于最没主见的状态此时它提供给生成器的梯度信号也是最弱的。这就是GAN训练不稳定问题的根源之一后面会展开讲。所以直觉上完美的均衡和实践中可用的均衡往往不是一回事。2. 目标函数与训练流程迈过最大的理解门槛2.1 极大极小博弈公式拆解GAN的核心目标函数是一个极大极小博弈论文原文写成min_G max_D V(D, G) E[log D(x)] E[log(1 - D(G(z)))]其中第一个期望是对真实数据分布采样的 x 求的第二个期望是对噪声分布采样的 z 求的。从判别器的视角看它想最大化这个函数把真图判成真log D(x) 趋近0把假图判成假log(1 - D(G(z))) 趋近0因为此时 D(G(z)) 趋近0log(1-0)log10。两个项都越大越好所以 D 是在做梯度上升。从生成器的视角看它想最小化这个函数。但注意第一项真实数据的期望对生成器来说是不起作用的——生成器管不着真实数据的 log D(x)所以这一项在生成器的梯度中是没有贡献的。生成器能影响的只有第二项 log(1 - D(G(z)))它希望这一项尽量小也就是希望 D(G(z)) 尽量接近1让判别器把假图误判成真图。所以最终的局面是D 想区分真伪G 想混淆真伪。目标函数的对抗性就藏在同一个式子里这也是生成对抗这个名字的来源。2.2 交替训练的具体步骤目标函数清楚了训练流程反而比想象中简单。每次迭代做下面几件事从真实数据集中采样一个 minibatch 的真实图片。从标准正态分布中采样一个 minibatch 的噪声向量用生成器生成对应数量的假图。把真图和假图一起喂给判别器计算判别损失用梯度上升更新判别器参数。再重新采样一批噪声用当前刚更新过的判别器的梯度去更新生成器参数。这里有一个关键细节第4步重新采样噪声时用的是更新过一次的判别器来提供梯度。这保证了生成器是在和当前最强版本的专家过招而不是在和旧版本过招。另一个新手容易忽略的点在每一轮迭代里判别器和生成器各自只更新一步而不是先让判别器训练到完全收敛再训练生成器。如果让判别器彻底收敛它会变得过于强大给生成器提供的梯度会趋近于零生成器就彻底学不动了。这个度非常微妙也是后面训练技巧大量出现的直接原因。下面是我在学习时整理的一段伪代码配合上面的步骤看会更直观for epoch in range(num_epochs): for batch in real_dataloader: # 1. 准备真实图片和噪声 real_images batch.to(device) z torch.randn(batch_size, latent_dim).to(device) fake_images generator(z) # 2. 更新判别器最大化 log(D(real)) log(1 - D(fake)) d_loss -torch.mean(torch.log(discriminator(real_images) 1e-8) torch.log(1 - discriminator(fake_images.detach()) 1e-8)) d_optimizer.zero_grad() d_loss.backward() d_optimizer.step() # 3. 更新生成器最小化 log(1 - D(G(z))) z torch.randn(batch_size, latent_dim).to(device) fake_images generator(z) g_loss torch.mean(torch.log(1 - discriminator(fake_images) 1e-8)) g_optimizer.zero_grad() g_loss.backward() g_optimizer.step()2.3 新手最常困惑的符号问题为什么生成器要用 -log(D) 而不是 log(1-D)上面伪代码里的写法其实和很多开源代码不一样。绝大多数现代实现里生成器的损失并不是按论文原始公式直接算 log(1 - D(G(z)))而是用 -log(D(G(z)))。原因要从梯度大小说起。训练初期判别器很容易分辨真图和粗糙的假图D(G(z)) 的值通常很小比如0.01。此时 log(1 - 0.01) 约为 log(0.99) ≈ -0.004这个损失非常小导数也极平缓。也就是说在生成器最需要大幅度改进的时候梯度反而最小学习几乎停滞。这种情况叫saturating gradient饱和梯度。改成 -log(D(G(z))) 之后D(G(z))0.01 时-log(0.01) ≈ 4.6损失很大梯度也很大。生成器在初期能获得足够的推动力去快速改进。等生成器变强D(G(z)) 接近1-log 趋近0梯度变小自然放慢进步节奏。这个改动并不改变博弈的最终目标只是把学习曲线变得更好走了。还有个相关的小技巧是标签反转训练判别器时把真图标签设为0、假图标签设为1生成器的目标变成让判别器输出接近1。原理类似本质都是避免饱和梯度。我个人推荐直接用 -log(D(G(z))) 的写法语义更清晰。3. DCGAN用卷积把GAN从玩具变成工具3.1 为什么全连接网络搞不定图像原始的GAN用的是全连接网络在MNIST这种28x28小灰度图上能跑出不错的效果但换到稍微大一点的图片比如64x64的彩色图就明显力不从心。全连接网络处理图像时需要把每个像素都展开成一维向量这会带来两个问题。一是参数量爆炸。64x64x3的输入展平是12288维第一层全连接的参数就是百万量级训练又慢又容易过拟合。二是丢失空间结构。图像里相邻像素有关联、远处像素相关性弱这种空间局部性全连接层完全意识不到它只会把输入当作一堆没有排列顺序的数字。卷积神经网络刚好天生契合图像它通过滑动窗口提取局部特征天然保留了空间位置关系而且参数共享大大减少了参数量。把卷积引入GAN就是DCGANDeep Convolutional GAN的核心思路。这篇论文发表于2015年作者是Alec Radford等人它的贡献不只是把卷积塞进GAN还总结了一整套可复现的架构规范这些规范直到今天仍被广泛沿用。3.2 DCGAN的架构规范DCGAN对生成器和判别器的设计提出了很具体的约束我把关键几条列出来生成器里用转置卷积Transposed Convolution也叫反卷积实现上采样替代原来插值卷积的做法。从100维噪声开始先通过全连接层映射并重塑成4x4x1024的特征图然后连续做4次转置卷积逐步把空间尺寸翻倍最终得到64x64x3的输出。判别器里用步长为2的普通卷积替代池化层。池化会丢失位置信息而步长卷积可以在下采样的同时让网络自己学习如何压缩信息。两个网络都加批归一化Batch Normalization但生成器的输出层和判别器的输入层不加。输出层不加是为了让tanh激活函数的输出范围-1到1不被归一化破坏输入层不加是因为判别器需要看到原始输入分布。生成器内部激活函数用ReLU输出层用tanh判别器内部全部用LeakyReLU泄漏系数0.2。LeakyReLU在负半轴保留了一个小坡度避免判别器出现神经元死亡。这套规范解决了原始GAN训练极其不稳定的问题。转置卷积的每一层都可以看作在做特征到图像的逐步放大我在实际实验中发现前几轮迭代时生成器会先学到图像的整体色调然后逐步出现轮廓最后才有清晰的纹理这个过程是肉眼可见的。3.3 训练参数与效果DCGAN论文里给出的默认参数相当好使至少在我的实验中它们是第一组让我稳定跑出清晰图像的参数参数取值说明优化器Adam两个网络都用学习率0.0002比常规分类任务的默认值小很多Adam的beta10.5关键默认0.9会导致训练震荡batch size128论文默认值小显存可降到64噪声维度100从标准正态分布采样生成器输出激活tanh输出像素范围映射到[-1, 1]这里特别说一下beta10.5的原因。Adam默认的beta10.9意味着梯度的一阶矩估计会保留很长时间的惯性但在GAN这种动态博弈中生成器和判别器的优势方向会在训练中频繁反转。如果动量太大参数更新会滞后于当前的对抗形势造成震荡甚至发散。beta1降到0.5之后动量衰减更快参数能更及时地响应梯度变化训练稳定性会明显提升。我在第一次复现时就是因为偷懒用了默认参数结果损失曲线像心电图一样上下乱跳后来改成0.5才稳定下来。还有一个小细节输入图像的预处理要把像素从[0,255]映射到[-1,1]对应tanh输出范围。很多新手栽在这上面——忘了归一化或者用ReLU作为生成器输出导致生成图像范围不对。4. ACGAN给生成过程加上条件约束4.1 类别条件从哪里来到了DCGAN这一步我们能生成逼真的图像了但有一个问题我们完全无法控制生成的内容。在MNIST上训练一个DCGAN它有时生成3有时生成7你无法指定它生成一个特定的数字。解决这个问题的思路很直接把类别信息作为条件输入到生成过程中。这样生成器学到的映射就从噪声 - 任意图像变成了噪声 类别标签 - 该类别的图像。实现条件输入的最朴素方式是CGANConditional GAN把类别标签经过embedding之后和噪声向量拼接在一起送给生成器同时把标签信息也拼到判别器的输入里。ACGANAuxiliary Classifier GAN采用了不同的思路——它不在判别器输入端拼标签而是给判别器增加一个辅助分类头让判别器同时完成两件事判断真假、判断类别。4.2 辅助分类器损失的作用ACGAN的损失函数由两部分组成对抗损失Adversarial Loss和分类损失Classification Loss。对抗损失和普通GAN一样L_S E[log P(Sreal | x_real)] E[log P(Sfake | x_fake)]判断样本真假。分类损失则是L_C E[log P(Cc | x_real)] E[log P(Cc | x_fake)]要求判别器对真实图像和生成图像都能正确预测类别。注意生成器的输入是噪声 z 和类别标签 c所以 x_fake G(z, c)分类损失要求判别器把这张图分类成 c。在训练时判别器最大化 L_S L_C生成器则最大化 L_C - L_S。这里生成器最大化 L_C 的含义是它希望生成的图像在类别上足够清晰让判别器的分类器能稳定识别出来。这带来一个额外的强梯度信号——生成器不仅要骗过判别器的真伪眼还要满足判别器的分类眼从而被迫学会生成每个类别最具辨识度的特征。我在跑ACGAN的时候最直观的感受是收敛速度变快了。同样的迭代次数下ACGAN生成的图像类别正确率明显比无条件的DCGAN高而且整体画质也更好。分类损失相当于给生成器提供了一条监督学习的辅助通道缓解了纯对抗训练的梯度信号稀疏问题。4.3 ACGAN与CGAN的对比初学者经常把ACGAN和CGAN搞混因为两者都用了类别条件。我整理了它们的核心区别对比项CGANACGAN条件信息输入位置生成器和判别器都拼接标签只拼接生成器判别器加分类头判别器的任务只判断真假判断真假 预测类别额外损失无分类损失 L_C条件信息对生成的约束强度依赖拼接后的特征融合依赖分类损失的反传梯度适用场景有标签数据集想简单加条件有标签数据集希望提升生成质量和类别可控性从实际效果看ACGAN的生成图像在类别一致性上通常更稳定因为分类损失直接对类别可判别性做了优化。而CGAN的结构更简单改动成本更低在某些任务上与ACGAN差距并不大。我在做实验时倾向于先试CGAN确认条件生成能跑通再换ACGAN提升质量。ACGAN也有一个需要我们注意的细节分类损失和对抗损失的量级需要平衡。如果数据集类别很多且类别间区分困难分类损失可能会压过对抗损失让判别器过度关注这张图是哪个类而忽略了这张图真不真。在实践中如果发现生成图像类别正确但细节模糊可以考虑降低分类损失的权重。5. 复现GAN系列模型时踩过的坑5.1 模式坍塌生成器偷懒的后果模式坍塌Mode Collapse是我在训练GAN时遇到的第一个大坑。表现是生成器找到了一两个能稳定骗过判别器的万能样本于是反复输出相似的图像完全丢失了数据分布中其他模式。比如MNIST里生成器可能只会生成那两三个数字其他数字完全不会。为什么模型会走到这一步因为从生成器的角度看骗过判别器是唯一目标如果某个退化的输出恰好能最大化这个目标梯度下降就会强化这个方向。判别器如果无法有效识别这种重复模式的破绽就会形成恶性循环。我踩坑时的排查过程是这样的先固定一个随机噪声向量每隔几百步用生成器生成图像保存下来发现某段时间所有噪声都输出几乎相同的结构这就确认是模式坍塌了。解决手段我试过几种一是调大判别器的学习率让判别器更快识别重复样本二是给判别器输入加上小噪声扰动逼生成器扩大覆盖范围三是直接在架构上换ACGAN让分类损失迫使生成器覆盖多个类别。第三种对我的实验最有效。5.2 判别器过强导致的梯度消失与模式坍塌相对的另一类问题是判别器太强。症状是判别器损失迅速降到接近0而生成器的损失几乎不再变化生成图像一直停留在模糊状态。原因是当判别器能100%区分真图和假图时它对假图输出接近0此时生成器的梯度要么消失如果用 log(1-D) 形式要么被不稳定的数值主导如果用 -log D 形式网络无法有效学习。我常用的对策有三个。第一个是标签平滑Label Smoothing把真实图像的标签从1改成0.9给判别器留一点不自信的空间避免梯度过于极端。第二个是降低判别器的更新频率比如判别器每更新1次生成器更新3次让生成器有更多机会追赶。第三个是控制生成器和判别器的复杂度差距——不要一上来就给判别器一个比生成器大得多的模型两者的武力值要保持在同一水平。5.3 超参数与工程细节的经验汇总除了上面两个大坑实践中还有不少零碎的工程细节我把踩过的坑和对应的策略整理成了一张表现象可能原因建议策略训练震荡严重Adam的beta1太大设为0.5生成图像全是噪声输出激活函数/归一化范围不对输出用tanh输入归一化到[-1,1]损失不降但图像在变好GAN损失与图像质量非线性相关以人工观察为准别只盯loss某一类图像总是消失类别不平衡或模式坍塌用ACGAN、降低batch size显存不足生成器和判别器太大降低通道数倍数或用小图训练测试时图像质量崩坏BatchNorm用错模式推理时用running statistics而非当前batch最后一条值得单独强调训练时BatchNorm用的是batch内统计量但推理时应该用训练阶段累计的running mean和running variance。很多框架会自动处理这个切换但如果你手动实现推理流程很容易踩中训练效果好、测试效果差的奇葩问题。还有一个我养成的习惯训练时固定一个或多个噪声向量每隔固定步数用生成器输出一次图像并保存。这样可以直观看到生成器从一个噪声点出发的学习轨迹比只盯loss曲线靠谱得多。GAN的loss曲线和图像质量并不总是正相关有时候loss几乎不动样本却在肉眼可见地变好。6. 一个容易让新手空转的坑GAN网络与GaN半导体的命名碰撞6.1 搜索资料时遇到的大量无关内容学习GAN的过程中我遇到过一件很尴尬的事在搜索引擎里输入GAN这个关键词排在前面的结果经常不是生成对抗网络而是关于氮化镓GaN半导体的内容——比如GaN HEMT、GaN图腾柱电路、甚至质子单粒子效应之类的器件物理研究。这不是个例。GaN是第三代半导体材料近几年在快充、电源管理、射频芯片领域非常热门学术讨论和产业新闻都很多而GaN这个缩写和生成对抗网络GAN在字母大小写上只有细微差别全大写 vs 首字母大写搜索时往往会被混为一谈。这类内容本身没问题但对于一个刚入门、想找GAN学习资料的初学者来说很容易被这些完全不相干的搜索结果带偏白白消耗大量时间。我的建议是搜索时尽量用完整的中文术语生成对抗网络或者英文全称Generative Adversarial Network再或者带上具体的模型名如DCGANACGANWGAN。检索精度会高很多。如果检索英文资料可以加上deep learning或computer vision做限定能有效过滤掉半导体方向的页面。6.2 给后来者的学习路线与资料建议结合我自己的学习过程我建议按这样的顺序入门先掌握基础的神经网络和反向传播理解卷积与转置卷积的区别然后按经典GAN - DCGAN - CGAN/ACGAN - WGAN - StyleGAN的顺序推进。每一篇论文都要搭配开源代码看只看论文容易在训练细节上迷路只看代码又容易陷入调参能跑但不懂原理的状态。实验环境方面一张中端显卡就能跑完DCGAN和ACGAN在MNIST、CIFAR这类小数据集上的完整训练不需要一开始就上大规模数据集。我甚至建议先在MNIST上把训练流程吃透再逐步换到更复杂的数据集。把最小可复现的流程跑通理解每一步在干什么比一次性追求复杂模型重要得多。最后我想说GAN系列模型是少有的理解了原理也未必能训好的模型很多问题必须亲自踩一遍才能长记性。我记录的这些坑多数是在被loss曲线折磨了好几个晚上之后才真正理解的。如果你在复现过程中遇到训练不收敛别急着怀疑模型写错先检查判别器和生成器的参数是否均衡、学习率是否过大、输入归一化是否正确——这三个问题占了我早期排查工作的八成以上。顺着这个思路走你会少走很多弯路。
返回列表