0%

生成对抗网络(GAN)

前言

  在生成对抗网络(Generative Adversarial Network,简称 GAN)发明之前,变分自编码器(VAE)被认为是理论完备,实现简单,使用神经网络训练起来很稳定,生成的图片逼近度也较高,但是人眼还是可以很轻易地分辨出真实图片与机器生成的图片。但在2014年GAN被提出之后,在之后的几年里面里迅速发展,生成的图片越来越逼真。

1 GAN

1.1 相关介绍

  GAN模型的核心思想就是博弈思想,是生成器(造假者)和判别器(鉴别者)之间的博弈,在提出GAN的原始论文中,作者举了货币制造的例子。即像一台验钞机和一台制造假币的机器之间的博弈,两者不断博弈,博弈的结果假币越来越像真币,直到验钞机无法识别一张货币是假币还是真币为止。

1.2 原理

1.2.1 网络架构

  生成对抗网络包含了两个子网络:生成网络(Generator,简称 G)和判别网络(Discriminator,简称 D),其中生成网络 G 负责学习样本的真实分布,判别网络 D 负责将生成网络采样的样本与真实样本区分开来。
  生成网络G(𝒛) :生成网络 G 和自编码器的 Decoder 功能类似,从先验分布$p_z$(∙)采样获得潜在空间点向量,经过网络生成图片样本$\bar{x}$~$𝑝_𝑔(x|z)$。在这里插入图片描述
  生成器的网络($𝑝_𝑔(x|z)$)可以由深度神经网络来参数化,如:卷积网络和转置卷积网络。下图中从均匀分布$𝑝𝒛$(∙)中采样出隐藏变量$z$,经过多层转置卷积层网络参数化的$𝑝_𝑔(x|z)$分布中采样出样本$x_f$,从输入输出层面来看,生成器 G 的功能是将隐向量𝒛通过神经网络转换为样本向量$x_f$,下标𝑓代表假样本(Fake samples)。在这里插入图片描述
  判别网络D(𝒙):判别网络和普通的二分类网络功能类似,网络的输入数据集由采样自真实数据分布$p_𝑟$(∙)的样本$x_𝑟$ ~ $𝑝_𝑟$(∙)和采样自生成网络的假样本$x_𝑓$ ~ $𝑝_𝑔(x|z)$组成。判别网络输出为$x$属于真实样本的概率𝑃($x$为真|$x$),我们把所有真实样本$x_r$的标签标注为真(1),所有生成网络产生的样本,所有生成网络产生的样本$x_f$标注为假(0),通过最小化判别网络 D 的预测值与标签之间的误差来优化判别网络参数。在这里插入图片描述

1.2.2 网络训练

  GAN 博弈学习的思想体现在在它的训练方式上,由于生成器 G 和判别器 D 的优化目标不一样,不能和之前的网络模型的训练一样,只采用一个损失函数。所以我们要分别对生成器和判别器进行训练。
  判别网络D(𝒙):它的目标是能够很好地分辨出真样本$x_r$与假样本$x_f$。则其损失函数既要考虑识别真图像能力,又要考虑识别假图像能力,而不能只考虑一方面,故判别器的损失函数为两者的和。因此 D 的分类问题是二分类问题,以图片生成来说,交叉熵损失函数定义为:在这里插入图片描述
因此判别网络 D 的优化目标是:
在这里插入图片描述
将最小化转成最大化的问题并写成期望的形式:
在这里插入图片描述
  具体代码如下:D表示判别器、G为生成器、real_labels、fake_labels分别表示真图像标签、假图像标签。images是真图像,z是从潜在空间随机采样的向量,通过生成器得到假图像。

1
2
3
4
5
6
7
8
9
10
11
12
# 定义判断器对真图像的损失函数 
outputs = D(images)
d_loss_real = criterion(outputs, real_labels)
real_score = outputs
# 定义判别器对假图像(即由潜在空间点生成的图像)的损失函数
z = torch.randn(batch_size, latent_size).to(device)
fake_images = G(z)
outputs = D(fake_images)
d_loss_fake = criterion(outputs, fake_labels)
fake_score = outputs
# 得到判别器总的损失函数
d_loss = d_loss_real + d_loss_fake

  生成网络G(𝒛) :我们希望$x_f$ = 𝐺(𝒛)能够很好地骗过判别网络 D,假样本$x_f$在判别网络的输出越接近真实的标签越好。也就是说,在训练生成网络时,希望判别网络的输出𝐷(𝐺(𝒛))越逼近 1 越好,最小化𝐷(𝐺(𝒛))与 1 之间的交叉熵损失函数:在这里插入图片描述
