在TEB数据集上搭建翻译模型——Seq2Seq

   Seq2Seq模型的基本思想非常简单——使用一个循环神经网络读取输入句子,将整个句子的信息压缩到一个固定维度的编码中;再使用另一个循环神经网络读取这个编码,将其“解压”为目标语言的一个句子。这两个循环神经网络分别为编码器和解码器,这个模型也称为encoder-decoder模型。
  解码器部分结构与语言模型几乎完全相同:输入为单词的词向量,输出为softmax层产生的单词概率,损失函数为log perplexity。事实上,解码器可以理解为以输入编码为基础的语言模型。语言模型中的一些技巧,如共享softmax层、词向量的参数,都可以直接用于Seq2Seq模型的解码器中。

在这里插入图片描述

  编码器部分则为更简单。它与解码器一样拥有词向量层和循环神经网络,但是,由于在编码的过程中并未输出,因此,不需要softmax层。
  在训练过程中,编码器顺序读入每个单词的词向量,然后将最终的隐藏状态复制到解码器作为初始状态。解码器的第一个输入是一个特殊的“sos”符号,每一步预测的单词是训练数据的目标句子,预测序列的最后一个单词是与语言模型相同的“eos”字符。
在TEB数据集上搭建翻译模型——Seq2Seq_第1张图片

1.预处理

import tensorflow as tf

