使用keras实现BiLSTM+CNN+CRF文字标记NER
import tensorflow as tf
from keras.models import Model, Input
from keras.layers import LSTM, Embedding, Dense, TimeDistributed, Dropout, Bidirectional,Conv1D
from keras_contrib.layers.crf import CRF
命名实体识别(NER)是自然语言处理(NLP)的重要分支,它旨在从文本中识别实体类别,如人名、组织、位置、时间等等。在早期的NER中,基于规则的方法是主流,但由于特定语言中的复杂性和规则的缺失,机器学习方法成为了NER的主流。BiLSTM-CRF是NER任务的一种最常用的架构。如果文本中有重叠实体,CNN层能够帮助提取这类特征,从而提高NER的准确性。因此,本篇文章将介绍BiLSTM-CNN-CRF模型的实现。
1.数据处理
首先,我们需要加载数据集。此处选择的是conll2003数据集,它包含了完全标记的英文数据。因为NER数据集通常都是按每行一个单词的格式存储,如下所示:
U.N. NNP I-ORG O
official NN O O
Ekeus NNP I-PER O
heads VBZ O O
for IN O O
Baghdad NNP I-LOC B-LOC
. . O O
B-和I-前缀分别标记了开始和中间标签的单词。下面是代码加载conll2003数据集的过程:
import os
import codecs
import numpy as np
def load_data():
## 定义数据集文件路径
TRAIN_FILE = './data/train.txt'
DEV_FILE = './data/dev.txt'
TEST_FILE = './data/test.txt'
## 读取并返回数据
train_sentences, train_tags = read_conll2003(TRAIN_FILE)
dev_sentences, dev_tags = read_conll2003(DEV_FILE)
test_sentences, test_tags = read_conll2003(TEST_FILE)
return train_sentences, train_tags, dev_sentences, dev_tags, test_sentences, test_tags
def read_conll2003(filename):
"""
读取文件,返回一组列表,其中每个单词存储在字符串中,并保存标签/命名实体标记。
返回的列表是按句子分组的,其中每个词都由单独空格分开。
:param filename: 文件路径
:return: ([句子1],[句子1的标记/命名实体标记], [句子2], [句子2的标记], ...)
"""
sentences = []
labels = []
with codecs.open(filename, 'r', encoding='utf-8', errors='ignore') as f:
words = []
tags = []
for line in f:
line = line.strip() ## 清除两侧空白
if len(line) == 0 or line.startswith('-DOCSTART-'): ## 文档开始或结束
if len(words) > 0:
sentences.append(words)
labels.append(tags)
words = []
tags = []
else:
splits = line.split(' ')
words.append(splits[0])
if len(splits) > 1:
tags.append(splits[-1].replace('\n', '')) ## 移除结尾换行符
## 如果还有剩下的句子,组成一批。
if len(words) > 0 and len(tags) > 0:
sentences.append(words)
labels.append(tags)
return sentences, labels
为了把文本输入到神经网络中,需要将每个单词转换为数字。这通常使用词嵌入(Word Embedding)来实现,它将每个单词映射到向量空间中的矢量表示。在本例中,我们使用GloVe预训练的词嵌入。GloVe是预训练的词向量集,它基于整个维基百科巨型数据集和一些互联网文本数据训练得到,经过训练的结果可以直接用来进行通用任务的单词嵌入(类比于word2vec)。
def load_glove_embeddings():
## 加载GloVe预训练的词嵌入向量
embeddings = {}
with codecs.open('./data/glove.6B.100d.txt', 'r',
encoding='utf8') as f:
for line in f:
values = line.strip().split(' ')
word = values[0]
vector = np.asarray(values[1:], dtype='float32')
embeddings[word] = vector
return embeddings
## 加载数据并处理,甚至可以手动定义每个单词的转换
train_sentences, train_tags, dev_sentences, dev_tags, test_sentences, test_tags = load_data()
print('加载训练集句子数:%d' % len(train_sentences))
train_word_embeddings = load_glove_embeddings()
2.构建模型
在构建模型之前,我们需要先对文字进行padding,使他们具有相同的长度。对于较短的文字,这意味着添加0到末尾;对于过长的文字,这意味着截取文本开头(或结尾)。我们使用Keras的“pad_sequences”方法实现上述操作。
专门针对NER的双向LSTM模型通常被用于将先前的和后续的标记考虑在内。它根据先前和下一个标记的定义来推理当前标记,从而寻求命名实体。因此,我们在模型的第一层中添加双向LSTM层。
def get_bilstm_cnn_crf_model(word_embeddings, train=False):
## 输入层定义
input_layer = Input(shape=(None,), dtype='int32', name='words_input')
## 计算需要padding的长度
max_sentence_length = 200
input_layer_padding = tf.keras.layers.Lambda(lambda x: tf.pad(x, [[0, 0], [0, max_sentence_length - tf.shape(x)[1]]], 'CONSTANT'))(input_layer)
## 词嵌入
embedding_weights = np.random.randn(len(word_embeddings) + 1, 100)
for word, index in word_embeddings.items():
if index is not None:
embedding_weights[index] = word_embeddings[word]
embedding_layer = Embedding(len(word_embeddings) + 1,
100,
weights=[embedding_weights],
trainable=train,
mask_zero=True,
name='word_embedding_layer')(input_layer_padding)
dropout_1 = Dropout(0.5, name='dropout_1')(embedding_layer)
## 双向LSTM
bilstm_layer = Bidirectional(LSTM(units=256, return_sequences=True, recurrent_dropout=0.1), name='bilstm_layer')(dropout_1)
## 一维卷积层
cnn_1d = Conv1D(filters=100, kernel_size=1,activation='relu')(bilstm_layer)
## CRF层
crf_layer = CRF(len(tag_set), sparse_target=True)
out = crf_layer(cnn_1d)
return Model([input_layer], [out])
我们在LSTM的顶部添加了一层1D卷积层(CNN)。这个想法来源于一篇论文:《End-to-end Sequence Labeling via Bi-directional LSTM-CNNs-CRF》。与LSTM相比,CNN能够提取序列中的局部特征。因此,在结合LSTM模型时,CNN可以帮助提高模型的性能。CNN的过滤器大小设置为1,这意味着我们只考虑当前单词的特征。我们还在模型的最后一层添加了CRF,以进一步提高NER模型的准确性。CRF通常比Softmax分类器更适用于序列标注任务,因为它可以将标签之间的依赖性考虑进去。
3.模型的训练与评估
我们训练上述模型,并输出验证和测试准确性:
## 加载数据集
train_sentences, train_tags, dev_sentences, dev_tags, test_sentences, test_tags = load_data()
train_word_embeddings = load_glove_embeddings()
## 定义标签训练集
tag_set = set(tag for doc in train_tags + dev_tags + test_tags for tag in doc)
tag2index = {'': 0, 'O': 1}
for tag in tag_set:
if tag != 'O':
tag2index['B-' + tag] = len(tag2index)
tag2index['I-' + tag] = len(tag2index)
## 转换数据集为模型所需的形式
def sentences_to_indices(data_sentences):
data_idx = []
for sentence in data_sentences:
sentence_idx = []
for word in sentence:
if word in train_word_embeddings:
sentence_idx.append(train_word_embeddings[word])
else:
sentence_idx.append(0)
if len(sentence_idx) > 0:
data_idx.append(sentence_idx)
return data_idx
x_train = sentences_to_indices(train_sentences)
y_train = sentences_to_indices(train_tags)
x_dev = sentences_to_indices(dev_sentences)
y_dev = sentences_to_indices(dev_tags)
x_test = sentences_to_indices(test_sentences)
y_test = sentences_to_indices(test_tags)
x_train = tf.keras.preprocessing.sequence.pad_sequences(x_train, maxlen=200, padding='post', truncating='post', value=0)
y_train = tf.keras.preprocessing.sequence.pad_sequences(y_train, maxlen=200, padding='post', truncating='post', value=0)
x_dev = tf.keras.preprocessing.sequence.pad_sequences(x_dev, maxlen=200, padding='post', truncating='post', value=0)
y_dev = tf.keras.preprocessing.sequence.pad_sequences(y_dev, maxlen=200, padding='post', truncating='post', value=0)
x_test = tf.keras.preprocessing.sequence.pad_sequences(x_test, maxlen=200, padding='post', truncating='post', value=0)
y_test = tf.keras.preprocessing.sequence.pad_sequences(y_test, maxlen=200, padding='post', truncating='post', value=0)
y_train = np.array([np.eye(len(tag2index))[np.array([tag2index[tag] for tag in sent_tags])] for sent_tags in train_tags])
y_dev = np.array([np.eye(len(tag2index))[np.array([tag2index[tag] for tag in sent_tags])] for sent_tags in dev_tags])
y_test = np.array([np.eye(len(tag2index))[np.array([tag2index[tag] for tag in sent_tags])] for sent_tags in test_tags])
model = get_bilstm_cnn_crf_model(train_word_embeddings)
## 训练模型
optimizer = tf.keras.optimizers.Adam(lr=0.001, beta_1=0.9, beta_2=0.999, epsilon=1e-08, decay=0.0)
model.compile(optimizer=optimizer,loss=crf_layer.loss_function,metrics=[crf_layer.accuracy])
model.fit(x_train, y_train, validation_data=(x_dev, y_dev),batch_size=32, epochs=10)
## 评估模型
from seqeval.metrics import precision_score, recall_score, f1_score, classification_report
print('开始评估模型')
pred = model.predict(x_test, verbose=1)
pred_tags = n_viterbi_decode(pred, crf_layer.trans_params)
for i in range(len(pred_tags)):
for j in range(len(pred_tags[i])):
pred_tags[i][j] = list(tag2index.keys())[list(tag2index.values()).index(np.argmax(pred_tags[i][j]))]
test_tags2 = []
for i in range(len(y_test)):
for j in range(len(y_test[i])):
if list(tag2index.keys())[list(tag2index.values()).index(np.argmax(y_test[i][j]))] == '':
break
test_tags2.append(list(tag2index.keys())[list(tag2index.values()).index(np.argmax(y_test[i][j]))])
print('准确率:', f1_score(test_tags2, pred_tags))
print(classification_report(test_tags2, pred_tags))
ceil_label是将小数转换为整数的方法,我们在结果中加了一些样本标签。在训练完成后,我们对模型进行了评估。首先,我们使用test数据集获得预测标签,并使用Seqeval库中提供的函数计算准确性。
4.总结
本文我们介绍了如何使用Python和Keras开发名为BiLSTM-CNN-CRF的Named Entity Recognition (NER)模型。NER是自然语言处理中的一个常见任务,它涉及识别文本中的命名实体,如人名,组织和位置等。
与传统的方法不同,我们的模型结合了双向LSTM、CNN和CRF,以帮助提取序列中的特征并提高准确性。我们还介绍了如何加载GloVe预训练的词嵌入,以便能够将单词转换为数字向量。