Skip to content

NER 说明文档

冬日新雨 edited this page Oct 25, 2021 · 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_entitiescontext_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),用于给出不一致实体对的上下文信息。
  • 输入的文本和实体,必须以字符级别为基准,不可以用词汇级别为基准。

分割数据集

analyse_dataset

针对 NER 的标注数据,分割其为训练、验证、测试集,并给出各个实体类型的统计信息,计算各子集的分布与全数据集分布的相对熵。

>>> import jionlp as jio
>>> dataset_x = ['马成宇在...', 
                 '金融国力教育公司...', 
                 '延平区人民法院曾经...',
                 ...]
>>> dataset_y = [[{'type': 'Person', 'text': '马成宇', 'offset': (0, 3)}],
                 [{'type': 'Company', 'text': '国力教育公司', 'offset': (2, 8)}],
                 [{'type': 'Organization', 'text': '延平区人民法院', 'offset': (0, 7)}],
                 ...]
>>> train_x, train_y, valid_x, valid_y, test_x, test_y, stats = \
    ... jio.ner.analyse_dataset(dataset_x, dataset_y)
>>> print(stats)

    whole dataset:
    Company                    573        39.68%
    Person                     495        34.28%
    Organization               376        26.04%
    total                    3,000        100.00%

    train dataset: 80.00%
    Company                    464        40.38%
    Person                     379        32.99%
    Organization               306        26.63%
    total                    2,400        100.00%

    valid dataset: 5.00%
    Person                      32        47.06%
    Company                     22        32.35%
    Organization                14        20.59%
    total                      150        100.00%

    test dataset: 15.00%
    Company                     87        38.33%
    Person                      84        37.00%
    Organization                56        24.67%
    total                      450        100.00%

    train KL divergence: 0.000546, info dismatch: 0.03%
    valid KL divergence: 0.048423, info dismatch: 3.10%
    test KL divergence: 0.002364, info dismatch: 0.15%
  • info dismatch 信息,百分比越小,说明数据子集类别分布越合理。

实体收集

collect_dataset_entities

将一个实体识别标注数据集(也包括其它同类型的序列标注任务,如主体抽取、要素抽取等)中的所有实体进行收集。

>>> import json
>>> import jionlp as jio
>>> dataset_y = [[{'type': 'Person', 'text': '马成宇', 'offset': (0, 3)},
                  {'type': 'Company', 'text': '百度', 'offset': (10, 12)},
                  {'type': 'Company', 'text': '百度', 'offset': (20, 22)}],
                 [{'type': 'Company', 'text': '国力教育公司', 'offset': (2, 8)}],
                 [{'type': 'Organization', 'text': '延平区人民法院', 'offset': (0, 7)},
                  {'type': 'Company', 'text': '百度', 'offset': (10, 12)},
                  {'type': 'Company', 'text': '百度', 'offset': (20, 22)}]]
>>> res = jio.ner.collect_dataset_entities(dataset_y)
>>> print(json.dumps(res, ensure_ascii=False, indent=4, separators=(',', ':')))

# {
#     "Person":{
#         "马成宇":1
#     },
#     "Company":{
#         "百度":4,
#         "国力教育公司":1
#     },
#     "Organization":{
#         "延平区人民法院":1
#     }
# }
  • 其中包括每个类型实体的频次,有可能同一实体,具有不同的类型,如“金华”既存在于人名,又存在于地名类型中。

时间实体抽取

extract_time

给定一篇文本,从中抽取出其中的时间实体(不依赖模型)。

>>> import time
>>> import json
>>> import jionlp as jio
>>> text = '''8月临近尾声,中秋、国庆两个假期已在眼前。2021年中秋节是9月21日,星期二。
              有不少小伙伴翻看放假安排后,发现中秋节前要"补"假。
              记者注意到,根据放假安排,9月18日(星期六)上班,9月19日至21日放假调休,也就是从周日开始放假3天。
              由于中秋节后上班不到 10天,又将迎来一个黄金周—国庆长假,因此工作也就"安排"上了。
              双节来袭,仍有人要坚守岗位。'''
>>> res = jio.ner.extract_time(text, time_base=time.time() with_parsing=False)
>>> print(json.dumps(res, ensure_ascii=False, indent=4, separators=(',', ':')))

# {'text': '8月', 'offset': [41, 43], 'type': 'time_point'}
# {'text': '中秋', 'offset': [48, 50], 'type': 'time_point'}
# {'text': '国庆', 'offset': [51, 53], 'type': 'time_point'}
# {'text': '2021年中秋节', 'offset': [62, 70], 'type': 'time_point'}
# {'text': '9月21日', 'offset': [71, 76], 'type': 'time_point'}
# {'text': '星期二', 'offset': [77, 80], 'type': 'time_point'}
# {'text': '中秋节前', 'offset': [98, 102], 'type': 'time_span'}
# {'text': '9月18日', 'offset': [136, 141], 'type': 'time_point'}
# {'text': '星期六', 'offset': [142, 145], 'type': 'time_point'}
# {'text': '9月19日至21日', 'offset': [149, 158], 'type': 'time_span'}
  • 返回结果与 ner 格式相同,其中,type类型指示了该实体的具体类型,包括时间点、时间范围、时间段,时间周期四种类型。
  • 参数time_base用于时间解析时提供时间基,而with_parsing(bool)用于指示返回结果是否返回解析信息。

货币金额实体抽取

extract_money

给定一篇文本,从中抽取出其中的货币金额实体(不依赖模型)。

>>> import time
>>> import json
>>> import jionlp as jio
>>> text = '张三赔偿李大花人民币车费601,293.11元,工厂费大约一万二千三百四十五元,利息9佰日元,打印费十块钱。'
>>> res = jio.ner.extract_money(text, with_parsing=False)
>>> print(json.dumps(res, ensure_ascii=False, indent=4, separators=(',', ':')))

# [{'text': '601,293.11元', 'offset': [12, 23], 'type': 'money'},
#  {'text': '大约一万二千三百四十五元', 'offset': [27, 39], 'type': 'money'},
#  {'text': '9佰日元', 'offset': [42, 46], 'type': 'money'},
#  {'text': '人民币十块钱', 'offset': [50, 56], 'type': 'money'}]
  • 返回结果与 ner 格式相同,其中,type类型固定为money
  • 参数with_parsing(bool)用于指示返回结果是否返回解析信息。
  • ret_all(bool): 某些货币金额表达,在大多数情况下并非表达货币金额,如 “几分” 之于 “他有几分不友善”,默认按绝大概率处理, 即不返回此类伪货币金额表达,该参数默认为 False;若希望返回所有抽取到的货币金额表达,须将该参数置 True。
  • 该工具配合 jio.parse_moneyparse_money说明使用。