生成式模型的作用
密度估计
给定一组数据\(D=\left \{ x^{n} \right \}^{N}_{n=1}\),假设它们都是独立地从相同的概率密度函数为\(p_{r}(x)\)的未知分布中产生的。密度估计是根据数据集\(D\)来估计其概率密度函数\(p_{\theta}(x)\)。
在机器学习中,密度估计是一类无监督学习问题。比如在手写体数字图像的密度估计问题中,我们将图像表示为一个随机变量\(X\),其中每一维都表示一个像素值。假设手写体数字图像都服从一个未知的分布\(p_{r}{x}\),希望通过一些观测样本来估计其分布。但是手写体数字图像中不同像素之间存在复杂的依赖关系,很难用一个明确的图模型来描述其依赖关系,所以直接建模\(p_{r}{x}\),比较困难。因此,我们通过引入隐变量\(z\)来简化模型,这样密度估计问题可以转化为估计变量(x, z)的两个局部条件概率\(p_{\theta}(z)\)和\(p_{\theta}(x|z)\)。一般为了简化模型,假设隐变量\(z\)的先验分布为标准高斯分布\(N(0, I)\)。隐变量\(z\)的每一维度之间都是独立的,密度估计的重点是估计条件分布\(p(x|z; \theta)\)。
如果要建模隐含变量的分布,就需要用EM算法来进行密度估计,而在EM算法中,需要估计条件分布\(p(x|z; \theta)\)以及后验概率分布\(p(z|x; \theta)\)。当这两个分布比较复杂时,就可以利用神经网络来建模(如变分自编码器)。
生成样本
在知道\(p_{\theta}(z)\)和得到\(p_{\theta}(x|z)\)之后就可以生成新的数据:
- 从隐变量的先验分布\(p_{\theta}(z)\)中采样,得到样本\(z\)。
- 根据条件概率分布\(p_{\theta}(x|z)\)进行采样,得到新的样本\(x\)
生成对抗网络
本文的重点是生成对抗网络(GAN)。与一般的生成式模型(如VAE、DQN)不同,GAN并不直接建模\(p(x)\),而是直接通过一个神经网络学习从隐变量\(z\)到数据\(x\)的映射,称为生成器;然后将生成的样本交给判别网络判断是否是真实的样本。可以看出,生成网络和判别网络的训练是彼此依存、交替进行的。
判别网络
判别网络\(D(\boldsymbol x;\phi )\)的目标是区分出一个样本\(\boldsymbol x\)是来自于真实分布\(p_{r}(\boldsymbol x)\)还是来自于生成模型\(p_{\theta}(\boldsymbol x)\)。由此可见,判别网络实际上是一个二分类的分类器。用标签\(y=1\)来表示样本来自真实分布,\(y=0\)表示样本来自生成模型,判别网络\(D(\boldsymbol x;\phi )\)的输出为\(\boldsymbol x\)属于真实数据分布的概率,即:
样本来自生成模型的概率为:
给定一个样本\((x,y),y= \left \{ 1,0 \right \}\),表示其来自于\(p_{r}(\boldsymbol x)\)还是\(p_{\theta}(\boldsymbol x)\),判别网络的目标函数为最小化交叉熵,即:
假设分布\(p(\boldsymbol x)\)是由分布\(p_{r}(\boldsymbol x)\)和分布\(p_{\theta}(\boldsymbol x)\)等比例混合而成,即\(p(\boldsymbol x) = \frac{1}{2} * \left (p_{r}(\boldsymbol x) + p_{\theta}(\boldsymbol x) \right )\),则上式等价于:
其中\(\theta\)和\(\phi\)分别是生成网络和判别网络的参数。
生成网络
生成网络的目标刚好和判别网络相反,即让判别网络将自己生成的样本判别为真是样本。
两个目标函数是等价的,但一般使用前者,因为其梯度性质更好。
训练
和单目标的优化任务相比,生成对抗网络的两个网络的优化目标刚好相反。因此生成对抗网络的训练比较难,往往不太稳定. 一般情况下,需要平衡两个网络的能力。对于判别网络来说,一开始的判别能力不能太强,否则难以提升生成网络的能力。但是,判别网络的判别能力也不能太弱,否则针对它训练的生成网络也不会太好。 在训练时需要使用一些技巧,使得在每次迭代中,判别网络比生成网络的能力强一些,但又不能强太多。具体做法是,判别网络更新\(K\)次,生成网络更新1次。
代码实现
为了能在个人电脑上快速运行GAN程序,本文利用GAN生成二次曲线。首先在[-1, 1]区间内采样10000个二次曲线的真实样本,以此为训练数据。
hyperparam.py文件
超参数配置模块
import argparse
class HyperParam:
def __init__(self):
self.parse = argparse.ArgumentParser()
self.parse.add_argument("--latent_dim", type=int, default=5) # 隐含变量的维度
self.parse.add_argument("--data_dim", type=int, default=10) # 观测变量的维度
self.parse.add_argument("--data_size", type=int, default=10000) # 样本数
self.parse.add_argument("--g_lr", type=float, default=0.001)
self.parse.add_argument("--d_lr", type=float, default=0.001)
self.parse.add_argument("--epochs", type=int, default=300)
self.parse.add_argument("--K", type=int, default=5)
self.parse.add_argument("--sample_size", type=int, default=128)
self.parse.add_argument("--batch_size", type=int, default=128)
gan.py文件
GAN的实现部分
import numpy as np
import torch
from hyperparam import HyperParam
import torch.nn as nn
import torch.utils.data as Data
import matplotlib.pyplot as plt
np.random.seed(1000)
torch.manual_seed(1000)
def get_real_data(data_dim, data_size, batch_size):
base = np.linspace(-1, 1, data_dim)
a = np.random.uniform(8, 15, data_size).reshape(-1, 1)
c = np.random.uniform(0.5, 10, data_size).reshape(-1, 1)
# 构造真实数据
X = a * np.power(base, 2) + c
X = torch.from_numpy(X).type(torch.float32)
data_set = Data.TensorDataset(X)
data_loader = Data.DataLoader(dataset=data_set,
batch_size=batch_size)
return base, data_loader
class GAN(nn.Module):
def __init__(self, latent_dim, data_dim, K, sample_size):
super().__init__()
self.latent_dim = latent_dim
self.data_dim = data_dim
self.K = K
self.sample_size = sample_size
self.g = self._generator()
self.d = self._discriminator()
self.g_optimizer = torch.optim.Adam(self.g.parameters(), lr=0.001)
self.d_optimizer = torch.optim.Adam(self.d.parameters(), lr=0.001)
def _generator(self):
model = nn.Sequential(
nn.Linear(self.latent_dim, 64),
nn.ReLU(),
nn.Linear(64, self.data_dim)
)
return model
def _discriminator(self):
model = nn.Sequential(
nn.Linear(self.data_dim, 64),
nn.ReLU(),
nn.Linear(64, 1),
nn.Sigmoid()
)
return model
def d_loss_fn(self, pred_data_result, true_data_result):
return -torch.mean(torch.log(true_data_result) + torch.log(1 - pred_data_result))
def g_loss_fn(self, pred_data_result):
return -torch.mean(torch.log(pred_data_result))
def train_d(self, true_data):
sample_size = true_data.shape[0]
for i in range(self.K):
# 采样
sample = torch.rand(sample_size, self.latent_dim)
# 生成
fake_data = self.g(sample)
# 生成数据的判定结果
fake_data_result = self.d(fake_data)
# 真实数据的判定结果
true_data_result = self.d(true_data)
loss = self.d_loss_fn(fake_data_result, true_data_result)
self.d_optimizer.zero_grad()
loss.backward()
self.d_optimizer.step()
def train_g(self):
# 采样
sample = torch.rand(self.sample_size, self.latent_dim)
# 生成
fake_data = self.g(sample)
# 生成数据的判定结果
fake_data_result = self.d(fake_data)
loss = self.g_loss_fn(fake_data_result)
self.g_optimizer.zero_grad()
loss.backward()
self.g_optimizer.step()
def step(self, true_data):
self.train_d(true_data) # 先训练判别器
self.train_g() # 再训练生成器
def train(epochs, latent_dim, data_dim, K, sample_size, data_loader, base):
print('正在训练......')
model = GAN(latent_dim, data_dim, K, sample_size)
plt.ion()
for epoch in range(epochs):
for true_data in data_loader:
model.step(true_data[0]) # [128, 15]
if (epoch + 1) % 50 == 0:
print('epoch: [{}/{}]'.format(epoch + 1, epochs))
# 采样
sample = torch.rand(1, latent_dim)
# 生成
fake_data = model.g(sample)
plt.cla()
plt.plot(base, fake_data.data.numpy().flatten())
plt.show()
plt.pause(0.1)
plt.ioff()
plt.show()
torch.save(model.state_dict(), 'gan_param.pkl')
print('模型保存成功')
if __name__ == "__main__":
instance = HyperParam()
hp = instance.parse.parse_args()
epochs = hp.epochs
latent_dim = hp.latent_dim
data_dim = hp.data_dim
K = hp.K
sample_size = hp.sample_size
data_size = hp.data_size
batch_size = hp.batch_size
base, data_loader = get_real_data(data_dim, data_size, batch_size)
train(epochs, latent_dim, data_dim, K, sample_size, data_loader, base)
运行结果及分析
运行结果
从图中可以看出,从左到右,生成模型绘制二次曲线的能力越来越强了,训练500个epoch之后,生成的图形比较接近真实的二次曲线。
结果分析
实际运行程序时会发现,GAN的生成效果对激活函数和超参数的依赖非常大,特别是超参数K(训练K次判别器之后再训练一次生成器)的取值,如果K的取值稍微不合理,那么会直接导致生成器的损失太大,无法继续优化下去。此外,GAN需要足够的多的样本学习,特别是如果隐变量维度较多的话,需要更多的样本才有可能学得比较好的模型;模型训练过程中存在明显的震荡现象。
值得一提的是,如果主动给数据加标签,用二分类交叉熵损失函数计算误差,模型的效果会比无监督的训练方式稍微好一些。具体做法只需简单修改部分代码即可:
# 重新定义损失函数
self.d_loss = torch.nn.CrossEntropyLoss()
self.g_loss = torch.nn.CrossEntropyLoss()
# 计算损失值
def d_loss_fn(self, pred_data_result, fake_label, true_data_result, y):
# return -torch.mean(torch.log(true_data_result) + torch.log(1 - pred_data_result))
# 需要将真实数据、生成数据;真实标签、生成标签拼接到一起
pred = torch.cat((pred_data_result, true_data_result))
label = torch.cat((fake_label, y))
return self.d_loss(pred, label)
def g_loss_fn(self, pred_data_result):
# return -torch.mean(torch.log(pred_data_result))
# 构造标签
# 在训练生成器时,对生成的数据构造真实标签(即标签值为1),这样才能计算生成的结果与真实的差距,进而训练生成器
label = torch.ones(pred_data_result.shape[0], dtype=torch.long)
return self.g_loss(pred_data_result, label)
GAN的优缺点分析
优点
- GAN是一种生成式模型,相比较其他生成模型(玻尔兹曼机和GSNs)只用到了反向传播,而不需要复杂的马尔科夫链。
- 相比其他所有模型, GAN可以产生更加清晰,真实的样本。
- GAN采用的是一种无监督的学习方式训练,可以被广泛用在无监督学习和半监督学习领域。
- 相比于变分自编码器, GANs没有引入任何决定性偏置( deterministic bias),变分方法引入决定性偏置,因为他们优化对数似然的下界,而不是似然度本身,这看起来导致了VAEs生成的实例比GANs更模糊。
- 相比VAE, GANs没有变分下界,如果鉴别器训练良好,那么生成器可以完美的学习到训练样本的分布。换句话说,GANs是渐进一致的,但是VAE是有偏差的。
缺点
- GAN不适合处理离散形式的数据,比如文本。
- GAN存在训练不稳定、梯度消失、模式崩溃的问题(目前已解决)
关于GAN的一些问题
模式崩溃的原因
一般出现在GAN训练不稳定的时候,具体表现为生成出来的结果非常差,但是即使加长训练时间后也无法得到很好的改善。
具体原因可以解释如下:GAN采用的是对抗训练的方式,G的梯度更新来自D,所以G生成的好不好,得看D怎么说。具体就是G生成一个样本,交给D去评判,D会输出生成的假样本是真样本的概率(0-1),相当于告诉G生成的样本有多大的真实性,G就会根据这个反馈不断改善自己,提高D输出的概率值。但是如果某一次G生成的样本可能并不是很真实,但是D给出了正确的评价,或者是G生成的结果中一些特征得到了D的认可,这时候G就会认为我输出的正确的,那么接下来我就这样输出肯定D还会给出比较高的评价,实际上G生成的并不怎么样,但是他们两个就这样自我欺骗下去了,导致最终生成结果缺失一些信息,特征不全。
为什么优化器不常用SGD
- SGD容易震荡,容易使GAN训练不稳定。
- GAN的目的是在高维非凸的参数空间中找到纳什均衡点,GAN的纳什均衡点是一个鞍点,但是SGD只会找到局部极小值,因为SGD解决的是一个寻找最小值的问题,GAN是一个博弈问题。
为什么不适合处理文本数据
Goodfellow的回答
- GANs再NLP领域表现不是很好,因为GAN只对定义在实数域的连续数值有效。生成器的更新方式是判别器的误差对生成网络参数的导数,这个导数告诉你如何对生成的数据做一个微小的改变以生成更为真实的数据。如果数据是连续的,那么可以很方便的做微调;但如果数据是离散的,这种微调是没有意义的。比如,如果你输出了一张图片,其像素值是1.0,那么接下来你可以将这个值改为1.0001。如果你输出了一个单词“penguin”,那么接下来就不能将其改变为“penguin + .001”,因为没有“penguin +.001”这个单词。
其他网友的看法
- 文本数据相比较图片数据来说是离散的,因为对于文本来说,通常需要将一个词映射为一个高维的向量,最终预测的输出是一个one-hot向量,假设softmax的输出是(0.2, 0.3, 0.1,0.2,0.15,0.05)那么变为onehot是(0,1,0,0,0,0),如果softmax输出是(0.2, 0.25, 0.2, 0.1,0.15,0.1 ),one-hot仍然是(0, 1, 0, 0, 0, 0),所以对于生成器来说,G输出了不同的结果但是D给出了同样的判别结果,并不能将梯度更新信息很好的传递到G中去,所以D最终输出的判别没有意义。
- GAN的损失函数是JS散度,JS散度不适合衡量不相交分布之间的距离。
训练GAN的技巧
- 输入规范化到(-1,1)之间,最后一层的激活函数使用tanh(BEGAN除外)
- 使用wassertein GAN的损失函数
- 如果有标签数据的话,尽量使用标签,也有人提出使用反转标签效果很好,另外使用标签平滑,单边标签平滑或者双边标签平滑
- 使用mini-batch norm, 如果不用batch norm 可以使用instance norm 或者weight norm
- 避免使用RELU和pooling层,减少稀疏梯度的可能性,可以使用leakrelu激活函数
- 优化器尽量选择ADAM,学习率不要设置太大,初始1e-4可以参考,另外可以随着训练进行不断缩小学习率
- 给D的网络层增加高斯噪声,相当于是一种正则
GAN理论缺陷和应用缺陷
理论缺陷
- 原始GAN在判别器\(D\)趋于最优时,会面临梯度消失问题
- 采用\(-logD\)技巧改进版本的生成器会同样存在一些问题,包括训练梯度不稳定、惩罚不平衡导致的模式坍塌(缺乏多样性)、不好判断收敛性以及难以评价生成数据的质量和多样性
应用缺陷
- GAN在理论上的收敛性不能保证实际应用的收敛性
- 训练时可能导致局部纳什均衡,出现模式坍塌
- 何时应该停止训练,以及生成数据的好坏的评估,缺乏理想的评价方法和准则
参考
- 《神经网络与深度学习》——邱锡鹏著
- 莫烦python
- GAN的原理 优缺点