生成对抗网络由 Ian Goodfellow 于 2014 年提出。GAN 不是神经网络应用在无监督学习中的唯一途径,还有玻尔兹曼机(Geoffrey Hinton 和 Terry Sejnowski,1985)和自动解码器(Dana H. Ballard,1987)。三者皆致力于通过学习恒等函数 f(x)= x 从数据中提取特征,且都依赖马尔可夫链来训练或生成样本。
GAN 设计之初衷就是避免使用马尔可夫链,因为后者的计算成本很高。相对于玻尔兹曼机的另一个优点是 GAN 的限制要少得多(只有几个概率分布适用于马尔可夫链抽样)。
判别算法和生成算法有何不同?简单地说:判别算法学习类之间的边界(如判别器做的那样),而生成算法学习类的分布(如生成器做的那样)。
思考:如果 Discriminator 只是做区分的话,其实在文本中是没有什么意义的,因为文本能区分开并不意味着文本本身很通畅。所以 Gan 在自然语言处理中的关键是如何生成有效通畅的文本。也就是说,我们其实要解决文本生成的问题,而不是对抗训练的问题。
而且这很像强化学习接一层 CNN。
当判别器不能区分 p_g
和 p_data
,即 D(x,θ_d)= 1/2
时,训练过程停止。达成生成器与判别器之间判定误差的平衡。
P_z
生成 P_G(z;θ)
P_G(z;θ)
与 P_data(x)
做分类D(X)=1, D(G(z))=0
,最大化:D_G*
,定义价值函数为:D=D_G*
时,V 达到极大值,此时,G 的目标是让 V 最小(这样 D 就分不出来啦,哈哈),因此最优的 G 为最小化 V,写成我们熟知的极小极大化博弈形式:P_G
和 P_data
之间的差异或距离。目标:若给定一个样本数据的分布 P_data(x)
和生成的数据分布 P_G(z;θ)
,那么 GAN 希望能找到一组参数 θ 使分布 P_G(z;θ)
和 P_data(x)
之间的距离最短,也就是找到一组生成器参数而使得生成器能生成十分逼真的图片。
从 P_data(x)
中抽取 m 个真实样本 {𝑥^1,𝑥^2,…,𝑥^𝑚}
,计算 P_G(x^i;θ)
,即在由 θ 确定的生成分布中,x^i
样本所出现的概率,以此训练 θ 使 P_G 逼近真实分布。抽取的 m 个真实样本在 P_G(x;θ)
分布中全部出现的概率值可以表达为 L:
$$L = \prod_{i=1}^m P_G(x^i;\theta)$$
最大化 L 求的离真实分布最近的生成分布(即最优的 θ):
$$\theta^* = argmax_{\theta} \prod_{i=1}^m P_G(x^i;\theta) = argmax_{\theta} \log\prod_{i=1}^m P_G(x^i;\theta) = argmax_{\theta} \sum_{i=1}^m \log P_G(x^i;\theta)$$
$$\approx argmax_{\theta} E_x \sim P_{data} [\log P_G(x^i;\theta)] = argmax_{\theta} \int_x P_{data}(x)logP_G(x;\theta)dx$$
$$\theta^* = argmax_{\theta} \int_x P_{data}(x)logP_G(x;\theta)dx - \int_x P_{data}(x)\log P_{data}(x)dx$$
$$=argmin_{\theta} (\int_x P_{data}(x)\log P_{data}(x)dx - \int_x P_{data}(x)\log P_G(x;\theta)dx)$$
$$=argmin_{\theta} KL(P_{data} \parallel P_G(x;\theta))$$
约等于是因为 m 个只是取样,不是全体。
只要最小化 KL 散度就能取得最优参数 θ。
推导的问题:G 不需要满足可逆条件(也就是说默认 G(z) 可以生成 x)。证明能够成立需要满足:
$$E_z \sim p_z(z) \log(1-D(G(z))) = E_x \sim p_G(x) \log(1-D(x))$$
放到表达式中:
$$V = \int_x p_{data}(x)\log(D(x)) + \int_zp(z)\log(1-D(G(z)))$$
$$= \int_x p_{data}(x)\log(D(x)) + \int_x p_G(x)\log(1-D(x))$$
这里使用了积分换元公式,但进行积分换元就必须计算 G^(-1)
,而 G 的逆却并没有假定为存在。并且在神经网络的实践中,它也并不存在。
知识卡:设函数 y=f(x)(x∈A) 的值域是 C,若找得到一个函数 g(y) 在每一处 g(y) 都等于 x,这样的函数 x= g(y)(y∈C) 叫做函数 y=f(x)(x∈A) 的反函数,记作 y=f^(-1)(x) 。若一函数有反函数,此函数便称为可逆的(invertible)。
求积分的最大值可以转化为求被积函数的最大值:
$$P_{data}(x) log (D(x)) + P_G(x) log (1-D(x))$$
令 a = P_data, b = P_G, D(x) = y
:
$$a\log(y) + b\log(1-y)$$
为了求极值,如果 a+b≠0,一阶导:
$$f'(y) = 0 => \frac{a}{y} - \frac{b}{1-y} = 0 => y = \frac{a}{a+b}$$
继续求 f(y) 在驻点的二阶导:
$$f''(\frac{a}{a+b}) = -\frac{a}{y^2} - \frac{b}{(1-y)^2} < 0, \ a,b \in (0,1)$$
因为一阶导等于零、二阶导小于零,所以我们知道 a/(a+b) 为极大值:
$$D(x) = \frac{a}{a+b} = \frac{P_{data}(x)} {P_{data}(x)+P_G(x)}$$
所以:
$$V(G,D) = \int_{x} p_{data}(x)\log D(x) + \int_x p_G(x)\log(1-D(x)) dx$$
$$\le \int_x max_y p_{data}(x)\log y + \int_x p_G(x)\log(1-y) dx$$
y 为上式的 D(x)
如果我们令 D(x)=P_data/(P_data+p_G)
,那么我们就可以令价值函数 V(G,D) 取极大值。因为 f(y) 在定义域内有唯一的极大值,最优 D 也是唯一的,并且没有其它的 D 能实现极大值。
其实该最优的 D 在实践中并不是可计算的,但在数学上十分重要。我们并不知道先验的 P_data(x),所以我们在训练中永远不会用到它。另一方面,它的存在令我们可以证明最优的 G(D?) 是存在的,并且在训练中我们只需要逼近 D。
最终的目标是 P_G = P_data
,代入 D_G*
:
$$D_G^* = \frac{P_{data}(x)}{P_{data}(x)+P_G(x)} = \frac{1}{2}$$
此时,判别器无法分辨 P_data
与 P_G
,基于此可以证明:【当且仅当 P_G=P_data,Cost(G)=maxV(G,D)
的全局最小点可以达到。】这就是极大极小博弈的第二步,求令 V(G,D*)
最小的生成器 G,此时 D 固定。
之所以当 P_G(x)=P_data(x)
可以令价值函数最小化,是因为这时候两个分布的 JS 散度 [JSD(P_data(x) || P_G(x))]
等于零。
先证明反向(反向指预先知道最优条件并做推导),设 P_G=P_data
:
$$V(G, D_G^*) = \int_x p_{data}(x)\log\frac{1}{2} + P_G(x) \log (1-\frac{1}{2})dx$$
$$= -\log2 \int_x P_{data}(x) dx - \log2 \int_x P_G(x) dx$$
$$= -\log2 (\int_x P_{data}(x )dx + \int_x P_G(x) dx) = -2\log2 = -\log4$$
该值是全局最小值的候选,因为它只有在 P_G=P_data
的时候才出现。
现在需要从正向证明这一个值常常为最小值,也就是同时满足「当」和「仅当」的条件。放弃 P_G=P_data
的假设,对任意一个 G,我们可以将上一步求出的最优判别器 D*
代入到 Cost(G)=maxV(G,D) 中:
$$Cost(G) = \int_x p_{data}(x) \log \frac{P_{data}(x)}{P_{data}(x)+P_G(x)} + p_{G}(x) \log \frac{P_{G}(x)}{P_{data}(x)+P_G(x)} dx$$
因为已知 -log4 为全局最小候选值,所以我们希望构造某个值以使方程式中出现 log2。因此我们可以在每个积分中加上或减去 log2,并乘上概率密度:
$$Cost(G) = \int_x (\log2 - \log2)P_{data}(x) + p_{data}(x) \log \frac{P_{data}(x)}{P_{data}(x)+P_G(x)} + (\log2 - \log2)P_{G}(x) + p_{G}(x) \log \frac{P_{G}(x)}{P_{data}(x)+P_G(x)} dx$$
$$= \int_x P_{data}(x)(\log2 + \log \frac{P_{data}(x)}{P_{data}(x)+P_G(x)}) + P_{G}(x)(\log2 + \log \frac{P_{G}(x)}{P_{data}(x)+P_G(x)}) dx - \log2 \int_x P_{data}(x)+P_G(x) dx$$
因为概率密度的定义,P_G
和 P_data
在它们积分域上的积分等于 1,即:
$$- \log2 \int_x P_{data}(x)+P_G(x) dx = - \log2(1+1) = -\log4$$
代入 Cost(G):
$$Cost(G) = -\log4 + \int_x P_{data}(x)(\log \frac{P_{data}(x)}{(P_{data}(x)+P_G(x))/2}) + P_{G}(x)(\log \frac{P_{G}(x)}{(P_{data}(x)+P_G(x))/2}) dx$$
$$= -\log4 + KL(P_{data} \parallel \frac{P_{data} + P_G}{2}) + KL(P_{G} \parallel \frac{P_{data} + P_G}{2})$$
KL 散度非负,所以 -log4 就是 Cost(G) 全局最小值。如果进一步证明只有一个 G 能达到这一个值,因为 P_G=P_data
将会成为令 Cost(G)=−log4 的唯一点,所以整个证明就能完成了。
由于 KL 散度是非对称的,所以 C(G) 中的 KL(P_data || (P_data+P_G)/2)
左右两项是不能交换的,但如果同时加上另一项 KL(P_G || (P_data+P_G)/2)
,它们的和就能变成对称项。这两项 KL 散度的和即可以表示为 JS 散度(Jenson-Shannon divergence):
$$JSD(P \parallel Q) = \frac{1}{2}KL(P \parallel M) + \frac{1}{2}KL(Q \parallel M), \ M = \frac{1}{2} (P+Q)$$
JS 散度的取值为 0 到 log2。若两个分布完全没有交集,那么 JS 散度取最大值 log2;若两个分布完全一样,那么 JS 散度取最小值 0。
所以,Cost(G) 可以根据 JS 散度改写为:
$$Cost(G) = -\log4 + 2*JSD(P_{data} \parallel P_G)$$
这一散度其实就是 Jenson-Shannon 距离度量的平方。根据它的属性:当 P_G=P_data
时,JSD(P_data||P_G)
为 0,Cost(G) 取得极小值。
综上所述,生成分布当且仅当等于真实数据分布式时,我们可以取得最优生成器。
若我们需要寻找最优的生成器,那么给定一个判别器 D,我们可以将 maxV(G,D) 看作训练生成器的损失函数 L(G)。
既然设定了损失函数,那么我们就能使用 SGD、Adam 等优化算法更新生成器 G 的参数,梯度下降的参数优化过程如下:
$$\theta_G \gets \theta_G - \eta \partial L(G)/\partial \theta_G$$
G_0
,最大化 V(G_0,D)
求得 D_0*
,将最优的 D_0*
代入 V(G_0,D)
得到 - log4+2*JSD
D_0*
,最小化 JSD,求得更新后的 G_1
G_1
,最大化 V(G_1,D_0*)
求得 D_1*
,将最优的 D_1*
代入 V(G_1,D)
得到更新后的 - log4+2*JSD
D_1*
,最小化 更新后的 JSD,求得更新后的 G_2
...
根据前面价值函数 V(G,D) 的定义,我们需要求两个数学期望,即 E[log(D(x))]
和 E[log(1-D(G(z)))]
,其中 x 服从真实数据分布,z 服从初始化分布。但在实践中,我们是没有办法利用积分求这两个数学期望的,所以一般我们能从无穷的真实数据和无穷的生成器中做采样以逼近真实的数学期望。
若现在给定生成器 G,并希望计算 maxV(G,D) 以求得判别器 D。那么我们首先需要从 P_data(x)
采样 m 个样本 {𝑥^1,𝑥^2,…,𝑥^𝑚}
,从生成器 P_G(x)
采样 m 个样本 {𝑥^1',𝑥^2',…,𝑥^𝑚'}
。因此最大化价值函数 V(G,D) 就可以使用以下表达式近似替代:
$$Maximize\ V = \frac{1}{m} \sum_{i=1}^m \log D(x^i) + \frac{1}{m} \sum_{i=1}^m \log (1-D(x'^i))$$
若我们需要计算上述的极大化过程,可以采用等价形式的训练方法。若我们有一个二元分类器 D(参数为θ_d),当然该分类器可以是深度神经网络,那么极大化过程的输出就为该分类器 D(x)。
现在我们从 P_data(x)
抽取样本作为正样本,从 P_G(x)
抽取样本作为负样本,同时将逼近负 V(G,D) 的函数作为损失函数,因此我们就将其表述为一个标准的二元分类器的训练过程(有疑惑,已改):
在实践中,我们必须使用迭代和数值计算的方法实现极小极大化博弈过程。在训练的内部循环中完整地优化 D 在计算上是不允许的,并且有限的数据集也会导致过拟合。因此我们可以在 k 个优化 D 的步骤和一个优化 G 的步骤间交替进行。那么我们只需慢慢地更新 G,D 就会一直处于最优解的附近,这种策略类似于 SML/PCD 训练的方式。
对每一次迭代:
P_data
中抽取 m 个样本P_prior(z)
中抽取 m 个噪声样本{𝑥^1',𝑥^2',…,𝑥^𝑚'}
,通过最大化 V 的近似而更新判别器参数 θ_d
,即极大化:以上是学习判别器 D 的过程。因为学习 D 的过程是计算 JS 散度的过程,并且我们希望能最大化价值函数,所以该步骤会重复 k 次。
P_prior(z)
中抽取另外 m 个噪声样本 {z^1,...z^m}
θ_g
,即极小化:以上是学习生成器参数的过程,这一过程在一次迭代中只会进行一次,因此可以避免更新太多而令 JS 散度上升。
import os
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import tensorflow as tf
from tensorflow.examples.tutorials.mnist import input_data
#该函数将给出权重初始化的方法
def variable_init(size):
in_dim = size[0]
#计算随机生成变量所服从的正态分布标准差
w_stddev = 1. / tf.sqrt(in_dim / 2.)
return tf.random_normal(shape=size, stddev=w_stddev)
#定义输入矩阵的占位符,输入层单元为784,None代表批量大小的占位,X代表输入的真实图片。占位符的数值类型为32位浮点型
X = tf.placeholder(tf.float32, shape=[None, 784])
#定义判别器的权重矩阵和偏置项向量,由此可知判别网络为三层全连接网络
D_W1 = tf.Variable(variable_init([784, 128]))
D_b1 = tf.Variable(tf.zeros(shape=[128]))
D_W2 = tf.Variable(variable_init([128, 1]))
D_b2 = tf.Variable(tf.zeros(shape=[1]))
theta_D = [D_W1, D_W2, D_b1, D_b2]
#定义生成器的输入噪声为100维度的向量组,None根据批量大小确定
Z = tf.placeholder(tf.float32, shape=[None, 100])
#定义生成器的权重与偏置项。输入层为100个神经元且接受随机噪声,
#输出层为784个神经元,并输出手写字体图片。生成网络根据原论文为三层全连接网络
G_W1 = tf.Variable(variable_init([100, 128]))
G_b1 = tf.Variable(tf.zeros(shape=[128]))
G_W2 = tf.Variable(variable_init([128, 784]))
G_b2 = tf.Variable(tf.zeros(shape=[784]))
theta_G = [G_W1, G_W2, G_b1, G_b2]
#定义一个可以生成m*n阶随机矩阵的函数,该矩阵的元素服从均匀分布,随机生成的z就为生成器的输入
def sample_Z(m, n):
return np.random.uniform(-1., 1., size=[m, n])
#定义生成器
def generator(z):
#第一层先计算 y=z*G_W1+G-b1,然后投入激活函数计算G_h1=ReLU(y),G_h1 为第二次层神经网络的输出激活值
G_h1 = tf.nn.relu(tf.matmul(z, G_W1) + G_b1)
#以下两个语句计算第二层传播到第三层的激活结果,第三层的激活结果是含有784个元素的向量,该向量转化28×28就可以表示图像
G_log_prob = tf.matmul(G_h1, G_W2) + G_b2
G_prob = tf.nn.sigmoid(G_log_prob)
return G_prob
#定义判别器
def discriminator(x):
#计算D_h1=ReLU(x*D_W1+D_b1),该层的输入为含784个元素的向量
D_h1 = tf.nn.relu(tf.matmul(x, D_W1) + D_b1)
#计算第三层的输出结果。因为使用的是Sigmoid函数,则该输出结果是一个取值为[0,1]间的标量(见上述权重定义)
#即判别输入的图像到底是真(=1)还是假(=0)
D_logit = tf.matmul(D_h1, D_W2) + D_b2
D_prob = tf.nn.sigmoid(D_logit)
#返回判别为真的概率和第三层的输入值,输出D_logit是为了将其输入tf.nn.sigmoid_cross_entropy_with_logits()
# 以构建损失函数
return D_prob, D_logit
#该函数用于输出生成图片
def plot(samples):
fig = plt.figure(figsize=(4, 4))
gs = gridspec.GridSpec(4, 4)
gs.update(wspace=0.05, hspace=0.05)
for i, sample in enumerate(samples):
ax = plt.subplot(gs[i])
plt.axis('off')
ax.set_xticklabels([])
ax.set_yticklabels([])
ax.set_aspect('equal')
plt.imshow(sample.reshape(28, 28), cmap='Greys_r')
return fig
#输入随机噪声z而输出生成样本
G_sample = generator(Z)
#分别输入真实图片和生成的图片,并投入判别器以判断真伪
D_real, D_logit_real = discriminator(X)
D_fake, D_logit_fake = discriminator(G_sample)
#以下为原论文的判别器损失和生成器损失,但本实现并没有使用该损失函数
# D_loss = -tf.reduce_mean(tf.log(D_real) + tf.log(1. - D_fake))
# G_loss = -tf.reduce_mean(tf.log(D_fake))
# 我们使用交叉熵作为判别器和生成器的损失函数,因为sigmoid_cross_entropy_with_logits内部会对预测输入执行Sigmoid函数,
#所以我们取判别器最后一层未投入激活函数的值,即D_h1*D_W2+D_b2。
#tf.ones_like(D_logit_real)创建维度和D_logit_real相等的全是1的标注,真实图片。
D_loss_real = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_real, labels=tf.ones_like(D_logit_real)))
D_loss_fake = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.zeros_like(D_logit_fake)))
#损失函数为两部分,即E[log(D(x))]+E[log(1-D(G(z)))],将真的判别为假和将假的判别为真
D_loss = D_loss_real + D_loss_fake
#同样使用交叉熵构建生成器损失函数
G_loss = tf.reduce_mean(
tf.nn.sigmoid_cross_entropy_with_logits(logits=D_logit_fake, labels=tf.ones_like(D_logit_fake)))
#定义判别器和生成器的优化方法为Adam算法,关键字var_list表明最小化损失函数所更新的权重矩阵
D_solver = tf.train.AdamOptimizer().minimize(D_loss, var_list=theta_D)
G_solver = tf.train.AdamOptimizer().minimize(G_loss, var_list=theta_G)
#选择训练的批量大小和随机生成噪声的维度
mb_size = 128
Z_dim = 100
#读取数据集MNIST,并放在当前目录data文件夹下MNIST文件夹中,如果该地址没有数据,则下载数据至该文件夹
mnist = input_data.read_data_sets("./data/MNIST/", one_hot=True)
Extracting ./data/MNIST/train-images-idx3-ubyte.gz Extracting ./data/MNIST/train-labels-idx1-ubyte.gz Extracting ./data/MNIST/t10k-images-idx3-ubyte.gz Extracting ./data/MNIST/t10k-labels-idx1-ubyte.gz
#打开一个会话运行计算图
sess = tf.Session()
#初始化所有定义的变量
sess.run(tf.global_variables_initializer())
#如果当前目录下不存在out文件夹,则创建该文件夹
if not os.path.exists('out/'):
os.makedirs('out/')
#初始化,并开始迭代训练,100W次
i = 0
for it in range(20000):
#每2000次输出一张生成器生成的图片
if it % 2000 == 0:
samples = sess.run(G_sample, feed_dict={Z: sample_Z(16, Z_dim)})
fig = plot(samples)
plt.savefig('out/{}.png'.format(str(i).zfill(3)), bbox_inches='tight')
i += 1
plt.close(fig)
#next_batch抽取下一个批量的图片,该方法返回一个矩阵,即shape=[mb_size,784],每一行是一张图片,共批量大小行
X_mb, _ = mnist.train.next_batch(mb_size)
#投入数据并根据优化方法迭代一次,计算损失后返回损失值
_, D_loss_curr = sess.run([D_solver, D_loss], feed_dict={X: X_mb, Z: sample_Z(mb_size, Z_dim)})
_, G_loss_curr = sess.run([G_solver, G_loss], feed_dict={Z: sample_Z(mb_size, Z_dim)})
#每迭代2000次输出迭代数、生成器损失和判别器损失
if it % 2000 == 0:
print('Iter: {} D_loss: {:.4}, G_loss: {:.4}'.format(it, D_loss_curr, G_loss_curr))
Iter: 0 D_loss: 1.446, G_loss: 2.288 Iter: 2000 D_loss: 0.0645, G_loss: 6.974 Iter: 4000 D_loss: 0.08388, G_loss: 4.614 Iter: 6000 D_loss: 0.254, G_loss: 4.711 Iter: 8000 D_loss: 0.7669, G_loss: 3.501 Iter: 10000 D_loss: 0.7431, G_loss: 2.662 Iter: 12000 D_loss: 0.65, G_loss: 3.146 Iter: 14000 D_loss: 0.73, G_loss: 2.457 Iter: 16000 D_loss: 0.7591, G_loss: 2.304 Iter: 18000 D_loss: 0.6435, G_loss: 2.266
主要看其设计和过程。