GAN、Policy Gradient、Sequence Generation和SeqGAN

最近看了台湾大学李宏毅老师关于深度学习的系列教程(Machine Learning and Having It Deep and Structured),收获颇多。教程主要介绍了深度学习的基本知识,并且介绍了大量比较新的技术,例如Seq2Seq、Attention、Generative Model、Language Model等等。与以往其他“以公式为中心”的教程不同,这个教程从“定性”的角度介绍了这些技术,能让我们详细了解到这些技术存在的合理性。同时又有着详细的公式推导,并且将事实之间的逻辑关系梳理的非常清晰。

这篇文章将会以李宏毅老师的讲解为范本,详细介绍使用GAN进行序列生成的相关工作。


序列生成

在NLP领域中,序列生成是一个很常见的主题。所谓序列生成就是指通过某种输入(如经过编码的其他序列、历史信息等),并得到一个输出序列(句子、语音、视频等时间序列的内容)。深度学习领域中的序列生成基本上都是基于Encoder Decoder或Sequence to Sequence框架。

Encoder Decoder框架通常包含一个Encoder和一个Decoder(Generator)。Encoder的目的是将历史信息、输入数据等内容编码到一个编码空间中。这个编码空间的一个向量将能表示所有输入数据中的信息。而Generator则会根据任务的设计,以Encoder输出的一个编码为输入,得到一个输出序列。

由于生成的序列不一定是定长的,所以一般我们使用递归网络模型(如RNN、LSTM等)作为生成器。生成器网络每一个时间步同时输入输入信息的编码和上一个时间步网络产生的输出,并且给出一个输出。重复这个过程,就得到了一个输出序列。

在训练时,我们的目的是让网络尽可能得到与训练数据相同的输出,所以我们可以首先计算每次输出和对应训练语料之间的交叉熵,然后再对整个序列上所有的交叉熵求和,最终得到的总损失作为损失函数。有了损失函数之后就能够进行反向传播、梯度下降了。

假设现有训练样本:\((h,\hat{x})\),其中\(h\)为输入句子和历史(上下文)数据。\(\hat{x}\)为应该的输出(词序列)。假设\(\hat{x}_t\)为词序列中的第\(t\)个词,\(\hat{x}_{1:t}\)为序列中的前\(t\)个词,损失函数\(C\):

$$ \begin{align} C&=-\sum_t\log P(\hat{x}_t|\hat{x}_{1:t-1}, h) \\
&=\log [P(\hat{x}_1|h)\times P(\hat{x}_2|\hat{x}_{1:2},h)\times P(\hat{x}_3|\hat{x}_{1:3},h)\dots\times P(\hat{x}_T|\hat{x}_{1:T-1},h)]\\ &=-\log P(\hat{x}|h) \end{align} $$

当给定要给训练样本\(h,\hat{x}\),当生成器接收到\(h\),则我们想要让生成器输出越接近\(\hat{x}\)越好。这就是传统的训练Sequence Generator的方法(MLE)。

Reinforcement Learning for Sequence Generation

前面提到的传统的训练方法是以具备完整的训练数据为前提的。即我们需要拥有一个输出样本组成的训练集。但是现在有另外一种情况:当我们不知道训练样本,只知道输出到最后外界能提供一个反馈时,应该如何训练这个模型呢?

这个情况其实挺常见的。例如我们想要训练一个聊天机器人,我们可以不用传统的方法来教会机器人“该说什么”。相反,机器人可以从我们对机器人说出的话的反馈中得到学习。比如说,当机器人的回答与我们的提问完全不相干时,我们可以给出一个负反馈,反之亦然。此时我们不再是去知道机器人“说我们想让他说的话”,而是让机器人“随便说话”,我们来评判机器人说的话“对不对”。

这个过程,就是一个增强学习过程:我们并不能对学习过程中采取的每一个动作做标注。但是我们可以对动作最终的结果产生一个评估。也就是说,整个模型和训练过程变成了这样:

