TensorFlow实现序列标注:用bi-LSTM+CRF和字符嵌入实现NER和POS

简介:

我记得我第一次听说深度学习在自然语言处理(NLP)领域的魔力。 我刚刚与一家年轻的法国创业公司Riminder开始了一个项目,这是我第一次听说字嵌入。 生活中有一些时刻,与新理论的接触似乎使其他一切无关紧要。 听到单词向量编码了单词之间相似性和意义就是这些时刻之一。 当我开始使用这些新概念时,我对模型的简单性感到困惑,构建了我的第一个用于情感分析的递归神经网络。 几个月后,作为法国大学高等理工学院硕士论文的一部分,我正在 Proxem 研究更高级的序列标签模型。

Tensorflow vs Theano 当时,Tensorflow刚刚开源,Theano是使用最广泛的框架。对于那些不熟悉这两者的人来说,Theano在矩阵级别运行,而Tensorflow则提供了大量预编码层和有用的训练机制。使用Theano有时很痛苦,但却强迫我注意方程中隐藏的微小细节,并全面了解深度学习库的工作原理。

快进几个月:我在斯坦福,我正在使用 Tensorflow。有一天,我在这里,问自己:“如果你试图在Tensorflow中编写其中一个序列标记模型怎么办?需要多长时间?“答案是:不超过几个小时。

这篇文章的目标是提供一个如何使用 Tensorflow 构建一个最先进的模型(类似于本文)进行序列标记,并分享一些令人兴奋的NLP知识的例子!

与这篇文章一起,我发布了代码,并希望有些人会发现它很有用。您可以使用它来训练您自己的序列标记模型。我将假设关于递归神经网络的概念性知识。顺便说一句,在这一点上,我必须分享我对 karpathy 的博客的钦佩(特别是这篇文章“递归神经网络不合理的有效”)。对于刚接触 NLP 的读者,请看看令人惊叹的斯坦福NLP课程。

The Unreasonable Effectiveness of Recurrent Neural Networks
http://karpathy.github.io/2015/05/21/rnn-effectiveness/

斯坦福NLP课程
http://web.stanford.edu/class/cs224n/

任务和数据

首先,让我们讨论一下序列标记是什么。 根据您的背景,您可能听说过不同的名称:命名实体识别,词性标注等。本文其余部分我们将专注于命名实体识别(NER)。 你可以查看维基百科。 一个例子是:

John lives in New York and works for the European Union
B-PER O O B-LOC I-LOC O O O O B-ORG I-ORG

在 CoNLL2003 任务中,实体是 LOC,PER,ORG和MISC,用于位置,人员,组织和杂项。无实体标签是O.因为一些实体(如纽约)有多个单词,我们使用标记方案来区分开头(标签B -…)或实体内部(标签I-。 ..)。存在其他标记方案(IOBES等)。但是,如果我们暂停一下并以抽象的方式思考它,我们只需要一个系统为一个句子中的每个单词分配一个类(一个对应于一个标签的数字)。

“但等等,为什么这是一个问题?只需保留一份地点,通用名称和组织清单!“

我很高兴你问这个问题。使这个问题变得非常重要的是许多实体,如名称或组织,只是我们没有任何先验知识的虚构名称。因此,我们真正需要的是从句子中提取上下文信息的东西,就像人类一样!

对于我们的实现,我们假设数据存储在.txt文件中,每行包含一个单词及其实体,如下例所示:

EU B-ORG
rejects O
German B-MISC
call O
to O
boycott O
British B-MISC
lamb O
. O
Peter B-PER
Blackburn I-PER

模型

“让我猜一下…… LSTM?”

你是对的。 像大多数NLP系统一样,我们在某些时候会依赖于递归神经网络。 但在深入研究我们模型的细节之前,让我们分成3个部分:

  • Word表示:我们需要使用稠密表示。对于每个单词。 我们能做的第一件事就是加载一些预先训练好的单词嵌入(GloVe, Word2Vec, Senna,等)。 我们还将从字符中提取一些含义。 正如我们所说的,许多实体甚至没有预先训练的单词向量,并且单词以大写字母开头的事实可能有所帮助。
  • 上下文词表示:对于其上下文中的每个词,我们需要获得有意义的表示。 好猜,我们将在这里使用LSTM。
  • 解码:最终的一步。 一旦我们有一个代表每个单词的向量,我们就可以用它来做出预测。

