本篇还是按上一篇的规矩,先上图:
首先看汉子变女神:
再来看美女变猛男:
最后,向泰坦尼克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严重的参考了StarGAN和AttGAN,毕竟任务目标是差不多的。下面我会介绍这个项目一些具体的技术细节,希望读者能通过本文更深入的了解GAN、卷积网络和深度学习训练。
face_gan的生成器基本使用了StarGAN的生成器网络结构(而StarGAN是从CycleGAN那里学来的,呵呵~):
两次Down Sampling后直接接残差块,然后再两次Up Sampling恢复原始尺寸。分辨率不降太多,保持足够多的空间位置信息,对提高图像质量很有帮助,网络也更容易训练。不过face_gan和StarGAN有三个区别:
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初始化的网络更不容易陷入病态。
StarGAN,AttGAN,face_gan本质上都是使用相同的组成部分构成损失,但训练方式略有不同。
StarGAN如下图:
AttGAN如下图:
它们都使用原图 X a X^a Xa训练分类,使用转换属性后的 X b ^ X^{\hat b} Xb^训练生成器转换属性的能力,都使用像素的L1距离作为重构损失。face_gan最开始想使用facenet做Perceptual Loss,但感觉并不好用,有可能Perceptual Loss的约束太强,不利于属性转换。而直接使用像素损失的效果也不错,还更加节省运算和内存,何乐而不为呢?
StarGAN和AttGAN主要的区别是StarGAN使用 G ( G ( X a , b ) , a ) G(G(X^a, b), a) G(G(Xa,b),a)训练重构损失,而AttGAN使用 G ( X a , a ) G(X^a, a) G(Xa,a)训练重构损失。另外StarGAN把目标属性b和 X a X^a Xa一起输入G,而AttGAN在G的瓶颈处输入b。
face_gan看上去是上面两者的混合:
然而face_gan并不是为了和这两者有所区别而故意把它们的训练流程混合在一起。
实际上,StarGAN使用 G ( G ( X a , b ) , a ) G(G(X^a, b), a) G(G(Xa,b),a)训练重构损失存在问题。在这个流程里,把 X a X^a Xa转换成 X b ^ X^{\hat b} Xb^时,无法保证 X a X^a Xa的原始信息完全包含在 X b ^ X^{\hat b} Xb^的像素里,当 X b ^ X^{\hat b} Xb^再次通过生成器输出 X a ^ X^{\hat a} Xa^时,却要求 X a ^ X^{\hat a} Xa^尽可能还原 X a X^a Xa,这本身就是不可能完成的任务(或者说贝叶斯误差不可能为零)。举例来说,假设第一步把一个金发美女的照片和属性“黑发”输入生成器,这时生成器输出的是一张黑发美女照片,第二步则是把这幅黑发美女的照片和属性“金发”输入生成器,输出金发美女照片,并且希望这张照片和原始照片尽量相同,可是在第二步中,生成器的输入只有一张黑发美女的照片,现在告诉它要转换成金发,它怎么能知道有多金?!为了达到重构损失最小,唯一合理的就是输出一个金发的平均颜色。StarGAN论文中Figure 8的样例图片(应该是使用实际生成的样图)就展现了这种情况,棕发美女的还原图发色和原来是不一样的!StarGAN这样做是因为CycleGAN就是这样做的,而它学习了CycleGAN的方法。所以实际上CycleGAN也一样有这个贝叶斯误差,但是StarGAN的任务场景和CycleGAN不一样,它并不一定要使用CycleGAN的这种cycle consistency loss,而只需要使用AttGAN的方式就可以了。
归根结底,属性是一个非常抽象的信息,实际图像中包含的复杂的细节信息可以归纳成某种属性,但从一个属性还原出原始的细节是不可能的。StarGAN这个问题的结果是导致/强迫它做属性转换时尽可能的保留原始图像的信息,尽可能少的修改原图。它在做男女转换时都非常微妙,头发绝对不动,比如下面这两张图:
这是face_gan把 G ( X a , a ) G(X^a, a) G(Xa,a)改成 G ( G ( X a , b ) , a ) G(G(X^a, b), a) G(G(Xa,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
需要说明的是做多属性转换其实并没有什么技术上的障碍,目前只做性别转换主要是为了控制项目范围,并且男女转换在所有属性转换里无疑是最有趣的。
要实现人脸属性转换这个目标,引入对抗训练是必需的。单靠分类约束,根本无法生成满意的效果。这一点AttGAN的论文也提到过,没有对抗,生成图像基本就相当于对抗攻击样本,可以骗过分类器,但人眼基本看不出图像产生了什么变化。
face_gan/face_adversarial_translator.py把一些可调的超参数或者权重都放在文件开头,方便调整,但其实缺省的数值基本就都是最优的。下面对一些超参数进行一下说明:
深度学习项目开发中有许多重要的经验,本文只想说非常重要的一点,那就是在训练中提供/打印出足够的信息。比如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个问题:
可能的改进方法包括:
至于生成真实的图像去填补消去的区域,主要依赖GAN能学到多少真实分布的信息,暂时没有有把握的思路。
人脸属性转换的这个系列到此就结束了,让人工智能进行创造这个梦想才刚刚开始实现!