Keras实现BiLSTM+CRF中文命名实体识别--实战篇(序列标注)

本文主要是利用Keras框架搭建BiLSTM+CRF的序列标注模型,完成中文的命名实体识别任务。这里使用的数据集是提前处理过的,已经转成命名实体识别需要的“BIO”标注格式。

详细代码和数据:https://github.com/huanghao128/zh-nlp-demo
输入的训练数据集格式如下:

你	O
会	O
遇	O
见	O
数	O
十	O
次	O
可	B-ORG
口	I-ORG
可	I-ORG
乐	I-ORG
的	O
广	O
告	O

十	O
几	O
次	O
百	B-ORG
事	I-ORG
可	I-ORG
乐	I-ORG
的	O
广	O
告	O

十	O
多	O
次	O
介	O
绍	O
.
.
.

模型结构

BiLSTM+CRF模型未双向LSTM模型在输出位置上连接一个CRF层,目的是学习相邻输出之间的依赖关系,从而提高输出标签的整体准确率,模型结构图如下:
在这里插入图片描述

数据处理

本文使用的数据是已经处理过的,所以直接加载数据就好了。首先我们要加载字符词典文件,还有BIO标记类别的索引化。其中BIO标记中B-PER和I-PER表示人名,B-LOC和I-LOC表示地名,B-ORG和I-ORG表示机构名。
代码如下:

char_vocab_path = "./data/char_vocabs.txt" # 字典文件
train_data_path = "./data/train_data" # 训练数据
test_data_path = "./data/test_data" # 测试数据

special_words = ['<PAD>', '<UNK>'] # 特殊词表示

# "BIO"标记的标签
label2idx = {"O": 0,
             "B-PER": 1, "I-PER": 2,
             "B-LOC": 3, "I-LOC": 4,
             "B-ORG": 5, "I-ORG": 6
             }
# 索引和BIO标签对应
idx2label = {idx: label for label, idx in label2idx.items()}

# 读取字符词典文件
with open(char_vocab_path, "r", encoding="utf8") as fo:
    char_vocabs = [line.strip() for line in fo]
char_vocabs = special_words + char_vocabs

# 字符和索引编号对应
idx2vocab = {idx: char for idx, char in enumerate(char_vocabs)}
vocab2idx = {char: idx for idx, char in idx2vocab.items()}

然后加载训练和测试集,并把原始数据和BIO标记转成索引和类别编号。

char_vocab_path = "./data/char_vocabs.txt" # 字典文件
train_data_path = "./data/train_data" # 训练数据
test_data_path = "./data/test_data" # 测试数据

special_words = ['<PAD>', '<UNK>'] # 特殊词表示

# "BIO"标记的标签
label2idx = {"O": 0,
             "B-PER": 1, "I-PER": 2,
             "B-LOC": 3, "I-LOC": 4,
             "B-ORG": 5, "I-ORG": 6
             }
# 索引和BIO标签对应
idx2label = {idx: label for label, idx in label2idx.items()}

# 读取字符词典文件
with open(char_vocab_path, "r", encoding="utf8") as fo:
    char_vocabs = [line.strip() for line in fo]
char_vocabs = special_words + char_vocabs

# 字符和索引编号对应
idx2vocab = {idx: char for idx, char in enumerate(char_vocabs)}
vocab2idx = {char: idx for idx, char in idx2vocab.items()}

数据的填充,以及类别的one-hot编码

import keras
from keras.preprocessing import sequence

MAX_LEN = 100
VOCAB_SIZE = len(vocab2idx)
CLASS_NUMS = len(label2idx)

# padding data
train_datas = sequence.pad_sequences(train_datas, maxlen=MAX_LEN)
train_labels = sequence.pad_sequences(train_labels, maxlen=MAX_LEN)
test_datas = sequence.pad_sequences(test_datas, maxlen=MAX_LEN)
test_labels = sequence.pad_sequences(test_labels, maxlen=MAX_LEN)
print('x_train shape:', train_datas.shape)
print('x_test shape:', test_datas.shape)
# encoder one-hot
train_labels = keras.utils.to_categorical(train_labels, CLASS_NUMS)
test_labels = keras.utils.to_categorical(test_labels, CLASS_NUMS)
print('trainlabels shape:', train_labels.shape)
print('testlabels shape:', test_labels.shape)

模型构建和训练

模型构建主要使用keras自带的基础模型组装,首先是双向LSTM模型,然后输出接CRF模型,输出对每个时刻的分类。

## BiLSTM+CRF模型构建
from keras.models import Sequential
from keras.models import Model
from keras.layers import Masking, Embedding, Bidirectional, LSTM, Dense, Input, TimeDistributed, Activation
from keras_contrib.layers import CRF
from keras_contrib.losses import crf_loss
from keras_contrib.metrics import crf_viterbi_accuracy
from keras import backend as K

