GAN(Generative Adversarial Nets)
引言
GAN由Ian J. Goodfellow等人提出,是Ian J. Goodfellow的代表作之一,他还出版了大家耳熟能详的花书(Deep Learning深度学习),GAN主要的思想是同时训练两个模型,生成模型G用于获取数据分布,判别模型D用于估计样本来自训练数据而不是G的概率。G的训练过程是最大化D犯错误的概率。这个过程对应于一个极小极大两人博弈。在任意函数G和D的空间中,存在唯一解,其中G恢复训练数据分布,并且D处处等于1/2。
用一个警察与小偷的故事来阐述:假设一个城市里有许多小偷,在这些小偷中,部分是技艺高超的偷窃高手,另一部分则是毫无技术的新手。警察开始进行对小偷的抓捕,其中一批“学艺不精”的小偷就被捉住了。这些小偷被抓住或许是因为识别他们毫无难度,警察不需要特殊本领,但是剩下的“偷窃高手”警察就很难抓捕。于是警察们开始继续训练自己的破案技术,开始抓住那些技艺高超的小偷。随着这些他们的落网,警察们也练就了特别的本事,他们能很快能从一群人中识别终逮捕嫌犯;随着警察们的水平大大提高,为了避免被捕,小偷们努力表现得不那么“可疑”,而魔高一尺、道高一丈,警察也在不断提高自己的水平,争取将小偷和无辜的普通群众区分开。随着警察和小偷之间的这种“交流”与“切磋”,小偷们都变得非常谨慎,他们有着极高的偷窃技巧,表现得跟普通群众一模一样,而警察们都练就了“火眼金睛”,一旦发现可疑人员,就能马上发现并及时控制——最终,我们同时得到了最强的小偷和最强的警察。其中,小偷就可以视作生成模型G,警察可以视作判别模型D,通过G和D的对抗,能够获得效果较好的生成模型(判别模型也是如此)参考链接
主要架构
根据引言,一个GAN主要包含两个基础模型:生成器(G)与判别器(D)。其中,生成器用于生成新数据,其生成数据的基础往往是一组噪音或者随机数,而判别器用于判断生成的数据和真实数据哪个才是真的。生成器执行无监督任务;而判别器执行有监督任务,用于二分类,其label是“假与真”(0与1)。
生成器的目标是生成尽量真实的数据(这也是我们对生成对抗网络的要求),最好能够以假乱真、让判别器判断不出来,因此生成器的学习目标是让判别器上的判断准确性越来越低;相反,判别器的目标是尽量判别出真伪,因此判别器的学习目标是让自己的判断准确性越来越高。
当生成器生成的数据越来越真时,判别器为维持住自己的准确性,就必须向判别能力越来越强的方向迭代。当判别器越来越强大时,生成器为了降低判别器的判断准确性,就必须生成越来越真的数据。在这个奇妙的关系中,判别器与生成器同时训练、相互内卷,对损失函数的影响此消彼长。参考链接
理论支撑
m
i
n
G
m
a
x
D
V
(
D
,
G
)
=
E
x
∼
p
d
a
t
a
(
x
)
[
l
o
g
D
(
x
)
]
+
E
z
∼
p
z
(
z
)
[
l
o
g
(
1
−
D
(
G
(
z
)
)
)
]
\underset{G}{min}\underset{D}{max}V(D,G) = {\mathbb E_{x \sim{p}_{data}(x)} [logD(x)]} + {\mathbb E _{z \sim{p}_{z}(z)}[{log(1-D({G(z)}))}]}
GminDmaxV(D,G)=Ex∼pdata(x)[logD(x)]+Ez∼pz(z)[log(1−D(G(z)))]
V
V
V是一个值函数(损失函数),
x
x
x表示真实数据,
p
d
a
t
a
p_{data}
pdata表示数据的真实分布,
z
z
z是与真实数据相同分布的随机数据,
G
(
z
)
G(z)
G(z)是生成器中基于
z
z
z生成的数据,
D
(
x
)
D(x)
D(x)是判别器在真实数据
x
x
x上判断的结果,
D
(
G
(
x
)
)
D(G(x))
D(G(x))表示判别器在生成器生成的数据
G
(
z
)
G(z)
G(z)上判断出的结果。
那么需要做的就是:
1. 对于判别器D来说,尽可能找到生成器生成的数据
2. 对于生成器G来说,尽可能让生成的数据接近真实数据,使得判别器D无法判别出来
上面表达式需要做的是,首先固定G,在D的层面使得值最大(即让判别器能够精确区分真实数据和生成数据),然后固定D,在G的层面使得值最小(即在判别器能够精确区分数据的情况下,让生成器能够生成更接近真实的数据,使得判别器无法区分),从而实现了D和G的对抗,如此可以找到最好的生成器(生成模型)。
图(a)中展示了生成器G、判别器D以及真实数据初始状态,此时真实数据与生成数据分布明显不同,判别器此时也只是初始状态;图(b)展示了判别器经过训练后能够进行区分真实数据和生成数据;图©展示了生成器经过训练后能够更加接近真实分布;图(d)展示了经过多次循环之后,生成器和判别器的状态,此时生成数据已经无限接近真实数据分布,同时判别器难以区分出真实数据和生成数据,导致判别答案始终为1/2。
算法
算法存在的一个问题是需要选择一个较好的k,在算法中要保证:不能一次性让判别器就能够准确的识别出所有生成数据,这会导致生成器没有办法继续提升,生成更加接近真实分布的数据,同时也不能让生成模型一下子生成非常接近真实分布的数据,这会导致判别器难以进行识别能力的提升。
公式分析
对于判别器D
判别器的作用是尽可能找出生成器生成的数据与真实数据分布之间的差异,这是一个二分类的问题,将G固定后,公式就变为:
m
a
x
D
V
(
D
,
G
)
=
E
x
∼
p
d
a
t
a
(
x
)
[
l
o
g
D
(
x
)
]
+
E
z
∼
p
z
(
z
)
[
l
o
g
(
1
−
D
(
G
(
z
)
)
)
]
\underset{D}{max}V(D,G) = {\mathbb E_{x \sim{p}_{data}(x)} [logD(x)]} + {\mathbb E _{z \sim{p}_{z}(z)}[{log(1-D({G(z)}))}]}
DmaxV(D,G)=Ex∼pdata(x)[logD(x)]+Ez∼pz(z)[log(1−D(G(z)))]
该公式等价于交叉熵,只不过交叉熵是取负的对数。这个函数的输入一部分是真实数据,分布为
p
d
a
t
a
{p_{data}}
pdata,一部分是生成器的数据(噪声数据),生成器接收的数据
z
z
z服从分布
p
(
z
)
p(z)
p(z),输入
z
z
z经过生成器的计算生产的数据分布设为
p
G
(
x
)
{p_{G}}(x)
pG(x),这个函数要取得最大值,必然是对于真实数据
D
(
x
)
=
1
D(x)=1
D(x)=1,对于生成数据
D
(
x
)
=
0
D(x)=0
D(x)=0,这一步用于优化D,因此可以简写为
D
G
∗
=
m
a
x
D
V
(
G
,
D
)
D_G^* = \underset D {max}V(G,D)
DG∗=DmaxV(G,D)
此时,这是D的一元函数,进行求导,得到
取导数为0,算最优点得到
对于生成器G
当且仅当
P
G
(
x
)
=
P
d
a
t
a
(
x
)
{P_G}(x) = {P_{data}}(x)
PG(x)=Pdata(x)时,有
D
G
∗
=
P
d
a
t
a
(
x
)
P
G
(
x
)
+
P
d
a
t
a
(
x
)
=
1
2
D_G^* = \frac{{{P_{data}}(x)}}{{{P_G}(x) + {P_{data}}(x)}} = \frac{1}{2}
DG∗=PG(x)+Pdata(x)Pdata(x)=21
此时生成器无法判别数据是真实数据或者生成数据。
我们假设
P
G
(
x
)
=
P
d
a
t
a
(
x
)
{P_G}(x) = {P_{data}}(x)
PG(x)=Pdata(x),可以反向推出
V
(
G
,
D
G
∗
)
=
∫
x
P
d
a
t
a
(
x
)
l
o
g
1
2
+
P
G
(
x
)
l
o
g
(
1
−
1
2
)
d
x
{V(G,D_G^*) = \int_x {{P_{data}}(x)log\frac{1}{2}}+ {P_G}(x)log(1 - \frac{1}{2})dx}
V(G,DG∗)=∫xPdata(x)log21+PG(x)log(1−21)dx
⇔
V
(
G
,
D
G
∗
)
=
−
log
2
∫
x
P
G
(
x
)
d
x
−
log
2
∫
x
P
d
a
t
a
(
x
)
d
x
=
−
2
log
2
=
−
log
4
\Leftrightarrow {V(G,D_G^*) = - \log 2\int\limits_x {{P_G}(x)} dx - \log 2\int\limits_x {{P_{data}}(x)} dx = - 2\log 2 = - \log 4}
⇔V(G,DG∗)=−log2x∫PG(x)dx−log2x∫Pdata(x)dx=−2log2=−log4
该值是全局最小值的候选,因为它只有在
P
G
(
x
)
=
P
d
a
t
a
(
x
)
{P_G}(x) = {P_{data}}(x)
PG(x)=Pdata(x)
的时候才出现。
对于任意一个G,将
D
∗
{D^*}
D∗带入到
V
(
G
,
D
)
V(G,D)
V(G,D)中:
结合KL散度得到:
=
−
2
l
o
g
2
+
K
L
(
P
d
a
t
a
(
x
)
∣
∣
P
d
a
t
a
(
x
)
+
P
G
(
x
)
2
)
+
K
L
(
P
G
(
x
)
∣
∣
P
d
a
t
a
(
x
)
+
P
G
(
x
)
2
)
{ = - 2log2 + KL({P_{data}}(x)||\frac{{{P_{data}}(x) + {P_G}(x)}}{2}) + KL({P_G}(x)||\frac{{{P_{data}}(x) + {P_G}(x)}}{2})}
=−2log2+KL(Pdata(x)∣∣2Pdata(x)+PG(x))+KL(PG(x)∣∣2Pdata(x)+PG(x))
最后根据JS散度得到:
V
(
G
,
D
)
=
−
log
4
+
2
∗
J
S
D
(
P
d
a
t
a
(
x
)
∣
P
G
(
x
)
)
V(G,D) = - \log 4 + 2*JSD({P_{data}}(x)|{P_G}(x))
V(G,D)=−log4+2∗JSD(Pdata(x)∣PG(x))
根据他的属性:当
P
G
(
x
)
=
P
d
a
t
a
(
x
)
{P_G}(x) = {P_{data}}(x)
PG(x)=Pdata(x)
时,
为0。综上所述,生成分布当前仅当等于真实数据分布式时,我们可以取得最优生成器。前后逻辑自洽。
注:
对于判别器D的优化:这是一个二分类,满足 y l o g q + ( 1 − y ) l o g ( 1 − q ) ylogq+(1-y)log(1-q) ylogq+(1−y)log(1−q),对于x,标签只会为1,因此只有log(D(x))这一项;对于g(z),其标签只会为0,因此只有log(1-D(G(z)))这一项,因此可以有损失函数:
l o s s = c r o s s E n t r o p y L o s s ( D ( x ) , 1 ) + c r o s s E n t r o p y L o s s ( D ( x ) , 0 ) loss = crossEntropyLoss(D(x),1)+crossEntropyLoss(D(x),0) loss=crossEntropyLoss(D(x),1)+crossEntropyLoss(D(x),0)
对于生成器G的优化:因为D(x)这一项,并不包含生成器的优化参数,因此在求梯度的时候D(x)这一项为0,因此只有log(1-D(G(z)))这一项,损失函数:
l o s s = c r o s s E n t r o p y L o s s ( D ( G ( z ) ) , 1 ) loss = crossEntropyLoss(D(G(z)),1) loss=crossEntropyLoss(D(G(z)),1)
代码
import argparse
import os
import numpy as np
import math
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)
parser = argparse.ArgumentParser()
parser.add_argument("--n_epochs", type=int, default=50, 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")
#使用jupyter时需要传入list,
opt = parser.parse_args(args=[])
#opt = parser.parse_args()
#print(opt)
#图像形状为:1*28*28,图像大小为784
img_shape = (opt.channels, opt.img_size, opt.img_size)
#这里提前做了一下cuda的判断
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
#生成器G,用于进行数据的生成
class Generator(nn.Module):
def __init__(self):
super(Generator, self).__init__()
#定义模型的中间块
def block(in_feat, out_feat, normalize=True):
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
#多个模型块进行组合(MLP),输入维度的映射过程为:input_dim->128->256->512->1024->1*28*28
self.model = nn.Sequential(
*block(opt.latent_dim, 128, normalize=False),
*block(128, 256),
*block(256, 512),
*block(512, 1024),
nn.Linear(1024, int(np.prod(img_shape))),
nn.Tanh() #Tanh取值范围为-1,1,即将输出映射到-1,1
)
def forward(self, z):
img = self.model(z) #对噪声数据处理进行数据生成
img = img.view(img.size(0), *img_shape) #生成数据为(batch_size,1,28,28)
return img
#判别器D,用于区分真实数据和生成数据
class Discriminator(nn.Module):
def __init__(self):
super(Discriminator, self).__init__()
#维度映射过程:1*28*28->512->256->1,使用sigmoid进行二分类激活,映射为0,1之间的数
self.model = nn.Sequential(
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
adversarial_loss = torch.nn.BCELoss() #损失函数使用BCE(二分类交叉熵)
# Initialize generator and discriminator
generator = Generator()
discriminator = Discriminator()
if cuda:
generator.to(device)
discriminator.to(device)
adversarial_loss.to(device)
# Configure data loader
os.makedirs("./data/mnist", exist_ok=True)
#数据集加载
dataloader = torch.utils.data.DataLoader(
datasets.MNIST(
"./data/mnist",
train=True,
download=True,
transform=transforms.Compose(
[transforms.Resize(opt.img_size), transforms.ToTensor(), transforms.Normalize([0.5], [0.5])]
),
),
batch_size=opt.batch_size,
shuffle=True,
)
# Optimizers
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))
# ----------
# Training
# ----------
for epoch in range(opt.n_epochs):
for i, (imgs, _) in enumerate(dataloader):
# Adversarial ground truths
valid = torch.tensor([[1.0]] * imgs.size(0), requires_grad=False).to(device) #定义真实数据标号为1
fake = torch.tensor([[0.0]] * imgs.size(0), requires_grad=False).to(device) #定义虚假数据标号为0
# Configure input
real_imgs = torch.tensor(imgs.type(torch.Tensor)).to(device)
# -----------------
# Train Generator
# -----------------
optimizer_G.zero_grad()
# Sample noise as generator input
z = torch.tensor(np.random.normal(0, 1, (imgs.shape[0], opt.latent_dim)), dtype=torch.float32).to(device) #随机生成噪声
# Generate a batch of images
gen_imgs = generator(z) #生成数据
# Loss measures generator's ability to fool the discriminator
g_loss = adversarial_loss(discriminator(gen_imgs), valid)
g_loss.backward()
optimizer_G.step()
# ---------------------
# Train Discriminator
# ---------------------
optimizer_D.zero_grad()
# Measure discriminator's ability to classify real from generated samples
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())
)
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)