NER学习笔记

初入NRE,前期打算是先理解CRF,以及CRF,HMM,贝叶斯,最大熵之间的关系,然后用CRF实现命名实体识别,在结合BRNN+CRF训练,对比效果,之后在看看Attention,主动学习,对抗生成网络等等

use CoNLL 2002 data to build a NER system

这个是python-crfsuite对应的github上的example,我简单的过一遍,以及添加一些疑问,后续有时间解决。
先来看一下训练集,以一个句子为例:
Melbourne ( Australia ) , 25 may ( EFE ) .
其表示形式如下,这里主要是用到了词性的信息:

1
2
3
4
5
6
7
8
9
10
11
[(u'Melbourne', u'NP', u'B-LOC'),
(u'(', u'Fpa', u'O'),
(u'Australia', u'NP', u'B-LOC'),
(u')', u'Fpt', u'O'),
(u',', u'Fc', u'O'),
(u'25', u'Z', u'O'),
(u'may', u'NC', u'O'),
(u'(', u'Fpa', u'O'),
(u'EFE', u'NC', u'B-ORG'),
(u')', u'Fpt', u'O'),
(u'.', u'Fp', u'O')]

然后看一下如何对每一个单词(x)进行找特征的,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def word2features(sent, i):
word = sent[i][0]
postag = sent[i][1]
features = [
'bias',
'word.lower=' + word.lower(),
'word[-3:]=' + word[-3:],
'word[-2:]=' + word[-2:],
'word.isupper=%s' % word.isupper(),
'word.istitle=%s' % word.istitle(),
'word.isdigit=%s' % word.isdigit(),
'postag=' + postag,
'postag[:2]=' + postag[:2],
]
if i > 0:
word1 = sent[i-1][0]
postag1 = sent[i-1][1]
features.extend([
'-1:word.lower=' + word1.lower(),
'-1:word.istitle=%s' % word1.istitle(),
'-1:word.isupper=%s' % word1.isupper(),
'-1:postag=' + postag1,
'-1:postag[:2]=' + postag1[:2],
])
else:
features.append('BOS')
if i < len(sent)-1:
word1 = sent[i+1][0]
postag1 = sent[i+1][1]
features.extend([
'+1:word.lower=' + word1.lower(),
'+1:word.istitle=%s' % word1.istitle(),
'+1:word.isupper=%s' % word1.isupper(),
'+1:postag=' + postag1,
'+1:postag[:2]=' + postag1[:2],
])
else:
features.append('EOS')
return features

这个代码我有一些疑问,这里是对每个字生成特征,每个字一个特征集合,然后将一句话的每个字的特征集合组合成一个list,将这个作为一条输入,那么这些特征到底是如何组合的,等等内部细节不是很理解,有时间研究研究源码。

然后得到输入和输出数据:

1
2
3
4
5
6
7
8
def sent2features(sent):
return [word2features(sent, i) for i in range(len(sent))]
def sent2labels(sent):
return [label for token, postag, label in sent]
X_train = [sent2features(s) for s in train_sents]
y_train = [sent2labels(s) for s in train_sents]

然后就是开始训练

1
2
3
trainer = pycrfsuite.Trainer(verbose=False)
for xseq, yseq in zip(X_train, y_train):
trainer.append(xseq, yseq)

设置参数,这里不是特别懂,可以通过trainer.params()来查看有什么参数

1
2
3
4
5
6
7
8
trainer.set_params({
'c1': 1.0, # coefficient for L1 penalty
'c2': 1e-3, # coefficient for L2 penalty
'max_iterations': 50, # stop earlier
# include transitions that are possible, but not observed
'feature.possible_transitions': True
})

训练

1
trainer.train('conll2002-esp.crfsuite')

通过trainer.logparser.iterations打印具体参数变化情况

预测部分和评估部分参见[5]中给的网址,这里不做复述

后面提到了如何核查学习到的分类器:通过查看转移特征和状态特征的权重,得到最相关的和最不相关的转移特征和状态特征。
转移特征:

