關於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 分別定義為:

非負性:$D_{KL} (P \parallel Q)$ 恆為非負的,且在 $P,Q$ 為同一分布時 $D_{KL} (P||Q) = 0$。

不對稱性:$D_{KL} (P \parallel Q) \neq D_{KL} (Q||P)$。

1.2 Jensen–Shannon divergence

假設 $P(x), Q(x)$ 是隨機變量 $X$ 的兩個分布,Jensen–Shannon divergence 定義為:

其中 $M={\frac {1}{2}}(P+Q)$。

JS divergence 解決了 KL divergence 不對稱性的問題,一般地,JS divergence 是對稱的,且取值 $0\leq {\rm {JSD}}(P\parallel Q)\leq \log(2)$,注意這里的 $\log$ 即 $\ln$。

KL divergence 和 JS divergence 有一個同樣的問題:如果兩個分布 $P,Q$ 完全沒有重疊,那么 KL divergence 是沒有意義的,而 JS divergence 是一個常數 $\log (2)$。

關於兩個分布無重疊時 JS divergence 為 $\log (2)$ 的證明也很簡單:

 

顯然對於第一個積分,在 $p(x) \neq 0$ 時必然有 $q(x) = 0$,所以第一個積分值為零。對第二個積分也是同理。所以兩個分布無重疊時 JS divergence 為 $\log (2)$。

1.3 Wasserstein distance

假設 $P, Q$ 是兩個概率分布,則 Wasserstein distance 定義為:

其中,$\gamma \in \Pi(x, y)$ 表示 $\gamma$ 是一個聯合分布,而它的邊緣分布即 $P$ 和 $Q$。

如果 $P, Q$ 是連續型的概率分布,那么就有 $W(P,Q) = \inf_{\gamma \in \Pi[P,Q]} \iint \gamma(x,y)d(x,y)dxdy$。$d(x,y)$ 即 $\left \| x-y \right \|$ 代表 $x,y$ 間的某種距離。

根據《從Wasserstein距離、對偶理論到WGAN》的說法,事實上 $\gamma$ 描述了一種運輸方案。假設 $P$ 是原始分布,$Q$ 是目標分布,$p(x)$ 的意思是原來在位置 $x$ 處有 $p(x)$ 數量的貨物,而 $q(x)$ 是指最終 $x$ 處要存放的貨物數量,如果某處 $x$ 的 $p(x)>q(x)$,那么就要把 $x$ 處的一部分貨物運到別處,反之,如果 $p(x)<q(x)$,那么就要從別的地方運一些貨物到 $x$ 處。而 $\gamma(x,y)$ 的意思是指,要從 $x$ 處搬 $γ(x,y)$ 數量的東西到 $y$ 處。

