github链接:https://github.com/LantaoYu/SeqGAN
论文及appendix里有很好的代码说明。
sequence_gan.py
是主文件
main函数里,首先定义了generator和discriminator结构体。
generator里:
是RNN生成长度为20的句子的过程(即:一行有20个数字)。LSTM结构是自己实现的,即g_recurrent_unit
函数。由于输出的是hidden_state,再用g_output_unit
函数转换为output_token_prob。
placeholder里,self.x
是real sentence,self.rewards
是RL里的Rewards。
self.h0
里包括了LSTM的hidden_state和Ct。
gen_x
和gen_o
是generator生成的sentence(word id)和每个word的prob。
接下来是一个循环,循环生成20个word。start_token和start_hidden_state是初始化好的。具体生成方法见函数_g_recurrence
。依次把结果写入gen_x
和gen_o
。
到以上部分截止,都和待输入的placeholder没什么关系,就是简单的RNN网络。
接下来是有监督学习部分,即用MLE的思想来训练网络。和GAN没关系。用和上文循环相似的结构来生成prediction,即每个word的prob,而不是具体的word id。通过和转化成one-hot形式的placeholder里的x作对比来计算self.pretrain_loss
。
再接下来是无监督学习部分,即GAN的generator部分。根据论文里的公式
self.g_loss = -tf.reduce_sum(
tf.reduce_sum(
tf.one_hot(tf.to_int32(tf.reshape(self.x, [-1])), self.num_emb, 1.0, 0.0) * tf.log(
tf.clip_by_value(tf.reshape(self.g_predictions, [-1, self.num_emb]), 1e-20, 1.0)
), 1) * tf.reshape(self.rewards, [-1]) # rewards是RL才有的
)
把输入的64*20个单词全变成一维,乘以对应的reward,再加和得到expected rewards。取负即为GAN的generator部分的loss。
target_lstm里:
结构大致和generator相同,但是它的作用是生成真实的句子,所以不存在训练过程。
设置tf.set_random_seed(66)
,这就是real sentence的特征。
discriminator里:
待输入的placeholder是x(sentence), y([0,1] or [1,0]), keep_prob.
其内容是标准的CNN用于text classification的代码。
# Convolution Layer
filter_shape = [filter_size, embedding_size, 1, num_filter]
W = tf.Variable(tf.truncated_normal(filter_shape, stddev=0.1), name="W")
b = tf.Variable(tf.constant(0.1, shape=[num_filter]), name="b")
conv = tf.nn.conv2d(
self.embedded_chars_expanded, #(batch_size, sequence_length , embedding_size, 1)
W,
strides=[1, 1, 1, 1],
padding="VALID",
name="conv")
# Apply nonlinearity
h = tf.nn.relu(tf.nn.bias_add(conv, b), name="relu")#(batch_size, sequence_length,filter_size, num_filter )
# Maxpooling over the outputs
pooled = tf.nn.max_pool(
h,
ksize=[1, sequence_length - filter_size + 1, 1, 1],
strides=[1, 1, 1, 1],
padding='VALID',
name="pool")#(batch_size, filter_size, 1, num_filter)
最后把输出转化成[-1, num_filters_total],再用linear函数输出最后的prob。
回到主函数里。
使用target_lstm生成real sentence,写入real_data.txt文件。
pre-train generator:
outputs = sess.run([self.pretrain_updates, self.pretrain_loss],
feed_dict={self.x: x}) # 不需要generator生成sentence
pre-training discriminator:
feed = {
discriminator.input_x: x_batch,
discriminator.input_y: y_batch,
discriminator.dropout_keep_prob: dis_dropout_keep_prob
}
_ = sess.run(discriminator.train_op, feed)
到了最关键的部分,roll-out policy,即计算reward的部分。
先定义reward结构体
参数设置都和generator一样,placeholder里的x也是real sentence,但是新增了placeholder given_num
,也就是上文公式中的t,意指generator句子的长度为given_num
。
当i<given_num的时候,不用生成sentence,直接读取:
h_t = self.g_recurrent_unit(x_t, h_tm1) # hidden_memory_tuple
x_tp1 = ta_emb_x.read(i)
gen_x = gen_x.write(i, ta_x.read(i))
return i + 1, x_tp1, h_t, given_num, gen_x
反之,还是要按基本法,依次选出单词:
# 这里的input_x是generator生成的fake sentence
h_t = self.g_recurrent_unit(x_t, h_tm1) # hidden_memory_tuple
o_t = self.g_output_unit(h_t) # batch x vocab , logits not prob
log_prob = tf.log(tf.nn.softmax(o_t))
next_token = tf.cast(tf.reshape(tf.multinomial(log_prob, 1), [self.batch_size]), tf.int32)
x_tp1 = tf.nn.embedding_lookup(self.g_embeddings, next_token) # batch x emb_dim
gen_x = gen_x.write(i, next_token) # indices, batch_size
return i + 1, x_tp1, h_t, given_num, gen_x
在get_reward
函数中,从1到20尝试given_num,计算expected reward。
for given_num in range(1, 20): # 最后只到19
feed = {self.x: input_x, self.given_num: given_num}# 这里需要输入input_x才能生成gen_x,因为gen_x的前一部分是固定好的
samples = sess.run(self.gen_x, feed)
feed = {discriminator.input_x: samples, discriminator.dropout_keep_prob: 1.0}
ypred_for_auc = sess.run(discriminator.ypred_for_auc, feed)
ypred = np.array([item[1] for item in ypred_for_auc])# 得到每个句子的分值
if i == 0:
rewards.append(ypred)
else:
rewards[given_num - 1] += ypred # 把所有得分加在一起
# the last token reward
feed = {discriminator.input_x: input_x, discriminator.dropout_keep_prob: 1.0}#最后一个word就不用自己run生成了,直接读取input_x
ypred_for_auc = sess.run(discriminator.ypred_for_auc, feed)
ypred = np.array([item[1] for item in ypred_for_auc])
if i == 0:
rewards.append(ypred)
else:
rewards[19] += ypred
这样,就得到了对于任意长度的句子的rewards。
就是对于已经生成的不同长度的sentence,扩展到完整的句子,再用discriminator来打分。
再根据形如
self.Wi = self.update_rate * self.Wi + (1 - self.update_rate) * tf.identity(self.lstm.Wi)
的公式更新roll-out里的参数。
discriminator还是根据相同的公式来训练。
网友评论