为了账号安全,请及时绑定邮箱和手机立即绑定

NER依存关系模型:原理,建模及代码实现


关键词:seq2seq,RNN,LSTM,NER依存关系

命名实体识别(Named Entity Recognization, NER)是AI任务中重要的一类,而且在技术落地方面已经走在各种应用的前列,通过命名实体识别,我们已经能够识别出诸如 “我  去  五道口  吃 肯德基” 这句话中的地址(五道口)和餐馆(肯德基),利用这个信息,我们就可以给用户展示五道口的导航信息,和肯德基的餐馆信息等。目前在各种智能手机上已经广泛集成了该功能,如小米的传送门,Oppo/Vivo的智慧识屏等。但是NER识别有个局限,我们只能识别出独立的实体,实际上一句话中不同实体间很多时候是存在关联的,比如上面的例句中“五道口”这个地址就限制了“肯德基”餐馆的位置,所以我们就知道用户想搜索的是五道口的那家肯德基,而不是其他地方的肯德基,那我们如何找出这些实体间的关系,本文将利用seq2seq模型进行获取。

之前读过很多文章,它们介绍了各种各样的seq2seq模型,但是始终没找到一个从理论到实践能完全串联起来的文章,总是让人觉得云里雾里,似懂非懂。本文试图通过以下三个部分的讲解,提供一个从理论到实践的完整连贯的介绍:

  1. 首先介绍seq2seq模型的理论基础,包括循环神经网络(RNN)和长短时记忆网络(LSTM)。

  2. 讲解针对NER依存关系这个问题,我们怎么进行建模。

  3. 最后结合代码介绍如何实现seq2seq模型。

seq2seq理论基础

seq2seq模型是一种机器学习领域常用的模型,适用于将一个序列转换成另外一种序列的问题,可以是将一种语言翻译成另一种语言,将一篇文章转换成一段摘要,将一段语音转换成文字,又或者是将一句话的命名实体序列转换成实体间的关系序列。seq2seq模型通过循环神经网络(RNN)实现,循环神经网络可以记录序列前面几步的信息,从而推算下一步的输出。

一个简单的RNN Cell可以表示如下:


https://img1.sycdn.imooc.com//5d5808f70001cdf406300900.png

基本的循环神经网络

或者等效展开如下:


https://img1.sycdn.imooc.com//5d5808f800018f4608810273.png

展开的循环神经网络

如果把神经网络的内部结构画出来,会是下面的结构:


https://img1.sycdn.imooc.com//5d5808fc000131c709350382.png

RNN内部结构

这里,依次输入“我  去  五道口  吃  肯德基”每个单词的词嵌入向量,每一步都会输出一个隐藏状态(hidden state)。在计算某一步输出的隐藏状态的时候,会结合前一步的输出,生成一个新的隐藏状态。这样,每一步生成的隐藏状态相当于包含了前面所有步骤的信息,这个步骤称为编码(Encoder),最后一步输出的隐藏状态Ht就可以作为整个输入序列的表示,参与下一步的解码(Decoder)过程。

理论上RNN网络结构能够包含输入序列的所有信息,但是实际上它只能记住当前附近的几步输入的信息,随着距离的增加,RNN能记住的有效信息越来越少,这个有点儿类似狗熊掰棒子,记住了最近的信息,忘掉了之前的信息。对于只需要最近几步的依赖(短距离依赖)就可以完成的工作,RNN可以胜任,比如“下雨天我需要一把雨伞”,根据这句话猜测粗体的部分的“雨伞”,由于整个句子比较短,RNN网络需要分析的前后文距离比较短,可以解决这种问题。换一句话,“天气预报今天下雨,我要出远门,.....,我需要一把雨伞”,在这句话中,由于最后的雨伞需要依赖句子开头的“下雨”才能分析出来,距离很长,这种情况下RNN网络就捉襟见肘了。此时需要一种能够长距离记录信息的网络,这种网络是长短时记忆网络(Long-Short term memory, LSTM)。

相比于上面的RNN内部结构包含的单层的神经网络,LSTM结构更加复杂,共包含四层神经网络:


https://img1.sycdn.imooc.com//5d58090000015ece08500356.png

LSTM网络内部结构

在LSTM网络结构中,四层神经网络分为三个部分,红框表示的遗忘门(forget gate),蓝框表示的输入门(input gate),和绿框表示的输出门(output gate),它们分别控制如何将之前的记忆删除一部分,如何加入当前的记忆,如何将整合后的记忆和这一步的输入联合起来计算一个输出。图中两条水平向右的线,上面的叫CellState,可以认为是承载着前面遥远记忆的一条传送带,下面的叫HiddenState,是结合了当前输入,前一步输出,以及遥远记忆后的输出。当一句话的所有单词都经过LSTM网络处理后,最后输出的HiddenState Ht就是Encoder编码过程的输出,包含了整个输入序列的信息。

上面给出的是基本的LSTM网络结构,针对LSTM还有很多人提出了很多变种,如下图所示,此处不再一一介绍。


https://img1.sycdn.imooc.com//5d580903000114b509090698.png

https://img1.sycdn.imooc.com//5d58090800017f0708990273.png

将输入门和遗忘门合并进输出门的LSTM,GRU

理解了上面的LSTM网络结构,在看下面的seq2seq整体结构就很容易理解了:


https://img1.sycdn.imooc.com//5d580910000143e809100285.png

seq2seq模型原理图

NER依存关系建模

有了上面的理论知识,我们就可以针对实际问题进行建模。我们的目的是将输入的一句话中实体间的关系提取出来。
输入:
我(O)  去(O)  惠新西街甲8号(ADDR)  的(O)  星巴克(CATER)  喝(O)  咖啡(O),预订(O)  电话(O)  18701500685(PHONE_NUM)

我们在这句话分词后面给出了每个单词的实体类型,其中ADDR代表地址,CATER代表餐馆,O代表未识别的其他类型。实体的类型作为输入的特征向量之一,连同每个单词的次嵌入向量一并作为LSTM网络的输入。

上面的一句话中实体关系表如下:


惠新西街甲8号星巴克18701500685
惠新西街甲8号-right_descnull
星巴克--left_desc
星巴18701500685---

按照顺序,每个实体依次和其他实体产生一个关系,比如我们认为惠新西街甲8号是对星巴克的描述,那我们可以定义这种关系为right_desc(右侧描述),惠新西街甲8号和18701500685没有关系,我们定义为null, 18701500685也是对星巴克的描述,所以星巴克和18701500685的关系定义为left_desc(左侧描述)。这样,对于有N个非O类型的实体,它们之间的关系数是N*(N-1)/2个,我们就可以把两两之间的关系按照顺序作为输出序列:
输出:
right_desc  null  left_desc

这样就转换成了一个标准的seq2seq问题。
输入向量我们使用预训练的word embedding,尺寸是500000行128列,代表500000个单词,每个单词用128维向量表示。同时,我们将实体类型也用数字表示,加入到128维后面,所以每个单词用129维的向量表示。

代码实现

首先构造编码器:

# 输入序列第一部分:单词的embedding (batch_size, 50, 128)self.sentence_words_emb = tf.nn.embedding_lookup(self.encoder_embedding, self.input_sentence_words_ids)# 输入序列第二部分:单词的ner类型 (batch_size, 50) -> (batch_size, 50, 1)self.input_sentence_ner_expand = tf.expand_dims(self.input_sentence_ner_ids, 2, name='expand_dims_tag')# 两部分合并起来作为输入序列 (batch_size, 50, 128+1)self.input_feature = tf.concat([self.sentence_words_emb, self.input_sentence_ner_expand], 2)# 构建单个的LSTMCell,同时添加了Dropout信息self.encode_cell = self.build_encoder_cell()

encode_input_layer = Dense(self.hidden_units, dtype=tf.float32, name='input_projection')self.encoder_inputs_embedded = encode_input_layer(self.input_feature)# 将这个embedding信息作为tf.nn.dynamic_rnn的输入# encoder_outputs:[h_0, h_1, ..., h_t]   encoder_output_state: LSTMStateTuple(c_t, h_t)self.encoder_outputs, self.encoder_output_state = tf.nn.dynamic_rnn(cell=self.encode_cell,
        inputs=self.encoder_inputs_embedded,
        sequence_length=self.encode_inputs_length,                                                                      # 存储每句话的实际长度
         dtype=tf.float32,
         time_major=False)

