通过四篇经典论文,大二学弟用飞桨学GAN是这么干的

2021/05/28 04:07
阅读数 71

点击左上方蓝字关注我们

飞桨开发者说】李宇奇,金陵科技学院本科在读。爱好领域:计算机视觉、对抗网络。

最近在AI Studio上学习李宏毅老师的强化学习课程时,老师提及了GAN这个概念。老师说GAN的思想与强化学习很像,李宏毅老师还发表了一篇StepGAN,将强化学习中的Q-learning与GAN结合了起来,很有意思。

经了解,GAN是一种很炫酷的技术,使用它,可以进行AI换脸,风格迁移,对文本生成进行优化,甚至可以看看将三维的我们展开为二维的纸片人的形象。

下面让我们开始GAN的学习吧~

Traditional GAN

1 GAN的原理

我们先学习一下传统的GAN,也就是我们无敌的Goodfellow大神的那篇开山之作Generative Adversarial Nets。首先看论文的标题,Generative 表明我们这次玩的是生成模型。目前深度学习大部分任务都是鉴别模型,这主要归功于反向传播算法与网络的加深。生成模型一直是学界的一个难题,第一大原因:在最大似然估计和相关策略中出现许多难以处理的概率计算,生成模型难以逼近。第二大原因:生成模型难以在生成环境中利用分段线性单元的好处,因此其影响较小。再看看后面的Adversarial和Nets,我们注意到是Nets而不是Net,说明这里应该有多个网络,并且它们的关系是Adversarial,相互对抗的。看到这里,我们大概对本篇论文有了大致的了解,下面让我们看看GAN的原理。

GAN开创性地提出了一种对抗关系,设计了两个网络,一个鉴别器(Discriminator),一个生成器(Generator)。为了让生成器学会训练集上的数据x的概率分布Px,我们定义了一个关于输入噪声变量Pz(z)的先验分布。通过生成器映射为G(z;θg),使其与训练集图片的像素概率分布Px尽可能接近。鉴别器则接收一个像素矩阵(可能来自训练集,也可能来自生成器),输出一个标量,代表输入是来自训练集而不是G(z;θg)的概率。我们训练鉴别器D以最大限度地提升为训练样本和来自G的样本分配正确标签的概率。同时训练生成器G以最小化对数(1−D(G(z)))。换句话说,D和G玩以下具有值函数V(G、D)的双人极大最小游戏。

下图展示了GAN的训练过程:

相关说明:

  • 蓝色虚线为Discriminator的鉴别分布

  • 黑色虚线为训练集的数据分布

  • 绿色实线为Generator的生成分布

  • 向上箭头显示映射x=G(z)如何将非均匀分布Pg施加在转换样本上

流程介绍:

  • (a) D和G都是随机初始化后的分布

  • (b) D经历了迭代,对于接近训练集的数据予以高分,对Pg分布予以低分

  • (c) G也经历了迭代,D的梯度已经引导Pg流向更有可能被归类为训练集的区域

  • (d) D和G经历了很多次迭代,Pg已经完全拟合了训练集的分布,而D也无法分辨出Pg与Px的区别,统统给了0.5分(注意,这是非常理想的情况,实作中可能达不到)

2 实操

下面,我们将分别介绍网络架构模型训练

2.1 ConvBN层的定义

下面程序中,ConvBN类包含了基础的二维卷积、批标准化和激活函数层,三者构成卷积网络的基本单元。有划水员可能发现笔者在work下的ConvBN里定义了mish激活,笔者本来想用mish的,但是发现效果一般,并且消耗计算资源比较多,就放弃了。但是mish的曲线确实丝滑,神经元活性十足,大家可以尝试一下。

import paddle.nn as nn
import paddle
class ConvBN(nn.Layer):
    def __init__(self,num_channels,num_filters,filter_size,stride=1,padding="valid"):
        super(ConvBN, self).__init__()
        #nn.initializer.set_global_initializer(nn.initializer.KaimingUniform(),nn.initializer.Constant(0.1))
        self.conv=nn.Conv2D(num_channels,num_filters,filter_size,stride,padding=padding,bias_attr=False)
        self.BN=nn.BatchNorm(num_filters)
        self.lrelu=nn.LeakyReLU(0.2)

    def mish(self,x):
        return x*paddle.tanh(paddle.log(paddle.exp(x)+1))

    def forward(self,x):
        x=self.conv(x)
        x=self.BN(x)
        x=self.lrelu(x)
        return x