首先输入数据是\(h\),经过由\(\theta\)控制的Encoder和Generator之后产生了一个序列\(x\)。这个\(x\)将会由人类进行评判。所以我们的目的是找到能让这个回报\(R\)最大的参数\(\theta\):

$$ \theta=\arg\max_\theta \bar R_{\theta} \\ \bar R_{\theta}=\sum_h P(h)\sum_x R(h,x)P_{\theta}(x|h) $$

其中\(P(h)\)表示输入/上下文信息是\(h\)的概率,后面对\(x\)求和是因为生成器具备了一定的随机性。也就是说,即便输入了相同的\(h\),每次得到的\(x\)都是有可能不同的。而表示这个随机性的概率分布就是\(P_\theta(x|h)\)。所以后面我们需要对所有可能的\(x\)都做一个求和(每一个\(x\)都有由\(P_\theta(x|h)\)决定的权重)。

一般来说,有概率权重的时候就是有计算期望的时候!有期望的时候就是能把不可能的求和(积分)变成可能的采样算法的时候:

$$ \begin{align} \bar R_\theta&=\sum_h P(h)\sum_xR(h,x)P_{\theta}(x|h) \\ &=E_{h\sim P(h)}\left[E_{x\sim P_\theta(x|h)}[R(h,x)]\right] \\ &=E_{h\sim P(h),x\sim P_\theta(x|h)}[R(h,x)] \\ &\approx \frac{1}{N}\sum_{i=1}^NR(h^i, x^i) \end{align} $$

后面用了约等于,就是因为我们用了采样的方法来模拟进行这个求和操作。而样本就是从那两个分布中采集得到的\((h^1,x^1), (h^2,x^2),\dots,(h^N,x^N)\)等等。

有了期望回报函数,我们现在只需要对参数求个微分就能用梯度上升来最大化期望回报了。但是。。。公式中的\(\theta\)在哪????

没错,看一看公式就会发现,我们的“替代期望回报函数”中并没有参数\(\theta\)。因为\(\theta\)的作用被隐藏在了样本采样的过程中,当有了样本,就不再有\(\theta\)什么事儿了。所以算法没法继续进行了。

策略梯度 Policy Gradient

虽然说上面的操作没法进行,但其实我们可以通过一些巧妙的变化让这个参数\(\theta\)重新出现在期望回报函数的微分表达式中(式子中的\(\nabla\)是对\(\theta\)进行的):

$$ \begin{align} \bar R_\theta&=\sum_hP(h)\sum_xR(h,x)P_\theta(x|h) \\ \nabla\bar R_\theta&=\sum_hP(h)\sum_xR(h,x)\nabla P_\theta(x|h) \\ &=\sum_hP(h)\sum_xR(h,x)P_\theta(x|h)\frac{\nabla P_\theta(x|h)}{P_\theta(x|h)} \\ &=\sum_hP(h)\sum_xR(h,x)P_\theta(x|h)\nabla\log P_\theta(x|h) \\ &=E_{h\sim P(h), x\sim P_\theta(x|h)}[R(h,x)\nabla\log P_\theta(x|h)] \\ &\approx\frac{1}{N}\sum_{i=1}^NR(h^i,x^i)\nabla\log P_\theta(x|h) \end{align} $$

公式的第三行到第四行是因为\(\frac{d\log(f(x))}{dx}=\frac{1}{f(x)}\frac{df(x)}{dx}\)。这时候再看我们的回报函数中包含了\(P_\theta\),也就包含了\(\theta\)。所以就能求微分了!

从公式中我们能发现,当\(R(h^i,x^i)\)是正回报时,\(\theta\)更新后\(P_\theta(x^i|h^i)\)将会增加。反之当\(R(h^i,x^i)\)是负回报时,\(\theta\)更新后\(P_\theta(x^i|h^i)\)将会减少。也就是说,如果最终的回报是正的,那么导致这个结果的所有中间动作(词)都会被鼓励。

