在之前的博客冷启动,知多少?中提到规则用于冷启动,在实际的工作中,多数算法同学是不乐意写规则的,在博主之前做中文纠错的时候,术语纠错子任务就是一个标准的规则系统。博主年少轻狂的时候,也曾怼过业界做知识图谱的一位相对有名的同学,认为搞规则很Low。

观察到在之前的同组同学的工作中,一个关于NER的工作,需要抽取四个字段。第一版方案是基于纯规则做的,ROI较低。第二版方案基于模型来做,指标上暴打第一版,实现成本还比较低。

但是从另外一个角度,引用一个朋友说的话:“没有写过一万条规则的人工智能系统,他是不敢用的。”。

有很多关于模型和规则的争议,使用模型是希望利用模型的泛化性,在保证高召回的前提下,希望保证高准确,但是由于泛化问题的存在,无法保证绝对的高准确。这个时候,虽然规则一般情况下的召回较低,但是准确性很高,因此在多数的实际业务服务中,可以看到模型+规则的组合服务模式,甚至在博主近期的一段经历中,在需要高准确的场景下,看到过基本整个算法团队都在写规则的情况,这也不足为怪了。

同样,在和一些做生成的团队交流的时候,虽然大家一直在研究生成的模型方案,但是实际上线的方案仍旧是基于模板的技术,模型只是在整个环节中的单点做贡献。

具体优缺点对比如下:

方法 优点 缺点
模型 高召回,一定程度上的高准确,泛化能力 无法保证所有badcase的覆盖
规则 高准确,简单快速反馈 低召回,劳动密集型工作
模型+规则 理论上的高召回+高准确,快速反应能力 减缓模型迭代周期,加速规则迭代(比重提升)

这篇博客主要梳理一些规则相关的方法论,笔者经验和观察有限,后续会进一步完善。

词典怎么用

在多数的开源分词工具中,如Jieba和LAC等,都会提供用户自定义词典,主要解决的问题是在自定义词典中的词需要保证单独切分出来。这里需要考虑三个问题:第一,词典的获取;第二,词典的检索实现。第三,保证切分正确的实现;在LAC中,不仅提供了分词干预的功能,同时提供了NER的标注功能,虽然从建模角度,二者区分不大。

作为一个对外开放的工具,词典是由用户提供的。但是在多数场景下,词典的获取是知识沉淀的过程,可以借助新词发现技术,人工编纂,知识图谱映射等方式来获取。

假设词典很大,就需要提升检索的速度。常用的方式是基于AC自动机的实现(Trie树+KMP算法),相关开源实现较多。LAC中对于第一和第二的处理逻辑如下:

def load_customization(self, filename, sep=None):
        """装载人工干预词典"""
        self.ac = TriedTree()
        with open(filename, 'r', encoding='utf8') as f:
            for line in f:
                if sep == None:
                    words = line.strip().split()
                else:
                    sep = strdecode(sep)
                    words = line.strip().split(sep)

                if len(words) == 0:
                    continue

                phrase = ""
                tags = []
                offset = []
                for word in words:
                    if word.rfind('/') < 1:
                        phrase += word
                        tags.append('')
                    else:
                        phrase += word[:word.rfind('/')]
                        tags.append(word[word.rfind('/') + 1:])
                    offset.append(len(phrase))

                if len(phrase) < 2 and tags[0] == '':
                    continue

                self.dictitem[phrase] = (tags, offset)
                self.ac.add_word(phrase)
        self.ac.make()#构建ac自动机

上述逻辑之后,比较重要的是如何结合模型预测结果给出最终的预测结果(LAC中正是基于模型+规则的方式)。LAC中的实现如下:

def parse_customization(self, query, lac_tags):
        """使用人工干预词典修正lac模型的输出"""
        if not self.ac:
            logging.warning("customization dict is not load")
            return

        # FMM前向最大匹配
        ac_res = self.ac.search(query)

        for begin, end in ac_res:
            phrase = query[begin:end]
            index = begin

            tags, offsets = self.dictitem[phrase]
            for tag, offset in zip(tags, offsets):
                while index < begin + offset:
                    if len(tag) == 0:
                        lac_tags[index] = lac_tags[index][:-1] + 'I'
                    else:
                        lac_tags[index] = tag + "-I"
                    index += 1

            lac_tags[begin] = lac_tags[begin][:-1] + 'B'
            for offset in offsets:
                index = begin + offset
                if index < len(lac_tags):
                    lac_tags[index] = lac_tags[index][:-1] + 'B'


阅读源码可以发现,逻辑上是一个基于规则的修正。但是这里是分词,词性,NER三种,modeling上是一种任务,因此修正逻辑可以统一为一种。

规则引擎

在LAC的加载词典过程中,可以看到涉及对词典的解析逻辑。实际上,这里正是一个mini型的加载引擎。官方定义的词典格式:

