Tensorflow BiLSTM+CRF处理序列标注任务

本文以Word简历信息抽取为例子来处理序列标注问题。

传统的机器学习中,用于处理NLP任务中的序列标注问题的方法通常是隐马尔科夫模型(HMM)+ 维特比算法(Viterbi Algorithm),但随着深度学习的大热,越来越多的问题倾向于用深度学习技术来解决,而本文主要介绍的是基于Tensorflow的BiLSTM+CRF的深度学习模型。

在一份简历中,工作经历介绍通常包括时间、公司名称、职位名称、工作内容等,例如下面的数据样例:

2017.1-2018.5    某某公司    软件开发工程师    工作内容:主导后台开发

而我们的任务就是从这样的内容中提取出时间、公司名称、职位名称和工作内容。

首先我们需要大量类似样例的中文文本数据,并做BIO标注,作为训练数据。可以用CB代表公司的起始字、CI代表公司的剩余字,其他三项提取字段也以这样的方式进行标注,O代表不属于这四个字段的内容。按这样的规则,样例可标注为:

TB TI TI TI TI TI TI TI TI TI TI TI TI CB CI CI CI PB PI PI PI PI PI PI NB NI NI NI NI NI NI NI NI NI NI

然后用所有文本数据整合出字表,每个字代表一个ID,从1开始依次往后(0作为无效字符),并将文本数据中的每个字转换成对应的ID,标注数据也一样,转换成ID表示(0-9,0为无效字符)。

另外需要限制每一句的最大长度,大于该值截断,小于则padding。

做完简单的数据预处理之后,我们开始搭建BiLSTM+CRF模型。

模型需要三组输入数据:包括上面处理的两组ID序列,和每一句文本的真实长度。

self.input = tf.placeholder(tf.int32, shape=[self.batch_size, self.step_num])
self.output = tf.placeholder(tf.int32, shape=[self.batch_size, self.step_num])
self.sequence_lengths = tf.placeholder(tf.int32, shape=[self.batch_size])

这里要注意,如果将batch_size写成None的话,会报错,原因未知。

然后是word embedding层,这里没有使用预训练的embedding,代码如下:

with tf.name_scope("embedding_layer"):
    embeddings = tf.Variable(tf.random_uniform([self.vocabulary_size, self.embedding_size], -1.0, 1.0))
    sentence = tf.nn.embedding_lookup(embeddings, self.input)

然后是双向LSTM层,调用了bidirectional_dynamic_rnn()方法,代码如下:

with tf.name_scope("LSTM_scope"):
    lstm_fw_cell = tf.contrib.rnn.LSTMCell(self.hidden_units)
    lstm_bw_cell = tf.contrib.rnn.LSTMCell(self.hidden_units)
    (output_fw, output_bw), _ = tf.nn.bidirectional_dynamic_rnn(lstm_fw_cell, lstm_bw_cell, sentence, sequence_length=self.sequence_lengths, dtype=tf.float32)
    output = tf.concat([output_fw, output_bw], axis=-1)

该方法需要传入前向传播的Cell和后向传播的Cell,输入序列,以及输入序列的真实长度(可选,默认为输入序列的最大长度),而返回的output_fw和output_bw分别表示前向和后向各时序上的输出,然后用concat将两个输出合并。

接下来要利用CRF了,但在那之前,需要reshape上一层的输出,并传入全连接层转换一下维度,代码如下:

with tf.name_scope('fully_connected_layer'):
    w_d = self.__weight_variable([self.hidden_units * 2, 10])
    b_d = self.__bias_variable([10])
    out = tf.reshape(output, [-1, self.hidden_units * 2])
    y = tf.add(tf.matmul(out, w_d), b_d)

这里的10就代表最终得到的10种标注类型。

最后以crf_log_likelihood作为损失函数,利用条件随机场计算损失:

with tf.name_scope('loss'):
    self.logits = tf.reshape(y, [-1, self.step_num, 10])
    log_likelihood, self.transition_params = tf.contrib.crf.crf_log_likelihood(self.logits, self.output, self.sequence_lengths)
    self.loss = tf.reduce_mean(-log_likelihood)

这里可能有人会疑惑,这里应该可以直接用softmax_cross_entropy_with_logits计算交叉熵吧。没错,是可以。但是这就该提到CRF的优势了,它根据计算出的转移概率矩阵,使标签结果能够包含上下文信息,也就是说,结合了CRF,输出的标签结果将不再是相互独立的,而是最佳的标签序列。

模型搭建完成,接下来是训练部分,这里就不再详述了。详细代码已上传Github:https://github.com/supervipcard/LSTM-CRF-Tensorflow

测试的时候,通常使用viterbi_decode,代码如下:

viterbi, _ = tf.contrib.crf.viterbi_decode(logits, transition_params)

其中logits和transition_params需要先将测试句子传入模型计算得出,具体可参照Github上的代码。返回的viterbi便是测试句子的最终标签序列结果。

点赞 分享

发表评论

共有 0 条评论