命名实体识别,Named Entity Recognition,简称NER。指的是构建合适的模型,从给定的数据(常常是文本)中得到所需实体的过程。
1、什么是命名实体
命名实体指的就是所有以名称来作为标识的实体。在有的资料1中,将命名实体分为三大类(实体类、时间类和数字类)七小类(人名、地名、机构名、时间、日期、货币和百分比)。
随着技术的发展以及语言习惯的更新,上述分类方法可能并不适用于所有场景,根据具体业务的不同,可以增减适当的实体种类。
2、NER的关键
明白了什么是实体之后,也就很容易搞懂什么是实体的识别了。在进行实体识别的过程中,有两个问题是十分关键的:
- 实体边界的确认
- 实体类别的判断
这两个关键问题也是很好理解的。
所谓实体边界的确认,指的是对一个句子中的实体词进行正确的划分。例如在句子江大桥同志发表新年讲话
中,一个好的识别算法必须将实体词江大桥
进行正确的标记,而不是在其他的位置进行划分。
所谓实体类别的判断,仍以上例说明,算法必须判定江大桥
为人名实体,而不是其他类型的实体。
在解释第一个关键问题时说到,一个好的算法应该能够对句子中每个字进行正确的标记以区分该字是否为实体;进一步,如果是实体,还需表明该词在实体词中的位置信息,比如,是实体词的第一个字,还是中间位置的字,还是最后一个字。读者不难理解这些标记对于实体边界确认的重要性。
最简单的表示法有B-I-O
表示法,即B
表示实体的起始字,I
表示实体的其他字,O
表示非实体字。按照这种方法,上面的句子贴标签后表示如下(每个字均对应一个标签):
江 大 桥 同 志 发 表 新 年 讲 话
B I I O O O O O O O O
这种方法最简单,问题也是显而易见的:实体的末尾字不容易得到区分。在实践中发现,如果语料库采用上述方式进行标记,在进行机构实体识别时可能产生错误,具体表现为,将机构实体末尾字的下一个字(也可能两个或多个,视具体情况而定)也贴上I
标签,从而得到错误的实体名称。
另外一种相对复杂的表示方法可以表示为B-M-E-S-O
,类似地,B
表示实体的开始字(Begin),M
表示实体的中间字(Middle),E
表示实体的末尾字(End),S
为实体只有一个字时的标记(Single,在中文任务中并不常见),O
表示非实体字。上述句子采用这种表示方法得到的结果是:
江 大 桥 同 志 发 表 新 年 讲 话
B M E O O O O O O O O
当然,还有其他的方法,但目的都是一致的,这里不过多介绍了。
3、NER的研究现状
在吴军老师的《数学之美》一书中对中文分词有这样的总结2:这个问题属于已经解决的问题,不是什么难题了。人们再怎么花精力去研究,所得到的提升也是有限的。现在来看,这也是学术界对NER的研究热情并不是十分高涨的原因之一了。
另一个原因,个人认为则是语料库的限制。为了比较算法的性能,学术界一般采用固定的语料库,而如果将其研究成果进行落体实施,那么语料库的选取则是一个需要考量的问题。大部分公司可能都不会花精力去构建一个本领域的预料库来训练算法,只能利用现有的语料库,那么,再优秀的算法最后也可能是落得个“巧妇难为无米之炊”的下场。
一般来说,现在的NER工作的“标配”是BiLSTM+CRF3,即“双向长短记忆网络”+“条件随机场”模型,也有人提出新的改进方法4。关于CRF的理论性介绍,可以参考我的另一篇翻译博客。
事实上,单纯根据CRF或者BiLSTM也可以做NER(文章最后附上单独采用CRF模型的NER数据及代码),但单纯根据CRF的弊端在于需要人工手动设置特征模板,特征模板决定了特征函数,特征函数的输出对于CRF的工作效果有至关重要的影响。而单独根据BiLSTM进行NER虽然不需要进行什么手工操作,但是我们知道BiLSTM进行的实质上就是一个分类工作,而它在分类时是单独对每个字进行操作的,也就是说,不会利用到上下文已经分好的类标签,这就容易出现一种逻辑错误,比如M
标签在E
标签后面,B
后面接O
等情况。
4、基于条件随机场NER模型训练
本部分将说明如何基于条件随机场进行以及现有的工具包来训练一个粗糙的NER模型,主要参考了这篇博客,在此表示感谢。
4.1 数据集介绍
采用了标注后的人民日报1998年1月语料库进行训练,该语料库的前五行展示如下:
19980101-01-001-001/m 迈向/v 充满/v 希望/n 的/u 新/a 世纪/n ——/w 一九九八年/t 新年/t 讲话/n (/w 附/v 图片/n 1/m 张/q )/w
<由于众所周知的原因这一行没办法显示>
19980101-01-001-003/m (/w 一九九七年/t 十二月/t 三十一日/t )/w
<由于众所周知的原因这一行没办法显示>
19980101-01-001-005/m 同胞/n 们/k 、/w 朋友/n 们/k 、/w 女士/n 们/k 、/w 先生/n 们/k :/w
关于语料库中各个标注的含义,网上有很多说明,可以参考这里,本文不再赘述。
4.2 任务说明及语料库预处理
任务: 作为一个简单的实践,目标是从给定的句子中抽取出人名、地名、机构名和时间四类实体。
预处理: 预处理的最终目的是将预料库中每一个字符与其所属的实体标签进行一一对应,举例如下:
国 家 主 席 * * * 1 9 9 8 年 的 讲 话
O O O O B_Per I_Per I_Per B_T I_T I_T I_T I_T O O O
即:对于不属于实体的词(字),以O
进行标记;对于实体词,不但要标记类别(如上例中的Per
表示人名,T
表示时间),而且要标记实体边界。
要做到这一点,我们进行的主要步骤包括:
- 字符转换
- 时间词进行合并
- 人名进行合并
- 大粒度词的合并
下面一一进行说明。
1、字符转换
将一个字符串中的全角字符(如果有的话)转换为半角字符。实现函数为q_to_b(str)
。
2、时间词合并
语料库中存在类似12月/t 31日/t
的文本,这里应该将其合并为12月31日/t
。实现这个功能的函数是process_t(words)
。
存在的问题:例如对文本1998年/t 新年/t
,也会按照上面的形式进行合并,而这并不是我们想要的。
3、人名合并
语料库中的中文人名都是按姓、名分开标注的:江/nr 大桥/nr
,因此将其合并:江大桥/nr
。实现这个功能的函数是process_nr(words)
。
存在的问题:由于外国人名在本语料库中不区分姓和名,因此只占一个词,如果连续的多个中国人名中间存在一个外国人名而且它们之间没有标点间隔时,会出错。举例如下:
#有标点间隔时不会出错
a = '卢/nr 嘉锡/nr 、/w 布赫/nr 、/w 铁木尔·达瓦买提/nr 、/w 吴/nr 阶平/nr 、/w 宋/nr 健/nr'
print(process_nr(a.split())
#输出为:['卢嘉锡/nr', '、/w', '布赫/nr', '、/w', '铁木尔·达瓦买提/nr', '、/w', '吴阶平/nr', '、/w', '宋健/nr']
b = '卢/nr 嘉锡/nr 布赫/nr 铁木尔·达瓦买提/nr 吴/nr 阶平/nr 宋/nr 健/nr'
print(process_nr(b.split())
# 结果错误
#输出为:['卢嘉锡/nr', '布赫铁木尔·达瓦买提/nr', '吴阶平/nr', '宋健/nr']
4、大粒度词合并
将预料库中以“[]”括起来的词进行合并,并以大粒度的标签进行标注,例如对于[香港/ns 普通话/n 台/n]nt
,处理为:香港普通话台/nt
,实现这个功能的函数是process_k(words)
。
4.3 训练流程
经过上述预处理之后,该语料仍不能直接进行计算,还需要进行以下操作:
4.3.1 定义映射文件及处理函数
定义一个_maps
字典,其功能是根据语料库中词的词性来对应其实体属性,内容如下:
_maps = {u't': u'T',
u'nr': u'PER',
u'ns': u'LOC',
u'nt': u'ORG'}
_maps
的键为词性,值为实体标签。
有了映射文件,接下来对语料库中的所有词的词性进行替换,这一步通过pos_to_tag(p)
函数实现。
此时,每个词都有对应的实体标签了,接下来确定实体的边界,即对一个实体中首字贴上B_
,非首字贴上I_
。这个功能通过tag_perform(tag, index)
函数实现。
至此,我们得到的数据应该包含每个单字以及与之对应的实体标签,当然,还有一些函数用于对语料库中的数据进行批量处理,具体内容及说明可以看代码注释。标签一共有9种:O
,B_PER
,I_PER
,B_T
,I_T
,B_LOC
,I_LOC
,B_ORG
,I_ORG
。
4.3.2 调用sklearn_crfsuite
sklearn_crfsuite是一个python工具包,它将CRF的功能进行了包装,并且使用了类似于sklearn的模型语法,使用户可以在python环境下训练、保存自己的CRF模型。
特征模板与特征函数
特征模板的含义从字面上理解最好:就是产生你所需要的特征的一个“模具”。它就是一个框架,可以在你所输入的数据中提取出指定的特征。
在代码中,这个功能是通过extract_feature(word_gram)
函数实现的。该函数中有一行代码如下:
feature = {u'w-1': word_gram[0], u'w': word_gram[1], u'w+1': word_gram[2],
u'w-1:w': word_gram[0]+word_gram[1], u'w:w+1': word_gram[1]+word_gram[2],
u'bias': 1.0}
这个字典中的内容就是特征模板了。可以看出这里一共用了5个模板来“制造”特征,分别是:当前字、当前字的前一个字、当前字的后一个字、前一个字与当前字的组合,当前字与后一个字的组合。
有了模板之后,就可以对语料提取特征了。将这一组模板想象为一个窗口,然后将其沿着语料进行移动,移动到一个字w
处时,通过该模板自然就获取了w-1
、w+1
、w-1:w
、w:w+1
的内容。如果这些内容第一次出现,就将它们加入到一个集合中,否则不添加。遍历整个语料库后,得到的该集合中的内容就是所有的特征函数的输入。例如,我得到的模型的该集合前几个内容是:
['w-1:<BOS> ', 'w:迈', 'w+1:向', 'w-1:w:<BOS>迈', 'w:w+1:迈向', 'bias', 'w-1:迈', 'w:向', 'w+1:充', 'w-1:向', 'w:充', 'w+1:满', 'w:满', 'w+1:希', 'w-1:满', 'w:希', 'w+1:望', 'w:w+1:希望',...]
其中的每一个元素都可以理解为一种“现象”,满足这种现象,那么该函数在该字出的输出为1,否则为0。比如对于第一个字迈
,显然它满足前5个现象,即w-1:<BOS>
,w:迈
,w+1:向
,w-1:w:<BOS>迈
,w:w+1:迈向
。因此这5个特征函数的输出为1,其余的输出均为0。
对语料库中的每一个字都要计算其在所有特征函数下面的值,因此总的计算次数为 m ∗ N m*N m∗N,其中 m m m为特征函数的个数, N N N为语料库大小。
模型的训练及其他
模型的训练与sklearn十分相似,首先声明一个模型实例,然后喂入数据,训练之后的实例有predict
属性可以用来进行预测,详见代码。
这里应用的只是sklearn_crfsuite中一小部分功能,用户还可以查看特征转移的概率以及利用交叉验证等功能,具体可以查看原始文档。
5、备注
训练好的基于CRF的NER模型、数据以及详细地训练流程已经上传到github上了,欢迎下载、查看、运行。
后续会增加BiLSTM模型,总结完善学习BiLSTM及CRF理论的笔记。