-
Notifications
You must be signed in to change notification settings - Fork 0
/
seq2seq.py
217 lines (190 loc) · 10.5 KB
/
seq2seq.py
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
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
import tensorflow as tf
# 假设输入数据已经用9.2.1小节中的方法转换成了单词编号的格式。
SRC_TRAIN_DATA = "/Users/songyaheng/Downloads/train.from" # 源语言输入文件。
TRG_TRAIN_DATA = "/Users/songyaheng/Downloads/train.to" # 目标语言输入文件。
CHECKPOINT_PATH = "/Users/songyaheng/Downloads/data" # checkpoint保存路径。
HIDDEN_SIZE = 1024 # LSTM的隐藏层规模。
NUM_LAYERS = 2 # 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 6500 # 源语言词汇表大小。
TRG_VOCAB_SIZE = 6500 # 目标语言词汇表大小。
BATCH_SIZE = 100 # 训练数据batch的大小。
NUM_EPOCH = 5 # 使用训练数据的轮数。
KEEP_PROB = 0.8 # 节点不被dropout的概率。
MAX_GRAD_NORM = 5 # 用于控制梯度膨胀的梯度大小上限。
SHARE_EMB_AND_SOFTMAX = True # 在Softmax层和词向量层之间共享参数。
MAX_LEN = 50 # 限定句子的最大单词数量。
SOS_ID = 1 # 目标语言词汇表中<sos>的ID。
# 使用Dataset从一个文件中读取一个语言的数据。
# 数据的格式为每行一句话,单词已经转化为单词编号。
def MakeDataset(file_path):
dataset = tf.data.TextLineDataset(file_path)
# 根据空格将单词编号切分开并放入一个一维向量。
dataset = dataset.map(lambda string: tf.string_split([string]).values)
# 将字符串形式的单词编号转化为整数。
dataset = dataset.map(
lambda string: tf.string_to_number(string, tf.int32))
# 统计每个句子的单词数量,并与句子内容一起放入Dataset中。
dataset = dataset.map(lambda x: (x, tf.size(x)))
return dataset
# 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和
# batching操作。
def MakeSrcTrgDataset(src_path, trg_path, batch_size):
# 首先分别读取源语言数据和目标语言数据。
src_data = MakeDataset(src_path)
trg_data = MakeDataset(trg_path)
# 通过zip操作将两个Dataset合并为一个Dataset。现在每个Dataset中每一项数据ds
# 由4个张量组成:
# ds[0][0]是源句子
# ds[0][1]是源句子长度
# ds[1][0]是目标句子
# ds[1][1]是目标句子长度
dataset = tf.data.Dataset.zip((src_data, trg_data))
# 删除内容为空(只包含<EOS>)的句子和长度过长的句子。
def FilterLength(src_tuple, trg_tuple):
((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
src_len_ok = tf.logical_and(
tf.greater(src_len, 1), tf.less_equal(src_len, MAX_LEN))
trg_len_ok = tf.logical_and(
tf.greater(trg_len, 1), tf.less_equal(trg_len, MAX_LEN))
return tf.logical_and(src_len_ok, trg_len_ok)
dataset = dataset.filter(FilterLength)
# 从图9-5可知,解码器需要两种格式的目标句子:
# 1.解码器的输入(trg_input),形式如同"<sos> X Y Z"
# 2.解码器的目标输出(trg_label),形式如同"X Y Z <eos>"
# 上面从文件中读到的目标句子是"X Y Z <eos>"的形式,我们需要从中生成"<sos> X Y Z"
# 形式并加入到Dataset中。
def MakeTrgInput(src_tuple, trg_tuple):
((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
trg_input = tf.concat([[SOS_ID], trg_label[:-1]], axis=0)
return ((src_input, src_len), (trg_input, trg_label, trg_len))
dataset = dataset.map(MakeTrgInput)
# 随机打乱训练数据。
dataset = dataset.shuffle(10000)
# 规定填充后输出的数据维度。
padded_shapes = (
(tf.TensorShape([None]), # 源句子是长度未知的向量
tf.TensorShape([])), # 源句子长度是单个数字
(tf.TensorShape([None]), # 目标句子(解码器输入)是长度未知的向量
tf.TensorShape([None]), # 目标句子(解码器目标输出)是长度未知的向量
tf.TensorShape([]))) # 目标句子长度是单个数字
# 调用padded_batch方法进行batching操作。
batched_dataset = dataset.padded_batch(batch_size, padded_shapes)
return batched_dataset
# 定义NMTModel类来描述模型。
class NMTModel(object):
# 在模型的初始化函数中定义模型要用到的变量。
def __init__(self):
# 定义编码器和解码器所使用的LSTM结构。
self.enc_cell = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
for _ in range(NUM_LAYERS)])
self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
[tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
for _ in range(NUM_LAYERS)])
# 为源语言和目标语言分别定义词向量。
self.src_embedding = tf.get_variable(
"src_emb", [SRC_VOCAB_SIZE, HIDDEN_SIZE])
self.trg_embedding = tf.get_variable(
"trg_emb", [TRG_VOCAB_SIZE, HIDDEN_SIZE])
# 定义softmax层的变量
if SHARE_EMB_AND_SOFTMAX:
self.softmax_weight = tf.transpose(self.trg_embedding)
else:
self.softmax_weight = tf.get_variable(
"weight", [HIDDEN_SIZE, TRG_VOCAB_SIZE])
self.softmax_bias = tf.get_variable(
"softmax_bias", [TRG_VOCAB_SIZE])
# 在forward函数中定义模型的前向计算图。
# src_input, src_size, trg_input, trg_label, trg_size分别是上面
# MakeSrcTrgDataset函数产生的五种张量。
def forward(self, src_input, src_size, trg_input, trg_label, trg_size):
batch_size = tf.shape(src_input)[0]
# 将输入和输出单词编号转为词向量。
src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)
trg_emb = tf.nn.embedding_lookup(self.trg_embedding, trg_input)
# 在词向量上进行dropout。
src_emb = tf.nn.dropout(src_emb, KEEP_PROB)
trg_emb = tf.nn.dropout(trg_emb, KEEP_PROB)
# 使用dynamic_rnn构造编码器。
# 编码器读取源句子每个位置的词向量,输出最后一步的隐藏状态enc_state。
# 因为编码器是一个双层LSTM,因此enc_state是一个包含两个LSTMStateTuple类
# 张量的tuple,每个LSTMStateTuple对应编码器中的一层。
# enc_outputs是顶层LSTM在每一步的输出,它的维度是[batch_size,
# max_time, HIDDEN_SIZE]。Seq2Seq模型中不需要用到enc_outputs,而
# 后面介绍的attention模型会用到它。
with tf.variable_scope("encoder"):
enc_outputs, enc_state = tf.nn.dynamic_rnn(
self.enc_cell, src_emb, src_size, dtype=tf.float32)
# 使用dyanmic_rnn构造解码器。
# 解码器读取目标句子每个位置的词向量,输出的dec_outputs为每一步
# 顶层LSTM的输出。dec_outputs的维度是 [batch_size, max_time,
# HIDDEN_SIZE]。
# initial_state=enc_state表示用编码器的输出来初始化第一步的隐藏状态。
with tf.variable_scope("decoder"):
dec_outputs, _ = tf.nn.dynamic_rnn(
self.dec_cell, trg_emb, trg_size, initial_state=enc_state)
# 计算解码器每一步的log perplexity。这一步与语言模型代码相同。
output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
logits = tf.matmul(output, self.softmax_weight) + self.softmax_bias
loss = tf.nn.sparse_softmax_cross_entropy_with_logits(
labels=tf.reshape(trg_label, [-1]), logits=logits)
# 在计算平均损失时,需要将填充位置的权重设置为0,以避免无效位置的预测干扰
# 模型的训练。
label_weights = tf.sequence_mask(
trg_size, maxlen=tf.shape(trg_label)[1], dtype=tf.float32)
label_weights = tf.reshape(label_weights, [-1])
cost = tf.reduce_sum(loss * label_weights)
cost_per_token = cost / tf.reduce_sum(label_weights)
# 定义反向传播操作。反向操作的实现与语言模型代码相同。
trainable_variables = tf.trainable_variables()
# 控制梯度大小,定义优化方法和训练步骤。
grads = tf.gradients(cost / tf.to_float(batch_size),
trainable_variables)
grads, _ = tf.clip_by_global_norm(grads, MAX_GRAD_NORM)
optimizer = tf.train.GradientDescentOptimizer(learning_rate=1.0)
train_op = optimizer.apply_gradients(
zip(grads, trainable_variables))
return cost_per_token, train_op
# 使用给定的模型model上训练一个epoch,并返回全局步数。
# 每训练200步便保存一个checkpoint。
def run_epoch(session, cost_op, train_op, saver, step):
# 训练一个epoch。
# 重复训练步骤直至遍历完Dataset中所有数据。
while True:
try:
# 运行train_op并计算损失值。训练数据在main()函数中以Dataset方式提供。
cost, _ = session.run([cost_op, train_op])
if step % 10 == 0:
print("After %d steps, per token cost is %.3f" % (step, cost))
# 每200步保存一个checkpoint。
if step % 200 == 0:
saver.save(session, CHECKPOINT_PATH, global_step=step)
step += 1
except tf.errors.OutOfRangeError:
break
return step
def main():
# 定义初始化函数。
initializer = tf.random_uniform_initializer(-0.05, 0.05)
# 定义训练用的循环神经网络模型。
with tf.variable_scope("nmt_model", reuse=None,
initializer=initializer):
train_model = NMTModel()
# 定义输入数据。
data = MakeSrcTrgDataset(SRC_TRAIN_DATA, TRG_TRAIN_DATA, BATCH_SIZE)
iterator = data.make_initializable_iterator()
(src, src_size), (trg_input, trg_label, trg_size) = iterator.get_next()
# 定义前向计算图。输入数据以张量形式提供给forward函数。
cost_op, train_op = train_model.forward(src, src_size, trg_input,
trg_label, trg_size)
# 训练模型。
saver = tf.train.Saver()
step = 0
with tf.Session() as sess:
tf.global_variables_initializer().run()
for i in range(NUM_EPOCH):
print("In iteration: %d" % (i + 1))
sess.run(iterator.initializer)
step = run_epoch(sess, cost_op, train_op, saver, step)
if __name__ == "__main__":
main()