命名实体识别(Named Entity Recognition,NER),又称作“专名识别”,是指识别文本中具有特定意义的实体,主要包括:
NER是:
应用领域的重要基础工具,在自然语言处理技术走向实用化的过程中占有重要的地位。
例如在搜索场景下,NER是深度查询理解(Deep Query Understanding,简称 DQU)的底层基础信号,主要应用于搜索召回、用户意图识别、实体链接等环节,NER信号的质量,直接影响到用户的搜索体验,是NLP中一项非常基础的任务。
这里针对搜索召回稍微展开一些细节。
在O2O搜索中,对商家POI的描述是商家名称、地址、品类等多个互相之间相关性并不高的文本域。如果对O2O搜索引擎也采用全部文本域命中求交的方式,就可能会产生大量的误召回。一种解决方法如下图所示,
即让特定的查询只在特定的文本域做倒排检索,我们称之为“结构化召回”,可保证召回商家的强相关性。
举例来说,对于“海底捞”这样的请求,有些商家地址会描述为“海底捞附近几百米”,若采用全文本域检索这些商家就会被召回,显然这并不是用户想要的。而结构化召回基于NER将“海底捞”识别为商家,然后只在商家名相关文本域检索,从而只召回海底捞品牌商家,精准地满足了用户需求。
要了解NER是一回什么事,首先要先说清楚,什么是实体。简单的理解,实体,可以认为是某一个概念的实例。
例如,
所谓实体识别,就是将你想要获取到的实体类型,从一句话里面挑出来的过程。
小明 在 北京大学 的 燕园 看了
PER ORG LOC
中国男篮 的一场比赛
ORG
如上面的例子所示,句子“小明在北京大学的燕园看了中国男篮 的一场比赛”,通过NER模型,将“小明 ”以PER,“北京大学”以ORG,“燕园”以LOC,“中国男篮”以ORG为类别分别挑了出来。
NER是一种序列标注问题,因此他们的数据标注方式也遵照序列标注问题的方式,主要是以下几种方法:
先列出来BIOES分别代表什么意思:
将“小明在北京大学的燕园看了中国男篮的一场比赛”这句话,进行标注,结果就是:
[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的过程,就是根据输入的句子,预测出其标注序列的过程。
例子:B/I-XXX,其中:
方法 | 代表技术 | 核心思想 |
基于规则方法 | 字典、规则 | 关注规则 |
基于机器学习方法 | HMM、HEMM、ME、SVM、CRF | 关注概率 |
基于深度学习方法 | BiLSTM-CNN-CRF、BERT-BiLSTM-CRF | 关注整体效果 |
基于大模型方法 | 注意力模型、迁移学习、GPT-3.5、Llama | 关注整体效果、性能 |
基本思想是匹配规则,
在很多搜索场景实体识别的工业实践中,整体技术选型往往是“实体词典匹配+模型预测”的框架,如下图所示,
实体词典匹配,通俗来说就是”规则匹配“,这种算法主要有如下优点:
既然词典匹配有这么多优点,那为什么还需要模型预测呢?答案主要有两方面原因:
HMM的相关细节可以参阅这篇文章。
CRF的相关细节可以参阅这篇文章。
抽象来说,基于深度学习进行NER任务可以分为如下两个步骤:
采用LSTM作为特征抽取器,再接一个CRF层来作为输出层。
CNN虽然在长序列的特征提取上有弱势,但是CNN模型可有并行能力,有运算速度快的优势。膨胀卷积的引入,使得CNN在NER任务中,能够兼顾运算速度和长序列的特征提取。
BERT中蕴含了大量的通用知识,利用预训练好的BERT模型,再用少量的标注数据进行FINETUNE是一种快速的获得效果不错的NER的方法。
参考链接:
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地址 |
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/
近年来,随着硬件计算能力的发展以及词的分布式表示(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层自动从训练数据中学到。
约束可能是:
有了这些约束条件,无效的预测标签序列将急剧减少。
项目使用了conll2003_v2数据集,其中标注的命名实体共计九类:
实现了将输入识别为命名实体的模型,如下所示:
# input ['EU', 'rejects', 'German', 'call', 'to', 'boycott', 'British', 'lamb', '.'] # output ['B-ORG', 'O', 'B-MISC', 'O', 'O', 'O', 'B-MISC', 'O', 'O']
数据下载并解压,以供训练,下载解压后可以看到三个文件:
打开后可以看到,数据格式如下:
我们只需要每行开头和最后一个数据,他们分别是文本信息和命名实体。
我们需要将数据进行处理,使之成为网络能接收的形式。
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
模型结构:
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 _________________________________________________________________
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”完成该任务。
序列标注任务是中文自然语言处理(NLP)领域在句子层面中的主要任务,在给定的文本序列上预测序列中需要作出标注的标签。常见的子任务有:
在我们的NER任务重,需要预测的标签序列就是输入文本的实体标签序列。
安装相关依赖:
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])
数据集 | 简要说明 | 访问地址 |
---|---|---|
位置、组织、人… | 这是来自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
命名实体识别(NER)是一种典型的序列标注任务,它将给定句子X = {x1, ..., xn} 中的每个单词x分配一个实体类型y∈Y,其中Y表示实体标签集合,n表示给定句子的长度。
大规模语言模型(LLMs)表现出令人印象深刻的在上下文学习方面的能力:只需少量特定任务的示例作为演示,LLMs就能为新的测试输入生成结果。在上下文学习框架下,LLMs在各种自然语言处理任务中取得了有希望的结果,包括机器翻译(MT),问答(QA)和命名实体抽取(NEE)。
尽管取得了进展,LLMs在NER任务上的性能仍远低于监督基准,这主要是因为如下几个原因:
为了解决第一个问题,GPT-NER被提出,主要的解决方案思路如下:
GPT-NER将NER任务转化为一项可以被LLMs轻松适应的文本生成任务。具体而言,将在输入文本“Columbus is a city”中找到位置实体的任务转化为生成文本序列“@@Columbus## is a city”,其中特殊标记@@##表示实体。我们发现,与其他形式化方法相比,所提出的策略可以显著降低生成完全编码输入序列标签信息的文本的难度,因为模型只需标记实体的位置并为其余标记复制。实验证明,所提出的策略显著提高了性能。
为了解决第二个问题,提出了一种自我验证策略,该策略位于实体提取阶段之后,促使LLMs自问提取的实体是否属于已标记的实体标签。自我验证策略作为一种调节功能来抵消LLMs过度自信的效果,在解决幻觉问题方面非常有效,导致性能显著提升。
特别值得注意的是,GPT-NER在低资源和少样本NER设置中表现出令人印象深刻的熟练程度:当训练数据极为稀缺时,GPT-NER比监督模型表现显著更好。这说明了GPT-NER在实际的NER应用中的潜力,即使标记样本的数量很少。
GPT-NER使用大型语言模型来解决NER任务。GPT-NER遵循基于上下文学习的一般范例,可以分解为三个步骤:
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提供合适的实例样本,以提高LLM生成输出的质量。
最直接的策略是从训练集中随机选择k个示例。明显的缺点是:无法保证检索到的示例在语义上与输入接近。
我们可以从训练集中检索输入序列的k个最近邻(kNN):我们首先计算所有训练示例的向量化表示,然后根据这些向量化表示获取输入测试序列的k个最近邻,并用这k个最近邻作为few-shot prompt中的实例样本。
向量化表示有两种可选的方法,
要在训练集中找到kNN示例,一种直观的方法是使用文本相似性模型,例如SimCSE:我们首先为训练示例和输入序列获取句子级表示,然后使用余弦相似度找到kNN。
基于句子级表示的kNN的缺点是显而易见的:NER是一个基于标记级别的任务,更关注局部证据,而不是句子级任务。很容易遇到一个问题,即检索到的句子(例如,他是一名士兵)与输入(例如,John是一名士兵)在句子级别上语义上相似,但对于标记输入不提供任何参考帮助。在上面的例子中,检索到的句子不包含NER,因此无法对标记输入提供证据。
为了解决上述问题,我们需要基于标记级别的表示而不是句子级别的表示来检索kNN示例。
我们首先使用经过微调的NER标记模型从所有训练示例的所有标记中提取实体级表示作为数据存储。对于长度为N的给定输入序列,我们首先迭代遍历序列中的所有标记,为每个标记找到kNN,得到K×N个检索到的标记。接下来,我们从K×N个检索到的标记中选择前k个标记,并使用它们关联的句子作为示范。
整个过程如下图所示,
假设我们需要为输入句子“Obama lives in Washington”检索少样本演示,其中定义了LOC实体。
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进一步验证提取的实体是否正确,回答是或否。
我们构建了自我验证的提示,如下图所示。
以提取位置实体为例,提示从任务描述开始:“任务是验证给定句子中的单词是否是从位置实体中提取出来的”。
按照上述生成过程同样的样例选择策略,我们需要少量示范来提高自我验证器的准确性。如上图中的黄色矩形所示,每个示范包含三行:
在少量示范设置中,我们将多个示范放入提示中。示范后面是测试示例,然后将其输入LLM以获取输出。
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.
在之前的讨论中,我们使用了“@@Columbus## is a city”这种格式作为LLM的输出格式。我们尝试更改一下LLM的输出格式,以此评估不同输出格式下的预测效果。
直接输出开头,中间,结束,以及每个标记的单例指示符。
Input:White House is in Washington Output:B-ORG E-ORG O O O
要求LLM输出实体,及其实体在句子中的位置。
Input:White House is in Washington Output:White House (0)
三种输出格式的评测分数如下:
其中,BMES和Entity+Position,明显表现不佳。
一种可能的解释如下:
我们进行实验来估计示例数量的影响。实验是在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