上面的代码核心是调用tf.nn.dynamic_rnn函数进行编码,该函数的参数及含义如下:
cell:用于编码的神经网络构成,可以是单层RNNCell,也可以是多层RNNCell,这里我们使用的是MultiRNNCell,具体实现如下:

    def build_encoder_cell(self):        return MultiRNNCell([self.build_encode_single_cell() for i in range(self.depth)])    
    def build_decode_single_cell(self):
        cell = LSTMCell(self.hidden_units)

        cell = DropoutWrapper(cell, dtype=tf.float32, output_keep_prob=self.keep_prob_placeholder)        return cell

inputs:输入向量,我们将每个单词的word embedding(128维)和ner类型(1维)结合起来,构成输入向量(129维)
sequence_length:batch里面每句话不考虑填充部分的实际长度矩阵
time_major:inputs和outputs Tensor的格式,如果是true,格式为[max_time, batch_size, depth],如果是false,格式为[batch_size, max_time, depth]。这里我们指定为false

dynamic_rnn函数返回两个变量,第一个encoder_outputs是一个包含了编码过程中每一步输出的hidden_state的列表[h_0, h_1, ..., h_t]   ,第二个变量是一个tuple类型,存储的是编码过程最后一步输出的c_t和h_t,encoder_output_state: LSTMStateTuple(c_t, h_t)。其中h_t就是我们在解码过程中的输入,如果使用了Attention机制,还会用到hidden_state列表[h_0, h_1, ..., h_t] 。

解码过程:
解码过程要区分训练还是预测,训练的时候输出结果是已知的,预测的时候是未知的。下面是训练阶段的解码代码:

        with tf.variable_scope('decoder'):            const = [[0], [1], [2], [3], [4], [5], [6], [7]]  # decode embedding目前用的是一维的,回头试试8维,16维或者64维
            initializer = tf.constant_initializer(const)            self.decoder_embedding = tf.get_variable(name='decoder_embeddings', shape=[self.num_classes, 1],
                                                     initializer=initializer, dtype=tf.float32)            # 构建输出层全连接网络,输出的类别数目是label的种类8
            decoder_output_layer = Dense(self.num_classes, name='decoder_output_projection')            if self.mode == 'train':
                decoder_cell, decoder_initial_state = self.build_decoder_cell()                # 将目标结果转换成对应的embedding表示  (batch_size, decode_sentence_max_len) -> (batch_size, decode_sentence_max_len, 1)
                decoder_results_embedded = tf.nn.embedding_lookup(self.decoder_embedding, self.targets_train)  # tf.expand_dims(targets, 2)

                # TrainingHelper用于在Decoder过程中自动获取每个batch的数据
                training_helper = seq2seq.TrainingHelper(inputs=decoder_results_embedded,
                                                         sequence_length=self.train_decoder_results_length,
                                                         time_major=False,
                                                         name='training_helper')

                training_decoder = seq2seq.BasicDecoder(cell=decoder_cell,  # 加入Attention的decoder cell
                                                        helper=training_helper,  # 获取目标输出数据的helper函数
                                                        initial_state=decoder_initial_state,                                                        # Encoder过程输出的state作为Decoder过程的输入State
                                                        output_layer=decoder_output_layer)  # Decoder完成之后经过全连接网络映射到最终输出的类别

                # 获取一个batch里面最长句子的长度
                max_decoder_length = tf.reduce_max(self.train_decoder_results_length)                ## 使用training_decoder进行dynamic_decode操作,输出decoder结果
                decoder_outputs, _, _ = seq2seq.dynamic_decode(decoder=training_decoder,
                                                              impute_finished=True,
                                                              maximum_iterations=max_decoder_length)                # decoder_outputs = (rnn_outputs, sample_id)
                # 其中:rnn_output: [batch_size, decoder_targets_length, vocab_size],保存decode每个时刻每个单词的概率,可以用来计算loss
                # sample_id: [batch_size, decode_vocab_size], tf.int32,保存最终的编码结果,也就是rnn_output每个时刻概率最大值对应的类别。可以表示最后的答案

                # 生成一个和decoder_logits.rnn_output结构一样的tensor,代表一次训练的结果
                decoder_logits_train = tf.identity(decoder_outputs.rnn_output)                # 选择logits的最大值的位置作为预测选择的结果
                self.decoder_pred_train = tf.argmax(decoder_logits_train, axis=-1, name='decoder_pred_train')                # 根据输入batch中每句话的长度,和指定处理的最大长度,填充mask数据,这样可以提高计算效率,同时不影响最终结果
                masks = tf.sequence_mask(lengths=self.train_decoder_results_length,
                                         maxlen=max_decoder_length, dtype=tf.float32, name='masks')                # 计算loss
                self.loss = seq2seq.sequence_loss(logits=decoder_logits_train,  # 预测值
                                             targets=self.targets_train,  # 实际值
                                             weights=masks,  # mask值
                                             average_across_timesteps=True,
                                             average_across_batch=True, )                ## 接下来手动进行梯度更新
                # 首先获得trainable variables
                trainable_params = tf.trainable_variables()                # 使用gradients函数,计算loss对trainable_params的导数,trainable_params包含各个可训练的参数
                gradients = tf.gradients(self.loss, trainable_params)                # 对可训练参数的梯度进行正则化处理,将权重的更新限定在一个合理范围内,防止权重更新过于迅猛造成梯度爆炸或梯度消失
                clip_gradients, _ = tf.clip_by_global_norm(gradients, self.max_gradient_norm)                # 一次训练结束后更新参数权重
                self.optimizer = tf.train.AdamOptimizer(learning_rate=self.learning_rate).apply_gradients(
                    zip(clip_gradients, trainable_params), global_step=self.global_step)

