男女速变 —— GAN

本篇还是按上一篇的规矩,先上图:
首先看汉子变女神:
在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
在这里插入图片描述在这里插入图片描述
再来看美女变猛男:

在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述在这里插入图片描述

最后,向泰坦尼克CP致敬:
在这里插入图片描述在这里插入图片描述
让我穿越命运之门
寻找另一个你的眼神
如梦幻中的星辰
飘落那冰冷红尘
璀璨色彩
照耀飘飘长发
暮色阴沉
雕刻时光里的皱纹

我要是再写得飘逸一点,也能进梨花诗派吧。。。

这个效果明显比上一篇的VAE要好得多,全部代码在github/face_gan(Starred by AttGAN作者),和VAE一样使用tensorflow和tensorflow内置的Keras,项目的pictures目录中有更多的样例图片。

那些不是GAN的GAN说的,其实face_gan也和StarGAN、AttGAN一样不能算一个严格意义上的GAN,但因为一说GAN大家就明白怎么回事,所以这个GitHub项目和本文都是用GAN的名字。

毋庸讳言,face_gan严重的参考了StarGANAttGAN,毕竟任务目标是差不多的。下面我会介绍这个项目一些具体的技术细节,希望读者能通过本文更深入的了解GAN、卷积网络和深度学习训练。

网络结构

face_gan的生成器基本使用了StarGAN的生成器网络结构(而StarGAN是从CycleGAN那里学来的,呵呵~):
在这里插入图片描述
两次Down Sampling后直接接残差块,然后再两次Up Sampling恢复原始尺寸。分辨率不降太多,保持足够多的空间位置信息,对提高图像质量很有帮助,网络也更容易训练。不过face_gan和StarGAN有三个区别:

  • StarGAN使用了6个Residual Block,face_gan使用9个。之所以增加3个是因为:StarGAN输入128x128的图像,而face_gan输入160x160(最早想用facenet做Perceptual Loss作为重构损失,facenet输入160x160,不想resize图像所以face_gan的输入就也用160x160了。后来决定直接用pixel的L1做重构损失,但输入尺寸就没再改。),是StarGAN的四分之五倍,增加3层Residual Block相应的增大网络的感受野。从结果看,增加的残差块确实对图像质量有一定的提高。
  • StarGAN和AttGAN的上采样均采用转置卷积,实测感觉Upsampling2D+Conv2D的效果更好,所以采用Upsampling2D+Conv2D。
  • StarGAN的Residual Block的实现其实并不标准,分支最后没加ReLU激活,应该是为了避免输出只有大于0的部分。face_gan使用标准的pre-activation方式来保证更广的输出范围。

StarGAN和AttGAN的discriminator网络结构采用的都是最通用形式,对抗损失/训练方式都使用WGAN-GP(所以不能用Batch Normalization),face_gan也一样。只不过StarGAN的discriminator没用任何Normalization,face_gan的discriminator用的是Instance Normalization(新版的Keras不能直接支持Layer Normalization了,所以我也就没试),实测对训练的稳定和收敛还是有帮助的。

另外,face_gan所有的初始化都用的是glorot_normal,Keras缺省glorot_uniform,感觉glorot_normal初始化的网络更不容易陷入病态。

损失和训练方法

StarGANAttGANface_gan本质上都是使用相同的组成部分构成损失,但训练方式略有不同。

StarGAN如下图:在这里插入图片描述
AttGAN如下图:
在这里插入图片描述
它们都使用原图XaX^a训练分类,使用转换属性后的Xb^X^{\hat b}训练生成器转换属性的能力,都使用像素的L1距离作为重构损失。face_gan最开始想使用facenet做Perceptual Loss,但感觉并不好用,有可能Perceptual Loss的约束太强,不利于属性转换。而直接使用像素损失的效果也不错,还更加节省运算和内存,何乐而不为呢?

StarGAN和AttGAN主要的区别是StarGAN使用G(G(Xa,b),a)G(G(X^a, b), a)训练重构损失,而AttGAN使用G(Xa,a)G(X^a, a)训练重构损失。另外StarGAN把目标属性b和XaX^a一起输入G,而AttGAN在G的瓶颈处输入b。

face_gan看上去是上面两者的混合:
在这里插入图片描述
然而face_gan并不是为了和这两者有所区别而故意把它们的训练流程混合在一起。