下面我们分别介绍生成器与鉴别器的架构:

2.2 生成器

生成器Tips:

  • 笔者经过多次实验,发现生成器的卷积要尽可能大一些。另外,每一层都加一层Batchnorm,除了输出层(DCGAN论文里是这么说的,笔者经过实作发现确实如此。如果在输出层加了Batchnorm,收敛会不稳定,同时比较慢)。

  • 尽量不用反卷积,少用转置卷积,否则很容易出现棋盘效应,图像的颗粒感很严重。

  • 解决方法:知乎大神说,可以使用上采样加多层卷积(upsample+conv) / (pixelshuffle+conv),后者听说效果更好,在超分辨率里也有使用。

  • 生成器激活函数使用ReLU,DCGAN原文中如是说。

%matplotlib inline #调用魔术方法,显示matplotlib的图像
import matplotlib.pyplot as plt
#从work目录中导入ConvBN类
from work.ConvBN import ConvBN
import paddle.nn as nn
#将warning干掉
import warnings
warnings.filterwarnings("ignore", category=Warning)

class Generator(nn.Layer):
    def __init__(self):
        super(Generator, self).__init__()
        #nn.initializer.set_global_initializer(nn.initializer.KaimingUniform(), nn.initializer.Constant(0.1))
        model=[
            #padding方式设为same,即不会改变图像的形状,当然步长要设为1
            ConvBN(1,128,5,1,padding="same"),
            #将图像的通道数增大4**2倍,同时将大小减2至8
            ConvBN(128,128*16,3,1),
            #上采样,将大小扩大4倍,同时将通道数缩小4**2倍
            nn.PixelShuffle(4),
            ConvBN(128,128,5,1,padding="same"),
            #最后一层去掉Batchnorm
            nn.Conv2D(128,1,5,1,padding="same"),
            #将RGB值限制在0到1,也同样可以读取图像色彩
            nn.Sigmoid()
        ]
        self.model=nn.Sequential(*model)

    def forward(self,x):
        #将数据展开
        x=paddle.reshape(x,[batch_size,1,10,10])
        return self.model(x)

2.3 鉴别器

鉴别器Tips:

  • 不同的初始化经过实验发现差别很大,但只是在随机生成任务中(低维度随机生成的向量映射到高维图像)差别很大,在其它的GAN任务中差别不大。总之,笔者这里借用了百度官方的初始化方式。

  • 生成器与鉴别器的卷积层数量最好一样,本质上是让两个神经网络的参数量相同,即复杂度相同。

  • 生成器激活函数使用Leaky ReLU,DCGAN原文中如是说,参数设为0.2.

  • 输出层使用Sigmoid激活,使图像的分数限制在0到1.

#从work目录导入ConvBN类
from work.ConvBN import ConvBN
class Discriminator(nn.Layer):
    def __init__(self):
        super(Discriminator, self).__init__()
        #nn.initializer.set_global_initializer(nn.initializer.KaimingUniform(), nn.initializer.Constant(0.1))
        model=[
            #将通道数扩大至64,学习更多特征,同时卷积核大小设为5,感知野更大
            ConvBN(1,64,5,2),
            #增大通道数,进一步缩小图像大小
            ConvBN(64,128,5,2),
            #缩小图像大小,通道数不变,因为是手写数字,特征已经够多了
            ConvBN(128,128,5,2),
            #卷积核大小为1,步长为1,单纯合并通道
            nn.Conv2D(128,1,1,1),
            #将数据展开
            nn.Flatten(),
            #将分数限制在0到1
            nn.Sigmoid()
        ]
        self.model=nn.Sequential(*model)

    def forward(self,x):
        x=self.model(x)
        return x

2.4 模型训练

 2.4.1 迭代算法

For  每轮  do
    For k次(k是超参,即训练鉴别器k次,再训练生成器1次)  do
        从噪声分布中取出m笔数据z_i
        从数据集中取出m笔数据x_i
        使用梯度上升更新迭代器:
            最大化log(D(x_i))+log(1-D(G(z_i)))
    执行一次
        从噪声分布中取出m笔数据z_i
        使用梯度下降更新生成器:
            最小化log(1-D(G(z_i)))

