Menu

基于深度学习的中英机器翻译

  • 从 WMT18(News Commentary)中抽取的中英句子对
    • 数据集规模:10000
      • 10K 版本,train: 8000, test: 2000, dev: 2000
      • 100K 版本(非必须),train: 80000, test: 20000, dev: 20000
      • 可根据提供的数据处理脚本自定义数据集大小(不能太小)
  • 数据格式
    • source: 每行一条中文句子
    • target: 每行一条 source 中对应行数的英文句子
  • 平台:Pytorch
  • 模型要求:
    • 两个 LSTM 分别作为 Encoder 和 Decoder
    • 实现基于注意力机制的机器翻译
    • 自行选择分词工具
    • 改变 teacher forcing ratio,观察效果
    • Beam Search 策略
  • 评估指标:BLEU 值(BLEU-4)
  • 词向量:随机初始化或自选预训练词向量
  • 设备:CPU/GPU
  • 要求:
    • 描述清楚核心代码逻辑和 tensor 维度
    • 描述清楚代码的运行环境和软件版本
    • 独立完成,不得抄袭!不得抄袭!不得抄袭!

Reference

参考资料

相关论文

  • Effective Approaches to Attention-based Neural Machine Translation, Luong et al., EMNLP 2015
  • Neural Machine Translation by Jointly Learning to Align and Translate, Bahdanau et al., ICLR 2015
  • Bleu: a Method for Automatic Evaluation for Machine Translation, Papineni et al., ACL 2002

实验环境

本地环境

所用机器型号为 VAIO Z Flip 2016。

  • Intel(R) Core(TM) i7-6567U CPU @3.30GHZ 3.31GHz
  • 8.00GB RAM
  • Windows 10, 64-bit (Build 17763) 10.0.17763
  • Visual Studio Code 1.39.2
    • Python 2019.10.41019:九月底发布的 VSCode Python 插件支持在编辑器窗口内原生运行 juyter nootbook 了,非常赞!
    • Remote - WSL 0.39.9:配合 WSL,在 Windows 上获得 Linux 接近原生环境的体验。
  • Windows Subsystem for Linux [Ubuntu 18.04.2 LTS]:WSL 是以软件的形式运行在 Windows 下的 Linux 子系统,是近些年微软推出来的新工具,可以在 Windows 系统上原生运行 Linux。
    • Python 3.7.4 64-bit (‘anaconda3’:virtualenv):安装在 WSL 中。
      • jieba==0.39
      • nltk==3.3

集群环境

借用 V100 集群中的一个节点,硬件参数和软件配置如下。属于相当豪华的配置了,和上次实验相比这次不到一个小时就跑完了,有钱真好…

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
$ NodeName=gpu27 Arch=x86_64 CoresPerSocket=14
   CPUAlloc=28 CPUTot=28 CPULoad=4.05
   AvailableFeatures=(null)
   ActiveFeatures=(null)
   Gres=(null)
   NodeAddr=gpu27 NodeHostName=gpu27
   OS=Linux 3.10.0-957.el7.x86_64 #1 SMP Sat Oct 12 02:01:43 CST 2019
   RealMemory=256000 AllocMem=0 FreeMem=219734 Sockets=2 Boards=1
   State=ALLOCATED ThreadsPerCore=1 TmpDisk=0 Weight=0 Owner=N/A MCS_label=N/A
   Partitions=gpu_v100
   BootTime=2020-01-10T15:25:50 SlurmdStartTime=2020-01-10T15:32:49
   CfgTRES=cpu=28,mem=250G,billing=28
   AllocTRES=cpu=28,mem=250G,billing=28
   CapWatts=n/a
   CurrentWatts=0 AveWatts=0
   ExtSensorsJoules=n/s ExtSensorsWatts=0 ExtSensorsTemp=n/s
$ module list
Currently Loaded Modulefiles:
 1) CUDA/10.0              3) TensorFlow/1.13.2-gpu-py3.6-cuda10
 2) cudnn/7.4.1-CUDA10.0