我们可以对比一下增强学习和之前的MLE极大似然估计的方法:

  • 目标函数
    • 极大似然:\(\frac{1}{N}\sum_{i=1}^N\log P_\theta(\hat{x}^i|h^i)\)
    • 增强学习:\(\frac{1}{N}\sum_{i=1}^NR(h^i,x^i)\log P_\theta(x^i|h^i)\)
  • 梯度
    • 极大似然:\(\frac{1}{N}\sum_{i=1}^N\nabla\log P_\theta(\hat{x}^i|h^i)\)
    • 增强学习:\(\frac{1}{N}\sum_{i=1}^NR(h^i,x^i)\nabla\log P_\theta(x^i|h^i)\)
  • 训练数据
    • 极大似然:\(\{(h^1,\hat{x}^1),\dots,(h^N,\hat{x}^N)\}\),这相当于增强学习中对所有的\(i\)有:\(R(h^i,\hat{x}^i)=1\)
    • 增强学习:\(\{(h^1,x^1,\dots,(h^N,x^N))\}\),每个样本对训练的影响为\(R(h^i,x^i)\)
带有Baseline的Policy Gradient

由于我们的\(P_\theta(x|h)\)是一个概率,所以它需要保证和为一。理想情况下,我们取到的每一个能让\(R(h^i,x^i)>0\)的样本都应该得到或大或小的概率上的提升,提升的概率应该从那些让回报函数为负的样本中分得。如下图:

但实际情况下,由于我们使用的是采样的算法,所以一些没有被采样得到的能让回报函数为正的样本也会被剥夺一部分概率值。从而导致这个原本正确的样本很难被采样得到!如下图:

于是在实际使用中,我们经常用这样的一个损失函数来代替原来的损失函数:

$$ \frac{1}{N}\sum_{i=1}^N(R(h^i,x^i)-b)\log\nabla P_\theta(x^i|h^i) $$

如此一来,那些并不大的正回报会被削减成负回报,而那些非常大的正回报则不会被改变,这样一来,那些没有被采样到的正回报样本的概率就不会被直接瓜分,情况如下图:

SeqGAN

上面介绍了用增强学习的方法来训练一个序列生成模型。说到生成模型,就不得不提GAN了,前面我们也仔细介绍过GAN。前面介绍的GAN大多是用来产生图片,那么它是否也可以被用来生成序列呢?比如说生成一句话?

对于序列生成这个问题来说,一个GAN模型基本可以描述成这个样子:

模型包含了一个生成器,它输入一个编码空间的\(z\),然后产生一个句子\(x\)。然后将这个句子送入到判别器,判别器来识别一个句子是真实的句子还是生成的句子。

整个算法也跟平时的方法一样:

  1. 从数据集中随机采样真实的句子\(x\)
  2. 用生成模型产生假句子\(\tilde{x}\)
  3. 更新判别器,让\(D(x)\)变得更大,\(D(\tilde{x})\)变得更小
  4. 更新生成器来减小判别器识别正确的概率。

这个过程描述起来很简单,但实际上想要训练它,还需要一些小技巧。

回想一下使用MLE时训练模型的算法,将每一个时间步的输出与该时间步下的样本比较,从而定义损失函数,然后优化这个损失函数完成训练。但是对于GAN来说,生成器所产生的输出将会被打包输入到判别器中,然后我们将损失函数定义在判别器的输出上。但是判别器输出的这个标量并不能被用来优化:我们没法对它进行微分。这是因为微分的本质就是对输入的微小变化导致的输出的变化。由于我们在每一个时间步的输出,是softmax之后最大的那个对应的词。这个“最大”就导致即便我们的输入做了微小的变化,但这个“最大值”仍然有可能是同一个结果,所以说判别器的输出是不可微的。

