如何从零开始实施GANs

118 阅读8分钟

从零开始实施GANs

当识别现有数据中的模式时,机器学习算法的效果非常好。然后,这种洞察力可以被用于分类和回归的目的。然而,当被要求生成新的数据时,机器一直在挣扎;2014年,当伊恩-古德费罗(Ian Goodfellow)将生成式对抗网络(GANs)的想法引入机器学习的世界时,这一切都发生了改变。

生成式建模的力量使得GANs扩展了神经网络的能力。这种技术使机器能够生成真实的数据,这些数据似是而非地来自于现有的样本分布。

这篇博客将介绍GANs的基础知识,并使用PyTorch库实现一个GANs。

前提条件

尽管我已经尽量使本文自成一体,但仍然需要有机器学习的基本知识。此外,请读者参考PyTorch库的文档。PyTorch是基于Python的开源机器学习库,我们将用它来实现GAN。

主要收获

在本博客结束时,你将理解以下概念。

  • 生成式建模的介绍。
  • 什么是GANs?
  • GANs是如何工作的?
  • GAN架构。
  • 如何训练一个GAN。
  • 使用PyTorch从头开始实现一个GAN。

简介

"生成对抗网络是过去10年中ML领域最有趣的想法"

Yann LeCun,Facebook的首席人工智能科学家

GAN中的 "生成 "一词表明其唯一目的,即生成新数据。GANs生成的数据取决于我们在哪个数据集上训练了我们的GAN。

另一方面,对抗性这个词指的是生成器和判别器之间类似游戏的竞争,我们将在博客的后半部分了解到这一点。

在开始讨论GANs之前,了解生成模型是很有必要的,因为它是GAN概念的基础。

什么是生成式建模

为了解释生成式建模,我将以一个场景为例,我们已经得到了一个机器学习模型,该模型将给定的图像分类为猫或狗的图像。

现在,如果我们想把这个过程倒过来,也就是说,我们已经得到了关于狗的特征,我们想让我们的模型通过使用给定的特征来生成一个狗的图像,会怎么样?这就是生成式模型的最简单的形式。

为了从数学上解释这个问题,让我们假设我们有一个数据集,其中有xx实例和相应的yy标签。机器学习中主要有两种类型的建模。

  1. 判别性建模。该模型试图捕捉条件概率,p(yx)p(y|x)
  2. 生成式建模。如果是监督学习,该模型试图捕捉联合概率p(x,y)p(x,y),否则p(x)p(x)

在生成模型中,分布p(x)p(x)被学习并表示在一个潜在空间zz中。潜伏空间是xx的隐藏表示。此外,生成模型使用这个潜伏空间作为灵感来源来生成xx'。为了解释为什么zz被称为灵感之源,我将举一个人类思维的例子。

当我们把人的思想作为生成模型时,人的思想也做了类似的事情。例如,让我们考虑这样一个场景:我们试图向一个一生中从未见过狗的人解释狗是什么。我们将通过给出狗的所有必要特征(x)(x)来描述狗在人眼中的样子。然后,一个人的大脑会把这些信息(p(x))(p(x))作为编码信息储存在他们的头脑中(z)(z)

