Skip to content

NER 说明文档

冬日新雨 edited this page Jul 8, 2020 · 15 revisions

前言

  • 该工具包是针对基于模型的 NER 任务,辅助性工具包,旨在加快模型开发,加速模型的并行预测效率。
  • 该工具包中,对 NER 模型的输入、输出数据做了规范范式,数据格式有两种,一种是易于理解的 entity 格式,另外一种是方面模型接收的 tag 格式,同时,模型一般接收的数据分为字符(char)级别和词汇(word)级别作为输入 token,样例如下:
>>> # 字 token
>>> text = ['胡', '静', '静', '在', '水', '利', '局', '工', '作', '。']
>>> entity = [{'text': '胡静静', 'offset': [0, 3], 'type': 'Person'},
              {'text': '水利局', 'offset': [4, 7], 'type': 'Orgnization'}]
>>> tag = ['B-Person', 'I-Person', 'E-Person', 'O', 'B-Orgnization',
           'I-Orgnization', 'E-Orgnization', 'O', 'O', 'O']

>>> # 词 token
>>> text = ['胡静静', '在', '重庆', '水利局', '人事科', '工作', '。']
>>> entity = [{'text': '胡静静', 'offset': [0, 1], 'type': 'Person'},
              {'text': '水利局', 'offset': [2, 5], 'type': 'Orgnization'}]
>>> tag = ['S-Person', 'O', 'B-Orgnization', 'I-Orgnization', 'E-Orgnization', 'O', 'O']
  • 本工具包中的所有 NER 数据处理均基于以上两种数据格式、两个 token 级别的转换。
  • tag 标签的标注标准是 B(Begin)I(Inside)O(Others)E(End)S(Single) 格式。

entity 转 tag

entity2tag

给定文本以及其 entity,返回其对应的 tag 格式标签。

>>> import jionlp as jio
>>> token_list = '胡静静在水利局工作。'  # 字级别
>>> token_list = ['胡', '静', '静', '在', '水', 
                  '利', '局', '工', '作', '。']  # 字或词级别
>>> ner_entities =                 
        [{'text': '胡静静', 'offset': [0, 3], 'type': 'Person'},
         {'text': '水利局', 'offset': [4, 7], 'type': 'Orgnization'}]
>>> print(jio.ner.entity2tag(token_list, ner_entities))

# ['B-Person', 'I-Person', 'E-Person', 'O', 'B-Orgnization',
#  'I-Orgnization', 'E-Orgnization', 'O', 'O', 'O']
  • 不支持批量处理,仅支持单条数据处理。

tag 转 entity

tag2entity

给定文本以及其 tag,返回其对应的 entity 格式标签。

>>> import jionlp as jio
>>> token_list = '胡静静在水利局工作。'  # 字级别
>>> token_list = ['胡', '静', '静', '在', '水', 
                  '利', '局', '工', '作', '。']  # 字或词级别
>>> tags = ['B-Person', 'I-Person', 'E-Person', 'O', 'B-Orgnization',
            'I-Orgnization', 'E-Orgnization', 'O', 'O', 'O']
>>> print(jio.ner.tag2entity(token_list, tags))
        
# [{'text': '胡静静', 'offset': [0, 3], 'type': 'Person'},
#  {'text': '水利局', 'offset': [4, 7], 'type': 'Orgnization'}]]
  • 不支持批量处理,仅支持单条数据处理。
  • 该步骤在实施时,一般在模型预测(predict、inference)阶段完成后,BIOES 标注标准下,实体的范式为 S|B(nI)E,因此,必定会存在不满足范式的 tag,造成实体无法识别,因此,函数中提供了参数 verbose(bool),指示是否打印无法转为 entity 的 tag。

字 token 转词 token

char2word

给定字符级别的 token 文本以及其 entity,返回其对应的词汇级别的 entity。