词表示

对于每个单词,我们想要构建一个向量,这将为我们任务获取含义和相关热证。 我们将构建此向量作为来自 GloVe 的词嵌入和一个包含从字符级别提取的特征的向量的串联。 一种选择是使用手工选择的特征,例如,如果单词以大写字母开头,则为0或1的组件。 另一个更好的选择是使用某种神经网络为我们自动进行这种提取。 在这篇文章中,我们将在字符级别使用双向LSTM,但我们可以在字符或n-gram级别使用任何其他类型的递归神经网络甚至卷积神经网络。

在单词 w = [c1,c2,······,ci] 每个字符 ci(我们区分大小写)都和一个向量关联。我们在字符嵌入序列上运行双向 LSTM 并连接最终状态以获得固定大小的向量 wchars 。直观地,该向量捕获单词的形态。 然后,我们连接起来wchars 和 wglove,得到一个代表我们单词的向量 w=[wchars , wglove]。

我们来看看Tensorflow代码。 回想一下,当 Tensorflow 接收批量的单词和数据时,我们需要填充句子以使它们具有相同的长度。 因此,我们需要定义2个占位符:

# shape = (batch size, max length of sentence in batch)
word_ids = tf.placeholder(tf.int32, shape=[None, None])

# shape = (batch size)
sequence_lengths = tf.placeholder(tf.int32, shape=[None])

现在,让我们使用 tensorflow 内置函数来加载单词嵌入。 假设 embeddings 是一个带有我们的 GloVe embeddings 的 numpy 数组,这样embeddings [i]给出了第 i 个单词的向量。

L = tf.Variable(embeddings, dtype=tf.float32, trainable=False)
# shape = (batch, sentence, word_vector_size)
pretrained_embeddings = tf.nn.embedding_lookup(L, word_ids)

你应该使用 tf.Variable 加上参数 trainable = False 而不是 tf.constant,否则你会出现内存问题!

现在,让我们从字符构建我们的表示。 由于我们需要填充单词以使它们具有相同的长度,我们还需要定义2个占位符:

# shape = (batch size, max length of sentence, max length of word)
char_ids = tf.placeholder(tf.int32, shape=[None, None, None])

# shape = (batch_size, max_length of sentence)
word_lengths = tf.placeholder(tf.int32, shape=[None, None])

“等等,我们可以像这样使用 None 吗? 我们为什么需要呢?“

嗯,这取决于我们。 这取决于我们如何执行填充,但在这篇文章中我们选择动态地进行填充,即填充批次中的最大长度。 因此,句子长度和字长将取决于批次。 现在,我们可以从字符构建词嵌入。 这里,我们没有任何预训练的字符嵌入,所以我们调用 tf.get_variable ,它将使用默认的初始值设定项(xavier_initializer)为我们初始化矩阵。 我们还需要改变维度4维张量的维度以匹配 bidirectional_dynamic_rnn 的要求。 请特别注意此函数返回的类型。 此外,lstm的状态是记忆和隐藏状态的元组。

# 1. get character embeddings
K = tf.get_variable(name="char_embeddings", dtype=tf.float32,
 shape=[nchars, dim_char])
# shape = (batch, sentence, word, dim of char embeddings)
char_embeddings = tf.nn.embedding_lookup(K, char_ids)

# 2. put the time dimension on axis=1 for dynamic_rnn
s = tf.shape(char_embeddings) # store old shape
# shape = (batch x sentence, word, dim of char embeddings)
char_embeddings = tf.reshape(char_embeddings, shape=[-1, s[-2], s[-1]])
word_lengths = tf.reshape(self.word_lengths, shape=[-1])

# 3. bi lstm on chars
cell_fw = tf.contrib.rnn.LSTMCell(char_hidden_size, state_is_tuple=True)
cell_bw = tf.contrib.rnn.LSTMCell(char_hidden_size, state_is_tuple=True)

_, ((_, output_fw), (_, output_bw)) = tf.nn.bidirectional_dynamic_rnn(cell_fw,
 cell_bw, char_embeddings, sequence_length=word_lengths,
 dtype=tf.float32)
# shape = (batch x sentence, 2 x char_hidden_size)
output = tf.concat([output_fw, output_bw], axis=-1)

