GAN的基本概念

以下内容由我的CSDN博客迁移。

之前学习了基本的深度学习与神经网络模型,让我们开始学习一些进阶的东西。个人对于生成对抗网络很感兴趣,就学习它吧。(主要参考李宏毅的GAN教程,以及其他一些博客)

生成对抗网络(Generative Adversial Network, GAN) ,是很有创新性的一种模型。它提供了一种思想就是对抗训练,让两个或多个模型间互相促进,犹如自然选择一般进行进化。近年来有关 GAN 的应用越来越多,可以前往thisxdoesnotexist.com/,里面有许多有趣的项目。同时关于 GAN 的论文也越来越多:在这里插入图片描述 甚至在“GAN”前加上两个字母都有重复的名字(例如LSGAN), github 上有一个 the-gan-zoo 里面汇集了很多的 GAN 模型及论文。

随着 GAN 的研究不断发展,它不仅能够应用于图像生成,还能够生成文章,生成语音等等,总之 GAN 的应用可以充分发挥你的想象力。

这一节我们说一说最基本的 GAN 的思想并实现成二次元头像。 ## 一、基本思想 GAN 主要由两部分组成:生成器(Generator)判别器(Discriminitor)

1. 生成器

在这里插入图片描述 最基本的生成器的就是输入一些随机向量,然后它就可以生成一些对应的图片或是句子。向量的每个维度都代表了一些特征,譬如头发颜色,有无眼镜等等。 在这里插入图片描述 生成器本质上就是一个神经网络,它将特征值映射到目标任务上。

2. 判别器

判别器的作用就是分辨给定的图片或数据是来自真实数据分布生成的还是生成器所生成的假数据,它会给一个评分。在这里插入图片描述 #### 3. 对抗训练 生成器和判别器之间需要进行对抗训练,也就是说,生成器要尽可能“骗过”判别器,而判别器要尽可能识别出生成器的假样本。两者相互对抗,最后共同进化。 在这里插入图片描述 #### 4. 训练过程 具体的训练过程如下:

在每个循环中: + 首先我们需要从真实分布中采集出 m 个真实样本,给它们标为 1 (即表示为真实标签),同时随机生成 m 个向量,投入生成器中生成 m 个假样本记作 0 (即假标签)。 + 将真实标签的数据和假标签的数据都喂入判别器中,计算出它的 Loss ,并反向传播进行梯度优化。 + 固定住判别器的权重,然后将之前生成的假样本的标签改为 1 ,即对于生成器来说这种假样本在判别器中的真实性应该是越真实越好。然后重新喂入判别器中,进行反向传播梯度优化。

至于 Loss 的选择有许多,对于最基本的 GAN, 它采用了 BCELoss,即二类交叉熵损失。因此,具体过程如图: 在这里插入图片描述 ## 二、对抗训练的好处 为什么需要对抗训练?为什么判别器或者生成器不能自主学习?让我们分情况说明。 #### 1. 仅有生成器的学习 仅有生成器的学习思想与自编码器相当。我们可以依照 Auto-encoder 的训练过程进行训练,此时的 Decoder 可以看作是生成器。在这里插入图片描述 但是自编码器仅仅要求还原的图片与原图片尽可能像就行,而没有考虑到邻近像素间的差异(即人眼的感受)。例如对于目标 在这里插入图片描述 以下四个生成图前两个与原目标仅有一个像素的差异,后两个有六个像素差异,在这里插入图片描述 然而直观来看,我们可以一眼就感觉出前两个生成的效果并不好。

自编码器并不能很好的考虑到邻近像素的相关性,可能需要很深的网络才可以,因此只有生成器并不能很好的完成生成任务,需要一个判别器帮助判断像素间是否符合直观感受。 #### 2. 只有判别器的学习 如果我们有一个表现很好的判别器 D(x) 。我们可以解决以下的问题来实现生成: 在这里插入图片描述 然而这需要我们列举出全部可能的x,这几乎是不可能的事情。

此外,即便我们可以列举出全部的 x ,但是由于来自于真实分布的数据必然全部都是真实的,不存在假的样本,因此我们无法去获取假的真实样本。

3. 生成器与判别器的结合才能各尽所能

生成器和判别器各有优缺点: 在这里插入图片描述 因此只有两者结合起来对抗训练才能互相补充,发挥特长。 在这里插入图片描述 如图。从判别器的角度来看,生成器事实上就是一种列举 x 的有效手段,并且可以产生出假的样本;同时从生成器的角度看,判别器可以帮助判断生成器生成的是否符合直观,可以注意到不同部分的关联。

三、GAN 实践:生成动漫头像

