一 NER任务的认识
参考:https://www.6aiq.com/article/1593878752671
1.Overiew/Introduction
文本数据结构化是 NLP最有价值的任务。一个句子中,命名实体更受到人们的关注。中文分词任务关注句子中的词汇之间的边界,词性标注关注这些被分出边界的词在词法上的类型。而命名实体识别关注的是命名实体的边界。它的粒度通常比中文分词要粗——是多个单词构成的复合词或短语,比如《那些年,我们一起追过的女孩》,《我们仍未知道那天所看见的花的名字》。它的类别通常比词性标注更具混淆性——是基于自然语言体系构建的抽象世界中,某个领域下的概念归属,比如人名,地名,组织机构名、股票、影视,书籍,游戏,艺术、医学术语等等。这些依托人类想象力构筑的事物,会随着时间的向前而不断变迁。它是信息抽取任务的焦点,在实际生产中需求很迫切,但做起来又很难。
2. 难点1:命名[命名实体]
NER 之所以难做,第一点是因为我们对命名实体定义上的模糊。我们要如何命名命名实体呢? Hanlp 作者何晗在《自然语言处理入门》一书中总结了命名实体的以下三个共性:
(1)数量无穷。比如宇宙中恒星名称、生物界中的蛋白质名称,即便是人名,也是会随着新生儿的命名不断出现新的组合。人们接触到的命名实体是一个开放的集合。有一些是已经存在,但我们的观测的视野有限,所以还未知晓,直到它在某个突如其来的一天成为了热点。还有一些则是还未存在,但任何人都有可能在某个时刻创造出它们。
(2)构词灵活。比如中国工商银行,既可以称为工商银行,也可以简称为工行。一些机构名甚至存在嵌套现象,比如“联合国销毁伊拉克大规模杀伤性武器特别委员会”内部就嵌套了地名和另一个机构名。
(3)类别模糊。一些命名实体之间的区别比较模糊,比如地名和机构名。有一些地名本身也是机构,比如“国家博物馆”,从地址角度来看属于地名,但从博物馆工作人员来看则是一个机构。
当我们人都需要费力地去分辨、定义实体时,会带来两个问题。一是工业界对 NER标注数据紧缺。因为数据标注就很困难,需要专家好好定义标准规范。而这个标准是基于人类共有知识的。它还不是一成不变的,你标注完了就能一劳永逸。而是隔几个月热点实体词就要大换一次血。过去标注好了一些实体,因为未来人们谈论它的语境都变了,可能不再适用。二是超越人类专家水平的NER 系统难以实现。当前的 SOTA 模型大都是建立在有监督模型的基础之上。通常我们会把它看作是一个序列标注问题,可以用 HMM 和 CRF等机器学习算法,或用 BiLSTM / CNN / BERT + CRF的深度学习范式来学习解码。这些机器学习算法在封闭式的数据集可以表现得很好,但依然满足不了工业界开放式的业务场景需求
3.难点2:实体的无穷
实体命名识别要面对的是排列组合可能无穷的词表。模型对 OOV 的泛化能力远低于我们的预期,所以通常做法是以统计为主,规则词典为辅。Hanlp 作者在书中对适合用规则词典来识别的实体分成了两类:
(1)对于结构性较强的命名实体,比如网址、E-mail、ISBN、商品编号,电话,网址,日期,淘宝或拼多多口令等,都可以用正则表达式来处理。这部分我们会在预处理中,先用规则匹配,而未匹配部分交给统计模型处理。比如数词,标点,字母,数字,量词等混合在一起。对于类如「三六零」这样的中文,我们可以把大写中文汉字不放入核心词典中,这样就可以将其作为未知字符用正则进行匹配。虽然我们需要耗费大量人力去设计规则。但好处是,比起训练玄学的机器学习模型,这里努力性价比更高,都能带来直接有效的回报。
(2)对于较短的命名实体,如人名,完全可以用分词方法去确定边界,用词性标注去确定类别。这样就无需再专门准备命名实体模块及语料库,只需要用分词语料库就可以做到。比如音译人名,我们会先用事先构建好的一个带词性的词典,对句子粗略地分词。再对分词后的句子从左往右扫描遇到人名词典中的词语则合并。这个词典被储存在一个DoubleArrayTrie中用作高效匹配。词典中的单字要谨慎选择,因为音译人名常用字通常也是汉语常用字。这套逻辑虽然难以识别未知的人名片段,但可以召回极长的音译人名。
基于词典和规则的方法只在部分类型的实体识别上有用,要获得更好的泛化能力,我们也可以用「张华平」和「刘群」等教授在提出的「角色标注框架」去结合模型和规则预测人名。它的思路是,我们先为构成命名实体的短语打好标签,若标签的序列满足某种模式则识别为某种类别的实体。它是一种层叠的隐马尔可负模型HMM。第一层是以标签为状态,去发射观测到的构成实体的字词短语,发射的概率和标签与标签之间的转移存在的依赖关系由 HMM建模。第二层则以实体类型作为状态,去发射观测到的标签。实体内的标签到标签之间的转移存在依赖关系,举个书中的人名识别的例子:
标签 | 意义 | 例子
B | 姓氏 | 「罗」志 祥
C | 双名首字 | 罗「志」 祥
D | 双名末字 | 罗 志 「祥」
E | 单名 | 时(chi)「翔」
F | 前缀 | 「老」王
G | 后缀 | 罗「胖」
K | 人名上文 | 「又看到」卢宇正在吃饭
L | 人名下文 | 又看到卢宇正「在吃饭」
M | 两个人名之间的成分 | 张飞「和」关羽
等等...
首先我们会把语料中的数据标注成如上的标签,再用词典去记录每个标签可能发射的词短语,以及频次。接着,我们根据语料数据去计算每个标签到其它标签的转移概率,即标签x 到标签 y 的转移次数除以标签 x 到所有标签的转移次数和。这样我们就获得了模型的参数。模型可以是 HMM 或CRF。它们可以学到很多种规律,比如,标签姓氏 B 后面接 双名、单名或后缀标签的概率远远大于接其它的标签概率。再比如,有双名首字 C就一定有双名末字 D。又比如,姓氏 B 后面接单名 E 的概率会容易和后面接后缀 G 的概率混淆。通过收集大型的人名库,可以构建出更完善的依赖。
深度学习模型的输入数据要考虑词的粒度问题。即便我们是在一段中文的序列中,也可能掺杂着类如 DOTA2 和 CSOL这样的英文单词。英文单词与中文的词有显著的不同。由单个字母组成的英文单词显然并没有中文单字那样丰富的语义。所以在英文中很少用 char作模型的输入。即便是细粒度,也是用类似于前缀后缀这种比字母粗一个粒度,比单词细一个粒度的 sub char语素来作为深度学习模型的输入。在工程上,我们需要注意归一化和分字的问题。
>>> sentence = "褚泽宇是马尼拉特锦赛中的DoTa2选手"
>>> ' '.join(sentence).split() # 未归一化以单字输入
['褚', '泽', '宇', '是', '马', '尼', '拉', '特', '锦', '赛', '中', '的', 'D', 'o', 'T', 'a', '2', '选', '手']
>>> import re
>>> def is_chinese(char):
... return '\u4e00' <= char <= '\u9fff'
...
>>> def segment(sentence):
... i = 0
... tokens = []
... while i < len(sentence):
... tok = sentence[i]
... if is_chinese(tok):
... tokens.append(tok)
... i += 1
... else:
... i += 1
... while i < len(sentence) and not is_chinese(sentence[i]):
... tok += sentence[i]
... i += 1
...
... tokens.append(tok)
... return tokens
>>> segment(sentence.lower()) # 归一化后,中文以单字,英文以单词输入
['褚', '泽', '宇', '是', '马', '尼', '拉', '特', '锦', '赛', '中', '的', 'dota2', '选', '手']
若单独分 char,连续的单词会被分成一个个的字母,这会对模型识别标注带来一定困难。所以我们可以把序列中的连续字母会先通过预处理给过滤掉。比如「data2」会先用正则获得,然后通过词典匹配到它的类别后,在把原序列的英文单词替换。再用替换过的序列输入给模型。注意,'< 游戏>'是单独的一个 token,会在 word embedding 的词表中额外添加。
['褚', '泽', '宇', '是', '马', '尼', '拉', '特', '锦', '赛', '中', '的', '<游戏>', '选', '手']
类似的,如果文本序列中有可以用规则确定处理的,我们也会做替换。
['有', '事', '可', '以', '打', '我', '电', '话', ':', '1', '2', '3', '4', '5', '6', '7', '9', '0', '9']
['有', '事', '可', '以', '打', '我', '电', '话', ':', '<电话号码>']
关于规则和词典,这个 Repro 提供了比较充足的语料和规则资源:
[https://github.com/fighting41love/funNLP/github.com
](https://link.zhihu.com/?target=https%3A//github.com/fighting41love/funNLP/tree/master/data)----)-
4.难点3:歧义消除
传统的词典规则方法可以很容易召回文本序列中在词表匹配到的词,但它的局限在无法解决歧义问题。一种典型的歧义是多种可能划分问题。比如下面这个例子
输入序列:又看到卢宇正在吃饭
可以分为:又 / 看到 / 卢宇 / 正在 / 吃饭
也能分为:又 / 看到 / 卢宇正 / 在 / 吃饭
通常我们会用二元语法最短路径分词的方法去判断哪种分法路径最短。具体做法是记录所有二元字转移的概率,把文本序列构建成一个词图,然后用最短路径算法找出代价最小的路径。虽然这个例子它能够通过,但这个方案无法考虑复杂的语境。拿我们人的常识来说,识别的人名实体应该为
「卢宇正」,体现在「又」这个字。「又」和「正在」是有一点点冲突的。前者是描述再次看到的状态,而后者体现是当下时刻的进行时。所以第二种分法更符合常识。我们再来看另一个例子
a) 马云指着天发誓说,我不喜欢钱。b) 远方有一朵马云仿佛奔腾在宽阔的江面上。并不是所有的马云都是马云。
字典匹配会把字典中的词都召回,所以会出现把第二个句子判断成人名的错误。而深度学习模型考虑了上下文语境。人可以“指着”,所以第一句是人名。云才是“一朵”,所以不是人名。当然之前说的基于角色标注的层叠隐马尔科夫模型也可以做到。但复杂一点的语境,深度学习模型能做得更好。在类如搜索推荐的下游任务中我们还需要对实体的指代做进一步的细分。这部分是实体链接任务,日后可以再展开。
5.难点4:边界的界定
虽然深度学习对歧义的消解有显著优势,但它通常会遇到的问题是对新词的边界把握模糊。而词典中包含了大量词的边界信息。因此如何把词典信息融入到深度学习模型中是近几年研究的主流。一种直观的方法是先执行分词,再对分词序列标注。但这种分割后再做NER 的流程会遇到误差传播的问题。名词是分割中 OOV 的重要来源,并且分割错误的实体边界会导致 NER错误。这个问题在开放领域可能会很严重,因为跨领域分词仍然是一个未解决的难题。简单说就是,分词分不好,NER 也难做。而分词确实经常分不好。
PS:在LSTM中,输入门决定当前输入有多少加入cell,遗忘门决定cell要保留多少信息,输出门决定更新后的cell还有多少输出。