关于GAN的一些笔记

目录

1 Divergence

  1.1 Kullback–Leibler divergence

  1.2 Jensen–Shannon divergence

  1.3 Wasserstein distance

2 GAN

  2.1 Theory

  2.2 Algorithm

    Objective function for generator in real implementation

    Code

    运行结果

 

 

1 Divergence

这是一些比较重要的前置知识。

1.1 Kullback–Leibler divergence

假设 P(x),Q(x) 是随机变量 X 的两个分布,在离散和连续随机变量的情形下,KL divergence 分别定义为:

非负性:DKL(PQ) 恒为非负的,且在 P,Q 为同一分布时 DKL(P||Q)=0

不对称性:DKL(PQ)DKL(Q||P)

1.2 Jensen–Shannon divergence

假设 P(x),Q(x) 是随机变量 X 的两个分布,Jensen–Shannon divergence 定义为:

其中 M=12(P+Q)

JS divergence 解决了 KL divergence 不对称性的问题,一般地,JS divergence 是对称的,且取值 0JSD(PQ)log(2),注意这里的 logln

KL divergence 和 JS divergence 有一个同样的问题:如果两个分布 P,Q 完全没有重叠,那么 KL divergence 是没有意义的,而 JS divergence 是一个常数 log(2)

关于两个分布无重叠时 JS divergence 为 log(2) 的证明也很简单:

 

显然对于第一个积分,在 p(x)0 时必然有 q(x)=0,所以第一个积分值为零。对第二个积分也是同理。所以两个分布无重叠时 JS divergence 为 log(2)

1.3 Wasserstein distance

假设 P,Q 是两个概率分布,则 Wasserstein distance 定义为:

其中,γΠ(x,y) 表示 γ 是一个联合分布,而它的边缘分布即 PQ

如果 P,Q 是连续型的概率分布,那么就有 W(P,Q)=infγΠ[P,Q]γ(x,y)d(x,y)dxdyd(x,y)xy 代表 x,y 间的某种距离。

根据《从Wasserstein距离、对偶理论到WGAN》的说法,事实上 γ 描述了一种运输方案。假设 P 是原始分布,Q 是目标分布,p(x) 的意思是原来在位置 x 处有 p(x) 数量的货物,而 q(x) 是指最终 x 处要存放的货物数量,如果某处 xp(x)>q(x),那么就要把 x 处的一部分货物运到别处,反之,如果 p(x)<q(x),那么就要从别的地方运一些货物到 x 处。而 γ(x,y) 的意思是指,要从 x 处搬 γ(x,y) 数量的东西到 y 处。

