『一起学AI』生成对抗网络(GAN)原理学习及实战开发

177 阅读13分钟

                  

参考并翻译教程:d2l.ai/chapter_gen…,加入笔者的理解和心得

 

1。生成对抗网络原理

在Colab中打开笔记本

在某种形式上,我们使用了深度神经网络学习的从数据示例到标签的映射。这种学习称为判别学习,例如,我们希望能够区分照片中的猫和狗中的照片。分类器和回归器都是歧视性学习的例子。通过反向传播训练的神经网络颠覆了我们认为关于大型复杂数据集的判别式学习的所有知识。在短短5至6年间,高分辨率图像的分类精度已从无用提高到了人类水平。我们将为您提供其他所有关于深度神经网络效果惊人的其他判别任务的帮助。

但是,机器学习不仅仅是解决区分性任务。例如,给定一个大型数据集,而没有任何标签,我们可能想要学习一个可以准确捕获此数据特征的模型。给定这样一个模型,我们可以对类似于训练数据分布的综合数据示例进行采样。例如,给定大量的面部照片,我们可能希望能够生成新的真实感图像,看起来好像它可能来自同一数据集。这种学习称为生成建模。

直到最近,我们还没有能够合成新颖的逼真的图像的方法。但是,深度神经网络在判别式学习中的成功开辟了新的可能性。在过去三年中,一大趋势是应用区分性深网来克服我们通常不认为是监督学习问题的问题中的挑战。递归神经网络语言模型是使用判别网络(经过训练可预测下一个字符)的一个示例,该判别网络一旦受过训练就可以充当生成模型。

2014年,一篇突破性的论文介绍了生成对抗网络(GAN)[Goodfellow et al。,2014],这是一种利用判别模型的力量来获得良好生成模型的聪明新方法。GAN的核心思想是,如果我们不能分辨真实数据之外的虚假数据,那么数据生成器就很好。在统计中,这称为两次抽样检验-回答数据集是否存在的问题的检验X={x1,…,xn}和 X′={x′1,…,x′n}是从相同的分布中得出的。大多数统计文件与GAN之间的主要区别在于,后者以建设性的方式使用了这一思想。换句话说,他们不只是训练模型说“嘿,这两个数据集看起来好像不是来自同一分布”,而是使用两次样本检验为生成的模型提供训练信号。这使我们能够改进数据生成器,直到它生成类似于真实数据的内容为止。至少,它需要愚弄分类器。即使我们的分类器是最先进的深度神经网络。

图1.1生成对抗网络

GAN架构图1。如您所见,GAN架构中有两个部分-首先,我们需要一个设备(例如,深层网络,但实际上可能是任何东西,例如游戏渲染引擎),它可能能够生成看起来很漂亮的数据。就像真实的东西一样。如果要处理图像,则需要生成图像。如果要处理语音,则需要生成音频序列,依此类推。我们称其为生成器网络。第二部分是鉴别器网络。它试图将伪造数据与真实数据区分开。这两个网络相互竞争。生成器网络尝试欺骗鉴别器网络。在这一点上,鉴别器网络适应了新的伪造数据。该信息继而用于改善生成器网络,等等。

鉴别器是一个二进制分类器,用于区分输入是否 x是真实的(来自真实数据)还是伪造的(来自生成器)。通常,鉴别器输出标量预测 o∈R 用于输入 x,例如使用隐藏大小为1的密集层,然后应用S形函数来获得预测的概率 D(x)=1/(1+e−o)。假设标签y 因为真正的数据是 1 和 0虚假数据。我们培养鉴别,以尽量减少交叉熵损失,

(1.1) minD{−ylogD(x)−(1−y)log(1−D(x))},

 

对于生成器,它首先绘制一些参数 z∈Rd来自随机源,例如正态分布z∼N(0,1)。我们经常称z为隐变量。然后应用一个函数来生成x′=G(z)。生成器的目的是欺骗鉴别器进行分类 x′=G(z)作为真实数据,我们想要 D(G(z))≈1。换句话说,对于给定的歧视者D,我们更新生成器的参数 G 最大化交叉熵损失 y=0,

(1.2) maxG{−(1−y)log(1−D(G(z)))}=maxG{−log(1−D(G(z)))}.

 

如果生成器做得很好,那么 D(x′)≈1 因此上述损失接近于0,这导致梯度太小而无法使鉴别器取得良好的进展。因此,通常我们将以下损失降到最低:

(1.3) minG{−ylog(D(G(z)))}=minG{−log(D(G(z)))},

 

