2019-08-10
聊生成对抗网络时,很多人一开始会被各种图绕晕。明明只想搞懂一句话,结果一眼望过去全是 `G`、`D`、真假标签、双向箭头、回环路径,脑子直接开始打结。
原文开头把 CNN、RNN、GAN 放在一起提,其实是很典型的那个时代的深度学习视角。前两个大家都比较熟,而 GAN 最特别的地方在于:
它不是单边学,而是两边互相顶着练。
其中:
G 是生成器 GeneratorD 是判别器 DiscriminatorD 的任务很直接:
10而 G 的任务就有点“使坏”的意思:
D 骗过去你如果把整个过程翻成大白话,其实就一句:
先把 D 练成一个很会辨别真假的裁判,再逼 G 把假图做得越来越像真图,直到裁判都分不清。
这也就是原文里那句意思的核心:
D 拟合到 1G 生成负样本D 拟合到 0G 生成更像真的东西所以训练 GAN 时,表面看像是两条线,真正最有戏的,其实是那条“生成器通过判别器拿反馈”的路径。
普通卷积网络大多是在做识别:
GAN 不一样,它更像是在做“造东西”。
也就是说,它不是只会判断:
这是猫还是狗
它还想学会:
怎么生成一张像猫或者像狗的图
这个转变很大。因为一旦模型开始学“生成”,很多之前只是识别层面的任务,就会自然延展到:
CycleGAN 正是这个方向里非常有代表性的一个版本。
原文这里的重心很明确,不是泛泛讲 GAN,而是以 CycleGAN 为例说明:
生成对抗网络可以把风格迁移这件事往前推很多。
如果普通 GAN 的基本单元是:
GD那么 CycleGAN 会扩成:
G_AB:把 A 域翻成 B 域G_BA:把 B 域翻成 A 域D_A:判断 A 域真假D_B:判断 B 域真假也就是说,它不再是“一条单向生成线”,而是形成了两个方向的来回翻译。
这里最值得强调的一点,也是原文特别提醒的点:
图里上下两条看起来像分开的路线,但并不是四套独立网络。
换句话说:
G_AB,其实是同一个 G_ABG_BA 也是同一个 G_BA这点如果不看源码,只看示意图,很容易误会。
关键就在那个 Cycle。
它不是只要求:
而是要求:
所以原文里用更直觉的话把训练路线概括成四条:
A → AA → B → AB → BB → A → B从今天更常见的术语来看,这里面大致对应两类约束:
cycle consistencyidentity / self mapping 风格的稳定性约束但如果你不想记术语,其实抓住一句话就够了:
翻过去可以,翻回来也得说得通。
这个要求一加上,网络就不只是“会模仿另一个域的样子”,而是开始被要求:
迁移时别把原图的关键信息搞没了。
原文有一句话很抓重点:
CycleGAN 能对风格做双向迁移。
这句话看着平平,实际上是它最亮眼的卖点之一。
比如你有两个域:
那 CycleGAN 学到的,不只是:
还包括:
而且它希望这两个方向都别太离谱。
这就让 CycleGAN 比很多“只会单边翻译”的结构更自由,也更适合风格迁移这种来回都讲得通的任务。
原文里给出的对比非常直觉:
Pix2Pix 更偏单向CycleGAN 更偏双向这个说法虽然是偏工程直觉的总结,但很好懂。
你可以把 Pix2Pix 想成:
给定一个输入条件,朝目标输出方向做翻译。
而 CycleGAN 更像:
不仅能朝一个方向翻,还能朝反方向翻,而且中间要保证闭环。
从论文视角看,Pix2Pix 是典型的 paired image-to-image translation,而 CycleGAN 的代表性标签则是 unpaired image-to-image translation。原文没有把这层术语展开得很学术,但它抓住了更容易让人记住的差别:
一个更像定向翻译,一个更像双向来回翻。
原文后半段有个很鲜明的判断:
CNN 的主干结构已经比较成熟,而 GAN 还有很多没被挖完的空间。
这个判断放在原文语境里,其实很好理解。
因为当时的视觉主干,大体已经走过了:
这些主路线。
而 GAN 系列还在不断冒出新结构:
也就是说,GAN 的想象力更多来自“结构怎么玩”,而不只是“主干再加几层”。
这也是为什么 CycleGAN 会让人觉得眼前一亮。它不是单纯堆参数,而是换了思路:把映射关系做成回环,直接把风格迁移这件事推到一个更灵活的位置上。
如果你想更有手感一点,可以先看一个非常小的 PyTorch 生成器例子。它不是完整训练代码,但足够帮助理解 G 在干什么。
import torch
import torch.nn as nn
class TinyGenerator(nn.Module):
def __init__(self, z_dim=100, img_dim=28 * 28):
super().__init__()
self.net = nn.Sequential(
nn.Linear(z_dim, 256),
nn.ReLU(inplace=True),
nn.Linear(256, 512),
nn.ReLU(inplace=True),
nn.Linear(512, img_dim),
nn.Tanh()
)
def forward(self, z):
return self.net(z)
z = torch.randn(8, 100)
G = TinyGenerator()
fake_imgs = G(z)
print(fake_imgs.shape) # [8, 784]
这个小网络做的事很纯粹:
至于它像不像真图,就要交给 D 去挑刺了。
CycleGAN 最关键的不是把图翻过去,而是翻过去以后还能不能回来。
下面这个小例子专门演示 cycle consistency 的味道:
import torch
import torch.nn.functional as F
# real_A -> G_AB(real_A) -> fake_B -> G_BA(fake_B) -> rec_A
real_A = torch.randn(2, 3, 256, 256)
rec_A = torch.randn(2, 3, 256, 256)
cycle_loss_A = F.l1_loss(rec_A, real_A)
print("cycle_loss_A:", cycle_loss_A.item())
这里的意思非常简单:
real_A 是原始 A 域图像rec_A 是绕一圈回来的结果这就是为什么 CycleGAN 不只是“变风格”,而是“变完还得能圆回来”。
再看一个更贴近图像翻译的最小块:
from tensorflow import keras
from tensorflow.keras import layers
def res_block(x, filters):
shortcut = x
x = layers.Conv2D(filters, 3, padding="same")(x)
x = layers.ReLU()(x)
x = layers.Conv2D(filters, 3, padding="same")(x)
return layers.Add()([shortcut, x])
inputs = keras.Input(shape=(256, 256, 3))
x = layers.Conv2D(64, 7, padding="same")(inputs)
x = layers.ReLU()(x)
x = res_block(x, 64)
model = keras.Model(inputs, x)
model.summary()
这不是完整的 CycleGAN,只是帮你先把“生成器里会有残差块和图像变换路径”这件事摸熟。