用双向lstm+CRF做命名实体识别(附tensorflow代码)——NER系列(四)

这一篇文章,主要讲一下用深度学习(神经网络)的方法来做命名实体识别。现在最主流最有效的方法基本上就是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。我们将模型的讲解分为以下三个部分:

  1. 词向量表示
  2. 词的上下文信息表示
  3. 解码

2.1 词向量表示

对于每一个单词,我们用词向量w \in \mathbb{R}^n来表示,用来捕获词本身的信息。这个词向量由两部分concat起来,一部分是用GloVe训练出来的词向量w_{glove} \in \mathbb{R}^{d_1},另一部分,是字符级别的向量w_{chars} \in \mathbb{R}^{d_2}

在以往,我们会手工提取并表示一些特征,比如用1,0来表示某个单词是否是大写开头,而在这个模型里面,我们不需要人工提取特征,只需要字符级别上面使用双向LSTM,就可以提取到一些拼写层面的特征了。当然了,CNN或者其他的RNN也可以干类似的事情。

Word level representation from characters embeddings

对于每一个单词w = [c_1, \ldots, c_p]里面的每一个字母(区分大小写),我们用c_i \in \mathbb{R}^{d_3}这个向量来表示,对字母级别的embedding跑一个bi-LSTM,然后将最后的隐状态输出拼接起来(因为是双向,所以有两个最后隐状态,如上图),得到一个固定长度的表达w_{chars} \in \mathbb{R}^{d_2},直觉上,我们可以认为这个向量提取了字母级别的特征,比如大小写、拼写规律等等。然后,我们将这个向量w_{chars}和Glove训练好的w_{glove}拼接起来,得到某个词最终的词向量表达:w = [w_{glove}, w_{chars}] \in \mathbb{R}^n,其中n = d_1 + d_2

看一下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 词的上下文信息表示

当有了词向量w之后,就可以对一个句子里的每一个词跑LSTM或者双向LSTM了,然后得到另一个向量表示:h \in \mathbb{R}^k,如下图:
利用双向LSTM提取上下文信息

对应的tensorflow代码很直观,这次我们用每一个隐藏层的输出,而不是最后一个单元的输出。因此,我们输入一个句子,有m个单词:w_1, \ldots, w_m \in \mathbb{R}^n,得到m个输出:h_1, \ldots, h_m \in \mathbb{R}^k。现在的输出,是包含上下文信息的:

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,那么我们可以得到权重矩阵W \in \mathbb{R}^{9 \times k}和偏置矩阵b \in \mathbb{R}^9,最后计算某个词的得分向量s \in \mathbb{R}^9 = W \cdot h + b,s[i]可以解释为,某个词标记成第i个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的影响的。定义一系列词w_1, \ldots, w_m,一系列的得分向量s_1, \ldots, s_m,还有一系列标签y_1, \ldots, y_m,线性CRF的计算公式是这样的:

    \[ \begin{aligned} C(y_1, \ldots, y_m) &= b[y_1] &+ \sum_{t=1}^{m} s_t [y_t] &+ \sum_{t=1}^{m-1} T[y_{t}, y_{t+1}] &+ e[y_m]\\ &= \text{begin} &+ \text{scores} &+ \text{transitions} &+ \text{end} \end{aligned} \]

在上面的式子里,T是转移矩阵,尺寸为\mathbb{R}^{9 \times 9},用来刻画相邻tag的依赖、转移关系;e, b \in \mathbb{R}^9是结束、开始tag的代价向量。下面是一个计算例子:转移得分计算例子

了解了CRF得分式子,接下来要做两件事:

  • 找到得分最高的tag序列。
  • 计算句子的tag概率分布。

“仔细想想,计算量是不是太大了?”

没错,计算量相当大。就上面的例子而言,有9种tag,一个句子有m个单词,一共有9^m种可能,代价太大了。