这只是送 x′=G(z) 进入鉴别器,但给标签 y=1.

总结, D 和 G正在玩具有综合目标功能的“ minimax”游戏:

(1.4)minDmaxG{−Ex∼DatalogD(x)−Ez∼Noiselog(1−D(G(z)))}.

 

许多GAN应用程序都在图像的上下文中。作为演示目的,我们将首先满足于简化演示版。我们将说明如果使用GAN为高斯数据建立参数估计器,将会发生什么情况。让我们开始吧。

 

%matplotlib inline
import torch
from torch import nn
from d2l import torch as d2l

 

1.1。生成一些“真实”数据

由于这将是世界上最繁琐的示例,因此我们只需要从高斯生成数据即可。

 

X = torch.normal(0.0, 1, (1000, 2))
A = torch.tensor([[1, 2], [-0.1, 0.5]])
b = torch.tensor([1, 2])
data = torch.matmul(X, A) + b

 

让我们看看我们得到了什么。这应该是以某种相当随意的方式用均值平移的高斯b和协方差矩阵 ATA.

 

d2l.set_figsize()
d2l.plt.scatter(data[:100, (0)].detach().numpy(), data[:100,
                                                       (1)].detach().numpy())
print(f'The covariance matrix is\n{torch.matmul(A.T, A)}')

 

The covariance matrix is
tensor([[1.0100, 1.9500],
        [1.9500, 4.2500]])

 

batch_size = 8
data_iter = d2l.load_array((data,), batch_size)

 

1.2。生成器

我们的生成器网络将是最简单的网络-单层线性模型。这是因为我们将使用高斯数据生成器来驱动线性网络。因此,它实际上只需要学习参数就可以完美地伪造事物。

 

net_G = nn.Sequential(nn.Linear(2, 2))

1.3。判别器

对于区分器,我们将更具区分性:我们将使用具有3层的MLP,使事情变得更加有趣。

 

net_D = nn.Sequential(nn.Linear(2, 5), nn.Tanh(), nn.Linear(5, 3), nn.Tanh(),
                      nn.Linear(3, 1))

 

1.4。训练

首先,我们定义一个函数来更新鉴别器。

 

#@save
def update_D(X, Z, net_D, net_G, loss, trainer_D):
    """Update discriminator."""
    batch_size = X.shape[0]
    ones = torch.ones((batch_size,), device=X.device)
    zeros = torch.zeros((batch_size,), device=X.device)
    trainer_D.zero_grad()
    real_Y = net_D(X)
    fake_X = net_G(Z)
    # Do not need to compute gradient for `net_G`, detach it from
    # computing gradients.
    fake_Y = net_D(fake_X.detach())
    loss_D = (loss(real_Y, ones.reshape(real_Y.shape)) +
              loss(fake_Y, zeros.reshape(fake_Y.shape))) / 2
    loss_D.backward()
    trainer_D.step()
    return loss_D

 

生成器的更新类似。在这里,我们复用了交叉熵损失,但是标签从0变为了1.

 

#@save
def update_G(Z, net_D, net_G, loss, trainer_G):
    """Update generator."""
    batch_size = Z.shape[0]
    ones = torch.ones((batch_size,), device=Z.device)
    trainer_G.zero_grad()
    # We could reuse `fake_X` from `update_D` to save computation
    fake_X = net_G(Z)
    # Recomputing `fake_Y` is needed since `net_D` is changed
    fake_Y = net_D(fake_X)
    loss_G = loss(fake_Y, ones.reshape(fake_Y.shape))
    loss_G.backward()
    trainer_G.step()
    return loss_G

 

鉴别器和生成器都执行具有交叉熵损失的二进制逻辑回归。我们使用封装来简化培训过程。在每次迭代中,我们首先更新鉴别器,然后更新生成器。我们将损失和生成的示例可视化。

 

