logo

NLP-命名实体识别

王哲峰 / 2022-04-05


目录

命名实体识别介绍

命名实体识别(NER, Named Entity Recognition)与自动分词、词性标注一样, 命名实体识别也是自然语言处理的一个基础任务, 是信息抽取、信息检索、机器翻译、 问答系统等多种自然语言处理技术必不可少的组成部分

命名实体识别的目的是识别语料中人名、地名、组织机构名等命名实体。由于这些命名实体数量不断增加, 通常不可能在词典中穷尽列出, 且其构成方法具有各自的规律性, 因此, 通常把对这些词的识别在词汇形态处理(如汉语切分)任务中独立处理, 称为命名实体识别(Named Entities Recognition, NER)。

命名实体识别研究的命名实体一般分为:

由于数量、时间、日期、货币等实体识别通常可以采用 模式匹配 的方式获得较好的识别效果, 相比之下人名、地名、机构名较复杂, 因此近年来的研究主要以这几种实体为主。

命名实体识别效果的评判主要看:

中文命名实体识别:

命名实体识别也划分为三种方法:

命名实体识别中目前的主流方法

基于条件随机场的命名实体识别

HMM 的局限性

在 HMM 中, 将分词作为字标注问题来解决, 其中有两条非常经典的独立性假设:

通过这两条假设, 使得 HMM 的计算成为可能, 模型的计算也简单许多。

但多数场景下, 尤其在大量真实语料中, 观察序列更多的是以一种多重的交互特征形式表现出来, 观察元素之间广泛存在的长程相关性, 这样 HMM 的效果就受到了制约。

CRF

基于 HMM, 20001 年, Lafferty 等学者提出了条件随机场, 其主要思想来源于 HMM, 也是一种用来标记和切分序列化数据的统计模型。不同于 HMM 的是, 条件随机场是在给定观察的标记序列下, 计算整个标记序列的联合概率, 而 HMM 是在给定当前状态下, 定义下一个状态的分布

条件随机场的定义

示例

日期识别

当针对结构化数据时, 日期设置一般有良好的规范, 在数据入库时予以类型约束, 在需要时能够通过解析还原读取到对应的日期。然而在一些非结构化的数据应用场景下, 日期和文本混杂在一起, 此时日期的识别就变得艰难许多。非结构化数据下的日期识别多是与具体需求有关

任务及背景

任务实现技术

  1. 通过 Jieba 分词将带有时间信息的词进行切分, 然后记录连续时间信息的词
    • Jieba 词性标注提取文本中
      • “m”: 数字
      • “t”: 时间
import re
from datetime import datetime, timedelta
from dateutil.parser import parse
import jieba.posseg as psg

def time_extract(text):
    time_res = []
    word = ""
    keyDate = {
        "今天": 0,
        "明天": 1,
        "后天": 2,
    }

    for key, value in psg.cut(text):
        if key in keyDate:
            if word != "":
                time_res.append(word)
            word = (datetime.today() + timedelta(days = keyDate.get(key, 0))).strftime("%Y年%m月%d日")
        elif word != "":
            if value in ["m", "t"]:
                word = word + key
            else:
                time_res.append(word)
                word = ""
        elif value in ["m", "t"]:
            word = key
    if word != "":
        time_res.append(word)
    result = list(filter(lambda x: x is not None, [check_time_valid(w) for w in time_res]))
    final_res = [parse_datetime(w) for w in result]

    return [x for x in final_res if x is not None]


def check_time_valid(word):
    m = re.match("\d+$", word)
    if m:
        if len(word) <= 6:
            return None
    word1 = re.sub("[号|日]\d+$", "日", word)
    if word1 != word:
        return check_time_valid(word1)
    else:
        return word1


def parse_datetime(msg):
    if msg is None or len(msg) == 0:
        return None

    try:
        dt = parse(msg, fuzzy=True)
        return dt.strftime('%Y-%m-%d %H:%M:%S')
    except Exception as e:
        m = re.match(
            r"([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?",
            msg)
        if m.group(0) is not None:
            res = {
                "year": m.group(1),
                "month": m.group(2),
                "day": m.group(3),
                "hour": m.group(5) if m.group(5) is not None else '00',
                "minute": m.group(6) if m.group(6) is not None else '00',
                "second": m.group(7) if m.group(7) is not None else '00',
            }
            params = {}

            for name in res:
                if res[name] is not None and len(res[name]) != 0:
                    tmp = None
                    if name == 'year':
                        tmp = year2dig(res[name][:-1])
                    else:
                        tmp = cn2dig(res[name][:-1])
                    if tmp is not None:
                        params[name] = int(tmp)
            target_date = datetime.today().replace(**params)
            is_pm = m.group(4)
            if is_pm is not None:
                if is_pm == u'下午' or is_pm == u'晚上' or is_pm =='中午':
                    hour = target_date.time().hour
                    if hour < 12:
                        target_date = target_date.replace(hour=hour + 12)
            return target_date.strftime('%Y-%m-%d %H:%M:%S')
        else:
            return None


