只需130 行代碼,用 GAN 生成二維樣本的小例子

2020-12-11 雷鋒網

雷鋒網(公眾號:雷鋒網)按:此前雷鋒網曾編譯了一篇英文教程,詳細介紹了如何基於 PyTorch 平臺用 50 行代碼實現 GAN(生成對抗網絡),詳情參見:《GAN 很複雜?如何用不到 50 行代碼訓練 GAN》。近期,針對文中介紹的「50 行代碼 GAN 模型」,有開發者指出了局限性,並基於此模型給出了改進版本,也就是本文將要介紹的「130 行代碼實現 GAN 二維樣本」。本文原載於知乎專欄,作者達聞西,雷鋒網經授權發布。

50行GAN代碼的問題

Dev Nag 寫的 50 行代碼的 GAN,大概是網上流傳最廣的,關於GAN最簡單的小例子。這是一份用一維均勻樣本作為特徵空間(latent space)樣本,經過生成網絡變換後,生成高斯分布樣本的代碼。結構非常清晰,卻有一個奇怪的問題,就是判別器(Discriminator)的輸入不是2維樣本,而是把整個mini-batch整體作為一個維度是batch size(代碼中batch size等於cardinality)那麼大的樣本。也就是說判別網絡要判別的不是一個一維的目標分布,而是batch size那麼大維度的分布:

...

d_input_size = 100   # Minibatch size - cardinality of distributions

...

class Discriminator(nn.Module):

    def __init__(self, input_size, hidden_size, output_size):

        super(Discriminator, self).__init__()

        self.map1 = nn.Linear(input_size, hidden_size)

        self.map2 = nn.Linear(hidden_size, hidden_size)

        self.map3 = nn.Linear(hidden_size, output_size)


    def forward(self, x):

        x = F.elu(self.map1(x))

        x = F.elu(self.map2(x))

        return F.sigmoid(self.map3(x))

...

D = Discriminator(input_size=d_input_func(d_input_size), hidden_size=d_hidden_size, output_size=d_output_size)

...

for epoch in range(num_epochs):

    for d_index in range(d_steps):

        # 1. Train D on real+fake

        D.zero_grad()


        #  1A: Train D on real

        d_real_data = Variable(d_sampler(d_input_size))

        d_real_decision = D(preprocess(d_real_data))

        d_real_error = criterion(d_real_decision, Variable(torch.ones(1)))  # ones = true

        d_real_error.backward()  # compute/store gradients, but don't change params


        #  1B: Train D on fake

        d_gen_input = Variable(gi_sampler(minibatch_size, g_input_size))

        d_fake_data = G(d_gen_input).detach()  # detach to avoid training G on these labels

        d_fake_decision = D(preprocess(d_fake_data.t()))

        d_fake_error = criterion(d_fake_decision, Variable(torch.zeros(1)))  # zeros = fake

        d_fake_error.backward()

        d_optimizer.step()  # Only optimizes D's parameters; changes based on stored gradients from backward()


    for g_index in range(g_steps):

        # 2. Train G on D's response (but DO NOT train D on these labels)

        G.zero_grad()


        gen_input = Variable(gi_sampler(minibatch_size, g_input_size))

        g_fake_data = G(gen_input)

        dg_fake_decision = D(preprocess(g_fake_data.t()))

        g_error = criterion(dg_fake_decision, Variable(torch.ones(1)))  # we want to fool, so pretend it's all genuine


        g_error.backward()

        g_optimizer.step()  # Only optimizes G's parameters


...

不知作者是疏忽了還是有意為之,總之這麼做的結果就是如此簡單的例子收斂都好。可能作者自己也察覺了收斂問題,就想把方差信息也放進來,於是又寫了個預處理函數(decorate_with_diffs)計算出每個樣本距離一批樣本中心的距離平方,作為給判別網絡的額外輸入,其實這樣還增加了輸入維度。結果當然是加不加這個方差信息都能勉強收斂,但是都不穩定。甚至作者自己貼出來的生成樣本分布(下圖)都不令人滿意:

