github鏈接:https://github.com/LantaoYu/SeqGAN
論文及appendix里有很好的代碼說明。
sequence_gan.py
是主文件
main函數(shù)里,首先定義了generator和discriminator結(jié)構(gòu)體摊灭。
generator里:
是RNN生成長度為20的句子的過程(即:一行有20個數(shù)字)。LSTM結(jié)構(gòu)是自己實現(xiàn)的煤杀,即g_recurrent_unit
函數(shù)沈自。由于輸出的是hidden_state,再用g_output_unit
函數(shù)轉(zhuǎn)換為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。
接下來是一個循環(huán)构回,循環(huán)生成20個word夏块。start_token和start_hidden_state是初始化好的。具體生成方法見函數(shù)_g_recurrence
纤掸。依次把結(jié)果寫入gen_x
和gen_o
脐供。
到以上部分截止,都和待輸入的placeholder沒什么關(guān)系借跪,就是簡單的RNN網(wǎng)絡(luò)政己。
接下來是有監(jiān)督學(xué)習(xí)部分,即用MLE的思想來訓(xùn)練網(wǎng)絡(luò)掏愁。和GAN沒關(guān)系歇由。用和上文循環(huán)相似的結(jié)構(gòu)來生成prediction,即每個word的prob果港,而不是具體的word id沦泌。通過和轉(zhuǎn)化成one-hot形式的placeholder里的x作對比來計算self.pretrain_loss
。
再接下來是無監(jiān)督學(xué)習(xí)部分京腥,即GAN的generator部分赦肃。根據(jù)論文里的公式
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個單詞全變成一維,乘以對應(yīng)的reward公浪,再加和得到expected rewards他宛。取負(fù)即為GAN的generator部分的loss。
target_lstm里:
結(jié)構(gòu)大致和generator相同欠气,但是它的作用是生成真實的句子厅各,所以不存在訓(xùn)練過程。
設(shè)置tf.set_random_seed(66)
预柒,這就是real sentence的特征队塘。
discriminator里:
待輸入的placeholder是x(sentence), y([0,1] or [1,0]), keep_prob.
其內(nèi)容是標(biāo)準(zhǔn)的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)
最后把輸出轉(zhuǎn)化成[-1, num_filters_total]宜鸯,再用linear函數(shù)輸出最后的prob憔古。
回到主函數(shù)里。
使用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)
到了最關(guān)鍵的部分,roll-out policy,即計算reward的部分焰情。
先定義reward結(jié)構(gòu)體
參數(shù)設(shè)置都和generator一樣陌凳,placeholder里的x也是real sentence,但是新增了placeholder given_num
内舟,也就是上文公式中的t合敦,意指generator句子的長度為given_num
。
當(dāng)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
函數(shù)中,從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砸王。
就是對于已經(jīng)生成的不同長度的sentence推盛,擴展到完整的句子,再用discriminator來打分谦铃。
再根據(jù)形如
self.Wi = self.update_rate * self.Wi + (1 - self.update_rate) * tf.identity(self.lstm.Wi)
的公式更新roll-out里的參數(shù)耘成。
discriminator還是根據(jù)相同的公式來訓(xùn)練。