将最小化转成最大化的问题并写成期望的形式:在这里插入图片描述
等价成:
在这里插入图片描述
其中𝜙为生成网络 G 的参数集,可以利用梯度下降算法来优化参数𝜙。具体代码如下:

1
2
3
4
z = torch.randn(batch_size, latent_size).to(device) 
fake_images = G(z)
outputs = D(fake_images)
g_loss = criterion(outputs, real_labels)

  通过对生成器和判别器的损失函数的求解,GAN的架构如下:
在这里插入图片描述
算法流程为:
在这里插入图片描述

1.3 用GAN生成图像

  本次实验为了方便,我使用的是 MNIST 手写数字数据集,下面进行每部分的代码实现。

1.3.1 判别器

  定义判别器网络结构,这里使用LeakyReLU为激活函数,输出一个节点 并经过Sigmoid后输出,用于真假二分类。

1
2
3
4
5
6
7
8
9
class Discriminator(nn.Module) :
def __init__(self) :
super(Discriminator, self).__init__()
self.D = nn.Sequential(nn.Linear(IMAGE_SIZE, HIDDEN_SIZE),
nn.LeakyReLU(0.2),
nn.Linear(HIDDEN_SIZE, HIDDEN_SIZE),
nn.LeakyReLU(0.2),
nn.Linear(HIDDEN_SIZE, 1),
nn.Sigmoid())

1.3.2 生成器

  生成器与AVE的生成器类似,不同的地方是输出为nn.tanh,使用nn.tanh 将使数据分布在[-1,1]之间。其输入是潜在空间的向量z,输出维度与真图像相同。

1
2
3
4
5
6
7
8
9
10
11
class Generator(nn.Module) :
def __init__(self):
super(Generator, self).__init__()
self.G = nn.Sequential(nn.Linear(Z_SIZE, HIDDEN_SIZE),
nn.ReLU(),
nn.Linear(HIDDEN_SIZE, HIDDEN_SIZE),
nn.ReLU(),
nn.Linear(HIDDEN_SIZE, IMAGE_SIZE),
nn.Tanh())
def forward(self, z) :
return self.G(z)

1.3.3 训练模型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
for epoch in range(MAX_EPOCH) :
for i, (images, labels) in enumerate(Dataloader) :

images = images.reshape(BATCH_SIZE, -1).cuda()
#真样本与生成样本的标签设置
real_labels = torch.ones(BATCH_SIZE, 1).cuda()
fake_labels = torch.zeros(BATCH_SIZE, 1).cuda()
#训练判别器
d_optimizer.zero_grad()
g_optimizer.zero_grad()
out = D(images)
real_score = out
d_loss_real = criterion(out, real_labels)

z = torch.randn(BATCH_SIZE, Z_SIZE).cuda()
fake_images = G(z)
out = D(fake_images)
fake_score = out
d_loss_fake = criterion(out, fake_labels)

d_loss = d_loss_fake + d_loss_real

d_loss.backward()
d_optimizer.step()

#训练生成器
d_optimizer.zero_grad()
g_optimizer.zero_grad()
z = torch.randn(BATCH_SIZE, Z_SIZE).cuda()
fake_images = G(z)
out = D(fake_images)
g_loss = criterion(out, real_labels)

g_loss.backward()
g_optimizer.step()

if (i + 1) % 200 == 0:
print('Epoch [{}/{}], Step [{}/{}], d_loss: {:.4f}, g_loss: {:.4f}, D(x): {:.2f}, D(G(z)): {:.2f}'
.format(epoch, MAX_EPOCH, i + 1, len(Dataloader), d_loss.item(), g_loss.item(),
real_score.mean().item(), fake_score.mean().item()))

# 保存真图片
if (epoch + 1) == 1:
images = images.reshape(images.size(0), 1, 28, 28)
save_image(denorm(images), os.path.join(sample_dir, 'real_images.png'))

# 保存假图片
fake_images = fake_images.reshape(fake_images.size(0), 1, 28, 28)
save_image(denorm(fake_images), os.path.join(sample_dir, 'fake_images-{}.png'.format(epoch + 1)))

# 保存模型
torch.save(G.state_dict(), 'G.ckpt')
torch.save(D.state_dict(), 'D.ckpt')

  效果,分别展示epoch为1、100、200时生成的图片,其中当epoch为200时噪声就已经很少了,但是对数字的分布结构并不能很好的描述出来。
在这里插入图片描述

2 GAN变种

2.1 CGAN

  AVE和GAN都能基于潜在空间的随机向量z生成新图片,GAN生成的图 像比AVE的更清晰,质量更好些。不过它们生成的都是随机的,无法预先控制你要生成的哪类或哪个数。我们希望 生成某个数字,生成某个主题或类别的图像,实现按需生成的目的,这样的应用应该非常广泛。CGAN正是针对这类问题而提出的。

