中文命名实体识别(Named Entity Recognition,NER)初探 - 郑瀚Andrew
2023-9-6 09:36:0 Author: www.cnblogs.com(查看原文) 阅读量:7 收藏

命名实体识别(Named Entity Recognition,NER),又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括:

  • 人名
  • 地名
  • 机构名
  • 专有名词等

NER是:

  • 信息提取
  • 问答系统
  • 句法分析
  • 机器翻译
  • 面向Semantic Web的元数据标注等

应用领域的重要基础工具,在自然语言处理技术走向实用化的过程中占有重要的地位。

例如在搜索场景下,NER是深度查询理解(Deep Query Understanding,简称 DQU)的底层基础信号,主要应用于搜索召回、用户意图识别、实体链接等环节,NER信号的质量,直接影响到用户的搜索体验,是NLP中一项非常基础的任务。

这里针对搜索召回稍微展开一些细节。

在O2O搜索中,对商家POI的描述是商家名称、地址、品类等多个互相之间相关性并不高的文本域。如果对O2O搜索引擎也采用全部文本域命中求交的方式,就可能会产生大量的误召回。一种解决方法如下图所示,

即让特定的查询只在特定的文本域做倒排检索,我们称之为“结构化召回”,可保证召回商家的强相关性。

举例来说,对于“海底捞”这样的请求,有些商家地址会描述为“海底捞附近几百米”,若采用全文本域检索这些商家就会被召回,显然这并不是用户想要的。而结构化召回基于NER将“海底捞”识别为商家,然后只在商家名相关文本域检索,从而只召回海底捞品牌商家,精准地满足了用户需求。

0x1:命名实体识别是什么? 

要了解NER是一回什么事,首先要先说清楚,什么是实体。简单的理解,实体,可以认为是某一个概念的实例。

例如,

  • “人名”是一种概念,或者说实体类型,那么“蔡英文”就是一种“人名”实体了。
  • “时间”是一种实体类型,那么“中秋节”就是一种“时间”实体了。

所谓实体识别,就是将你想要获取到的实体类型,从一句话里面挑出来的过程。

小明 在 北京大学 的 燕园 看了
PER ORG LOC

中国男篮 的一场比赛
ORG

如上面的例子所示,句子“小明在北京大学的燕园看了中国男篮 的一场比赛”,通过NER模型,将“小明 ”以PER,“北京大学”以ORG,“燕园”以LOC,“中国男篮”以ORG为类别分别挑了出来。

0x2:命名实体识别的数据标注方式 

NER是一种序列标注问题,因此他们的数据标注方式也遵照序列标注问题的方式,主要是以下几种方法:

  • IOB
  • BIOES
  • Markup

1、BIOES数据标注方式

先列出来BIOES分别代表什么意思:

  • B,即Begin,表示开始
  • I,即Intermediate,表示中间
  • E,即End,表示结尾
  • S,即Single,表示单个字符
  • O,即Other,表示其他,用于标记无关字符

将“小明在北京大学的燕园看了中国男篮的一场比赛”这句话,进行标注,结果就是:

[B-PER,E-PER,O, B-ORG,I-ORG,I-ORG,E-ORG,O,B-LOC,E-LOC,O,O,B-ORG,I-ORG,I-ORG,E-ORG,O,O,O,O]

换句话说,NER的过程,就是根据输入的句子,预测出其标注序列的过程。

2、IOB数据标注方式

  • I,表示内部
  • O,表示外部
  • B,表示开始

例子:B/I-XXX,其中:

  • B/I,表示这个词属于命名实体的开始或内部
  • XXX,表示命名实体的类型

0x3:命名实体识别的方法介绍

方法 代表技术 核心思想
基于规则方法 字典、规则 关注规则
基于机器学习方法 HMM、HEMM、ME、SVM、CRF 关注概率
基于深度学习方法 BiLSTM-CNN-CRF、BERT-BiLSTM-CRF 关注整体效果
基于大模型方法 注意力模型、迁移学习、GPT-3.5、Llama 关注整体效果、性能

1、实体词典匹配 - 基于规则方法

基本思想是匹配规则,

在很多搜索场景实体识别的工业实践中,整体技术选型往往是“实体词典匹配+模型预测”的框架,如下图所示,

实体词典匹配,通俗来说就是”规则匹配“,这种算法主要有如下优点:

  • 1、搜索中用户查询的头部流量通常较短、表达形式简单,且集中在商户、品类、地址等三类实体搜索,实体词典匹配虽简单但处理这类查询准确率也可达到90%以上。
  • 2、词典是NER领域强相关的,通过挖掘业务数据资源获取业务实体词典,经过在线词典匹配后可保证识别结果是领域适配的。
  • 3、新业务接入更加灵活,只需提供业务相关的实体词表就可完成新业务场景下的实体识别。
  • 4、NER下游使用方中有些对响应时间要求极高,词典匹配速度快,基本不存在性能问题。

