【论文泛读】4. 机器翻译:Neural Machine Translation by Jointly Learning to Align and Translate

更新进度:■■■■■■■■■■■■■■■■■■■■■■■|100%
理论上一周更一个经典论文
刚刚开始学习,写的不好,有错误麻烦大家留言给我啦
这位博主的笔记短小精炼,爱了爱了:点击跳转

准备

文章标题:
Neural Machine Translation by Jointly Learning to Align and Translate
基于联合学习对齐和翻译的神经机器翻译
作者:
Dzmitry Bahdanau1, KyungHyun Cho2, Yoshua Bengio*2
单位:

  1. 不来梅雅克布大学Jacobs University Bremen, Germany
  2. 蒙特利尔大学Universite de Montr ´ eal

发表会议及时间:
ICLR 2015综合性会议
最早将注意力机制引入机器翻译的论文

理论知识

在这里插入图片描述
模型:
在这里插入图片描述
在这里插入图片描述

代码实现(英文翻中文)

数据集:IWLST TED数据集、PTB数据集

1. 数据预处理

在这里插入图片描述

step 1,2: 分词,统计

import codecs
import collections

counter = collections.Counter()  # 建立一个空的Counter
with codecs.open(RAW_DATA, "r", "utf-8") as f:  # RAW_DATA 是 ptb.train.txt
    for line in f:
        for word in line.strip().split():
            counter[word] += 1

输出:按词频排列的词典
在这里插入图片描述

  1. collections.Counter()是Python的计数器,快速的统计一个字典里面每个元素出现的次数,参数必须是list类型的,不能是二维的,或者其他维度的。
    可以创建一个空的Counter,对空的Counter进行操作。
    参考文章
  2. line.strip().split():
    strip()表示删除掉数据中的换行符,split()表示数据中遇到空格就隔开。
  3. 字典(Dictionary).items() 函数以列表(list)形式返回可遍历的(键, 值) 元组数组。
  4. 列表的排列

例:list1 = [(3, 8, 2), (1, 5, 8), (2, 5, 7)]

方法1:利用list自带sort()方法
list1.sort(key=lambda t: t[0])
print(list1) # [(1, 5, 8), (2, 5, 7), (3, 8, 2)]

方法2:利用集合公共函数sorted()–>排序后的集合
ret = sorted(list1, key=lambda t: t[0])
print(ret) # [(1, 5, 8), (2, 5, 7), (3, 8, 2)]

step 3: 分配ID

# 按词频顺序对单词进行排序。
sorted_word_to_cnt = sorted(counter.items(), key=itemgetter(1), reverse=True)
sorted_words = [x[0] for x in sorted_word_to_cnt]

在这里插入图片描述

# 稍后我们需要在文本换行处加入句子结束符"<eos>",这里预先将其加入词汇表。
sorted_words = ["<eos>"] + sorted_words

在这里插入图片描述
输出:词汇vocabe

with codecs.open(VOCAB_OUTPUT, 'w', 'utf-8') as file_output:
    for word in sorted_words:
        file_output.write(word + "\n")

step 4: 生成词汇表

with codecs.open("./ptb.vocab", "r", "utf-8") as f_vocab:
    vocab = [w.strip() for w in f_vocab.readlines()]
word_to_id = {k: v for (k, v) in zip(vocab, range(len(vocab)))}

将词汇频率对应vocab
在这里插入图片描述
输出:生成词汇表(字典)
在这里插入图片描述
如果出现了不在词汇表内的低频词,则替换为"unk"

def get_id(word):
    return word_to_id[word] if word in word_to_id else word_to_id["<unk>"]
  1. zip(x,y)

x = [1, 2, 3]
y = [4, 5, 6]
zipped = zip(x, y)

list(zipped)
[(1, 4), (2, 5), (3, 6)]

step 5: 编号表示

fin = codecs.open(RAW_DATA, "r", "utf-8")
fout = codecs.open(OUTPUT_DATA, 'w', 'utf-8')

for line in fin:
    words = line.strip().split() + ["<eos>"]  # 读取单词并添加<eos>结束符
    # 将每个单词替换为词汇表中的编号
    out_line = ' '.join([str(get_id(w)) for w in words]) + '\n'
    fout.write(out_line)

在这里插入图片描述

step 6: 预料填充(分批次处理,每一批次语句长度一样)

输入:已经用空格隔开并转换为编码的数据集

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)  # SRC_TRAIN_DATA = "./train.en"  
    trg_data = MakeDataset(trg_path)  # TRG_TRAIN_DATA = "./train.zh"
    # 通过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
  1. 填充调用函数为:tf. data. Dataset.padded_batch(batch_size, padded_shapes)
  2. lambda+map函数的使用

第一个问题: lambda如何用?
lambda叫做匿名函数:不需要提前定义函数就可以使用的函数
如果不使用lambda函数:
输入:

def func(x):
… return x+1

输出:

func(1)

使用lambda函数:
输入:

a = lambda x:x+1

输出:

a(1)

第二个问题: map如何用?
例:有一个元组列表[(‘a’,1),(‘b’,2),(‘c’,3),(‘d’,4)],需将字母项提取为新的列表。
vartuple = [(‘a’,1),(‘b’,2),(‘c’,3),(‘d’,4)]
list(map(lambda x:x[0],vartuple))
输出:[‘a’, ‘b’, ‘c’, ‘d’]

2. 模型搭建

熟悉模型流程+输入输出

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

3. 训练


'''
训练步骤:
1.初始化
2.定义模型
3.定义输入数据
4.定义前向图
5.训练模型
'''


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)

4. 测试

import tensorflow as tf
import codecs
import sys

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

# 模型参数。必须与训练时的模型参数保持一致。
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"

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

# 定义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)
            # 填入第一个单词<sos>作为解码器的输入。
            init_array = init_array.write(0, SOS_ID)
            # 构建初始的循环状态。循环状态包含循环神经网络的隐藏状态,保存生成句子的
            # TensorArray,以及记录解码步数的一个整数step。
            init_loop_var = (enc_state, init_array, 0)

            # tf.while_loop的循环条件:
            # 循环直到解码器输出<eos>,或者达到最大步数为止。
            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()

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

    # 定义个测试句子。
    test_en_text = "This is a test . <eos>"
    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['<unk>'])
                   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()

代码可学习技巧

  1. checkpoint保存每一步权重文件
  2. 使用argparse.ArgumentParser(),argparse 模块可以让人轻松编写用户友好的命令行接口,将变量通过命令行赋值
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

浪里摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值