训练Tips:

  • 生成器和鉴别器的优化器分开定义,因为梯度反向传播时要防止互相影响。

  • 要先迭代生成器k次,再迭代鉴别器,此处k是超参,需要自己调,炼丹当然要自己炼。

  • 注意上述算法中对鉴别器的迭代需要梯度上升(gradient ascent),而对生成器是梯度下降。

  • 笔者实作时发现极其容易梯度爆炸,经过两个星期的研究,发现是log与鉴别器的sigmoid产生了矛盾,刚开始迭代时鉴别器是随机初始化,很容易给出0分,而当输入为0左右时,log趋向与负无穷,所以梯度爆炸。强烈建议把log去掉。

2.4.2 实操训练

#设置训练轮数
epoch_num = 100
#开启visualdl
from visualdl import LogWriter
import warnings
#此处可以把warning干掉
warnings.filterwarnings("ignore", category=Warning)
log_writer = LogWriter("./log/vdl_log")

#设置学习率,可以把生成器和鉴别器的学习率分开设置,让鉴别器学的更快
g_learning_rate = 2e-4
d_learning_rate = 2e-4
k = 3  #鉴别器迭代k轮,生成器迭代1轮
g_clip = paddle.nn.ClipGradByNorm(clip_norm=1e-2) #梯度裁剪,防止梯度爆炸,但后续发现加了梯度裁剪也会使收敛变慢, 
d_clip = paddle.nn.ClipGradByNorm(clip_norm=1e-2) #经过尝试发现去掉梯度裁剪在traditional GAN中不会梯度爆炸。。。 
#去掉注释即开启梯度裁剪
#g_optimizer = paddle.optimizer.Adam(learning_rate=g_learning_rate, parameters=generator.parameters(),
                                    #beta1=0.5, beta2=0.999,grad_clip=g_clip)
#定义优化器,此处的超参都是根据DCGAN论文设置,DCGAN这篇论文确实讲的很细
g_optimizer = paddle.optimizer.Adam(learning_rate=g_learning_rate, parameters=generator.parameters(),
                                    beta1=0.5, beta2=0.999)
d_optimizer = paddle.optimizer.Adam(learning_rate=d_learning_rate, parameters=discriminator.parameters(),
                                     beta1=0.5, beta2=0.999)