既然词典匹配有这么多优点,那为什么还需要模型预测呢?答案主要有两方面原因:

  • 1、随着搜索体量的不断增大,中长尾搜索流量表述复杂,越来越多OOV(Out Of Vocabulary)问题开始出现,实体词典已经无法满足日益多样化的用户需求,模型预测具备泛化能力,可作为词典匹配的有效补充。
  • 2、实体词典匹配无法解决歧义问题,比如“黄鹤楼美食”,“黄鹤楼”在实体词典中同时是武汉的景点、北京的商家、香烟产品,词典匹配不具备消歧能力,这三种类型都会输出,而模型预测则可结合上下文,不会输出“黄鹤楼”是香烟产品。

2、HMM和CRF等机器学习算法 - 基于机器学习方法

HMM的相关细节可以参阅这篇文章。 

CRF的相关细节可以参阅这篇文章。  

3、基于深度学习方法

抽象来说,基于深度学习进行NER任务可以分为如下两个步骤:

  • representation extraction(词向量化):旨在获取输入序列中每个标记的高维表示。为了嵌入每个输入单词x,首先将输入句子X输入到编码器模型(例如BERT)。然后,将单词嵌入模型的最后一层的输出用作高维表示hi ∈ Rm×1,其中n表示输入句子的长度,m表示向量的维数的可变参数。
  • classification(多分类):对于分类,每个嵌入的高维向量h被发送到一个多层感知器,然后使用softmax函数生成命名实体词汇的分布: pNER = softmax MLP( h ∈ Rm×1 )

1)LSTM+CRF

采用LSTM作为特征抽取器,再接一个CRF层来作为输出层。

2)CNN+CRF

CNN虽然在长序列的特征提取上有弱势,但是CNN模型可有并行能力,有运算速度快的优势。膨胀卷积的引入,使得CNN在NER任务中,能够兼顾运算速度和长序列的特征提取。 

3)BERT+(LSTM)+CRF

BERT中蕴含了大量的通用知识,利用预训练好的BERT模型,再用少量的标注数据进行FINETUNE是一种快速的获得效果不错的NER的方法。

4、ChatGPT zero-shot prompt技术 - 基于大模型方法

0x4:命名实体识别的未来挑战

  • 数量无穷:业务不断发展,用户量不断增加,命名实体的数量不断增加
  • 构词灵活:例如”广州恒大淘宝俱乐部“、”广州恒大“、”恒大“
  • 类别模糊:例如”广州未赢够“、“广州下雪嘞” 

参考链接:

https://zhuanlan.zhihu.com/p/88544122 
https://tech.meituan.com/2020/07/23/ner-in-meituan-nlp.html
https://zhuanlan.zhihu.com/p/156914795

中文命名实体识别工具有很多,

工具简介访问地址
Stanford NER 斯坦福大学开发的基于条件随机场的命名实体识别系统,该系统参数是基于CoNLL、MUC-6、MUC-7和ACE命名实体语料训练出来的。 官网 | GitHub 地址
MALLET 麻省大学开发的一个统计自然语言处理的开源包,其序列标注工具的应用中能够实现命名实体识别。 官网
Hanlp HanLP是一系列模型与算法组成的NLP工具包,由大快搜索主导并完全开源,目标是普及自然语言处理在生产环境中的应用。支持命名实体识别。 官网 | GitHub 地址
NLTK NLTK是一个高效的Python构建的平台,用来处理人类自然语言数据。 官网 | GitHub 地址
SpaCy 工业级的自然语言处理工具,遗憾的是不支持中文。 官网 | GitHub 地址
Crfsuite 可以载入自己的数据集去训练CRF实体识别模型。 文档 | GitHub 地址
CRF++   GitHub地址

0x1:HanLP

1、安装

2、中文分词 

3、依存句法分析

4、API调用

from pyhanlp import *

print(HanLP.segment('你好,欢迎在Python中调用HanLP的API'))
for term in HanLP.segment('下雨天地面积水'):
    print('{}\t{}'.format(term.word, term.nature)) # 获取单词与词性
testCases = [
    "商品和服务",
    "结婚的和尚未结婚的确实在干扰分词啊",
    "买水果然后来世博园最后去世博会",
    "中国的首都是北京",
    "欢迎新老师生前来就餐",
    "工信处女干事每月经过下属科室都要亲口交代24口交换机等技术性器件的安装工作",
    "随着页游兴起到现在的页游繁盛,依赖于存档进行逻辑判断的设计减少了,但这块也不能完全忽略掉。"]
for sentence in testCases: print(HanLP.segment(sentence))
# 关键词提取
document = "水利部水资源司司长陈明忠9月29日在国务院新闻办举行的新闻发布会上透露," \
           "根据刚刚完成了水资源管理制度的考核,有部分省接近了红线的指标," \
           "有部分省超过红线的指标。对一些超过红线的地方,陈明忠表示,对一些取用水项目进行区域的限批," \
           "严格地进行水资源论证和取水许可的批准。"
print(HanLP.extractKeyword(document, 2))
# 自动摘要
print(HanLP.extractSummary(document, 3))
# 依存句法分析
print(HanLP.parseDependency("徐先生还具体帮助他确定了把画雄鹰、松鼠和麻雀作为主攻目标。"))

参考链接:

https://hanlp.hankcs.com/
https://github.com/hankcs/pyhanlp
https://easyai.tech/ai-definition/ner/

0x1:模型架构简述