如果直接把這份代碼改成二維的,就會發現除了簡單的對稱分布以外,其他分布基本都無法生成。

理論上講神經網絡作為一種通用的近似函數,只要capacity夠,學習多少維分布都不成問題,但是這樣寫法顯然極大增加了收斂難度。更自然的做法應該是:判別網絡只接受單個二維樣本,通過batch size或是多步迭代學習分布信息。

另:這份代碼其實有130行。

從自定義的二維分布採樣

不管怎樣Dev Nag的代碼還是提供了一個用於理解和試驗GAN的很好的框架,做一些修改就可以得到一份更適合直觀演示,且更容易收斂的代碼,也就是本文的例子。

從可視化的角度二維顯然比一維更直觀,所以我們採用二維樣本。第一步,當然是要設定一個目標分布,作為二維的例子,分布的定義方式應該儘量自由,這個例子中我們的思路是通過灰度圖像定義的概率密度,進而來產生樣本,比如下面這樣:

二維情況下,這種採樣的一個實現方法是:求一個維度上的邊緣(marginal)概率+另一維度上近似的條件概率。比如把圖像中白色像素的值作為概率密度的相對大小,然後沿著x求和,然後在y軸上求出marginal probability density,接著再根據y的位置,近似得到對應x關於y的條件概率。採樣的時候先採y的值,再採x的值就能近似得到符合圖像描述的分布的樣本。具體細節就不展開講解了,看代碼:

from functools import partial

import numpy

from skimage import transform


EPS = 1e-6

RESOLUTION = 0.001

num_grids = int(1/RESOLUTION+0.5)


def generate_lut(img):

    """

    linear approximation of CDF & marginal

    :param density_img:

    :return: lut_y, lut_x

    """

    density_img = transform.resize(img, (num_grids, num_grids))

    x_accumlation = numpy.sum(density_img, axis=1)

    sum_xy = numpy.sum(x_accumlation)

    y_cdf_of_accumulated_x = [[0., 0.]]

    accumulated = 0

    for ir, i in enumerate(range(num_grids-1, -1, -1)):

        accumulated += x_accumlation[i]

        if accumulated == 0:

            y_cdf_of_accumulated_x[0][0] = float(ir+1)/float(num_grids)

        elif EPS < accumulated < sum_xy - EPS:

            y_cdf_of_accumulated_x.append([float(ir+1)/float(num_grids), accumulated/sum_xy])

        else:

            break

    y_cdf_of_accumulated_x.append([float(ir+1)/float(num_grids), 1.])

    y_cdf_of_accumulated_x = numpy.array(y_cdf_of_accumulated_x)


    x_cdfs = []

    for j in range(num_grids):

        x_freq = density_img[num_grids-j-1]

        sum_x = numpy.sum(x_freq)

        x_cdf = [[0., 0.]]

        accumulated = 0

        for i in range(num_grids):

            accumulated += x_freq[i]

            if accumulated == 0:

                x_cdf[0][0] = float(i+1) / float(num_grids)

            elif EPS < accumulated < sum_xy - EPS:

                x_cdf.append([float(i+1)/float(num_grids), accumulated/sum_x])

            else:

                break

        x_cdf.append([float(i+1)/float(num_grids), 1.])

        if accumulated > EPS:

            x_cdf = numpy.array(x_cdf)

            x_cdfs.append(x_cdf)

        else:

            x_cdfs.append(None)


    y_lut = partial(numpy.interp, xp=y_cdf_of_accumulated_x[:, 1], fp=y_cdf_of_accumulated_x[:, 0])

    x_luts = [partial(numpy.interp, xp=x_cdfs[i][:, 1], fp=x_cdfs[i][:, 0]) if x_cdfs[i] is not None else None for i in range(num_grids)]


    return y_lut, x_luts