>>> import jionlp as jio
>>> char_token_list = '胡静静喜欢江西红叶建筑公司'  # 字级别
>>> char_token_list = [
        '胡', '静', '静', '喜', '欢', '江', '西',
        '红', '叶', '建', '筑', '公', '司']  # 字或词级别
>>> char_entity_list = [
        {'text': '胡静静', 'offset': [0, 3], 'type': 'Person'},
        {'text': '江西红叶建筑公司', 'offset': [5, 13], 'type': 'Company'}]
>>> word_token_list = ['胡静静', '喜欢', '江西', '红叶', '建筑', '公司']
>>> print(jio.ner.char2word(char_entity_list, word_token_list))

# [{'text': '胡静静', 'offset': [0, 1], 'type': 'Person'},
#  {'text': '江西红叶建筑公司', 'offset': [2, 6], 'type': 'Company'}]
  • 不支持批量处理,仅支持单条数据处理。
  • 所有转换在 entity 格式基础上。
  • 由于分词器的词汇边界可能和标注实体的边界不一致造成偏差,会导致一部分实体无法转换为词级别,称为分词偏差。因此,提供参数 verbose(bool) 打印无法转换的实体。
  • 分词偏差,根据经验,jieba 分词器错误率在 4.62%,而 pkuseg 分词器错误率在 3.44%。

词 token 转字 token

word2char

给定词汇级别的 token 文本以及其 entity,返回其对应的字符级别的 entity。

>>> import jionlp as jio
>>> word_entity_list = [
        {'type': 'Person', 'offset': [0, 1], 'text': '胡静静'},
        {'type': 'Company', 'offset': [2, 6], 'text': '江西红叶建筑公司'}]
>>> word_token_list = ['胡静静', '喜欢', '江西', '红叶', '建筑', '公司']
>>> print(jio.ner.word2char(word_entity_list, word_token_list))

# [{'text': '胡静静', 'offset': [0, 3], 'type': 'Person'},
#  {'text': '江西红叶建筑公司', 'offset': [5, 13], 'type': 'Company'}]
  • 不支持批量处理,仅支持单条数据处理。
  • 所有转换在 entity 格式基础上。

基于词典 NER

LexiconNER

给定各个类别的实体数据,采用基于 Trie 树的前向最大匹配方法,匹配找出文本中的实体。

>>> import jionlp as jio
>>> entity_dicts = {
        'Person': ['张大山', '岳灵珊', '岳不群'],
        'Organization': ['成都市第一人民医院', '四川省水利局']}
>>> lexicon_ner = jio.ner.LexiconNER(entity_dicts)
>>> text = '岳灵珊在四川省水利局上班。'
>>> result = lexicon_ner(text)
>>> print(result)

# [{'type': 'Person', 'text': '岳灵珊', 'offset': [0, 3]},
#  {'type': 'Organization', 'text': '四川省水利局', 'offset': [4, 10]}]
  • 实体词典如上 entity_dicts 所示。
  • 所有匹配找出的实体均为 entity 格式。
  • 构建 trie 树时,当同一个实体出现在多个类别中,则打印警告信息,仅默认选取其中一个类别进行构建。

NER 模型预测加速

TokenSplitSentence、TokenBreakLongSentence、TokenBatchBucket

NER 模型在训练好后,并行预测阶段,可以有若干种方法对模型进行加速,提高并行处理能力。具体使用方法如下:

>>> import jionlp as jio
>>> # 1、并用样例:
>>> text_list = [list(line) for line in text_list]

>>> def func(token_lists, para=1):
>>> ... token_lists = [['S-' + chr(ord(token) + para) for token in token_list]
>>> ...                for token_list in token_lists]
>>> ... return token_lists

>>> max_sen_len = 70
>>> token_batch_obj = jio.ner.TokenBatchBucket(func, max_sen_len=max_sen_len, batch_size=30)
>>> token_break_obj = jio.ner.TokenBreakLongSentence(token_batch_obj, max_sen_len=max_sen_len)
>>> token_split_obj = jio.ner.TokenSplitSentence(token_break_obj, max_sen_len=max_sen_len, combine_sentences=True)

