命名实体识别——日期识别

一、命名实体识别简介

其目的是识别语料中的人名、地名、组织结构名等命名实体,由于这些命名实体在不断地更新,很难在词典中全部列出,所以就对这些词的识别在词汇形态处理任务中单独处理,也就是NER技术。

而命名实体识别效果的评判标准主要是看实体的边界是否划分正确,以及实体的类型是否标注正确,对于英文来说命名实体的边界识别相对简单,因为一般都有明显的形式标志,而对于实体类型的确定相对较难。在中文中相较于实体类别标注,实体边界的识别更加困难。

中文命名实体识别难点主要有以下几点:
命名实体的数量众多、命名实体构成规律复杂、嵌套情况复杂、长度不确定、等

命名实体的识别目前主要采用的是基于规则和统计的混合方法。因为单纯的基于规则的话需要手工修改规则,难以覆盖所有的语言现象,也就存在可移植性差,维护困难。而基于统计的命名实体识别,其对语料库的依赖比较大,而用来建设和评估命名实体识别系统的大规模通用语料库又比较少。因此目前多采用混合的方法来做。

这里举一个简单的日期识别的实例:
例如现有一个基于语音问答系统的酒店预订系统根据用户输入的每句语音进行分析,识别出用户的酒店预订需求,然而由于语音转换的文字大都不是严格的数字形式,这时就需要通过一定的规则来进行处理。

二、日期识别:

主要思想是:这里主要通过正则表达式和jieba分词来完成任务。
首先将输入的要识别的句子进行jieba分词,提取出其带有时间词性的词,比如词性是“m”(数字),“t”(时间)的词,然后再通过正则化处理,得到相应的时间实体。注意这里是没有训练语料,直接用的是jieba的分词和词性标注功能。

示例代码:

# -*- coding: utf-8 -*-

#日期识别
import re
from datetime import datetime,timedelta
from dateutil.parser import parse
import jieba.posseg as psg

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

def parse_datetime(msg):
    #print('msg:',msg)
    if msg is None or len(msg) == 0:
        return None


    m = re.match(
        r"([0-9零一二两三四五六七八九十]+年)?([0-9一二两三四五六七八九十]+月)?([0-9一二两三四五六七八九十]+[号日])?([上中下午晚早]+)?([0-9零一二两三四五六七八九十百]+[点:\.时])?([0-9零一二三四五六七八九十百]+分?)?([0-9零一二三四五六七八九十百]+秒)?",
             msg)       
    #print('m.group:',m.group(0),m.group(1),m.group(2),m.group(3),m.group(4),m.group(5))
    if m.group(0) is not None:
        res = {
            "year": m.group(1),
            "month": m.group(2),
            "day": m.group(3),
            "noon":m.group(4),  # 上中下午晚早
            "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)
        #print('target_date:',target_date)
        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


# 对提取出的拼接日期串进行进一步的处理,进行有效性判断
def check_time_valid(word):
    #print('check:',word)
    m = re.match("\d+$", word)
    if m:
        if len(word) <= 6:
            return None
    word1 = re.sub('[号|日]\d+$', '日', word)
    #print('word1:',word1)
    if word1 != word:
        return check_time_valid(word1)
    else:
        return word1

#时间提取
def time_extract(text):
    time_res = []
    word = ''
    keyDate = {'今天': 0, '明天':1, '后天': 2}
    for k, v in psg.cut(text):
        #print(k,v)
        if k in keyDate:
            if word != '':
                time_res.append(word)  
            # 日期的转换,timedelta提取任意延迟天数的信息
            word = (datetime.today() +timedelta(days=keyDate.get(k, 0))).\
                      strftime('%Y{y}%m{m}%d{d}').format(y='年',m='月',d='日') 

        elif word != '':
            if v in ['m', 't']:
                word = word + k
            else:
                time_res.append(word)
                word = ''
        elif v in ['m', 't']:  # m:数字 t:时间
            word = k            
    #print('word:',word)
    if word != '':
        time_res.append(word)
    #print('time_res:',time_res)
    # filter() 函数用于过滤序列,过滤掉不符合条件的元素,返回由符合条件元素组成的新列表
    result = list(filter(lambda x: x is not None, [check_time_valid(w) for w in time_res]))
    #print('result:',result)
    final_res = [parse_datetime(w) for w in result]
    #print('final_res:',final_res)
    return [x for x in final_res if x is not 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=':')


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

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

运行结果:

我要住到明天下午三点:['2018-07-27 15:00:00']
预定28号的房间:['2018-07-28 00:00:00']
我要从26号下午4点住到112号:['2018-07-26 16:00:00', '2018-11-02 00:00:00']
今天30号呵呵:['2018-07-26 00:03:00']
我要预订今天到30的房间:['2018-07-26 00:00:00']

从运行结果来看,前三句话都很多好的识别出日期了,而后面两句则识别不出来,这也正是基于规则识别的限制所在,因为不可能覆盖所有的规则场景,但好处就是不需要在系统建设初期为搜集数据标注训练而烦恼。

三、笔记:

格式转换成带有汉字的形式可以通过如下方式:

timedelta(days=keyDate.get(k, 0))).strftime('%Y年%m月%d日')

UnicodeEncodeError: 'locale' codec can't encode character '\u5e74' in position 2: Illegal byte sequence

解决办法:

time.strftime('%Y{y}%m{m}%d{d} %H{h}%M{f}%S{s}').format(y='年',m='月',d='日',h='时',f='分',s='秒')

参考:《pytho自然语言处理实战 核心技术与算法》

你可能感兴趣的:(NLP)