#这里使用交叉熵损失,用来衡量预测值与标签的差距
loss=nn.BCELoss()
with log_writer as logger:
    #step_d,step_g记录每一次迭代后的损失值
    step_d, step_g = 0, 0
    #将网络的模式设置为训练模式
    generator.train()
    discriminator.train()
    for epoch in range(epoch_num):
        for i, x in enumerate(dataloader()):
            #预先清除梯度,防止对迭代造成影响
            d_optimizer.clear_grad()
            #取出图像数据
            real = x[0]
            #获取噪声
            noise = paddle.randn([batch_size, 100])
            #得到虚假图像
            fake = generator(noise)
            #获取真实图像与噪声的分数
            fake_score = discriminator(fake)
            real_score = discriminator(real)
            #真实图像的满分标签
            ones=paddle.ones([batch_size,1])
            #得到真实图像的分数与标签的损失值
            real_d_loss=loss(real_score,ones)
            real_d_loss.backward()  #这里不需要optimize.step(),梯度自动累计,在下面再进行更新

            #噪声的零分标签
            zeros=paddle.zeros([batch_size,1])
            #计算虚假图像的损失值
            fake_d_loss=loss(fake_score,zeros)
            #反向传播梯度
            fake_d_loss.backward()
            #更新网络梯度
            d_optimizer.step()
            #清除梯度
            d_optimizer.clear_grad()
            #对d_loss求和
            d_loss=fake_d_loss+real_d_loss
            #记录至log中
            logger.add_scalar(tag="d_loss", step=step_d, value=d_loss)
            #记录鉴别器迭代次数
            step_d+=1

            if i % k == 0:
                #预先清除梯度
                g_optimizer.clear_grad()
                #使用正态分布获取噪声
                noise = paddle.randn([batch_size, 100])
                #得到虚假图像
                fake = generator(noise)
                #计算虚假图像的分数
                fake_score = discriminator(fake)
                #这里因为是训练生成器,目标是最大化噪声的分数,所以这里给出满分标签
                ones=paddle.ones([batch_size,1])
                #交叉熵损失衡量分数与标签的差距
                g_loss=loss(fake_score,ones)
                #使用链式法则反向传播计算梯度
                g_loss.backward()
                #最小化g_loss
                g_optimizer.minimize(g_loss)
                #清楚梯度
                g_optimizer.clear_grad()
                #将g_loss值添加入log中
                logger.add_scalar(tag="g_loss", step=step_g, value=g_loss)
                #记录生成器迭代次数
                step_g += 1
            if (i + 1) % 100 == 0:
                print("epoch:%d,i:%d,d_loss:%f,g_loss:%f,fake_score:%f,real_score:%f" % (
                epoch, i, d_loss, g_loss, paddle.mean(fake_score), paddle.mean(real_score)))

        #图形化,看看当前生成器的水平
        noise = paddle.randn([batch_size, 100])
        #获取虚假图像
        fake = generator(noise)
        #将tensor转化为numpy
        generated_image = fake.numpy()
        #设置框的大小
        plt.figure(figsize=(15,15))
        try:
            for i in range(10):
                #取出图像
                image = generated_image[i].transpose()
                #获取像素值大于0的RGB值,小于0的地方填0
                image = np.where(image > 0, image, 0)
                #改变image的形状
                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(epoch, i, d_loss.numpy(), g_loss.numpy())
            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)

        if (epoch + 1) % 10 == 0:
            #保存模型参数
            paddle.save(generator.state_dict(), "work/generator.pdparams")
            paddle.save(discriminator.state_dict(), "work/discriminator.pdparams")

‍‍‍‍‍‍‍‍‍‍‍3 Traditional GAN的优缺点

缺点:

  • 没有Pg(x)(即G的数据空间)的明确表示,随机性比较大。

  • D在训练期间必须与G同步(特别是在不更新D的情况下不能训练太多,以避免G将太多的随机分布映射到相同的图片,使G的数据空间具备多样性)。

优点:

  • 不需要马尔可夫链,只使用反向支撑来获得梯度。

  • 在学习过程中不需要推理,并且在模型中可以纳入各种函数(变化很多,有很大的提升空间)。

LSGAN

1 Abstract(摘要)

众所周知,Traditional GAN真的是极其难train,动不动就梯度消失或者爆炸。另外,生成的图像质量也不咋滴。因此,LSGAN横空出世,缓解了上述两个问题。不说废话,直接讲原理和实作~

2 Algorithm(算法)

LSGAN的迭代算法如上图所示,与Traditional GAN有较大不同,将原先的F散度改成了最小二乘损失函数。原先最小化GAN的目标函数会出现梯度的消失,这使得很难更新生成器。LSGAN可以缓解这个问题,因为LSGANs根据样本到决策边界的距离来惩罚样本,从而产生更多的梯度来更新生成器。另外,与常规GAN几乎不会丢失决策边界正确一侧的样本不同,即使样本正确分类,LSGAN也会对其进行惩罚,有效缓解了梯度消失的问题。

3 Implement(实操)

理论一大堆,其实实操很简单,就是将交叉熵换成最小二乘。

loss=nn.BCELoss()->loss=nn.MSELoss()

BCELoss就是binary cross entropy损失,使用交叉熵衡量预测值与标签的差距

MSELoss就是mean square error损失,使用均方误差衡量预测值与标签差距

Tips:

  • 上述的算法有两种实现方法

  • 这里要将鉴别器的输出层改成Tanh激活,使输出限制在-1到1

  • 这里跟traditional GAN差不多,网络代码不用改。

WGAN与WGAN-GP

1 Abstract(摘要)

经历了让人心碎的Traditional GAN的迭代算法后,我们又学习了LSGAN,用来稳定GAN的训练与提升图像的质量。最后,让我们来学习无敌的WGAN-GP,虽然有难度……依然不讲废话,直接讲原理和算法,吼吼~

