天天看點

命名實體識别——日期識别

一、命名實體識别簡介

其目的是識别語料中的人名、地名、組織結構名等命名實體,由于這些命名實體在不斷地更新,很難在詞典中全部列出,是以就對這些詞的識别在詞彙形态處理任務中單獨處理,也就是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': , '3': , '4': ,
    '5': , '6': , '7': , '8': , '9': 
}

UTIL_CN_UNIT = {'十': , '百': , '千': , '萬': }

def cn2dig(src):
    if src == "":
        return None
    m = re.match("\d+", src)
    if m:
        return int(m.group())
    rsl = 
    unit = 
    for item in src[::-]:
        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()) == :
            return int(datetime.datetime.today().year/)* + int(m.group())
        else:
            return int(m.group())
    else:
        return None

def parse_datetime(msg):
    #print('msg:',msg)
    if msg is None or len(msg) == :
        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() is not None:
        res = {
            "year": m.group(),
            "month": m.group(),
            "day": m.group(),
            "noon":m.group(),  # 上中下午晚早
            "hour": m.group() if m.group() is not None else '00',
            "minute": m.group() if m.group() is not None else '00',
            "second": m.group() if m.group() is not None else '00',
        }
        params = {}
        for name in res:
            if res[name] is not None and len(res[name]) != :
                tmp = None
                if name == 'year':
                    tmp = year2dig(res[name][:-])
                else:
                    tmp = cn2dig(res[name][:-])
                if tmp is not None:
                    params[name] = int(tmp)
        target_date = datetime.today().replace(**params)
        #print('target_date:',target_date)
        is_pm = m.group()
        if is_pm is not None:
            if is_pm == u'下午' or is_pm == u'晚上' or is_pm =='中午':
                hour = target_date.time().hour
                if hour < :
                    target_date = target_date.replace(hour=hour + )
        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) <= :
            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 = {'今天': , '明天':, '後天': }
    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, ))).\
                      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']
預定号的房間:['2018-07-28 00:00:00']
我要從号下午點住到月号:['2018-07-26 16:00:00', '2018-11-02 00:00:00']
今天号呵呵:['2018-07-26 00:03:00']
我要預訂今天到的房間:['2018-07-26 00:00:00']
           

從運作結果來看,前三句話都很多好的識别出日期了,而後面兩句則識别不出來,這也正是基于規則識别的限制所在,因為不可能覆寫所有的規則場景,但好處就是不需要在系統建設初期為搜集資料标注訓練而煩惱。

三、筆記:

格式轉換成帶有漢字的形式可以通過如下方式:

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

UnicodeEncodeError: 'locale' codec can't encode character '\u5e74' in position : 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自然語言處理實戰 核心技術與算法》

繼續閱讀