1
2
3
4
5
6
7
8
9
10
11
12
from collections import Counter
info = tagger.info()
def print_transitions(trans_features):
for (label_from, label_to), weight in trans_features:
print("%-6s -> %-7s %0.6f" % (label_from, label_to, weight))
print("Top likely transitions:")
print_transitions(Counter(info.transitions).most_common(15))
print("\nTop unlikely transitions:")
print_transitions(Counter(info.transitions).most_common()[-15:])

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
Top likely transitions:
B-ORG -> I-ORG 8.631963
I-ORG -> I-ORG 7.833706
B-PER -> I-PER 6.998706
B-LOC -> I-LOC 6.913675
I-MISC -> I-MISC 6.129735
B-MISC -> I-MISC 5.538291
I-LOC -> I-LOC 4.983567
I-PER -> I-PER 3.748358
B-ORG -> B-LOC 1.727090
B-PER -> B-LOC 1.388267
B-LOC -> B-LOC 1.240278
O -> O 1.197929
O -> B-ORG 1.097062
I-PER -> B-LOC 1.083332
O -> B-MISC 1.046113
Top unlikely transitions:
I-PER -> B-ORG -2.056130
I-LOC -> I-ORG -2.143940
B-ORG -> I-MISC -2.167501
I-PER -> I-ORG -2.369380
B-ORG -> I-PER -2.378110
I-MISC -> I-PER -2.458788
B-LOC -> I-PER -2.516414
I-ORG -> I-MISC -2.571973
I-LOC -> B-PER -2.697791
I-LOC -> I-PER -3.065950
I-ORG -> I-PER -3.364434
O -> I-PER -7.322841
O -> I-MISC -7.648246
O -> I-ORG -8.024126
O -> I-LOC -8.333815

状态特征:

1
2
3
4
5
6
7
8
9
def print_state_features(state_features):
for (attr, label), weight in state_features:
print("%0.6f %-6s %s" % (weight, label, attr))
print("Top positive:")
print_state_features(Counter(info.state_features).most_common(20))
print("\nTop negative:")
print_state_features(Counter(info.state_features).most_common()[-20:])

如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
Top positive:
8.886516 B-ORG word.lower=efe-cantabria
8.743642 B-ORG word.lower=psoe-progresistas
5.769032 B-LOC -1:word.lower=cantabria
5.195429 I-LOC -1:word.lower=calle
5.116821 O word.lower=mayo
4.990871 O -1:word.lower=día
4.910915 I-ORG -1:word.lower=l
4.721572 B-MISC word.lower=diversia
4.676259 B-ORG word.lower=telefónica
4.334354 B-ORG word[-2:]=-e
4.149862 B-ORG word.lower=amena
4.141370 B-ORG word.lower=terra
3.942852 O word.istitle=False
3.926397 B-ORG word.lower=continente
3.924672 B-ORG word.lower=acesa
3.888706 O word.lower=euro
3.856445 B-PER -1:word.lower=según
3.812373 B-MISC word.lower=exteriores
3.807582 I-MISC -1:word.lower=1.9
3.807098 B-MISC word.lower=sanidad
Top negative:
-1.965379 O word.lower=fundación
-1.981541 O -1:word.lower=británica
-2.118347 O word.lower=061
-2.190653 B-PER word[-3:]=nes
-2.226373 B-ORG postag=SP
-2.226373 B-ORG postag[:2]=SP
-2.260972 O word[-3:]=uia
-2.384920 O -1:word.lower=sección
-2.483009 O word[-2:]=s.
-2.535050 I-LOC BOS
-2.583123 O -1:word.lower=sánchez
-2.585756 O postag[:2]=NP
-2.585756 O postag=NP
-2.588899 O word[-2:]=om
-2.738583 O -1:word.lower=carretera
-2.913103 O word.istitle=True
-2.926560 O word[-2:]=nd
-2.946862 I-PER -1:word.lower=san
-2.954094 B-PER -1:word.lower=del
-3.529449 O word.isupper=True

其实我是看到这个后,在想前面使用到的特征提取和这个的联系是怎么样的。如何将前面的特征变成了这里的转移特征和状态特征的。

这里我先搬出统计学习方法中CRF的一些定义
线性链的条件随机场的定义如下:
nlp1011
这里可以看到还是有简化的,比如的概率是只和有关,和全部的有关,条件随机场的解决办法如下,即其参数形式:
nlp1012
nlp1013

也是前面提到的问题,如何将这个公式和前面的特征联系上。。。

Chinese NER use CRF

这个需要安装python-crfsuite,本来很简单的事情,由于我的Mac中安装了Anaconda,导致电脑中有两个python3,结果默认使用pip3安装到了Anaconda的路径下,我pycharm中的是系统的python3,就一直没安装上,发现终端中使用的python3是Anaconda的,最后将~/bash_profile里面的Anaconda的python3路径export ...注释掉,在安装,就好了,(期间不知道为什么pycharm中不能直接安装)
这里和前面讲到的区别就是多加了字的结束与开始标志,然后是基于字符的。
首先我获得到的是如下训练集:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
海 O
钓 O
比 O
赛 O
地 O
点 O
在 O
厦 B-LOC
门 I-LOC
与 O
金 B-LOC
门 I-LOC
之 O
间 O
的 O
海 O
域 O
。 O

然后将其转为如下数据:

1
2
3
4
5
6
7
8
9
海,B,nr,O
钓,E,nr,O
比,B,vn,O
赛,E,vn,O
地,B,n,O
点,E,n,O
在,S,p,O
厦,B,ns,B-LOC
门,E,ns,I-LOC

主要的方法就是将一句话组合起来,然后使用jieba分词,添加词性信息,添加每个字符处于词的什么位置信息(我这里可以理解为这个特征是因为词到字的转变需要)。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def get_cut_and_seg(token):
wordlist = jbpos.cut(get_sentence(token))#这里就是对原来的句子使用jieba进行分词
#[pair('海钓', 'nr'), pair('比赛', 'vn'), pair('地点', 'n'), pair('在', 'p'), pair('厦门', 'ns'), pair('与', 'p'), pair('金门', 'n'), pair('之间', 'f'), pair('的', 'uj'), pair('海域', 'n'), pair('。', 'x')]
index=0
for w in wordlist:
for i in range(len(w.word)):
if len(w.word) == 1:
status = 'S'
elif i == 0:
status = 'B'
elif i == len(w.word) - 1:
status = 'E'
else:
status = 'I'
token[index][1]=status
token[index][2]=w.flag#这个就是词性
index += 1

然后就是添加特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
#[[海,B,nr,O],[钓,E,nr,O],[比,B,vn,O],[赛,E,vn,O]...
#这个代码是核心
def word2features(sent, i):
#print(sent)
word = sent[i][0]#
cuttag = sent[i][1]#
postag = sent[i][2]#
features = [
'bias',
'word='+word,
# 'word.lower=' + word.lower(),
# 'word[-3:]=' + word[-3:],
#'word[-2:]=' + word[-2:],
# 'word.isupper=%s' % word.isupper(),
#'word.istitle=%s' % word.istitle(),
'word.isdigit=%s' % word.isdigit(),
'postag=' + postag,
'cuttag=' + cuttag,
# 'postag[:2]=' + postag[:2],
]
if i > 0:
word1 = sent[i - 1][0]
postag1 = sent[i - 1][2]
cuttag1 = sent[i - 1][1]
features.extend([
'-1:word='+word1,
'-1:postag=' + postag1,
'-1:cuttag=' + cuttag1,
# '-1:postag[:2]=' + postag1[:2],
])
else:
features.append('BOS')
if i < len(sent) - 1:
word1 = sent[i + 1][0]
postag1 = sent[i + 1][2]
cuttag1 = sent[i + 1][1]
features.extend([
'+1:word=' + word1,
'+1:postag=' + postag1,
'+1:cuttag=' + cuttag1,
#'+1:postag[:2]=' + postag1[:2],
])
else:
features.append('EOS')
return features

