简介:
我记得我第一次听说深度学习在自然语言处理(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
斯坦福NLP课程
任务和数据
首先,让我们讨论一下序列标记是什么。 根据您的背景,您可能听说过不同的名称:命名实体识别,词性标注等。本文其余部分我们将专注于命名实体识别(NER)。 你可以查看维基百科。 一个例子是:
John lives in New York and works for the European UnionB-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-ORGrejects OGerman B-MISCcall Oto Oboycott OBritish B-MISClamb O. OPeter B-PERBlackburn 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 embeddingsK = 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_rnns = 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 charscell_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) + bscores = 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 masklosses = 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就可以轻松实现任何类型的深度学习系统。 但是,如果你正在尝试一些新的东西,你仍然需要更深层次的...
标签: 我们
②文章观点仅代表原作者本人不代表本站立场,并不完全代表本站赞同其观点和对其真实性负责。
③文章版权归原作者所有,部分转载文章仅为传播更多信息、受益服务用户之目的,如信息标记有误,请联系站长修正。
④本站一律禁止以任何方式发布或转载任何违法违规的相关信息,如发现本站上有涉嫌侵权/违规及任何不妥的内容,请第一时间反馈。发送邮件到 88667178@qq.com,经核实立即修正或删除。