我的项目背景

刚刚过来的冬奥会开幕式,能够说是一场美轮美奂的视觉盛宴。其中,科技与艺术的交融铸造了各种梦幻的视觉效果,让咱们看到AI在艺术畛域大有可为。而明天分享的我的项目也是AI+艺术的一个小方向,灵感来源于我的小女儿。

一天,我的小女儿说:“爸爸,我长大要当漫画家,明天我要画哆啦A梦!”。这让人很快慰,她们这些孩子不用像我和我的父辈小时候那样,学好什么是为了走遍天下都“不怕”,她们学习只是因为“喜爱”。可是,喜爱也是没那么容易能喜爱的。通过半天的“挥墨行空”,“小漫画家”总是感觉本人画的哆啦A梦没有书上的难看,逐步有点气馁了。眼看孩子幻想还没腾飞,翅膀就要折断,都怪这平缓的学习曲线。忽然想起以前介绍的一个叫 GauGAN 的模型,能按图像语义编辑图片。那么,为什么不必这个模型做一个涂鸦游戏,让小朋友们都像小马良一样可能“妙笔生画”呢?

技术介绍

本文介绍的涂鸦利用采纳的模型出自文章《Semantic Image Synthesis with Spatially-Adaptive Normalization》。这个模型有个好听的名字GauGAN [1] ,Gau就是梵高的Gau,在格调迁徙网络Pix2PixHD的生成器上进行了改良,应用 SPADE(Spatially-Adaptive Normalization)模块代替了原来的BN层,以解决图片特色图在通过BN层时信息被“洗掉”的问题。Pix2PixHD实际上是一个CGAN(Conditional GAN)条件生成反抗网络,它可能通过输出的管制标签,也就是语义宰割掩码来管制生成图片各个局部的内容。上面就具体介绍一下GauGAN各个部件的实现细节。

1.多尺度判断器

(Multi-scale discriminators) 所谓“多尺度”判断器,就是将多个构造雷同、输出特色图尺寸不同的一组判断器交融在一起应用。其判断图片时,先将图片缩放成不同尺寸别离送入这些判断器,而后将这些判断器的输入加权相加失去最初的判断输入,这样能够加强判断器的判断能力,使得生成器输入的图片更真切。

# Multi-scale discriminators 判断器代码class MultiscaleDiscriminator(nn.Layer):    def __init__(self, opt):        super(MultiscaleDiscriminator, self).__init__()        for i in range(opt.num_D):            sequence = []            feat_size = opt.crop_size            for j in range(i):                sequence += [nn.AvgPool2D(3, 2, 1)]                feat_size = np.floor((feat_size + 1 * 2 - (3 - 2)) / 2).astype('int64') # 计算各个判断器输出的缩放比例            opt_downsampled = copy.deepcopy(opt)            opt_downsampled.crop_size = feat_size            sequence += [NLayersDiscriminator(opt_downsampled)]            sequence = nn.Sequential(*sequence)            self.add_sublayer('nld_'+str(i), sequence)    def forward(self, input):        output = []        for layer in self._sub_layers.values():            output.append(layer(input))        return output

集成的各个判断器别离输出不同缩放尺度的图片计算判断后果,缩放比例通过feat_size = np.floor((feat_size + 1 * 2 - (3 - 2)) / 2).astype('int64')计算失去。

2.逐步精细化的生成器

(Coarse-to-fine generator) 生成器的思路和判断器差不多,先训练一个低分辨率的生成器,而后再加上高分辨率的生成器一起训练。训练高分生成器时应用低分生成器的特色图做辅助。Pix2PixHD 模型的生成器输出语义标签,输入照片格调图片,所以具备残缺的“Encoder-Decoder(编解码器)构造”,而 GauGAN 模型的生成器只须要输出一个正态分布的随机噪声,而不须要编码器局部。它们的构造对比方下图:

\

