这一篇文章,主要讲一下用深度学习(神经网络)的方法来做命名实体识别。现在最主流最有效的方法基本上就是lstm+CRF了。其中CRF部分,只是把转移矩阵加进来了而已,而其它特征的提取则是交由神经网络来完成。当然了,特征提取这一部分我们也可以使用CNN,或者加入一些attention机制。
接下来,我将参考国外的一篇博客《Sequence Tagging with Tensorflow》,结合tensorflow的代码,讲一下用双向lstm+CRF做命名实体识别。
1.命名实体识别简述
命名实体识别任务本质上就是序列标注任务。来一个例子:
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。由于有一些实体(比如New York)由多个词组成,所以我们使用用一种简单的标签体系:
B-来标记实体的开始部分,I-来标记实体的其它部分。
我们最终只是想对句子里面的每一个词,分配一个标签。
2.模型
整个模型的主要组成部分就是RNN。我们将模型的讲解分为以下三个部分:
- 词向量表示
- 词的上下文信息表示
- 解码
2.1 词向量表示
对于每一个单词,我们用词向量来表示,用来捕获词本身的信息。这个词向量由两部分concat起来,一部分是用GloVe训练出来的词向量,另一部分,是字符级别的向量。
在以往,我们会手工提取并表示一些特征,比如用1,0来表示某个单词是否是大写开头,而在这个模型里面,我们不需要人工提取特征,只需要字符级别上面使用双向LSTM,就可以提取到一些拼写层面的特征了。当然了,CNN或者其他的RNN也可以干类似的事情。
对于每一个单词里面的每一个字母(区分大小写),我们用这个向量来表示,对字母级别的embedding跑一个bi-LSTM,然后将最后的隐状态输出拼接起来(因为是双向,所以有两个最后隐状态,如上图),得到一个固定长度的表达,直觉上,我们可以认为这个向量提取了字母级别的特征,比如大小写、拼写规律等等。然后,我们将这个向量和Glove训练好的w_{glove}拼接起来,得到某个词最终的词向量表达:,其中。
看一下tensorflow对应的实现代码。
# 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的内置函数来读取word embeddings。假设这个embeddings
是一个由GloVe训练出来的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
,否则可能会面临内存问题。
好,接下来,让我们来对字母建立向量。
# 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
呢?
其实这取决于我们。在我们的代码实现中,我们的padding是动态的,也就是和batch的最大长度对齐。因此,句子长度和单词长度取决于batch。
好了,继续。在这里,我们没有任何预训练的字母向量,所以我们调用tf.get_variable
来初始化它们。我们也要reshape一下四维的tensor,以符合bidirectional_dynamic_rnn
的所需要的输入。代码如下:
# 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
这个参数的用法,它让我们可以得到最后一个有效的state,对于无效的time steps,dynamic_rnn
直接穿过这个state,返回零向量。
2.2 词的上下文信息表示
当有了词向量之后,就可以对一个句子里的每一个词跑LSTM或者双向LSTM了,然后得到另一个向量表示:,如下图:
对应的tensorflow代码很直观,这次我们用每一个隐藏层的输出,而不是最后一个单元的输出。因此,我们输入一个句子,有m个单词:,得到m个输出:。现在的输出,是包含上下文信息的:
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)
2.3 解码
最后,我们要对每一个词分配一个tag。用一个全连接层就可以搞定。
假如,一共有9种tag,那么我们可以得到权重矩阵和偏置矩阵,最后计算某个词的得分向量,可以解释为,某个词标记成第个tag的得分,tensorflow的实现是这样的:
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
来初始化偏置。
有了分数之后,我们有两种方案用来计算最后的tag:
- softmax:将得分归一化为概率。
- 线性CRF:第一种方案softmax,只做了局部的考虑,也就是说,当前词的tag,是不受其它的tag的影响的。而事实上,当前词tag是受相邻词tag的影响的。定义一系列词,一系列的得分向量,还有一系列标签,线性CRF的计算公式是这样的:
在上面的式子里,是转移矩阵,尺寸为,用来刻画相邻tag的依赖、转移关系;是结束、开始tag的代价向量。下面是一个计算例子:
了解了CRF得分式子,接下来要做两件事:
- 找到得分最高的tag序列。
- 计算句子的tag概率分布。
“仔细想想,计算量是不是太大了?”
没错,计算量相当大。就上面的例子而言,有9种tag,一个句子有m个单词,一共有种可能,代价太大了。
幸运的是,由于式子有递归的特性,所以我们可以用动态规划的思想来解决这个问题。假设是时间步的解(每个时间步都是有9种可能的),那么,继续往前推,时间步的解,可以由下式表示:
每一个递归步骤的复杂度为,由于我们进行了步,所以总的复杂度是。
最后,我们需要在CRF层应用softmax,将得分概率分布计算出来。我们得计算出所有的可能,如下式子:
上面提到的递归思想在这里也可以应用。先定义,表示从时间步开始、以为tag开始的序列,计算公式如下:
最后,序列概率计算式子如下:
2.4 训练
最后,就是训练部分了。训练的损失函数采用的是cross-entropy(交叉熵),计算公式如下:
其中,为正确的标注序列,它的概率计算公式如下:
- CRF:
- local softmax:
“额..CRF层的损失很难计算吧..?”
没错,但是大神早就帮你做好了。在tensorflow里面,一行就能调用。下面的代码会帮我们计算CRF的loss,同时返回矩阵T,以助我们做预测:
# 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)
local softmax的loss计算过程很经典,但我们需要用tf.sequence_mask
将sequence转化为bool向量:
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)
最后,定义train op:
optimizer = tf.train.AdamOptimizer(self.lr) train_op = optimizer.minimize(self.loss)
2.5 使用模型
最后的预测步骤很直观:
labels_pred = tf.cast(tf.argmax(self.logits, axis=-1), tf.int32)
至于CRF层,仍然用到上面提到过的动态规划思想。
# shape = (sentence, nclasses) score = ... viterbi_sequence, viterbi_score = tf.contrib.crf.viterbi_decode( score, transition_params)
最终通过这份代码,F1值能跑到90%到91%之间。
3.后记
神经网络做NER,大部分套路都是这样:用基本的RNN、CNN模型做特征提取,最后加上一层CRF,再加点attention机制能稍微提升一下效果,基本上就到瓶颈了。
在2017年6月份,谷歌团队出品这篇论文《Attention Is All You Need》还是给我们带来不少震撼的,不用RNN,CNN,只用attention机制,就刷新了翻译任务的最好效果。所以,我们是不是可以想,把这种结构用到命名实体识别里面呢?
果然,已经有人开始做相关研究。《Deep Semantic Role Labeling with Self-Attention》这篇论文发表于2017年12月,实现了一个类似刚才说到的谷歌的模型,做的是SRL任务,也取得了不错的效果,同时他们也有放出实现代码:https://github.com/XMUNLP/Tagger
值得学习一下。
另外,用多模态来做实体识别也是一个方向,特别是对于一些类似微博的语料(有图片),这样做效果更佳。
您好,我最近刚想做命名实体识别,还是小白一个,感觉您的文章思路很清晰,从系列一到系列四一步步提升,能不能把这四个系列部分的代码和语料都发给我呢??谢谢了。
最近有点忙,晚些整理了发你
请问为啥我用ner做识别时预测出很多不是词的词,比如北京海淀,预测京海。
可能训练的数据量太少。另外,按照你的说法,你应该是以字符级别做的模型。也可以尝试中文分词后,再做一版模型看看
已发送,请查收
大神在闲暇之余一定要记得给我发哦(渴望的眼神.jpg)
一口气把您NER的博文读完了,受益匪浅,既有理论又有实践指导,很期待能够复现您的方法,能否把这个系列的代码和语料发我一份,84752220@qq.com.不胜感激。
近期有空整理一下再发过去
已发送
大神好,可以麻烦发我一份代码吗,非常感谢!
已发送
辛苦辛苦,多谢!
博主说的真的有够专业,我最近也在做一个类似实体识别的project,感觉您说的可以帮我拓展思路,请问能否把这个系列的代码和语料发我一份,1542624983@qq.com.非常感谢啦!
已发送,请查收
大神好,可以麻烦发我一份代码吗,非常感谢!
已发送到你邮箱
节日期间,不辞辛劳,万分感谢!
素昧平生,慷慨共享,深受感动!
博主可以给我也发一份语料和代码嘛,感觉你讲解得超清楚(深陷期末泥淖的我需要学习一下这方面的知识做一个presentation)
万分感谢~手动笑脸
这是代码:https://github.com/guillaumegenthial/sequence_tagging
语料可以搜一下conll2003
您好,麻烦可以发给我一份代码吗?非常感谢!
https://github.com/guillaumegenthial/sequence_tagging
大佬~请问可以麻烦发一份代码到我的邮箱里吗?感激不尽!!
邮箱是yut1998@126.com
Thanks♪(・ω・)ノ
没事没事不用麻烦了!刚看到您发的代码地址了!
您好,语料可以发我一份吗?conll2003的语料网上找不到。
https://www.lookfor404.com/命名实体识别的语料和代码/
不好意思,github的log里面给的语料的连接打不开了,您能发我一份吗?多谢!
博主好,找到conll2003的数据集了。
想请教一下,如果想加入句法分析特征,直接添加在test.txt中,做为一列出现,可以吗?也就是把conll2003的格式word、pos tag、chunk tag、ner tag,变成word、pos tag、chunk tag、句法tag、ner tag。能否这样添加特征?还想加入一些位置特征,0、1、2、3数值型,不知是否可以?
请博主不吝指点,是在小白一个,多谢博主。
可以加的啊,一般不同的特征可以单独作为一列。在CRF++里面就可以直接用这个格式了~如果在神经网络里面,一般会把这些特征单独映射到一个随机初始化的embedding层,然后concate到词向量后面
https://www.lookfor404.com/命名实体识别的语料和代码/
大神,可以发一份语料和代码吗?我的邮箱453472804@qq.com,非常感谢!!!
https://www.lookfor404.com/命名实体识别的语料和代码/
求大佬的CoNLL语料库,在网上翻遍了也没有找到,万分感谢!
如果博主看到了麻烦传一份语料库,帮大忙了,谢谢!邮箱是fuyj@mail.ustc.edu.cn
https://www.lookfor404.com/命名实体识别的语料和代码/
你好,也帮忙发我一份,424062028@qq.com,万分感谢!
https://www.lookfor404.com/命名实体识别的语料和代码/
您好,
我正在尝试BiLSTM+crf做序列标注,但是代码一直有些问题。可以参考一下您的语料和代码吗?(714753195@qq.com)
https://www.lookfor404.com/命名实体识别的语料和代码/
请问一下,测评的时候有没有去掉‘O’标签。去掉前后差别有多大?
为什么要去掉O标签呢?每一个单词要有对应的标签才行呀,O代表着其它成分
hi 你好!我run了一下你github的代码,出现下面的错误,训练不能成功,麻烦看看是什么意思,对python不熟悉,希望用python来训练模型,然后用C++来提供NER服务。
make run:
python build_data.py
Building vocab…
– done. 21 tokens
Building vocab…
– done. 400000 tokens
Writing vocab…
– done. 23 tokens
Writing vocab…
– done. 9 tokens
Traceback (most recent call last):
File “build_data.py”, line 55, in
main()
File “build_data.py”, line 46, in main
config.filename_trimmed, config.dim_word)
File “/home/sunxx/work/tensorflow/sequence_tagging/model/data_utils.py”, line 217, in export_trimmed_glove_vectors
np.savez_compressed(trimmed_filename, embeddings=embeddings)
File “/usr/lib64/python2.7/site-packages/numpy/lib/npyio.py”, line 659, in savez_compressed
_savez(file, args, kwds, True)
File “/usr/lib64/python2.7/site-packages/numpy/lib/npyio.py”, line 716, in _savez
pickle_kwargs=pickle_kwargs)
File “/usr/lib64/python2.7/site-packages/numpy/lib/format.py”, line 562, in write_array
version)
File “/usr/lib64/python2.7/site-packages/numpy/lib/format.py”, line 308, in _write_array_header
header = asbytes(_filter_header(header))
File “/usr/lib64/python2.7/site-packages/numpy/lib/format.py”, line 467, in _filter_header
return tokenize.untokenize(tokens)
File “/usr/lib64/python2.7/tokenize.py”, line 262, in untokenize
return ut.untokenize(iterable)
File “/usr/lib64/python2.7/tokenize.py”, line 198, in untokenize
self.add_whitespace(start)
File “/usr/lib64/python2.7/tokenize.py”, line 187, in add_whitespace
assert row <= self.prev_row
AssertionError
make: *** [run] Error 1
建议你检查一下glove文件路径是否配置正确~我这边跑没有问题
另外,这个不是我的代码哈~如果实在搞不定,可以直接去作者的github起一个issue
您好,我最近在学习做NER,刚入门,感觉您的文章写的非常好,通俗易懂。希望您能将把这四个系列的代码和语料都发给我,我复现实验进行学习。我的邮箱是:zhanlaoban@gmail.com。麻烦您了,感激不尽!谢谢!
请参看:https://www.lookfor404.com/命名实体识别的语料和代码/
你好,请问怎样在lstm+crf 模型加入attention?
您好,最近需要做一个命名实体识别的实验,可以把这个系列的代码和语料发我一份吗?408401390@qq.com,好像是这个系列第三篇文章没有代码和语料集。如果方便的话,可以给发一个系列的吗?谢谢您
https://www.lookfor404.com/命名实体识别的语料和代码/
兄台,您这个NER模型的词向量训练语料和模型训练语料是否可以一样?请不吝赐教,谢谢……
一般词向量的话,用外部预训练好的效果更好。用内部语料训练的话,可以解决oov问题,但能学习到的信息有限。
但是特定领域的话没有对应的语料,比如说电子元器件的一些资料,这种就没有找到对应的语料,我目前训练结果很不理想,请教下这种情况应该什么改进呢?
1.没有语料的话,是没办法用hmm或者crf来做模型的,只能结合词典和规则来做。
2.第二个问题不太理解你的意思呢,可以补充说明一下吗?
比如说我有100万个PDF资料,是否可以使用这些资料去训练词向量?
我的想法是用这100万的资料去训练词向量,然后再和GloVe官方训练好的语料进行合并作为最终词向量,不知道这种方式是否可行,这种方式就可以解决oov问题,而且还能扩充词向量。
而针对第二个问题,主要是说有这么个场景,一篇资料里面有可能有文本、图片和表格,而对于表格来说,里面提取出的文本是没有语义的,我是否可以和正常文本一样处理,或者说需要做一些特殊的标记,又或者说我只能单独提出做规则处理?
目前我使用的就是这篇文章的算法来实现,最终的F1 score只有47%,现在没有明确的想法去提升这个结果,望不吝赐教,谢谢!
1.如果是单独拿100万个PDF的语料来训练词向量的话,训练出来的词向量的含义和glove官方预训练模型会有偏差,因为训练语料规模领域以及模型的设置不一样。最好完全按照官方预训练模型一样的参数来训练,再作为补充向量。
2.主要看你最终要预测的文本形态是怎样的,假如你最终要预测的文本都是自然句子,那么表格内的这些语料用来训练模型的意义不大。
还请教个问题,比如说我想标记一篇有表格的文章里面的实体,是否可以使用自然语言的命名实体识别实现,如果能够实现,那么需要做什么特殊处理吗?比如说增加表格的标记等,望不吝赐教,谢谢……
楼主:
首先感谢楼主分享,但是文中一点有点疑惑。
就是在2.1小节,建立字母向量表示也就是代码中的char-embedding。从代码中可以看到,应该是使用随机数初始化一个embedding矩阵,之后使用lookup查字典的方式去寻找这个向量。本质上是利用bilstm来获得这个embedding。这个embedding到底学习到的是字母的embedding么?我觉着不太对。
突然想到ELMO模型中,也是使用word-embedding之后接了bilstm,但是在那篇论文中这个bilstm学习到的是句法和语义信息。这里应该是同理的!但是哪里是将三个embedding依据不同权值做累加,这里直接concat之后作为输入,是不是可以理解为concat之后的embedding包含两个特征,就像机器学习中的不同变量一样。思路有点混乱,小白一枚,说错的地方,海涵。
1.char-embedding确实是随机初始化的,然后concat在对应的word后面,作为整个word的表示,在模型训练的过程中,char-embedding是会随着模型一起微调学习的, 所以会学习到字母的embedding。
2.这个模型和ELMO是不一样的,ELMO的网络结构比较深一点,取3层向量依据不同权重加起来(不是concat),可以得到不同的语义表示。而这个模型的embedding层,正如你所说,可以concat起来可以看作是两个特征,让模型去学习。
您好,我看网上很多代码都是利用glove来训练词向量或者初始化词向量,但是我不想用这种,我们公司自己训练了一个word2vec的词向量,保存的格式是kv,但是我不知道怎么嵌入到bilstm+crf中,请教下
基本上每种框架都有实现embedding层,你把word2vec的词向量load进去就行。具体的搜索一下哈
哇塞这是什么宝藏博主!!收藏了!
你好,请问跑通了吗?
你好!我根据步骤run了一下build_data,显示错误如下:
UnicodeDecodeError: ‘gbk’ codec can’t decode byte 0x93 in position 125: illegal multibyte sequence
我查了一些解决方案,比如加上ucf=108之类的,但是无果。请教下。