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可以表示如下:
或者等效展开如下:
如果把神经网络的内部结构画出来,会是下面的结构:
这里,依次输入**“我 去 五道口 吃 肯德基”**每个单词的词嵌入向量,每一步都会输出一个隐藏状态(hidden state)。在计算某一步输出的隐藏状态的时候,会结合前一步的输出,生成一个新的隐藏状态。这样,每一步生成的隐藏状态相当于包含了前面所有步骤的信息,这个步骤称为编码(Encoder),最后一步输出的隐藏状态Ht就可以作为整个输入序列的表示,参与下一步的解码(Decoder)过程。
理论上RNN网络结构能够包含输入序列的所有信息,但是实际上它只能记住当前附近的几步输入的信息,随着距离的增加,RNN能记住的有效信息越来越少,这个有点儿类似狗熊掰棒子,记住了最近的信息,忘掉了之前的信息。对于只需要最近几步的依赖(短距离依赖)就可以完成的工作,RNN可以胜任,比如“下雨天我需要一把雨伞”,根据这句话猜测粗体的部分的“雨伞”,由于整个句子比较短,RNN网络需要分析的前后文距离比较短,可以解决这种问题。换一句话,“天气预报今天下雨,我要出远门,…,我需要一把雨伞”,在这句话中,由于最后的雨伞需要依赖句子开头的“下雨”才能分析出来,距离很长,这种情况下RNN网络就捉襟见肘了。此时需要一种能够长距离记录信息的网络,这种网络是长短时记忆网络(Long-Short term memory, LSTM)。
相比于上面的RNN内部结构包含的单层的神经网络,LSTM结构更加复杂,共包含四层神经网络:
在LSTM网络结构中,四层神经网络分为三个部分,红框表示的遗忘门(forget gate),蓝框表示的输入门(input gate),和绿框表示的输出门(output gate),它们分别控制如何将之前的记忆删除一部分,如何加入当前的记忆,如何将整合后的记忆和这一步的输入联合起来计算一个输出。图中两条水平向右的线,上面的叫CellState,可以认为是承载着前面遥远记忆的一条传送带,下面的叫HiddenState,是结合了当前输入,前一步输出,以及遥远记忆后的输出。当一句话的所有单词都经过LSTM网络处理后,最后输出的HiddenState Ht就是Encoder编码过程的输出,包含了整个输入序列的信息。
上面给出的是基本的LSTM网络结构,针对LSTM还有很多人提出了很多变种,如下图所示,此处不再一一介绍。
理解了上面的LSTM网络结构,在看下面的seq2seq整体结构就很容易理解了:
NER依存关系建模
有了上面的理论知识,我们就可以针对实际问题进行建模。我们的目的是将输入的一句话中实体间的关系提取出来。
输入:
我(O) 去(O) 惠新西街甲8号(ADDR) 的(O) 星巴克(CATER) 喝(O) 咖啡(O),预订(O) 电话(O) 18701500685(PHONE_NUM)
我们在这句话分词后面给出了每个单词的实体类型,其中ADDR代表地址,CATER代表餐馆,O代表未识别的其他类型。实体的类型作为输入的特征向量之一,连同每个单词的次嵌入向量一并作为LSTM网络的输入。
上面的一句话中实体关系表如下:
惠新西街甲8号 | 星巴克 | 18701500685 | |
---|---|---|---|
惠新西街甲8号 | - | right_desc | null |
星巴克 | - | - | 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)