2.1.1 原理

  在GAN这种完全无监督的方式加上一个标签或一点监督信息,使整个网络就可看成半监督模型。其基本架构与GAN类似,只要添加一个条件y即可,y就是加入的监督信息,比如说MNIST数据集可以提供某个数字的标签 信息,人脸生成可以提供性别、是否微笑、年龄等信息,带某个主题的图像 等标签信息。
在这里插入图片描述
  对生成器输入一个从潜在空间随机采样的一个向量z及一个条件y,生成 一个符合该条件的图像G(z/y)。对判别器来说,输入一张图像x和条件y,输 出该图像在该条件下的概率D(x/y)。

2.1.2 PyTorch实现

  CGAN实现采用的数据集依然是 MNIST 手写数字数据集,其实现过程与原始的GAN的相差不大,主要差异时是标注信息的添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
import torch
import torch.nn as nn
import torch.optim as optim
import os
import matplotlib.pyplot as plt
import torchvision.transforms as transforms
from torchvision import datasets
from torch.utils.data import DataLoader
from torchvision.utils import save_image
from torch.utils.tensorboard import SummaryWriter
from torchvision.utils import make_grid

#设置超参数
MAX_EPOCH = 50
LR_RATE = 0.0001
BATCH_SIZE = 100

writer = SummaryWriter(log_dir = 'logs')
sample_dir = 'samples_CGAN'
os.makedirs(sample_dir, exist_ok = True)

Dataset = datasets.MNIST(root = 'data',
download = False,
train = True,
transform = transforms.Compose([transforms.ToTensor(),
transforms.Normalize([0.5], [0.5])]))

Dataloader = DataLoader(Dataset, batch_size = BATCH_SIZE, shuffle = True, drop_last = True)

#生成器
class Generator(nn.Module) :
def __init__(self):
super(Generator, self).__init__()
self.embedding = nn.Embedding(10, 10)

self.G = nn.Sequential(nn.Linear(110, 256),
nn.LeakyReLU(0.2),
nn.Linear(256, 512),
nn.LeakyReLU(0.2),
nn.Linear(512, 1024),
nn.LeakyReLU(0.2),
nn.Linear(1024, 784),
nn.Tanh())
def forward(self, z, labels) :
y = self.embedding(labels)
x = torch.cat([z, y], dim = 1)
out = self.G(x)
return out.view(z.size(0), 28, 28)

#判别器
class Discriminator(nn.Module) :
def __init__(self) :
super(Discriminator, self).__init__()
self.embedding = nn.Embedding(10, 10)
self.D = nn.Sequential(nn.Linear(794, 1024),
nn.LeakyReLU(0.2),
nn.Dropout(0.4),
nn.Linear(1024, 512),
nn.LeakyReLU(0.2),
nn.Dropout(0.4),
nn.Linear(512, 256),
nn.LeakyReLU(0.2),
nn.Dropout(0.4),
nn.Linear(256, 1),
nn.Sigmoid())

def forward(self, x, labels):
x = x.view(x.size(0), -1)
y = self.embedding(labels)
x = torch.cat([x, y], dim = 1)
out = self.D(x)
return out

#Clamp函数x限制在区间[min, max]内
def denorm(x):
out = (x + 1) / 2
return out.clamp(0, 1)

D = Discriminator().cuda()
G = Generator().cuda()
d_optimizer = optim.Adam(D.parameters(), lr = LR_RATE)
g_optimizer = optim.Adam(G.parameters(), lr = LR_RATE)
criterion = nn.BCELoss()

#训练
for epoch in range(MAX_EPOCH) :
for i, (images, labels) in enumerate(Dataloader) :
step = epoch * len(Dataloader) + i + 1
images, labels = images.reshape(BATCH_SIZE, -1).cuda(), labels.cuda()
real_labels = torch.ones(BATCH_SIZE, 1).cuda()

d_optimizer.zero_grad()
g_optimizer.zero_grad()
out = D(images, labels)
real_score = out
d_loss_real = criterion(out, real_labels)

z = torch.randn(BATCH_SIZE, 100).cuda()
fake_labels = torch.randint(0, 10, (BATCH_SIZE, )).cuda()

fake_images = G(z, fake_labels)
out = D(fake_images, fake_labels)
fake_score = out
d_loss_fake = criterion(out, torch.zeros(BATCH_SIZE, 1).cuda())

d_loss = d_loss_fake + d_loss_real

d_loss.backward()
d_optimizer.step()

d_optimizer.zero_grad()
g_optimizer.zero_grad()
z = torch.randn(BATCH_SIZE, 100).cuda()
fake_images = G(z, fake_labels)
out = D(fake_images, fake_labels)
g_loss = criterion(out, real_labels)

g_loss.backward()
g_optimizer.step()

if (i + 1) % 200 == 0:
print('Epoch [{}/{}], Step [{}/{}], d_loss: {:.4f}, g_loss: {:.4f}, D(x): {:.2f}, D(G(z)): {:.2f}'
.format(epoch, MAX_EPOCH, i + 1, len(Dataloader), d_loss.item(), g_loss.item(),
real_score.mean().item(), fake_score.mean().item()))

# 保存真图片
if (epoch + 1) == 1:
images = images.reshape(images.size(0), 1, 28, 28)
save_image(denorm(images), os.path.join(sample_dir, 'real_images.png'))

# 保存假图片
fake_images = fake_images.reshape(fake_images.size(0), 1, 28, 28)
save_image(denorm(fake_images), os.path.join(sample_dir, 'fake_images-{}.png'.format(epoch + 1)))

# 可视化损失值
writer.add_scalars('scalars', {'d_loss': d_loss.item(), 'g_loss': g_loss.item()}, step)
# 保存模型
torch.save(G.state_dict(), 'G.ckpt')
torch.save(D.state_dict(), 'D.ckpt')

#利用网格(10×10)的形式显示指定条件下生成的图像。
z = torch.randn(100, 100).cuda()
labels = torch.LongTensor([i for i in range(10) for _ in range(10)]).cuda()
images = G(z, labels).unsqueeze(1)
grid = make_grid(images, nrow = 10, normalize = True)
fig, ax = plt.subplots(figsize = (10, 10))
ax.imshow(grid.permute(1, 2, 0).detach().cpu().numpy(), cmap = 'binary')
ax.axis('off')
plt.show()

#可视化指定单个数字条件下生成的数字
def generate_digit(generator, digit) :
z = torch.randn(1, 100).cuda()
label = torch.LongTensor([digit]).cuda()
img = generator(z, label).detach().cpu()
img = 0.5 * img + 0.5
return transforms.ToPILImage()(img)
generate_digit(G, 8)

利用网格(10×10)的形式显示指定条件下生成的图像:
在这里插入图片描述
可视化指定单个数字条件下生成的数字:
在这里插入图片描述
可视化生成器和判别器损失值如下 :
在这里插入图片描述
由上图可知,CGAN的训练过程不像一般神经网络的过程,它是判别 器和生成器互相竞争的过程,最后两者达成一个平衡。

2.2 DCGAN

  在前面中无论是原始的GAN还是CGAN我们建立的网络都是基于全连接网络构建的,这样的网络由于图片的维度较高,网络参数量巨大,不能很好地学习到图片地特征,导致训练效果不佳。DCGAN提出了使用转置卷积层实现的生成网络,普通卷积层来实现的判别网络,大大地降低了网络参数量,同时图片的生成效果也大幅提升,展现了 GAN 模型在图片生成效果上超越 VAE 模型的潜质。注:虽然使用卷积网络会大大降低参数量,但是所需要的样本数要更多一些。
加粗样式

2.3 CycleGAN

CycleGAN 是一种无监督方式,主要用于图片风格相互转换的。CycleGAN 基本的思想是,如果由图片 A 转换到图片 B,再从图片 B 转换到A′,那么A′应该和 A 是同一张图片。因此除了设立标准的 GAN 损失项外,CycleGAN 还增设了循环一致性损失(Cycle Consistency Loss),来保证A′尽可能与 A 逼近。在这里插入图片描述

2.4 WGAN

  GAN 的训练问题一直被诟病,很容易出现训练不收敛和模式崩塌的现象。WGAN 从理论层面分析了原始的 GAN 使用 JS 散度存在的缺陷,并提出了可以使用 Wasserstein 距 离来解决这个问题。在 WGAN-GP 中,作者提出了通过添加梯度惩罚项,从工程层面很好的实现了 WGAN 算法,并且实验性证实了 WGAN 训练稳定的优点。

3 训练GAN的技巧

  • 批量加载和批规范化,有利于提升训练过程中博弈的稳定性。
  • 使用tanh激活函数作为生成器最后一层,将图像数据规范在-1和1之间,一般不用sigmoid。
  • 选用Leaky ReLU作为生成器和判别器的激活函数,有利于改善梯度的稀疏性,稀疏的梯度会妨碍GAN的训练。
  • 使用卷积层时,考虑卷积核的大小能被步幅整除,否则,可能导致生成的图像中存在棋盘状伪影。

全部代码可以参考此处
参考

  • 《Python深度学习基于PyTorch》
  • 《TensorFlow深度学习》