现在,使用(z)(z)作为灵感的来源,人的大脑将试图重新创造一只狗会是什么样子(x)(x')。注意,这个人的大脑不需要在他们的大脑中生成一个狗的确切形象。这将完全取决于你给(x)(x)的信息和它的建模。

GANs简介

GANs是一类ML技术,由两个同时训练的神经网络组成。一个是用于生成假数据的生成器,另一个是鉴别器,用于对给它的输入是真的还是假的进行分类。

想象一下这样一个场景:一个小偷想从博物馆偷一幅画,并用一幅假画代替,而有一个馆长的工作是检测这幅画是真的还是假的。小偷和馆长之间会有一场竞争,他们两个人都会有一种敌对关系。在我们的案例中,小偷是发电机,而馆长是鉴别者

GAN架构

architecture

GAN的架构由以下部分组成。

  1. 训练数据集(x)(x)。我们训练鉴别器网络的真实数据样本,以区分真实和虚假数据。你想产生的数据类型取决于你的训练数据集。
  2. 随机噪声(z)(z):它是我们生成器网络的一个起点(原始输入)。这个噪声在生成器的帮助下被转化为假数据。
  3. 生成器网络。它是一个将随机噪声(z)(z)作为输入并产生假数据(x)(x')作为输出的神经网络。它的目标是生成不能被鉴别器网络检测为假数据的数据。
  4. 鉴别器网络。它也是一个神经网络,将训练数据集中的数据(x)(x)和假数据(x)(x')作为输入,并将它们分类为真实或假的。
  5. 迭代训练。由于GAN由两个神经网络组成,训练阶段同时训练发生器和鉴别器。我们将在下一部分中详细介绍这一点。

训练一个GAN

如前所述,GAN由两个不同的神经网络组成,即判别器和生成器,它们同时被训练。鉴别器是直接在真实和生成的数据上训练的,而生成器是间接训练的。它是通过鉴别器网络训练的。让我们看看这是如何发生的。

训练生成器

  • 从我们的训练数据集中取一个随机数据样本xx,标记为真实数据
  • 同样,取一个由生成器网络生成的假数据样本xx',标记为假数据

这两个输入都通过鉴别器神经网络,计算出分类误差。接下来,反向传播总误差以更新判别器网络的权重和偏置,寻求分类误差最小化

训练鉴别器

  • 以随机噪声向量zz作为输入,用生成器网络生成一个假的样本$x'。
  • xx'作为输入传给鉴别器网络以计算分类误差。
  • 通过反推这个分类误差来更新生成器网络的权重和偏置,以寻求判别器的误差最大化
训练何时收敛?

为了解释GAN是如何训练的,首先,我将讨论博弈论中的一个设置,即零和博弈,这是一个双人游戏,其中一个玩家提高了一定的数量,另一个玩家恶化了同样的数量。所有的零和博弈都有一个平衡点,在这个平衡点上,双方都不能改善自己的状况。

这个点被称为纳什均衡。我们在GAN的案例中也有类似的情况。在GAN中,一个判别器与一个生成器竞争,与零和游戏的两个玩家一样。同样地,GAN的训练在达到纳什均衡时也会收敛。

在GAN中实现纳什均衡的时间是:

  • 鉴别器以0.5美元的概率对某一特定数据进行真假分类。
  • 生成器产生的假数据与真实数据没有区别。

在实践中,由于GAN的训练复杂性,几乎不可能在训练时实现纳什均衡,这意味着GAN的训练实际上永远不会收敛。然而,如果仔细训练,甚至不收敛,GAN也能产生令人满意的结果。

实施

为了实现GAN,我们将使用标准的MNIST数据集。在这个数据集上训练完GAN后,我们将尝试在MNIST数据集的基础上生成新数据。

首先,如果你在你的本地机器上工作,请安装PyTorch库。如果你在Google Colab中工作,你不需要担心这个问题,因为PyTorch已经安装了。

#importing essential libraries
import torch
import torch.optim as optim
import torch.nn as nn
from torch.utils import data
import torchvision

import matplotlib.pyplot as plt
import numpy as np

如果你的机器上有GPU,设备将自动选择为Cuda;否则,将是CPU。我们还在下面的单元格中设置了训练所需的超参数。

#setting up the hyperparameters
learning_rate = 2e-4
noise_dim = 32
image_dim = 28 * 28 * 1 
batch_size = 32
num_epochs = 25

生成器网络

在下面的单元格中,我们定义了Generator类,它继承自nn.Module类。我们将发电机模型定义为一个仅由两个隐藏层组成的神经网络。

随机噪声zz和图像的尺寸被作为参数传递给我们类的构造函数。然后,它将随机噪声作为网络的输入。

class Generator(nn.Module):
    def __init__(self, noise_dim, image_dim):
        super(Generator,self).__init__()
        self.linear1 = nn.Linear(noise_dim, 128)
        self.relu = nn.LeakyReLU(0.01)
        self.linear2 = nn.Linear(128, image_dim)
        self.tanh = nn.Tanh()

    def forward(self, x):
        out = self.linear1(x)
        out = self.relu(out)
        out = self.linear2(out)
        out = self.tanh(out)
        return out

鉴别器网络

在下面的单元格中,我们将判别器网络定义为一个继承自PyTorch的nn.Module class 。它又是一个简单的两层网络,就像发电机一样。首先,它将图像作为输入。

注意,最后一层的激活函数是sigmoid,这意味着网络的输出在0和1之间。1代表MNIST数据集中的真实图像,0代表Generator网络生成的假图像。

class Discriminator(nn.Module):
    def __init__(self, in_image):
        super(Discriminator,self).__init__()
        self.linear1 = nn.Linear(in_image, 64)
        self.relu = nn.LeakyReLU(0.01)
        self.linear2 = nn.Linear(64, 1)
        self.sigmoid = nn.Sigmoid()

    def forward(self, x):
        out = self.linear1(x)
        out = self.relu(out)
        out = self.linear2(out)
        out = self.sigmoid(out)
        return out

加载数据

我们不需要明确地下载MNIST数据集,PyTorch库的数据集类将为我们下载和转换MNIST数据集。

在转换中,我们需要在将图像送入神经网络之前对其进行归一化处理,并将其转换为张量。现在我们将使用DataLoader类来加载数据,以馈送给神经网络。

discriminator = Discriminator(image_dim)
generator = Generator(noise_dim, image_dim)
noise = torch.randn((batch_size, noise_dim))
tf = torchvision.transforms.Compose(
    [torchvision.transforms.ToTensor(),torchvision.transforms.Normalize((0.5,), (0.5,)),]
)
ds = torchvision.datasets.MNIST(root="dataset/", transform=tf, download=True)
loader = data.DataLoader(ds, batch_size=batch_size, shuffle=True)
#Visualizing real data
real_sample = iter(loader).next()[0]
img_grid_real = torchvision.utils.make_grid(real_sample, normalize=True)
npgrid = img_grid_real.cpu().numpy()
plt.imshow(np.transpose(npgrid, (1, 2, 0)), interpolation='nearest')
plt.axis('off')

snapshot

#setting up the optimizers
opt_discriminator = optim.Adam(discriminator.parameters(), lr=learning_rate)
opt_generator = optim.Adam(generator.parameters(), lr=learning_rate)
criterion = nn.BCELoss()

训练阶段

正如我们之前在训练GANs时讨论的那样,同样的原理在PyTorch库的帮助下以编程方式实现。

注意,我们正在反向传播损失D,这是损失D_real损失D_fake的平均值,以更新判别器网络的权重和偏差。

for epoch in range(num_epochs):
    for id, (training_sample, _) in enumerate(loader):
        training_sample = training_sample.view(-1, 784)
        batch_size = training_sample.shape[0]

        ### Training the Discriminator
        noise = torch.randn(batch_size, noise_dim)
        fake_sample = generator(noise)
        disc_realSample = discriminator(training_sample).view(-1)
        lossD_realSample = criterion(disc_realSample, torch.ones_like(disc_realSample))
        disc_fakeSample = discriminator(fake_sample).view(-1)
        lossD_fakeSample = criterion(disc_fakeSample, torch.zeros_like(disc_fakeSample)) 
        lossD = (lossD_realSample + lossD_fakeSample) / 2
        discriminator.zero_grad()
        #we are trying to minimize the total classification error
        lossD.backward(retain_graph=True)
        opt_discriminator.step()

        ### Training the Generator
        lossD_fakeSample = discriminator(fake_sample).view(-1)
        lossG = criterion(lossD_fakeSample, torch.ones_like(lossD_fakeSample))#we are trying to maximize the error of classification of fake image by the discriminator
        generator.zero_grad()
        lossG.backward()
        opt_generator.step()

        if id == 0:
            print( "Epoch: {epoch} \t Discriminator Loss: {lossD} Generator Loss: {lossG}".format( epoch=epoch, lossD=lossD, lossG=lossG))

生成假数据

现在我们的训练已经完成,让我们试着生成一些新的数据。我们使用fixed_noise来生成新数据,从而将其传递给我们的生成器网络。

由于我们的批处理量是32美元,所以生成器网络将返回32美元的图像。我们正在使用torchvision.utils的make_grid来显示所有的图像。

fake = gen(fixed_noise).reshape(-1, 1, 28, 28)
img_grid_fake = torchvision.utils.make_grid(fake, normalize=True)
npgrid = img_grid_fake.cpu().numpy()
lt.imshow(np.transpose(npgrid, (1, 2, 0)), interpolation='nearest')
plt.axis('off')

snapshot

总结

我们在这个博客中实现了一个GAN,基于MNIST数据集使用随机噪声生成了新的数据。需要注意的一点是,我们不得不处理MNIST数据。

在这里,分布是相当简单的,并由生成器建模,但如果我们的数据是非常复杂的,并且有多模型分布,那该怎么办。这仍然是GANs的一个问题,它限制了GANs产生多样化结果的能力。