【自然语言处理(NLP)】基于LSTM的命名实体识别

时间:2022-10-11 10:18:02

【自然语言处理(NLP)】基于LSTM的命名实体识别


作者简介:在校大学生一枚,华为云享专家,阿里云专家博主,腾云先锋(TDP)成员,云曦智划项目总负责人,全国高等学校计算机教学与产业实践资源建设专家委员会(TIPCC)志愿者,以及编程爱好者,期待和大家一起学习,一起进步~ . 博客主页ぃ灵彧が的学习日志 . 本文专栏机器学习 . 专栏寄语:若你决定灿烂,山无遮,海无拦 . 【自然语言处理(NLP)】基于LSTM的命名实体识别

(文章目录)


前言

(一)、任务描述

命名实体识别任务主要识别文本中的实体,并且给识别出的实体进行分类,比如人名、地名、机构名或其它类型。本质上,对于给定的文本,只需要对其中的每个单词进行分类,只不过需要对分类的标签进行重新定义。


(二)、环境配置

本示例基于飞桨开源框架2.0版本。

import paddle
import numpy as np
import matplotlib.pyplot as plt
print(paddle.__version__)

输出结果如下图1所示: 【自然语言处理(NLP)】基于LSTM的命名实体识别


一、数据准备


(一)、导入相关包

import paddle
import paddle.nn as nn

import paddlenlp
from paddlenlp.datasets import MapDataset
from paddlenlp.data import Stack, Tuple, Pad
from paddlenlp.layers import LinearChainCrf, ViterbiDecoder, LinearChainCrfLoss
from paddlenlp.metrics import ChunkEvaluator

(二)、数据集加载

from paddlenlp.datasets import load_dataset

# 由于MSRA_NER数据集没有dev dataset,我们这里重复加载test dataset作为dev_ds
train_ds, dev_ds, test_ds = load_dataset(
        'msra_ner', splits=('train', 'test', 'test'), lazy=False)

label_vocab = {label:label_id for label_id, label in enumerate(train_ds.label_list)}
words = set()
word_vocab = []
for item in train_ds:
    word_vocab += item['tokens'] 
word_vocab = {k:v+2 for v,k in enumerate(set(word_vocab))}
word_vocab['PAD'] = 0
word_vocab['OOV'] = 1
lens = len(train_ds)+len(dev_ds)
print(len(train_ds)/lens,len(dev_ds)/lens,len(test_ds)/lens)

输出结果如下图2所示:

【自然语言处理(NLP)】基于LSTM的命名实体识别

def convert_tokens_to_ids(tokens, vocab, oov_token='OOV'):
    token_ids = []
    oov_id = vocab.get(oov_token) if oov_token else None
    for token in tokens:
        token_id = vocab.get(token, oov_id)
        token_ids.append(token_id)
    return token_ids


def convert_example(example):
        tokens, labels = example['tokens'],example['labels']
        token_ids = convert_tokens_to_ids(tokens, word_vocab, 'OOV')
        label_ids = labels #convert_tokens_to_ids(labels, label_vocab, 'O')
        return token_ids, len(token_ids), label_ids

train_ds.map(convert_example)
dev_ds.map(convert_example)
test_ds.map(convert_example)
# print(train_ds)

输出结果如下图3所示:

【自然语言处理(NLP)】基于LSTM的命名实体识别


(三)、构造dataloder

batchify_fn = lambda samples, fn=Tuple(
        Pad(axis=0, pad_val=word_vocab.get('OOV')),  # token_ids
        Stack(),  # seq_len
        Pad(axis=0, pad_val=label_vocab.get('O'))  # label_ids
    ): fn(samples)

train_loader = paddle.io.DataLoader(
        dataset=train_ds,
        batch_size=32,
        shuffle=True,
        drop_last=True,
        return_list=True,
        collate_fn=batchify_fn)

dev_loader = paddle.io.DataLoader(
        dataset=dev_ds,
        batch_size=32,
        drop_last=True,
        return_list=True,
        collate_fn=batchify_fn)

test_loader = paddle.io.DataLoader(
        dataset=test_ds,
        batch_size=32,
        drop_last=True,
        return_list=True,
        collate_fn=batchify_fn)

二、网络构建

随着深度学习的发展,目前主流的序列化标注任务基于词向量(word embedding)进行表示学习。下面介绍模型的整体训练流程如下:

【自然语言处理(NLP)】基于LSTM的命名实体识别


序列标注任务常用的模型是RNN+CRF。GRU和LSTM都是常用的RNN单元。这里我们以Bi-LSTM+CRF模型为例,介绍如何使用 PaddlePaddle 定义序列化标注任务的网络结构。如下图所示,LSTM的输出可以作为 CRF 的输入,最后 CRF 的输出作为模型整体的预测结果。

【自然语言处理(NLP)】基于LSTM的命名实体识别