实际上,StarGAN使用G(G(Xa,b),a)G(G(X^a, b), a)训练重构损失存在问题。在这个流程里,把XaX^a转换成Xb^X^{\hat b}时,无法保证XaX^a的原始信息完全包含在Xb^X^{\hat b}的像素里,当Xb^X^{\hat b}再次通过生成器输出Xa^X^{\hat a}时,却要求Xa^X^{\hat a}尽可能还原XaX^a,这本身就是不可能完成的任务(或者说贝叶斯误差不可能为零)。举例来说,假设第一步把一个金发美女的照片和属性“黑发”输入生成器,这时生成器输出的是一张黑发美女照片,第二步则是把这幅黑发美女的照片和属性“金发”输入生成器,输出金发美女照片,并且希望这张照片和原始照片尽量相同,可是在第二步中,生成器的输入只有一张黑发美女的照片,现在告诉它要转换成金发,它怎么能知道有多金?!为了达到重构损失最小,唯一合理的就是输出一个金发的平均颜色。StarGAN论文中Figure 8的样例图片(应该是使用实际生成的样图)就展现了这种情况,棕发美女的还原图发色和原来是不一样的!StarGAN这样做是因为CycleGAN就是这样做的,而它学习了CycleGAN的方法。所以实际上CycleGAN也一样有这个贝叶斯误差,但是StarGAN的任务场景和CycleGAN不一样,它并不一定要使用CycleGAN的这种cycle consistency loss,而只需要使用AttGAN的方式就可以了。

归根结底,属性是一个非常抽象的信息,实际图像中包含的复杂的细节信息可以归纳成某种属性,但从一个属性还原出原始的细节是不可能的。StarGAN这个问题的结果是导致/强迫它做属性转换时尽可能的保留原始图像的信息,尽可能少的修改原图。它在做男女转换时都非常微妙,头发绝对不动,比如下面这两张图:
在这里插入图片描述在这里插入图片描述
这是face_gan把G(Xa,a)G(X^a, a)改成G(G(Xa,b),a)G(G(X^a, b), a)用于重构损失,训练8个epochs(从第5个epoch开始做学习率衰减)的结果,你似乎能看出生成器想动头发又不敢动,生怕还原不回去的挣扎的样子。。。

所以face_gan采用AttGAN的方式训练重构损失。这种方式下,重构损失可以最大程度的帮助保留原图的信息,属性转换时又可以无拘无束的放飞自我~~

但是face_gan并没有像AttGAN一样在生成器的瓶颈处输入目标属性,主要是觉得这样似乎没有必要,属性在网络的开始就加进去,暗示着网络从一开始就朝着属性转换这个目标努力。实际上,这个决定对最终的效果应该没有什么影响。AttGAN强调编码器解码器的概念,所以它把属性插入到网络瓶颈处输入,这种结构可以做到不同人脸先融合再编辑属性,不过在它的论文里没有提。

face_gan和StarGAN实现/AttGAN实现在训练方法上另一个不同的地方是,StarGAN和AttGAN都是用类似下面的方式(参见StarGAN/solver.py)随机生成目标属性:

# Generate target domain labels randomly.
rand_idx = torch.randperm(label_org.size(0))
label_trg = label_org[rand_idx]

我猜测之所以使用这种方式,一个很可能的原因是:它们都是训练网络进行多属性转换,而很多属性在数据集里是数据不平衡的,比如Bald这个标签,大部分人脸应该都不是秃子。上面的代码保证了属性转换任务训练的平衡性,虽然训练量会缩小到相对少的标签数量。
另一个原因也许是它们想降低训练的难度,在一次训练中,只有一部分属性被要求转换。

但因为face_gan只专注于男女转换这一项任务,所以这两个原因都不适合,CelebA里男女基本各半,不存在不平衡问题。因此face_gan每次训练都做相反的属性转换:

target_label = 1 - label

需要说明的是做多属性转换其实并没有什么技术上的障碍,目前只做性别转换主要是为了控制项目范围,并且男女转换在所有属性转换里无疑是最有趣的。

GAN训练中的其他超参数

要实现人脸属性转换这个目标,引入对抗训练是必需的。单靠分类约束,根本无法生成满意的效果。这一点AttGAN的论文也提到过,没有对抗,生成图像基本就相当于对抗攻击样本,可以骗过分类器,但人眼基本看不出图像产生了什么变化。

