生成對抗網絡介紹(附TensorFlow代碼)

生成對抗網絡介紹(附TensorFlow代碼)

最近在生成模型方面研究非常熱門(例如,參見OpenAI博客文章)。這些模型可以學習創建與我們提供的數據類似的數據。這個背後的直覺是,如果我們能夠得到一個模型來寫出高質量的新聞文章,那麼一定也會從中學到很多關於新聞的文章。換句話說,這個模型也應該有一個很好的新聞報道的內部表現形式。然後我們可以用這個表示來幫助我們完成其他相關的任務,比如按主題對新聞文章進行分類。

實際上,培養模型來創建這樣的數據並不容易,但近年來一些方法開始工作得很好。一種這樣的有希望的方法是使用生成對抗網絡(GAN)。 Facebook上最着名的深度學習研究員和AI研究主任Yann LeCun最近引用GANs作爲深度學習最重要的新發展之一:

“There are many interesting recent development in deep learning…The most important one, in my opinion, is adversarial training (also called GAN for Generative Adversarial Networks). This, and the variations that are now being proposed is the most interesting idea in the last 10 years in ML, in my opinion.” – Yann LeCun

本文的其餘部分將更詳細地描述GAN公式,並提供一個使用GAN來解決問題的簡單示例(帶有TensorFlow中的代碼)。

Discriminative vs. Generative models

在研究GAN之前,讓我們簡要回顧一下生成模型(generative model)和判別模型(discriminative model)的區別:

  • 判別模型:學習將輸入數據(x)映射到某個期望輸出類別標籤(y)的函數。用概率術語,他們直接學習條件分佈P(y | x)。
  • 生成模型:試圖同時學習輸入數據和標籤的聯合概率,即P(x,y)。這可以通過貝葉斯規則轉換爲P(y | x)進行分類,但是生成能力也可以用於其他方面,例如創建可能的新(x,y)樣本。

兩種類型的模型都是有用的,但是生成模型比歧視模型有一個有趣的優勢 - 即使沒有標籤,他們也有可能理解和解釋輸入數據的基本結構。當處理現實世界中的數據建模問題時,這是非常可取的,因爲未標記的數據當然是豐富的,但獲得標記的數據往往是昂貴的。

Generative Adversarial Networks

GAN是一個有趣的想法,由Ian Goodfellow(現在在OpenAI)領導的蒙特利爾大學的一組研究人員於2014年首次提出。 GAN背後的主要思想是有兩個競爭的神經網絡模型。 一個將噪聲作爲輸入並生成樣本(所以稱爲生成器)。 另一個模型(稱爲鑑別器)從發生器和訓練數據中接收樣本,並且必須能夠區分這兩個源。 這兩個網絡連續遊戲,發生器正在學習產生越來越多的現實樣本,而鑑別器正在學習如何更好地區分生成的數據和真實的數據。 這兩個網絡是同時訓練的,希望這個競爭將使生成的樣本與真實的數據無法區分。

在這裏插入圖片描述

這裏經常用到的比喻是,“生成器”就像一個試圖製造僞造材料的僞造者,“鑑別器”就像是警方試圖檢測僞造的物品。這種說法也似乎有點讓人想起強化學習,其中“生成器”接收來自“鑑別器”的獎勵信號,從而知道所產生的數據是否準確。然而,與GAN的主要區別在於,我們可以將梯度信息從“鑑別器”反向傳播回“生成器”網絡,因此“生成器”知道如何調整其參數以產生可以欺騙“鑑別器”的輸出數據。

到目前爲止,GAN主要應用於圖像建模。他們現在在圖像生成任務中產生了優異的結果,生成的圖像比那些“基於最大似然訓練目標方法訓練”的圖像要清楚得多。以下是由GAN生成的圖像的一些示例:

在這裏插入圖片描述
Generated bedrooms. Source: “Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks” https://arxiv.org/abs/1511.06434v2
在這裏插入圖片描述

Approximating a 1D Gaussian distribution