近年来,随着硬件计算能力的发展以及词的分布式表示(word embedding)的提出,神经网络可以有效处理许多NLP任务。这类方法对于序列标注任务(如CWS、POS、NER)的处理方式是类似的:将token从离散one-hot表示映射到低维空间中成为稠密的embedding,随后将句子的embedding序列输入到RNN中,用神经网络自动提取特征,Softmax来预测每个token的标签。

这种方法使得模型的训练成为一个端到端的过程,而非传统的pipeline,不依赖于特征工程,是一种数据驱动的方法,但网络种类繁多、对参数设置依赖大,模型可解释性差。此外,这种方法的一个缺点是对每个token打标签的过程是独立的进行,不能直接利用上文已经预测的标签(只能靠隐含状态传递上文信息),进而导致预测出的标签序列可能是无效的,例如标签I-PER后面是不可能紧跟着B-PER的,但Softmax不会利用到这个信息。

为了解决这个问题,学界提出了DL-CRF模型做序列标注。在神经网络的输出层接入CRF层(重点是利用标签转移概率)来做句子级别的标签预测,使得标注过程不再是对各个token独立分类。 

应用于NER中的BiLSTM-CRF模型主要由Embedding层(主要有词向量,字向量以及一些额外特征),双向LSTM层,以及最后的CRF层构成。

实验结果表明BiLSTM-CRF已经达到或者超过了基于丰富特征的CRF模型,成为目前基于深度学习的NER方法中的最主流模型。

在特征方面,该模型继承了深度学习方法的优势,无需特征工程,使用词向量以及字符向量就可以达到很好的效果,如果有高质量的词典特征,能够进一步提升效果。

接下里的问题是,什么是CRF层?为什么要用CRF?

首先,句子xxx中的每个单词表达成一个向量,该向量包含了上述的word embedding和character embedding,其中character embedding随机初始化,word embedding通常采用预训练模型初始化。所有的embeddings 将在训练过程中进行微调。

其次,BiLSTM-CRF模型的的输入是上述的embeddings,输出是该句子xxx中每个单词的预测标签

从上图可以看出,BiLSTM层的输出是每个标签的得分,如单词w0w_0w0,BiLSTM的输出为1.5(B-Person),0.9(I-Person),0.1(B-Organization), 0.08 (I-Organization) and 0.05 (O),这些得分就是CRF层的输入。

将BiLSTM层预测的得分喂进CRF层,具有最高得分的标签序列将是模型预测的最好结果。

根据上文,能够发现,如果没有CRF层,即我们用下图所示训练BiLSTM命名实体识别模型: 

因为BiLSTM针对每个单词的输出是标签得分,对于每个单词,我们可以选择最高得分的标签作为预测结果。

例如,对于w0w_0w0,“B-Person"得分最高(1.5),因此我们可以选择“B-Person”最为其预测标签;同样的,w1w_1w1的标签为"I-Person”,w2w_2w2的为"O", w3w_3w3的标签为"B-Organization",w4w_4w4的标签为"O"。

按照上述方法,对于xxx虽然我们得到了正确的标签,但是大多数情况下是不能获得正确标签的,例如下图的例子:

显然,输出标签“I-Organization I-Person” 和 “B-Organization I-Person”是不对的。

CRF层可以对最终的约束标签添加一些约束条件,从而保证预测标签的有效性。而这些约束条件是CRF层自动从训练数据中学到。

约束可能是:

  • 一句话中第一个单词的标签应该是“B-“ or “O”,而不能是"I-";
  • “B-label1 I-label2 I-label3 I-…”中,label1, label2, label3 …应该是相同的命名实体标签。如“B-Person I-Person”是有效的,而“B-Person I-Organization” 是无效的;
  • “O I-label” 是无效的。一个命名实体的第一个标签应该以 “B-“ 开头,而不能以“I-“开头,换句话说, 应该是“O B-label”这种模式;

有了这些约束条件,无效的预测标签序列将急剧减少。

0x2:代码示例

1、命名实体识别的数据标注结构

项目使用了conll2003_v2数据集,其中标注的命名实体共计九类:

  • 'O'
  • 'B-LOC'
  • 'B-PER'
  • 'B-ORG'
  • 'I-PER'
  • 'I-ORG'
  • 'B-MISC'
  • 'I-LOC'
  • 'I-MISC'

实现了将输入识别为命名实体的模型,如下所示: 

# input
['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.']

# output
['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']

2、数据预处理

数据下载并解压,以供训练,下载解压后可以看到三个文件:

  • test.txt
  • train.txt
  • valid.txt

打开后可以看到,数据格式如下:

我们只需要每行开头和最后一个数据,他们分别是文本信息和命名实体。

我们需要将数据进行处理,使之成为网络能接收的形式。

from tqdm import tqdm
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.optimizers import *
import numpy as np