def sample_2d(lut, N):

    y_lut, x_luts = lut

    u_rv = numpy.random.random((N, 2))

    samples = numpy.zeros(u_rv.shape)

    for i, (x, y) in enumerate(u_rv):

        ys = y_lut(y)

        x_bin = int(ys/RESOLUTION)

        xs = x_luts[x_bin](x)

        samples[i][0] = xs

        samples[i][1] = ys


    return samples


if __name__ == '__main__':

    from skimage import io

    density_img = io.imread('batman.jpg', True)

    lut_2d = generate_lut(density_img)

    samples = sample_2d(lut_2d, 10000)


    from matplotlib import pyplot

    fig, (ax0, ax1) = pyplot.subplots(ncols=2, figsize=(9, 4))

    fig.canvas.set_window_title('Test 2D Sampling')

    ax0.imshow(density_img, cmap='gray')

    ax0.xaxis.set_major_locator(pyplot.NullLocator())

    ax0.yaxis.set_major_locator(pyplot.NullLocator())


    ax1.axis('equal')

    ax1.axis([0, 1, 0, 1])

    ax1.plot(samples[:, 0], samples[:, 1], 'k,')

    pyplot.show()

二維GAN的小例子

雖然網上到處都有,這裡還是貼一下GAN的公式:

就是一個你追我趕的零和博弈,這在Dev Nag的代碼裡體現得很清晰:判別網絡訓一撥,然後生成網絡訓一撥,不斷往復。按照上節所述,本文例子在Dev Nag代碼的基礎上,把判別網絡每次接受一個batch作為輸入的方式變成了:每次接受一個二維樣本,通過每個batch的多個樣本計算loss。GAN部分的訓練代碼如下:

DIMENSION = 2


...


generator = SimpleMLP(input_size=z_dim, hidden_size=args.g_hidden_size, output_size=DIMENSION)

discriminator = SimpleMLP(input_size=DIMENSION, hidden_size=args.d_hidden_size, output_size=1)


...


for train_iter in range(args.iterations):

    for d_index in range(args.d_steps):

        # 1. Train D on real+fake

        discriminator.zero_grad()


        #  1A: Train D on real

        real_samples = sample_2d(lut_2d, bs)

        d_real_data = Variable(torch.Tensor(real_samples))

        d_real_decision = discriminator(d_real_data)

        labels = Variable(torch.ones(bs))

        d_real_loss = criterion(d_real_decision, labels)  # ones = true


        #  1B: Train D on fake

        latent_samples = torch.randn(bs, z_dim)

        d_gen_input = Variable(latent_samples)

        d_fake_data = generator(d_gen_input).detach()  # detach to avoid training G on these labels

        d_fake_decision = discriminator(d_fake_data)

        labels = Variable(torch.zeros(bs))

        d_fake_loss = criterion(d_fake_decision, labels)  # zeros = fake


        d_loss = d_real_loss + d_fake_loss

        d_loss.backward()


        d_optimizer.step()     # Only optimizes D's parameters; changes based on stored gradients from backward()


    for g_index in range(args.g_steps):

        # 2. Train G on D's response (but DO NOT train D on these labels)

        generator.zero_grad()


        latent_samples = torch.randn(bs, z_dim)

        g_gen_input = Variable(latent_samples)

        g_fake_data = generator(g_gen_input)

        g_fake_decision = discriminator(g_fake_data)

        labels = Variable(torch.ones(bs))

        g_loss = criterion(g_fake_decision, labels)  # we want to fool, so pretend it's all genuine


        g_loss.backward()

        g_optimizer.step()  # Only optimizes G's parameters


    ...


...

和Dev Nag的版本比起來除了上面提到的判別網絡,和樣本維度的修改,還加了可視化方便直觀演示和理解,比如用一個二維高斯分布產生一個折線形狀的分布,執行:

python gan_demo.py inputs/zig.jpg

訓練過程的可視化如下:

更多可視化例子可以參考如下連結:

http://t.cn/Ro8aNJz

Conditional GAN

