自然语言处理N天-从seq2seq到Transformer02

自然语言处理N天-从seq2seq到Transformer02_第1张图片
新建 Microsoft PowerPoint 演示文稿 (2).jpg

这个算是在课程学习之外的探索,不过希望能尽快用到项目实践中。在文章里会引用较多的博客,文末会进行reference。
搜索Transformer机制,会发现高分结果基本上都源于一篇论文Jay Alammar的《The Illustrated Transformer》(图解Transformer),提到最多的Attention是Google的《Attention Is All You Need》。

  • 对于Transformer的运行机制了解即可,所以会基于这篇论文来学习Transformer,结合《Sklearn+Tensorflow》中Attention注意力机制一章完成基本的概念学习;
  • 找一个基于Transformer的项目练手

0.引言

Transformer由论文《Attention is All You Need》提出。Transformer和传统RNN的主要区别
1.传统RNN是通过不断循环完成学习,通过每次迭代后的输出实现对上下文的记忆功能,这样才有了LSTM和GRU模型,因为它们能够较好处理RNN梯度爆炸和梯度消失问题,通过对各类门的操作实现“记忆”。但是问题是RNN的训练非常缓慢,之前实现作诗软件的那篇文章,一个2层的LSTM我用I7处理器跑了将近8个小时……
2.Transformer不需要循环,通过构建self-attention机制来完成对上下文和距离较远词汇的结合,并且通过并行进行处理,让每个单词在多个处理步骤中注意到句子中的其他单词,Transformer 的训练速度比 RNN 快很多,而且其翻译结果也比 RNN 好得多。但是在处理小型结构化的语言理解任务或是简单算法任务时表现不如传统模型。

现在Transformer已经被扩展为一个通用模型

研究者将该模型建立在 Transformer 的并行结构上,以保持其快速的训练速度。但是他们用单一、时间并行循环的变换函数的多次应用代替了 Transformer 中不同变换函数的固定堆叠(即,相同的学习变换函数在多个处理步骤中被并行应用于所有符号,其中每个步骤的输出馈入下一个)。RNN 逐个符号(从左到右)处理序列,而 Universal Transformer 同时处理所有符号(像 Transformer 一样),然后使用自注意力机制在循环处理步骤(步骤数量可变)上,对每个符号的解释进行改进。这种时间并行循环机制比 RNN 中使用的顺序循环(serial recurrence)更快,也使得 Universal Transformer 比标准前馈 Transformer 更强大。

Transformer模型能做什么?
目前最火的那个帖子认为Transformer已经可以完成大多数的任务。这个也是这几天我要探究的。

1.从Encoder到Decoder实现Seq2Seq模型

本文来源《从Encoder到Decoder实现Seq2Seq模型》
采用seq2seq框架来实现MT(机器翻译)现在已经是一个非常热点的研究方向,各种花式设计归根离不开RNN、CNN同时辅以attention。但是正如上文所说的,对于NLP来讲不论是RNN还是CNN都存在其固有的缺陷,即使attention可以缓解长距依赖的问题。
因此我觉得可以先从seq2seq开始,在这里观察一个简单的Seq2Seq,使用TensorFlow来实现一个基础版本的Seq2Seq,主要帮助理解Seq2Seq中的基础架构。

最基础的Seq2Seq模型包含三个部分:Encoder、Decoder以及连接两者固定大小的State Vector
Encoder通过学习输入,将其编码成一个固定大小的状态向量S,继而将S传给Decoder,Decoder再通过对状态向量S的学习来进行输出。
下面利用TensorFlow来构建一个基础的Seq2Seq模型,通过向我们的模型输入一个单词(字母序列),例如hello,模型将按照字母顺序排序输出,即输出ehllo。

1)数据集

2)读取数据和预处理

3)模型构建

接上一节,在完成encoder和decoder层的处理后,需要将两层进行整合。
seq2seq模型的整合
构建好了Encoder层与Decoder以后,需要将它们连接起来构建Seq2Seq模型。
注意:在前面的decoder生成和这里模型整合的时候,都会出现下划线'_',作用大概是占位,比如这里的 _, encoder_state = get_encoder_layer()只是要获取encoder_state状态,其他数据不要。