class NerDatasetReader:
    def read(self, data_path):
        data_parts = ['train', 'valid', 'test']
        extension = '.txt'
        dataset = {}
        for data_part in tqdm(data_parts):
            file_path = data_path + data_part + extension
            dataset[data_part] = self.read_file(str(file_path))
        return dataset
            
    def read_file(self, file_path):
        fileobj = open(file_path, 'r', encoding='utf-8')
        samples = []
        tokens = []
        tags = []

        for content in fileobj:
            content = content.strip('\n')
            if content == '-DOCSTART- -X- -X- O':
                pass
            elif content == '':
                if len(tokens) != 0:
                    # 每一句保存为了两个list,一个是单词list,另一个是标注list
                    samples.append((tokens, tags))
                    tokens = []
                    tags = []
            else:
                contents = content.split(' ')
                tokens.append(contents[0])
                tags.append(contents[-1])
        return samples


def get_dicts(datas):
    w_all_dict, n_all_dict = {}, {}
    for sample in datas:
        for token, tag in zip(*sample):
            if token not in w_all_dict.keys():
                w_all_dict[token] = 1
            else:
                w_all_dict[token] += 1
            
            if tag not in n_all_dict.keys():
                n_all_dict[tag] = 1
            else:
                n_all_dict[tag] += 1

    sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)
    sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)
    # 保留前15999个常用的单词,新增了一个"UNK"代表未知单词。
    w_keys = [x for x, _ in sort_w_list[:15999]]
    w_keys.insert(0, "UNK")

    n_keys = [x for x, _ in sort_n_list]
    w_dict = {x:i for i, x in enumerate(w_keys)}
    n_dict = {x:i for i, x in enumerate(n_keys)}
    return(w_dict, n_dict)


def w2num(datas, w_dict, n_dict):
    ret_datas = []
    for sample in datas:
        num_w_list, num_n_list = [], []
        for token, tag in zip(*sample):
            if token not in w_dict.keys():
                token = "UNK"
            if tag not in n_dict:
                tag = "O"
            num_w_list.append(w_dict[token])
            num_n_list.append(n_dict[tag])
        ret_datas.append((num_w_list, num_n_list, len(num_n_list)))
    return(ret_datas)


def len_norm(data_num, lens=80):
    ret_datas = []
    for sample1 in list(data_num):
        sample = list(sample1)
        ls = sample[-1]
        # print(sample)
        while(ls < lens):
            sample[0].append(0)
            ls = len(sample[0])
            sample[1].append(0)
        else:
            sample[0] = sample[0][:lens]
            sample[1] = sample[1][:lens]
        ret_datas.append(sample[:2])
    return(ret_datas)


def build_model(num_classes=9):
    model = Sequential()
    model.add(Embedding(16000, 256, input_length=80))
    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))
    model.add(Bidirectional(LSTM(128,return_sequences=True),merge_mode="concat"))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(num_classes, activation='softmax'))
    return(model)


Train = True

if __name__ == "__main__":
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    # print(dataset["test"])
    w_dict, n_dict = get_dicts(dataset["train"])
    # print(w_dict)
    # print(n_dict)
    data_num = {}
    # ([token对应的词典索引数字...], [标签对应的索引数字...], [句子tokens长度])
    data_num["train"] = w2num(dataset["train"], w_dict, n_dict)
    # print(data_num["train"][:10])
    # 我们输出句子长度的统计,发现最大值113,最小值为1,为了方便统一训练,我们归一化长度为80
    w_lens = [data[-1] for data in data_num["train"]]
    # print(max(w_lens), min(w_lens))
    # 句子长度归一化操作,这里采用padding为0,就是当做“UNK”与“O”来用,其实也可以使用Mask方法等
    data_norm = {}
    data_norm["train"] = len_norm(data_num["train"])
    print(data_norm["train"][:10])
    '''
    model = build_model()
    print(model.summary())
    opt = Adam(0.001)
    model.compile(loss="sparse_categorical_crossentropy",optimizer=opt)

    train_data = np.array(data_norm["train"])
    train_x = train_data[:,0,:]
    train_y = train_data[:,1,:]


    if(Train):
        print(train_x.shape)

        

        model.fit(x=train_x,y=train_y,epochs=10,batch_size=200,verbose=1,validation_split=0.1)
        model.save("model.h5")
    else:
        model.load_weights("model.h5")
        pre_y = model.predict(train_x[:4])

        print(pre_y.shape)

        pre_y = np.argmax(pre_y,axis=-1)

        for i in range(0,len(train_y[0:4])):
            print("label "+str(i),train_y[i])
            print("pred "+str(i),pre_y[i])
    '''

View Code

3、模型搭建

模型结构:

Model: "sequential"
_________________________________________________________________
 Layer (type)                Output Shape              Param #   
=================================================================
 embedding (Embedding)       (None, 80, 256)           4096000   
                                                                 
 bidirectional (Bidirectiona  (None, 80, 256)          394240    
 l)                                                              
                                                                 
 bidirectional_1 (Bidirectio  (None, 80, 256)          394240    
 nal)                                                            
                                                                 
 dense (Dense)               (None, 80, 128)           32896     
                                                                 
 dense_1 (Dense)             (None, 80, 9)             1161      
                                                                 
=================================================================
Total params: 4,918,537
Trainable params: 4,918,537
Non-trainable params: 0
_________________________________________________________________

4、模型训练

5、模型应用 - 文本实体识别

from tqdm import tqdm
from tensorflow.keras.layers import *
from tensorflow.keras.models import *
from tensorflow.keras.optimizers import *
import numpy as np