最后是 $\inf$,表示下確界,簡單來說就是取最小,也就是說,要從所有的運輸方案中,找出總運輸成本 $\iint \gamma(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 $P_{data}(x)$,$x$ 是一張圖片(或者說是a high-dimensional vector), $P_{data}(x)$ 是一個固定的分布,而我們的database中的圖片,都是來自 $P_{data}(x)$ 的一個個sample,如下圖

為了方便,圖中的 $x$ 是二維空間中的一個點(一個向量)。

如果我們的database是二次元人物頭像的,那么就有一個對應的固定的 $P_{data}(x)$,database里二次元人物頭像圖片,就是data points from distribution $P_{data}(x)$。很顯然,往往這個分布中高概率的區域只占整個image space的很小很小的一部分。

(假設藍色區域就是高概率區域,而剩余的就是低概率區域)

 

顯然, 我們不可能知道 $P_{data}(x)$ 的公式是怎么樣的,我們唯一能做的事情就是sample from $P_{data}(x)$。

我們能做的事情就是:我們有一個distribution $P_G (x;\theta)$ parameterized by $\theta$,通過調整參數 $\theta$ 使得 $P_G (x;\theta)$ 接近 $P_{data}(x)$。

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

  假設我們有樣本 ${x_1, \cdots, x_m}$ 來自 $P_{data}(x)$,那么likelihood function

 

  log-likelihood function為

   那么 

  也就是說,我們用MLE來估計 $\theta$,就約等於在minimize KL divergence。

  (由於 $\int_{x} \log(P_{data}(x)) \cdot P_{data}(x) dx$ 與 $\theta$ 無關,所以加上這一項並不影響 $\arg\max\limits_{\theta}$)

  (關於上面的約等於號怎么來的,參考伯努利大數定律,假設 $x$ 只有 $n$ 個可能的取值,表示成 $x^{(1)}, x^{(2)}, \cdots, x^{(n)}$,當 $m$ 很大時,$m$ 個樣本中取值為 $x^{(k)}$ 的樣本,其數目占總樣本數的比例 $\frac{count(x^{(k)})}{m}$,就約等於 $P_{data}(x^{(k)})$,所以 $\sum_{i=1}^{m} f(x_i) = \sum_{k=1}^{n} \frac{count(x^{(k)})}{m} f(x^{(k)}) \approx \sum_{k=1}^{n} P_{data}(x^{(k)}) f(x^{(k)}) = E_{x \sim P_{data}}[f(x)]$。當然,這不是嚴格證明,這僅僅是我在思考這個約等於號時的一點思路。) 

 

上面這個經典的MLE思路當然是可行的,如果我們可以先確定 $P_G(x;\theta)$ 的表達式,那么就可以通過MLE求出 $\hat{\theta}$,進而得到一個確定的 $P_G(x;\hat{\theta})$,就可以sample from $P_G(x;\hat{\theta})$ 來生成圖片了,但實際上這樣的效果並不好,因為 $P_{data}$ 其實是非常復雜的,我們需要更加復雜的 $P_G$ 來接近 $P_{data}$。

 

我們令 $G$ 是一個mapping,輸入一個隨機噪聲 $z$,輸出一個高維向量(圖片) $x = G(z)$,隨機噪聲 $z$ 可能服從Gaussian distribution,也可能服從uniform distribution,關系不大,但是經過 $G$ 之后,$x$ 就可以服從一個非常復雜的distribution $P_{G}$。

所以有

即尋找一個 $G^{*}$ 使得 $P_{data}$ 和 $P_G$ 之間的某種divergence最小。這個divergence可以是KL divergence,也可以是別的divergence。minimize KL divergence只不過是正好近似等價於MLE罷了。

然后問題就來了,由於 $P_{data}$ 是不可知的,而且如果mapping $G$ 很復雜的話,那么 $P_{G}$ 其實也是不可知的,所以我們其實沒辦法直接去算 $P_{data}$ 和 $P_G$ 之間的divergence,這就引出了discriminator的作用。

 

discriminator其實也是一個mapping,記作做 $D$,輸入是一個高維向量(圖像)$x$,輸出是一個標量,$D$ 的作用是,分辨輸入的圖像到底是來自 $P_{data}$,還是來自 $P_G$。我們訓練discriminator的做法如下:

Objective function for $D$:

注意,這里的 $G$ 是固定的,也就是說此時對於 $D$ 來說 $P_G$ 是固定的。

訓練:

 

給定 $G$,最優的 $D^{*}$ 會最大化

我們需要假設 $D(x)$ 是可以是任意函數,那么對於任意的 $x_1 \neq x_2$,$D(x_1)$ 和 $D(x_2)$ 之間其實沒有任何的相互限制,所以可以把每個 $x$ 分開來看待,

所以進一步給定 $x$,最優的 $D^{*}$ 會最大化

記 $a = P_{data}(x), b = P_G(x)$,記 $f(D) = a \log(D) + b \log(1-D)$,則令 $\frac{df}{dD}$ 等於 $0$ 得到

 

如果我們繪制 $f(D) = 0.5 \log(D) + 0.5 \log(1-D)$ 的圖像

其實無論 $a,b$ 在 $(0,1)$ 之間如何變動,該函數始終只有一個最大值,因此上面的方法是可行的。

 

因此,我們找到了 $D^{*}$,將其回代就可以得到

 

因此,我們現在有一種divergence $D(P_{data}, P_{G}) = 2JSD(P_{data} \parallel P_{G}) - 2\log2 = \max\limits_{D}V(D,G)$,所以將這個 $D(P_{data}, P_{G}) = \max\limits_{D}V(D,G)$ 代回到 $G^{*} = \arg\min\limits_{G} D(P_{data}, P_{G})$ 中即可得

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

這里的 $p_{z}(z)$ 是隨機噪聲所服從的分布。

 

這里的 $\min\limits_{G}\max\limits_{D}V(D,G)$ 似乎有些繞,其實並不難理解。從朴素的思想來看,我要尋找一個最優的 $G^{*}$ 使得divergence $D(P_{data}, P_{G})$ 最小,那就枚舉所有可能的 $G$ 好了,看看哪個算出來的divergence $D(P_{data}, P_{G})$ 最小不就好了。那么對於一個給定的 $G = G'$,我們不會算 $D(P_{data}, P_{G})$,但是我知道 $D(P_{data}, P_{G}) = \max\limits_{D}V(D,G)$,注意此時的 $G'$ 是給定的,所以 $D(P_{data}, P_{G}) = \max\limits_{D}V(D,G') = \max\limits_{D}V(D)$,這就很簡單了,我們也會算了,不就是找一個自變量為 $D$ 的一元函數 $V(D)$ 的最大值嘛,找到這個函數的最大值,這個值就是當 $G = G'$ 時的divergence $D(P_{data}, P_{G})$。然后我們就可以去枚舉下一個 $G = G''$ 了。

 

2.2 Algorithm

上面我說的那種朴素的枚舉所有可能的 $G$ 的思路顯然是不可能真的去實現的,求最大值、最小值的一個經典方法就是梯度下降法。

首先記 $\theta_{G}$ 是 mapping $G$ 的參數,$\theta_{D}$ 是 mapping $D$ 的參數。

 

上述算法存在的一個問題是,例如,當我找到了 $D_{0}^{*}$ 使得 $V(D,G_0)$ 取得最大值,但是經過 $\theta_G \leftarrow \theta_G - \eta \cdot \partial V(D_{0}^{*},G) / \partial \theta_G$ 更新之后,$G$ 已經變成了 $G_1$,$V(D,G_0)$ 和 $V(D,G_1)$ 這兩個自變量為 $D$ 的函數,很可能差別比較大,例如下圖

 

那么 $V(D,G_1)$ 的最大值很有可能反而比 $V(D,G_0)$ 的最大值還要大,換句話說,當我的 $G$ 從 $G_0$ 更新到 $G_1$,使得 $P_{data}$ 和 $P_G$ 之間的divergence反而增大了,這些然不是我們想要的情況。因此我們必須使得 $\theta_G$ 的更新盡量小一些,這樣 $V(D,G_1)$ 和 $V(D,G_0)$ 這兩個自變量為 $D$ 的函數的形狀就會比較相似,就不會出現使得divergence反而增大的情況。

訓練 $D$ 是在計算divergence,而訓練 $G$ 是在降低divergence。

 

Objective function for generator in real implementation

對於generator的objective function $V = E_{x \sim P_G}[\log(1-D(x))]$,由於在一開始discriminator很容易區分圖片的真假,所以它對於 $x \sim P_G$ 給出的 $D(x)$ 值是很小的,這就導致 $log(1-D(x))$ 的導數值很小,使得訓練速度偏慢。 

所以就把generator的objective function改成了 $V = E_{x \sim P_G}[-\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,計算公式如下:

其中 $y_i$ 是真值的第 $i$ 項(注意取值是 $0$ 或者 $1$),而 $\hat{y}_i$ 是對應的第 $i$ 項估計值(取值為 $[0,1]$)。而 $l_i$ 即對應的第 $i$ 項loss值。

而 nn.BCELoss() 中有一個參數 reduction='mean',可以取值為 'mean' 或者 'sum' 或者 'none',默認取值 'mean',分別代表對上面的 $l_i$ 求均值、求和、不進一步操作。

 

所以上面的代碼中,對於 $Dloss$ 有

所以梯度下降最小化 $Dloss$ 和之前的算法描述(Update discriminator parameters to maximize $\widetilde{V} = \frac{1}{m} \sum_{i=1}^{m}\log D(x^i) + \frac{1}{m} \sum_{i=1}^{m}\log(1-D(\widetilde{x}^i))$)是一致的。

而對於代碼中的 $Gloss$ 有

梯度下降這也之前描述的在實際代碼實現中用NSGAN而非MMGAN一致。

 

運行結果

這是運行了100 epochs中某幾代的生成結果:

  

  

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM