众所周知,通过Bilstm已经可以实现分词或命名实体标注了,同样地单独的CRF也可以很好的实现。既然LSTM都已经可以预测了,为啥要搞一个LSTM + CRF的混合模型?因为单独LSTM预测出来的标注可能会出现(I-Organization-> I-Person,B-Organization - > I-Person)这样的问题序列。
但这种错误在CRF中是不存在的,因为慢性肾功能衰竭的特征函数的存在就是为了对输入序列观察,学习各种特征,这些特征就是在限定窗口尺寸下的各种词之间的关系。
将CRF接在LSTM网络的输出结果后,让LSTM负责在CRF的特征限定下,依照新的loss function,学习出新的模型。
假定我们使用Bakeoff-3评测中所采用的的BIO标注集,即B-PER,I-PER代表人名首字,人名非首字,B-ORG,I-ORG代表组织机构名首字,组织机构名非首字,O-代表该字不属于命名实体的一部分
为直观的看到加入后的区别我们可以借用网络中的图来表示:其中XX表示输入的句子,包含5个字分别用w1w1,w2w2,w3w3,w4w4,w5w5表示
没有CRF层的网络示意图
含有CRF层的网络输出示意图
上图可以看到在没有CRF层的情况下出现了B-Person-> I-Person的序列,而在有CRF层层的网络中,我们将LSTM的输出再次送入CRF层中计算新的结果。而在CRF层中会加入一些限制,以排除可能会出现上文所提及的不合法的情况
CRF loss function 如下:
Loss Function = PRealPathP1+P2+…+PNPRealPathP1+P2+…+PN
主要包括两个部分Real path score 和 total path scroe
PRealPathPRealPath =eSieSi
因此重点在于求出:
SiSi = EmissionScore + TransitionScore
EmissionScore=x0,START+x1,B−Person+x2,I−Person+x3,O+x4,B−Organization+x5,O+x6,ENDx0,START+x1,B−Person+x2,I−Person+x3,O+x4,B−Organization+x5,O+x6,END
因此根据转移概率和发射概率很容易求出PRealPathPRealPath
total scroe的计算相对比较复杂,可参看https://createmomo.github.io/2017/11/11/CRF-Layer-on-the-Top-of-BiLSTM-5/
使用2.1.4版本的keras,在keras版本里面已经包含bilstm模型,但crf的loss function还没有,不过可以从keras contribute中获得,具体可参看:https://github.com/keras-team/keras-contrib
构建网络模型代码如下:
model = Sequential()
model.add(Embedding(len(vocab), EMBED_DIM, mask_zero=True)) # Random embedding
model.add(Bidirectional(LSTM(BiRNN_UNITS // 2, return_sequences=True)))
crf = CRF(len(chunk_tags), sparse_target=True)
model.add(crf)
model.summary()
model.compile('adam', loss=crf.loss_function, metrics=[crf.accuracy])
清晰数据是最麻烦的一步,首先我们采用网上开源的语料库作为训练和测试数据。语料库中已经做好了标记,其格式如下:
月ø
油ö
印ö
的ö
“O-
北B-LOC
京I-LOC
文ö
物ö
保ö
存ö
保ö
管ö
语料库中对每一个字分别进行标记,比较包括如下几种:
'O', 'B-PER', 'I-PER', 'B-LOC', 'I-LOC', "B-ORG", "I-ORG"
分别表示,其他,人名第一个,人名非第一个,位置第一个,位置非第一个,组织第一个,非组织第一个
train = _parse_data(open('data/train_data.data', 'rb'))
test = _parse_data(open('data/test_data.data', 'rb'))
word_counts = Counter(row[0].lower() for sample in train for row in sample)
vocab = [w for w, f in iter(word_counts.items()) if f >= 2]
chunk_tags = ['O', 'B-PER', 'I-PER', 'B-LOC', 'I-LOC', "B-ORG", "I-ORG"]
# save initial config data
with open('model/config.pkl', 'wb') as outp:
pickle.dump((vocab, chunk_tags), outp)
train = _process_data(train, vocab, chunk_tags)
test = _process_data(test, vocab, chunk_tags)
return train, test, (vocab, chunk_tags)
在处理好数据后可以训练数据,本文中将批量大小= 16获得较为高的精度(99%左右),进行了10个历元的训练。
import bilsm_crf_model
EPOCHS = 10
model, (train_x, train_y), (test_x, test_y) = bilsm_crf_model.create_model()
# train model
model.fit(train_x, train_y,batch_size=16,epochs=EPOCHS, validation_data=[test_x, test_y])
model.save('model/crf.h5')
import bilsm_crf_model
import process_data
import numpy as np
model, (vocab, chunk_tags) = bilsm_crf_model.create_model(train=False)
predict_text = '中华人民共和国国务院总理周恩来在外交部长陈毅的陪同下,连续访问了埃塞俄比亚等非洲10国以及阿尔巴尼亚'
str, length = process_data.process_data(predict_text, vocab)
model.load_weights('model/crf.h5')
raw = model.predict(str)[0][-length:]
result = [np.argmax(row) for row in raw]
result_tags = [chunk_tags[i] for i in result]
per, loc, org = '', '', ''
for s, t in zip(predict_text, result_tags):
if t in ('B-PER', 'I-PER'):
per += ' ' + s if (t == 'B-PER') else s
if t in ('B-ORG', 'I-ORG'):
org += ' ' + s if (t == 'B-ORG') else s
if t in ('B-LOC', 'I-LOC'):
loc += ' ' + s if (t == 'B-LOC') else s
print(['person:' + per, 'location:' + loc, 'organzation:' + org])
输出结果如下:
['person: 周恩来 陈毅, 王东', 'location: 埃塞俄比亚 非洲 阿尔巴尼亚', 'organzation: 中华人民共和国国务院 外交部']
源码地址:https ://github.com/stephen-v/zh-NER-keras