def seq2seq_model(input_data, targets, lr, target_sequence_length,
                  max_target_sequence_length, source_sequence_length,
                  source_vocab_size, target_vocab_size,
                  encoder_embedding_size, decoder_embedding_size,
                  rnn_size, num_layers,batch_size):
    # 获取encoder的状态输出
    _, encoder_state = get_encoder_layer(input_data,
                                         rnn_size,
                                         num_layers,
                                         source_sequence_length,
                                         source_vocab_size,
                                         encoder_embedding_size)

    # 预处理后的decoder输入
    decoder_input = process_decoder_input(targets, target_letter_to_int, batch_size)

    # 将状态向量与输入传递给decoder
    training_decoder_output, predicting_decoder_output = decoding_layer(target_letter_to_int,
                                                                        decoder_embedding_size,
                                                                        num_layers,
                                                                        rnn_size,
                                                                        target_sequence_length,
                                                                        max_target_sequence_length,
                                                                        encoder_state,
                                                                        decoder_input)

    return training_decoder_output, predicting_decoder_output

定义loss function、optimizer以及gradient clipping

# 构造graph
train_graph = tf.Graph()
with train_graph.as_default():
    # 获得模型输入
    input_data, targets, lr, target_sequence_length, max_target_sequence_length, source_sequence_length = get_inputs()

    training_decoder_output, predicting_decoder_output = seq2seq_model(input_data,
                                                                       targets,
                                                                       lr,
                                                                       target_sequence_length,
                                                                       max_target_sequence_length,
                                                                       source_sequence_length,
                                                                       len(source_letter_to_int),
                                                                       len(target_letter_to_int),
                                                                       encoding_embedding_size,
                                                                       decoding_embedding_size,
                                                                       rnn_size,
                                                                       num_layers)

    training_logits = tf.identity(training_decoder_output.rnn_output, 'logits')
    predicting_logits = tf.identity(predicting_decoder_output.sample_id, name='predictions')

    masks = tf.sequence_mask(target_sequence_length, max_target_sequence_length, dtype=tf.float32, name='masks')

    with tf.name_scope("optimization"):
        # Loss function
        cost = tf.contrib.seq2seq.sequence_loss(
            training_logits,
            targets,
            masks)

        # Optimizer
        optimizer = tf.train.AdamOptimizer(lr)

        # Gradient Clipping
        gradients = optimizer.compute_gradients(cost)
        capped_gradients = [(tf.clip_by_value(grad, -5., 5.), var) for grad, var in gradients if grad is not None]
        train_op = optimizer.apply_gradients(capped_gradients)

目前为止完成了整个模型的构建,但还没有构造batch函数,batch函数用来每次获取一个batch的训练样本对模型进行训练。
在这里,我们还需要定义另一个函数对batch中的序列进行补全操作。假如定义了batch=2,里面的序列分别是
[['h', 'e', 'l', 'l', 'o'],
['w', 'h', 'a', 't']]
那么这两个序列的长度一个是5,一个是4,变长的序列对于RNN来说是没办法训练的,这时要对短序列进行补全,补全以后如下
[['h', 'e', 'l', 'l', 'o'],
['w', 'h', 'a', 't', '']]
这样就保证了每个batch中的序列长度是固定的。

def pad_sentence_batch(sentence_batch, pad_int):
    '''
    对batch中的序列进行补全,保证batch中的每行都有相同的sequence_length

    参数:
    - sentence batch
    - pad_int: 对应索引号
    '''
    max_sentence = max([len(sentence) for sentence in sentence_batch])
    return [sentence + [pad_int] * (max_sentence - len(sentence)) for sentence in sentence_batch]