# Coarse-to-fine generator 生成器代码class SPADEGenerator(nn.Layer):    def __init__(self, opt):        super(SPADEGenerator, self).__init__()        self.opt = opt        nf = opt.ngf        self.sw, self.sh = self.compute_latent_vector_size(opt)        if self.opt.use_vae:            self.fc = nn.Linear(opt.z_dim, 16 * opt.nef * self.sw * self.sh)            self.head_0 = SPADEResnetBlock(16 * opt.nef, 16 * nf, opt)        else:            self.fc = nn.Conv2D(self.opt.semantic_nc, 16 * nf, 3, 1, 1)            self.head_0 = SPADEResnetBlock(16 * nf, 16 * nf, opt)        self.G_middle_0 = SPADEResnetBlock(16 * nf, 16 * nf, opt)        self.G_middle_1 = SPADEResnetBlock(16 * nf, 16 * nf, opt)        self.up_0 = SPADEResnetBlock(16 * nf, 8 * nf, opt)        self.up_1 = SPADEResnetBlock(8 * nf, 4 * nf, opt)        self.up_2 = SPADEResnetBlock(4 * nf, 2 * nf, opt)        self.up_3 = SPADEResnetBlock(2 * nf, 1 * nf, opt)        final_nc = nf        if opt.num_upsampling_layers == 'most':            self.up_4 = SPADEResnetBlock(1 * nf, nf // 2, opt)            final_nc = nf // 2        self.conv_img = nn.Conv2D(final_nc, 3, 3, 1, 1)        self.up = nn.Upsample(scale_factor=2)    def forward(self, input, z=None):        seg = input        if self.opt.use_vae:            x = self.fc(z)            x = paddle.reshape(x, [-1, 16 * self.opt.nef, self.sh, self.sw])        else:            x = F.interpolate(seg, (self.sh, self.sw))            x = self.fc(x)        x = self.head_0(x, seg)        x = self.up(x)        x = self.G_middle_0(x, seg)        if self.opt.num_upsampling_layers == 'more' or \           self.opt.num_upsampling_layers == 'most':            x = self.up(x)        x = self.G_middle_1(x, seg)        x = self.up(x)        x = self.up_0(x, seg)        x = self.up(x)        x = self.up_1(x, seg)        x = self.up(x)        x = self.up_2(x, seg)        x = self.up(x)        x = self.up_3(x, seg)        if self.opt.num_upsampling_layers == 'most':            x = self.up(x)            x = self.up_4(x, seg)        x = self.conv_img(F.gelu(x))        x = F.tanh(x)        return x

去掉了编码器局部的生成器由head(0),G_middle(0,1)和up(0,1,2,3)三局部组成。head次要解决生成器输出噪声。如果应用VAE(变分自编码器) 进行多模型生成,则输出的是 VAE 从特色图片提取的latent code(潜变量),以管制输入图片的格调。两层 G_middle 解决特色映射。4层up(或5层,根据输入尺寸而定)逐层将特色图上采样,直至达到输入尺寸。

为了进一步改善生成图片的品质,模型还给生成器增加了Instance Map(实例宰割标签)作为控制变量:

有了Instance Map提供的边缘信息,模型生成的图片中紧邻的同一类型不同物体的边缘更加清晰正当,如上图中相邻汽车的边缘所示。

3.空间自适应归一化 SPADE

(Spatially-Adaptive Normalization)

为了解决Pix2PixHD在通过语义标签生成照片格调图像时,特色信息通过归一化层被“洗掉”的问题,GauGAN提出了Spatially-Adaptive (De)Normalization,即“空间自适应(反)归一化”,简称SPADE。

SPADE模块将输出的语义标签别离embedding到两个卷积层上,而后用这两个保留了语义标签空间信息的卷积层代替原来归一化层中的缩放系数和偏置。应用SPADE模块前后的对比方下图:

比照可见,有了SPADE模块的加持,格调迁徙网络再也不怕转换大块的的语义标签了。所以,这种按语义掩码生成图片的模型也被称为“语义图像合成网络”。