# shape = (batch, sentence, 2 x char_hidden_size)
char_rep = tf.reshape(output, shape=[-1, s[1], 2*char_hidden_size])

# shape = (batch, sentence, 2 x char_hidden_size + word_vector_size)
word_embeddings = tf.concat([pretrained_embeddings, char_rep], axis=-1)

请注意使用特殊参数 sequence_length,以确保我们获得的最后一个状态是最后一个有效状态。 感谢这个参数,对于无效的步长,dynamic_rnn 传递状态并输出零向量。

上下文字表示

一旦我们有了单词表示 w,我们只是在字向量序列上运行 LSTM(或bi-LSTM)并获得另一个向量序列(LSTM的隐藏状态或bi-LSTM情况下两个隐藏状态的串联)。

TensorFlow代码是直截了当的。这一次我们使用每个时间步骤的隐藏状态,而不仅仅是最终状态。因此,我们输入了 m 个 词向量 w1,……,wi,现在我们有了一系列向量 h1,……,hi。wi 只捕获单词级别(语法和语义)的信息,hi 还要考虑上下文。

cell_fw = tf.contrib.rnn.LSTMCell(hidden_size)
cell_bw = tf.contrib.rnn.LSTMCell(hidden_size)

(output_fw, output_bw), _ = tf.nn.bidirectional_dynamic_rnn(cell_fw,
 cell_bw, word_embeddings, sequence_length=sequence_lengths,
 dtype=tf.float32)

context_rep = tf.concat([output_fw, output_bw], axis=-1)

解码

在这个阶段计算标注分数,每个单词 w 和一个获取词意义的向量 h 相关联。从字的含义,字符及其上下文中捕获信息。 让我们用它来做出最后的预测。 我们可以使用全连接的神经网络来获得一个向量,其中每个条目对应于每个标签的分数。

W = tf.get_variable("W", shape=[2*self.config.hidden_size, self.config.ntags],
 dtype=tf.float32)

b = tf.get_variable("b", shape=[self.config.ntags], dtype=tf.float32,
 initializer=tf.zeros_initializer())

ntime_steps = tf.shape(context_rep)[1]
context_rep_flat = tf.reshape(context_rep, [-1, 2*hidden_size])
pred = tf.matmul(context_rep_flat, W) + b
scores = tf.reshape(pred, [-1, ntime_steps, ntags])

注意我们为偏置项使用了 zero_initializer 。

接下来我们有两个选择来做出最后的预测 softmax 和 linear-chain CRF。

训练

这就是开源的神奇之处! 实现CRF只需要一行!

# shape = (batch, sentence)
labels = tf.placeholder(tf.int32, shape=[None, None], name="labels")

log_likelihood, transition_params = tf.contrib.crf.crf_log_likelihood(scores, labels, sequence_lengths)

loss = tf.reduce_mean(-log_likelihood)

在局部 softmax 的情况下,损失的计算更经典,但我们必须特别注意填充并使用 tf.sequence_mask 将序列长度转换为布尔向量(掩码)。

losses = tf.nn.sparse_softmax_cross_entropy_with_logits(logits=scores, labels=labels)
# shape = (batch, sentence, nclasses)
mask = tf.sequence_mask(sequence_lengths)
# apply mask
losses = tf.boolean_mask(losses, mask)

loss = tf.reduce_mean(losses)

最后,我们可以将我们的训练操作定义为:

optimizer = tf.train.AdamOptimizer(self.lr)
train_op = optimizer.minimize(self.loss)

使用预训练模型

对于局部softmax方法,执行最终预测很简单,该类只是每个时间步长得分最高的类。 这是通过tensorflow完成的:

labels_pred = tf.cast(tf.argmax(self.logits, axis=-1), tf.int32)

对于CRF,我们必须使用动态规划,如上所述。 再说一次,这只需要一行 tensorflow 代码!

# shape = (sentence, nclasses)
score = ...
viterbi_sequence, viterbi_score = tf.contrib.crf.viterbi_decode(
 score, transition_params)

使用之前的代码,您应该获得90到91之间的F1分数!

结论

只要您正在寻找的网络层已经实现,Tensorflow就可以轻松实现任何类型的深度学习系统。 但是,如果你正在尝试一些新的东西,你仍然需要更深层次的…

    原文作者:人工智能遇见磐创
    原文地址: https://www.jianshu.com/p/a05fbb74651c
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