class NerDatasetReader:
    def read(self, data_path):
        data_parts = ['train', 'valid', 'test']
        extension = '.txt'
        dataset = {}
        for data_part in tqdm(data_parts):
            file_path = data_path + data_part + extension
            dataset[data_part] = self.read_file(str(file_path))
        return dataset
            
    def read_file(self, file_path):
        fileobj = open(file_path, 'r', encoding='utf-8')
        samples = []
        tokens = []
        tags = []

        for content in fileobj:
            content = content.strip('\n')
            if content == '-DOCSTART- -X- -X- O':
                pass
            elif content == '':
                if len(tokens) != 0:
                    # 每一句保存为了两个list,一个是单词list,另一个是标注list
                    samples.append((tokens, tags))
                    tokens = []
                    tags = []
            else:
                contents = content.split(' ')
                tokens.append(contents[0])
                tags.append(contents[-1])
        return samples


def get_dicts(datas):
    w_all_dict, n_all_dict = {}, {}
    for sample in datas:
        for token, tag in zip(*sample):
            if token not in w_all_dict.keys():
                w_all_dict[token] = 1
            else:
                w_all_dict[token] += 1
            
            if tag not in n_all_dict.keys():
                n_all_dict[tag] = 1
            else:
                n_all_dict[tag] += 1

    sort_w_list = sorted(w_all_dict.items(),  key=lambda d: d[1], reverse=True)
    sort_n_list = sorted(n_all_dict.items(),  key=lambda d: d[1], reverse=True)
    # 保留前15999个常用的单词,新增了一个"UNK"代表未知单词。
    w_keys = [x for x, _ in sort_w_list[:15999]]
    w_keys.insert(0, "UNK")

    n_keys = [x for x, _ in sort_n_list]
    w_dict = {x:i for i, x in enumerate(w_keys)}
    n_dict = {x:i for i, x in enumerate(n_keys)}
    return(w_dict, n_dict)


def w2num(datas, w_dict, n_dict):
    ret_datas = []
    for sample in datas:
        num_w_list, num_n_list = [], []
        for token, tag in zip(*sample):
            if token not in w_dict.keys():
                token = "UNK"
            if tag not in n_dict:
                tag = "O"
            num_w_list.append(w_dict[token])
            num_n_list.append(n_dict[tag])
        ret_datas.append((num_w_list, num_n_list, len(num_n_list)))
    return(ret_datas)


def len_norm(data_num, lens=80):
    ret_datas = []
    for sample1 in list(data_num):
        sample = list(sample1)
        ls = sample[-1]
        # print(sample)
        while(ls < lens):
            sample[0].append(0)
            ls = len(sample[0])
            sample[1].append(0)
        else:
            sample[0] = sample[0][:lens]
            sample[1] = sample[1][:lens]
        ret_datas.append(sample[:2])
    return(ret_datas)


def build_model(num_classes=9):
    model = Sequential()
    model.add(Embedding(16000, 256, input_length=80))
    model.add(Bidirectional(LSTM(128, return_sequences=True), merge_mode="concat"))
    model.add(Bidirectional(LSTM(128, return_sequences=True), merge_mode="concat"))
    model.add(Dense(128, activation='relu'))
    model.add(Dense(num_classes, activation='softmax'))
    return(model)


def load_dataset(dataset_type="test"):
    ds_rd = NerDatasetReader()
    dataset = ds_rd.read("./conll2003_v2/")
    # print(dataset["test"])
    w_dict, n_dict = get_dicts(dataset[dataset_type])
    # print(w_dict)
    # print(n_dict)
    data_num = {}
    # ([token对应的词典索引数字...], [标签对应的索引数字...], [句子tokens长度])
    data_num[dataset_type] = w2num(dataset[dataset_type], w_dict, n_dict)
    # print(data_num[dataset_type][:10])
    # 我们输出句子长度的统计,为了方便统一训练,我们归一化长度为80
    w_lens = [data[-1] for data in data_num[dataset_type]]
    # print(max(w_lens), min(w_lens))
    # 句子长度归一化操作,这里采用padding为0,就是当做“UNK”与“O”来用,其实也可以使用Mask方法等
    data_norm = {}
    data_norm[dataset_type] = len_norm(data_num[dataset_type])
    # print(data_norm[dataset_type][:10])
    return data_norm[dataset_type]


def do_train():
    data_norm = load_dataset(dataset_type="train")
    # 模型搭建
    model = build_model()
    print(model.summary())
    opt = Adam(0.001)
    model.compile(loss="sparse_categorical_crossentropy", optimizer=opt)

    train_data = np.array(data_norm)
    train_x = train_data[:, 0, :]
    train_y = train_data[:, 1, :]
    print(train_x.shape)
    model.fit(
        x=train_x,
        y=train_y,
        epochs=10,
        batch_size=200,
        verbose=1,
        validation_split=0.1
    )
    model.save("model.h5")


