GAN和VAE作为最近非常火的生成模型, 其特点在于

  1. 可以学习数据的分布.
  2. 无监督学习.

颜乐存说过,2016年深度学习领域最让他兴奋技术莫过于对抗学习。对抗学习确实是解决非监督学习的一个有效方法,而无监督学习一直都是人工智能领域研究者所孜孜追求的“终极目标”之一。

Sounds simple enough, but why do we care about these networks? As Yann LeCun stated in his Quora post, the discriminator now is aware of the “internal representation of the data” because it has been trained to understand the differences between real images from the dataset and artificially created ones. Thus, it can be used as a feature extractor that you can use in a CNN. Plus, you can just create really cool artificial images that look pretty natural to me (link).

GAN的基本原理其实非常简单,这里以生成图片为例进行说明。假设我们有两个网络,G(Generator)和D(Discriminator)。正如它的名字所暗示的那样,它们的功能分别是:

  • G是一个生成图片的网络,它接收一个随机的噪声z,通过这个噪声生成图片,记做G(z)。

  • D是一个判别网络,判别一张图片是不是“真实的”。它的输入参数是x,x代表一张图片,输出D(x)代表x为真实图片的概率,如果为1,就代表100%是真实的图片,而输出为0,就代表不可能是真实的图片。

在训练过程中,生成网络G的目标就是尽量生成真实的图片去欺骗判别网络D。而D的目标就是尽量把G生成的图片和真实的图片分别开来。这样,G和D构成了一个动态的“博弈过程”。

最后博弈的结果是什么?在最理想的状态下,G可以生成足以“以假乱真”的图片G(z)。对于D来说,它难以判定G生成的图片究竟是不是真实的,因此D(G(z)) = 0.5。

这样我们的目的就达成了:我们得到了一个生成式的模型G,它可以用来生成图片。

以上只是大致说了一下GAN的核心原理,如何用数学语言描述呢?这里直接摘录论文里的公式:

这实际上就是个Binary cross entropy损失.(BCE), 可以把这个看做一个min-max问题.

GANs

算法

训练算法如下:

实际上就是两个部分, 首先训练D, 然后训练G, 这里D的训练次数要比G的次数要多.

对D的训练过程如下

  1. 取真实的数据和G伪造的数据给D判别
  2. 根据损失进行SGD更新参数.

对G的训练过程如下

  1. G生成数据给D判别.
  2. 根据D的判别结果计算损失, 以此为根据利用SGD更新G的参数.

DCGAN

进一步就可以引入DCGAN, 实际上就是用两个卷积网络来作为G和D的模型. 同时这里的玩过的结构有一点改变, 以提高样本的质量和收敛的速度.

  1. 取消所有pooling层。G网络中使用转置卷积(transposed convolutional layer)进行上采样,D网络中用加入stride的卷积代替pooling。
  2. 在D和G中均使用batch normalization
  3. 去掉FC层,使网络变为全卷积网络
  4. G网络中使用ReLU作为激活函数,最后一层使用tanh
  5. D网络中使用LeakyReLU作为激活函数

DCGAN的G网络如下

实现

代码

import numpy as np
import torch
import torch.nn as nn
import torch.nn.functional as F
import torch.optim as optim
from torch.autograd import Variable
import pandas as pd
import matplotlib as mpl
import matplotlib.pyplot as plt
import seaborn as sns
%matplotlib inline
# Data params
data_mean = 4
data_stddev = 1.25
# Model params
g_input_size = 1     # Random noise dimension coming into generator, per output vector
g_hidden_size = 50   # Generator complexity
g_output_size = 1    # size of generated output vector
d_input_size = 100   # Minibatch size - cardinality of distributions
d_hidden_size = 50   # Discriminator complexity
d_output_size = 1    # Single dimension for 'real' vs. 'fake'
minibatch_size = d_input_size
d_learning_rate = 2e-4  # 2e-4
g_learning_rate = 2e-4
optim_betas = (0.9, 0.999)
num_epochs = 40000
print_interval = 1000
d_steps = 10  # 'k' steps in the original GAN paper. Can put the discriminator on higher training freq than generator
g_steps = 1
# ### Uncomment only one of these
#(name, preprocess, d_input_func) = ("Raw data", lambda data: data, lambda x: x)
(name, preprocess, d_input_func) = ("Data and variances", lambda data: decorate_with_diffs(data, 2.0), lambda x: x * 2)
print("Using data [%s]" % (name))
# ##### DATA: Target data and generator input data
def get_distribution_sampler(mu, sigma):
    return lambda n: torch.Tensor(np.random.normal(mu, sigma, (1, n)))  # Gaussian