这里我使用 pytorch 简单地搭建了一个 DCGAN 网络,来实现生成一些二次元头像。(就是李宏毅课的作业 HW3-1,我还主要参考了torch官网的这篇教程

首先让我们导入相关的库,设置一个 seed

import matplotlib.pyplot as plt
import torch
import torch.nn as nn
import torch.optim as op
import torch.utils.data as dat
import torch.backends.cudnn as cudnn
import torchvision.datasets as dset
import torchvision.transforms as transforms
import torchvision.utils as vutils
import numpy as np

%matplotlib inline

# 设置 seed
seed = 2020
torch.manual_seed(seed)

接着定义一些超参数: root : 数据存放地址 lr : 学习率 batch_size : 批量 input_size : 随机向量的长度 image_size : 图像的大小 epochs : 迭代次数

# 超参数
root = r'data'
lr = 0.0006
batch_size = 64
input_size = 256
image_size = 96
epochs = 10
读取数据

dataset = dset.ImageFolder(root,
                           transform=transforms.Compose([transforms.Resize((96,96)),
                              							 transforms.ToTensor(),
                           							     transforms.Normalize((0.5, 0.5, 0.5),
                                            						          (0.5, 0.5, 0.5)),
                           ]))
dataloader = dat.DataLoader(dataset,
                            shuffle=True,
                            batch_size=batch_size,
                            drop_last=True,
                            num_workers=2)
# 在 GPU 或者 CPU 上运行
device = torch.device("cuda:0" if (torch.cuda.is_available()) else "cpu")

我们可以检查一下数据:

real_batch = next(iter(dataloader))
plt.figure(figsize=(8, 8))
plt.axis("off")
plt.title("Training Images")
plt.imshow(
    np.transpose(vutils.make_grid(real_batch[0].to(device).cpu(), normalize=True),
                 (1, 2, 0)))
输出:

按照之前学习的流程,首先我们定义初始化:

def weights_init(m):
    classname = m.__class__.__name__
    if classname.find('Conv') != -1:
        nn.init.normal_(m.weight.data, 0.0, 0.02)
    elif classname.find('BatchNorm') != -1:
        nn.init.normal_(m.weight.data, 1.0, 0.02)
        nn.init.constant_(m.bias.data, 0)

定义生成器:

 # Generator

class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.upsample = nn.Sequential(
            nn.ConvTranspose2d(256, 512, 7, 2, 0, bias=False),
            nn.BatchNorm2d(512), nn.ReLU(True),
            nn.ConvTranspose2d(512, 256, 7, 2, 0, bias=False),
            nn.BatchNorm2d(256), nn.ReLU(True),
            nn.ConvTranspose2d(256, 128, 7, 2, 0, bias=False),
            nn.BatchNorm2d(128), nn.ReLU(True),
            nn.ConvTranspose2d(128, 64, 5, 2, 0, bias=False),
            nn.BatchNorm2d(64), nn.ReLU(True),
             nn.ConvTranspose2d(64, 64, 5, 1, 0, bias=False),
            nn.BatchNorm2d(64), nn.ReLU(True),
            nn.ConvTranspose2d(64, 3, 4, 1, 0, bias=False), nn.Tanh())

    def forward(self, input):
        input = self.upsample(input)
        return input


# 创建并初始化
G = Generator().to(device)
G.apply(weights_init)
print(G)

判别器:

# Discriminator


class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.shape = 128 * 8 * 8
        self.main = nn.Sequential(
            nn.Conv2d(3, 64, 7, 2, 1),
            nn.LeakyReLU(0.2, True),
            nn.Conv2d(64, 128, 5, 2, 1),
            nn.BatchNorm2d(128),
            nn.LeakyReLU(0.2, True),
            nn.Conv2d(128, 256, 5, 2, 1),
            nn.BatchNorm2d(256),
            nn.LeakyReLU(0.2, True),
            nn.Conv2d(256, 512, 5, 2, 1),
            nn.BatchNorm2d(512),
            nn.LeakyReLU(0.2, True),
            nn.Conv2d(512, 1, 4, 1, 0),
            nn.Sigmoid()
        )

    def forward(self, input):
        return self.main(input)


# 创建并初始化
D = Discriminator().to(device)
D.apply(weights_init)
print(D)
(输出略)

接下来我们定义损失函数和优化器,并生成一组测试向量:

# 损失函数
criterion = nn.BCELoss()

# 生成一组要查看生成图片的随机数
test_noise = torch.randn(64, input_size, 1, 1, device=device)

# 建立标签
real_label = 1
fake_label = 0

# 优化器
optD = op.Adam(D.parameters(), lr=lr, betas=(0.5, 0.999))
optG = op.Adam(G.parameters(), lr=lr, betas=(0.5, 0.999))

开始训练!

# 训练

# 一些记录信息
img_list = []
G_losses = []
D_losses = []
iters = 0

print('开始训练!!')
for epoch in range(epochs):
    for i, data in enumerate(dataloader, 0):
        # 第一步:更新 D max L = avg(log x) + avg(log 1-\tilde x)
        # 真实样本
        D.zero_grad()
        real_cpu = data[0].to(device)
        b_size = real_cpu.size(0)
        label = torch.full((b_size,), real_label, device=device)
        output = D(real_cpu).view(-1)
        err_real_D = criterion(output, label)
        err_real_D.backward()
        # 记录当前的正例的分数
        D_x = output.mean().item()
        # 对抗样本
        noise = torch.randn((b_size, input_size, 1, 1), device=device)
        fake = G(noise)
        label.fill_(fake_label)
        output = D(fake.detach()).view(-1)
        err_fake_D = criterion(output, label)
        err_fake_D.backward()
        D_G_z1 = output.mean().item()

        # 将两个损失加在一起
        errD = err_fake_D + err_real_D
        # 更新 D
        optD.step()
        
        # 第二步 更新 G min L = avg(log \tilde x)
        G.zero_grad()
        label.fill_(real_label)  # 对于生成器,假样本就是真的
        output = D(fake).view(-1)
        errG = criterion(output, label)
        errG.backward()
        D_G_z2 = output.mean().item()

        # 更新 G 
        optG.step()

        # 记录训练信息
        if i % 50 == 0:
            print(
                '[%d/%d][%d/%d]\tLoss_D: %.4f\tLoss_G: %.4f\tD(x): %.4f\tD(G(z)): %.4f / %.4f'
                % (epoch, epochs, i, len(dataloader), errD.item(),
                   errG.item(), D_x, D_G_z1, D_G_z2))

        # 记录损失,以画出学习曲线
        G_losses.append(errG.item())
        D_losses.append(errD.item())

        # 通过查看之前固定的随机生成值,我们可以看到生成器的进步
        if (iters % 500 == 0) or ((epoch == epochs - 1) and
                                  (i == len(dataloader) - 1)):
            with torch.no_grad():
                fake = G(test_noise).detach().cpu()
            img_list.append(vutils.make_grid(fake, padding=2, normalize=True))

        iters += 1

输出学习曲线:

plt.figure(figsize=(10,5))
plt.title("Generator and Discriminator Loss During Training")
plt.plot(G_losses,label="G")
plt.plot(D_losses,label="D")
plt.xlabel("iterations")
plt.ylabel("Loss")
plt.legend()
plt.show()

再看看生成器具体的情况:

import matplotlib.animation as animation

fig = plt.figure(figsize=(8,8))
plt.axis("off")
ims = [[plt.imshow(np.transpose(i,(1,2,0)), animated=True)] for i in img_list]
ani = animation.ArtistAnimation(fig, ims, interval=1000, repeat_delay=1000, blit=True)
from IPython.display import HTML
HTML(ani.to_jshtml())

如图所示,我们可以看到生成器的进步:

最后再对比一下:

# Grab a batch of real images from the dataloader
real_batch = next(iter(dataloader))

# Plot the real images
plt.figure(figsize=(15,15))
plt.subplot(1,2,1)
plt.axis("off")
plt.title("Real Images")
plt.imshow(np.transpose(vutils.make_grid(real_batch[0].to(device)[:64], padding=5, normalize=True).cpu(),(1,2,0)))

# Plot the fake images from the last epoch
plt.subplot(1,2,2)
plt.axis("off")
plt.title("Fake Images")
plt.imshow(np.transpose(img_list[-1],(1,2,0)))
plt.show()

最后得到的结果 在这里插入图片描述

我们于是便得到了一个二次元头像生成器,虽然效果并不怎么好,但毕竟是最基本的GAN 架构,之后我们再继续学习更好的模型。

四、训练 GAN 的技巧

在训练 GAN 的过程中,我深刻体会到了深度炼丹确实名副其实。学习率,初始化的改变,都有可能使得梯度爆炸或者梯度归零,使得判别器和生成器的 loss 区别很大。有可能判别器 loss 为 0 而生成器的 loss 不断增大,没有减少的趋势,又或者是二者反过来,每次我都会感到深度玄学的神秘。

训练 GAN 有很多技巧,我这里给出一些博客上的:

  1. 更大的kernel,更多的filter 大的kernel能够覆盖更多的像素,也因此能够获得过多的信息。在CIFAR-10数据上,使用55的kernel能够得到很好的效果,而使用33的kernel会导致判别器的损失函数快速跌落至0。对于生成器,顶层更大的kernel某种程度上来说能够保证平滑,对于底层改变kernel的大小不会有什么影响。 filter数目的增加将极大增加网络的参数量,通常来说,filter的数目越多越好。在上面的实验中,我使用了128个filter。当使用很少的filter时,尤其生成器包含很少的filter时,生成的图像会特别模糊。所以,更多的filter能够获取更多的信息,并最终保证生成的图像具有足够的清晰度。

  2. 更改样本标签 在训练判别器时,这个操作非常有用。0或者1的标签,我们称之为hard label,这种标签可能会使得判别器的损失值迅速跌落至0。所以我们使用了soft label,对于标签为0的样本,其标签设置为(0,0.1)内的随机数;而对于标签为1的样本,其标签设置为(0.9,1)内的随机数。当然,这个操作在训练生成器的时候不需要做。 在训练样本的标签上加入随机噪声,也能提升模型效果。进入判别器中5%的图像,随机地翻转其标签,即真的猫图片标记成假的,生成的猫图片标记为真的猫图片,也能够提升一些效果。

  3. Batch normalization 加入batch normalization能够使得生成的图像更加细腻,但当kernel和filter的数目设置不对,或者判别器的损失函数快速跌落至0时,增加BN没有什么作用。

  4. 不要early stopping 刚刚接触GAN的训练时,一个常见的错误:当我们发现损失值不变时,或者生成的图像一直模糊时,通常会终止训练,调整模型。这个时候我们也要注意一下,GAN的训练通常非常耗时,所以有时候多等一等会有意想不到的“收获”。 值得注意的是,当判别器的损失值快速接近0时,通常生成器很难学到任何东西了,就需要及时终止训练,修改网络、重新训练。

  5. 规范化输入

    • 将输入图片规范化到-1到1之间
    • 生成器最后一层的输出使用tanh激活函数 毋庸置疑规范化是最重要的,未经处理的图片是没有办法收敛的。图片规范化一种简单的方法是(images-127.5)/127.5,然后送到判别器去训练,同理生成的图片也要经过判别器,所以生成器的输出也是-1到1之间(和原图的区间范围保持一致) 这里有一个坑,因为生成的图是-1到1之间,需要再经过处理回到0-255区间才能正常显示。经过测试matplotlib似乎没法显示-1-1的图,而scipy.misc可以,所以写GAN我通常结合scipy.misc来看结果。
  6. 用修正的损失函数 在GAN论文里用min (log 1-D)来优化G,实际上max(log D)更好 实际代码中用反转标签来训练G更方便,即把生成图片当成real的标签来训练 损失函数恐怕是一个超级热门的研究点,参照当前的研究进展,很多实验中已经很少用到上面提到的交叉熵损失了,因为效率实在太低而且不稳定。

  7. 使用一个具有球形结构的随机噪声 不要使用均匀分布,而是从高斯分布中采样 Tom White的论文Sampling Generative Networks,项目代码https://github.com/dribnet/plat中查看更多的细节

  8. 避免引入稀疏梯度:ReLU,MaxPool GAN的稳定性会因为引入稀疏梯度受到很大影响 尽量使用LeakyReLU作为激活函数 对于下采样,使用:Average Pooling或者Conv2d + stride 对于上采样,使用:PixelShuffle或者ConvTranspose2d + stride PixelShuffle的原文:[1609.05158] Real-Time Single Image and Video Super-Resolution Using an Efficient Sub-Pixel Convolutional Neural Network 建议使用全卷积,避免使用任何pooling,因为使用pooling会损失信息,这对于GAN训练很不好。当然如果计算资源不够,该用pooling还是要用的。

  9. 如果你还有图片的类别标签,训练判别器在判别真伪的同时对其分类

  10. 早早的追踪到训练失败的信号 例如D的loss稳定下降,变得很小,或者稳定上升,变得很大。这些都是网络没有balance的信号 这一点其实更需要自己实际训练当中的经验,而且每个人的习惯不一样,例如在一定epoch的时候,输出generated image到路径看一看,一般看到全是噪声,基本可以停止训练了,再往下训练也不会有改善。所以不要把时间浪费在无谓,病态的梯度更新上。

  11. 训练和测试阶段,在G中使用DropOut 使用DropOut也是为了引入一定的噪声 在生成器的某几层中使用DropOut,而且测试和训练阶段都要做 常规的Dropout是测试阶段关闭的,而GAN里面,测试阶段同样也要使用Dropout

当然这些技巧可能在现在已经过时了,但是对于我这种小白来说还是比较有用的。

参考链接: GAN的使用技巧与注意事项笔记

怎样训练一个GAN?一些小技巧让GAN更好的工作

训练GAN的一些骚操作


以上内容如有谬误还请告知。 代码部分可能结果不一样。