$ nvidia-smi
Tue Jan 14 08:25:27 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 418.67       Driver Version: 418.67       CUDA Version: 10.1     |
|-------------------------------+----------------------+----------------------+
| GPU  Name        Persistence-M| Bus-Id        Disp.A | Volatile Uncorr. ECC |
| Fan  Temp  Perf  Pwr:Usage/Cap|         Memory-Usage | GPU-Util  Compute M. |
|===============================+======================+======================|
|   0  Tesla V100-SXM2...  Off  | 00000000:8A:00.0 Off |                    0 |
| N/A   30C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   1  Tesla V100-SXM2...  Off  | 00000000:8B:00.0 Off |                    0 |
| N/A   27C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   2  Tesla V100-SXM2...  Off  | 00000000:B3:00.0 Off |                    0 |
| N/A   28C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+
|   3  Tesla V100-SXM2...  Off  | 00000000:B4:00.0 Off |                    0 |
| N/A   29C    P0    37W / 300W |      0MiB / 16130MiB |      0%      Default |
+-------------------------------+----------------------+----------------------+

+-----------------------------------------------------------------------------+
| Processes:                                                       GPU Memory |
|  GPU       PID   Type   Process name                             Usage      |
|=============================================================================|
|  No running processes found                                                 |
+-----------------------------------------------------------------------------+

实验过程

由于集群并没有连外网,数据清洗、分词时需要安装一些依赖和包的,因此都在本地完成了,而核心的深度学习过程是在集群上进行。

预处理preprocess.py

使用jieba对中文文本进行分词,并做了数据清洗工作:使用英文标点替代中文标点;并洗掉了原来数据中的一些乱码。

1
2
3
4
5
6
7
8
9
10
11
12
13
def getCMN(cmn):
    import jieba
    cmn_lines = open(cmn, 'r', encoding='utf-8').readlines()
    for i in range(len(cmn_lines)):
        s = ' '.join(jieba.cut(cmn_lines[i], cut_all=False))
        s = re.sub(r"&#[0-9]+;", r"", s)
        s = re.sub(r"�", r"", s)
        for zh, en in [("。", "."), ("!", "!"), ("?", "?"), (",", ",")]:
            s = s.replace(zh, en)
        s = re.sub(u"[^a-zA-Z0-9\u4e00-\u9fa5,.!?]", u" ", s)
        s = re.sub(r"\s+", r" ", s)
        cmn_lines[i] = s.lower().strip().split()
    return cmn_lines

英文部分的处理更加简单,这里不直接放出了。

下面是根据上一步分词的结果建立词典的过程。由于在数据集中数据显然不会是均匀分布的,常用词汇的频率明显会高于冷门词汇,因此我按照出现的频数从大到小排序并删去了频数最小的那部分词。这样做的道理是,冷门词汇很少会出现在翻译结果中,如果保留下来的话会增大词向量但并不能给最终的结果带来多少好的改进,反而会大大减慢我们学习的效率。

此外,我添加了三个标记词:

  • <unk>:代表未出现或难匹配,索引 0
  • <sos>:代表句子开头,索引 1
  • <eos>:代表句子结尾,索引 2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def build_vocab(lang, vocab, vocab_size):
    counter = {}
    for line in lang:
        for word in line:
            if word not in counter:
                counter[word] = 0
            counter[word] += 1
    id_to_word = ["<unk>", "<sos>", "<eos>"]+[x[0]
                                              for x in sorted(counter.items(), key=lambda x: x[1], reverse=True)]
    if len(id_to_word) > vocab_size:
        id_to_word = id_to_word[:vocab_size]
    with open(vocab, 'w', 'utf-8') as f:
        for word in id_to_word:
            f.write(word + '\n')

最后是根据词典重新给数据集标号,同时为原来的数据集加上句尾标记<eos>

1
2
3
4
5
6
7
8
9
def build_data(lang, vocab, data):
    id_to_word = open(vocab, 'r', encoding='utf-8').readlines()
    word_to_id = {}
    for i in range(len(id_to_word)):
        word_to_id[id_to_word[i].strip()] = i
    with open(data, 'w', 'utf-8') as fd:
        for line in lang:
            fd.write(' '.join(
                [str(word_to_id[w if w in word_to_id else '<unk>']) for w in line+['<eos>']]) + '\n')

训练模型train.py