def train(net_D, net_G, data_iter, num_epochs, lr_D, lr_G, latent_dim, data):
    loss = nn.BCEWithLogitsLoss(reduction='sum')
    for w in net_D.parameters():
        nn.init.normal_(w, 0, 0.02)
    for w in net_G.parameters():
        nn.init.normal_(w, 0, 0.02)
    trainer_D = torch.optim.Adam(net_D.parameters(), lr=lr_D)
    trainer_G = torch.optim.Adam(net_G.parameters(), lr=lr_G)
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)
    for epoch in range(num_epochs):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3)  # loss_D, loss_G, num_examples
        for (X,) in data_iter:
            batch_size = X.shape[0]
            Z = torch.normal(0, 1, size=(batch_size, latent_dim))
            metric.add(update_D(X, Z, net_D, net_G, loss, trainer_D),
                       update_G(Z, net_D, net_G, loss, trainer_G), batch_size)
        # Visualize generated examples
        Z = torch.normal(0, 1, size=(100, latent_dim))
        fake_X = net_G(Z).detach().numpy()
        animator.axes[1].cla()
        animator.axes[1].scatter(data[:, 0], data[:, 1])
        animator.axes[1].scatter(fake_X[:, 0], fake_X[:, 1])
        animator.axes[1].legend(['real', 'generated'])
        # Show the losses
        loss_D, loss_G = metric[0] / metric[2], metric[1] / metric[2]
        animator.add(epoch + 1, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec')

 

现在,我们指定超参数以拟合高斯分布。

 

lr_D, lr_G, latent_dim, num_epochs = 0.05, 0.005, 2, 20
train(net_D, net_G, data_iter, num_epochs, lr_D, lr_G, latent_dim,
      data[:100].detach().numpy())

 

loss_D 0.693, loss_G 0.693, 1519.0 examples/sec

 

1.5。概括

  • 生成对抗网络(GAN)由两个深层网络(生成器和鉴别器)组成。
  • 生成器通过最大化交叉熵损失(即) 来生成尽可能接近真实图像的图像,以欺骗鉴别器。maxlog(D(x′)).
  • 鉴别器尝试从真实图像区分所生成的图像,通过最小化交叉熵损失,, min−ylogD(x)−(1−y)log(1−D(x)).

1.6。练习题

  • 在生成器获胜的地方是否存在均衡,鉴别器最终无法区分有限样本上的两个分布?

 

 

2。深度卷积生成对抗网络

 

Open the notebook in Colab

1节中,我们介绍了GAN的工作原理。我们证明了他们可以从一些简单,易于采样的分布(如均匀分布或正态分布)中抽取样本,并将它们转换为看起来与某些数据集的分布相匹配的样本。尽管我们提出的匹配2D高斯分布的例子很明确,但这并不是特别令人兴奋。

在本节中,我们将演示如何使用GAN生成逼真的图像。我们将基于[Radford et al。,2015]中引入的深度卷积GAN(DCGAN)建立模型 。我们将借鉴已证明在区分计算机视觉问题上非常成功的卷积架构,并展示如何通过GAN来利用它们来生成逼真的图像。

 

import warnings
import torch
import torchvision
from torch import nn
from d2l import torch as d2l

 

2.1。宠物小精灵数据集

我们将使用的数据集是从pokemondb获得的Pokemon精灵的集合 。首先下载,提取并加载此数据集。

 

#@save
d2l.DATA_HUB['pokemon'] = (d2l.DATA_URL + 'pokemon.zip',
                           'c065c0e2593b8b161a2d7873e42418bf6a21106c')

data_dir = d2l.download_extract('pokemon')
pokemon = torchvision.datasets.ImageFolder(data_dir)

 

Downloading ../data/pokemon.zip from http://d2l-data.s3-accelerate.amazonaws.com/pokemon.zip...

我们将每个图像调整为 64×64。该ToTensor 改造项目将像素值成[0,1],而我们的生成器将使用tanh函数来获取 [−1,1]。因此我们用0.5 意思是和 0.5 标准偏差以匹配值范围。

 

batch_size = 256
transformer = torchvision.transforms.Compose([
    torchvision.transforms.Resize((64, 64)),
    torchvision.transforms.ToTensor(),
    torchvision.transforms.Normalize(0.5, 0.5)])
pokemon.transform = transformer
data_iter = torch.utils.data.DataLoader(
    pokemon, batch_size=batch_size, shuffle=True,
    num_workers=d2l.get_dataloader_workers())

 

让我们可视化前20张图像。

 

warnings.filterwarnings('ignore')
d2l.set_figsize((4, 4))
for X, y in data_iter:
    imgs = X[0:20, :, :, :].permute(0, 2, 3, 1) / 2 + 0.5
    d2l.show_images(imgs, num_rows=4, num_cols=5)
    break

 

 

2.2。生成器

生成器需要映射噪声变量 z∈Rd,长度-d 向量,将RGB图像的宽度和高度设置为 64×64。使用转置卷积层来扩大输入大小的完全卷积网络 。生成器的基本块包含一个转置的卷积层,然后进行批量归一化和ReLU激活。

 

class G_block(nn.Module):
    def __init__(self, out_channels, in_channels=3, kernel_size=4, strides=2,
                 padding=1, **kwargs):
        super(G_block, self).__init__(**kwargs)
        self.conv2d_trans = nn.ConvTranspose2d(in_channels, out_channels,
                                               kernel_size, strides, padding,
                                               bias=False)
        self.batch_norm = nn.BatchNorm2d(out_channels)
        self.activation = nn.ReLU()

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d_trans(X)))

 

