从0开始GAN-2-sequence generation by GAN

paper list

为什么GAN不适合文本生成

前面学过了GAN很自然的就会想到将GAN引入到文本生成中来,比如对话可以看作是conditional GAN, 但实际上却并不如想象中那样简单,原因是GAN只适用于连续数据的生成,对离散数据效果不佳。

Role of RL in Text Generation by GAN(强化学习在生成对抗网络文本生成中扮演的角色) 这里面从两方面讲的很清楚:

  • sampling:从生成得到的softmax probability到one-hot向量,从而查询出对应index的词,这一步称为“sampling”,显然是不可微的。
  • 去掉sampling,将softmax probability和one-hot vector作为discriminator的输入,如果是discriminator是一个二分类器的话,判别器D很容易“作弊”,它根本不用去判断生成分布是否与真实分布更加接近,它只需要识别出给到的分布是不是除了一项是 1 ,其余都是 0 就可以了。因此,我们也可以想到用WGAN来解决这个问题。Improved Training of Wasserstein GANs也给出了文本生成的实验,效果当然是好了很多,不至于直接崩了。

但是WGAN为什么没那么好呢?将一个softmax probability强行拉倒一个one-hot vector真的可行吗?

Gumbel-softmax,模拟Sampling的softmax

RL in text generation

reinforcement learning

reinforcement learning 和监督学习、非监督学习一起构成机器学习的三大范式。

Reinforcement learning (RL) is an area of machine learning concerned with how software agents ought to take actions in an environment so as to maximize some notion of cumulative reward.
It differs from supervised learning in that labelled input/output pairs need not be presented, and sub-optimal actions need not be explicitly corrected. Instead the focus is finding a balance between exploration (of uncharted territory) and exploitation (of current knowledge)

RL所适用的环境是一个典型的马尔科夫决策过程(Markov decision process,MDP)。所以强化学习实际上也可以看作是一种动态规划的方法。不过与传统的dynamic programming方法不同的是,RL不会假设MDP的精确数学模型的知识。我的理解是,在很多DP问题中,状态转移矩阵是已知的,但是RL所处理的问题,从一个状态到另一个状态,不是根据已有的知识,而是取决于当前action带来的reward以及未来的reward,所以这也就涉及到了 exploration 和 exploitation 的平衡问题。