def do_test():
    data_norm = load_dataset(dataset_type="test")
    # 模型搭建
    model = build_model()
    model.load_weights("model.h5")
    # 准备测试数据
    test_data = np.array(data_norm)
    test_x = test_data[:, 0, :]
    test_y = test_data[:, 1, :]
    print(test_x.shape)
    # 生成预测序列
    pre_y = model.predict(test_x[:100])
    print(pre_y.shape)
    pre_y = np.argmax(pre_y, axis=-1)
    overlap_rate_avg = 0
    for i in range(0, len(test_x[0:100])):
        print("label " + str(i), test_y[i])
        print("pred " + str(i), pre_y[i])
        overlap_rate = np.sum(test_y[i] == pre_y[i]) / len(test_y[i])
        overlap_rate_avg += overlap_rate
        print("准确率:", overlap_rate)
    print("平均准确率:", overlap_rate_avg / i)


if __name__ == "__main__":
    # do_train()
    do_test()

View Code

需要提醒读者朋友注意的是,这里仅仅演示了基于分词后的语料进行命名实体识别的过程,从原始连续文本分词语料这一步同样是需要额外处理的。分词是分词,命名实体识别是命名实体识别。

参考链接:

https://github.com/xiaosongshine/NLP_NER_RNN_Keras/tree/master 
https://mp.weixin.qq.com/s/kMxdjdAZhgkAbbsExkdOOQ 

这里使用开源框架“Kashgari”完成该任务。

0x1:任务分析

序列标注任务是中文自然语言处理(NLP)领域在句子层面中的主要任务,在给定的文本序列上预测序列中需要作出标注的标签。常见的子任务有:

  • 命名实体识别(NER)
  • Chunk 提取
  • 词性标注(POS)等

在我们的NER任务重,需要预测的标签序列就是输入文本的实体标签序列。 

0x2:搭建环境和数据准备

安装相关依赖:

pip install kashgari==2.0.2
pip install tensorflow==2.2.0
pip install pandas==2.0.3
pip install keras==2.13.1
pip install tensorflow_addons==0.11.2
pip install torch==2.0.0
pip install rich==13.5.2

打印测试数据:

from kashgari.corpus import SMP2018ECDTCorpus
import kashgari
from kashgari.tasks.classification import BiLSTM_Model
import logging
logging.basicConfig(level='DEBUG')


if __name__ == "__main__":
    # Kashgari provides the basic intent-classification corpus for experiments. You could also use your corpus in any language for training.
    # Load build-in corpus.
    train_x, train_y = SMP2018ECDTCorpus.load_data('train')
    valid_x, valid_y = SMP2018ECDTCorpus.load_data('valid')
    test_x, test_y = SMP2018ECDTCorpus.load_data('test')
    print(train_x[0])
    print(train_y[0])

    # Or use your own corpus
    train_x = [['Hello', 'world'], ['Hello', 'Kashgari'], ['I', 'love', 'Beijing']]
    train_y = [['O', 'O'], ['O', 'B-PER'], ['O', 'B-LOC']]
    valid_x, valid_y = train_x, train_y
    test_x, test_x = train_x, train_y

    print(train_x[0])
    print(train_y[0])

0x3:其他可用于训练的相关数据集

数据集简要说明访问地址
位置、组织、人… 这是来自GMB语料库的摘录,用于训练分类器以预测命名实体,例如姓名,位置等。 kaggle
口语 NLPCC2018开放的任务型对话系统中的口语理解评测 NLPCC

参考链接:

https://www.kaggle.com/datasets/abhinavwalia95/entity-annotated-corpus?resource=download 
https://eliyar.biz/nlp_chinese_bert_ner/
https://github.com/BrikerMan/Kashgari 

0x1:LLM在NER任务上的研究进展 

命名实体识别(NER)是一种典型的序列标注任务,它将给定句子X = {x1, ..., xn} 中的每个单词x分配一个实体类型y∈Y,其中Y表示实体标签集合,n表示给定句子的长度。

大规模语言模型(LLMs)表现出令人印象深刻的在上下文学习方面的能力:只需少量特定任务的示例作为演示,LLMs就能为新的测试输入生成结果。在上下文学习框架下,LLMs在各种自然语言处理任务中取得了有希望的结果,包括机器翻译(MT),问答(QA)和命名实体抽取(NEE)。

尽管取得了进展,LLMs在NER任务上的性能仍远低于监督基准,这主要是因为如下几个原因:

  • 1、第一个问题是NER和LLMs之间固有的差距:NER本质上是一个序列标注任务,模型需要为句子中的每个标记分配一个实体类型标签,而LLMs是在文本生成任务下形式化的。语义标注任务与文本生成模型之间的差距导致了将LLMs应用于解决NER任务时性能较差。
  • 2、LLMs在NER任务中的另一个大问题是幻觉问题,即LLMs倾向于过于自信地将NULL输入标记为实体。

为了解决第一个问题,GPT-NER被提出,主要的解决方案思路如下:

GPT-NER将NER任务转化为一项可以被LLMs轻松适应的文本生成任务。具体而言,将在输入文本“Columbus is a city”中找到位置实体的任务转化为生成文本序列“@@Columbus## is a city”,其中特殊标记@@##表示实体。我们发现,与其他形式化方法相比,所提出的策略可以显著降低生成完全编码输入序列标签信息的文本的难度,因为模型只需标记实体的位置并为其余标记复制。实验证明,所提出的策略显著提高了性能。