默认情况下,转置的卷积层使用 kh=kw=4  内核,一个 sh=sw=2  大步向前, ph=pw=1 填充。输入形状为 n′h×n′w=16×16 ,生成器块将输入的宽度和高度加倍。

 

 

 

x = torch.zeros((2, 3, 16, 16))
g_blk = G_block(20)
g_blk(x).shape
torch.Size([2, 20, 32, 32])

 

 

如果将转置的卷积层更改为 4×4 核心, 1×1大步前进和零填充。输入大小为 1×1,输出的宽度和高度将分别增加3。

 

x = torch.zeros((2, 3, 1, 1))
g_blk = G_block(20, strides=1, padding=0)
g_blk(x).shape

 

torch.Size([2, 20, 4, 4])

生成器由四个基本块组成,这些块将输入的宽度和高度从1增加到32。同时,它首先将隐变量投影到 64×8频道,然后每次将频道减半。最后,转置的卷积层用于生成输出。它进一步将宽度和高度加倍以匹配所需的64×64 形状,并将通道尺寸减小到 3。tanh激活功能适用于将输出值投影到(−1,1) 范围。

 

n_G = 64
net_G = nn.Sequential(
    G_block(in_channels=100, out_channels=n_G * 8, strides=1,
            padding=0),  # Output: (64 * 8, 4, 4)
    G_block(in_channels=n_G * 8,
            out_channels=n_G * 4),  # Output: (64 * 4, 8, 8)
    G_block(in_channels=n_G * 4,
            out_channels=n_G * 2),  # Output: (64 * 2, 16, 16)
    G_block(in_channels=n_G * 2, out_channels=n_G),  # Output: (64, 32, 32)
    nn.ConvTranspose2d(in_channels=n_G, out_channels=3,
                       kernel_size=4, stride=2, padding=1, bias=False),
    nn.Tanh())  # Output: (3, 64, 64)

 

生成一个100维的潜在变量,以验证生成器的输出形状。

 

x = torch.zeros((1, 100, 1, 1))
net_G(x).shape

 

torch.Size([1, 3, 64, 64])

2.3。判别器

鉴别器是普通的卷积网络,只是它使用泄漏的ReLU作为其激活功能。给定 α∈[0,1],其定义是

 

可以看出,如果 α=0,以及一个身份函数(如果 α=1。为了α∈(0,1),泄漏ReLU是一个非线性函数,为负输入提供非零输出。它旨在解决“失活的ReLU”问题,因为神经元可能总是输出负值,因此由于ReLU的梯度为0,因此无法得到更新。

 

alphas = [0, .2, .4, .6, .8, 1]
x = torch.arange(-2, 1, 0.1)
Y = [nn.LeakyReLU(alpha)(x).detach().numpy() for alpha in alphas]
d2l.plot(x.detach().numpy(), Y, 'x', 'y', alphas)

 

 

鉴别器的基本模块是卷积层,然后是批处理归一化层和泄漏的ReLU激活。卷积层的超参数类似于生成器块中的转置卷积层。

 

class D_block(nn.Module):
    def __init__(self, out_channels, in_channels=3, kernel_size=4, strides=2,
                 padding=1, alpha=0.2, **kwargs):
        super(D_block, self).__init__(**kwargs)
        self.conv2d = nn.Conv2d(in_channels, out_channels, kernel_size,
                                strides, padding, bias=False)
        self.batch_norm = nn.BatchNorm2d(out_channels)
        self.activation = nn.LeakyReLU(alpha, inplace=True)

    def forward(self, X):
        return self.activation(self.batch_norm(self.conv2d(X)))

 

将具有默认设置的基本块将使输入的宽度和高度减半。例如,给定输入形状nh=nw=16,具有内核形状 kh=kw=4,一个大步的形状 sh=sw=2以及填充形状 ph=pw=1,输出形状将为:

 

 

 

x = torch.zeros((2, 3, 16, 16))
d_blk = D_block(20)
d_blk(x).shape

 

torch.Size([2, 20, 8, 8])

鉴别器与生成器对应的。

 

n_D = 64
net_D = nn.Sequential(
    D_block(n_D),  # Output: (64, 32, 32)
    D_block(in_channels=n_D,
            out_channels=n_D * 2),  # Output: (64 * 2, 16, 16)
    D_block(in_channels=n_D * 2,
            out_channels=n_D * 4),  # Output: (64 * 4, 8, 8)
    D_block(in_channels=n_D * 4,
            out_channels=n_D * 8),  # Output: (64 * 8, 4, 4)
    nn.Conv2d(in_channels=n_D * 8, out_channels=1, kernel_size=4,
              bias=False))  # Output: (1, 1, 1)

 

它使用带输出通道的卷积层 11 作为获取单个预测值的最后一层。

 

x = torch.zeros((1, 3, 64, 64))
net_D(x).shape

 

torch.Size([1, 1, 1, 1])

2.4。训练

1节中的基本GAN相比,我们对生成器和鉴别器使用相同的学习率,因为它们彼此相似。另外,改变β1中0.9 至 0.5。它会降低动量的平滑度(过去的梯度的指数加权移动平均值),以照顾快速变化的梯度,因为生成器和鉴别器会相互竞争。此外,随机产生的噪声Z为4-D张量,我们使用GPU来加速计算。

 

def train(net_D, net_G, data_iter, num_epochs, lr, latent_dim,
          device=d2l.try_gpu()):
    loss = nn.BCEWithLogitsLoss(reduction='sum')
    for w in net_D.parameters():
        nn.init.normal_(w, 0, 0.02)
    for w in net_G.parameters():
        nn.init.normal_(w, 0, 0.02)
    net_D, net_G = net_D.to(device), net_G.to(device)
    trainer_hp = {'lr': lr, 'betas': [0.5, 0.999]}
    trainer_D = torch.optim.Adam(net_D.parameters(), **trainer_hp)
    trainer_G = torch.optim.Adam(net_G.parameters(), **trainer_hp)
    animator = d2l.Animator(xlabel='epoch', ylabel='loss',
                            xlim=[1, num_epochs], nrows=2, figsize=(5, 5),
                            legend=['discriminator', 'generator'])
    animator.fig.subplots_adjust(hspace=0.3)
    for epoch in range(1, num_epochs + 1):
        # Train one epoch
        timer = d2l.Timer()
        metric = d2l.Accumulator(3)  # loss_D, loss_G, num_examples
        for X, _ in data_iter:
            batch_size = X.shape[0]
            Z = torch.normal(0, 1, size=(batch_size, latent_dim, 1, 1))
            X, Z = X.to(device), Z.to(device)
            metric.add(d2l.update_D(X, Z, net_D, net_G, loss, trainer_D),
                       d2l.update_G(Z, net_D, net_G, loss, trainer_G),
                       batch_size)
        # Show generated examples
        Z = torch.normal(0, 1, size=(21, latent_dim, 1, 1), device=device)
        # Normalize the synthetic data to N(0, 1)
        fake_x = net_G(Z).permute(0, 2, 3, 1) / 2 + 0.5
        imgs = torch.cat([
            torch.cat([fake_x[i * 7 + j].cpu().detach()
                       for j in range(7)], dim=1)
            for i in range(len(fake_x) // 7)], dim=0)
        animator.axes[1].cla()
        animator.axes[1].imshow(imgs)
        # Show the losses
        loss_D, loss_G = metric[0] / metric[2], metric[1] / metric[2]
        animator.add(epoch, (loss_D, loss_G))
    print(f'loss_D {loss_D:.3f}, loss_G {loss_G:.3f}, '
          f'{metric[2] / timer.stop():.1f} examples/sec on {str(device)}')

 

我们仅以少数几个时期来训练模型,仅用于演示。为了获得更好的性能,num_epochs可以将变量设置为更大的数字。

 

latent_dim, lr, num_epochs = 100, 0.005, 20
train(net_D, net_G, data_iter, num_epochs, lr, latent_dim)

loss_D 0.020, loss_G 8.420, 1082.3 examples/sec on cuda:0


 

2.5。概括

  • DCGAN体系结构具有四个用于鉴别器的卷积层和四个用于生成器的“小跨度”卷积层。
  • 鉴别器是具有批归一化(输入层除外)和泄漏ReLU激活的4层跨卷积。
  • 泄漏的ReLU是一个非线性函数,为负输入提供非零输出。它旨在解决“垂死的ReLU”问题,并帮助渐变在整个体系结构中更轻松地流动。

2.6。练习题

  1. 如果我们使用标准的ReLU激活而不是泄漏的ReLU,将会发生什么?
  2. 将DCGAN应用于Fashion-MNIST,看看哪个类别有效,哪个类别无效。