UTIL_CN_NUM = {
    '零': 0, '一': 1, '二': 2, '两': 2, '三': 3, '四': 4,
    '五': 5, '六': 6, '七': 7, '八': 8, '九': 9,
    '0': 0, '1': 1, '2': 2, '3': 3, '4': 4,
    '5': 5, '6': 6, '7': 7, '8': 8, '9': 9
}
UTIL_CN_UNIT = {'十': 10, '百': 100, '千': 1000, '万': 10000}

def cn2dig(src):
    if src == "":
        return None
    m = re.match("\d+", src)
    if m:
        return int(m.group(0))
    rsl = 0
    unit = 1
    for item in src[::-1]:
        if item in UTIL_CN_UNIT.keys():
            unit = UTIL_CN_UNIT[item]
        elif item in UTIL_CN_NUM.keys():
            num = UTIL_CN_NUM[item]
            rsl += num * unit
        else:
            return None
    if rsl < unit:
        rsl += unit
    return rsl

def year2dig(year):
    res = ''
    for item in year:
        if item in UTIL_CN_NUM.keys():
            res = res + str(UTIL_CN_NUM[item])
        else:
            res = res + item
    m = re.match("\d+", res)
    if m:
        if len(m.group(0)) == 2:
            return int(datetime.datetime.today().year/100)*100 + int(m.group(0))
        else:
            return int(m.group(0))
    else:
        return None

text1 = '我要住到明天下午三点'
print(text1, time_extract(text1), sep=':')

text2 = '预定28号的房间'
print(text2, time_extract(text2), sep=':')

text3 = '我要从26号下午4点住到11月2号'
print(text3, time_extract(text3), sep=':')

text4 = '我要预订今天到30的房间'
print(text4, time_extract(text4), sep=':')

text5 = '今天30号呵呵'
print(text5, time_extract(text5), sep=':')

地名识别

地名识别中将采用基于条件随机场的方法进行地名识别任务. 条件随机场模型的实现需要先安装 CRF++, 它是一款基于 C++ 高效实现 CRF 的工具

CRF++ 安装

Windows 安装:

Linux/macOS 安装(>gcc3.0):

$ git clone https://github.com/taku910/crfpp
$ cd crfpp
$ ./configure
$ make
$ sudo make install

CRF++ Python 接口

可以通过接口加载训练好的模型

$ cd python
$ python setup.py build
$ sudo python setup.py install 

使用 CRF++ 进行地名识别

任务及背景:

采用的预料数据集, 是 1998 年人民日报分词数据集, 该语料数据集主要是一个词性标注集。 可以使用其中被标记为 “ns” 的部分来构造地名识别语料。如:"[香港/ns特别/a行政区/n]ns”, 可以提取出 “香港特别行政区(中括号以内的"ns"在这里不再单独作为一个地名)"。按照这种思路, 对人民日报语料进行数据分析, 并切割了部分作为测试集进行验证。

  1. 确定标签体系

如同分词和词性标注一样, 命名实体识别也有自己的标签体系。一般用户可以按照自己的想法自行设计, 如下为地理位置标记规范, 即针对每个字标记为:“B”, “E”, “M”, “S”, “O” 中的一个:

标注 含义
B 当前词为地理命名实体的首部
M 当前词为地理命名实体的内部
E 当前词为地理命名实体的尾部
S 当前词单独构成地理命名实体
O 当前词不是地理命名实体或组成部分
  1. 语料数据处理

CRF++ 的训练数据要求一定的格式, 一般是一行一个 token, 一句话由多行 token 组成, 多个句子之间用空行分开。其中每行又分成多列, 除最后一列之外, 其他列表示特征。 因此一般至少需要两列, 最后一列表示要预测的标签(“B”, “E”, “M”, “S”, “O”)

  1. 特征模板设计
  2. 模型训练和测试
  3. 模型使用