class BiLSTMWithCRF(nn.Layer):
    def __init__(self,
                 emb_size,
                 hidden_size,
                 word_num,
                 label_num,
                 use_w2v_emb=False):
        super(BiLSTMWithCRF, self).__init__()
        self.word_emb = nn.Embedding(word_num, emb_size)
        self.lstm = nn.LSTM(emb_size,
                          hidden_size,
                          num_layers=2,
                          direction='bidirectional')
        self.fc = nn.Linear(hidden_size * 2, label_num + 2)  # BOS EOS
        self.crf = LinearChainCrf(label_num)
        self.decoder = ViterbiDecoder(self.crf.transitions)

    def forward(self, x, lens):
        embs = self.word_emb(x)
        output, _ = self.lstm(embs)
        output = self.fc(output)
        _, pred = self.decoder(output, lens)
        return output, lens, pred

# Define the model netword and its loss
network = BiLSTMWithCRF(300, 300, len(word_vocab), len(label_vocab))
model = paddle.Model(network)

三、网络配置

定义网络结构后,需要配置优化器、损失函数、评价指标。


评价指标

针对每条序列样本的预测结果,序列标注任务将预测结果按照语块(chunk)进行结合并进行评价。评价指标通常有 Precision、Recall 和 F1。

  1. Precision,精确率,也叫查准率,由模型预测正确的个数除以模型总的预测的个数得到,关注模型预测出来的结果准不准
  2. Recall,召回率,又叫查全率, 由模型预测正确的个数除以真实标签的个数得到,关注模型漏了哪些东西
  3. F1,综合评价指标,计算公式如下,$F1 = \frac{2PrecisionRecall}{Precision+Recall}$,同时考虑 Precision 和 Recall ,是 Precision 和 Recall 的折中。

paddlenlp.metrics中集成了ChunkEvaluator评价指标,并逐步丰富中,


optimizer = paddle.optimizer.Adam(learning_rate=0.001, parameters=model.parameters())
crf_loss = LinearChainCrfLoss(network.crf)
chunk_eval(label_list=label_vocab.keys(), suffix=True)
model.prepare(optimizer, crf_loss, chunk_evaluator)

四、模型训练

model.fit(train_data=train_loader,
              eval_data=dev_loader,
              epochs=5, 
              save_dir='./results',
              log_freq=100)

输出结果如下图4所示:

【自然语言处理(NLP)】基于LSTM的命名实体识别


五、模型评估

调用model.evaluate,查看序列化标注模型在测试集(test.txt)上的评测结果。


代码如下:

model.eval(eval_data=test_loader, log_freq=10)

六、模型预测

利用已有模型,可在未知label的数据集(此处复用测试集test.txt)上进行预测,得到模型预测结果及各label的概率。


def parse_decodes(ds, decodes, lens, label_vocab):
    decodes = [x for batch in decodes for x in batch]
    lens = [x for batch in lens for x in batch]
    print(len(decodes),len(decodes))
    id_label = dict(zip(label_vocab.values(), label_vocab.keys()))
    # print(id_label)
    outputs = []
    i=0
    for idx, end in enumerate(lens):
        sent = ds.data[idx]['tokens'][:end]
        tags = [id_label[x] for x in decodes[idx][:end]]
        sent_out = []
        tags_out = []
        words = ""
        for s, t in zip(sent, tags):
            # {'B-PER': 0, 'I-PER': 1, 'B-ORG': 2, 'I-ORG': 3, 'B-LOC': 4, 'I-LOC': 5, 'O': 6}?
            if t.startswith('B-') or t == 'O':
                if len(words):
                    sent_out.append(words)         # 上一个实体保存
                tags_out.append(t.split('-')[-1])   # 保存该实体的类型
                words = s                          
            else:   # 保存实体的
                words += s
        if len(sent_out) < len(tags_out):
            sent_out.append(words)
        if len(sent_out) != len(tags_out):
            print(len(sent_out),len(tags_out))
            continue
        cs = [str((s, t)) for s, t in zip(sent_out, tags_out)]
        ss = ''.join(cs)
        i+=1
        outputs.append(ss)

    return outputs
outputs, lens, decodes = model.predict(test_data=test_loader)
print(len(decodes),len(decodes[0]))
print(len(lens),len(lens[0]))

preds = parse_decodes(test_ds, decodes, lens, label_vocab)
print(preds[0])
print('----------------')
print(preds[1])
print('----------------')
print(preds[2]) 

总结

本系列文章内容为根据清华社出版的《自然语言处理实践》所作的相关笔记和感悟,其中代码均为基于百度飞桨开发,若有任何侵权和不妥之处,请私信于我,定积极配合处理,看到必回!!!

最后,引用本次活动的一句话,来作为文章的结语~( ̄▽ ̄~)~:

【**学习的最大理由是想摆脱平庸,早一天就多一份人生的精彩;迟一天就多一天平庸的困扰。**】

【自然语言处理(NLP)】基于LSTM的命名实体识别