# SPADE空间自适应归一化模块代码class SPADE(nn.Layer):    def __init__(self, config_text, norm_nc, label_nc):        super(SPADE, self).__init__()        parsed = re.search(r'spade(\D+)(\d)x\d', config_text)        param_free_norm_type = str(parsed.group(1))        ks = int(parsed.group(2))        self.param_free_norm = build_norm_layer(param_free_norm_type)(norm_nc) # 此解决须敞开归一化层的自适应参数        # The dimension of the intermediate embedding space. Yes, hardcoded.        nhidden = 128        pw = ks // 2        self.mlp_shared = nn.Sequential(*[            nn.Conv2D(label_nc, nhidden, ks, 1, pw),            nn.GELU(),        ])        self.mlp_gamma = nn.Conv2D(nhidden, norm_nc, ks, 1, pw)        self.mlp_beta = nn.Conv2D(nhidden, norm_nc, ks, 1, pw)    def forward(self, x, segmap):        # Part 1. generate parameter-free normalized activations        normalized = self.param_free_norm(x)        # Part 2. produce scaling and bias conditioned on semantic map        segmap = F.interpolate(segmap, x.shape[2:])        actv = self.mlp_shared(segmap)        gamma = self.mlp_gamma(actv)        beta = self.mlp_beta(actv)        # apply scale and bias        out = normalized * (1 + gamma) + beta        return out

SPADE模块首先关掉了归一化层的自适应缩放系数和偏置,而后将缩放后的特色图(以适应前一层不同尺寸的输入)embedding 到 mlp_gamma 卷积层中,而后在别离映射到 gamma 卷积层(缩放)和 beta 卷积层(偏置),这样就实现了“用2d卷积层替换替换标量缩放系数和偏置”的操作,以达到保留“通过BN层的空间信息”的目标。

4.GauGAN的Loss 计算

(hinge Loss、Feat Loss、Perceptual Loss) GauGAN的多尺度判断器岂但集成了多个缩放尺寸的判断器,而且在计算判断 Loss时,岂但计算最初一层输入的后果,判断器中间层输入的特色图也参加Loss计算,公式如下:

Pix2PixHD还应用了ImageNet数据集上预训练的VGG19模型作为额定特征提取器计算Perceptual Loss,用于比对虚实图片。与应用判断器中间层输入的特色图计算Loss时不同,应用VGG19中间层特色图计算Loss时要逐层加权,使得模型对高层的语义特色更敏感。

GauGAN的Loss由“反抗损失”、“判断器辅助损失”和“生成器辅助损失” 三局部组成。

①反抗损失采纳Hinge Loss

# 判断器反抗损失df_ganloss = 0.for i in range(len(pred)):    pred_i = pred[i][-1][:batch_size]    new_loss = -paddle.minimum(-pred_i - 1, paddle.zeros_like(pred_i)).mean() # hingle loss    df_ganloss += new_lossdf_ganloss /= len(pred)dr_ganloss = 0.for i in range(len(pred)):    pred_i = pred[i][-1][batch_size:]    new_loss = -paddle.minimum(pred_i - 1, paddle.zeros_like(pred_i)).mean() # hingle loss    dr_ganloss += new_lossdr_ganloss /= len(pred)# 生成器反抗损失g_ganloss = 0.for i in range(len(pred)):    pred_i = pred[i][-1][:batch_size]    new_loss = -pred_i.mean() # hinge loss    g_ganloss += new_lossg_ganloss /= len(pred)

df_ganloss和dr_ganloss别离是判断假图片和真图片的Loss,g_ganloss是生成器损失。应用hinge loss计算判断器损失时,每次只用局部样本的损失更新梯度,稳固了生成器的更新。因而,起初的改良模型甚至去掉了用于稳固判断器更新的谱归一化层。

②判断器辅助损失应用判断器中间层输入的特色图计算 L1 Loss 加和而成

g_featloss = 0.for i in range(len(pred)):    for j in range(len(pred[i]) - 1): # 除去最初一层的中间层featuremap        unweighted_loss = (pred[i][j][:batch_size] - pred[i][j][batch_size:]).abs().mean() # L1 loss        g_featloss += unweighted_loss * opt.lambda_feat / len(pred)