此处我参考了《Tensorflow:实战 Google 深度学习框架》这本书中的教学代码,它的好处是内容深入浅出,并有比较细致的代码注释。

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
# 定义NMTModel类来描述模型。
class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell_fw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.enc_cell_bw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
          [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
           for _ in range(DECODER_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对应编码器中的一层。
        # 张量的维度是 [batch_size, HIDDEN_SIZE]。
        # enc_outputs是顶层LSTM在每一步的输出,它的维度是[batch_size,
        # max_time, HIDDEN_SIZE]。Seq2Seq模型中不需要用到enc_outputs,而
        # 后面介绍的attention模型会用到它。
        # 下面的代码取代了Seq2Seq样例代码中forward函数里的相应部分。
        with tf.variable_scope("encoder"):
            # 构造编码器时,使用bidirectional_dynamic_rnn构造双向循环网络。
            # 双向循环网络的顶层输出enc_outputs是一个包含两个张量的tuple,每个张量的
            # 维度都是[batch_size, max_time, HIDDEN_SIZE],代表两个LSTM在每一步的输出。
            enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(
                self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size,
                dtype=tf.float32)
            # 将两个LSTM的输出拼接为一个张量。
            enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1)

        with tf.variable_scope("decoder"):
            # 选择注意力权重的计算模型。BahdanauAttention是使用一个隐藏层的前馈神经网络。
            # memory_sequence_length是一个维度为[batch_size]的张量,代表batch
            # 中每个句子的长度,Attention需要根据这个信息把填充位置的注意力权重设置为0。
            attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
                HIDDEN_SIZE, enc_outputs,
                memory_sequence_length=src_size)

            # 将解码器的循环神经网络self.dec_cell和注意力一起封装成更高层的循环神经网络。
            attention_cell = tf.contrib.seq2seq.AttentionWrapper(
                self.dec_cell, attention_mechanism,
                attention_layer_size=HIDDEN_SIZE)

            # 使用attention_cell和dynamic_rnn构造编码器。
            # 这里没有指定init_state,也就是没有使用编码器的输出来初始化输入,而完全依赖
            # 注意力作为信息来源。
            dec_outputs, _ = tf.nn.dynamic_rnn(
                attention_cell, trg_emb, trg_size, dtype=tf.float32)

        # 计算解码器每一步的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

这里词向量我是使用随机初始化的。

1
initializer = tf.random_uniform_initializer(-0.05, 0.05)

模型预测test.py

可以加载上一步中训练的模型对单个句子进行预测。注意这里我没有读取字典,而让预测的结果仍然是在词典中的索引。主要是因为集群上的环境还是很不稳定,使用utf-8编码的时候老是出现这样那样的报错;而在集群上调试还是不如本地折腾来得痛快。

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
# -*- coding: utf-8 -*-
# To add a new cell, type '# %%'
# To add a new markdown cell, type '# %% [markdown]'
# %%
import tensorflow as tf
import codecs
import sys
from config import *

# 读取checkpoint的路径。7600表示是训练程序在第7600步保存的checkpoint。
CHECKPOINT_PATH += "-7600"

# %% [markdown]
# #### 2.定义NMT模型和解码步骤。

# %%
# 定义NMTModel类来描述模型。