對於一些複雜的分布,原始的GAN就會很吃力,比如用一個二維高斯分布產生兩坨圓形的分布:

因為latent space的分布就是一坨二維的樣本,所以即使模型有很強的非線性,也難以把這個分布「切開」並變換成兩個很好的圓形分布。因此在上面的動圖裡能看到生成的兩坨樣本中間總是有一些殘存的樣本,像是兩個天體在交換物質。要改進這種情況,比較直接的想法是增加模型複雜度,或是提高latent space維度。也許模型可以學習到用其中部分維度產生一個圓形,用另一部分維度產生另一個圓形。不過我自己試了下,效果都不好。

其實這個例子人眼一看就知道是兩個分布在一個圖裡,假設我們已經知道這個信息,那麼生成依據的就是個條件概率。把這個條件加到GAN裡,就是Conditional GAN,公式如下:

示意圖如下:

條件信息變相降低了生成樣本的難度,所以生成的樣本效果好很多。

在網絡中加入條件的方式沒有固定的原則,這裡我們採用的是可能最常見的方法:用one-hot方式將條件編碼成一個向量,然後和原始的輸入拼一下。注意對於判別網絡和生成網絡都要這麼做,所以上面公式和C-GAN原文簡化過度的公式比起來多了兩個y,避免造成迷惑。

C-GAN的代碼實現就是GAN的版本基礎上,利用pytorch的torch.cat()對條件和輸入進行拼接。其中條件的輸入就是多張圖片,每張定義一部分分布的PDF。比如對於上面兩坨分布的例子,就拆成兩張圖像來定義PDF:

具體實現就不貼這裡了,參考本文的Github頁面:

http://t.cn/Ro8Svq4

加入條件信息後,兩坨分布的生成就輕鬆搞定了,執行:

python cgan_demo.py inputs/binary

得到下面的訓練過程可視化:

對於一些更複雜的分布也不在話下,比如:

這兩個圖案對應的原始GAN和C-GAN的訓練可視化對比可以在這裡看到。

應用樣例

其實現在能見到的基於 GAN 的有意思應用基本都是 Conditional GAN,下篇打算介紹基於 C-GAN 的一個實(dan)用(teng)例子:

提高駕駛技術:用GAN去除(愛情)動作片中的馬賽克和衣服

本文完整代碼

http://t.cn/Ro8Svq4

雷鋒網版權文章,未經授權禁止轉載。詳情見轉載須知。