最后是 inf,表示下确界,简单来说就是取最小,也就是说,要从所有的运输方案中,找出总运输成本 γ(x,y)d(x,y)dxdy 最小的方案,这个方案的成本,就是我们要算的 W(P,Q)。如果将上述比喻中的“货物”换成“沙土”,那么Wasserstein距离就是在求最省力的“搬土”方案了,所以Wasserstein距离也被称为“推土机距离”(Earth Mover's Distance)。更加形象的讲解可以参考李宏毅老师的GAN课程中关于WGAN的那一节。

 

2 GAN

2.1 Theory

We want to find data distribution Pdata(x)x 是一张图片(或者说是a high-dimensional vector), Pdata(x) 是一个固定的分布,而我们的database中的图片,都是来自 Pdata(x) 的一个个sample,如下图

为了方便,图中的 x 是二维空间中的一个点(一个向量)。

如果我们的database是二次元人物头像的,那么就有一个对应的固定的 Pdata(x),database里二次元人物头像图片,就是data points from distribution Pdata(x)。很显然,往往这个分布中高概率的区域只占整个image space的很小很小的一部分。

(假设蓝色区域就是高概率区域,而剩余的就是低概率区域)

 

显然, 我们不可能知道 Pdata(x) 的公式是怎么样的,我们唯一能做的事情就是sample from Pdata(x)

我们能做的事情就是:我们有一个distribution PG(x;θ) parameterized by θ,通过调整参数 θ 使得 PG(x;θ) 接近 Pdata(x)

很自然,我们就能想到maximum likelihood estimation (MLE):

  假设我们有样本 x1,,xm 来自 Pdata(x),那么likelihood function

 

  log-likelihood function为

   那么 

  也就是说,我们用MLE来估计 θ,就约等于在minimize KL divergence。

  (由于 xlog(Pdata(x))Pdata(x)dxθ 无关,所以加上这一项并不影响 argmaxθ

  (关于上面的约等于号怎么来的,参考伯努利大数定律,假设 x 只有 n 个可能的取值,表示成 x(1),x(2),,x(n),当 m 很大时,m 个样本中取值为 x(k) 的样本,其数目占总样本数的比例 count(x(k))m,就约等于 Pdata(x(k)),所以 i=1mf(xi)=k=1ncount(x(k))mf(x(k))k=1nPdata(x(k))f(x(k))=ExPdata[f(x)]。当然,这不是严格证明,这仅仅是我在思考这个约等于号时的一点思路。) 

 

上面这个经典的MLE思路当然是可行的,如果我们可以先确定 PG(x;θ) 的表达式,那么就可以通过MLE求出 θ^,进而得到一个确定的 PG(x;θ^),就可以sample from PG(x;θ^) 来生成图片了,但实际上这样的效果并不好,因为 Pdata 其实是非常复杂的,我们需要更加复杂的 PG 来接近 Pdata

 

我们令 G 是一个mapping,输入一个随机噪声 z,输出一个高维向量(图片) x=G(z),随机噪声 z 可能服从Gaussian distribution,也可能服从uniform distribution,关系不大,但是经过 G 之后,x 就可以服从一个非常复杂的distribution PG

所以有

即寻找一个 G 使得 PdataPG 之间的某种divergence最小。这个divergence可以是KL divergence,也可以是别的divergence。minimize KL divergence只不过是正好近似等价于MLE罢了。

然后问题就来了,由于 Pdata 是不可知的,而且如果mapping G 很复杂的话,那么 PG 其实也是不可知的,所以我们其实没办法直接去算 PdataPG 之间的divergence,这就引出了discriminator的作用。

 

discriminator其实也是一个mapping,记作做 D,输入是一个高维向量(图像)x,输出是一个标量,D 的作用是,分辨输入的图像到底是来自 Pdata,还是来自 PG。我们训练discriminator的做法如下:

Objective function for D

注意,这里的 G 是固定的,也就是说此时对于 D 来说 PG 是固定的。

训练:

 

给定 G,最优的 D 会最大化

我们需要假设 D(x) 是可以是任意函数,那么对于任意的 x1x2D(x1)D(x2) 之间其实没有任何的相互限制,所以可以把每个 x 分开来看待,

所以进一步给定 x,最优的 D 会最大化

a=Pdata(x),b=PG(x),记 f(D)=alog(D)+blog(1D),则令 dfdD 等于 0 得到

 

如果我们绘制 f(D)=0.5log(D)+0.5log(1D) 的图像

其实无论 a,b(0,1) 之间如何变动,该函数始终只有一个最大值,因此上面的方法是可行的。

 

因此,我们找到了 D,将其回代就可以得到

 

因此,我们现在有一种divergence D(Pdata,PG)=2JSD(PdataPG)2log2=maxDV(D,G),所以将这个 D(Pdata,PG)=maxDV(D,G) 代回到 G=argminGD(Pdata,PG) 中即可得

这就是《Generative Adversarial Nets》中式(1)

这里的 pz(z) 是随机噪声所服从的分布。

 

这里的 minGmaxDV(D,G) 似乎有些绕,其实并不难理解。从朴素的思想来看,我要寻找一个最优的 G 使得divergence D(Pdata,PG) 最小,那就枚举所有可能的 G 好了,看看哪个算出来的divergence D(Pdata,PG) 最小不就好了。那么对于一个给定的 G=G,我们不会算 D(Pdata,PG),但是我知道 D(Pdata,PG)=maxDV(D,G),注意此时的 G 是给定的,所以 D(Pdata,PG)=maxDV(D,G)=maxDV(D),这就很简单了,我们也会算了,不就是找一个自变量为 D 的一元函数 V(D) 的最大值嘛,找到这个函数的最大值,这个值就是当 G=G 时的divergence D(Pdata,PG)。然后我们就可以去枚举下一个 G=G 了。

 

2.2 Algorithm

上面我说的那种朴素的枚举所有可能的 G 的思路显然是不可能真的去实现的,求最大值、最小值的一个经典方法就是梯度下降法。

首先记 θG 是 mapping G 的参数,θD 是 mapping D 的参数。

 

上述算法存在的一个问题是,例如,当我找到了 D0 使得 V(D,G0) 取得最大值,但是经过 θGθGηV(D0,G)/θG 更新之后,G 已经变成了 G1V(D,G0) 和 V(D,G1) 这两个自变量为 D 的函数,很可能差别比较大,例如下图

 

那么 V(D,G1) 的最大值很有可能反而比 V(D,G0) 的最大值还要大,换句话说,当我的 GG0 更新到 G1,使得 PdataPG 之间的divergence反而增大了,这些然不是我们想要的情况。因此我们必须使得 θG 的更新尽量小一些,这样 V(D,G1)V(D,G0) 这两个自变量为 D 的函数的形状就会比较相似,就不会出现使得divergence反而增大的情况。

训练 D 是在计算divergence,而训练 G 是在降低divergence。

 

Objective function for generator in real implementation

对于generator的objective function V=ExPG[log(1D(x))],由于在一开始discriminator很容易区分图片的真假,所以它对于 xPG 给出的 D(x) 值是很小的,这就导致 log(1D(x)) 的导数值很小,使得训练速度偏慢。 

所以就把generator的objective function改成了 V=ExPG[log(D(x))]。仅仅是因为两者的趋势是一致的,仅仅是因为斜率不一样,所以作者认为这样是可以的。

两者分别有命名

 

Code

复制代码
import torch
import torchvision
import torch.nn as nn
import torch.nn.functional as F
from torchvision import datasets
from torchvision import transforms
from torchvision.utils import save_image
from torch.autograd import Variable
import os

if not os.path.exists('./img'):
    os.mkdir('./img')


def to_img(x):
    out = 0.5 * (x + 1)
    out = out.clamp(0, 1)
    out = out.view(-1, 1, 28, 28)
    return out


batch_size = 128
num_epoch = 100
z_dimension = 100

# Image processing
img_transform = transforms.Compose([
    transforms.ToTensor(),
    transforms.Normalize(mean=[0.5], std=[0.5])
])
# MNIST dataset
mnist = datasets.MNIST(
    root='./data/', train=True, transform=img_transform, download=True)
# Data loader
dataloader = torch.utils.data.DataLoader(
    dataset=mnist, batch_size=batch_size, shuffle=True)


class Discriminator(nn.Module):
    def __init__(self):
        super(Discriminator, self).__init__()
        self.dis = nn.Sequential(
            nn.Linear(784, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 256),
            nn.LeakyReLU(0.2),
            nn.Linear(256, 1),
            nn.Sigmoid())

    def forward(self, x):
        return self.dis(x)


class Generator(nn.Module):
    def __init__(self):
        super(Generator, self).__init__()
        self.gen = nn.Sequential(
            nn.Linear(100, 256),
            nn.ReLU(True),
            nn.Linear(256, 256),
            nn.ReLU(True),
            nn.Linear(256, 784),
            nn.Tanh())

    def forward(self, x):
        return self.gen(x)


D = Discriminator().cuda()
G = Generator().cuda()

# Binary cross entropy loss and optimizer
criterion = nn.BCELoss()
d_optimizer = torch.optim.Adam(D.parameters(), lr=0.0003)
g_optimizer = torch.optim.Adam(G.parameters(), lr=0.0003)

# Start training
for epoch in range(num_epoch):
    for i, (img, _) in enumerate(dataloader):
        num_img = img.size(0)

        # region Train discriminator

        img = img.view(num_img, -1)
        real_img = img.cuda()
        real_label = torch.ones([num_img, 1]).cuda()
        fake_label = torch.zeros([num_img, 1]).cuda()

        # compute loss of real_img
        real_out = D(real_img)
        d_loss_real = criterion(real_out, real_label)
        real_scores = real_out  # closer to 1 means better

        # compute loss of fake_img
        z = torch.randn(num_img, z_dimension).cuda()
        fake_img = G(z)
        fake_out = D(fake_img)
        d_loss_fake = criterion(fake_out, fake_label)
        fake_scores = fake_out  # closer to 0 means better

        # bp and optimize
        d_loss = d_loss_real + d_loss_fake
        d_optimizer.zero_grad()
        d_loss.backward()
        d_optimizer.step()

        # endregion

        # region train generator

        # compute loss of fake_img
        z = torch.randn(num_img, z_dimension).cuda()
        fake_img = G(z)
        output = D(fake_img)
        g_loss = criterion(output, real_label)

        # bp and optimize
        g_optimizer.zero_grad()
        g_loss.backward()
        g_optimizer.step()

        # endregion

        if (i + 1) % 100 == 0:
            print('Epoch [{}/{}], d_loss: {:.6f}, g_loss: {:.6f}, '
                  'D real: {:.6f}, D fake: {:.6f}.'
                  .format(epoch, num_epoch, d_loss.item(),
                          g_loss.item(), real_scores.data.mean(),
                          fake_scores.data.mean()))

    if epoch == 0:
        real_images = to_img(real_img.cpu().data)
        save_image(real_images, './img/real_images.png')

    fake_images = to_img(fake_img.cpu().data)
    save_image(fake_images, './img/fake_images-{}.png'.format(epoch + 1))
复制代码

 

关于BCELoss,即binary cross entropy loss,计算公式如下:

其中 yi 是真值的第 i 项(注意取值是 0 或者 1),而 y^i 是对应的第 i 项估计值(取值为 [0,1])。而 li 即对应的第 i 项loss值。

而 nn.BCELoss() 中有一个参数 reduction='mean',可以取值为 'mean' 或者 'sum' 或者 'none',默认取值 'mean',分别代表对上面的 li 求均值、求和、不进一步操作。

 

所以上面的代码中,对于 Dloss

所以梯度下降最小化 Dloss 和之前的算法描述(Update discriminator parameters to maximize V~=1mi=1mlogD(xi)+1mi=1mlog(1D(x~i)))是一致的。

而对于代码中的 Gloss

梯度下降这也之前描述的在实际代码实现中用NSGAN而非MMGAN一致。

 

运行结果

这是运行了100 epochs中某几代的生成结果:

  

  

 

posted @   Dilthey  阅读(1175)  评论(0编辑  收藏  举报
编辑推荐:
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
阅读排行:
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
历史上的今天:
2019-02-20 CCPC-Wannafly Winter Camp Day4 Div1 - 咆咆咆哮 - [三分+贪心]
2019-02-20 CCPC-Wannafly Winter Camp Day4 Div1 - 置置置换 - [DP]
点击右上角即可分享
微信分享提示