训练过程的解码通过seq2seq.dynamic_decode进行,该函数的参数decoder我们使用BasicDecoder,BasicDecoder参数含义如下:
cell:解码的网络结构,该网络我们在build_decoder_cell函数里生成
helper:如何在每一步获取数据
initial_state:编码过程输出的h_t
output_layer:解码数据转换成最终识别类别的网络,这里我们使用Dense构建了一个输出数量为num_classes的全连接网络

build_decoder_cell函数代码如下:

    def build_decoder_cell(self):  
        encoder_outputs = self.encoder_outputs
        encoder_last_state = self.encoder_output_state
        encoder_inputs_length = self.encode_inputs_length        # Building attention mechanism: Default Bahdanau
        # 'Bahdanau' style attention: https://arxiv.org/abs/1409.0473
        self.attention_mechanism = attention_wrapper.BahdanauAttention(
            num_units=self.hidden_units, memory=encoder_outputs,
            memory_sequence_length=encoder_inputs_length, )        # Building decoder_cell
        self.decoder_cell_list = [self.build_decode_single_cell() for i in range(self.depth)]        def attn_decoder_input_fn(inputs, attention):            # Essential when use_residual=True
            _input_layer = Dense(self.hidden_units, dtype=tf.float32, name='attn_input_feeding')            return _input_layer(tf.concat([inputs, attention], -1))        # AttentionWrapper wraps RNNCell with the attention_mechanism
        # Note: We implement Attention mechanism only on the top decoder layer
        self.decoder_cell_list[-1] = attention_wrapper.AttentionWrapper(
            cell=self.decoder_cell_list[-1],
            attention_mechanism=self.attention_mechanism,
            attention_layer_size=self.hidden_units,
            cell_input_fn=attn_decoder_input_fn,
            initial_cell_state=encoder_last_state[-1],
            alignment_history=False,
            name='Attention_Wrapper')        # To be compatible with AttentionWrapper, the encoder last state
        # of the top layer should be converted into the AttentionWrapperState form
        # We can easily do this by calling AttentionWrapper.zero_state

        # Also if beamsearch decoding is used, the batch_size argument in .zero_state
        # should be ${decoder_beam_width} times to the origianl batch_size
        batch_size = self.batch_size
        initial_state = [state for state in encoder_last_state]

        initial_state[-1] = self.decoder_cell_list[-1].zero_state(batch_size=batch_size, dtype=tf.float32)
        decoder_initial_state = tuple(initial_state)        return MultiRNNCell(self.decoder_cell_list), decoder_initial_state

这段代码我们构建了解码的网络,可以使一个单一的RNNCell,也可以是多个RNNCell,我们使用的后者。在最后一个Cell上,我们添加了Attention机制,Attention机制通过AttentionWrapper实现,作用在decode_cell_list的最后一个Cell上,AttentionWrapper各参数含义:
cell:需要被Wrapper的网络节点本身,这里是我们节点列表的最后一个节点
attention_mechanism:attention_mechanism我们使用BahdanauAttention,BahdanauAttention的介绍见下面解释
attention_layer_size:网络输出层尺寸
cell_input_fn:如何整合网络的原始输入和attention,这里我们简单将两个tensor连接起来,通过一个Dense全连接网络
initial_cell_state:编码过程最后一个节点输出的h_t