Markov decision process 包括:GANs-in-NLP/Reinforcement_learning_diagram.png - 环境以及agent状态的集合 S;
- agent能采取的动作的集合 \(A\)
- 状态之间转换的规则 \(P_a(s,s')=Pr(s_{t+1}=s'|s_t=s,a_t=a)\)
- 规定转换之后的即时奖励 \(R_a(s,s')\)
- 描述主体能够观察到什么的规则(这是啥玩意??)

policy

将从头到尾所有的动作连在一起就称为一个“策略”或“策略路径” \(pi\) ,强化学习的目标就是找出能够获得最多奖励的最优策略.
\(\pi: A\times S \rightarrow [0,1]\)
\(\pi(a,s)=Pr(a_t=a|s_t=s)\)

state-value function

状态-值函数 \(V_{\pi}(s)\) 定义在当前状态 s下,按照策略 \(\pi\) 接下来能获得的 reward.也就是说,given state s,当前以及未来的reward期望.
\[V_{\pi}(s)=E[R]=E[\sum_{t=0}^{\infty}\gamma^tr_t|s_0=s]\]
其中 \(\gamma^t\) 是折扣因子,因为还是当前利益最重要嘛,所以未来的reward要打个折。 \[R=\sum_{t=0}^{\infty}\gamma^tr_t\]

value function

value funcion 和 state-value function 的区别是后者给定了一个 state. 而value function是计算给定任意初始状态,得到的reward. \[V^{\pi}=E[R|s,\pi]\]

所以最优的 policy 实际上就是 value function 的期望最大。\(\rho^{\pi}=E[V^{\pi}(S)]\), 其中状态S是从一个分布 \(\mu\) 随机采样得到的。

尽管 state-value 足够定义最优 policy,再定义一个 action-value 也是很有用的。 given state s, action a, policy \(\pi\), action-value: \[Q^{\pi}(s,a)=E[R|s,a,\pi]\]

个人理解,在强化学习的应用场景中,很多时候是由 action 来确定下一个 state 的。所以 action-value 这个function会更实用吧。比如 text generation,sample当前词就是 action,然后才有下一个时刻的 state.

Monte Carlo methods

Temporal difference methods

RL应用到对话场景下

Deep Reinforcement Learning for Dialogue Generation 对话生成任务本身非常符合强化学习的运行机理(让人类满意,拿奖励)。

输入句子是 h,模型返回的response是 x,其从人类得到的奖励是 \(R(h,x)\). 基于RL的目标函数就是最大化对话的期望奖励。上图中 \(p_{\theta}(x,h)\) 表示在 \(\theta\) 参数下,一组对话 \((x,h)\) 出现的概率。\(P(h)\) 表示出现句子 h 的概率。

最大化奖励期望: \[公式(1)\] - 上式中 \(h\sim P(h)\) 可以看作是均匀分布,所以 \(E_{h\sim P(h)}\approx \dfrac{1}{N}\).
- 其中 \(E_{x\sim P_{\theta}(x|h)}\) 的计算无法考虑所有的对话,所以通过采样 \((h^1,x^1), (h^2,x^2), .., (h^N,x^N)\) 来计算。

然后问题来了,我们需要优化的参数 \(\theta\) 不见了,这怎么对 \(\theta\) 进行求导呢?可以采用强化学习中常用的 policy gradient 进行变形: \[\dfrac{dlog(f(x))}{dx}=\dfrac{1}{f(x)}\dfrac{df(x)}{dx}\]

适当变形后,对 \(\theta\) 进行求导:
\[公式(2)\]

这样一来,梯度优化的重心就转化到了生成对话的概率上来,也就是说,通过对参数 \(\theta\) 进行更新,奖励会使模型趋于将优质对话的出现概率提高,而惩罚则会让模型趋于将劣质对话的出现概率降低。

自AlphaGo使得强化学习猛然进入大众视野以来,大部分对于强化学习的理论研究都将游戏作为主要实验平台,这一点不无道理,强化学习理论上的推导看似逻辑通顺,但其最大的弱点在于,基于人工评判的奖励 Reward 的获得,让实验人员守在电脑前对模型吐出来的结果不停地打分看来是不现实的,游戏系统恰恰能会给出正确客观的打分(输/赢 或 游戏Score)。基于RL的对话生成同样会面对这个问题,研究人员采用了类似AlphaGo的实现方式(AI棋手对弈)——同时运行两个机器人,让它们自己互相对话,同时,使用预训练(pre-trained)好的“打分器”给出每组对话的奖励得分 R(a^i, x^i) ,关于这个预训练的“打分器” R ,可以根据实际的应用和需求自己DIY。

SeqGAN

seqGAN对前面仅基于RL的对话生成进行了改进,也就是前面用pre-trained的打分器(或者是人类),用GAN中的判别器进行了代替。

这里问题在于生成得到的response x输入到判别器时,这个过程涉及到了sampling的操作,所以固定discriminator来更新generator时,梯度无法回流。

这就需要RL的出现了。

总结一下RL在这里面的作用:这里的discriminator得到的是reward。我们fix住判别器D来优化生成器 \(\theta\) 的过程就变成了:生成器不再是原来的sample一个词,作为下一个time step的输入,因为这不可导。而是把当前time step作为一个state,然后采取action,这个action当然也是在词表中选一个词(用Monte Carlo Search). 以前是通过最大化似然概率(最小化交叉熵)来优化生成器,现在是寻找最优的 policy(最大化奖励期望)来优化生成器。而采用policy gradient可以将reward期望写成 \(\theta\) 的连续函数,然后就可以根据最大化reward期望来优化 \(\theta\),也就是梯度上升。

有了前面的基础再重新阅读seqGAN这篇paper.

motivation

传统的GAN在序列生成的能力有限主要是两个原因:
1. 无法处理离散的数据(前面已经讲过了)
2. 判别器D只能对完整的序列进行评价(原因是判别器就是基于完整的句子或dialogue进行训练的)。但是在序列生成的过程中,在生成部分序列的时候,对当前部分序列的评价也是很重要的。

传统的基于 RNN/attention 的序列生成模型也存在 exposure bias 的问题,也就是训练阶段和inference阶段不一致的问题。在训练阶段是teacher forcing,而在infer阶段,下一个词的预测仅仅依赖于当前的隐藏状态(attention-based会有attention vector). Bengio 的弟弟,另一个 Bengio 提出了 scheduled sampling 的方法,但这依然未能完全解决这个问题。

为此,作者提出基于RL的seqGAN。对序列生成的问题进行建模,把序列生成问题看作是马尔可夫决策过程(Data generation as sequential decision making),从而转换成基于RL的寻找最优policy的问题,有效的解决了上述三个问题。

Sequence Generative Adversarial Nets

这里先介绍一些数学符号:

我们的目的是训练得到一个生成模型 \(G_{\theta}\),使其能生成得到这样的一个序列 \(Y_{1:T}=(y_1,...,y_t,...,y_T)\). 其中 \(y_t\sim V\). V是候选词表。用RL来描述序列生成的过程就是:
1. 当前时间步 t 的状态 state s: \((y_1,...,y_{t-1})\)
2. action a 是选择下一个 token \(y_t\).
3. policy也就是生成模型 \(G_{\theta}(y_t|Y_{1:t-1})\)
4. 状态的转移取决于 action a. 比如状态转移的概率 \(\sigma_{s,s'}^a=1\),也就是在当前状态 \(s=Y_{1:t-1}\) 情况下,下一个状态是 \(s'\) 的概率为1,那么下一个状态是 \(s'=Y_{1:t}\),对应的action也就是 \(a=y_t\).

首先我们需要训练一个判别模型 \(D_{\phi}(Y_{1:T})\), 通过判断输入来自 real or fake 进行训练。而生成器的训练需要借助于判别器D的输出,也就是 reward.

SeqGAN via Policy Gradient

如果不考虑中间每一个时间步的奖励,也就是只考虑整个sentence的reward, 那么基于生成模型(policy)\(G_{\theta}(y_t|Y_{1:t-1})\) 的最大奖励期望的函数是: \[J(\theta)=E[R_T|s_0,\theta]=\sum_{y\sim V}G_{\theta}(y|s_0)\cdot Q_{D_{\phi}}^{G_{\theta}}(s_0,y)\]

其中 \(R_T\) 是对整个sentence的奖励, \(G_{\theta}(y|s_0)\) 是 given \(s_0\),生成 \(y\) 的概率,\(Q_{D_{\phi}}^{G_{\theta}}(s_0,y )\) 是 action-value 函数,也就是 given \(s_0\) 和 policy \(G_{\theta}\) 后采取的 action 是 \(y\) 时对应的 reward. 在这篇论文里面,reward 就是判别器判断生成的sentence为real的概率。

\[Q_{D_{\phi}}^{G_{\theta}}(a=y_T,s=Y_{1:T-1})=D_{\phi}(Y_{1:T})\]

但是对于序列生成问题,不能仅仅考虑完整的句子的reward,还要考虑到每一个 time step. 但是在每一个time step也不能贪心的只考虑当前最大的reward,还要考虑到未来的情况. 作者提出基于 Monte Carlo search 的方法。

Monte Carlo methods can be used in an algorithm that mimics policy iteration. Policy iteration consists of two steps: policy evaluation and policy improvement. Monte Carlo is used in the policy evaluation step. In this step, given a stationary, deterministic policy \({\displaystyle \pi }\), the goal is to compute the function values \({\displaystyle Q^{\pi }(s,a)}\) (or a good approximation to them) for all state-action pairs \({\displaystyle (s,a)}\). Assuming (for simplicity) that the MDP is finite, that sufficient memory is available to accommodate the action-values and that the problem is episodic and after each episode a new one starts from some random initial state. Then, the estimate of the value of a given state-action pair \({\displaystyle (s,a)}\) can be computed by averaging the sampled returns that originated from \({\displaystyle (s,a)}\) over time. Given sufficient time, this procedure can thus construct a precise estimate \({\displaystyle Q}\) of the action-value function \({\displaystyle Q^{\pi }}\). This finishes the description of the policy evaluation step.
policy iteration分为两个步骤,policy evaluation和policy improvement.蒙特卡洛被用在policy evaluation step中,给定一个静态的,判别型的policy \(\pi\),其目标是计算

具体来说,在当前状态 \(s=Y_{1:t}\) 下,基于一个 roll-out policy \(G_{\beta}\) 生成剩下的 T-t 个tokens,这个过程重复 N 次. \[\{Y_{1:T}^1,...,Y_{1:T}^N\}=MC^{G_{\beta}}(Y_{1:t;N})\] 式子左边是 N 个完整的sentence。 对于 roll-out policy \(G_{\beta}\) 作者在这篇 paper 中采用的与生成模型一样的 \(G_{\theta}\). 如果追求速度的话,可以选择更简单的策略。

这样基于 Monte Carlo method 就能计算每一个 time step 的能考虑到 future 的reward.

\[Q_{D_{\phi}}^{G_{\theta}}(s=Y_{1:t-1}, a=y_t)= \begin{cases} \dfrac{1}{N}\sum_{n=1}^ND_{\phi}(Y_{1:T}^n),Y_{1:T}^n \sim MC^{G_{\beta}}(Y_{1:t;N}), \quad \text{for t < T}\\ D_{\phi}(Y_{1:t}),\quad\text{for t = T} \end{cases}\quad (4)\]

公式还是比较好理解的。所以事实上判别器 \(D_{\phi}\) 依旧是只能判断完整的sentence,但是在每一个 time step 可以借助于 roll-out policy 来得到完整的sentence,进而对当前 action 进行评分,计算得到 \(a=y_t\) 的reward。

知道了如何计算reward,就可以利用最大化这个奖励期望来优化我们的生成器(policy \(G_{\theta}\)).对 \(\theta\) 求导: \[\nabla J(\theta)=\sum_{t=1}^T\mathbb{E}_{Y_{1:t-1}\sim G_{\theta}}[\sum_{y_t\sim V}\nabla_{\theta}G_{\theta}({y_t|Y_{1:t-1}})\cdot Q_{D_{\phi}}^{G_{\theta}}(Y_{1:t-1},y_t)]\quad\text{公式(3)}\]

公式(3)与前面李弘毅老师讲的公式(2)是一致的,只不过这里考虑的中间 reward.上式中 \(E_{Y_{1:t-1}\sim G_{\theta}}[\cdot]\) 等同于前面提到的 \(E_{x\sim P_{\theta}(x|h)}\) 都是通过sample 来计算的。同样 reward 的计算式 \(Q_{D_{\phi}}^{G_{\theta}}(Y_{1:t-1},y_t)\) 也是不包含生成器的参数 \(\theta\) 的。

上述公式中 \(\sum_{y_t\sim V}\sim G_{\theta}(y_t|Y_{1:t-1})\)

然后基于梯度上升来优化参数 \(\theta\). \[\theta \leftarrow \theta + \alpha_h\nabla J(\theta)\quad(8)\] 作者建议使用 Adam 或 RMSprop 优化算法。

除了生成器的优化,这里的判别器D是动态的。这样相比传统基于pre-train的判别器会更叼吧。优化判别器的目标函数是: \[\min_{\phi}-\mathbb{E}_{Y\sim p_{data}}[logD_{\phi}(Y)]-\mathbb{E}_{Y\sim G_{\theta}}[log(1-D_{\phi}(Y))]\quad(5)\]

具体的算法步骤是:

And to reduce the vari- ability of the estimation, we use different sets of negative samples combined with positive ones, which is similar to bootstrapping (Quinlan 1996)

The Generative Model for Sequences

作者使用基于 LSTM 的生成器G。 \[h_t=g(h_{t-1},x_t)\] \[p(y_t|x_1,...,x_t)=z(h_t)=softmax(c+Vh_t)\]

The Discriminative Model for Sequences

作者使用基于 CNN 的判别器,用来预测一个sentence为real的概率。

一些细节 + 一些延伸

到目前为止,基本理解了seqGAN的大部分细节,需要看看源码消化下。
接下来会有更多的细节和改进可先参考:Role of RL in Text Generation by GAN(强化学习在生成对抗网络文本生成中扮演的角色)

seagan 代码学习

TensorArray 和 基于lstm的MDP模拟文本生成

这也是seqgan的核心,用Monte Carlo search代替sampling来选择next token.在看具体代码之前先了解下 tensorarray.

TensorArray

Class wrapping dynamic-sized, per-time-step, write-once Tensor arrays This class is meant to be used with dynamic iteration primitives such as while_loop and map_fn. It supports gradient back-propagation via special "flow" control flow dependencies. 一个封装了动态大小、per-time-step 写入一次的 tensor数组的类。在序列生成中,序列的长度通常是不定的,所以会需要使用动态tensorarray.

类初始化
1
2
3
4
5
6
7
8
9
10
11
12
def __init__(self,
dtype,
size=None,
dynamic_size=None,
clear_after_read=None,
tensor_array_name=None,
handle=None,
flow=None,
infer_shape=True,
element_shape=None,
colocate_with_first_write_call=True,
name=None):
  • size: int32 scalar Tensor, 动态数组的大小
  • dynamic_size: Python bool, 是否可以增长,默认false
方法
  • stack
    1
    2
    3
    def stack(self, name=None):
    """Return the values in the TensorArray as a stacked `Tensor`.
    """

将动态数组 stack 起来,得到最终的 tensor.

  • concat
    1
    2
    3
    def concat(self, name=None):
    """Return the values in the TensorArray as a concatenated `Tensor`.
    """

将动态数组 concat 起来,得到最终的 tensor.

  • read

    1
    2
    3
    4
    def read(self, index, name=None):
    """Read the value at location `index` in the TensorArray.
    读过一次之后会清0. 不能读第二次。但可以再次写入之后。
    """

  • write

    1
    2
    3
    4
    5
    def write(self, index, value, name=None):
    """Write `value` into index `index` of the TensorArray.
    """
    - index: int32 scalar with the index to write to.
    - value: tf.Tensor

  • gather
  • unstack
  • split
  • scatter

tf.while_loop
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def while_loop_v2(cond,
body,
loop_vars,
shape_invariants=None,
parallel_iterations=10,
back_prop=True,
swap_memory=False,
maximum_iterations=None,
name=None):
"""Repeat `body` while the condition `cond` is true.
"""
- cond: callable, return boolean scalar tensor. 参数个数必须和 loop_vars 一致。
- body: vallable. 循环执行体,参数个数必须和 loop_vars 一致.
- loop_vars: 循环变量,tuple, namedtuple or list of numpy array.
example:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
matrix = tf.random.normal(shape=[5, 1], dtype=tf.float32)
sequence_length = 5
gen_o = tf.TensorArray(dtype=tf.float32, size=sequence_length,
dynamic_size=False, infer_shape=True)
init_state = (0, gen_o)
condition = lambda i, _: i < sequence_length
body = lambda i, gen_o : (i+1, gen_o.write(i, matrix[i] * 2))
n, gen_o = tf.while_loop(condition, body, init_state)
gen_o_stack = gen_o.stack()
gen_o_concat = gen_o.concat()用 LSTM 模拟马尔科夫决策过程

print(gen_o) # TensorArray object
print(gen_o_stack) # tf.Tensor(), [5,]
print(gen_o_concat) # tf.Tensor(), [5,1]
print(gen_o.read(3)) # -0.22972003, tf.Tensor 读过一次就被清0了
print(gen_o.write(3, tf.constant([0.22], dtype=tf.float32))) # TensorArray object
print(gen_o.concat()) # tf.Tensor([-2.568663 0.09471891 1.2042408 0.22 0.2832177 ], shape=(5,), dtype=float32)
print(gen_o.read(3)) # tf.Tensor([0.22], shape=(1,), dtype=float32)
print(gen_o.read(3)) # Could not read index 3 twice because it was cleared after a previous read

用 LSTM 模拟马尔科夫决策过程

  • current time t state: \((y_1,...,y_t)\). 但是马尔科夫决策过程的原理告诉我们一旦当前状态确定后,所有的历史信息都可以扔掉了。这个状态足够去预测 future. 所以在LSTM里面就是隐藏状态 \(h_{t-1}\). 以及当前可观测信息 \(x_t\).
  • action a: 选择 next token \(y_t\).
  • policy: \(G_{\theta}(y_t|Y_{1:t-1})\). 也就是生成next token的策略。下面代码的方法 \(o_t \rightarrow log(softmax(o_t))\). 然后基于这个 log-prob 的分布进行 sample. 问题是这个过程不可导呀?
generator

这是生成器生成sample的过程,初始状态是 \(h_0\).

g_recurrence 就是step-by-step的过程,next_token是通过tf.multinomial采样得到的,其采样的distribution是 log_prob [tf.log(tf.nn.softmax(o_t))]。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
class Generator(tf.keras.Model):
...

self.h0 = tf.zeros([self.batch_size, self.hidden_dim])
self.h0 = tf.stack([self.h0, self.h0])

# define variables
self.g_embeddings = tf.Variable(self.init_matrix([self.vocab_size, self.emb_dim]))
self.g_params.append(self.g_embeddings)
self.g_recurrent_unit = self.create_recurrent_unit(self.g_params) # maps h_{t-1} to h_t for generator
self.g_output_unit = self.create_output_unit(self.g_params) # maps h_t to o_t (output token logits)

def _unsuper_generate(self):
""" unsupervised generate. using in rollout policy.
:return: 生成得到的 token index
"""
"""
:param input_x: [batch, seq_len]
:param rewards: [batch, seq_len]
:return:
"""
gen_o = tf.TensorArray(dtype=tf.float32, size=self.sequence_length,
dynamic_size=False, infer_shape=True)
gen_x = tf.TensorArray(dtype=tf.int32, size=self.sequence_length,
dynamic_size=False, infer_shape=True)

def _g_recurrence(i, x_t, h_tm1, gen_o, gen_x):
h_t = self.g_recurrent_unit(x_t, h_tm1) # hidden_memory_tuple
o_t = self.g_output_unit(h_t) # [batch, vocab] , logits not prob
log_prob = tf.log(tf.nn.softmax(o_t))
#tf.logging.info("unsupervised generated log_prob:{}".format(log_prob[0]))
next_token = tf.cast(tf.reshape(tf.multinomial(logits=log_prob, num_samples=1),
[self.batch_size]), tf.int32)
x_tp1 = tf.nn.embedding_lookup(self.g_embeddings, next_token) # [batch, emb_dim]
gen_o = gen_o.write(i, tf.reduce_sum(tf.multiply(tf.one_hot(next_token, self.vocab_size, 1.0, 0.0),
tf.nn.softmax(o_t)), 1)) # [batch_size] , prob
gen_x = gen_x.write(i, next_token) # indices, batch_size
return i + 1, x_tp1, h_t, gen_o, gen_x

_, _, _, def _super_generate(self, input_x):
""" supervised generate.

:param input_x:
:return: 生成得到的是 probability [batch * seq_len, vocab_size]
"""
with tf.device("/cpu:0"):
self.processed_x = tf.transpose(tf.nn.embedding_lookup(self.g_embeddings, input_x),
perm=[1, 0, 2]) # [seq_len, batch_size, emb_dim]
# supervised pretraining for generator
g_predictions = tf.TensorArray(
dtype=tf.float32, size=self.sequence_length,
dynamic_size=False, infer_shape=True)

ta_emb_x = tf.TensorArray(
dtype=tf.float32, size=self.sequence_length)
ta_emb_x = ta_emb_x.unstack(self.processed_x) self.gen_o, self.gen_x = tf.while_loop(
cond=lambda i, _1, _2, _3, _4: i < self.sequence_length,
body=_g_recurrence,
loop_vars=(tf.constant(0, dtype=tf.int32),
tf.nn.embedding_lookup(self.g_embeddings, self.start_token),
self.h0, gen_o, gen_x))

self.gen_x = self.gen_x.stack() # [seq_length, batch_size]
self.gen_x = tf.transpose(self.gen_x, perm=[1, 0]) # [batch_size, seq_length]
return self.gen_x

所以是通过monte carlo的形式生成fake sample,作为discriminator的输入吗?那这个过程也不可导呀。其实不是这样的。我们再看对抗学习中更新generator的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def gen_reward_train_step(x_batch, rewards):
with tf.GradientTape() as tape:
g_loss = generator._get_generate_loss(x_batch, rewards)
g_gradients, _ = tf.clip_by_global_norm(
tape.gradient(g_loss, generator.trainable_variables), clip_norm=5.0)
g_optimizer.apply_gradients(zip(g_gradients, generator.trainable_variables))
return g_loss

tf.logging.info("------------------ 6. start Adversarial Training...--------------------------")
for total_batch in range(TOTAL_BATCH):
# fix discriminator, and train the generator for one step
for it in range(1):
samples = generator._unsuper_generate()
#tf.logging.info("unsuper generated samples:{}".format(samples[0]))
rewards = rollout.get_reward(samples, rollout_num=2, discriminator=discriminator) # 基于 monte carlo 采样16,计算并累计 reward.
#tf.logging.info("reward:{}".format(rewards[0]))
gen_reward_train_step(samples, rewards) # update generator.
# Update roll-out parameters
rollout.update_params() # update roll-out policy.

这儿采用的是 generator._get_generate_loss, 所以它对generator的参数都是可导的吗? 我们再看这个生成器中这个function的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
class Generator(tf.keras.Model):
...

def _super_generate(self, input_x):
""" supervised generate.

:param input_x:
:return: 生成得到的是 probability [batch * seq_len, vocab_size]
"""
with tf.device("/cpu:0"):
self.processed_x = tf.transpose(tf.nn.embedding_lookup(self.g_embeddings, input_x),
perm=[1, 0, 2]) # [seq_len, batch_size, emb_dim]
# supervised pretraining for generator
g_predictions = tf.TensorArray(
dtype=tf.float32, size=self.sequence_length,
dynamic_size=False, infer_shape=True)

ta_emb_x = tf.TensorArray(
dtype=tf.float32, size=self.sequence_length)
ta_emb_x = ta_emb_x.unstack(self.processed_x)

def _pretrain_recurrence(i, x_t, h_tm1, g_predictions):
h_t = self.g_recurrent_unit(x_t, h_tm1)
o_t = self.g_output_unit(h_t)
g_predictions = g_predictions.write(i, tf.nn.softmax(o_t)) # [batch, vocab_size]
x_tp1 = ta_emb_x.read(i) # supervised learning, teaching forcing.
return i + 1, x_tp1, h_t, g_predictions

_, _, _, self.g_predictions = tf.while_loop(
cond=lambda i, _1, _2, _3: i < self.sequence_length,
body=_pretrain_recurrence,
loop_vars=(tf.constant(0, dtype=tf.int32),
tf.nn.embedding_lookup(self.g_embeddings, self.start_token),
self.h0, g_predictions))
self.g_predictions = tf.transpose(self.g_predictions.stack(),
perm=[1, 0, 2]) # [batch_size, seq_length, vocab_size]
self.g_predictions = tf.clip_by_value(
tf.reshape(self.g_predictions, [-1, self.vocab_size]), 1e-20, 1.0) # [batch_size*seq_length, vocab_size]
return self.g_predictions # [batch_size*seq_length, vocab_size]

def _get_generate_loss(self, input_x, rewards):
"""

:param input_x: [batch, seq_len]
:param rewards: [batch, seq_len]
:return:
"""
self.g_predictions = self._super_generate(input_x)
real_target = tf.one_hot(
tf.to_int32(tf.reshape(input_x, [-1])),
depth=self.vocab_size, on_value=1.0, off_value=0.0) # [batch_size * seq_length, vocab_size]
self.pretrain_loss = tf.nn.softmax_cross_entropy_with_logits(labels=real_target,
logits=self.g_predictions) # [batch * seq_length]
self.g_loss = tf.reduce_mean(self.pretrain_loss * tf.reshape(rewards, [-1])) # scalar
return self.g_loss

所以seqgan的作者是怎么做的呢,利用 generator._unsuper_generate先生成fake sample,然后再利用 generator._super_generate 得到 g_predictions, 将fake sample作为 real_targetg_predictions 做交叉熵求出 pretrain_loss,然后乘以每一个token对应的rewards得到最终的loss. 这个过程是可导的。

通常情况下Monte carlo方法在里面的作用其实就是 collect data. collecting data的过程用到了policy,然后基于reward对policy进行求导。
但是seqgan的作者在代码中呈现的是另一种trick. 先用generator生成fake样本,然后用rollout policy对该样本进行打分reward.这里并不是直接对reward求导,而是把fake样本作为target进行MLE训练,得到pretrain_loss,reward作为权重乘以pretrain_loss作为最终的损失函数。

roll-policy

这个过程比较容易理解,对于给定的 given_num,小于 given_num 的直接 copy,但是 \(h_t\) 的计算依旧。大于 given_num 的token采用 generate._unsuper_generate.

疑问

看了代码总觉得代码写得与论文有出入。

基于policy gradient来更新policy(generator),按照公式应该是直接对rewards求导才对吧。基于Monte carlo采样的过程可以看作是sample不同的样本,是一种近似模拟 \(o_t\) 分布的方法,是不要求可导的。