主要代码和之前的都没有什么变化,这里就不复述了,具体代码参考我后面给出的github代码,会有全部的代码。
这里我给出精确率,召回率,F1 score分数,权重最大/小的15个转移特征和状态特征:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
precision recall f1-score support
B-LOC 0.88 0.86 0.87 3658
I-LOC 0.84 0.85 0.84 4948
B-ORG 0.86 0.73 0.79 2185
I-ORG 0.88 0.79 0.83 8756
B-PER 0.92 0.87 0.90 1864
I-PER 0.92 0.89 0.90 3601
avg / total 0.88 0.83 0.85 25012
Top likely transitions:
B-PER -> I-PER 4.541048
I-LOC -> I-LOC 4.516515
I-PER -> I-PER 4.356901
B-LOC -> I-LOC 4.221610
I-ORG -> I-ORG 4.121786
B-ORG -> I-ORG 3.394398
O -> O 2.263011
B-LOC -> B-LOC 1.059244
O -> B-PER 0.823109
I-LOC -> B-LOC 0.614743
O -> B-ORG 0.374764
O -> B-LOC 0.251798
I-LOC -> O -0.002602
I-PER -> B-PER -0.218017
I-PER -> O -0.267320
Top unlikely transitions:
I-PER -> B-LOC -4.207069
B-ORG -> I-LOC -4.499817
B-PER -> I-LOC -4.532694
I-PER -> I-LOC -4.543816
I-ORG -> I-PER -4.544963
I-LOC -> I-PER -4.718752
B-LOC -> I-PER -5.027208
I-ORG -> I-LOC -5.180083
B-PER -> I-ORG -5.460935
I-PER -> I-ORG -6.058599
B-LOC -> I-ORG -7.229793
I-LOC -> I-ORG -7.297481
O -> I-PER -7.558340
O -> I-LOC -8.567686
O -> I-ORG -9.775946
Top positive:
7.590840 B-PER word=阮
6.730205 O word=氏
6.602604 O word=杯
6.220573 O word=某
5.973532 B-PER word=刘
5.936717 B-LOC -1:word=℃
5.824072 O word=、
5.601934 O -1:word=该
5.373217 B-LOC word=淮
5.172862 O postag=uj
5.141689 B-PER word=李
5.116662 O word=是
4.907784 O word=,
4.815227 B-PER word=傅
4.748813 B-PER word=靳
Top negative:
-3.523168 I-LOC -1:word=洲
-3.621192 O -1:word=邓
-3.801631 O word=鲁
-3.853975 B-LOC +1:word=心
-3.988799 O +1:word=轩
-4.001329 O word=俊
-4.178284 I-ORG -1:word=部
-4.255063 O +1:word=氏
-4.351390 I-LOC word=方
-4.368295 O word=俄
-4.727653 O word=澳
-4.789469 O word=罗
-5.180110 O word=华
-5.929479 I-PER word=老
-7.077680 O word=京

状态特征是X,转移特征是Y,状态特征也只由当前状态的前后和当前的决定。

Ner for Chinese clinical text

这里就没什么要讲的,只是想说明一些上面的一些问题,比如上面对于每个,只用到了前后一个字的信息,这里用到了前后两个的信息,而且这个是基于词的。
给定一些文本,其中有如下标签:

1
2
# htmltag = ['症状和体征', '检查和检验', '治疗', '疾病和诊断', '身体部位']
# englishtag = ['SIGNS', 'CHECK', 'TREATMENT', 'DISEASE', 'BODY']

任务是将这些标记标出来。给出一些标准化的训练集如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
患者 n O
老年 t O
女性 n O
, x O
88 m O
岁 m O
; x O
2. m O
既往 t O
体健 n S-SIGNS
, x O
否认 v B-SIGNS
药物 n I-SIGNS
过敏 nr E-SIGNS