春天/SEASON
花/n 开/v
秋天的风
落 阳

默认要求:(1)每行一个item(2)用“/”分割phrase和tag(3)连续的phrase和tag组合用空格分开(4)tag可以确省

格式标准化之后,就可以开放给不懂具体技术的用户来完善词典。上述两段代码形成了一个常见的规则引擎的模式。三个要素如下:(1)标准化,易理解的用户输入模式(2)用户输入的加载解析逻辑(3)和上游模型的预测结果相融合的逻辑

思考的维度:(1)业务规则是多变的(2)规则可配置(3)界面友好

比如对话场景下的一个标准化方式:

Q:<PersonName>的生日是<Date>吗?
A:哥,我不敢认识<PersonName>.Value啊。

当Q中能够匹配上述模板的时候,就会把PersonName对应实体的Value作为A的可变值进行填充。在自然语言生成系统中,有基于模板的生成技术(上文提到的专家系统),要求由句子模板和词汇模板。下面是一个值得观察的询问天气场景中的模板(例子来自《自然语言处理实践-聊天机器人技术原理与应用》):

其中,句子模板如下:

Topic->weather
	Act->query
		Content: weather\_state
			->3 对不起,请[<tell>]您需要[<refer>]{<where>}的[<what>]。
			->2 请[<tell>]您需要[<refer>]的[<what>|具体内容]。
			->1  抱歉,请[<tell>]您需要{<refer>}{(day)|今天|[when]}{(location)|当前城市|<where>}的[<what>]。

其中的符号说明:

|:或者
\[\]:内部元素出现次数>=1
{}:内部元素出现次数<=1
():对话管理模块的模板中的变量
<>:自定义语料中的变量
句子中的数字:该句子的权重,权重越大句子出现的可能性越大

词汇模板如下:

<tell>->[告诉我|补充|说明|输入]
<refer>->[查询|知道|获取|收到|了解|咨询]
<where>->[哪里|何处|什么位置|什么地方|什么城市|哪个位置|哪个区域]
<what>->[天气|哪方面信息|什么信息|哪方面情况|哪方面内容|什么内容]
<when>->[哪天|什么时间|哪个时辰|什么时候]

在其他的一些业务场景中,可能需要处理的情况较多,就需要对规则体系进行合理分层,设计的实现架构要能够灵活根据业务变化可插拔。在博主之前的关于SPO抽取的相关工作中,就对该特点要求严格。

基础技术

核心逻辑仍旧是if-else,不过正则表达式也是重要工作。在LAC的工作中,暂时不支持正则表达式,比如通配符的支持是下一版要实现的,但是理论上应该不是一个困难的工作。写正则的一个关键点是:服务hang死的问题。虽然博主个人的经验不多,但是之前已经观察到组里两位同学的正则在面对一些特殊case处理的时候hang死的问题。

基础的词法,句法技术同样可以作为规则引擎的匹配输入信息。

规则是更靠近业务侧的,对于算法同学来说,经典的场景是要求业务反馈的badcase的快修复,复杂且多变。引擎是执行规则的解析器,需要理解规则,并给出相应结果。一般而言,从开发角度来说,如果是硬编码规则,每次规则的更新都需要重新走一遍发布流程,这个时候需要配置化。此外,对规则的管理也是需要考虑的重点问题。本质上,想要通过引擎将规则交给更靠近业务的同学,开发的同学专注引擎的工作。这篇文章站在算法同学的角度来理解规则引擎。从整体上看,NLP算法的历史经历了从规则(专家系统)到统计到深度,但是今天的算法业务系统,实则是深度或者统计+规则的组合,不唯深度论,也不唯规则论。

相关工作:

1.从0到1:构建强大且易用的规则引擎,美团的技术实践

2.关于规则引擎,总结了规则引擎的几个开源实现

3.复杂风控场景下,如何打造一款高效的规则引擎

4.Should I use a Rules Engine?

5.Stanford RegexNER,支持多种自定义NER的后处理规则(百度的LAC也同样支持,但是功能不如RegexNER),如下:

第一种(直接标注实体):

Bachelor of Arts	DEGREE
Bachelor of Laws	DEGREE

第二种(或关系标注):

Bachelor of (Arts|Laws|Science|Engineering|Divinity)	DEGREE

第三种(多标签标注):

Bachelor of (Arts|Laws|Science|Engineering|Divinity)	DEGREE
Lalor	LOCATION	PERSON
Labor	ORGANIZATION

第四种(规则优先级)

Bachelor of (Arts|Laws|Science|Engineering|Divinity)	DEGREE		2.0
Lalor	LOCATION	PERSON
Labor ORGANIZATION
Bachelor of Arts	EASY_DEGREE
Bachelor of Laws	HARD_DEGREE		3.0

6.Stanford TokensRegex,是RegexNER更底层的实现,更加灵活的同时,也更加的复杂。