幸运的是,由于式子有递归的特性,所以我们可以用动态规划的思想来解决这个问题。假设\tilde{s}_{t+1} (y^{t+1})是时间步t+1, \ldots, m的解(每个时间步都是有9种可能的),那么,继续往前推,时间步t, \ldots, m的解,可以由下式表示:

    \[ \begin{aligned} \tilde{s}_t(y_t) &= \operatorname{argmax}_{y_t, \ldots, y_m} C(y_t, \ldots, y_m)\\ &= \operatorname{argmax}_{y_{t+1}} s_t [y_t] + T[y_{t}, y_{t+1}] + \tilde{s}_{t+1}(y^{t+1}) \end{aligned} \]

每一个递归步骤的复杂度为O(9 \times 9),由于我们进行了m步,所以总的复杂度是O(9 \times 9 \times m)

最后,我们需要在CRF层应用softmax,将得分概率分布计算出来。我们得计算出所有的可能,如下式子:

    \[ \begin{aligned} Z = \sum_{y_1, \ldots, y_m} e^{C(y_1, \ldots, y_m)} \end{aligned} \]

上面提到的递归思想在这里也可以应用。先定义Z_t(y_t),表示从时间步t开始、以y_t为tag开始的序列,计算公式如下:

    \[ \begin{aligned} Z_t(y_t) &= \sum_{y_{t+1}} e^{s_t[y_t] + T[y_{t}, y_{t+1}]} \sum_{y_{t+2}, \ldots, y_m} e^{C(y_{t+1}, \ldots, y_m)} \\ &= \sum_{y_{t+1}} e^{s_t[y_t] + T[y_{t}, y_{t+1}]} \ Z_{t+1}(y_{t+1})\\ \log Z_t(y_t) &= \log \sum_{y_{t+1}} e^{s_t [y_t] + T[y_{t}, y_{t+1}] + \log Z_{t+1}(y_{t+1})} \end{aligned} \]

最后,序列概率计算式子如下:

    \[ \begin{aligned} \mathbb{P}(y_1, \ldots, y_m) = \frac{e^{C(y_1, \ldots, y_m)}}{Z} \end{aligned} \]

2.4 训练

最后,就是训练部分了。训练的损失函数采用的是cross-entropy(交叉熵),计算公式如下:

    \[ \begin{aligned} - \log (\mathbb{P}(\tilde{y})) \end{aligned} \]

其中,\tilde{y}为正确的标注序列,它的概率\mathbb{P}计算公式如下:

  • CRF:\mathbb{P}(\tilde{y}) = \frac{e^{C(\tilde{y})}}{Z}
  • local softmax:\mathbb{P}(\tilde{y}) = \prod p_t[\tilde{y}^t]

“额..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

值得学习一下。

另外,用多模态来做实体识别也是一个方向,特别是对于一些类似微博的语料(有图片),这样做效果更佳。

用CRF做命名实体识别——NER系列(三)

在上一篇文章《用隐马尔可夫模型(HMM)做命名实体识别——NER系列(二)》中,我们使用HMM模型来做命名实体识别,将问题转化为统计概率问题,进行求解。显然,它的效果是非常有限的。

在深度学习技术火起来之前,主流的、最有效的方法,就是CRF(条件随机场)模型。本文不对CRF模型进行展开讲解,而是结合我之前参加的CCF BDCI的其中一个赛题,直接用CRF++工具进行实战。下面直接进入正题。

1.赛题解读

赛题介绍:http://www.datafountain.cn/#/competitions/269/intro

任务描述

总结一下,这个题目要求我们对数据集中的每条记录,提取出正文中的主要机构实体,判断这些机构的风险等级(中性,正向或负向),并为每个实体生成相应的摘要和关键词。