WGAN提出了一种衡量模型预测的样本与真实样本的距离,极其复杂。原文称为Earth Mover,也叫推土机距离。一开始作者自己也不知道怎么实作,就简单加了个grad clipping,当然有效……后来,作者终于肝出来了,也就是我们的WGAN-GP,下面介绍算法与实作。

2 Algorithm(算法)


当生成器还没有收敛时,do
    迭代k次(k是超参,即训练鉴别器k次,再训练生成器1次)
        从数据集中取出数据x,噪声z,一个0至1的随机数m
        x_tita=G(z)
        x_hat=m*x+(1-m)*x_tita
        loss=D(x_tita)-D(x)+lamt*(D(x_hat)的梯度的第二范数-1)**2
        对生成器进行梯度下降迭代
    执行一次:
        获取噪声z
        对-D(G(Z))进行梯度下降迭代

3 Implement(实操)

#此处实现上图中的获取梯度惩罚项
def gradient_penalty(discriminator, real, fake, batchsize,lamt):
    #real是真实图像
    #fake是虚假图像
    t = paddle.uniform((batchsize,1,1,1))
    #扩大形状
    t = paddle.expand_as(t, real)
    #对真实图像与虚假图像取噪声求均值
    inter = t * real +  (1-t) * fake
    inter.stop_gradient = False
    inter_ = discriminator(inter)
    #调用Paddle的API求导
    grads = paddle.grad(inter_, [inter])[0]
    epsilon = 1e-12
    #获取norm
    norm = paddle.sqrt(
        paddle.mean(paddle.square(grads), axis=1) + epsilon
    )
    #lamt是倍率
    gp = paddle.mean((norm - 1)**2)*lamt

    return gp

以下是调用示范:

gp=gradient_penalty(discriminator,real,fake,10)
d_loss=paddle.mean(fake_score)-paddle.mean(real_score)+gp
d_loss.backward()
d_optimize.step()
g_loss=-paddle.mean(fake_score)

以上代码来自叶月火狐大佬~

项目链接:

https://aistudio.baidu.com/aistudio/projectdetail/1426558

Tips:

  • 对g_loss取负号,即将梯度下降转化为梯度上升。

  • 使用WGAN时,需要将鉴别器的输出层Sigmoid拔掉,使输出变成线性的结果,防止当鉴别器饱和时的梯度消失。

  • 将优化器改成Adam

结果展示


从总体来说,生成了较为清晰的图片,但数字略有模糊,说明我们的生成器的水平还有待提高。

总结与展望

通过使用飞桨框架2.0版本,我们完美完成了四篇GAN论文的复现,并且体验极其舒适,也达到了原论文的表现。

希望飞桨继续加速迭代,更好地支持最新技术,简化开发流程,受益大众。同时,也希望开源社区的伙伴们共同开发更多的算子,提升训练速度,让更多的模型可以在口袋里运行,造福百姓!

项目链接

https://aistudio.baidu.com/aistudio/projectdetail/1668279

如在使用过程中有问题,可加入官方QQ群进行交流:778260830。

如果您想详细了解更多飞桨的相关内容,请参阅以下文档。

·飞桨官网地址·

https://www.paddlepaddle.org.cn/

·飞桨开源框架项目地址·

GitHub: https://github.com/PaddlePaddle/Paddle 

Gitee: https://gitee.com/paddlepaddle/Paddle

????长按上方二维码立即star!????

飞桨(PaddlePaddle)以百度多年的深度学习技术研究和业务应用为基础,是中国首个开源开放、技术领先、功能完备的产业级深度学习平台,包括飞桨开源平台和飞桨企业版。飞桨开源平台包含核心框架、基础模型库、端到端开发套件与工具组件,持续开源核心能力,为产业、学术、科研创新提供基础底座。飞桨企业版基于飞桨开源平台,针对企业级需求增强了相应特性,包含零门槛AI开发平台EasyDL和全功能AI开发平台BML。EasyDL主要面向中小企业,提供零门槛、预置丰富网络和模型、便捷高效的开发平台;BML是为大型企业提供的功能全面、可灵活定制和被深度集成的开发平台。

END

本文同步分享在 博客“飞桨PaddlePaddle”(CSDN)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。

展开阅读全文
加载中

作者的其它热门文章

打赏
0
0 收藏
分享
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部