相關焦點

  • 不到200 行代碼,教你如何用 Keras 搭建生成對抗網絡(GAN)
    通常,我們會用下面這個例子來說明 GAN 的原理:將警察視為判別器,製造假幣的犯罪分子視為生成器。一開始,犯罪分子會首先向警察展示一張假幣。警察識別出該假幣,並向犯罪分子反饋哪些地方是假的。接著,根據警察的反饋,犯罪分子改進工藝,製作一張更逼真的假幣給警方檢查。這時警方再反饋,犯罪分子再改進工藝。不斷重複這一過程,直到警察識別不出真假,那麼模型就訓練成功了。
  • 經典GAN實戰教程:理解並運行自己的GAN生成手寫數字
    判別網絡的輸入則為真實樣本或生成網絡的輸出,其目的是將生成網絡的輸出從真實樣本中儘可能分辨出來。而生成網絡則要儘可能地欺騙判別網絡。兩個網絡相互對抗、不斷調整參數,最終目的是使判別網絡無法判斷生成網絡的輸出結果是否真實。一句話解釋:G要騙過A。
  • GAN快速入門資料推薦:17種變體的Keras開原始碼,附相關論文
    通過自己動手、探索模型代碼來學習,當然是墜吼的~如果用簡單易上手的Keras框架,那就更贊了。一位GitHub群眾eriklindernoren就發布了17種GAN的Keras實現,得到Keras親爸爸Franois Chollet在Twitter上的熱情推薦。
  • 【專知薈萃11】GAN生成式對抗網絡知識資料全集(理論/報告/教程/綜述/代碼等)
    今天專知為大家呈送第十一篇專知主題薈萃-生成式對抗網絡GAN知識資料大全集薈萃 (理論/報告/教程/綜述/代碼等),請大家查看!/soumith/ganhacks]OpenAI生成模型參考連結:[https://blog.openai.com/generative-models/]用Keras實現MNIST生成對抗模型參考連結:[https://oshearesearch.com/index.PHP/2016/07/01/mnist-generative-adversarial-model-in-keras
  • GAN(生成對抗網絡)萬字長文綜述
    對於生成器 G 來說,為了儘可能欺騙 D,所以需要最大化生成樣本的判別概率 D(G(z)),即最小化 log(1-D(G(z))),注意:log(D(x)) 一項與生成器 G 無關,所以可以忽略。實際訓練時,生成器和判別器採取交替訓練,即先訓練 D,然後訓練 G,不斷往復。值得注意的是,對於生成器,其最小化的是
  • 萬字綜述之生成對抗網絡(GAN)
    WGAN 也可以用最優傳輸理論來解釋,WGAN 的生成器等價於求解最優傳輸映射,判別器等價於計算 Wasserstein 距離,即最優傳輸總代價 [4]。關於 WGAN 的理論推導和解釋比較複雜,不過代碼實現非常簡單。
  • 資源 | NIPS 2017 Spotlight論文Bayesian GAN的TensorFlow實現
    項目連結:https://github.com/andrewgordonwilson/bayesgan/論文:Bayesian GAN論文連結:https://arxiv.org/abs/1705.09558摘要:生成對抗網絡(GAN)可以隱性地學習難以用顯性似然
  • 輕鬆構建 PyTorch 生成對抗網絡(GAN)
    如上圖所示,訓練用數據將來自 Amazon S3 的存儲桶;訓練用的框架和託管算法以容器鏡像的形式提供服務,在訓練時與代碼結合;模型代碼運行在 Amazon SageMaker 託管的計算實例中,在訓練時與數據結合;訓練輸出物將進入
  • 【前沿】NIPS2017貝葉斯生成對抗網絡TensorFlow實現(附GAN資料下載)
    導讀今年五月份康奈爾大學的 Andrew Gordon Wilson 和 Permutation Venture 的 Yunus Saatchi 提出了一個貝葉斯生成對抗網絡(Bayesian GAN),結合貝葉斯和對抗生成網絡,提出了一個實用的貝葉斯公式框架,用GAN來進行無監督學習和半監督式學習。
  • 帶你用4行代碼訓練RNN生成文本(附資源)
    本文介紹僅需幾行代碼就能訓練出任意大小和複雜度的文本的神經網絡文本發生器。如何在無需構建和調整神經網絡的情況下,輕鬆地生成文本?讓我們來看看下面這個項目,它允許您用幾行代碼就能在任意文本數據集上輕鬆地訓練出任意大小和複雜度的文本,這便是神奇的神經網絡文本發生器。
  • NVIDIA新作解讀:用GAN生成前所未有的高清圖像(附PyTorch復現) | PaperDaily #15
    從行文可以看出文章是臨時趕出來的,畢竟這麼大的實驗,用 P100 都要跑 20 天,更不用說調參時間了,不過人家在 NVIDIA,不缺卡。作者放出了基於 Lasagna 的代碼,今天我也會簡單解讀一下代碼。另外,我也在用 PyTorch 做復現。 在 PG-GAN 出來以前,訓練高解析度圖像生成的 GAN 方法主要就是 LAPGAN[2] 和 BEGAN[6]。
  • 十個生成模型(GANs)的最佳案例和原理 | 代碼+論文
    一個網絡稱為生成器(generator),能將噪聲作為輸入並生成樣本;另一個網絡稱為鑑別器(discriminator),能接收生成器數據和真實訓練數據,訓練得到能正確區分數據類型的分類器。這兩個網絡互相對抗。其中,生成器通過學習來產生更逼近真實數據的新樣本,用於愚弄鑑別器,反之鑑別器也需要更好地區分生成數據與真實數據。
  • Python random模塊詳解,指定分布類型的隨機樣本生成
    choice#在np.arange(5)中均勻抽取10個整數,返回一維數組#在np.arange(5)中均勻抽取10個整數,返回二維數組#在np.arange(5)中均勻抽取10個整數,返回二維數組#生成[0,1)中的均勻隨機浮點數,返回3行4列的二維數組
  • 能生成Deepfake也能診斷癌症,GAN與惡的距離
    事實證明這並不複雜,只需使網絡對某些層使用完全相同的權重。在我認為(可能不太謙虛),關於CoGAN最酷的事情不是提高圖像生成質量,也不是你可以在多個圖像域中進行訓練的事實。而是,事實上,你獲得兩張圖片的價格僅是之前的四分之三。
  • 科普 | ​生成對抗網絡(GAN)的發展史
    經過數千次迭代後,如果一切順利,生成器網絡可以完美生成逼真的虛假圖像,並且鑑別器網絡可以很好地判斷的圖像是真實的還是虛假的。換句話說,生成器網絡將來自潛在空間的隨機噪聲矢量(不是來自潛在空間的所有GAN樣本)變換為真實數據集的樣本。GAN的訓練是一個非常直觀的過程。GAN具有大量的實際用例,如圖像生成,藝術品生成,音樂生成和視頻生成。
  • 深度 | 生成對抗網絡初學入門:一文讀懂GAN的基本原理(附資源)
    在這篇文章中,我們將對生成對抗網絡(GAN)背後的一般思想進行全面的介紹,並向你展示一些主要的架構以幫你很好地開始學習,另外我們還將提供一些有用的技巧,可以幫你顯著改善你的結果。GAN 的發明生成模型的基本思想是輸入一個訓練樣本集合,然後形成這些樣本的概率分布的表徵。常用的生成模型方法是直接推斷其概率密度函數。
  • GAN生成式對抗網絡及應用詳解
    生成器 G(Z)從一個隨機噪聲 Z 中生成一個輸入並訓練自己騙過鑑別器 D,使之認為其生成的輸入都是真實的。因此訓練鑑別器 D(Y)的目標是使鑑別器 D(Y)最大化來自真實數據分布的圖像,並最小化不是來自真實數據分布的圖像。所以生成器 G 和鑑別器 D 在玩一個對立的遊戲:名為對抗訓練。
  • GAN(生成對抗網絡)的最新應用狀況
    G 的 loss 包含 content loss 部分,因此 G 並非完全的非監督,它也用到了監督信息:它強制要求生成圖像提取的特徵與真實圖像提取的特徵要匹配,文中用到的特徵提取網絡為 VGG,content loss 定義如下:
  • VAE、GAN、Info-GAN:全解深度學習三大生成模型
    而生成式模型解決這個問題就十分簡單,首先確定好z的取值,然後根據p(X|z)的分布進行隨機採樣就行了。 了解了兩種模型的不同,下面就來看看生成式模型的建模方法。接下來將用一段代碼展示這其中的道理。這個例子可以很清楚地看出KL不對稱性帶來的一些小問題。而JS具有對稱性,所以第二個實驗和第三個實驗的JS散度實際上是距離相等的分布組。從這個小例子我們可以看出,有時KL散度下降的程度和兩個分布靠近的程度不成比例,而JS散度靠近的程度更令人滿意,這也是GAN模型的一大優勢。
  • 在TensorFlow中對比兩大生成模型:VAE與GAN
    判別器用於對「真」圖像和「偽」圖像進行分類,生成器從隨機噪聲中生成圖像(隨機噪聲通常叫作本徵向量或代碼,該噪聲通常從均勻分布(uniform distribution)或高斯分布中獲取)。生成器的任務是生成可以以假亂真的圖像,令判別器也無法區分出來。也就是說,生成器和判別器是互相對抗的。