def get_generator_input_sampler():
    return lambda m, n: torch.rand(m, n)  # Uniform-dist data into generator, _NOT_ Gaussian
# ##### MODELS: Generator model and discriminator model
class Generator(nn.Module):
    def __init__(self, input_size, hidden_size, output_size):
        super(Generator, 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.sigmoid(self.map2(x))
        return self.map3(x)
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))
def extract(v):
    return v.data.storage().tolist()
def stats(d):
    return [np.mean(d), np.std(d)]
def decorate_with_diffs(data, exponent):
    mean = torch.mean(data.data, 1)
    mean_broadcast = torch.mul(torch.ones(data.size()), mean.tolist()[0][0])
    diffs = torch.pow(data - Variable(mean_broadcast), exponent)
    return torch.cat([data, diffs], 1)
d_sampler = get_distribution_sampler(data_mean, data_stddev)
gi_sampler = get_generator_input_sampler()
G = Generator(input_size=g_input_size, hidden_size=g_hidden_size, output_size=g_output_size)
D = Discriminator(input_size=d_input_func(d_input_size), hidden_size=d_hidden_size, output_size=d_output_size)
criterion = nn.BCELoss()  # Binary cross entropy: http://pytorch.org/docs/nn.html#bceloss
d_optimizer = optim.Adam(D.parameters(), lr=d_learning_rate, betas=optim_betas)
g_optimizer = optim.Adam(G.parameters(), lr=g_learning_rate, betas=optim_betas)
results = np.zeros(((num_epochs / print_interval + 1)*2, 2))
count = 0
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
    if epoch % print_interval == 0:
        #res = [extract(d_real_error)[0], extract(d_fake_error)[0]]
        res = extract(d_fake_error)[0] - extract(d_real_error)[0]
        for i in range(2):
            results[count+i,:] = [epoch,res]
            #print [epoch,res[i],i]
        count += 2
        if epoch % 1000 == 0:
            n_ = 20
            f,ax=plt.subplots(1)
            ax.hist(extract(d_real_data),n_)
            ax.hist(extract(d_fake_data),n_)
            ax.legend()
            plt.show()
        print("%s: D: %s/%s G: %s (Real: %s, Fake: %s) " % (epoch,
                                                            extract(d_real_error)[0],
                                                            extract(d_fake_error)[0],
                                                            extract(g_error)[0],
                                                            stats(extract(d_real_data)),
                                                            stats(extract(d_fake_data))))
results = pd.DataFrame(results, columns = ['Interval','d_fake_minus_real_error'])
#print results
#results['type'][results['type'] == 0] = 'd_real_error'
#results['type'][results['type'] == 1] = 'd_fake_error'
g = sns.lmplot(x="Interval", y="d_fake_minus_real_error",data=results, fit_reg = False)
plt.show()

这里训练了11000个周期, 可以看到基本上2000个周期后D对G生成数据和真实数据上的误差基本相等, 并且生成器生成数据与原始数据的方差和平均值已经基本一致了.

改进方向

Improved GANs

原始的GANs的损失函数就是判断是否真实数据, 利用交叉熵作为目标函数, 所以改进方向可以是不同的损失函数, 比如真实图片与生成图片的统计差异, 这里统计差异是插入在中间层而非结果层, 以防止过拟合, 要求真实图像和合成图像在中间层的距离最小.

Info GANs

到这可能有些同学会想到,我要是想通过GAN产生我想要的特定属性的图片改怎么办?普通的GAN输入的是随机的噪声,输出也是与之对应的随机图片,我们并不能控制输出噪声和输出图片的对应关系。这样在训练的过程中也会倒置生成模型倾向于产生更容易欺骗判别模型的某一类特定图片,而不是更好的去学习训练数据的分布,这样对模型的训练肯定是不好的. InfoGAN的提出就是为了解决这一问题,通过对输入噪声添加一些类别信息以及控制图像特征(如mnist数字的角度和厚度)的隐含变量来使得生成模型的输入不在是随机噪声。虽然现在输入不再是随机噪声,但是生成模型可能会忽略这些输入的额外信息还是把输入当成和输出无关的噪声,所以需要定义一个生成模型输入输出的互信息,互信息越高,说明输入输出的关联越大。