为了解决第二个问题,提出了一种自我验证策略,该策略位于实体提取阶段之后,促使LLMs自问提取的实体是否属于已标记的实体标签。自我验证策略作为一种调节功能来抵消LLMs过度自信的效果,在解决幻觉问题方面非常有效,导致性能显著提升。

特别值得注意的是,GPT-NER在低资源和少样本NER设置中表现出令人印象深刻的熟练程度:当训练数据极为稀缺时,GPT-NER比监督模型表现显著更好。这说明了GPT-NER在实际的NER应用中的潜力,即使标记样本的数量很少。

0x2:GPT-NER原理

GPT-NER使用大型语言模型来解决NER任务。GPT-NER遵循基于上下文学习的一般范例,可以分解为三个步骤:

  • (1)提示构建:对于给定的输入句子X,我们为X构建一个提示(表示为Prompt(X)),few-shot prompt如下图所示。
  • (2)将构建的提示输入到大型语言模型(LLM)中,以获得生成的文本序列W = {w1, ..., wn}。对于这个文件序列W的格式有两点约束要求:(i) 很容易被转换成实体标签序列;(ii) 很容易被LLM以高准确率的方式生成,即要考虑到LLM文本序列生成任务的难度。
  • (3)将文本序列W转换为实体标签序列,从而获得最终结果。

The example of the prompt of GPT-NER. Suppose that we need to recognize location entities for the given sentence: China says Taiwan spoils atmosphere for talks. The prompt consists of three parts:

  • (1) Task Description: It’s surrounded by a red rectangle, and instructs the GPT-3 model that the current task is to recognize Location entities using linguistic knowledge.

  • (2) Few-shot Demonstrations: It’s surrounded by a yellow rectangle giving the GPT-3 model few-shot examples for reference.

  • (3) Input Sentence: It’s surrounded by a blue rectangle indicating the input sentence, and the output of the GPT-3 model is colored green.

尽管基于上下文学习范例是直观的,但NER任务并不容易适应它,因为NER任务本质上是一个序列标注任务,而不是生成任务,上述方法存在如下几点问题:

  • 采用few-shot prompt的方法,每次次只能提取一种实体,因为要提取出所有的实体,必须遍历实体列表,但是LLM对提示的长度有一个硬性的限制(例如GPT-3的4096个tokens)。 

1、怎么在few-shot prompt中提供实例样本?

few-shot prompt提示构建这一步的目的主要有两个:

  • 规范LLM输出的格式
  • 给LLM提升生成输出的辅助证据

格式问题容易解决,这里主要需要解决的是实例样本质量的问题,我们需要按照一定的策略给LLM提供合适的实例样本,以提高LLM生成输出的质量。

1)Random Retrieval

最直接的策略是从训练集中随机选择k个示例。明显的缺点是:无法保证检索到的示例在语义上与输入接近。

2)kNN-based Retrieval 

我们可以从训练集中检索输入序列的k个最近邻(kNN):我们首先计算所有训练示例的向量化表示,然后根据这些向量化表示获取输入测试序列的k个最近邻,并用这k个最近邻作为few-shot prompt中的实例样本。

向量化表示有两种可选的方法,

  • kNN based on Sentence-level Representations

要在训练集中找到kNN示例,一种直观的方法是使用文本相似性模型,例如SimCSE:我们首先为训练示例和输入序列获取句子级表示,然后使用余弦相似度找到kNN。

基于句子级表示的kNN的缺点是显而易见的:NER是一个基于标记级别的任务,更关注局部证据,而不是句子级任务。很容易遇到一个问题,即检索到的句子(例如,他是一名士兵)与输入(例如,John是一名士兵)在句子级别上语义上相似,但对于标记输入不提供任何参考帮助。在上面的例子中,检索到的句子不包含NER,因此无法对标记输入提供证据。

  • Entity-level Embedding

为了解决上述问题,我们需要基于标记级别的表示而不是句子级别的表示来检索kNN示例。

我们首先使用经过微调的NER标记模型从所有训练示例的所有标记中提取实体级表示作为数据存储。对于长度为N的给定输入序列,我们首先迭代遍历序列中的所有标记,为每个标记找到kNN,得到K×N个检索到的标记。接下来,我们从K×N个检索到的标记中选择前k个标记,并使用它们关联的句子作为示范。

整个过程如下图所示,

假设我们需要为输入句子“Obama lives in Washington”检索少样本演示,其中定义了LOC实体。

  • 步骤1:数据存储构建:我们首先使用经过微调的NER模型提取训练集中每个句子的实体,并将它们形成(键,值)对,其中键是提取的实体,值是对应的句子。然后,我们将所有形成的(键,值)对连接起来构建数据存储。
  • 步骤2:表示提取:首先,利用经过微调的NER模型将输入句子嵌入为一系列高维向量。然后,根据softmax层将嵌入的高维向量分类为标签,其中“Obama”和“Washington”是两个识别的实体。
  • 步骤3:kNN搜索:使用提取的LOC实体“Washington”的嵌入作为查询,在数据存储中找到k个最近邻居,并将检索到的句子视为k个少样本演示。