我接下来主要讲提取实体这一部分,用的是CRF模型,训练直接使用CRF++工具(http://taku910.github.io/crfpp/)(似乎被墙了?)。

2.算法流程图

算法流程图

3.算法说明

3.1 定义实体标注集

为了确保最后的机构实体识别准确度,使用BMEWO标注集,各个标注的意义如下:

B:实体的开头

M:实体的中间部分

E:实体的结束

W:单独成实体

O:句子的其它成分

比如下面这个句子(已做分词处理):

山西      相立      山泉      饮品      开发      有限公司     生产      的桶装  饮用水  检出      铜绿      假   单胞菌

背后的标注为:

山西/B  相立/M 山泉/M 饮品/M 开发/M 有限公司/E  生产/O  的/O 桶装/O     饮用水/O     检出/O  铜绿/O   假/O 单胞菌/O

3.2训练文本、测试文本预处理

对训练文本进行中文分词、去除停用词的处理,并根据上述的标注集进行标注。同时,除了词本身,还引入了4个特征:

特征:【词性】,用jieba分词识别出来的词性

特征②:【是否是特征词】,该词是特征词,标记1;不是特征词,标记0。这里的特征词是指“实体通常的结尾词”,比如“有限公司”,“药监局”,“超市”等等,这些特征词来源于两个地方:

  1. 从训练集中分词得到。
  2. 从开源中文分词工具hanlp的机构名词典中整理得到。

特征③:【是否是地点】,该词是地点,标记为isloc;该词不是地点,标记为notloc。这里的地点信息我们是从jieba的分词词性标注功能中得到的,词性标注为ns的一般是地点。

特征④:【是否是句子结束】,该词是这个句子的结束词,标记为isend;否则标记为notend。

训练文本在经过预处理之后,格式如下:

宁夏      ns   0     isloc      notend B

物美      nz   0     notloc  notend M

超市      v     1     notloc  notend M

有限公司    n     1     notloc  notend M

森林公园    n     0     notloc  notend M

   n     1     notloc  isend    E

其中,第一列为词本身,第二列为特征①,第三列为特征②,第四列为特征③,第五列为特征④,第六列列为正确标注。

测试文本的预处理和上面的基本一样,区别在于,测试文本没有正确的实体标注,所以测试文本的预处理文件只有五列。最后我们要用CRF模型预测的是第六列标注。

3.3训练CRF模型

CRF模型的训练,需要一个特征模板,以便能够自动在训练文本中提取特征函数,特征模板的定义直接决定了最后的识别效果

针对此次的机构实体,我们定义了几种特征模板,最终选择了以下模板:

# Unigram

U01:%x[-2,0]

U02:%x[-1,0]

U03:%x[0,0]

U04:%x[1,0]

U05:%x[2,0]

U06:%x[-2,0]/%x[-1,0]/%x[0,0]

U07:%x[-1,0]/%x[0,0]/%x[1,0]

U08:%x[0,0]/%x[1,0]/%x[2,0]

U09:%x[-1,0]/%x[0,0]

U10:%x[0,0]/%x[1,0]

U11:%x[-1,1]/%x[0,1]/%x[1,1]

U12:%x[-1,1]/%x[0,1]

U13:%x[0,1]/%x[1,1]

U14:%x[0,1]/%x[0,2]

U15:%x[-1,1]/%x[0,2]

U16:%x[-2,1]/%x[-1,1]/%x[0,2]

U17:%x[-1,1]/%x[0,2]/%x[1,1]

U18:%x[0,2]/%x[1,1]/%x[2,1]

U19:%x[0,0]/%x[0,2]

U20:%x[-1,0]/%x[0,2]

U21:%x[-2,0]/%x[-1,0]/%x[0,2]

U22:%x[-1,0]/%x[0,2]/%x[1,0]

U23:%x[0,2]/%x[1,0]/%x[2,0]

# Bigram

B

下面解释一下上述特征模板:

①Unigram类型

每一行%x[#,#]生成一个CRFs中的点(state)函数: f(s, o), 其中s为t时刻的标签(output),o为t时刻的上下文以及特征信息。

比如:U06:%x[-2,0]/%x[-1,0]/%x[0,0]

U06是指这个特征模板的编号,对于%x[-2,0]而言,%x是指找到的字符;[-2,0]是定位信息,其中中括号里面的-2是指当前词的前两个词,0是指第0列。后面用/连接的是多个特征的组合。

对于以下的训练文本:

宁夏     ns   0     isloc      notend B

物美     nz   0     notloc  notend M

超市     v     1     notloc  notend M

有限公司   n     1     notloc  notend M

森林公园   n     0     notloc  notend M

  n     1     notloc  isend    E

假如当前识别到第三行,则U06:%x[-2,0]/%x[-1,0]/%x[0,0]对应识别出来的文本为宁夏/物美/超市。

这就相当于我们在文本中找到的一条特征。

②Bigram类型

每一行%x[#,#]生成一个CRFs中的边(Edge)函数:f(s’, s, o), 其中s’为t – 1时刻的标签.也就是说,Bigram类型与Unigram大致机同,只是还要考虑到t – 1时刻的标签.这里只写一个B,默认生成f(s’, s).

有了特征模板以及训练文本文件,就可以进行CRF模型训练了,我们采用了CRF++这个开源工具包进行训练,使用默认参数,最终模型识别出来的特征有11616755条。

3.4预测、生成实体

有了上述预处理测试文本和训练生成的CRF模型,我们可以进行测试文本的标签预测,生成crf_test_output.txt

由于crf_test_output.txt里面预测的是每个词背后的标注,我们还要做一个后处理工作才能把真正的实体提取出来。

用正则表达式B+M*E+或者W匹配文本,然后将其背后的文字提取出来,就是识别出来的机构实体。

3.4效果和缺点

在使用CRF模型之后,我们得到了不错的效果。线下训练文本的实体召回率可以达到91.3%,另外,识别出来的无效实体也少了很多。

和基于规则的实体识别相比,它有着以下优点:

  • 通过特征模板,能够最大限度的挖掘文本的特征,而不需要人工提取。
  • 能够考虑大量的上下文信息、特征。
  • 考虑了相邻词的标注信息,这是传统的最大熵算法所不具备的。
  • 和神经网络模型相比,CRF模型的可解释性强,具体到每一个特征都有可以解释的意义。因此调整起来比较容易。

当然,这个模型也不是完美的,比如,我们训练的这个模型就比较“看重”机构特征词。举个例子,如果“下属公司”单独出现,则它也可能会被识别为机构名,需要我们人工定义一些规则将其去除。


CRF++工具的使用就没有介绍了,训练的过程只需要预处理语料以及模板文件,预处理语料格式和模板文件,在上文已经体现出来了,感兴趣的朋友,缺少语料或者工具,可以找我要。

用隐马尔可夫模型(HMM)做命名实体识别——NER系列(二)

上一篇文章里《用规则做命名实体识别——NER系列(一)》,介绍了最简单的做命名实体识别的方法–规则。这一篇,我们循序渐进,继续介绍下一个模型——隐马尔可夫模型。

隐马尔可夫模型,看上去,和序列标注问题是天然适配的,所以自然而然的,早期很多做命名实体识别和词性标注的算法,都采用了这个模型。

这篇文章我将基于码农场的这篇文章《层叠HMM-Viterbi角色标注模型下的机构名识别》,来做解读。但原文中的这个算法实现是融入在HanLP里面的。不过他也有相应的训练词典,所以我在这篇文章里面也给出一个python实现,做一个简单的单层HMM模型,来识别机构名。

代码地址:https://github.com/lipengfei-558/hmm_ner_organization

1.隐马尔可夫模型(HMM)

隐马尔可夫模型(Hidden Markov Model,HMM),是一个统计模型。

关于这个模型,这里有一系列很好的介绍文章:http://www.52nlp.cn/category/hidden-markov-model

隐马尔可夫模型有三种应用场景,我们做命名实体识别只用到其中的一种——求观察序列的背后最可能的标注序列

即根据输入的一系列单词,去生成其背后的标注,从而得到实体。

2.在序列标注中应用隐马尔可夫模型

HMM中,有5个基本元素:{N,M,A,B,π},我结合序列标志任务对这5个基本元素做一个介绍:

  • N:状态的有限集合。在这里,是指每一个词语背后的标注。
  • M:观察值的有限集合。在这里,是指每一个词语本身。
  • A:状态转移概率矩阵。在这里,是指某一个标注转移到下一个标注的概率。
  • B:观测概率矩阵,也就是发射概率矩阵。在这里,是指在某个标注下,生成某个词的概率。
  • π:初始概率矩阵。在这里,是指每一个标注的初始化概率。

而以上的这些元素,都是可以从训练语料集中统计出来的。最后,我们根据这些统计值,应用维特比(viterbi)算法,就可以算出词语序列背后的标注序列了。

命名实体识别本质上就是序列标注,只需要自己定义好对应的标签以及模式串,就可以从标注序列中提取出实体块了。

3.实战:用HMM实现中文地名识别

3.1 参考论文以及网站

  • 张华平, 刘群. 基于角色标注的中国人名自动识别研究[J]. 计算机学报, 2004, 27(1):85-91.
  • 俞鸿魁, 张华平, 刘群. 基于角色标注的中文机构名识别[C]// Advances in Computation of Oriental Languages–Proceedings of the, International Conference on Computer Processing of Oriental Languages. 2003.
  • 俞鸿魁, 张华平, 刘群,等. 基于层叠隐马尔可夫模型的中文命名实体识别[J]. 通信学报, 2006, 27(2):87-94.
  • 码农场:层叠HMM-Viterbi角色标注模型下的机构名识别

3.2 任务

命名实体识别之中文机构名的识别。

3.3 语料

HanLP(https://github.com/hankcs/HanLP/releases)提供的语料:

我用的是data-for-1.3.3.zip,百度网盘下载地址:

https://pan.baidu.com/s/1o8Rri0y

下载后解压,我们要用的语料路径如下:

\data-for-1.3.3\data\dictionary\organization

其中,里面有两个我们要用到的语料文件,nt.txtnt.tr.txt。这两个文件的数据统计自人民日报语料库。

① nt.txt:

词语标注统计词典,比如里面有一行是这样的:

会议 B 163 C 107 A 10

意思是,会议这个词作为B标签出现了163次,作为C标签出现了107次,作为A标签出现了10次.

② nt.tr.txt:

标签转移矩阵。如下图:nt.tr.txt文件

即,每一个标签转移到另一个标签的次数。比如第二行第四列的19945,代表着【A标签后面接着是C标签】出现了19945次。

以上语料我都提取出来放到代码目录的./data下了。

3.4 代码实现

代码的思路很直观,只要按照上面第2部分所说的,准备好5元组数据,然后用viterbi算法解码即可。

3.4.1 N:状态的有限集合

在机构名识别的这个任务中,论文《基于角色标注的中文机构名识别》把状态(角色)定义为以下集合:原论文状态角色表

然而在HanLP的语料中,只有以下的标签,有多出来的,又不一样的:

A,B,C,D,F,G,I,J,K,L,M,P,S,W,X,Z

经过我的整理,完整的状态(角色)集合如下:

角色 意义 例子
A 上文 参与亚太经合组织的活动
B 下文 中央电视台报道
X 连接词 北京电视台天津电视台
C 特征词的一般性前缀 北京电影学院
F 特征词的人名前缀 何镜堂纪念馆
G 特征词的地名性前缀 交通银行北京分行
K 特征词的机构名、品牌名前缀 中共中央顾问委员会

美国摩托罗拉公司

I 特征词的特殊性前缀 中央电视台

中海油集团

J 特征词的简称性前缀 政府
D 机构名的特征词 国务院侨务办公室
Z 非机构成分
L 方位词 上游

M 数量词 36
P 数量+单位(名词) 三维

两国

W 特殊符号,如括号,中括号 ()

【】

S 开始标志 始##始

本程序以上面我整理的这个表格的状态角色为准(因为HanLP的语料词典里面就是这样定义的)。

3.4.2 M:观察值的有限集合

在这里,观察值就是我们看到的每个词。

不过有一个地方要注意一下,在语料词典nt.txt中,除了所有词语之外,还有下面8个特殊词语:

  • 始##始
  • 末##末
  • 未##串
  • 未##人
  • 未##团
  • 未##地
  • 未##数
  • 未##时

这些词语可以在层叠HMM中发挥作用,加进去可以提高识别精度,因为很多机构名里面都有人名和地名。

在使用我的这份代码之前,你可以用分词工具先识别出相关的词性,然后将对应命中的词语替换为上面的8个特殊词语,再调用函数,精确率会大大提高。

3.4.3 A:状态转移概率矩阵

在这里,它是指某一个标注转移到下一个标注的概率。

generate_data.pygenerate_transition_probability()函数就是干这事的,它会生成一个transition_probability.txt,即转移概率矩阵。

3.4.4 B:观测概率矩阵(发射概率矩阵)

在这里,他是指在某个标注下,生成某个词的概率。

generate_data.pygenerate_emit_probability()函数就是干这事的,它会生成一个emit_probability.txt,即观测概率矩阵(发射概率矩阵)。

3.4.5 π:初始概率矩阵

在这里,它是指每一个标注的初始化概率。

generate_data.pygenertate_initial_vector()函数就是干这事的,它会生成一个initial_vector.txt,即观测概率矩阵(发射概率矩阵)。

3.4.6 维特比(viterbi)算法解码

这部分代码是参考《统计方法》里面的实现写的,做了些调整,使之可以适用于这个机构名识别的任务。函数为viterbi() ,位于OrgRecognize.py里面。

使用这个函数,就能获得最佳标注序列。

3.4.7 匹配标注序列,得到机构名

在3.4.6里面,我们可以得到一个标注序列,哪些标注代表着实体呢?

HanLP作者整理了一个nt.pattern.txt(我也放置在./data/nt.pattern.txt下了),里面是所有可能是机构名的序列模式串(有点粗暴,哈哈),然后用Aho-Corasick算法来进行匹配。

为了简单起见突出重点,我的代码实现里,用的是循环遍历匹配,具体的实现在OrgRecognize.py里面的get_organization,函数的作用是,输入原词语序列、识别出来的标注序列和序列模式串,输出识别出来的机构名实体。

3.4.8 使用程序

代码地址:https://github.com/lipengfei-558/hmm_ner_organization

环境以及依赖:

  • python2.7
  • jieba分词(可选)

首先,运行以下脚本,生成transition_probability.txtemit_probability.txt以及initial_vector.txt

python generate_data.py

然后,运行

python OrgRecognize.py

就可以了,不出意外,“中海油集团在哪里”这句话,会识别出“中海油集团”这个机构实体。

具体输入的句子逻辑,可以在main函数里面灵活修改,也可以结合jieba一起用。另外,python2.7的中文编码问题要注意了,如果你的输出序列很奇怪,很有可能是编码问题。

4.总结、待改进

用HMM来实现的命名实体识别算法,关键在于标签的自定义,你需要人工定义尽可能多的标签,然后在训练语料集里面自动标注这些标签,这也是最麻烦的地方。标注完语料集,生成HMM中的转移概率、初始概率、发射概率就很简单了,就是纯粹的统计。

整个模型也没什么参数,用这些统计的数字即可计算。

算法可能可以改进的点如下:

  1. 针对命名实体的维特比(viterbi)算法中,如果遇到未登录词,默认发射概率为0。我们可以额外引入相似度机制来解决这个问题,比如利用同义词表或者词向量相似度,我们找到和未登录词相似、同时也在观测概率矩阵里面出现的词语,用这个词语的发射概率(或者对其乘一个缩放系数),来代替未登录词的发射概率。
  2. 初始化概率对最终效果的影响有待考证。因为初始化概率影响着单词序列第一个词的标注,假如,仅仅用发射概率来决定第一个词的标注,效果会不会更好?

HMM算法默认只考虑前一个状态(词)的影响,忽略了更多上下文信息(特征)。后来的MEMM、CRF,都是循序渐进的改进方法。传统机器学习方法里面,CRF是主流,下一篇我会继续介绍CRF在命名实体识别任务上的应用。

用规则做命名实体识别——NER系列(一)

兑现自己上一篇立下的flag,从头开始写这几个月对命名实体识别这个任务的探索历程。这是这个系列的第一篇——用规则来做命名实体识别。

1.什么是命名实体识别

命名实体识别(Named Entity Recognition,简称NER),是一个基本的NLP任务,按照传统,下面是百度百科对它的解释:

 一般来说,命名实体识别的任务就是识别出待处理文本中三大类(实体类、时间类和数字类)、七小类(人名、机构名、地名、时间、日期、货币和百分比)命名实体。

当然了,你可以自己定义这个任务,比如识别商品名,也是这个任务范畴。

说得更泛一点,命名实体识别本质上是序列标注问题。

2.为什么要做命名实体识别

第一次接触这个任务的时候,我也问过这个问题,甚至还觉得我直接搞一个数据库,把所有实体都存进去,然后直接匹配不就好了。

事实证明我太天真了。因为,命名实体识别存在以下几个问题:

  1. 实体数量巨大,而且在不断增长。举个例子,每年那么多新的机构、公司,要收集起来也是一个头条的问题。
  2. 实体组合多。很多实体都是组合词,包括中英文组合也很多。
  3. 歧义多。比如拿我的名字举个例,“李鹏飞起来了”,是不是可以识别出两个人名。/坏笑

等等等等,反正,这个任务是有意义的。

3.用规则来做命名实体识别

好了,进入今天的主题了。

先说一句,命名实体识别有非常多的方法,其中基于规则是最古老、最笨的方法,但是为了熟悉这个任务,以及认识到这个方法有多麻烦,我还是进行了探索。

2017CCF BDCI(CCF 大数据与计算智能大赛)有个赛题:《基于机构实体的智能摘要和风险等级识别》,里面有个子任务就是识别机构实体。

我先用规则来做,因为语料集里面大多数的实体,都是有规律的,比如:命名实体数据

上面命名实体都用颜色标注出来了,可以看到,基本上的组成机构是这样的:

若干地名+若干其他成分+若干特征词

其中,特征词指的是诸如公司有限公司管理局这些词语。

那么这个基于规则的实体识别算法就呼之欲出了。步骤如下:

1.分词

我用的是jieba分词:https://github.com/fxsjy/jieba

将原始文本进行分词,同时使用jieba的词性标注功能。(这个步骤也可以去停用词,依你的语料集而定)

2.收集特征词词典

将训练文本中的实体取出来,对于每一个实体,进行分词,取最后一个词,存入词典,最后做去重。

如果想把词典做的更细致,可以借助外部词典,比如hanlp的词典,具体他的词典构造,我将在之后写hmm模型的时候作介绍。

3.标注序列

对文本分词后的词语,进行标注。

①如果这个词语的词性为ns(地名),则将这个词标记为S。(如果不想用jieba的词性标注,这里也可以用地名词典来识别,需自己整理)

②如果这个词语在【步骤2】的特征词词典里,则将这个词标记为E。

③其它情况,标记为O。

这样,我们就把输入的句子转化为了标注序列。比如:

我 来到 北京 天安门

就会被标记为:

OOSE

4.正则匹配,识别实体

我们上面说到,对于这个语料集,我们发现实体的组成规律是这样的:

若干地名+若干其他成分+若干特征词

那么,我们只需要对标注后的序列做一个正则表达式的匹配即可。模式为:

S+O*E+

上面表达式的意思是,必须以1个以上地名开头,以1个以上特征词结尾,中间成分,数量就无所谓了。

匹配出符合要求的字符,将其背后的中文组合起来,就是我们要的实体。

你可以任意定义标注和模式,来适应你的规则。


方法很笨很暴力。

但是你不得不说,如果有的任务很简单,很有规律,用规则来提取实体是个不错的选择,因为它不费时,不费力,能解决问题。