EPOCHS = 20
BATCH_SIZE = 64
EMBED_DIM = 128
HIDDEN_SIZE = 64
MAX_LEN = 100
VOCAB_SIZE = len(vocab2idx)
CLASS_NUMS = len(label2idx)

# Input输入层
inputs = Input(shape=(MAX_LEN,), dtype='int32')
# masking屏蔽层
x = Masking(mask_value=0)(inputs)
# Embedding层
x = Embedding(VOCAB_SIZE, EMBED_DIM, mask_zero=True)(x)
# Bi-LSTM层
x = Bidirectional(LSTM(HIDDEN_SIZE, return_sequences=True))(x)
# Bi-LSTM展开输出
x = TimeDistributed(Dense(CLASS_NUMS))(x)
# CRF模型层
outputs = CRF(CLASS_NUMS)(x)

model = Model(inputs=inputs, outputs=outputs)
model.summary()

model.compile(loss=crf_loss, optimizer='adam', metrics=[crf_viterbi_accuracy])
# 训练模型
model.fit(train_datas, train_labels, epochs=EPOCHS, verbose=1, validation_split=0.1)

score = model.evaluate(test_datas, test_labels, batch_size=BATCH_SIZE)
print(model.metrics_names)
print(score)

# 保存模型
model.save("./model/ch_ner_model.h5")

结果预测

结果预测是我们训练好模型后,重新加载模型,输入新的要预测文本,然后识别出文本中的命名实体。这里首先要加载字符词典,然后加载模型,之后对输入文本预处理成字符序列,然后模型预测每个时刻的输出类别,最后把类别转成BIO标记,BIO标记组合成正确的命名实体。

char_vocab_path = "./data/char_vocabs.txt" # 字典文件
model_path = "./model/ch_ner_model.h5" # 模型文件

ner_labels = {"O": 0, "B-PER": 1, "I-PER": 2, "B-LOC": 3, "I-LOC": 4, "B-ORG": 5, "I-ORG": 6}
special_words = ['<PAD>', '<UNK>']
MAX_LEN = 100

with open(char_vocab_path, "r", encoding="utf8") as fo:
    char_vocabs = [line.strip() for line in fo]
char_vocabs = special_words + char_vocabs

idx2vocab = {idx: char for idx, char in enumerate(char_vocabs)}
vocab2idx = {char: idx for idx, char in idx2vocab.items()}

idx2label = {idx: label for label, idx in ner_labels.items()}

sentence = "中华人民共和国国务院总理周恩来在外交部长陈毅的陪同下,连续访问了埃塞俄比亚等非洲10国以及阿尔巴尼亚。"

sent2id = [vocab2idx[word] if word in vocab2idx else vocab2idx['<UNK>'] for word in sentence]
sent2input = np.array([sent2id[:MAX_LEN] + [0] * (MAX_LEN-len(sent2id))])

model = load_model(model_path, custom_objects={'CRF': CRF}, compile=False)
y_pred = model.predict(sent2input)

y_label = np.argmax(y_pred, axis=2)
y_label = y_label.reshape(1, -1)[0]
y_ner = [index2label[i] for i in y_label][-MAX_LEN:]

print(idx2label)
print(sent_chars)
print(sent2id)
print(y_ner)

从BIO标记的结果,解析成具体的人名、地名、机构名还需要一些操作,具体的解析过程如下:

# 对预测结果进行命名实体解析和提取
def get_valid_nertag(input_data, result_tags):
    result_words = []
    start, end =0, 1 # 实体开始结束位置标识
    tag_label = "O" # 实体类型标识
    for i, tag in enumerate(result_tags):
        if tag.startswith("B"):
            if tag_label != "O": # 当前实体tag之前有其他实体
                result_words.append((input_data[start: end], tag_label)) # 获取实体
            tag_label = tag.split("-")[1] # 获取当前实体类型
            start, end = i, i+1 # 开始和结束位置变更
        elif tag.startswith("I"):
            temp_label = tag.split("-")[1]
            if temp_label == tag_label: # 当前实体tag是之前实体的一部分
                end += 1 # 结束位置end扩展
        elif tag == "O":
            if tag_label != "O": # 当前位置非实体 但是之前有实体
                result_words.append((input_data[start: end], tag_label)) # 获取实体
                tag_label = "O"  # 实体类型置"O"
            start, end = i, i+1 # 开始和结束位置变更
    if tag_label != "O": # 最后结尾还有实体
        result_words.append((input_data[start: end], tag_label)) # 获取结尾的实体
    return result_words

result_words = get_valid_nertag(sent_chars, y_ner)
for (word, tag) in result_words:
    print("".join(word), tag)
  • 7
    点赞
  • 44
    收藏
    觉得还不错? 一键收藏
  • 7
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值