2、怎么对LLM生成的标签序列预测结果进行自我验证?

LLMs显著受到幻觉或过度预测问题的影响。特别是对于命名实体识别(NER)而言,LLMs倾向于过度自信地将空输入标记为实体,即使有示范存在。

以下是一个过度预测的例子: 

Prompt:
I am an excellent linguist. The task is to labellocation entities in the given sentence.
Below are some examples.
Input:Columbus is a city
Output:@@Columbus## is a city
Input:Rare Hendrix song sells for $17
Output:

GPT-3 Output:
Rare @@Hendrix## song sells for $17

在GPT-3中,“Hendrix”被识别为一个位置实体,这显然是不正确的。

为了解决这个问题,我们提出了自我验证策略。给定LLMs提取的实体,我们要求LLM进一步验证提取的实体是否正确,回答是或否。

我们构建了自我验证的提示,如下图所示。

以提取位置实体为例,提示从任务描述开始:“任务是验证给定句子中的单词是否是从位置实体中提取出来的”。

按照上述生成过程同样的样例选择策略,我们需要少量示范来提高自我验证器的准确性。如上图中的黄色矩形所示,每个示范包含三行:

  • (1) “输入句子:Only France and Britain backed Fischler's proposal”。
  • (2) “在输入句子中,单词"France"是一个位置实体吗?请用是或否回答”。
  • (3) 是。

在少量示范设置中,我们将多个示范放入提示中。示范后面是测试示例,然后将其输入LLM以获取输出。

0x3:实验结果

  • 模型:GPT-3 (Brown et al., 2020) (davinci-003)
  • 最大长度:512
  • 温度:0
  • top_p:1
  • frequency_penalty:0
  • presence_penalty:0
  • best_of:1

Results of sampled 100 pieces of data for two Flat NER datasets: CoNLL2003 and OntoNotes5.0.

Results of full data for two Flat NER datasets: CoNLL2003 and OntoNotes5.0.

0x4:消融研究

1、修改LLM输出格式

在之前的讨论中,我们使用了“@@Columbus## is a city”这种格式作为LLM的输出格式。我们尝试更改一下LLM的输出格式,以此评估不同输出格式下的预测效果。

1)BMES

直接输出开头,中间,结束,以及每个标记的单例指示符。

Input:White House is in Washington
Output:B-ORG E-ORG O O O

2)Entity+Position

要求LLM输出实体,及其实体在句子中的位置。

Input:White House is in Washington
Output:White House (0)

三种输出格式的评测分数如下:

  • ##@@策略:92.68
  • BMES策略:29.75
  • Entity+Position策略:38.73

其中,BMES和Entity+Position,明显表现不佳。

一种可能的解释如下:

  • 对于 BMES策略,LLM需要学习每个输入单词与每个BMES标签之间的对应关系:White对应B-ORG,House对应E-ORG,is对应O,in对应O,Washington对应O。通过分析错误样本,我们发现,对于LLM来说,甚至很难输出一个长度正确的BMES字符串,特别是当输入句子很长时,导致最终评估表现不佳。
  • 对于Entity+Position策略,我们发现LLM通常会混淆位置索引的含义(例如,它是字符索引还是单词索引),导致实体位置不正确。这个问题可以通过演示部分缓解,但考虑到GPT-3的4096个标记限制,问题仍然存在。不正确的位置索引使得很难将输出文本映射到序列标记评估格式,导致最终评估表现不佳。

2、Few-shot示例数量

我们进行实验来估计示例数量的影响。实验是在100个样本的CoNLL 2003数据集上进行的。结果如下图所示。

Comparisons by varying k-shot demonstrations.

我们可以观察到,随着k的增加,所有三种基于LLM的结果都在不断上升。当我们接近4096个示例的令牌限制时,结果仍未达到平稳状态。这意味着如果允许更多的示例,性能仍将提高。

有一个有趣的现象观察到,当示例数量较少时,即k = 2、4时,基于kNN的策略表现不如随机检索策略。一种可能的解释如下:基于kNN的检索倾向于选择与输入句子非常相似的演示。因此,如果输入句子不包含任何实体,检索到的示例很可能也不包含实体。在这种情况下,示例不包含我们希望强制执行的输出格式信息,导致LLM输出任意格式。

下面是一个例子: 当示例数量较少且GPT需要识别某种特定类型的实体(例如位置),但少量的示例句子都没有命名实体识别时,GPT会感到困惑,并以自己的格式输出,如下例所示:

Prompt:
I am an excellent linguist. The task is to label
organization entities in the given sentence. Below
are some examples.
Input:Korean pro-soccer games
Output:Korean pro-soccer games
Input:Australia defend the Ashes
Output:Australia defend the Ashes
Input:Japan get lucky win
Output:

GPT-3 Output:
Japan [Organization Entity] get lucky win 

参考链接:

https://arxiv.org/pdf/2304.10428v1.pdf
https://zhuanlan.zhihu.com/p/623739638
https://github.com/ShuheWang1998/GPT-NER 

文章来源: https://www.cnblogs.com/LittleHann/p/17675662.html
如有侵权请联系:admin#unsafe.sh