本文参考:
- paddle课程《生成对抗网络七日打卡营》、博客文章《NLP 中的对抗训练(附 PyTorch 实现)》及bilibili视频、天池新闻文本分类——bert模型源码(加入生成对抗网络)
- 生成式对抗网络系列论文地址在《PaddleGAN预习课程》中有。 建议对照打卡营视频讲解观看,更容易理解。
kaggle在2019年曾经举办一项奖金高达100万美元的比赛《Deepfake Detection Challenge》,主要是识别视频中哪些人脸是真实哪些是AI生成的。
生成式对抗网络,简称GAN,在图像/视频领域、人机交互领域都有应用,比如:
以下这幅图中的人脸就都是神经网络生成的:
根据文字描述生成对应图像、医疗影像由生成对抗网络进行数据增广和生成
2014年提出以来,生成对抗网络快速发展
以下红色部分在本次课程中会讲到:
我们之前学习的图片分类、语义分割、目标检测都是判别模型,根据图片特征训练后得到标签,而GAN是生成模型,根据噪声和标签生成需要的图片。
生成式对抗网络模型由两个基础神经网络组成,即生成器神经网络(Generator Neural Network) 和判别器神经网络(Discriminator Neural Network) 。其中一个用于生成内容,另一个则用于判别生成的内容。
生成器从给定噪声中(一般是指均匀分布或者正态分布)产生合成数据,判别器分辨生成器的的输出和真实数据。在训练过程中,生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。这样,G和D构成了一个动态的“博弈过程”。两个网络在对抗中进步,在进步后继续对抗,由生成式网络得的数据也就越来越完美,逼近真实数据,从而可以生成想要得到的数据(图片、序列、视频等)。
左图表示,均匀分布的噪声Random noise输入生成器得到假的图片Fake Image,Fake Image和Real Image一起输入判别器中,得到判别分数(分数1为真实图片,0为生成图片)。
右图是GAN的数学描述:
最终通过不断的训练,生成的图片会相当真实。
生成器G希望从数据的真实分布中采样到一种分布,加入随机噪声(比如0-1之间的均匀分布的噪声)后映射成接近真实分布的生成器分布。可视化就是:
如上图所示:
参考:论文《UNSUPERVISED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL GENERATIVE ADVERSARIAL NETWORKS》、代码链接
由于卷积神经网络(Convolutional neural network, CNN)比MLP有更强的拟合与表达能力,并在判别式模型中取得了很大的成果。因此,Alec等人将CNN引入生成器和判别器,称作深度卷积对抗神经网络(Deep Convolutional GAN, DCGAN),Convolutional表示卷积算子。另外还讨论了 GAN 特征的可视化、潜在空间插值等问题。
DCGAN的改进:
代码如下:(来自《DCGAN实践》、)
import os
import random
import paddle
import paddle.nn as nn
import paddle.optimizer as optim
import paddle.vision.datasets as dset
import paddle.vision.transforms as transforms
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
demo_dataset = paddle.vision.datasets.MNIST(mode='train')
#rezize到32×32,然后归一化到-1和1之间
dataset = paddle.vision.datasets.MNIST(mode='train', transform=transforms.Compose
([transforms.Resize((32,32)),
transforms.Normalize([127.5], [127.5])]))
dataloader = paddle.io.DataLoader(dataset, batch_size=32,shuffle=True, num_workers=4)
# Generator Code
class Generator(nn.Layer):
def __init__(self, ):
super(Generator, self).__init__()
self.gen = nn.Sequential(
# input is Z, [B,100,1,1] -> [B,64*4,4,4]
nn.Conv2DTranspose(100,64*4,4,1,0, bias_attr=False),
nn.BatchNorm2D(64*4),
nn.ReLU(True),
# state size. [B,64*4,4,4] -> [B,64*2,8,8]
nn.Conv2DTranspose(64*4,64*2,4,2,1, bias_attr=False),
nn.BatchNorm2D(64*2),
nn.ReLU(True),
# state size. [B,64*2,8,8] -> [B,64,16,16]
nn.Conv2DTranspose(64*2,64,4,2,1, bias_attr=False),
nn.BatchNorm2D(64),
nn.ReLU(True),
# state size. [B,64,16,16] -> [B,1,32,32]
nn.Conv2DTranspose(64,1,4,2,1, bias_attr=False),
nn.Tanh()#最后输出值在-1到1之间
)
def forward(self, x):
return self.gen(x)
class Discriminator(nn.Layer):
def __init__(self,):
super(Discriminator, self).__init__()
self.dis = nn.Sequential(
# input [B,1,32,32] -> [B,64,16,16]
nn.Conv2D(1,64,4,2,1, bias_attr=False),
nn.LeakyReLU(0.2),
# state size. [B,64,16,16] -> [B,128,8,8]
nn.Conv2D(64,64*2,4,2,1, bias_attr=False),
nn.BatchNorm2D(64*2),
nn.LeakyReLU(0.2),
# state size. [B,128,8,8] -> [B,256,4,4]
nn.Conv2D(64*2,64*4,4,2,1, bias_attr=False),
nn.BatchNorm2D(64*4),
nn.LeakyReLU(0.2),
# state size. [B,256,4,4] -> [B,1,1,1] -> [B,1]
nn.Conv2D(64*4,1,4,1,0,bias_attr=False),
nn.Sigmoid()
)
def forward(self, x):
return self.dis(x)
#定义的初始化函数weights_init略去了
netG = Generator()
netG.apply(weights_init)
netD = Discriminator()
netD.apply(weights_init)
loss = nn.BCELoss()#二分类损失函数
# 创建噪声
fixed_noise = paddle.randn([32, 100, 1, 1], dtype='float32')
# 设置真实图片和生成图片的标签
real_label ,fake_label= 1.,0.
# 设置两个优化器,训练一个网络时固定另一个网络的权重
optimizerD = optim.Adam(parameters=netD.parameters(), learning_rate=0.0002, beta1=0.5, beta2=0.999)
optimizerG = optim.Adam(parameters=netG.parameters(), learning_rate=0.0002, beta1=0.5, beta2=0.999)
losses = [[], []]
#plt.ion()
now = 0
for pass_id in range(100):
for batch_id, (data, target) in enumerate(dataloader):
"""
(1) Update D network: maximize log(D(x)) + log(1 - D(G(z)))
"""
optimizerD.clear_grad()#梯度清零
real_img = data
bs_size = real_img.shape[0]
label = paddle.full((bs_size, 1, 1, 1), real_label, dtype='float32')#判别器真实图片标签为1
real_out = netD(real_img)
errD_real = loss(real_out, label)
errD_real.backward()
"""
生成器根据噪声生成图片,并且把标签设为0
"""
noise = paddle.randn([bs_size, 100, 1, 1], 'float32')
fake_img = netG(noise)
label = paddle.full((bs_size, 1, 1, 1), fake_label, dtype='float32')
fake_out = netD(fake_img.detach())
errD_fake = loss(fake_out,label)
errD_fake.backward()
optimizerD.step()
optimizerD.clear_grad()
errD = errD_real + errD_fake
losses[0].append(errD.numpy()[0])
"""
(2) Update G network: maximize log(D(G(z)))
唯一不同是生成器生成的图片标签改为1,因为生成器要生成接近真实的图片
"""
optimizerG.clear_grad()
noise = paddle.randn([bs_size, 100, 1, 1],'float32')
fake = netG(noise)
label = paddle.full((bs_size, 1, 1, 1), real_label, dtype=np.float32,)
output = netD(fake)
errG = loss(output,label)
errG.backward()
optimizerG.step()
optimizerG.clear_grad()
losses[1].append(errG.numpy()[0])
"""
每一百步做一次可视化,打印输出
"""
if batch_id % 100 == 0:
generated_image = netG(noise).numpy()
imgs = []
plt.figure(figsize=(15,15))
try:
for i in range(10):
image = generated_image[i].transpose()
image = np.where(image > 0, image, 0)
image = image.transpose((1,0,2))
plt.subplot(10, 10, i + 1)
plt.imshow(image[...,0], vmin=-1, vmax=1)
plt.axis('off')
plt.xticks([])
plt.yticks([])
plt.subplots_adjust(wspace=0.1, hspace=0.1)
msg = 'Epoch ID={0} Batch ID={1} \n\n D-Loss={2} G-Loss={3}'.format(pass_id, batch_id, errD.numpy()[0], errG.numpy()[0])
print(msg)
plt.suptitle(msg,fontsize=20)
plt.draw()
plt.savefig('{}/{:04d}_{:04d}.png'.format('work', pass_id, batch_id), bbox_inches='tight')
plt.pause(0.01)
except IOError:
print(IOError)
paddle.save(netG.state_dict(), "work/generator.params")
paddle官网、github地址。官网没啥用,主要看github。
GAN和DCGAN存在以下问题:
模式坍塌的原因一句话概括就是:等 价优化的距离衡量(KL散度、JS散度)不合理,生成器随机初始化后的生成分布很难与真实分布有不可忽略的重叠。
论文:Least Squares Generative Adversarial Networks
针对GAN存在的JS散度导致的问题,LSGAN(LeastSquare GAN)提出用MSE损失函数代替二分类损失函数,改善了传统 GAN 生成的图片质量不高,且训练过程十分不稳定的问题。
训练营第二课作业《代码题 DCGAN改写LSGAN》中需要改的代码就两处:
参考:论文Wasserstein GAN、代码链接、论文解读《WGAN(Wasserstein GAN)看这一篇就够啦,WGAN论文解读》
WGAN利用EM距离代替JS,KL散度来表示生成与真实分布的距离衡量,从而改进了原始GAN存在的两类问题。(Wasserstein距离 优越性在于: 即使两个分布没有任何重叠,也可以反应他们之间的距离。)
假设真实分布是 P r P_r Pr,生成器分布是 P θ P_\theta Pθ,两种分布就像两堆土,如下图所示:
将右边土堆堆成左边土堆的方式有无数种,其中一种消耗最少的称为推土机距离EM(Earth-Moverdistance)。
参考:论文PROGRESSIVE GROWING OF GANS FOR IMPROVED QUALITY, STABILITY, AND VARIATION
如果直接生成大分辨率的图片,建立从latent code 到 1024x1024 pixels样本的映射网络G,肯定是很难工作的。因为,在生成的过程中, 判别器D很容易就可以识别出G生成的“假图像”,G难以训练 。因此,提出PGGAN(progressive gan)来进行逐层训练。
这项技术首先通过学习即使在低分辨率图像中也可以显示的基本特征,来创建图像的基本部分,并且随着分辨率的提高和时间的推移,学习越来越多的细节。由于每次前面的层已经训练好,所以会集中训练后添加的层,所以提高分辨率后,新的训练难度不会提高。低分辨率图像的训练不仅简单、快速,而且有助于更高级别的训练,因此,整体的训练也就更快。
如下图所示,模型先训练一个生成4*4分辨率图片的的生成器和对应的判别器,效果不错之后再添加一层,训练8*8分辨率的生成器和判别器。。。。。。不断逐层添加卷积层和转置卷积层,最终得到分辨率为1024*1024的生成对抗网络。
PGGAN网络结构如下:
当把生成器和判别器的分辨率加倍时,会平滑地增大新的层。我们以从16 × 16 像素的图片转换到 32 × 32 像素的图片为例。在转换(b)过程中,把在更高分辨 率上操作的层视为一个残缺块,权重 α 从 0 到 1 线性增长。当 α 为 0 的时候,相当于图(a),当 α 为 1 的时候,相当于图©。所以,在转换过程中,生成样本的像素,是从 16x16 到 32x32 转换的。同理,对真实样本也做了类似的平滑过渡,也就是,在这个阶段的某个 训练 batch,真实样本是:
当训练判别器时,插入下采样后的真实图片去匹配网络中 的当前分辨率。在分辨率转换过程中,会在两张真实图片的分辨率之间插值,类似于将两个分辨率结合到一起用生成器输出。其它改进还有:
由于 PPGAN 是逐级直接生成图片,我们没有对其增添控制,我们也就无法获知它在每一级上学 到的特征是什么,这就导致了它 控制所生成图像的特定特征的能力非常有限,即PPGAN 容易发生特征纠缠。换句话说,这些特性是互相关联的,因此尝试调整一下输入,即使是一点儿,通常也会同时影响多个特性。
如下图,比如我们希望噪声第二个维度可以控制人脸的肤色,理想是第二维向量由0.9改为0.7之后,会生成第二张图片。但是结果可能生成完全不一样的图片,比如第三张图,这就是相互纠缠的一个例子。
我们希望有一种更好的模型,能让我们控制住输出的图片是长什么样的,也就是在生成 图片过程中每一级的特征,要能够特定决定生成图片某些方面的表象,并且相互间的影响尽 可能小。于是,在 PPGAN 的基础上,StyleGAN 作出了进一步的改进与提升。
import matplotlib.pyplot as plt
import tensorflow as tf
import tensorflow_hub as hub
with tf.Graph().as_default():
# 提前从TFHub导入PGGAN
module = hub.Module("progan-128_1")
#运行时采样的维度
latent_dim = 512
# 改变种子得到不同的人脸
latent_vector = tf.random.normal([1, latent_dim], seed=1337)
# 使用该模块从潜在空间生成图像
interpolated_images = module(latent_vector)
# 运行Tensorflow session 得到(1,128,128,3)的图像
with tf.compat.v1.Session() as session:
session.run(tf.compat.v1.global_variables_initializer())
image_out = session.run(interpolated_images)
plt.imshow(image_out.reshape(128,128,3))
plt.show()
参考:
- 论文A Style-Based Generator Architecture for Generative Adversarial Networks
- 《StyleGAN 架构解读(重读StyleGAN )精细》
PGGAN的问题: 控制生成图像特定特征的能力有限。 以下图来说:
StyleGAN 用风格(style)来影响人脸的姿态、身份特征等,用噪声 ( noise ) 来影响头发丝、皱纹、肤色等细节部分。StyleGAN 的网络结构包含两个部分:映射网络Mapping network和Synthesis network。
Mapping network,即下图 (b)中的左部分,由隐藏变量 z 生成 中间隐藏变量 w的过程,这个 w 就是用来控制生成图像的style,即风格。
Synthesis network,它的作用是生成图像,创新之处在于给每一层子网络都喂了 A 和 B,A 是由 w 转换得到的仿射变换,用于控制生成图像的风格,B 是转换后的随机噪声,用于丰富生成图像的细节,即每个卷积层都能根据输入的A来调整"style",通过B来调整细节。
整个网络结构还是保持了 PG-GAN (progressive growing GAN) 的结构。最后论文还提供了一个高清人脸数据集FFHQ。
架构解读:
StyleGAN 首先重点关注了 ProGAN 的生成器网络,它发现,渐进层的一个的好处是,如果使用得当,它们能够控制图像的不同视觉特征。层和分辨率越低,它所影响的特征就越粗糙。简要将这些特征分为三种类型:
1、粗糙的——分辨率不超过82,影响姿势、一般发型、面部形状等;
2、中等的——分辨率为162至322,影响更精细的面部特征、发型、眼睛的睁开或是闭合等;
3、高质的——分辨率为642到10242,影响颜色(眼睛、头发和皮肤)和微观特征。
然后,StyleGAN 就在 ProGAN 的生成器的基础上增添了很多附加模块以实现样式上更细微和精确的控制。
StyleGAN的第一点改进是:Mapping network 对隐藏空间(latent space)进行解耦,缓解特征纠缠 。Generator的输入加上了由8个全连接层组成的Mapping Network,并且 Mapping Network 的输出W′与输入层Z(512×1)的形状大小相同。中间向量W′(或者叫潜在因子)后续会传给生成网络得到 18 个控制向量,使得该控制向量的不同元素能够控制不同的视觉特征。
如果不加这个 Mapping Network 的话,后续得到的 18个控制向量之间会存在特征纠缠的现象——比如说我们想调节 8*8 分辨率上的控制向量(假 设它能控制人脸生成的角度),但是我们会发现 32*32 分辨率上的控制内容(譬如肤色)也被改变了,这个就叫做特征纠缠。所以 Mapping Network 的作用就是为输入向量的特征解缠提供一条学习的通路。
为何 Mapping Network 能够学习到特征解缠呢?简单来说, 如果仅使用输入向量来控制视觉特征,能力是非常有限的,因此它必须遵循训练数据的概率密度。例如,如果黑头发 的人的图像在数据集中更常见,那么更多的输入值将会被映射到该特征上。因此,该模型无法将部分输入(向量中的元素)映射到特征上,这就会造成特征纠缠。然而, 通过使用另一个神经网络,该模型可以生成一个不必遵循训练数据分布的向量,并且可以减少特征之间的相关性。
StyleGAN第二点改进是,将特征解缠后的中间向量W′变换为样式控制向量,从而参与影响生成器的生成过程。AdaIN表示自适应实例归一化。
实例归一化是上图Instance Norm中,对蓝色部分进行归一化。每个batch中只取一个样本,计算其在每个通道上的均值 μ ( x ) \mu (x) μ(x)和标准差 σ ( x ) \sigma (x) σ(x),γ和β表示缩放因子和偏置。自适应归一化AdaIN是其变体。
上图右下是风格迁移任务的网络示意图,我们希望上面实景图有下面那张漫画图的风格。论文实验发现,在实例归一化中,将实景图的γ和β换成漫画图的均值和标准差,最终会取得比较好的风格迁移效果。这就是自适应归一化的过程。StyleGAN就借鉴了这一种思路。
风格迁移任务更多细节,可以参考我另一篇帖子:《动手深度学习13:计算机视觉——语义分割、风格迁移》
AdaIN 的具体实现过程如上右图所示:将潜在因子W′通过一个可学习的仿射变换A(简单理解就是一个全连接层)后输出,输出扩大为原来的两倍(2×n),分别作为缩放因子 y s , i y_{s,i} ys,i和偏差因子 y b , i y_{b,i} yb,i。输入 x i x_i xi进过标准化(减均值除方差)后,与两个因子进行AdaIN,就完成了一次W′影响原始输出 x i x_i xi的过程。
AdaIN 代码见左下,W′经过FC层之后变成原来两倍,reshape成前后两部分。这两部分分别作为两个因子,最后 x = y s , i ∗ x + y b , i x=y_{s,i}*x+y_{b,i} x=ys,i∗x+yb,i。(x在AdaIN之前先标准化)
生成器从 分辨率4*4,变换到 8*8,并最终到 1024*1024,一共由 9 个生成阶段组成,而每个阶段都会受两个控制向量(A)对其施加影响。其中一个控制向量在 Upsample之后对其影响一次,另外一个控制向量在 Convolution 之后对其影响一次,影响的方式都采用 AdaIN。因此,中间向量W′总共被变换成 18 个控制向量(A)传给生成器。
这种影响方式能够实现样式控制,主要是因为它让变换后的W′影响图片的全局信息(注意标准化抹去了对图片局部信息的可见性),而保留生成人脸的关键信息由上采样层和卷积层来决定,因此W′只能够影响到图片的样式信息。
上图左侧网络表示传统的GAN网络输入是一个随机变量或者隐藏变量 z,右侧表示Synthesis network中最开始的输入变成了常数张量。
既然 StyleGAN 生成图像的特征是由 ′ 和 AdaIN 控制的,那么生成器的初始输入可以 被忽略,并用常量值4×4×512输入替代(分辨率,通道数)。这样做的理由是,首先可以降低由于初始输入取值不当而生成出 一些不正常的照片的概率(这在 GANs 中非常常见),另一个好处是它有助于减少特征纠缠, 对于网络在只使用 ′ 不依赖于纠缠输入向量的情况下更容易学习。
左下代码是将input先定义为[batch_size=1,channel,size,size],然后获取实际输入的batch_size,再对其进行铺开(tile函数),最终得到[input_batch_size,512,4,4]的输入。
人脸很多小特征是随机性的,比如头发、皱纹、雀斑;不同时间、角度、地点都可能发生变化。将这些小特征插入 GAN 图像的常用方法是 在输入向量中添加随机噪声 (即通过在每次卷积后添加噪声 )。为了控制噪声仅影响图片样式上细微的变化, StyleGAN 采用类似于 AdaIN 机制的方式添加噪声。
噪声输入是由不相关的高斯噪声组成的单通道数据,它们被馈送到生成网络的每一层。即在 AdaIN 模块之前向每个通道添加一个缩放过的噪声,并稍微改变其操作的分辨率级别特征的视觉表达方式。 加入噪声后的生成人脸往往更加逼真与多样 。
左下代码中weight表示可学习的缩放因子,初始化shape=1,value=0。noise从高斯分布中取得。
风格影响的是整体(改变姿势、身份等),噪音影响无关紧要的随机变化(头发、胡须等)
StyleGAN 生成器在合成网络的每个层级中都使用了潜在因子,这有可能导致网络学习到这些层级是相关的。为了降低关联性,一个简单的想法是使用不同的潜在因子。论文中采用随机选择两个输入向量,映射后生成了两个潜在因子 ′ 。然后在所有网络层级中随机选取一个点,这个点之前的层级使用第一个它用第一个 ′,之后的层级使用第二个 ′。随机的切换确保了网络不会学习并依赖于一个合成网络级 别之间的相关性。下图代码中inject_index表示随机选取的点。
混合正则化并不会提高所有数据集上的模型性能,但是它能够以一种连贯的方式来组合多个图像。该模型生成了两个图像 A 和 B(第一行的第一张图片和第二行的第一张图片),然后通过从 A 中提取低级别的特征并从 B 中提取其余特征再组合这两个图像,这样能生成出混合了 A 和 B 的样式特征的新人脸 。
根据交叉点选取位置的不同,style组合的结果也不同。下图中分为三个部分,
- 第一部分是 Coarse styles from source B,分辨率(4x4 - 8x8)的网络部分使用B的style,其余使用A的style, 可以看到图像的身份特征随souce B,但是肤色等细节随source A;
- 第二部分是 Middle styles from source B,分辨率(16x16 - 32x32)的网络部分使用B的style,这个时候生成图像不再具有B的身份特性,发型、姿态等都发生改变,但是肤色依然随A;
- 第三部分 Fine from B,分辨率(64x64 - 1024x1024)的网络部分使用B的style,此时身份特征随A,肤色随B。
由此可以 大致推断, 低分辨率的style 控制姿态、脸型、配件 比如眼镜、发型等style,高分辨率的style控制肤色、头发颜色、背景色等style。
参考:
- 论文《Analyzing and Improving the Image Quality of StyleGAN》
- 《StyleGAN2学习笔记》、代码
StyleGAN 中,通过AdaIN实现特征解耦和风格控制,但是会带来水印问题,即生成的图片有水滴状伪影,在特征图上很明显。在StyleGAN2中,AdaIN被重构为权重解调(Weight Demodulation)。
下图左侧是StyleGAN 结构,右图是StyleGAN2结构,可以看出:
modelation是可学习的放射变换(FC层),scale是和weight形状有关的一个固定值,style是仿射变换之后的潜在因子。demodulate就是解调部分,rsqrt(x)就是x平方的导数。加一个小的ϵ 是为了避免分母为0,保证数值稳定性
StyleGAN2作者发现,生成图片时,部分细节不随主体变化而变化。例如下图的牙齿,在人脸变化后还是保持不变。作者认为在逐步增长的过程中,每个分辨率都会瞬间用作输出分辨率,迫使其生成最大频率细节,然后导致受过训练的网络在中间层具有过高的频率。神经网络中要产生细节充足和高频率的图片,那么网络的参数频率也要很高,从而损害了位移不变性。
作者根据MSG-GAN设计了b和c两种结构,解决了这个问题。(实验中人脸的眼珠子会转了,牙齿也会变化)
在生成方法的背景下,Skip connections,残差网络和分层方法也被证明是非常成功的。三种生成器(虚线上方)和判别器体系结构如上图。Up和Down分别表示双线性上和下采样。 在残差网络中,这些还包括1×1卷积以调整特征图的channel数。tRGB和fRGB在RGB和高维每像素数据之间转换。 Config E和F中使用的体系结构以绿色突出显示。
我们可以看到,从一开始,网络就专注于低分辨率图像,并随着训练的进行逐渐将其注意力转移到较大分辨率上。
总结
应用体验教程参考:StyleGAN V2
使用方法:
cd applications/
python -u tools/styleganv2.py \
--output_path <替换为生成图片存放的文件夹> \
--weight_path <替换为你的预训练模型路径> \
--model_type ffhq-config-f \
--seed 233 \
--size 1024 \
--style_dim 512 \
--n_mlp 8 \
--channel_multiplier 2 \
--n_row 3 \
--n_col 5 \
--cpu
weight_path可以不设置,会默认下载已经训练好的权重。
参考《PaddleGAN》
关于PaddleGAN的代码、各种应用,可以参考github资源上的教程,例如:
卡通画一直以幽默、风趣的艺术效果和鲜明直接的表达方式为大众所喜爱。近年来,随着多部动漫电影陆续成为现象级爆款,越来越多的人开始在社交网络中使用卡通画作为一种表意的文化载体。人们对于定制卡通画的需求与日俱增,然而高质量的卡通画需要经验丰富的画师精心绘制,从线稿设计到色彩搭配,整个流程耗时费力,对于大众而言购买成本较高。(淘宝上这种服务的店铺众多)
定制卡通画痛点:耗时长、成本高、要求高的话可能需要反复沟通修改、涉及隐私。
计算机生成卡通画任务要点:图像精美好看、男女老少都覆盖且保留其鲜明特点、卡通画和原照片有相同的身份信息(长得像)。
图像翻译:指从一副图像到另一副图像的转换。可以类比机器翻译,一种语言转换为另一种语言。下图就是一些典型的图像翻译任务:比如语义分割图转换为真实街景图,灰色图转换为彩色图,白天转换为黑夜…(Pixel2Pixel的效果图,下面会讲到):
图像翻译的三个比较经典的模型pix2pix,pix2pixHD, vid2vid。可参考《图像翻译三部曲:pix2pix, pix2pixHD, vid2vid》
本课任务就是将人物画翻译为动漫画。
鉴别器可以当做是一种可自行优化的损失函数,训练完生成器就可以丢掉了。
GAN中的噪声是随机的,无法控制生成器生成哪一种特征的图片。所以提出了Conditional GAN。
论文:《Conditional Generative Adversarial Nets》、代码
Conditional GAN希望可以控制GAN 生成的图片,而不是单纯的随机生成图片。具体地,Conditional GAN 在生成器和判别器的输入中增加了额外的条件信息y,生成器生成的图片只有足够真实且与条件y相符,才能够通过判别器。
条件信息y,可以是类别标签 或者是 其他类型的数据,使得 图像生成能够朝规定的方向进行。
网络模型:
论文《Image-to-Image Translation with Conditional Adversarial Networks》、代码
更多原理参考《图像翻译三部曲:pix2pix, pix2pixHD, vid2vid》
pix2pix是一个经典的图像翻译模型,使用成对数据进行训练。
pixel2pixel可用于生成街景、建筑物、黑白图→彩色图、线稿→实物等等(见上一节效果图)。
有些任务的成对数据是容易收集的,比如用技术手段将很多彩色图转为少见的黑白图,扩大了黑白图数据的规模,这样黑白-彩色成对数据就容易收集了。但是有些任务的成对数据很难通过简单的技术手段收集。CycleGAN就是一种基于非成对数据的图像翻译方法。
这里解释一下成对数据和非成对数据:
成对数据:两组数据有相似度级别的对应,比如图片的风格和纹理可以不同,但是位置信息要是一致的,比如脸型、五官等空间信息一致。成对数据一般获取难度较大。
论文:《Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks》、代码
简单来说, CycleGAN功能就是:自动将某一类图片转换成另外一类图片。CycleGAN不需要配对的训练图像。当然了配对图像也完全可以,不过大多时候配对图像比较难获取。所以CycleGAN可以做配对图像转换,也可以做图像从一个模式到另外一个模式的转换,转换的过程中,物体发生了改变,比如从猫到狗,从男人到女人。
CycleGAN结构如下:
CycleGAN其实是由两个判别器(Dx和Dy)以及两个生成器(G和F)组成。
CycleGAN虽然可以使用非成对数据训练,但是两个域的目标要规定好,即每个域的图像要具有一定的规则。比如X域图像都是油画,风格越统一越好。
CycleGAN缺陷:缺少有监督信息(pixel2pixel有成对数据,位置信息对应),所以需要的数据量会更多,收敛也更慢。
代码链接《CycleGAN算法原理(附源代码,可直接运行)》
论文《U-GAT-IT: UNSUPERVISED GENERATIVE ATTENTIONAL NETWORKS WITH ADAPTIVE LAYERINSTANCE NORMALIZATION FOR IMAGE-TO-IMAGE TRANSLATION》、代码
U-GAT-IT接近本次人像生成动漫画的任务,结构继承了CycleGAN的设计,也是有两个生成器和判别器,下图简化,只展示一个生成器;而loss有四种。
解决方法:Photo2Cartoon
Photo2Cartoon生成器有以下三个部分:
Photo2Cartoon生产落地时,面临多种多样的人脸数据。在绘制训练数据时,需要为不同类型的人群设计不同的风格。比如小朋友可以加红晕,更可爱。女青年睫毛更长,男性有胡须,老年人皱纹更明显。如果这些数据混合训练,会导致最终输出风格不确定。(比如输入短发女生,可能会匹配到男性风格,用户体验差)
如果分开训练,由于数据获取成本高,每个类别数据量更少。所以采用了递进训练,这样即使某一类数据匮乏,也能得到很好的训练效果。训练过程如下:
对比其它模型的结果:(精美程度和ID相似度都更胜一筹)
基于Photo2Cartoon的扩展应用:
项目地址《Pixel2Pixel:人像卡通化》
在AI Studio中搜索卡通,有四个数据集,第一个就是人像卡通化数据集。
Pixel2Pixel需要成对数据训练,卡通画没有找画师画,而是photo2cartoon生成的真实照片对应的卡通画。由于是有监督训练,收敛很快。
数据预处理:
其它代码内容请参考《Pixel2Pixel:人像卡通化》
项目地址:Photo2cartoon
%cd /home/aistudio/work/
!git clone https://gitee.com/hao-q/PaddleGAN.git
%cd PaddleGAN/
!pip install -v -e .
!pip install dlib -t /home/aistudio/external-libraries
!pip install scikit-image -t /home/aistudio/external-libraries
# 导入依赖库
import sys
sys.path.append('/home/aistudio/external-libraries')
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
from ppgan.apps import Photo2CartoonPredictor
# 下载测试图片
!wget https://raw.fastgit.org/minivision-ai/photo2cartoon-paddle/master/images/photo_test.jpg -P /home/aistudio/work/imgs
img_src = plt.imread('../imgs/photo_test.jpg')
plt.imshow(img_src)
plt.show()
# 测试
p2c = Photo2CartoonPredictor()
output = p2c.run('../imgs/photo_test.jpg')#使用Photo2CartoonPredictor的run方法得到卡通化结果
#查看测试效果
plt.figure(figsize=(10, 10))
img_input = plt.imread('./output/p2c_photo.png')
img_output = plt.imread('./output/p2c_cartoon.png')
img_show = np.hstack([img_input, img_output])
plt.imshow(img_show)
plt.show()
训练过程如下:
代码如下:
# 解压数据至PaddleGAN/data/
!unzip -q /home/aistudio/data/data68045/photo2cartoon_dataset.zip -d /home/aistudio/work/PaddleGAN/data/
# 训练数据统计
trainA_names = os.listdir('data/photo2cartoon/trainA')
print(f'训练集中真人照数据量: {len(trainA_names)}')
trainB_names = os.listdir('data/photo2cartoon/trainB')
print(f'训练集中卡通画数据量: {len(trainB_names)}')
testA_names = os.listdir('data/photo2cartoon/testA')
print(f'测试集中真人照数据量: {len(testA_names)}')
testB_names = os.listdir('data/photo2cartoon/testB')
print(f'测试集中卡通画数据量: {len(testB_names)}')
# 训练数据可视化
img_A = []
for img_name in np.random.choice(trainA_names, 5, replace=False):
img_A.append(cv2.resize(cv2.imread('data/photo2cartoon/trainA/'+img_name), (256,256)))
img_B = []
for img_name in np.random.choice(trainB_names, 5, replace=False):
img_B.append(cv2.resize(cv2.imread('data/photo2cartoon/trainB/'+img_name), (256,256)))
img_show = np.vstack([np.hstack(img_A), np.hstack(img_B)])[:,:,::-1]
plt.figure(figsize=(20, 20))
plt.imshow(img_show)
plt.show()
# 一行代码开始训练
!python -u tools/main.py --config-file configs/ugatit_photo2cartoon.yaml