通过四篇经典论文,大二学弟学GAN是这么干的
深圳福田部分社区开始全员核酸检测,希望大家积极响应。(采样只需数秒)
最近在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 实操
下面,我们将分别介绍网络架构、模型训练。
下面程序中,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)
鉴别器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
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去掉。
#设置训练轮数
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(实操)
理论一大堆,其实实操很简单,就是将交叉熵换成最小二乘。
上述的算法有两种实现方法 这里要将鉴别器的输出层改成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论文的复现,并且体验极其舒适,也达到了原论文的表现。
希望飞桨继续加速迭代,更好地支持最新技术,简化开发流程,受益大众。同时,也希望开源社区的伙伴们共同开发更多的算子,提升训练速度,让更多的模型可以在口袋里运行,造福百姓!
项目链接
GitHub: https://github.com/PaddlePaddle/Paddle
Gitee: https://gitee.com/paddlepaddle/Paddle
许式伟:Go+门槛比Go低,小孩6年级可开始学Go+
2021-06-05
一周暴涨1300+ Star,一款相当牛的超实用工具
2021-06-04
开源囧事|捅娄子了,写个bug被国家信息安全漏洞共享平台抓到了?
2021-06-06