目录
参考资料
论文:
Generative Adversarial Networks
博客:
Generative Adversarial Networks(GAN的故事)
视频:
可视化GAN:
代码:
第1章 前言
1.1 Background
从LeNet诞生(CNN开荒)、AlexNet在ImageNet一枝独秀,深度学习成为学术界、工业界的宠儿。神经网络结构一深再深、一宽再宽,其中应用广泛的包括VGG16、GoogLeNet、ResNet、ResNext、Xception、DenseNet、SENet等。不管网络结构如何变化,CNN在CV上的成功始终是一个黑盒子(DeepMind曾以心理学的方式解释神经网络),可解释性非常差,很难知道CNN到底学了什么东西。
当然生成模型在深度学习上的应用也是存在的,例如RBM、DBM、DBN等。但生成模型的使用,无法避开的一个问题是优化难题。我们知道,生成模型的求解是以贝叶斯理论为基础,以近似方法进行参数优化(MCMC,变分推断等)。由于独特的求解方式,计算问题成了生成模型在深度网络上的瓶颈(深度网络流行的一大原因是前馈神经网络和反向梯度传播的出现解决了参数优化问题)。GAN通过对抗的方式学习数据分布,避开了对近似优化方法的使用,巧妙解决了生成模型的瓶颈。
说到GAN不得不提“LSTM之父”Schmidhuber的PM模型(predictability minimization),2016年NIPS大会Goodfellow作GAN Tutorial两人也就PM与GAN互相争论。简单地说,GAN就PM存在三点不同。
- 1、最主要的一点不同,GAN的对抗思想贯穿在整个训练流程,PM的对抗只是为了各输出单元独立不依赖其它单元目的。
- 2、GAN的判别网络的输入为高维向量,PM则为标量。
- 3、GAN的对抗更像是一个对抗游戏,而PM的对抗则以一个Object Function进行。
1.2 What is GAN
生成对抗网络(GAN)
是一种神经网络,它以 随机噪声
为输入并生成输出(例如一张人脸的图片),输出是与训练集同分布的样本(例如其他人脸的集合)。
GAN
通过同时训练两个模型来实现这一壮举:
- 捕捉训练集分布的生成式模型。
- 判别模型估计样本来自训练数据而非生成模型的概率。
1.3 Why GAN
- (1)扩充数据集:如果训练数据不充分,GAN可以根据已知的数据并生成合成图像来扩充数据集;
- (2)创建看起来像人脸照片的图像,即使这些脸不属于给定分布中的任何真实的人;
- (3)用文本生成图像;
- (4)提高分辨率:
- (5)GAN也可以用于合成高保真音频或执行语音翻译;
1.4 The advantages of GAN
- 数据标签是一项昂贵的任务。GAN是无监督的,因此不需要有标签的数据来训练它们。
- GAN目前能生成最清晰的图像。对抗性训练使这成为可能。由均方误差产生的模糊图像在GAN面前是没有机会的。
- GAN中的两个网络都可以只用反向传播进行训练。
- 生成器G用判别器D训练,而不是原始数据,阻隔了过拟合;
1.5 How GAN works
我们将生成内容的网络称为 G(Generator)
,将鉴别内容的网络称为 D(Discriminator)
,下图中枯叶蝶进化的例子可以很好的说明 GAN
的工作原理。
图中的枯叶蝶扮演 Generator
的角色,相应的其天敌之一的麻雀扮演 Discriminator
的角色。起初,枯叶蝶的翅膀与其他的蝴蝶别无二致,都是色彩斑斓;
- 第一阶段:麻雀为了识别并捕杀蝴蝶升级自己的判别标准为非棕色翅膀;
- 第二阶段:为了躲避麻雀,枯叶蝶的翅膀进化为棕色;
- 第三阶段:麻雀更加聪明,识别枯叶蝶的标准升级为所看到的物体是否具有纹路;
- 第四阶段:枯叶蝶的翅膀进化出纹路更像枯叶;
- ……
如此不断的进行下去,伴随着枯叶蝶的不断进化和麻雀判别标准的不断升级,二者不断地相互博弈,最终导致的结果就是枯叶蝶的翅膀(输出)无限接近于真实的枯叶(真实物体)。
第2章 GAN网络
2.1 判别算法和生成算法
判别算法
是指给定实例的一些特征,我么根据这些特征来判断它所属的类别,它建模的是特征和标签之间的关系。例如在MNIST数据集中,我们需要判断一个图片是哪个数字。我们可以用后验概率来建模判别算法,假设一个数据的特征是
x
x
x ,它的标签是
y
y
y 。判别算法指的是在给定
x
x
x 的前提下,标签是
y
y
y 的概率,表示为
p
(
y
∣
x
)
p(y|x)
p(y∣x) 。
生成算法
并不关心数据的标签是什么,它关心的是能否生成和数据
x
x
x 同一个分布的特征。
2.2 GAN的逻辑
GAN
是一个由两个模型组成的系统:判别器( D )
和 生成器( G )
。
-
判别器的任务是判断输入图像是源自数据集中还是由机器生成的。判别器一般使用二分类的神经网络来构建,一般将取自数据集的样本视为正样本,而生成的样本标注为负样本。
-
生成器的任务是接收随机噪声,然后使用反卷积网络来创建一个图像。生成器的随机输入可以看做一个种子,相同的种子会得到相同的生成图像,不同的种子则得到的图像不同,大量种子的作用是保证生成图像的多样性。
在最原始的GAN论文中,它都是使用了 MLP
搭建生成模型和判别模型。
GAN
的双系统的目的是让生成器尽量去迷惑判别器,同时让判别器尽可能的对输入图像的来源进行判断。两个模型之间是互相对抗的关系,它们都会通过试图击败对方来使自己变得更好。生成器可以通过判别器得到它生成的图像和数据集图像分布是否一致的反馈,而判别器则可以通过生成器得到更多的训练样本。
2.3 生成器Generator
GAN中的Generator是一种神经网络,给定一组随机的值,通过一系列非线性计算产生真实的图像。该生成器产生假图像 X f a k e X_{fake} Xfake ,其中随机向量 Z Z Z,服从多元高斯分布采样。
生成器的作用是:
- 欺骗的判别器;
- 产生逼真的图像;
- 随着训练过程的完成,实现高性能生成效果;
2.4 判别器Discriminator
判别器试图用特定的标签对数据集中的不同类进行分类。因此,在本质上,它类似于一个监督分类问题。此外,判别器对观察结果的分类能力不仅限于图像,还包括视频、文本和许多其他领域(多模态)。
在GAN中,判别器的作用是解决一个二值分类问题,学习区分真假图像。它是这样做的:
- 预测观察结果是由生成器(
假的
)生成,还是来自原始数据分布(真实的
)。 - 在此过程中,它学习一组参数或权重。随着训练的进行,权重也在不断更新。
2.5 GAN的训练
判别器和生成器交替训练,训练
K
K
K 次 D
,训练一次 G
,通过博弈的手段来不断的对两个模型进行迭代的优化,它的基本流程如下:
1.初始化判别器的参数 θ D θ_D θD 和生成器的参数 θ G θ_G θG ;
2.从分布为 p d a t a ( x ) p_{data}(x) pdata(x) 的数据集中采样 m m m 个真实样本 x ( 1 ) , … , x ( m ) {x(1),…,x(m)} x(1),…,x(m) 。同时从噪声先验分布 p g ( z ) p_g(z) pg(z) 中采样 m m m 个噪声样本 { z ( 1 ) , … , z ( m ) } \{z(1),…,z(m)\} {z(1),…,z(m)} ,并且使用生成器获得 m m m 个生成样本 { x ~ ( 1 ) , … , x ~ ( m ) } \{\tilde{x}(1),…,\tilde{x}(m)\} {x~(1),…,x~(m)} ;
3.固定生成器,使用梯度上升策略训练判别器使其能够更好的判断样本是真实样本还是生成样本,如式(1);
这里为什么是【梯度上升】:因为对于判别器而言,它需要求最大化的损失函数,可以通过梯度上升思想来求取。
它分辨假数据的能力越强越好,所以 D ( G ( Z ( i ) ) ) D(G(Z^{(i)})) D(G(Z(i))) 的值越小越好,从而 l o g ( 1 − D ( G ( Z ( i ) ) ) ) log(1-D(G(Z^{(i)}))) log(1−D(G(Z(i)))) 的值越大越好;而对于真样本 D ( x ( i ) ) D(x^{(i)}) D(x(i)) 也应该越大越好,所以整个式子的值越大越好。
训练刚开始,生成的图像太假,太容易被判别器识破,此时 D ( G ( Z ( i ) ) ) D(G(Z^{(i)})) D(G(Z(i)))接近0, l o g ( 1 − D ( G ( Z ( i ) ) ) ) log(1-D(G(Z^{(i)}))) log(1−D(G(Z(i))))饱和无梯度,所以需要把原来的最小化 l o g ( 1 − D ( G ( Z ( i ) ) ) ) log(1-D(G(Z^{(i)}))) log(1−D(G(Z(i)))) 变成求最大化 l o g ( D ( G ( Z ( i ) ) ) ) log(D(G(Z^{(i)}))) log(D(G(Z(i))))。
4.循环多次对判别器的训练后,我们使用较小的学习率来对生成器进行优化,生成器使用梯度下降策略进行优化,如式(2);
这里为什么是【梯度下降】:因为对于生成器而言,它生成的图像越逼真,判别器给出的分数越高,也就是需要 D ( G ( Z ( i ) ) ) D(G(Z^{(i)})) D(G(Z(i))) 的值越大越好,从而 l o g ( 1 − D ( G ( Z ( i ) ) ) ) log(1-D(G(Z^{(i)}))) log(1−D(G(Z(i)))) 的值越小越好;
5.多次更新之后,我们的理想状态是生成器生成一个判别器无法分辨的样本,此时生成器和判别器达到 纳什均衡
,即最终判别器的分类准确率是 0.5 。
纳什均衡是博弈论中一种解的概念,它是指满足下面性质的策略组合:任何一位玩家在此策略组合下单方面改变自己的策略(其他玩家策略不变)都不会提高自身的收益。
算法伪代码如下图所示:
这里之所以先循环多次优化判别器,再优化生成器。是因为我们想要先拥有一个有一定效果的判别器,它能够比较正确区分真实样本和生成样本,这样我们才能够根据判别器的反馈来对生成器进行优化。如下图所示。
第3章 GAN的缺陷
具体数学推导和证明参考博客:
(1)Mode collapse(模式坍塌):
GAN存在模式崩溃现象,当生成器G学习到一个参数设置,可以生成对判别器D而言特别逼真的样本,由于此样本很容易骗过判别器D,所以生成器G可能会一次又一次的生成相同的伪样本,最终始终生成同样的样本点,出现模式坍塌,无法继续学习。(G一直生成同一张图像)
比如说,造一种假币,警察无法分辨,那就一直以这种配方造价币,不会再创新了。
(2)Diminished gradient (梯度衰退):
梯度消失也是GAN存在的问题之一,如果判别器D始终能够正确判断真实样本为真和生成样本为假(D训练的太好),那么无论生成器G生成的样本多么好,判别器D都可以把它们分类为假样本,此时损失降为零,导致生成器G没有学习,就产生了梯度消失现象。
一般的情况下,上述有关JS散度的目标函数会带来梯度消失的问题。也就是说,如果Discriminator训练得太好,Generator就无法得到足够的梯度继续优化,而如果Discriminator训练得太弱,指示作用不显著,同样不能让Generator进行有效的学习。这样一来,Discriminator的训练火候就非常难把控,这就是GAN训练难的根源。
(3)Non-convergence(不收敛):
模型参数振荡、不稳定且永不收敛。
(4)对hyperparameter(超参数)超敏感:
下图为学习率(x轴)和FID(一种评价指标,值越高越好)关系,我们可以看到,不同参数差别较大,还没有规律。
(5)没有合理的评价指标:
早期的评价指标比较原始,就是直接看图像生成的好坏,到后来这个指标就不再有用了,为什么?一是因为很多方法足够优秀,生成的图肉眼很难看出差别,另外一个原因就是这是炼丹,很多人的方法你复现不了,不是每次都成功,于是我急需一个指标。
Inception Score (IS)
:Inception network是一个常见的预训练好的分类模型,我们假设如果我们生成的图像骗过了这个分类器,让他给出了相应的分类,例如我们生成的人脸被分类模型也被分成了人脸,那么我们就认为我们生成的成功,这个人脸的信度就是我们IS的分数。IS主要考虑两类信息:生成图片质量和图片多样性。
其中 P ( y ∣ x ) P(y|x) P(y∣x) 体现的是图像质量,由Inception network的分类结果决定的,值越小越好,说明这个样本越清晰。 P ( y ) P(y) P(y) 体现的图片多样性,如果生成的图片多样, y y y 的分布应是高熵的(例如均匀分布)值越大越好,说明生成的样本在各类上分布越平均。IS的主要问题是,没办法对小批次的样本进行准确的评判,除此以外,不能反应过拟合,对参数敏感,有时候数据很好,图片很差,所以,IS指标很少用了。
Fréchet Inception Distance (FID)
:也是通过Inception network 来测量真实图像和生成图像的特征差值,FID越小越好,为什么需要这个呢?因为IS不能包含所有的物体种类,可能真实图像通过Inception networks也不一定能得到较准确的分类。用差值更能体现GAN的性能。同时FID对小批次也有效。但是FID也有问题, 比如比较两个真实图片,理想的结果是为0,但是现实是非0的,这和理论相佐。
第4章 各种改进的GAN
推荐两篇GAN相关的综述:
[1]王正龙,张保稳.生成对抗网络研究综述[J].网络与信息安全学报,2021,7(04):68-85.
第5章 Pytorch实现GAN
参考博客:
实现效果如下:
import argparse
import os
import numpy as np
from tqdm.autonotebook import tqdm
import torchvision.transforms as transforms
from torchvision.utils import save_image
from torch.utils.data import DataLoader
from torchvision import datasets
from torch.autograd import Variable
import torch.nn as nn
import torch.nn.functional as F
import torch
os.makedirs("images", exist_ok=True)
"""
argparse是一个Python模块:命令行选项、参数和子命令解析器。
主要有三个步骤:
创建 ArgumentParser() 对象
调用 add_argument() 方法添加参数
使用 parse_args() 解析添加的参数
参考:https://blog.csdn.net/lizhiyuanbest/article/details/104975848
"""
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=200, help="number of epochs of training")
parser.add_argument("--batch_size", type=int, default=64, help="size of the batches")
parser.add_argument("--lr", type=float, default=0.0002, help="adam: learning rate")
parser.add_argument("--b1", type=float, default=0.5, help="adam: decay of first order momentum of gradient")
parser.add_argument("--b2", type=float, default=0.999, help="adam: decay of first order momentum of gradient")
parser.add_argument("--n_cpu", type=int, default=8, help="number of cpu threads to use during batch generation")
parser.add_argument("--latent_dim", type=int, default=100, help="dimensionality of the latent space")
parser.add_argument("--img_size", type=int, default=28, help="size of each image dimension")
parser.add_argument("--channels", type=int, default=1, help="number of image channels")
parser.add_argument("--sample_interval", type=int, default=400, help="interval betwen image samples")
opt = parser.parse_args() # opt保存了训练用的相关参数
print(opt) # 输出配置好的参数
# 输出图片尺寸
img_shape = (opt.channels, opt.img_size, opt.img_size)
#如果有显卡,就在显卡(GPU)上进行训练,否则就在CPU上进行训练
cuda = True if torch.cuda.is_available() else False
"""
1.生成器(Generator)
"""
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
# 类似于残差网络,将网络层封装起来,默认每次进行一次BN,然后再使用先线性激活函数LeakyReLU()
def block(in_feat, out_feat, normalize=True):
# 利用 layers 存储网络,形成网络块
layers = [nn.Linear(in_feat, out_feat)]
if normalize:
layers.append(nn.BatchNorm1d(out_feat, 0.8))
layers.append(nn.LeakyReLU(0.2, inplace=True))
return layers
self.model = nn.Sequential(
# 第一层不进行归一化
*block(opt.latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
# np.prod(img_shape) 是计算图片的维度,比如图片的尺寸时28*28,np.prod(img_shape) = 784
nn.Linear(1024, int(np.prod(img_shape))),
nn.Tanh()
)
def forward(self, z):
img = self.model(z)
img = img.view(img.size(0), *img_shape)
return img
"""
2.判别器(Discriminator)
"""
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
self.model = nn.Sequential(
# np.prod(img_shape) 是计算图片的维度,比如图片的尺寸时28*28,np.prod(img_shape) = 784
nn.Linear(int(np.prod(img_shape)), 512),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(512, 256),
nn.LeakyReLU(0.2, inplace=True),
nn.Linear(256, 1),
nn.Sigmoid(),
)
def forward(self, img):
img_flat = img.view(img.size(0), -1)
validity = self.model(img_flat)
return validity
# Loss function
# 使用 BCE Loss function 该损失函数主要用来创建衡量目标和输出之间的二进制交叉熵的标准。
# 而在平常我们会更多的使用 CE(CrossEntropyLoss)
adversarial_loss = torch.nn.BCELoss()
# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()
if cuda:
generator.cuda()
discriminator.cuda()
adversarial_loss.cuda()
# Configure data loader
# 加载数据集,平常见的很多
os.makedirs("../../data/mnist", exist_ok=True)
pipline = transforms.Compose([
# 将图片尺寸resize到(28,28)
transforms.Resize(opt.img_size),
# 将图片转化为Tensor格式
transforms.ToTensor(),
# 正则化(当模型出现过拟合的情况时,用来降低模型的复杂度)
transforms.Normalize([0.5], [0.5])
])
dataloader = torch.utils.data.DataLoader(
datasets.MNIST("../../data/mnist", train=True, download=True, transform=pipline),
batch_size=opt.batch_size,
shuffle=True,
)
# Optimizers,使用了Adam 优化器
# 参数 betas (Tuple[float, float], 可选) – 用于计算梯度以及梯度平方的运行平均值的系数(默认:0.9,0.999)
optimizer_G = torch.optim.Adam(generator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
optimizer_D = torch.optim.Adam(discriminator.parameters(), lr=opt.lr, betas=(opt.b1, opt.b2))
Tensor = torch.cuda.FloatTensor if cuda else torch.FloatTensor
# ----------
# Training
# ----------
for epoch in range(1, opt.n_epochs):
loop = tqdm(dataloader, colour='red', unit='img')
for i, (imgs, _) in enumerate(loop):
# Adversarial ground truths
# 因为 batch 是64,所以在这里 imgs.size(0) = 64
# 这两句话本质就是为64张真图片和64张假图片打上标签,真图片标签为1,假图片标签为0
valid = Variable(Tensor(imgs.size(0), 1).fill_(1.0), requires_grad=False)
fake = Variable(Tensor(imgs.size(0), 1).fill_(0.0), requires_grad=False)
# Configure input
real_imgs = Variable(imgs.type(Tensor))
# -----------------
# 训练 Generator
# -----------------
# 梯度归零
optimizer_G.zero_grad()
# 产生输入噪声,维度为latent_dim
z = Variable(Tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim))))
# 把噪声输入生成器,生成fake图像
gen_imgs = generator(z)
# 计算生成器的损失
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward() # 反向传播
optimizer_G.step() # 更新参数
# ---------------------
# 训练 Discriminator
# ---------------------
optimizer_D.zero_grad()
# 估计判别器的判别能力
real_loss = adversarial_loss(discriminator(real_imgs), valid) # 判别真实数据时的损失
fake_loss = adversarial_loss(discriminator(gen_imgs.detach()), fake) # 判别假数据时的损失
d_loss = (real_loss + fake_loss) / 2 # 做平均
d_loss.backward()
optimizer_D.step()
# print(
# "[Epoch %d/%d] [Batch %d/%d] [D loss: %f] [G loss: %f]"
# % (epoch, opt.n_epochs, i, len(dataloader), d_loss.item(), g_loss.item())
# )
# 进度条参数
loop.set_description(f"Epoch [{epoch}/{opt.n_epochs}]")
loop.set_postfix(D_loss=d_loss.item(), G_loss=g_loss.item())
batches_done = epoch * len(dataloader) + i
if batches_done % opt.sample_interval == 0:
save_image(gen_imgs.data[:25], "images/%d.png" % batches_done, nrow=5, normalize=True)
第6章 分析GAN代码中的deatch()函数
参考博客: