[深度学习]半监督学习、无监督学习之DCGAN深度卷积生成对抗网络(附代码)
这篇论文介绍了生成对抗网络(GAN)的原理及其改进版本——深度卷积生成对抗网络(DCGAN)。GAN由生成网络(G)和判别网络(D)组成,G的目标是生成逼真的图像,D的目标是区分真实图像和生成图像。论文指出,传统的GAN(如2014年的DGAN)使用多层感知机,而DCGAN(2016年提出)引入了深度卷积神经网络,并对激活函数、损失函数和训练策略进行了优化。DCGAN通过取消池化层、使用批归一化和LeakyReLU激活函数等改进,显著提升了生成图像的质量和收敛速度。论文还详细描述了DCGAN在MNIST数据集上的实现,包括生成器和判别器的网络结构、训练参数(如学习率、批处理大小)以及生成图像的可视化效果。代码部分展示了如何定义生成器和判别器模型,并使用PyTorch进行训练和评估。
论文全称:《Generative Adversarial Nets》
论文地址:https://arxiv.org/pdf/1406.2661.pdf
The paper is titled《UNSUPERVUED REPRESENTATION LEARNING WITH DEEP CONVOLUTIONAL GENERATIVE ADVERSARIAL NETWORKS》.
论文地址:https://arxiv.org/pdf/1511.06434.pdf
论文代码:
该MNIST数据集基于PyTorch框架运行,具体代码可参考https://colab.research.google.com/github/smartgeometry-ucl/dl4g/blob/master/gan.ipynb。
PyTorch Celeb-A Faces Dataset:https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html
TensorFlow支持CelebA数据集:https://github.com/carpedm20/DCGAN-tensorflow。
目录
GAN的原理
DCGANs原理
DCGANs in pytorch with MNIST
首先,这里有两篇重要论文。第一篇是2014年Lan Goodfellow最先提出的GAN框架,使用的是多层感知机(MLP)。第二篇是2016年Facebook AI Research开发出的DCGANs,基于前一篇的框架。
GAN的原理
GAN主要包含两个部分,一个是生成器G,一个是判别器D。

他们的作用分别是:
G:利用随机的vector z生成一张图片,即为G(z)。
D:识别图片是否为真实图片。当输出为1时,表示图片100%为真实图片;输出为0时,则表示图片不可能是真实图片。
在训练过程中,生成器G的主要任务是尽可能制造与真实数据集高度相似的图像,以欺骗判别器D。与此同时,判别器D的使命则是通过精确区分生成器G产出的图像与真实数据中的图片,来维护数据源的鉴别能力。这样,生成器G与判别器D就形成了一个动态的对抗关系,彼此间的博弈过程推动着整个模型的优化与发展。
在最理想的状态下,G能够产出具有“以假乱真”效果的图片G(z)。对于D来说,它无法分辨G生成的图片是否为真实图像,因此D(G(z)) = 0.5。
通过我们的努力,我们成功实现了目标:我们成功开发了一个生成式模型G,该模型能够生成图片。
接下来我们看看D和G究竟需要怎么博弈去逼近我们想要的结果呢?

公式分析:
由两个部分组成整个式子。x代表真实图片,z为输入至G网络的向量,而G(z)代表G网络生成的图像。
D(x)表示D网络评估输入图片是否为真实图片的概率(当输入图片x为真实图片时,这个概率值越接近1,表示D网络对输入图片的判断越准确)。而D(G(z))是D网络评估通过生成器G生成的图片是否为真实图片的概率。
生成器G的目标是希望其生成的图片能够尽可能接近真实数据的特征。如前所述,D(G(z))代表的是D网络对由G生成的图片判别为真实数据的概率。生成器G的优化目标是通过调整生成器参数θg,使得D(G(z))的值最大化。具体而言,G的优化目标是通过调整生成器参数θg,使得D(G(z))的值最大化。这样,V(D, G)的值会减小。由此可知,生成器G的优化目标是通过最小化V(D, G)来实现对真实数据分布的逼近。
D的目标是,当D的能力越强时,D(x)和D(G(x))的值应该分别增大和减小。此时,V(D,G)的值会增加。因此,式子对于D来说,是最大化(max)的形式。
在训练过程中,也反映了min和max之间的顺序关系,具体来说,先进行D的训练,接着进行G的训练。特别注意蓝色方框中的ascending和descending的差异,具体而言,在训练D时,我们的目标是使V达到最大值,因此需要采用梯度上升方法;而在训练G时,我们的目标是使V达到最小值,因此需要采用梯度下降方法。

DCGANs原理
简单点说DCGANs就是CNN+GAN。DCGANs把原论文中的D和G用CNN代替了。
DCGAN对卷积神经网络的结构进行了优化调整,旨在通过提升样本质量及加快收敛速度来优化生成样本的质量和训练效率,具体体现在生成器和判别器的结构优化上。
移除所有池化层。在G网络中,采用转置卷积(transposed convolutional layer)进行上采样;而在D网络中,采用带stride的卷积层替代池化层。
在D和G中均使用batch normalization
去掉FC层,使网络变为全卷积网络
在G网络中,ReLU被用作激活函数,而该网络的最终层则采用tanh作为激活函数。
D网络中使用LeakyReLU作为激活函数
DCGAN中的G网络示意:

DCGANs in pytorch with MNIST
首先导入必要的包和设置超参数:
import os
import torch
import torch.nn as nn
import torch.nn.functional as F
latent_dims = 10
num_epochs = 20
batch_size = 128
learning_rate = 2e-4
use_gpu = True
加载MNIST:
import torchvision.transforms as transforms
from torch.utils.data import DataLoader
from torchvision.datasets import MNIST
img_transform = transforms.Compose([
transforms.Resize(64),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5))
])
train_dataset = MNIST(root='./data/MNIST', download=True, train=True, transform=img_transform)
train_dataloader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
test_dataset = MNIST(root='./data/MNIST', download=True, train=False, transform=img_transform)
test_dataloader = DataLoader(test_dataset, batch_size=batch_size, shuffle=True)
定义生成网络,注意:
通过转置卷积层(transposed convolutional layer)进行上采样,以实现特征图的空间还原。在生成器中,对各层输出进行批归一化(batchnorm)处理,有助于加快训练速度并提升生成质量。在生成器网络中,采用ReLU作为激活函数;而对于生成器的最后一层,采用tanh激活函数。
class Generator(nn.Module):
def __init__(self, d=128):
super(Generator, self).__init__()
self.deconv1 = nn.ConvTranspose2d(100, d*8, 4, 1, 0)
self.deconv1_bn = nn.BatchNorm2d(d*8)
self.deconv2 = nn.ConvTranspose2d(d*8, d*4, 4, 2, 1)
self.deconv2_bn = nn.BatchNorm2d(d*4)
self.deconv3 = nn.ConvTranspose2d(d*4, d*2, 4, 2, 1)
self.deconv3_bn = nn.BatchNorm2d(d*2)
self.deconv4 = nn.ConvTranspose2d(d*2, d, 4, 2, 1)
self.deconv4_bn = nn.BatchNorm2d(d)
self.deconv5 = nn.ConvTranspose2d(d, 1, 4, 2, 1)
def forward(self, input):
# x = F.relu(self.deconv1(input))
x = F.relu(self.deconv1_bn(self.deconv1(input)))
x = F.relu(self.deconv2_bn(self.deconv2(x)))
x = F.relu(self.deconv3_bn(self.deconv3(x)))
x = F.relu(self.deconv4_bn(self.deconv4(x)))
x = torch.tanh(self.deconv5(x))
return x
定义判别网络,使用普通2d卷积和batchnorm,注意:
D网络中使用LeakyReLU作为激活函数,最后一层使用sigmoid
class Discriminator(nn.Module):
def __init__(self, d=128):
super(Discriminator, self).__init__()
self.conv1 = nn.Conv2d(1, d, 4, 2, 1)
self.conv2 = nn.Conv2d(d, d*2, 4, 2, 1)
self.conv2_bn = nn.BatchNorm2d(d*2)
self.conv3 = nn.Conv2d(d*2, d*4, 4, 2, 1)
self.conv3_bn = nn.BatchNorm2d(d*4)
self.conv4 = nn.Conv2d(d*4, d*8, 4, 2, 1)
self.conv4_bn = nn.BatchNorm2d(d*8)
self.conv5 = nn.Conv2d(d*8, 1, 4, 1, 0)
def forward(self, input):
x = F.leaky_relu(self.conv1(input), 0.2)
x = F.leaky_relu(self.conv2_bn(self.conv2(x)), 0.2)
x = F.leaky_relu(self.conv3_bn(self.conv3(x)), 0.2)
x = F.leaky_relu(self.conv4_bn(self.conv4(x)), 0.2)
x = torch.sigmoid(self.conv5(x))
return x
初始化G和D,查看参数:
generator = Generator()
discriminator = Discriminator()
device = torch.device("cuda:0" if use_gpu and torch.cuda.is_available() else "cpu")
generator = generator.to(device)
discriminator = discriminator.to(device)
num_params_gen = sum(p.numel() for p in generator.parameters() if p.requires_grad)
num_params_disc = sum(p.numel() for p in discriminator.parameters() if p.requires_grad)
print('Number of parameters for generator: %d and discriminator: %d' % (num_params_gen, num_params_disc))

在训练网络的过程中,代码注释起到重要作用。首先,训练判别器D,使其在识别真伪方面的能力得到显著提升。接着,训练生成器G,使其生成的图片更加逼真,能够有效欺骗D。
# Train GAN
# 分别定义两个优化器,作为生成器和判别器的优化器
gen_optimizer = torch.optim.Adam(params=generator.parameters(), lr=learning_rate, betas=(0.5, 0.999))
disc_optimizer = torch.optim.Adam(params=discriminator.parameters(), lr=learning_rate, betas=(0.5, 0.999))
# 设置为训练模式
generator.train()
discriminator.train()
gen_loss_avg = []
disc_loss_avg = []
print("Training...")
for epoch in range(num_epochs):
gen_loss_avg.append(0)
disc_loss_avg.append(0)
num_batches = 0
for image_batch, _ in train_dataloader:
image_batch = image_batch.to(device)
label_real = torch.ones(image_batch.size(0), device=device)
label_fake = torch.zeros(image_batch.size(0), device=device)
# 随机生成,作为生成器的输入
latent = torch.randn(image_batch.size(0), 100, 1, 1, device=device)
fake_image_batch = generator(latent)
# 生成判别器的label,1为真,0为假
pred_real = discriminator(image_batch).squeeze() # squeeze()是去除张量里维度为一
pred_fake = discriminator(fake_image_batch.detach()).squeeze() # detach是分离生成器和判别器
# 定义判别器的loss,使他能区分真假
disc_loss = 0.5 * (
F.binary_cross_entropy(pred_fake, label_fake) + F.binary_cross_entropy(pred_real, label_real))
# 判别器的反向传播和权重更新
disc_optimizer.zero_grad()
disc_loss.backward()
disc_optimizer.step()
# 用权重依旧得到更新的判别器再次判别生成器生成的图像,这时候predict的结果应该比上面更能区分真假
pred_fake = discriminator(fake_image_batch).squeeze()
# 定义生成器的loss 使他更接近真实值
gen_loss = F.binary_cross_entropy(pred_fake, label_real)
# 生成器的反向传播和权重更新
gen_optimizer.zero_grad()
gen_loss.backward()
gen_optimizer.step()
# 累加loss
gen_loss_avg[-1] += gen_loss.item()
disc_loss_avg[-1] += disc_loss.item()
num_batches += 1
# 输出loss
gen_loss_avg[-1] /= num_batches
disc_loss_avg[-1] /= num_batches
print('Epoch [%d / %d] average loss generator vs. discrim.: %f vs. %f' %
(epoch+1, num_epochs, gen_loss_avg[-1], disc_loss_avg[-1]))

绘制训练loss的曲线:
import matplotlib.pyplot as plt
plt.ion()
fig = plt.figure()
plt.plot(gen_loss_avg, label='generator')
plt.plot(disc_loss_avg, label='discriminator')
plt.xlabel('Epochs')
plt.ylabel('Loss')
plt.legend()
plt.show()

在向量z之间进行插值计算,详细分析生成器生成的图像效果。
import numpy as np
import matplotlib.pyplot as plt
plt.ion()
import torchvision.utils
generator.eval()
def interpolation(lambda1, model, latent_1, latent_2):
with torch.no_grad():
# interpolation of the two latent vectors
inter_latent = lambda1* latent_1 + (1- lambda1) * latent_2
# reconstruct interpolated image
inter_latent = inter_latent.to(device)
inter_image = model(inter_latent)
inter_image = inter_image.cpu()
return inter_image
# sample two latent vectors from the standard normal distribution
latent_1 = torch.randn(1, 100, 1, 1, device=device)
latent_2 = torch.randn(1, 100, 1, 1, device=device)
# interpolation lambdas
lambda_range=np.linspace(0,1,10)
fig, axs = plt.subplots(2,5, figsize=(15, 6))
fig.subplots_adjust(hspace = .5, wspace=.001)
axs = axs.ravel()
for ind,l in enumerate(lambda_range):
inter_image=interpolation(float(l), generator, latent_1, latent_2)
inter_image = to_img(inter_image)
image = inter_image.numpy()
axs[ind].imshow(image[0,0,:,:], cmap='gray')
axs[ind].set_title('lambda_val='+str(round(l,1)))
plt.show()

随机生成一些latent vector ,看看生成器的生成效果。
import numpy as np
import matplotlib.pyplot as plt
plt.ion()
import torchvision.utils
generator.eval()
def to_img(x):
x = 0.5 * (x + 1)
x = x.clamp(0, 1)
return x
def show_image(img):
img = to_img(img)
npimg = img.numpy()
plt.imshow(np.transpose(npimg, (1, 2, 0)))
with torch.no_grad():
# sample latent vectors from the standard normal distribution
latent = torch.randn(image_batch.size(0), 100, 1, 1, device=device)
fake_image_batch = generator(latent)
fake_image_batch = fake_image_batch.cpu()
fig, ax = plt.subplots(figsize=(8, 8))
show_image(torchvision.utils.make_grid(fake_image_batch.data[:100],10,5))
plt.show()

