天天看点

命名实体识别(NER)学习笔记

参考《python自然语言处理实战》

命名实体识别(Named Entities Recognition,NER)是自然语言处理中的一个基础任务。主要分为实体类、事件类、数字类三大类和人名、地名、机构名、时间、日期、货币、百分比等小类。NER比较侧重召回率,即需要最大限度的找出给定句子中所有的命名实体。

和分词一样,NER主要方法也分为三类,基于规则的、基于统计的、规则和统计相结合的方法。序列标注方式是目前NER中的主流方式,一般基于隐马尔可夫(HMM)和条件随机场(CRF)。相较于HMM,CRF不只考虑前一个字的特征的影响,也将句子中其它位置字的特征的影响考虑在内,更符合实际情况。

以地名处理为例,NER处理方式可以参考分词的处理方式,即对每个字做状态标注,如果是地名,则对词首、词中、词尾分别标定为B、M、E,单独成词标定为S,非地名词统一标定为O,通过对输入逐字做状态标注,从而实现地名识别。因此其任务可以表述为,对于给定的句子x,输出一个状态序列y,例如输入x为"我来到北京",则输出"OOOBE"、或“OOOBB”等状态序列。

可以看出可能的序列有很多,那么如何选择一个最符合的序列呢?可以借助一些模型比如HMM或CRF来实现。HMM在分词任务中已经学习了,所以这里使用CRF来做地名识别。

对于序列"OOOBE"和“OOOBB”,前者肯定更好,因为两个字都是词首的情况显然不符合实际。假如可以制定一些规则来评判可能的y的好坏,那么形如"BB"、"EE"这些序列肯定是要减分的。

CRF中有特征函数的概念,一个特征就对应一条规则.对于一个给定的序列,用每一个规则对他校验并给分,再把所有规则下的得分相加作为这个序列的总分(由于不同规则的重要成程度不同,因此每条规则都要有自己的权重值,最后求和要进行加权)。对于所有可能的序列,取总分最高者作为输出序列。

上面提到规则的权重值,训练的任务就是拿到这些权重值。

CRF++对CRF模型做了封装,因此可以使用CRF++来做地名的识别。CRF++的使用分为几个步骤:

  1. 确定标签体系
  2. 调整训练集格式
  3. 设计特征模板
  4. 训练模型
  5. 测试模型
  6. 使用模型

CRF++要求训练集每行一个token(即每行一个字),句子间用空行隔开,每行的最后一列是标签,前面的列是这一行的特征。然后设计特征模板。特征模板中每一条就是对应于CRF中的特征函数,通过定义这些特征函数来使模型知晓上下文的信息,从而提高模型的准确率。准备好训练集和特征模板后便可以使用cfr_learn命令训练并生成模型,再使用crf_test命令将模型应用于测试集进行测试分析。

书中地名识别使用的是98年人民日报分词数据集,特征只取了单个字。训练完对测试集应用这个模型,得到该模型的查准率,召回率和调和平均值如下(data/testresult.txt是测试集,crf++将模型应用于测试集后,将预测结果放在了最后一列,如下):

迈	O	O
向	O	O
充	O	O
满	O	O
希	O	O
望	O	O
的	O	O
新	O	O
世	O	O
           
def calculatePRAndF1():
        with open("data/testresult.txt", mode="r", encoding="utf8") as f:
            allLocPre = 0  # 所有被模型识别为地名的数量
            locPreCorr = 0  # 被模型识别为地名且正确的数量
            allLocReal = 0  # 所有地名的数量
            for line in f:
                line = line.strip()
                if line == "":
                    continue
                word, realFlag, preFlag = line.split()  # 字, 实际状态标注, 预测状态标注
                if preFlag != "O":
                    allLocPre += 1
                if realFlag != "O":
                    allLocReal += 1
                if realFlag == preFlag:
                    if not realFlag == "O":
                        locPreCorr += 1
            precision = locPreCorr * 1.0 / allLocPre  # 查准率
            recall = locPreCorr * 1.0 / allLocReal  # 召回率
            f1 = 2 * precision * recall / (precision + recall)  # 调和平均
            return precision, recall, f1
           

结果如下:

precision -> 0.9080725791520089
recall -> 0.8419742489270386
f1 -> 0.8737751647960093
           

这是只把字作为特征的情况下得到的结果,如果再追加一个词性为特征,效果将会提高很多,如下(data/testresultwithflag.txt是增加了词性为特征后得到的模型的测试结果):

def calculatePRAndF1():
        with open("data/testresultwithflag.txt", mode="r", encoding="utf8") as f:
            allLocPre = 0  # 所有被模型识别为地名的数量
            locPreCorr = 0  # 被模型识别为地名且正确的数量
            allLocReal = 0  # 所有地名的数量
            for line in f:
                line = line.strip()
                if line == "":
                    continue
                word, _, realFlag, preFlag = line.split()  # 字, 实际状态标注, 预测状态标注
                if preFlag != "O":
                    allLocPre += 1
                if realFlag != "O":
                    allLocReal += 1
                if realFlag == preFlag:
                    if not realFlag == "O":
                        locPreCorr += 1
            precision = locPreCorr * 1.0 / allLocPre  # 查准率
            recall = locPreCorr * 1.0 / allLocReal  # 召回率
            f1 = 2 * precision * recall / (precision + recall)  # 调和平均
            return precision, recall, f1
           

结果如下:

precision -> 0.9910729613733905
recall -> 0.9910729613733905
f1 -> 0.9910729613733905
           

各个指标都有很大的提升。