此部分主要看一下特征处理这里:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# 特征定义
def word2features(sent, i):
"""返回特征列表"""
word = sent[i][0]
postag = sent[i][1]
features = [
'bias',
'word=' + word,
'word_tag=' + postag,
]
if i > 0:
features.append('word[-1]=' + sent[i-1][0])
features.append('word[-1]_tag=' + sent[i-1][1])
if i > 1:
features.append('word[-2]=' + sent[i-2][0])
features.append('word[-2, -1]=' + sent[i-2][0] + sent[i-1][0])
features.append('word[-2]_tag=' + sent[i-2][1])
if i < len(sent) - 1:
features.append('word[1]=' + sent[i+1][0])
features.append('word[1]_tag=' + sent[i+1][1])
if i < len(sent) - 2:
features.append('word[2]=' + sent[i+2][0])
features.append('word[1, 2]=' + sent[i+1][0] + sent[i+2][0])
features.append('word[2]_tag=' + sent[i+2][1])
return features

由此,应该大体上理解了CRF中的特征如何使用了,其具体细节以及后续的进展可以参考[7]

A simple BiLSTM-CRF model for Chinese Named Entity Recognition

先给出LTP的NER的标准类型
NE识别模块的标注结果采用O-S-B-I-E标注形式,其含义为

1
2
3
4
5
6
7
8
9
10
11
12
标记 含义
O 这个词不是NE
S 这个词单独构成一个NE
B 这个词为一个NE的开始
I 这个词为一个NE的中间
E 这个词位一个NE的结尾
LTP中的NE 模块识别三种NE,分别如下:
标记 含义
Nh 人名
Ni 机构名
Ns 地名

然后demo中给出了O, B-PER, I-PER, B-LOC, I-LOC, B-ORG, I-ORG,就很好理解了,B-PER表示的是人名的开头,I-PER表示的是人名的中间,后面的依次类推,就是地名,组织名。
然后其模型如下图所示:
nlp1001
第一层look-up layer,目标是将字符表示从one-hot转为字符矩阵,代码中是随机的初始化矩阵,这里可以自己使用语言的知识来训练一个前置矩阵,后面参考的论文中有提到。
第二层BiLSTM layer可以有效的使用过去和未来的输入信息来自动的提取特征
第三层CRF layer为每一个字符打标签,这里不使用Softmax的原因是如果使用Softmax后,可能得到无语法的标记序列,因为softmax只能独立的为每个位置打标签,我们知道I-LOC不能够在B-PER后面,但是Softmax不知道,CRF层可以使用句级标签信息并对两个不同标签的转换行为建模。这句话不是很理解。
这里需要了解一下HMM,CRF的知识,我在后面讲到了
这里我想用HMM和CRF先将命名实体识别实现一遍,见前面。

NB/MaxEnt/HMM/MEMM/CRF

先讲一下判别式和生成式:
判别模型:直接将数据Y(label),根据所提供的X(fratures),学习,最后得到模型(划分一个比较明显的边界来区分label),也就是说判别模型是直接对P(Y|X)进行建模。对所有样本只构建一个模型,确定总体判别边界。
生成模型:先从训练样本数据中将所有的数据的分布情况摸透,最终确定一个分布,来作为我们输入数据的分布,即联合分布P(X,Y)(X包含所有的特征,Y包含所有的label),然后来了新样本数据,通过学习模型的联合分布,再结合新样本给的X,通过条件概率得到Y。生成模型在训练阶段只对P(X,Y)建模,对分类而言,要对每个label建模。

朴素贝叶斯

这里我们的y是一个单变量,而不是一个序列,而X是一个序列。下面详细介绍:
nlp1002
这里如果之间如果不是独立的,其每个取值都有很多种,然后进行组合的可能就会非常庞大,而朴素贝叶斯在此做了一个强假设,之间是相互独立的。
注意其假设是对在出现y的情况下x的一个概率的假设,如下:
nlp1003
我们要求的后验概率通过贝叶斯转换后,就变为如下:
nlp1004
也就是我们需要找出在不同的y下X的概率最大的那个y