爲了更好地理解這一切是如何工作的,我們將使用GAN來解決TensorFlow中的玩具問題 - 學習逼近一維高斯分佈。 這是基於Eric Jang的一個類似目標的博客文章。 我們演示的完整源代碼可以在Github上找到(https://github.com/AYLIEN/gan-intro),在這裏我們只關注代碼中一些更有趣的部分。

首先我們創建“真實”的數據分佈,一個簡單的高斯均值爲4,標準差爲0.5。 它有一個樣本函數,用於從分佈中返回給定數量的樣本(按值排序)。

class DataDistribution(object):
    def __init__(self):
        self.mu = 4
        self.sigma = 0.5

    def sample(self, N):
        samples = np.random.normal(self.mu, self.sigma, N)
        samples.sort()
        return samples

我們將嘗試學習的數據分佈如下所示:

在這裏插入圖片描述

我們還定義了發生器輸入噪聲分佈(具有類似的採樣函數)。 以Eric Jang爲例,我們還對發生器輸入噪聲採取了分層採樣的方法 - 首先將樣本均勻地在指定的範圍內生成,然後隨機擾動。

class GeneratorDistribution(object):
    def __init__(self, range):
        self.range = range

    def sample(self, N):
        return np.linspace(-self.range, self.range, N) + np.random.random(N) * 0.01

我們的生成器鑑別器網絡非常簡單。生成器是先通過線性變換,再非線性(softplus函數)變換,然後是另一個線性變換。

def generator(input, hidden_size):
    h0 = tf.nn.softplus(linear(input, hidden_size, 'g0'))
    h1 = linear(h0, 1, 'g1')
    return h1

在這種情況下,我們發現確保“鑑別器”比“生成器”更強大是很重要的,否則它就沒有足夠的能力來學習能夠準確地區分生成的樣本和真實的樣本。 所以我們做了一個更深的神經網絡,有更多的維度。 除了最後一層以外,所有層都使用tanh非線性,這是一個sigmoid(我們可以將其解釋爲一個概率的輸出)。

def discriminator(input, hidden_size):
    h0 = tf.tanh(linear(input, hidden_size * 2, 'd0'))
    h1 = tf.tanh(linear(h0, hidden_size * 2, 'd1'))
    h2 = tf.tanh(linear(h1, hidden_size * 2, 'd2'))
    h3 = tf.sigmoid(linear(h2, 1, 'd3'))
    return h3

然後,我們可以在TensorFlow圖形中將這些部分連接在一起。 我們還定義了每個網絡的損失函數,“生成器”的目標是欺騙“鑑別器”。

with tf.variable_scope('G'):
    z = tf.placeholder(tf.float32, shape=(None, 1))
    G = generator(z, hidden_size)

with tf.variable_scope('D') as scope:
    x = tf.placeholder(tf.float32, shape=(None, 1))
    D1 = discriminator(x, hidden_size)
    scope.reuse_variables()
    D2 = discriminator(G, hidden_size)

loss_d = tf.reduce_mean(-tf.log(D1) - tf.log(1 - D2))
loss_g = tf.reduce_mean(-tf.log(D2))

我們使用TensorFlow中普通的GradientDescentOptimizer爲每個網絡創建優化器(指數學習速率衰減)。我們也應該注意到,在這裏的優化參數確實需要一些調整。

def optimizer(loss, var_list):
    initial_learning_rate = 0.005
    decay = 0.95
    num_decay_steps = 150
    batch = tf.Variable(0)
    learning_rate = tf.train.exponential_decay(
        initial_learning_rate,
        batch,
        num_decay_steps,
        decay,
        staircase=True
    )
    optimizer = GradientDescentOptimizer(learning_rate).minimize(
        loss,
        global_step=batch,
        var_list=var_list
    )
    return optimizer

vars = tf.trainable_variables()
d_params = [v for v in vars if v.name.startswith('D/')]
g_params = [v for v in vars if v.name.startswith('G/')]

opt_d = optimizer(loss_d, d_params)
opt_g = optimizer(loss_g, g_params)

爲了訓練模型,我們從數據分佈和噪聲分佈中抽取樣本,並優化鑑別器和生成器的參數。

with tf.Session() as session:
    tf.initialize_all_variables().run()

    for step in xrange(num_steps):
        # update discriminator
        x = data.sample(batch_size)
        z = gen.sample(batch_size)
        session.run([loss_d, opt_d], {
            x: np.reshape(x, (batch_size, 1)),
            z: np.reshape(z, (batch_size, 1))
        })

        # update generator
        z = gen.sample(batch_size)
        session.run([loss_g, opt_g], {
            z: np.reshape(z, (batch_size, 1))
        })

我們可以看到,在訓練過程開始時,生成器產生了與真實數據非常不同的分佈。 它最終學會了相當接近它(在框架750附近),然後收斂到一個集中於輸入分佈均值的較窄分佈。訓練結束後,這兩個分佈如下所示:

在這裏插入圖片描述

這很直觀。 鑑別器正在查看來自真實數據和來自我們的發生器的各個樣本。如果在這個簡單的例子中,生成器只是產生實際數據的平均值,那麼就很可能愚弄鑑別器。

這個問題有很多可能的解決方案。 在這種情況下,我們可以添加某種早期停止標準,當兩個分佈之間的相似度閾值達到時,暫停訓練。 然而,如何把這個問題推廣到更大的問題還不是很清楚,即使在簡單的情況下,也很難保證我們的生成器分佈總是會到達“有意義的早停”地步。 更有吸引力的解決方案是直接通過給予鑑別器一次檢查多個例子的能力來解決問題。

Improving sample diversity

根據最近由Tim Salimans和OpenAI的合作者撰寫的一篇論文,生成器輸出非常窄的點分佈是GAN的“主要失效模式之一”。值得慶幸的是,他們還提出了一個解決方案:允許鑑別者一次看多個樣本,這種技術稱爲“小批量鑑別”(minibatch discrimination)。

在本文中,“小批量辨別”被定義爲:鑑別器能夠查看整批樣品以決定它們是來自生成器還是來自真實數據。他們還提出了一個更具體的算法,該算法通過建模給定樣本與同一批次中的所有其他樣本之間的距離來工作。然後,這些距離與原始樣本結合並通過鑑別器,因此它可以在分類過程中選擇使用距離度量和樣本值。

該方法可以粗略地總結如下:

  • 取出鑑別器中間層的輸出。

  • 將其乘以三維張量以生成矩陣(在下面的代碼中,大小爲num_kernels x kernel_dim)。

  • 計算一個批次中所有樣本矩陣中行間的L1距離,然後應用負指數。

  • 樣本的minibatch特徵是這些指數化距離的總和。

  • 使用新創建的小塊功能將原始輸入連接到小批量圖層(以前的鑑別圖層的輸出),並將其作爲輸入傳遞到鑑別器的下一層。

  • 在TensorFlow中可以翻譯成如下的東西:

      def minibatch(input, num_kernels=5, kernel_dim=3):
          x = linear(input, num_kernels * kernel_dim)
          activation = tf.reshape(x, (-1, num_kernels, kernel_dim))
          diffs = tf.expand_dims(activation, 3) -  tf.expand_dims(tf.transpose(activation, [1, 2, 0]), 0)
          abs_diffs = tf.reduce_sum(tf.abs(diffs), 2)
          minibatch_features = tf.reduce_sum(tf.exp(-abs_diffs), 2)
          return tf.concat(1, [input, minibatch_features])
    

我們實現了提議的“小批量辨別”技術,看看它是否有助於解決我們例子中發生器輸出分佈的崩潰。下面顯示了訓練期間發生器網絡的新行爲。

在這種情況下很明顯,添加小批量區分會使發生器保持原始數據分佈的大部分寬度(儘管它還不完美)。 收斂之後,分佈現在看起來像這樣:

在這裏插入圖片描述

關於“小批量辨別”的最後一點是它使得批量的大小作爲一個超參數變得更加重要。在我們的例子中,我們必須保持批量相當小(小於16左右)以便訓練集中。也許只是限制每個距離度量的樣本數量就足夠了,而不是使用整個批次,但是這個又一個參數調整。

Final thoughts

生成對抗網絡是一個有趣的發展,爲我們提供了一種新的無監督學習方式。 GAN的大部分成功應用都在計算機視覺領域,但在這裏,我們正在研究如何將這些技術應用於自然語言處理。

這方面的一個大問題就是如何最好地評估這些模型。在圖像領域,至少看看生成的樣本是相當容易的,儘管這顯然不是一個令人滿意的解決方案。在文本領域,這一點甚至沒有用處(除非你的目標是產生散文)。對於基於最大似然訓練的生成模型,我們通常可以基於未知的測試數據的可能性(或者可能性的一些下限)產生一些度量,但是這不適用於此。一些GAN論文已經根據生成的樣本的核密度估計產生了似然估計,但是這種技術似乎在高維空間中被破壞了。另一個解決方案是隻評估一些下游任務(如分類)。

最後是完整代碼

'''
An example of distribution approximation using Generative Adversarial Networks
in TensorFlow.

Based on the blog post by Eric Jang:
http://blog.evjang.com/2016/06/generative-adversarial-nets-in.html,

and of course the original GAN paper by Ian Goodfellow et. al.:
https://arxiv.org/abs/1406.2661.

The minibatch discrimination technique is taken from Tim Salimans et. al.:
https://arxiv.org/abs/1606.03498.
'''

import argparse
import numpy as np
import tensorflow as tf
import matplotlib.pyplot as plt
from matplotlib import animation
import seaborn as sns

sns.set(color_codes=True)

seed = 42
np.random.seed(seed)
tf.set_random_seed(seed)


class DataDistribution(object):
    def __init__(self):
        self.mu = 4
        self.sigma = 0.5

    def sample(self, N):
        samples = np.random.normal(self.mu, self.sigma, N)
        samples.sort()
        return samples


class GeneratorDistribution(object):
    def __init__(self, range):
        self.range = range

    def sample(self, N):
        return np.linspace(-self.range, self.range, N) + \
            np.random.random(N) * 0.01


def linear(input, output_dim, scope=None, stddev=1.0):
    with tf.variable_scope(scope or 'linear'):
        w = tf.get_variable(
            'w',
            [input.get_shape()[1], output_dim],
            initializer=tf.random_normal_initializer(stddev=stddev)
        )
        b = tf.get_variable(
            'b',
            [output_dim],
            initializer=tf.constant_initializer(0.0)
        )
        return tf.matmul(input, w) + b


def generator(input, h_dim):
    h0 = tf.nn.softplus(linear(input, h_dim, 'g0'))
    h1 = linear(h0, 1, 'g1')
    return h1


def discriminator(input, h_dim, minibatch_layer=True):
    h0 = tf.nn.relu(linear(input, h_dim * 2, 'd0'))
    h1 = tf.nn.relu(linear(h0, h_dim * 2, 'd1'))

    # without the minibatch layer, the discriminator needs an additional layer
    # to have enough capacity to separate the two distributions correctly
    if minibatch_layer:
        h2 = minibatch(h1)
    else:
        h2 = tf.nn.relu(linear(h1, h_dim * 2, scope='d2'))

    h3 = tf.sigmoid(linear(h2, 1, scope='d3'))
    return h3


def minibatch(input, num_kernels=5, kernel_dim=3):
    x = linear(input, num_kernels * kernel_dim, scope='minibatch', stddev=0.02)
    activation = tf.reshape(x, (-1, num_kernels, kernel_dim))
    diffs = tf.expand_dims(activation, 3) - \
        tf.expand_dims(tf.transpose(activation, [1, 2, 0]), 0)
    abs_diffs = tf.reduce_sum(tf.abs(diffs), 2)
    minibatch_features = tf.reduce_sum(tf.exp(-abs_diffs), 2)
    return tf.concat([input, minibatch_features], 1)


def optimizer(loss, var_list):
    learning_rate = 0.001
    step = tf.Variable(0, trainable=False)
    optimizer = tf.train.AdamOptimizer(learning_rate).minimize(
        loss,
        global_step=step,
        var_list=var_list
    )
    return optimizer


def log(x):
    '''
    Sometimes discriminiator outputs can reach values close to
    (or even slightly less than) zero due to numerical rounding.
    This just makes sure that we exclude those values so that we don't
    end up with NaNs during optimisation.
    '''
    return tf.log(tf.maximum(x, 1e-5))


class GAN(object):
    def __init__(self, params):
        # This defines the generator network - it takes samples from a noise
        # distribution as input, and passes them through an MLP.
        with tf.variable_scope('G'):
            self.z = tf.placeholder(tf.float32, shape=(params.batch_size, 1))
            self.G = generator(self.z, params.hidden_size)

        # The discriminator tries to tell the difference between samples from
        # the true data distribution (self.x) and the generated samples
        # (self.z).
        #
        # Here we create two copies of the discriminator network
        # that share parameters, as you cannot use the same network with
        # different inputs in TensorFlow.
        self.x = tf.placeholder(tf.float32, shape=(params.batch_size, 1))
        with tf.variable_scope('D'):
            self.D1 = discriminator(
                self.x,
                params.hidden_size,
                params.minibatch
            )
        with tf.variable_scope('D', reuse=True):
            self.D2 = discriminator(
                self.G,
                params.hidden_size,
                params.minibatch
            )

        # Define the loss for discriminator and generator networks
        # (see the original paper for details), and create optimizers for both
        self.loss_d = tf.reduce_mean(-log(self.D1) - log(1 - self.D2))
        self.loss_g = tf.reduce_mean(-log(self.D2))

        vars = tf.trainable_variables()
        self.d_params = [v for v in vars if v.name.startswith('D/')]
        self.g_params = [v for v in vars if v.name.startswith('G/')]

        self.opt_d = optimizer(self.loss_d, self.d_params)
        self.opt_g = optimizer(self.loss_g, self.g_params)


def train(model, data, gen, params):
    anim_frames = []

    with tf.Session() as session:
        tf.local_variables_initializer().run()
        tf.global_variables_initializer().run()

        for step in range(params.num_steps + 1):
            # update discriminator
            x = data.sample(params.batch_size)
            z = gen.sample(params.batch_size)
            loss_d, _, = session.run([model.loss_d, model.opt_d], {
                model.x: np.reshape(x, (params.batch_size, 1)),
                model.z: np.reshape(z, (params.batch_size, 1))
            })

            # update generator
            z = gen.sample(params.batch_size)
            loss_g, _ = session.run([model.loss_g, model.opt_g], {
                model.z: np.reshape(z, (params.batch_size, 1))
            })

            if step % params.log_every == 0:
                print('{}: {:.4f}\t{:.4f}'.format(step, loss_d, loss_g))

            if params.anim_path and (step % params.anim_every == 0):
                anim_frames.append(
                    samples(model, session, data, gen.range, params.batch_size)
                )

        if params.anim_path:
            save_animation(anim_frames, params.anim_path, gen.range)
        else:
            samps = samples(model, session, data, gen.range, params.batch_size)
            plot_distributions(samps, gen.range)


def samples(
    model,
    session,
    data,
    sample_range,
    batch_size,
    num_points=10000,
    num_bins=100
):
    '''
    Return a tuple (db, pd, pg), where db is the current decision
    boundary, pd is a histogram of samples from the data distribution,
    and pg is a histogram of generated samples.
    '''
    xs = np.linspace(-sample_range, sample_range, num_points)
    bins = np.linspace(-sample_range, sample_range, num_bins)

    # decision boundary
    db = np.zeros((num_points, 1))
    for i in range(num_points // batch_size):
        db[batch_size * i:batch_size * (i + 1)] = session.run(
            model.D1,
            {
                model.x: np.reshape(
                    xs[batch_size * i:batch_size * (i + 1)],
                    (batch_size, 1)
                )
            }
        )

    # data distribution
    d = data.sample(num_points)
    pd, _ = np.histogram(d, bins=bins, density=True)

    # generated samples
    zs = np.linspace(-sample_range, sample_range, num_points)
    g = np.zeros((num_points, 1))
    for i in range(num_points // batch_size):
        g[batch_size * i:batch_size * (i + 1)] = session.run(
            model.G,
            {
                model.z: np.reshape(
                    zs[batch_size * i:batch_size * (i + 1)],
                    (batch_size, 1)
                )
            }
        )
    pg, _ = np.histogram(g, bins=bins, density=True)

    return db, pd, pg


def plot_distributions(samps, sample_range):
    db, pd, pg = samps
    db_x = np.linspace(-sample_range, sample_range, len(db))
    p_x = np.linspace(-sample_range, sample_range, len(pd))
    f, ax = plt.subplots(1)
    ax.plot(db_x, db, label='decision boundary')
    ax.set_ylim(0, 1)
    plt.plot(p_x, pd, label='real data')
    plt.plot(p_x, pg, label='generated data')
    plt.title('1D Generative Adversarial Network')
    plt.xlabel('Data values')
    plt.ylabel('Probability density')
    plt.legend()
    plt.show()


def save_animation(anim_frames, anim_path, sample_range):
    f, ax = plt.subplots(figsize=(6, 4))
    f.suptitle('1D Generative Adversarial Network', fontsize=15)
    plt.xlabel('Data values')
    plt.ylabel('Probability density')
    ax.set_xlim(-6, 6)
    ax.set_ylim(0, 1.4)
    line_db, = ax.plot([], [], label='decision boundary')
    line_pd, = ax.plot([], [], label='real data')
    line_pg, = ax.plot([], [], label='generated data')
    frame_number = ax.text(
        0.02,
        0.95,
        '',
        horizontalalignment='left',
        verticalalignment='top',
        transform=ax.transAxes
    )
    ax.legend()

    db, pd, _ = anim_frames[0]
    db_x = np.linspace(-sample_range, sample_range, len(db))
    p_x = np.linspace(-sample_range, sample_range, len(pd))

    def init():
        line_db.set_data([], [])
        line_pd.set_data([], [])
        line_pg.set_data([], [])
        frame_number.set_text('')
        return (line_db, line_pd, line_pg, frame_number)

    def animate(i):
        frame_number.set_text(
            'Frame: {}/{}'.format(i, len(anim_frames))
        )
        db, pd, pg = anim_frames[i]
        line_db.set_data(db_x, db)
        line_pd.set_data(p_x, pd)
        line_pg.set_data(p_x, pg)
        return (line_db, line_pd, line_pg, frame_number)

    anim = animation.FuncAnimation(
        f,
        animate,
        init_func=init,
        frames=len(anim_frames),
        blit=True
    )
    anim.save(anim_path, fps=30, extra_args=['-vcodec', 'libx264'])


def main(args):
    model = GAN(args)
    train(model, DataDistribution(), GeneratorDistribution(range=8), args)


def parse_args():
    parser = argparse.ArgumentParser()
    parser.add_argument('--num-steps', type=int, default=5000,
                        help='the number of training steps to take')
    parser.add_argument('--hidden-size', type=int, default=4,
                        help='MLP hidden size')
    parser.add_argument('--batch-size', type=int, default=8,
                        help='the batch size')
    parser.add_argument('--minibatch', action='store_true',
                        help='use minibatch discrimination')
    parser.add_argument('--log-every', type=int, default=10,
                        help='print loss after this many steps')
    parser.add_argument('--anim-path', type=str, default=None,
                        help='path to the output animation file')
    parser.add_argument('--anim-every', type=int, default=1,
                        help='save every Nth frame for animation')
    return parser.parse_args()


if __name__ == '__main__':
    main(parse_args())

代碼說明:

An introduction to Generative Adversarial Networks

This is the code that we used to generate our GAN 1D Gaussian approximation.
For more information see our blog post: http://blog.aylien.com/introduction-generative-adversarial-networks-code-tensorflow.

Installing dependencies

Written for Python 3.x (tested on 3.6.1).

For the Python dependencies, first install the requirements file:

$ pip install -r requirements.txt

If you want to also generate the animations, you need to also make sure that ffmpeg is installed and on your path.

Training

For a full list of parameters, run:

$ python gan.py --help

To run without minibatch discrimination (and plot the resulting distributions):

$ python gan.py

To run with minibatch discrimination (and plot the resulting distributions):

$ python gan.py --minibatch

requirements.txt文件

matplotlib==1.5.3
numpy==1.11.3
scipy==0.17.0
seaborn==0.7.1
tensorflow==1.2.0

運行前需要安裝
sudo apt-get install python3-tk

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章