face_gan/face_adversarial_translator.py把一些可调的超参数或者权重都放在文件开头,方便调整,但其实缺省的数值基本就都是最优的。下面对一些超参数进行一下说明:

  • 优化器:WGAN的论文建议GAN不使用带动量的优化器,可能是GAN的训练过程中优化的方向要经常改变,不像一般的网络会向一个固定的方向收敛。所以动量反而对GAN的训练有损害。加入梯度惩罚后可以用Adam了,不过用的时候基本都把β1\beta1设成0.5,也就是只保留很近的几次动量。StarGAN和AttGAN这样做,我在face_gan里也毫不犹豫的这样做 ?
  • batch_size:很多任务都需要适当的引入方差,face_gan里这个最优值是16,32或更大反而不好。
  • 学习率衰减:因为face_gan的网络结构是模仿StarGAN的,所以学习率衰减也使用StarGAN式的线性衰减。
  • 重构损失权重 lambda_rec:这项非常重要,前面提过StarGAN重构损失的训练方式会使属性转换后的图像只有一些微妙/微小的变化,如果重构损失权重设成100,即使不使用它的训练方式,也会有同样的效果。我希望属性转换能有一些生动的变化,所以尽管设成100会提高生成图像的清晰度,仍旧把这项权重设成10。
  • 其它权重:lambda_cls_gen和lambda_g_w可以一起调整,单调lambda_cls_gen可能会导致训练不稳。因为生成图像的属性类别和它的真实度有竞争/对抗的关系。两者一起提高会生成更加夸张的属性转换效果。

训练经验

深度学习项目开发中有许多重要的经验,本文只想说非常重要的一点,那就是在训练中提供/打印出足够的信息。比如face_gan的损失由很多部分组成:

discriminator_loss = wasserstein_loss + lambda_gp * gradient_penalty_loss() + lambda_cls_real * real_cls_loss
generator_loss = generator_wasserstein_loss + lambda_rec * generator_rec_loss + lambda_cls_gen * gen_class_loss

各项损失不能失衡,如果只是笼统的显示discriminator_loss和generator_loss,调整权重时就无从下手,如果训练过程出现问题,也无法知道具体是哪一项造成的。face_gan/face_adversarial_translator.py除了会打印这些loss,还打印出了图像准确度,分类准确度这些metrics作为参考。

在训练过程中进行监控尤其对GAN非常必要,一旦发现位面崩溃,就要立即撤出。。。本文的图样是由最后一次正式训练得到的模型生成的,实际上在那次训练中,epoch 4的末尾到epoch 5开始阶段位面险些崩溃,我都要中止那次训练了,它又从悬崖边溜达了回来。因此大概浪费了一个epoch的训练effort,不过我实在不愿花时间再来一次了,所以就用这最后一次的结果,训练的log上传在github上,供大家参考。如果训练真的崩溃了,不要灰心,有可能只是一次恰好陷入病态的初始化,重新来过就行 ? 当然,如果是系统性的问题,就需要根据前述的各项损失数值状况进行分析解决了。

评价GAN图像生成任务的效果,不仅要靠loss和metrics的数值,更需要根据生成图像的主观感受。因此在训练过程中,测试一下图像生成也非常重要。
比如前面提到的训练崩溃,在face_gan中生成的图像会有黑洞或白洞:
在这里插入图片描述
看着这些洞洞,仿佛又有了位面穿越的感觉。。。。
另外,更重要的是根据训练中生成的图像效果进行调整,不要等到训练完毕再进行。

问题和改进

现在face_gan生成的图像主要存在3个问题:

  1. 和原图比略有模糊(如果原图比较高清的话)
  2. 属性转换后,和原图不同的头发的生成质量不高
  3. 女变男长发变短发时,消去的区域生成器不知道如何补完

可能的改进方法包括:

  • 生成高质量的头发需要空间上大范围的关联信息,使用SAGAN的技术应该可以解决,并且实现简单,增加的运算负担小。很有可能对提高生成图像的整体质量都有帮助。
  • WGAN-GP使用梯度惩罚,对当前的生成数据分布具有高度的依赖性。随着训练过程进行,生成的数据分布空间会逐渐变化,就会导致这种方法对李氏连续约束的不稳定。谱归一化直接作用于网络参数,稳定性更高。但是谱归一化在Keras里实现起来比较麻烦,暂时不会考虑。

至于生成真实的图像去填补消去的区域,主要依赖GAN能学到多少真实分布的信息,暂时没有有把握的思路。

人脸属性转换的这个系列到此就结束了,让人工智能进行创造这个梦想才刚刚开始实现!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章