美文网首页人工智能与自然语言处理算法小白菜NLP
TensorFlow实现序列标注:用bi-LSTM+CRF和字符

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

作者: 人工智能遇见磐创 | 来源:发表于2019-03-29 15:37 被阅读2次

    简介:

    我记得我第一次听说深度学习在自然语言处理(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就可以轻松实现任何类型的深度学习系统。 但是,如果你正在尝试一些新的东西,你仍然需要更深层次的...

    相关文章

      网友评论

        本文标题:TensorFlow实现序列标注:用bi-LSTM+CRF和字符

        本文链接:https://www.haomeiwen.com/subject/ncglbqtx.html