BahdanauAttention各参数的含义:
num_units:Attention机制覆盖的距离,整合多大范围内的记忆
memory:encode输出的hidden_state列表[h_0, h_1, ..., h_t]
memory_sequence_length:输入句子的不考虑填充部分的实际长度

上面介绍的是train过程的解码过程,下面介绍预测过程的解码过程。

            decoder_cell_2, decoder_initial_state_2 = self.build_decoder_cell()            # Start_tokens: [batch_size,] `int32` vector
            start_tokens = tf.ones([self.batch_size, ], tf.int32) * self.output_start_token

            decode_input_layer = Dense(self.hidden_units, dtype=tf.float32, name='decode_input_layer')            # 解码过程中前一步的输出通过embedding_lookup转换成嵌入向量,并经过一个全连接网络,输出的是8个目标类别中每个类别的概率
            def embed_and_input_proj(inputs):  # todo: tensor经过Dense后变成什么??
                return decode_input_layer(tf.nn.embedding_lookup(self.decoder_embedding, inputs))            # Helper to feed inputs for greedy decoding: uses the argmax of the output
            predict_decoding_helper = seq2seq.GreedyEmbeddingHelper(start_tokens=start_tokens,
                                                                    end_token=self.output_end_token,
                                                                    embedding=embed_and_input_proj)            # Basic decoder performs greedy decoding at each time step
            print("building greedy decoder..")
            inference_decoder = seq2seq.BasicDecoder(cell=decoder_cell_2,
                                                     helper=predict_decoding_helper,
                                                     initial_state=decoder_initial_state_2,
                                                     output_layer=decoder_output_layer)

            predict_logits, final_state, final_sequence_lengths = seq2seq.dynamic_decode(
                decoder=inference_decoder,
                output_time_major=False,                # impute_finished=True, # error occurs
                maximum_iterations=self.decode_sentence_max_len)            #  [batch_size, max_time_step, 1]
            self.decoder_pred_decode = tf.expand_dims(predict_logits.sample_id, -1)

预测的过程和训练过程一样,也是通过dynamic_decode进行,主要区别在于BasicDecoder的helper参数不同,在训练的时候用到的是TrainingHelper,而预测过程用到的是GreedyEmbeddingHelper,区别在于训练过程不管每一步预测输出的是什么结果,下一步输入都不会使用这个数据,而是使用标记数据对应的正确结果作为输入,这样防止某个步骤输出的结果错误传递给后续的步骤,这是TrainingHelper的实现。而预测过程需要将某一步的输出通过argmax获取概率最大的作为结果,然后将这个结果转换成embedding作为下一步的输入,这就是GreedyEmbeddingHelper做的事情。GreedyEmbeddingHelper的参数如下:
start_tokens:输出序列的开始标志
end_token:输出序列的结束标志
embedding:如何将前一步的输出转换成下一步的输入,可以看到,我们的方法是先获取前一步输出的embeddings,然后经过一个全连接网络,再将输出作为下一步的输入

dynamic_decode输出的三个参数中的第一个predict_logits就是最终预测的结果,通过predict_logits.sample_id可以获取到每一步的预测结果,这就是我们最终需要的结果。

这样,从理论基础,到建模过程,再到最后的代码实现,我们完整的讲解了利用seq2seq模型根据输入序列生成输出序列的全过程,希望能够让你系统的了解到seq2seq模型是怎么回事,以及怎样运行的。

参考:
http://colah.github.io/posts/2015-08-Understanding-LSTMs/
https://zhuanlan.zhihu.com/p/28919765



作者:JackMeGo
链接:https://www.jianshu.com/p/787792b3ea91


点击查看更多内容
TA 点赞

若觉得本文不错,就分享一下吧!

评论

作者其他优质文章

正在加载中
  • 推荐
  • 评论
  • 收藏
  • 共同学习,写下你的评论
感谢您的支持,我会继续努力的~
扫码打赏,你说多少就多少
赞赏金额会直接到老师账户
支付方式
打开微信扫一扫,即可进行扫码打赏哦
今天注册有机会得

100积分直接送

付费专栏免费学

大额优惠券免费领

立即参与 放弃机会
意见反馈 帮助中心 APP下载
官方微信

举报

0/150
提交
取消