Logistic(Softmax) /MaxEnt

这两个是等价的,统计学习方法中也给出了证明,这里摘抄一个博客中讲到的:
nlp1005
即每条输入数据都会被表示为一个n维的向量,可以看成n个特征,而模型中每一类都有n个权重,与n个特征相乘后求和再经过Softmax的结果,代表这条输入数据分到这类的概率。
下面这段话,就是一个便利的转换:
nlp1006
这就是MaxEnt,其和Logistic的区别只是多加了几个特征而已(但是我不知道为什么要画两条线):
nlp1007
说一下这个图,上面的根节点是y标签,下面的叶子节点(灰色)是x
还是不太懂这两者的区别,后面有讲到为什么要画两条线

图表示

先用一张图展示如下:
nlp1008
然后给出MaxEnt的模型:
nlp1009
应该是解释了上面的疑问,也就是说MaxEnt会有多个势函数。
HMM是NB的扩展,CRF是MaxEnt的扩展,从上面的图也能看出来,此时的y变为一个序列Y
nlp1010
从这张图可以看出来(图示的不是特别好),我这里简述一下,其实很简单:对应每一个序列y,我们设定K组特征函数,和对应的权重向量,使用softmax的思想求解出概率最大的序列y即可。

HMM/MEMM/CRF

简单的来说HMM有两个假设,一个是输出观测值之间严格独立,二是状态转移过程中当前状态只与前一状态有关:
nlp1014
注意上面的话,说状态序列()作标记,标注问题是给定观测序列()预测其对应的标记序列(),记住这句话后,再来看网上给出的HMM模型图:
nlp1015
这个图上面的是观测序列,是状态序列,也就是标记。继续看统计学习方法中所举的例子,说给了4个盒子,从这4个盒子中选取一个盒子,然后从这个盒子中选出一个球,记下球的颜色,重复该过程5次,得到观测序列:

1
红,红,白,白,红

预测每个颜色的球最可能来自的盒子号的一个序列(状态序列),也就是说状态序列先一步观测序列。
由于是生成模型,我们用朴素贝叶斯的方法对其进行分析:

从这个公式中看上面这个模型图,箭头的方向也就不会别扭了吧。HMM先到这,再来看MEMM。
MEMM克服了观测之间的严格独立性,给出的图如下:
nlp1016
关于这个克服了观测之间的严格独立性,我的理解是状态由之前的只由决定变为由一起决定。
MEMM是局部归一化,会出现标注偏置的问题。CRF使用全局归一化,解决了标注偏置问题。具体的看下面MEMM和CRF的公式即可:
nlp1017
可以看到MEMM在求解状态序列时,是分每一步来求解的,而每一步的求解过程中都使用了softmax进行归一化,对比CRF,列出所有可能的状态序列,进行全局softmax归一化。那么偏置在何处呢,看下面这个例子:
nlp1018
上图是一个简化的状态转移图,假设过程0->1出现的次数多于过程0->4的次数,但是过程4->5出现的次数远大于过程1-2出现的次数,或者说过程5->3出现的次数远大于过程2->3出现的次数,使用MEMM的话,不管过程1->2,4->5,2->3,5->3它们之间出现的次数如何,归一化的结果都是1,也就是说上述情况下,序列0-1-2-3出现的概率大于序列0-4-5-3出现的概率。但是CRF就不一样了,略。

参考

1.A simple BiLSTM-CRF model for Chinese Named Entity Recognition
2.LTP文档
3.Logistic 最大熵 朴素贝叶斯 HMM MEMM CRF 几个模型的总结
4.ChineseNER
5.Let’s use CoNLL 2002 data to build a NER system
6.Ner for Chinese clinical text
7.Evaluation Tasks at CCKS 2017
8.HMM、MEMM和CRF的学习总结
9.MEMM标注偏置问题

如果觉得有帮助,给我打赏吧!