其他方面的应用

  1. Image captioning

  1. Image completion

  2. Image Augmentation

VAEs

VAE与GAN的区别主要在于VAE是一个回归问题, GAN是一个分类问题, GAN中引入了判别器进行真假分类, 而GAN中直接利用了生成图片与真实图片的平方差做为损失函数, 当然还要加上中间编码的损失, 这个隐变量我们要求他尽可能模拟正太分布, 加上这个限制后可以避免随机的噪声影响结果, 这也是VAE与普通的自编码器的区别, 普通自编码器只是为了降维, 而变分的限制使得我们能从一个固定的分布采样出样本然后解码出结果). 这里引入了贝叶斯网络的概念, 也就是说网路的参数和结构都是随机变量, 而非固定的结果, 之后引入变分逼近推理方法, 可以将这个问题转换成优化问题, 在VAE中并非全部假设随机变量, 而只是在隐节点上假设分布, 因此就把自编码器变成了变分推理网络, 这里的变分指的是引入了随机节点.

将分布的均值和方差视为传统网络的参数,并将方差乘以来自噪声发生器的样本以增加随机性。通过参数化隐藏分布,可以反向传播梯度得到编码器的参数,并用随机梯度下降训练整个网络。此过程能够学习到隐藏代码的均值与方差值,这就是所谓的“重新调参技巧”。

在经典版的神经网络中,可以用均方误差(MSE)简单测量网络输出与期望的目标值之间的误差。但在处理分布时,MSE不再是一个好的误差度量,因此用KL­散度测量两个分布之间的差异。事实证明变分近似和真实后验分布之间的距离不是很容易被最小化。它包括两个主要部分。因此,可以最大化较小的项(ELBO)。从自动编码器的角度来看,ELBO函数可以看作是输入的重建代价与正则化项的和

在最大化ELBO之后,数据的下限接近数据分布,则距离接近零,间接地最小化了误差距离。最大化下界的算法与梯度下降的完全相反。沿着梯度的正方向达到最大值,这整个算法被称为“自动编码变分贝叶斯”。

下面是一个伪代码,可以看到VAE的架构:

network= {

  # encoder
  encoder_x = Input_layer(size=input_size, input=data)
  encoder_h = Dense_layer(size=hidden_size, input= encoder_x)

  # the re-parameterized distributions that are inferred from data 
  z_mean = Dense(size=number_of_distributions, input=encoder_h)
  z_variance = Dense(size=number_of_distributions, input=encoder_h)
  epsilon= random(size=number_of_distributions)

  # decoder network needs a sample from the code distribution
  z_sample= z_mean + exp(z_variance / 2) * epsilon

  #decoder
  decoder_h = Dense_layer(size=hidden_size, input=z_sample)
  decoder_output = Dense_layer(size=input_size, input=decoder_h)
}

cost={
  reconstruction_loss = input_size * crossentropy(data, decoder_output)
  kl_loss = - 0.5 * sum(1 + z_variance - square(z_mean) - exp(z_variance))
  cost_total= reconstruction_loss + kl_loss
}

stochastic_gradient_descent(data, network, cost_total)

需要注意的是, 在生成隐变量的时候, 利用了一个技巧, 先直接生成隐变量的方差与平均值, 然后在计算KL散度, 这里的原因是反向传播不能对随机节点进行, 所以我们用确定的参数方差和平均值来代替这个随机节点, 然后反向传播只需要对着方差和平均值进行传播, 进而可以忽略掉随机的$\epsilon$.


条件VAE

由于普通VAE是由随机从分布中采样的样本来生成目标结果, 因此具体生成了什么无法确定, 条件自编码能够指定生成的目标结果是什么, 这个和InfoGAN的目标是一致的.

具体实现上, 在训练时编码解码都要加上一个one-hot变量(通过全连接层变换后连接到输入的地方, 相当于条件GAN), 解码时把想要生成的图片one-hot一起输入即可.

Reference

GAN
DCGAN
Improved Techniques for Training GANs
InfoGAN
Generative Adversarial Networks (GAN) in Pytorch
Variational Auto-Encoders and Extensions
Manifold Learning with Variational Auto-encoder for Medical Image Analysis
VAE