def get_batches(targets, sources, batch_size, source_pad_int, target_pad_int):
    '''
    定义生成器,用来获取batch
    '''
    for batch_i in range(0, len(sources) // batch_size):
        start_i = batch_i * batch_size
        sources_batch = sources[start_i:start_i + batch_size]
        targets_batch = targets[start_i:start_i + batch_size]
        # 补全序列
        pad_sources_batch = np.array(pad_sentence_batch(sources_batch, source_pad_int))
        pad_targets_batch = np.array(pad_sentence_batch(targets_batch, target_pad_int))

        # 记录每条记录的长度
        targets_lengths = []
        for target in targets_batch:
            targets_lengths.append(len(target))

        source_lengths = []
        for source in sources_batch:
            source_lengths.append(len(source))

        yield pad_targets_batch, pad_sources_batch, targets_lengths, source_lengths

训练模型

# 将数据集分割为train和validation
train_source = source_int[batch_size:]
train_target = target_int[batch_size:]
# 留出一个batch进行验证
valid_source = source_int[:batch_size]
valid_target = target_int[:batch_size]
(valid_targets_batch, valid_sources_batch, valid_targets_lengths, valid_sources_lengths) = next(
    get_batches(valid_target, valid_source, batch_size,
                source_letter_to_int[''],
                target_letter_to_int['']))

display_step = 50  # 每隔50轮输出loss

checkpoint = r"trained_model.ckpt"
with tf.Session(graph=train_graph) as sess:
    sess.run(tf.global_variables_initializer())

    for epoch_i in range(1, epochs + 1):
        for batch_i, (targets_batch, sources_batch, targets_lengths, sources_lengths) in enumerate(
                get_batches(train_target, train_source, batch_size,
                            source_letter_to_int[''],
                            target_letter_to_int[''])):

            _, loss = sess.run(
                [train_op, cost],
                {input_data: sources_batch,
                 targets: targets_batch,
                 lr: learning_rate,
                 target_sequence_length: targets_lengths,
                 source_sequence_length: sources_lengths})

            if batch_i % display_step == 0:
                # 计算validation loss
                validation_loss = sess.run(
                    [cost],
                    {input_data: valid_sources_batch,
                     targets: valid_targets_batch,
                     lr: learning_rate,
                     target_sequence_length: valid_targets_lengths,
                     source_sequence_length: valid_sources_lengths})

                print('Epoch {:>3}/{} Batch {:>4}/{} - Training Loss: {:>6.3f}  - Validation loss: {:>6.3f}'
                      .format(epoch_i,
                              epochs,
                              batch_i,
                              len(train_source) // batch_size,
                              loss,
                              validation_loss[0]))

    # 保存模型
    saver = tf.train.Saver()
    saver.save(sess, checkpoint)
    print('Model Trained and Saved')

预测
在完成了模型训练之后,可以进行数据验证了

def source_to_seq(text):
    '''
    对源数据进行转换
    '''
    sequence_length = 7
    return [source_letter_to_int.get(word, source_letter_to_int['']) for word in text] + [source_letter_to_int['']]*(sequence_length-len(text))


# 输入一个单词
input_word = 'hello'
text = source_to_seq(input_word)

# checkpoint = "./trained_model.ckpt"

loaded_graph = tf.Graph()
with tf.Session(graph=loaded_graph) as sess:
    # 加载模型
    loader = tf.train.import_meta_graph(checkpoint + '.meta')
    loader.restore(sess, checkpoint)

    input_data = loaded_graph.get_tensor_by_name('inputs:0')
    logits = loaded_graph.get_tensor_by_name('predictions:0')
    source_sequence_length = loaded_graph.get_tensor_by_name('source_sequence_length:0')
    target_sequence_length = loaded_graph.get_tensor_by_name('target_sequence_length:0')

    answer_logits = sess.run(logits, {input_data: [text] * batch_size,
                                      target_sequence_length: [len(input_word)] * batch_size,
                                      source_sequence_length: [len(input_word)] * batch_size})[0]

pad = source_letter_to_int[""]

print('原始输入:', input_word)

print('\nSource')
print('  Word 编号:    {}'.format([i for i in text]))
print('  Input Words: {}'.format(" ".join([source_int_to_letter[i] for i in text])))

print('\nTarget')
print('  Word 编号:       {}'.format([i for i in answer_logits if i != pad]))
print('  Response Words: {}'.format(" ".join([target_int_to_letter[i] for i in answer_logits if i != pad])))

原始输入: hello
Source
Word 编号: [13, 25, 16, 16, 26, 0, 0]
Input Words: h e l l o
Target
Word 编号: [25, 13, 16, 16, 26]
Response Words: e h l l o


原始输入: communication
Source
Word 编号: [21, 5, 7, 7, 9, 26, 22, 21, 10, 12, 22, 5, 26]
Input Words: c o m m u n i c a t i o n
Target
Word 编号: [21, 28, 22, 22, 19, 16, 9, 26, 26, 5, 7, 7, 3]
Response Words: c s i i l b u n n o m m

至此,实现了一个基本的序列到序列模型,Encoder通过对输入序列的学习,将学习到的信息转化为一个状态向量传递给Decoder,Decoder再基于这个输入得到输出。在运行后可以发现最终模型的训练loss相对已经比较低了,并且从例子看,其对短序列的输出还是比较准确的,但一旦我们的输入序列过长,比如15甚至20个字母的单词,其Decoder端的输出就非常的差。

你可能感兴趣的:(自然语言处理N天-从seq2seq到Transformer02)