前文传送门:
从零开始学自然语言处理(27)—— 开辟新纪元的Transformer
俗话说的好,NER该怎么做?双向LSTM+CRF啊。这LSTM听说比一般的RNN牛逼,那这双向的LSTM岂不是更牛逼了?(BERT心想:渣渣)
不管是实际业务需要,还是参加一些NLP比赛,亦或是参加面试,都有可能用到NER,那双向LSTM+CRF简直就是NER界的标配。今天我们就手把手教大家如何用Keras优雅的搭建Bi-LSTM+CRF模型。
我们假设大家已经有了可供训练的数据(文末提供数据)。其数据格式应该为这样:
这里我们以字为单元进行标注。“O”是other的缩写,表示非实体词。“B”表示“开头”,即实体词的开头字符,“I”表示“中间”,实体词的中间字符。我们的数据样例里面是使用的是“BIO”标签,没有“E”,“E”表示的是实体词的结尾。但是在绝大多数情况下,没有“E”问题也不大,毕竟很难找出混淆实体的例子。
关于命名实体识别(NER)任务和Bi-LSTM还有CRF,我们在之前的文章中写过:
从零开始学自然语言处理(六)—— 命名实体识别
从零开始学自然语言处理(22)—— 效果震撼的Bi-LSTM
从零开始学自然语言处理(23)—— 巧妙的条件随机场(CRF)(上)
从零开始学自然语言处理(24)—— 巧妙的条件随机场(CRF)(下)
我们引入我们需要的python包:
from keras.models import Model
from keras.layers import Embedding, Bidirectional, LSTM, Dense, Dropout, Input
from keras_contrib.layers import CRF
from keras.preprocessing.sequence import pad_sequences
from keras.utils import np_utils
keras_contrib.layers.CRF
是某个大神封装在keras的CRF包,keras可以直接调用。
构建模型。
class bi_lstm_crf():
def __init__(self,
vocab_size,
n_class,
embedding_dim=128,
rnn_units=128,
drop_rate=0.3,
):
self.vocab_size = vocab_size #词汇量
self.n_class = n_class #NER label种类数
self.embedding_dim = embedding_dim #embedding层维度
self.rnn_units = rnn_units #lstm单元维度
self.drop_rate = drop_rate
def creat_model(self):
inputs = Input(shape=(None,))
embedding = Embedding(input_dim=self.vocab_size, output_dim=self.embedding_dim)(inputs)
x = Bidirectional(LSTM(units=self.rnn_units, return_sequences=True))(embedding) #双向lstm对输入进行编码
x = Dropout(self.drop_rate)(x)
x = Dense(self.n_class)(x)
crf = CRF(self.n_class, sparse_target=False)
x = crf(x)
model = Model(inputs=inputs, outputs=x)
model.compile('adam',
loss=crf.loss_function,
metrics=[crf.accuracy])
return model
是不是很简单,模型的代码量其实只有不到10行。首先将文本进行id化,然后输入到embedding层取出对应的词向量,然后经过双向LSTM编码,再经过一层Dense(其实可有可无)的线性变换,最后经过CRF层,即可。
数据处理的代码稍微复杂一点。我们第一步要做的就是构建词典,即给语料中的每一个词一个唯一的id。同时,我们也需要对label(即那些B、I、O)进行id化处理。具体代码如下:
from collections import Counter
def get_dict(min_num = 5):
paths = [
'data/example.train',
'data/example.dev',
'data/example.test'
]
label_dict = []
word_dict = []
for path in paths:
with open(path) as f:
for line in f:
p = line.split()
if len(p) == 1:
word_dict.append(p[0])
elif len(p) == 2:
word_dict.append(p[0])
label_dict.append(p[1])
label_dict = Counter(label_dict)
word_dict = dict(Counter(word_dict))
label2id = {key: index for index, key in enumerate(label_dict)}
id2label = {v: k for k, v in label2id.items()}
word_dict = {v:k for k,v in dict(word_dict).items() if word_dict.get(k)>min_num }
word2id = {key: index+2 for index, key in enumerate(word_dict)}
word2id['PAD'] = 0
word2id['UNK'] = 1
id2word = {v: k for k, v in word2id.items()}
return word2id, id2word, label2id, id2label
#获取字符-di映射字典,和label-id映射字典
word2id, id2word, label2id, id2label = get_dict()
再写一个生成训练数据的函数:
def get_data(path, train=True):
X, Y = [], []
x, y = [], []
with open(path) as f:
for line in f:
if line == '\n':
X.append(x)
Y.append(y)
x = []
y = []
continue
p = line.split()
x.append(word2id.get(p[0], 1))
y.append(label2id.get(p[1]))
if len(x)>0:
X.append(x)
Y.append(y)
return X, Y
训练数据X,Y以及验证数据val_x,val_y,对其进行补零和截断操作。同时,将label进行to_categorical操作,转化为one-hot编码。
X, Y = get_data('data/example.train')
val_x, val_y = get_data('data/example.dev')
X, Y = pad_sequences(X, maxlen=200), pad_sequences(Y, maxlen=200)
val_x, val_y = pad_sequences(val_x, maxlen=200), pad_sequences(val_y, maxlen=200)
Y =np_utils.to_categorical(Y)
val_y =np_utils.to_categorical(val_y)
训练模型:
lstm_crf = bi_lstm_crf(vocab_size=len(word2id), n_class=len(label2id))
model = lstm_crf.creat_model()
model.fit(X, Y, batch_size=64, epochs=2,
validation_data=[val_x, val_y])
简单训练几轮就可以达到不错的效果。
欢迎关注公众号“数据科学杂谈”,回复“CRF”获取本文全部代码和数据,本公众号原创技术文章第一时间推送。
扫码下图关注我们不会让你失望!