③生成器辅助损失应用 VGG19 预训练模型中间层输入的特色图逐层加权计算 L1 Loss 加和而成

g_vggloss = paddle.to_tensor(0.)if not opt.no_vgg_loss:    rates = [1.0 / 32, 1.0 / 16, 1.0 / 8, 1.0 / 4, 1.0]    _, fake_features = vgg19(resize(fake_img, opt, 224))    _, real_features = vgg19(resize(image, opt, 224))    for i in range(len(fake_features)):        g_vggloss += rates[i] * l1loss(fake_features[i], real_features[i])    g_vggloss *= opt.lambda_vgg

④GauGAN总的损失函数判断器总损失函数:

d_loss = df_ganloss + dr_ganloss

生成器总损失函数:

if opt.use_vae:    g_loss = g_ganloss + g_featloss + g_vggloss + g_vaeloss    opt_e.clear_grad()    g_loss.backward(retain_graph=True)    opt_e.step()else:    g_loss = g_ganloss + g_featloss + g_vgglossopt_g.clear_grad()g_loss.backward()opt_g.step()

如果应用VAE管制生成图片的格调,还要加上VAE生成的变分散布与高斯先验散布的KL散度计算的g_vaeloss,以拉近输出的格调图片与生成图片的格调相似性。

工程实际及更多摸索

1.我的项目实现中遇到的一些问题

①数据处理

CycleGAN提出的时候已经吐槽过Pix2Pix这种像素格调迁徙模型重大依赖成对的数据集。然而,幸好“图像宰割”作为CV深度学习三剑客(图像分类、指标检测、图像宰割)之一,有大量的训练数据集和预训练模型能够用到“格调迁徙/语义图像合成”工作中。

训练“妙笔生画”须要的数据就能够应用宰割模型进行标注。首先,应用飞桨指标检测套件PaddleDetection在ade20k数据集上训练一个宰割模型,而后就能够应用这个宰割模型标注从其余数据集或资源中失去的风景图片。当然,如果有预训练模型的话,间接拿来用也能够,只有对分类类别进行相应的解决。除了ade20k上训练的宰割模型,我想coco数据集上训练的应该也能够用。

用来标注数据的宰割模型精度其实不是很高,但标注的数据用起来成果仿佛还能够。兴许生成模型拟合概率分布时,那些呈现频率低的谬误标注像素并没有被体现进去,如果再应用裁剪通道的形式压缩模型,那些低概率的谬误表白甚至就被剪掉了。

②部署

“妙笔生画”的后盾是应用飞桨预训练模型利用工具PaddleHub部署的,前端展现网页用的是H5写的Web页面,这就要解决JavaScript脚本跨域拜访的问题。当初的我的项目还是通过一个中继的PHP服务端脚本直达了一下http申请,然而这样会导致比拟大的数据传递累赘。如果可能在服务端通过设置跨域资源共享(CORS)的白名单来解决跨域拜访,就更高效了。办法还在摸索中,欢送大家一起探讨。

2.模型改良及正在进行的后续解决

①增加注意力

当初,注意力很风行,但语义图像合成离着上Transfomer还有点边远,那么就先用21年“上新”的SimAM(Simple, Parameter-Free Attention Module 简略无参注意力模块)试试吧。这个注意力机制借鉴了神经科学实践,应用能量函数评估神经元的重要性。代码如下:

def simam(x, e_lambda=1e-4):    b, c, h, w = x.shape    n = w * h - 1    x_minus_mu_square = (x - x.mean(axis=[2, 3], keepdim=True)) ** 2    y = x_minus_mu_square / (4 * (x_minus_mu_square.sum(axis=[2, 3], keepdim=True) / n + e_lambda)) + 0.5    return x * nn.functional.sigmoid(y)

果然是Simple,6行代码搞定(上方代码)。这个SimAM模块加在了生成器和判断器的各个残差快的激活前面,具体设置能够参考本文最初整顿的AI Studio开源我的项目。下图是GauGAN应用SimAM注意力前后的比照(左一列为应用SimAM后,左二列为原版GauGAN,左三列为实在图片,左边三列为deeplabv2预训练模型的宰割后果)。