>>> res = token_split_obj(text_list, para=1)  # 补充 func 函数的参数

>>> # 其中,三个工具的 max_sen_len 必须保持一致。

>>> # 2、分用样例:
>>> # 允许 TokenSplitSentence, TokenBreakLongSentence 两者结合

>>> token_break_obj = jio.ner.TokenBreakLongSentence(token_batch_obj, max_sen_len=max_sen_len)
>>> token_split_obj = jio.ner.TokenSplitSentence(token_break_obj, max_sen_len=max_sen_len, combine_sentences=True)

>>> res = token_break_obj(text_list, para=1)  # 补充 func 函数的参数
  • 原理说明:
    • 1、将短句进行拼接,至接近最大序列长度。 一般 NER 模型在输入模型前,须首先进行分句处理。但一般较短的句子,其上下文依赖 少,不利于挖掘上下文信息;另一方面,需要大量的 pad 操作,限制了模型效率。因此 须将较短的句子逐一拼接,至接近模型允许的序列最大长度。该方法主要由 TokenSplitSentence 实现。
    • 2、将超长句子进行重叠拆解,并使用规则对其进行合并。 输入的文本有一部分,长度超过模型允许序列最大长度,且无标点符号。这类句子一旦 直接应用模型,一方面造成模型并行性能急剧下降,另一方面其模型效果也会下降。因此 须将超长句子进行重叠拆分,然后再次利用规则合并,达到高速并行的效果。该方法已申 请专利。由 TokenBreakLongSentence 实现。
    • 3、将相近长度的句子拼接入一个 batch,提升模型的并行能力。 在 tensorflow 等框架中,动态处理 LSTM 等 RNN 序列,会以最长的序列为基准进行 计算。因此,若句子长度均相近,长句和长句放入一个 batch,短句和短句放入一个 batch,则会减少 pad 数量,提升模型的并行能力。由 TokenBatchBucket 实现。
  • 其中,样例中的func指,输入模型规定最长序列max_sen_len的一个batch_size 的句子序列集,输出其对应的 tag 标签。

比较 NER 标注实体与模型预测实体之间的差异

entity_compare

NER 的标注数据,经过模型训练后,预测得到的实体结果,两者之间存在差异,该方法提供了比较两者之间差异的功能。

>>> import jionlp as jio
>>> text = '张三在西藏拉萨游玩!之后去新疆。'
>>> labeled_entities = [
        {'text': '张三', 'offset': [0, 2], 'type': 'Person'},
        {'text': '西藏拉萨', 'offset': [3, 7], 'type': 'Location'}]
>>> predicted_entities = [
        {'text': '张三在', 'offset': [0, 3], 'type': 'Person'},
        {'text': '西藏拉萨', 'offset': [3, 7], 'type': 'Location'},
        {'text': '新疆', 'offset': [13, 15], 'type': 'Location'}]
>>> res = jio.ner.entity_compare(
        text, labeled_entities, predicted_entitiescontent_pad=1)
>>> print(res)

# [
#     {'context': '张三在西', 
#      'labeled_entity': {'text': '张三', 'offset': [0, 2], 'type': 'Person'},
#      'predicted_entity': {'text': '张三在', 'offset': [0, 3], 'type': 'Person'}},
#     {'context': '去新疆。',
#      'labeled_entity': None,
#      'predicted_entity': {'text': '新疆', 'offset': [13, 15], 'type': 'Location'}}
# ]
  • 针对标注语料的实体,以及模型训练后预测得到的实体,往往存在不一致,找出这些标注不一致的数据,能够有效分析模型的预测能力,找出 bad case。
  • 提供了参数context_pad(int),用于给出不一致实体对的上下文信息。
  • 输入的文本和实体,必须以字符级别为基准,不可以用词汇级别为基准。