# 语言预处理流程(数据里面的单词已经全部转化为单词编号)
# 使用Dataset从一个文件中读取一个语言的数据。
# 数据的格式为每行一句话,单词已经转化为单词编号。
class PrepDataset(object):
    def __init__(self, src_path, trg_path, batch_size, max_len, sos_id):
        self.batch_size = batch_size 
        self.src_path = src_path  
        self.trg_path = trg_path  
        self.max_len = max_len 
        self.sos_id = sos_id 
        
    def MakeDataset(self, 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
    
    # 删除内容为空(只包含)的句子和长度过长的句子。
    def FilterLength(self, 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, self.max_len)
        )
        trg_len_ok = tf.logical_and(
            tf.greater(trg_len, 1), tf.less_equal(trg_len, self.max_len)
        )
        return tf.logical_and(src_len_ok, trg_len_ok)
       
    # 解码器需要两种格式的目标句子:
        #   1.解码器的输入(trg_input),形式如同" X Y Z"
        #   2.解码器的目标输出(trg_label),形式如同"X Y Z "
        #  上面从文件中读到的目标句子是"X Y Z "的形式,我们需要从中生成" X Y Z" 形式并加入到Dataset中。
    def MakeTrgInput(self, src_tuple, trg_tuple):
        '''
        
        tf.concat()函数的用法:
        t1 = [[1, 2, 3], [4, 5, 6]]
        t2 = [[7, 8, 9], [10, 11, 12]]
        tf.concat([t1, t2], 0)  # [[1, 2, 3], [4, 5, 6], [7, 8, 9], [10, 11, 12]]
        tf.concat([t1, t2], 1)  # [[1, 2, 3, 7, 8, 9], [4, 5, 6, 10, 11, 12]]
       
        '''
        ((src_input, src_len), (trg_label, trg_len)) = (src_tuple, trg_tuple)
        trg_input = tf.concat(
            [[self.sos_id], trg_label[:-1]], axis=0
        )   
        return ((src_input, src_len), (trg_input, trg_label, trg_len))
    
    # 从源语言文件src_path和目标语言文件trg_path中分别读取数据,并进行填充和batching操作。
    def MakeSrcTrgDataset(self):
            # 首先分别读取源语言数据和目标语言数据。
        src_data = self.MakeDataset(src_path)
        trg_data = self.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))
        dataset = dataset.filter(self.FilterLength)
        dataset = dataset.map(self.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
# 测试一下padding batch有没有问题
if __name__ == "__main__":
    src_path = 'train.en'
    trg_path = 'train.zh'
    batch_size = 100 
    max_len = 50
    sos_id = 1
    makedataset = PrepDataset(src_path, trg_path, batch_size, max_len, sos_id)
    batched_dataset = makedataset.MakeSrcTrgDataset()
    iterator = batched_dataset.make_initializable_iterator()
    (src_input, src_len), (trg_input, trg_label, trg_len) = iterator.get_next()

2.定义翻译模型

  要使用的参数:

# 假设输入数据已经用9.2.1小节中的方法转换成了单词编号的格式。
SRC_TRAIN_DATA = "./train.en"        # 源语言输入文件。
TRG_TRAIN_DATA = "./train.zh"        # 目标语言输入文件。
CHECKPOINT_PATH = "./seq2seq_ckpt"   # checkpoint保存路径。  

HIDDEN_SIZE = 1024                   # LSTM的隐藏层规模。
NUM_LAYERS = 2                       # 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000               # 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                # 目标语言词汇表大小。
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    # 目标语言词汇表中的ID。

  定义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

3.训练过程主函数

# 使用给定的模型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()

In iteration: 1
After 0 steps, per token cost is 8.292
After 10 steps, per token cost is 7.941
After 20 steps, per token cost is 7.233
After 30 steps, per token cost is 7.367
After 40 steps, per token cost is 6.881
After 50 steps, per token cost is 6.818

After 380 steps, per token cost is 5.281
After 390 steps, per token cost is 5.087
After 400 steps, per token cost is 5.040
After 1780 steps, per token cost is 3.742
After 1790 steps, per token cost is 3.710
After 1800 steps, per token cost is 3.669

In iteration: 5
After 7220 steps, per token cost is 2.678
After 7230 steps, per token cost is 2.892
After 7240 steps, per token cost is 2.736
After 7250 steps, per token cost is 2.702
After 7260 steps, per token cost is 2.755
After 7270 steps, per token cost is 2.665

After 8960 steps, per token cost is 2.321
After 8970 steps, per token cost is 2.347
After 8980 steps, per token cost is 2.389
After 8990 steps, per token cost is 2.505
After 9000 steps, per token cost is 2.486
After 9010 steps, per token cost is 2.441

  模型建立好了之后,拿训练保存的参数对数据进行测试。在进行翻译的过程中,因为解码器无法读取目标训练句子(y),只能看到输入的句子(X),因此无法直接使用dynamic_rnn简单地展开成前馈网络,而是需要使用一个循环结构来实现,这个循环结构是由tf.while_loop()来实现的。下面进入测试阶段:

1.参数设置

import tensorflow as tf
import codecs
import sys
# 读取checkpoint的路径。9000表示是训练程序在第9000步保存的checkpoint。
CHECKPOINT_PATH = "./seq2seq_ckpt-9000"

# 模型参数。必须与训练时的模型参数保持一致。
HIDDEN_SIZE = 1024                         	# LSTM的隐藏层规模。
NUM_LAYERS = 2                             	# 深层循环神经网络中LSTM结构的层数。
SRC_VOCAB_SIZE = 10000                   	# 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                    	# 目标语言词汇表大小。
SHARE_EMB_AND_SOFTMAX = True            	# 在Softmax层和词向量层之间共享参数。

# 词汇表文件
SRC_VOCAB = "./en.vocab"
TRG_VOCAB = "./zh.vocab"

# 词汇表中的ID。在解码过程中需要用作为第一步的输入,并将检查
# 是否是,因此需要知道这两个符号的ID。
SOS_ID = 1
EOS_ID = 2

2.定义NMT模型和解码步骤

# 定义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])

    def inference(self, src_input):
        # 虽然输入只有一个句子,但因为dynamic_rnn要求输入是batch的形式,因此这里
        # 将输入句子整理为大小为1的batch。
        src_size = tf.convert_to_tensor([len(src_input)], dtype=tf.int32)
        src_input = tf.convert_to_tensor([src_input], dtype=tf.int32)
        src_emb = tf.nn.embedding_lookup(self.src_embedding, src_input)

        # 使用dynamic_rnn构造编码器。这一步与训练时相同。
        with tf.variable_scope("encoder"):
            enc_outputs, enc_state = tf.nn.dynamic_rnn(
                self.enc_cell, src_emb, src_size, dtype=tf.float32)
   
        # 设置解码的最大步数。这是为了避免在极端情况出现无限循环的问题。
        MAX_DEC_LEN=100

        with tf.variable_scope("decoder/rnn/multi_rnn_cell"):
            # 使用一个变长的TensorArray来存储生成的句子。
            init_array = tf.TensorArray(dtype=tf.int32, size=0,
                dynamic_size=True, clear_after_read=False)
            # 填入第一个单词作为解码器的输入。
            init_array = init_array.write(0, SOS_ID)
            # 构建初始的循环状态。循环状态包含循环神经网络的隐藏状态,保存生成句子的
            # TensorArray,以及记录解码步数的一个整数step。
            init_loop_var = (enc_state, init_array, 0)

            # tf.while_loop的循环条件:
            # 循环直到解码器输出,或者达到最大步数为止。
            def continue_loop_condition(state, trg_ids, step):
                return tf.reduce_all(tf.logical_and(
                    tf.not_equal(trg_ids.read(step), EOS_ID),
                    tf.less(step, MAX_DEC_LEN-1)))

            def loop_body(state, trg_ids, step):
                # 读取最后一步输出的单词,并读取其词向量。
                trg_input = [trg_ids.read(step)]
                trg_emb = tf.nn.embedding_lookup(self.trg_embedding,
                                                 trg_input)
                # 这里不使用dynamic_rnn,而是直接调用dec_cell向前计算一步。
                dec_outputs, next_state = self.dec_cell.call(
                    state=state, inputs=trg_emb)
                # 计算每个可能的输出单词对应的logit,并选取logit值最大的单词作为
                # 这一步的而输出。
                output = tf.reshape(dec_outputs, [-1, HIDDEN_SIZE])
                logits = (tf.matmul(output, self.softmax_weight)
                          + self.softmax_bias)
                next_id = tf.argmax(logits, axis=1, output_type=tf.int32)
                # 将这一步输出的单词写入循环状态的trg_ids中。
                trg_ids = trg_ids.write(step+1, next_id[0])
                return next_state, trg_ids, step+1

            # 执行tf.while_loop,返回最终状态。
            state, trg_ids, step = tf.while_loop(
                continue_loop_condition, loop_body, init_loop_var)
            return trg_ids.stack()

3.翻译一个测试句子

def main():
    # 定义训练用的循环神经网络模型。
    with tf.variable_scope("nmt_model", reuse=None):
        model = NMTModel()

    # 定义个测试句子。
    test_en_text = "This is a test . "
    print(test_en_text)
    
    # 根据英文词汇表,将测试句子转为单词ID。
    with codecs.open(SRC_VOCAB, "r", "utf-8") as f_vocab:
        src_vocab = [w.strip() for w in f_vocab.readlines()]
        src_id_dict = dict((src_vocab[x], x) for x in range(len(src_vocab)))
    test_en_ids = [(src_id_dict[token] if token in src_id_dict else src_id_dict[''])
                   for token in test_en_text.split()]
    print(test_en_ids)

    # 建立解码所需的计算图。
    output_op = model.inference(test_en_ids)
    sess = tf.Session()
    saver = tf.train.Saver()
    saver.restore(sess, CHECKPOINT_PATH)

    # 读取翻译结果。
    output_ids = sess.run(output_op)
    print(output_ids)
    
    # 根据中文词汇表,将翻译结果转换为中文文字。
    with codecs.open(TRG_VOCAB, "r", "utf-8") as f_vocab:
        trg_vocab = [w.strip() for w in f_vocab.readlines()]
    output_text = ''.join([trg_vocab[x] for x in output_ids])
    
    # 输出翻译结果。
    print(output_text.encode('utf8').decode(sys.stdout.encoding))
    sess.close()

if __name__ == "__main__":
    main()

This is a test . 《eos》
[90, 13, 9, 689, 4, 2]
INFO:tensorflow:Restoring parameters from ./seq2seq_ckpt-9000
[ 1 10 7 9 12 411 271 6 2]
《sos》 这 是 一 个 测 试 。 《eos》

参考:《TensorFlow实战Google深度学习框架》

你可能感兴趣的:(深度学习手记,深度学习,翻译模型,seq2seq,TensorFlow,LSTM)