②降级SPADE模块

SPADE模块尽管好用,但计算代价微小,所以,有人出品了个简化版的“自适应(反)归一化模块”

这个开始的思路是:真正使得模型晋升成果的是SPADE模块中保留的类别信息,而非空间信息。所以,改良版本的CLADE(Class-Adaptive(De)Normalization)模块只将类别信息映射到了反归一化模块的缩放系数和偏置中,大大节俭了参数量和计算量。但起初又发现,保留空间信息还是能使模型的成果晋升一些的,就又应用语义标签手动计算了ICPE(intra-class positional encoding类内地位嵌入码)乘到了缩放系数和偏置上。最终版本的CLADE-ICPE 在生成品质与SPADE相当的状况下,大大降低了参数量和计算量。

③压缩模型

除了对SPADE模块进行改良外,最近又出了两篇压缩GAN模型的文章,也正在试验,简略介绍一下。

GAN CAT(Compression And Teaching 压缩和蒸馏) GAN CAT办法的最大特点是:老师生成器TeacherG除了用于蒸馏,还作为模型搜寻空间应用,无需训练额定的Supernet模型。模型构造的搜寻空间通过InsResBlocks模块实现。裁剪过程同时抉择模型构造与通道。

CAT办法裁剪应用的阈值(归一化层的缩放因子)依据压缩指标主动求出,无需迭代裁剪过程。蒸馏应用 KA(Kernel Alignment)掂量不同通道数的卷积构造之间的相似性。

OMGD(Online Multi-Granularity Distillation 多粒度在线蒸馏) OMGD的思路十分清晰,就是用一个更深的模型和一个更宽的模型来进行蒸馏,一图以蔽之:

OMGD一边训练两个更深、更宽的老师生成器,一边用其进行蒸馏,可能使过程更加稳固,这就是在线。应用深度雷同宽度更宽的老师模型进行蒸馏时,loss函数岂但比对输入后果,而且对中间层的特色图也应用Structural Similarity (SSIM) Loss进行比对,是以称之为多粒度。

结语

最初,来看看“妙笔”是怎么“生画”的吧:

近期,更加风骚的GauGAN2公布了,八般武艺样样SOTA的女娲也公布了,甚至 StyleGAN也是啥工作都敢上,通通玩坏了,目测后方一大波好玩的模型来袭,让咱们在元宇宙里happy地大GAN一场吧 !

为了便于大家体验各种GAN模型,这里附上一些发到AI Studio上的开源我的项目:①GAN的“格调迁徙五部曲”

  • 《一文搞懂生成反抗网络之经典GAN》 https://aistudio.baidu.com/aistudio/projectdetail/551962
  • 《一文搞懂GAN的格调迁徙之Conditional GAN》 https://aistudio.baidu.com/aistudio/projectdetail/644398
  • 《一文搞懂GAN的格调迁徙之Pix2Pix》 https://aistudio.baidu.com/aistudio/projectdetail/1119048
  • 《一文搞懂GAN的格调迁徙之CycleGAN》 https://aistudio.baidu.com/aistudio/projectdetail/1153303
  • 《一文搞懂GAN的格调迁徙之SPADE论文复现》https://aistudio.baidu.com/aistudio/projectdetail/1964617
  • 《妙笔生画》https://aistudio.baidu.com/aistudio/projectdetail/2274565

②GAN“前传”

  • 《一文搞懂卷积网络之一(从LeNet到GoogLeNet)》 https://aistudio.baidu.com/aistudio/projectdetail/601071
  • 《入手学深度学习》Paddle 版源码(经典CV网络合集) https://aistudio.baidu.com/aistudio/projectdetail/1639856

参考文献[1] Park T, Liu M Y, Wang T C, et al. Semantic Image Synthesis With Spatially-Adaptive Normalization[C]// 2019 IEEE/CVF Conference on Computer Vision and Pattern Recognition (CVPR). IEEE, 2019.