class NMTModel(object):
    # 在模型的初始化函数中定义模型要用到的变量。
    def __init__(self):
        # 定义编码器和解码器所使用的LSTM结构。
        self.enc_cell_fw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.enc_cell_bw = tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
        self.dec_cell = tf.nn.rnn_cell.MultiRNNCell(
            [tf.nn.rnn_cell.BasicLSTMCell(HIDDEN_SIZE)
             for _ in range(DECODER_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)

        with tf.variable_scope("encoder"):
            # 使用bidirectional_dynamic_rnn构造编码器。这一步与训练时相同。
            enc_outputs, enc_state = tf.nn.bidirectional_dynamic_rnn(
                self.enc_cell_fw, self.enc_cell_bw, src_emb, src_size,
                dtype=tf.float32)
            # 将两个LSTM的输出拼接为一个张量。
            enc_outputs = tf.concat([enc_outputs[0], enc_outputs[1]], -1)

        with tf.variable_scope("decoder"):
            # 定义解码器使用的注意力机制。
            attention_mechanism = tf.contrib.seq2seq.BahdanauAttention(
                HIDDEN_SIZE, enc_outputs,
                memory_sequence_length=src_size)

            # 将解码器的循环神经网络self.dec_cell和注意力一起封装成更高层的循环神经网络。
            attention_cell = tf.contrib.seq2seq.AttentionWrapper(
                self.dec_cell, attention_mechanism,
                attention_layer_size=HIDDEN_SIZE)

        # 设置解码的最大步数。这是为了避免在极端情况出现无限循环的问题。
        MAX_DEC_LEN = 100

        with tf.variable_scope("decoder/rnn/attention_wrapper"):
            # 使用一个变长的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)
            # 调用attention_cell.zero_state构建初始的循环状态。循环状态包含
            # 循环神经网络的隐藏状态,保存生成句子的TensorArray,以及记录解码
            # 步数的一个整数step。
            init_loop_var = (
                attention_cell.zero_state(batch_size=1, dtype=tf.float32),
                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)
                # 调用attention_cell向前计算一步。
                dec_outputs, next_state = attention_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()

# %% [markdown]
# #### 翻译

# %%


def main():
    with codecs.open(OUT_TEST_DATA, 'a', 'utf-8') as f:
        input_id = codecs.open(
            SRC_TEST_DATA, 'r', 'utf-8').readlines()[int(sys.argv[1])]
        input_ids = [int(s) for s in input_id.strip().split()]
        # 定义训练用的循环神经网络模型。
        with tf.variable_scope("nmt_model", reuse=None):
            model = NMTModel()

        # 建立解码所需的计算图。

        output_op = model.inference(input_ids)

        sess = tf.Session()
        saver = tf.train.Saver()
        saver.restore(sess, CHECKPOINT_PATH)

        # 读取翻译结果。
        output_ids = sess.run(output_op)
        sess.close()
        for o in output_ids:
            f.write(str(o)+' ')
        f.write('\n')


if __name__ == "__main__":
    main()

训练参数config.py

训练模型的超参数如下,对应的数值和注释已经在这个文件里了,只要from config import *即可包含到每个文件中。

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
SRC_TRAIN_DATA = "./train.zh"          # 源语言输入文件。
SRC_TEST_DATA = "./test.zh"          # 源语言输入文件。
TRG_TRAIN_DATA = "./train.en"          # 目标语言输入文件。
TRG_TEST_DATA = "./test.en"          # 源语言输入文件。
OUT_TEST_DATA = "./out.en"
CHECKPOINT_PATH = "./attention_ckpt"   # checkpoint保存路径。
# 词汇表文件
SRC_VOCAB = "./zh.vocab"
TRG_VOCAB = "./en.vocab"

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

HIDDEN_SIZE = 1024                     # LSTM的隐藏层规模。
DECODER_LAYERS = 2                     # 解码器中LSTM结构的层数。这个例子中编码器固定使用单层的双向LSTM。
SRC_VOCAB_SIZE = 10000                 # 源语言词汇表大小。
TRG_VOCAB_SIZE = 4000                 # 目标语言词汇表大小。
BATCH_SIZE = 100                       # 训练数据batch的大小。
NUM_EPOCH = 100                        # 使用训练数据的轮数。
KEEP_PROB = 0.8                        # 节点不被dropout的概率。
MAX_GRAD_NORM = 5                      # 用于控制梯度膨胀的梯度大小上限。
SHARE_EMB_AND_SOFTMAX = True           # 在Softmax层和词向量层之间共享参数。

MAX_LEN = 50   # 限定句子的最大单词数量。

评估效果grade.py

最后是本地上对翻译效果的一个评估,这里直接使用了经典自然语言处理库nltk中的sentence_bleu进行实现。

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
from nltk.translate.bleu_score import sentence_bleu
from codecs import open
from config import *


if __name__ == '__main__':
    id_to_src = [x.strip() for x in open(SRC_VOCAB, 'r', 'utf-8').readlines()]
    id_to_trg = [x.strip() for x in open(TRG_VOCAB, 'r', 'utf-8').readlines()]

    questions = open(SRC_TEST_DATA, 'r', 'utf-8').readlines()
    reference = open(TRG_TEST_DATA, 'r', 'utf-8').readlines()
    candidate = open(OUT_TEST_DATA, 'r', 'utf-8').readlines()

    for i in range(len(questions)):
        print('<', end='\t')
        for j in questions[i].split():
            print(id_to_src[int(j)], end=' ')
        print('\n=', end='\t')
        for j in reference[i].split():
            print(id_to_trg[int(j)], end=' ')
        print('\n>', end='\t')
        for j in candidate[i].split():
            print(id_to_trg[int(j)], end=' ')

        print("\nBleu score = " +
              str(sentence_bleu([reference[i]], candidate[i]))+"\n")

结果分析

损失函数

训练过程中的损失函数变化如下。

Loss

可以看到训练前期训练是比较正常的,Loss 很快收敛,但是训练后期 Loss 不再下降,出现了过拟合的趋势。

翻译结果

选出几条比较有代表性的进行分析。其中<开头的为源语言句子,=开头为目标语言句子,>开头为模型预测结果。

这是预测结果里得分最低的一条,但是这里革命revolutiona fresh start其实意思上有那么点接近。主要原因是,评测集中每条源语言句子只提供了一条正确的目标语言句子,实际上一个句子可以有多种翻译方式,这就导致评测出来的 Bleu 值有很大局限性。

1
2
3
4
<	欧洲 的 容克 革命 <eos>
=	europe s <unk> revolution <eos>
>	<sos> a fresh start for europe <eos>
Bleu score = 3.641778071011159e-78

可以看到下面的的结果和前一个结果非常接近,在机器看来两个句子非常相似,前一个翻译很低分,在这里的评分就比较高了。

1
2
3
4
<	有关 欧洲 增长 的 议程 <eos>
=	an agenda for growth in europe <eos>
>	<sos> a german start for europe <eos>
Bleu score = 0.3384653583738009

而下面这一条是预测结果中得分最高的一条。由于我在数据清洗的时候将很多出现频度低的词直接标成<unk>,而在 Bleu 评分的过程中两个<unk>匹配也会被计分,因此它的评分有些虚高,参考价值不大。

1
2
3
4
<	就 像 许多 世纪 以前 <unk> <unk> <unk> 被 人 在 <unk> 教堂 里 杀害 一样 , 是因为 <unk> 相信 这 将 会 <unk> 国王 . <eos>
=	like the murder of <unk> <unk> <unk> in his <unk> <unk> many centuries ago , the crime was committed in the clear belief that it would <unk> the <unk> . <eos>
>	<sos> like the <unk> , even the <unk> treaty believes that so many <unk> in the <unk> they have , the state would want to negotiate the <unk> . <eos>
Bleu score = 0.47808191993705373

下面一句也取得了较高的 Bleu 评分,可以看到其实机器翻译模型将前一句的时间信息已经提取的比较准确了。这很大原因是国际政治新闻中的时间描述比较多,具体到具体的事件之后辨识度就没有那么高了,因此后面开始乱翻译了。

1
2
3
4
<	自从 2001 年 9 月 11 日 以后 , 美国 就 一直 在 针对 穆斯林 社会 的 <unk> 进行 一场 <unk> 运动 . <eos>
=	ever since september 11 , 2001 , the united states has been <unk> in a <unk> against the forces of evil in the muslim world . <eos>
>	<sos> since the events that has been overcome since september , the world has weakened consequences been entirely at the <unk> and weakened events in order . <eos>
Bleu score = 0.39792915797480105

这一条是我翻译结果里面我觉得最有意思的,结合最近的国际局势真的非常有灵性…

1
2
3
4
<	伊朗 是 矛盾 之地 . <eos>
=	iran is a land of <unk> . <eos>
>	<sos> iran won t stop . <eos>
Bleu score = 0.31068218135768266

对测试集的结果做 Bleu 评分后的分布大致如下。

Bleu

Beam Search 策略

经过测试和比较,集束搜索策略对我的结果几乎没有改进,大部分搜索的结果都和之前的接近,这意味着大部分情况下机器翻译的模型都能找到概率值最大的路径。同时,集束搜索的时空成本都大大增加了,有点得不偿失。

总结与思考(遇到的困难及采用的解决方法、后续改进方向等)

本学期的自然然语言处理课程终于结束,通过三次实验我体验了自然语言处理从数据挖掘、数据清洗再到模型搭建学习的完整过程。和本学期一些其他与人工智能相关的课程相比,这门课的机器学习的训练集合和训练时间都是最长的。好在使用用集群进行计算之后训练的速度确实大大加快,也能够塞更多的训练数据。听说大公司训练出的成熟对话模型的基本上都用了上 GB 级别的语料集合进行训练,那么看来做自然语言处理确实很吃计算资源。此外,在搭建模型的时候,要让写出来的模型能够运行,从数据爬取和清洗的过程就要很下一番功夫。然后,经过漫长时间的忙碌搭的模型终于跑出结果了,结果全是andthe一类的词,让人根本分不清到底是自己在实验的哪个过程出了问题,还是单纯的模型没有收敛完全,非常玄学和让人抓狂。调包调参上手很快,但是真正学好这门课,也完全不是一件容易的事。

记得自然语言处理的第一堂课上,老师说的这样一句话让人印象深刻:自然语言理解是人工智能皇冠上的明珠。和人工智能的另一大热门领域,计算机视觉比起来,自然语言处理这门学科的发展还处在发展更早期的阶段。虽然深度学习的热潮给自然语言处理领域带来了成果,但从理论方法的角度看,由于采集、整理、表示和有效应用大量知识的困难,这些系统更依赖于统计学的方法和其他「简单」的方法或技巧,而这些统计学的方法和其他「简单」的方法似乎也快达到它们的极限了。我非常期待这门学科在接下来的一段时期能够取得新理论的突破和机遇,为我们的生活提供更多的方便。