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

2021-01-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)
    為此,本文將以深度卷積生成對抗網絡(Deep Convolutional GAN,DCGAN)為例,介紹如何基於 Keras 2.0 框架,以 Tensorflow 為後端,在 200 行代碼內搭建一個真實可用的 GAN 模型,並以該模型為基礎自動生成 MNIST 手寫體數字。
  • 生成式對抗網絡GAN的高級議題
    這也是另一種方式使圖像可以純粹從單詞生成。以下是用於執行此文本到圖像條件合成的多條件GAN(MC-GAN)的示例:mc-gan的實現,用於將單詞翻譯成圖像數據擴充GAN像在查看VAE時一樣學習數據生成分布。因此,我們可以從我們的發生器中採樣並生成其他樣本,我們可以使用這些樣本來增強我們的訓練集。
  • 超全的GAN PyTorch+Keras實現集合
    本文介紹了主流的生成對抗網絡及其對應的 PyTorch 和 Keras 實現代碼,希望對各位讀者在 GAN 上的理解與實現有所幫助。在 ImageNet 的 1000 個類別中,128×128 的樣本要比手動調整為 32×32 的樣本高兩倍多的可辨識度。此外,84.7% 的類別具有與 ImageNet 真實圖像相媲美的樣本。
  • GAN快速入門資料推薦:17種變體的Keras開原始碼,附相關論文
    通過自己動手、探索模型代碼來學習,當然是墜吼的~如果用簡單易上手的Keras框架,那就更贊了。一位GitHub群眾eriklindernoren就發布了17種GAN的Keras實現,得到Keras親爸爸Franois Chollet在Twitter上的熱情推薦。
  • GAN(生成對抗網絡)萬字長文綜述
    對於生成器 G 來說,為了儘可能欺騙 D,所以需要最大化生成樣本的判別概率 D(G(z)),即最小化 log(1-D(G(z))),注意:log(D(x)) 一項與生成器 G 無關,所以可以忽略。實際訓練時,生成器和判別器採取交替訓練,即先訓練 D,然後訓練 G,不斷往復。值得注意的是,對於生成器,其最小化的是
  • 萬字綜述之生成對抗網絡(GAN)
    WGAN 也可以用最優傳輸理論來解釋,WGAN 的生成器等價於求解最優傳輸映射,判別器等價於計算 Wasserstein 距離,即最優傳輸總代價 [4]。關於 WGAN 的理論推導和解釋比較複雜,不過代碼實現非常簡單。
  • 十個生成模型(GANs)的最佳案例和原理 | 代碼+論文
    一個網絡稱為生成器(generator),能將噪聲作為輸入並生成樣本;另一個網絡稱為鑑別器(discriminator),能接收生成器數據和真實訓練數據,訓練得到能正確區分數據類型的分類器。這兩個網絡互相對抗。其中,生成器通過學習來產生更逼近真實數據的新樣本,用於愚弄鑑別器,反之鑑別器也需要更好地區分生成數據與真實數據。
  • 谷歌推出新框架:只需5行代碼,就能提高模型準確度和魯棒性
    NSL有什麼用?過去我們使用單獨的圖片來訓練計算機視覺神經網絡,這些訓練樣本之間彼此是孤立的,然而樣本之間包含著豐富的關係信息。如果用上這些數據的結構化信息,就能實現更高的模型精度,或者用更少的樣本來訓練模型,特別是在標記樣本數量相對較少的情況。
  • 用Keras搭建GAN:圖像去模糊中的應用(附代碼)
    所有的Keras代碼可點擊這裡。可點擊查看原始出版文章和Pytorch實現。快速回憶生成對抗網絡GAN中兩個網絡的訓練相互競爭。我們唯一的準則就是看判別器是否接受生成器的合成的例子。這些只是對生成對抗網絡的一個簡單回顧,如果還是不夠明白的話,可以參考完整介紹。數據Ian Goodfellow首次使用GAN模型是生成MNIST數據。
  • GAN版馬裡奧創作家來了:一個樣本即可訓練,生成關卡要素豐富
    現在,漢諾瓦大學的研究人員推出了一個新的GAN,能夠生成船新、可玩的超級馬裡奧關卡。畫風完美統一,難點出其不意:並且,僅需要一個示例,就可以進行訓練。比起我這樣的馬裡奧亂造家,看上去有邏輯多了。並且,他們已經把代碼開源啦。
  • 歷時6 年發展,GAN 領域當下最熱門的「弄潮兒」都有哪些?
    生成式模型的任務是從有限的訓練數據中總結出這個概率,將這個概率學習成一個「公式」、一個「模型」或者一種「模式」。當它學習到這個樣本的分布特點後,模型就能夠據此源源不斷地生成新的偽數據了。有些童鞋可能會問這些偽數據有什麼用呢?例如在影視作品中,每個群眾演員都是很貴的,只要露臉了他的薪酬可能就需要翻番。
  • 用GAN來做圖像生成,這是最好的方法
    前言在我們之前的文章中,我們學習了如何構造一個簡單的 GAN 來生成 MNIST 手寫圖片。對於圖像問題,卷積神經網絡相比於簡單地全連接的神經網絡更具優勢,因此,我們這一節我們將繼續深入 GAN,通過融合卷積神經網絡來對我們的 GAN 進行改進,實現一個深度卷積 GAN。如果還沒有親手實踐過 GAN 的小夥伴可以先去學習一下上一篇專欄:生成對抗網絡(GAN)之 MNIST 數據生成。
  • 先融合再填充,上海交大提出少樣本圖像生成新方法F2GAN
    少樣本圖像生成(few-shot image generation)任務是指用已知類別(seen category)的大量圖片訓練出一個生成模型,然後給定某個未知類別(unseen category)的少量圖片,即可為該未知類別生成大量真實且多樣的圖片。少樣本圖像生成屬於圖像數據增廣的範疇,可用來輔助很多下遊任務,如少樣本圖像分類等。
  • 程式設計師的樂趣,生成自定義二維碼,5行Python代碼就搞定
    生成二維碼的工具也層出不窮,但多數需要在線完成,並且生成的圖案也千篇一律,過於單調。那麼有沒有辦法實現自定義生成二維碼呢?近日,一位熱衷於終身學習的工程師兼攝影師 Arindom Bhattacharjee 撰寫了一篇自定義生成二維碼的方法,並且整個生成過程只需要 5 行 Python 代碼即可完成。感興趣的讀者可以自己實現下。
  • 生成Python函數一半沒問題,當前最「正統」的代碼生成是什麼樣?
    機器之心原創參與:思源大家都說深度神經網絡能力很強,那麼從函數注釋生成函數代碼,以及從函數代碼總結函數注釋這種最基礎的代碼任務到底能不能行?像 Python、Java 這樣的通用高級語言,到底在代碼生成上能達到什麼水平?本文介紹的就是這樣一篇北大前沿研究。開發者寫代碼,和數學家寫公式一樣是非常自然的一件事。
  • 如何使用GAN做一個禿頭生產器
    生成對抗網絡介紹  說到圖像生成,就必須說到GAN,它是一種非監督學習的方式,通過讓兩個神經網絡相互博弈的方法進行學習,該方法由lan Goodfellow等人在2014年提出。生成對抗網絡由一個生成網絡和一個判別網絡組成,生成網絡從潛在的空間(latent space)中隨機採樣作為輸入,其輸出結果需要儘量模仿訓練集中的真實樣本。
  • 5分鐘入門GANS:原理解釋和keras代碼實現
    GANs,用於生成圖像而不需要很少或沒有輸入。GANs允許我們生成由神經網絡生成的圖像。在我們深入討論這個理論之前,我想向您展示GANs構建您興奮感的能力。把馬變成斑馬(反之亦然)。歷史生成式對抗網絡(GANs)是由Ian Goodfellow (GANs的GAN Father)等人於2014年在其題為「生成式對抗網絡」的論文中提出的。
  • 一篇文章教你用11行Python代碼實現神經網絡
    聲明:本文是根據英文教程 (用 11 行 Python 代碼實現的神經網絡)學習總結而來,關於更詳細的神經網絡的介紹可以參考我的另一篇博客:。A Neural Network in 11 lines of Python從感知機到人工神經網絡如果你讀懂了下面的文章,你會對神經網絡有更深刻的認識,有任何問題,請多指教。
  • 提高駕駛技術:用GAN去除(愛情)動作片中的馬賽克和衣服
    這篇就介紹利用生成式對抗網絡(GAN)的兩個基本駕駛技能:1) 去除(愛情)動作片中的馬賽克2) 給(愛情)動作片中的女孩穿(tuo)衣服生成式模型上一篇《用GAN生成二維樣本的小例子》中已經簡單介紹了GAN,這篇再簡要回顧一下生成式模型,算是補全一個來龍去脈。
  • 40行Python代碼,實現卷積特徵可視化
    本文將向你解釋如何僅使用 40 行 Python 代碼將卷積特徵可視化。最近在閱讀 Jeremy Rifkin 的書《The End of Work》時,我讀到一個有趣的關於 AI 的定義。在本文中我將向你解釋如何僅用 40 行 Python 代碼來實現隨機圖像的像素值優化(如下圖),從而生成卷積神經網絡的特徵可視化。