SeqGAN则用了一种很直观的思想,那就是增强学习。SeqGAN将判别器作为评价函数,即:

$$ \require{cancel} \nabla\bar R_\theta\approx\frac{1}{N}\sum_{i=1}^N(\underbrace{\cancel{R(h^i, x^i)}}_{D(h^i,x^i)}-b)\nabla\log P_\theta(x^i|h^i)
$$

其实就是将原本代价函数中的\(R(h,x)\)替换成了判别器\(D(h,x)\)。SeqGAN与增强学习唯一不一样的地方就是,在增强学习中,评价尺度是一成不变的,而SeqGAN的评价尺度是判别器,而判别器在训练过程中是变化的。除此之外完全相同。

Reward for Every Generation Step

在SeqGAN之后,还有一个对它进行增强的版本。假如说我们此时在训练对话生成。\(h^i\)是“What is your name?”,\(x^i\)是“I don't know.”。这显然不是一个很好的回答,所以\(D(h^i,x^i)-b\)将会是一个负值。也就是说我们在更新\(\theta\)之后,\(\log P_\theta(x^i|h^i)\)将会减小。将这个函数展开:

$$ \begin{align} \log P_\theta(x^i|h^i)&=\log P(x^i_1|h^i)+\log P(x_2^i|h^i,x_1^i)+\log P(x^i_3|h^i,x^i_{1:2}) \\ &=\log P(\text{"I"}|h^i)+\log P(\text{"don't"}|h^i, \text{"I"})+\log P(\text{"know"}|h^i, \text{"I don't"}) \end{align} $$

由于\(\log P_\theta(x^i|h^i)\)是由后面三项的加和组成的,所以如果我们想要让\(\log P_\theta(x^i|h^i)\)减小,那就让这三项都减小。但是这是有问题的。因为当产生第一个词“I”的时候,我们没有理由认为这一项出现的机率应该被减小,因为有可能最终生成的内容是“I am xxx”。

这个想法也很直觉。在增强学习的例子中,如果我们最终结果是差的,那就认为整个序列过程中每一个动作都是差的,这显然是不合理的。所以SeqGAN也会遇到同样的问题。

不过,理论上只要采样的数据量足够大,这个问题并不是个大问题。因为数据集中有可能出现“I am xxx”这样的序列。这个时候上面三项中的第一项的概率就会有上升。

但实际上,我们的采样是有限的,如果我们只采样到了“I don't know”,而没有采样到“I am xxx”,那这个“I放在句首”的概率仍然会被压制。

所以如果能有一个方法让第一项概率增大,后面两项概率减小就好了。实际上真的有这个方法。

前面方法效果不好是由于打分的粒度是整个句子。如果我们将打分的粒度减小到句子中的词,那不同的词项就会得到不同的分数,这样“I”在句首的概率不会因为出现太多的“I don't know”而降低。

具体做法就是将原来的式子

$$ \nabla\bar R_\theta\approx\frac{1}{N}\sum_{i=1}^N(D(h^i,x^i)-b)\nabla\log P_\theta(x^i|h^i) $$

替换成

$$ \nabla\bar R_\theta\approx\frac{1}{N}\sum_{i=1}^N\sum_{t=1}^T(Q(h^i,x^i_{1:t})-b)\nabla\log P_\theta(x^i_t|h^i,x^i_{1:t-1}) $$

现在每一项都是由\(Q(h^i,x_{1:t}^1)\)来打分,而不是对整个句子进行打分。

那么这个打分函数\(Q\)怎么算?有两个方法,一个叫做Monte Carlo Search,另一个叫做Discriminator for Partially Decoded Sequences。

Monte Carlo Search

假如现在有一句话:\(Q(\underbrace{\text{"What is your name?"}}_{h^i},\underbrace{\text{I}}_{x_1^i})\)

想要估计它,我们首先从Generator中采样许多以“I”起始的句子,例如:

$$ \begin{align} x^A&=\text{I am John} \\
x^B&=\text{I am happy} \\
x^C&=\text{I don't know} \\
x^D&=\text{I am superman}
\end{align} $$

然后用我们的判别器\(D\)对每一个句子都打个分数,如:

$$ \begin{align} D(h^i,x^A)=1.0
D(h^i,x^B)=0.1
D(h^i,x^C)=0.1
D(h^i,x^D)=0.8
\end{align} $$

“I am John”应该比较正确,所以值是1.0。“I am happy”和“I don't know”应该很不正确,所以值是0.1。“I am superman”也还算靠谱,所以值是0.8。

在得到这些分数之后,把不同的样本的值平均起来:

$$ Q(h^i,\text{"I"})=\frac{1}{4}D(h^i,x^A)+D(h^i,x^b)+D(h^i,x^c)+D(h^i,x^d)=0.5
$$

这个值就作为以“I”起首的项的分数值。

Rewarding Partially Decoded Sequences

还有另一个方法,就是让判别器在句子生成的过程中判断某一个生成了一半的句子有多好。

假如说我们认为\((h=\text{"What is your name?"},x=\text{"I am John"})\)是正例,而\((h=\text{"What is your name?"},x=\text{"I don't know})\)是负例。现在我们要做的是能让判别器在句子没生成完时,就能判断这个句子好不好。这件事如何做?

首先我们将这些样本的\(x\)部分打散,产生更多的样本,即:

$$ \begin{align} \text{Positive} &\rightarrow (h=\text{"What is your name?"},x=\text{"I am John"}) \\ \text{Positive} &\rightarrow (h=\text{"What is your name?"},x=\text{"I am"}) \\ \text{Positive} &\rightarrow (h=\text{"What is your name?"},x=\text{"I"}) \\ \text{Negative} &\rightarrow (h=\text{"What is your name?"},x=\text{"I don't know"}) \\ \text{Negative} &\rightarrow (h=\text{"What is your name?"},x=\text{"I don't"}) \\ \text{Negative} &\rightarrow (h=\text{"What is your name?"},x=\text{"I"}) \\ \end{align} $$

把这些例子都加入到训练样本中用来训练。并且将原来的判别器(输入\(h\)和\(x\)输出标量)替换成一个新的判别器(输入\(h\)和\(x_{1:t}\),输出\(Q(h,x_{1:t})\)):

Teacher Forcing

还有一招可以用在SeqGAN的训练上。之前我们谈到过,在训练GAN的时候非常不稳定。有一个原因是生成器刚开始太弱,判别器太强。生成器无论怎么训练都是错的,所以在训练过程中它没机会采样到一个正例,根本就不知道正确的东西应该是什么样的,所以就无法训练。这就好像让一个小学生速成大学课程,很难通过简单的方法教会他。现在有一个办法能让生成器往正确的方向训练。

原来在训练GAN时,我们首先产生一些样本\(\{(h^1,x^1,\dots,(h^N,x^N))\}\),然后把这些样本送到判别器中给一个标注,再用这些样本去训练。但由于刚开始生成器都很弱的时候,这些样本的每一个\(D(h^i,x^i)\)都很小。这个时候我们可以再添加一些从真实数据中采样的数据\(\{(h^1,\hat{x}^1,\dots,(h^N,\hat{x}^N))\}\),然后让这些数据的\(D(h^i,\hat{x}^i)=1\)。这样相当于再\(G\)很弱的时候为它提供要给指导,告诉他一些正确答案。

其实感觉起来SeqGAN跟GAN关系并不大,只不过是将原本“人肉Discriminator”换成了GAN中的Discriminator。其对距离的度量等等都并不是真的GAN的那一个流程。

以上就是关于增强学习、Policy Gradient、Sequence Generation和SeqGAN相关的内容。

Friskit

继续阅读此作者的更多文章