关于人工智能:美景本天成妙笔偶得之妙笔是怎样炼成的

44次阅读

共计 9906 个字符,预计需要花费 25 分钟才能阅读完成。

我的项目背景

刚刚过来的冬奥会开幕式,能够说是一场美轮美奂的视觉盛宴。其中,科技与艺术的交融铸造了各种梦幻的视觉效果,让咱们看到 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_loss
df_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_loss
dr_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_loss
g_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_vggloss
opt_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.

正文完
 0