现实生活中,规则无处不在。法律、法规和各种制度均是;对于企业级应用来说,在IT技术领域,很多地方也应用了规则,比如路由表,防火墙策略,乃至角色权限控制(RBAC),或者Web框架中的URL匹配。不管是那种规则,都规定了一组确定的条件和此条件所产生的结果。
举一个例子:
IF
- 汽车是红色
- 车是运动型的
- 驾驶员是男性
- 驾驶员在16-25岁之间
THEN
- 保险费用增加20%
从这个例子可以看出:
- 每条规则都是一组条件决定的一系列结果
- 一条规则可能与其他规则共同决定最终结果。比如例子中的规则只产生了增量,还需要与确定基数的规则共同作用才能决定最终的费率
- 可能存在条件互相交叉的规则,此时有必要规定规则的优先级
规则作为一种知识,其典型运用就是通过实际情况,根据给定的一组规则,得出结论。这个结论可能是某种静态的结果,也可能是需要进行的一组操作。这种规则的运用过程叫做推理。如果由程序来处理推理过程,那么这个程序就叫做推理机/推理引擎。推理引擎根据知识表示的不同采取的控制策略也是不同的,常见的类型包括基于神经网络、基于案例和基于规则的推理机。其中,基于规则的推理机易于理解、易于获取、易于管理,被广泛采用。这种推理引擎被称为“规则引擎”。
规则引擎起源于基于规则的专家系统(专家系统CLIPS:源于1984年NASA的人工智能项目,现已开源,由C编写。),而基于规则的专家系统又是专家系统的其中一个分支。专家系统属于人工智能的范畴,它模仿人类的推理方式,使用试探性的方法进行推理,并使用人类能理解的术语解释和证明它的推理结论。基于规则的专家系统(RBES)包括三部分:Rule Base(knowledge base)、Working Memory(fact base)和Inference Engine。它们的结构如下系统所示:
推理引擎(Inference Engine)包括三部分:模式匹配器(Pattern Matcher)、议程(Agenda)和执行引擎(Execution Engine)。推理引擎通过决定哪些规则满足事实或目标,并授予规则优先级,满足事实或目标的规则被加入议程。
- 模式匹配器决定选择执行哪个规则,何时执行规则;
- 议程管理模式匹配器挑选出来的规则的执行次序;
- 执行引擎负责执行规则和其他动作。
和人类的思维相对应,规则引擎中也存在两种推理方式:正向推理(Forward-Chaining)和反向推理(Backward-Chaining)。
- 正向推理也叫演绎法,由事实驱动,从 一个初始的事实出发,不断地应用规则得出结论。首先在候选队列中选择一条规则作为启用规则进行推理,记录其结论作为下一步推理时的证据。如此重复这个过程,直到再无可用规则可被选用或者求得了所要求的解为止。
- 反向推理也叫归纳法,由目标驱动,首先提出某个假设,然后寻找支持该假设的证据,若所需的证据都能找到,说明原假设是正确的;若无论如何都找不到所需要的证据,则说明原假设不成立,此时需要另做新的假设。
将事实与规则进行匹配的算法。常见的模式匹配算法有RETE,LFA,TREAI,LEAPS。Rete算法是目前效率最高的一个演绎法推理算法,许多规则引擎都是基于Rete算法来进行推理计算的。
推理引擎的推理步骤如下:模式匹配、冲突消解、执行引擎。
- 将初始数据(fact)输入 Working Memory 。
使用 Pattern Matcher 比较规则库(rule base)中的规则(rule)和数据(fact)。 - 如果执行规则存在冲突(conflict),即同时激活了多个规则,将冲突的规则放入冲突集合。
- 解决冲突,将激活的规则按顺序放入Agenda。
- 使用执行引擎执行Agenda中的规则。重复步骤2至5,直到执行完毕所有Agenda中的规则。
规则引擎的作用:
- 规则外部化,即有利于规则知识的复用,也可避免改变规则时带来的代码变更问题
- 由规则引擎使用某种算法进行推理过程,不需要编写复杂晦涩的逻辑判断代码
- 开发人员的不需要过多关注逻辑判断,可以专注于逻辑处理
RETE算法
Rete在拉丁语中是“net”,有网络的意思。Rete算法由Carnegie Mellon University的Dr Charles L. Forgy设计发明,是一个用来实现产生式规则系统(production/inference)的高效模式匹配算法。
RETE算法可以分为两部分:规则编译(rule compilation)和运行时执行(runtime execution)。规则编译是指根据规则集生成推理网络的过程,运行时执行指将数据送入推理网络进行筛选的过程。
相关概念:
- 事实(Fact):对象之间及对象属性之间的关系
- 规则(rule):是由条件和结论构成的推理语句,一般表示为if…Then。一个规则的if部分称为LHS(left-hand-side),then部分称为RHS(right hand side)。
- 模式(module):就是指IF语句的条件。这里IF条件可能是有几个更小的条件组成的大条件。模式就是指的不能在继续分割下去的最小的原子条件。
RETE推理网络的生成过程:从规则集{规则1,规则2……..}中拿出一条来,根据一定算法,变成RETE推理网络的节点。不断循环将所有规则都处理完,RETE推理网络就生成了。RETE网络主要分为两个部分,alpha网络和beta网络。如下图所示。
- alpha网络:过滤working memory,找出符合规则中每一个模式的集合,生成alpha memory(满足该模式的集合)。有两种类型的节点,过滤type的节点和其他条件过滤的节点(我觉得这两种是依照需要设定的,也并不一定需要两种节点)。
- Beta网络:有两种类型的节点Beta Memory和Join Node。前者主要存储Join完成后的集合。后者包含两个输入口,分别输入需要匹配的两个集合,由Join节点做合并工作传输给下一个节点。
在一个产生式系统中,主要流程可以分为以下步骤:
- Match:找出符合LHS部分的working memory集合
- Confilict resolution:选出一个条件被满足的规则
- Act:执行RHS的内容
- 返回1
RETE算法主要改进Match的处理过程,通过构建一个网络进行匹配。
具体过程如下:
- 创建root节点(根节点),推理网络的入口。
- 拿到规则1,从规则1中取出模式1(前面说了,模式就是最小的原子条件,所以规则模式的关系是1:n)。
a) 检查模式1中的参数类型,如果是新类型,添加一个类型节点。
b) 检查模式1对应的Alpha节点是否存在,如果存在记录下节点的位置;如果没有,将模式1作为一个Alpha节点加入到网络中。同时根据Alpha节点建立Alpah内存表。
c) 重复b,直到处理完所有模式。
d) 组合Beta节点:Beta(2)左输入节点为Alpha(1),右输入节点为Alpha(2);Beta(i)左输入节点是Beta(i-1),右输入节点为Alpha(i),并将两个父节点的内存表内联成为自己的内存表
e) 重复d,直到所有Beta节点处理完毕
f) 将动作Then部分封装成最后节点做为Beta(n)
- 重复2,直到所有规则处理完毕
下面是一个从网上找得例子:
规则P1:
LHS:
- C1:(年纪:研2)
- C2:(性别:男)
- C3:(身材:较瘦)
- C4:(身高:大于175cm)
RHS:
- 标点符
Rete算法优于传统的模式匹配算法的特点
- 状态保存。事实集合中的每次变化,其匹配后的状态都被保存再alpha和beta节点中。在下一次事实集合发生变化时,绝大多数的结果都不需要变化,rete算法通过保存操作过程中的状态,避免了大量的重复计算。Rete算法主要是为那些事实集合变化不大的系统设计的,当每次事实集合的变化非常剧烈时,rete的状态保存算法效果并不理想。
- 节点共享
Drools中用到的RETE算法
编译算法描述了规则如何在Production Memory中产生一个有效的辨别网络。用一个非技术性的词来说,一个辨别网络就是用来过滤数据。方法是通过数据在网络中的传播来过滤数据。在顶端节点将会有很多匹配的数据。当我们顺着网络向下走,匹配的数据将会越来越少。在网络的最底部是终端节点(terminal nodes)。在Dr Forgy的1982年的论文中,他描述了4种基本节点:root, 1-input, 2-input and terminal。
下图是Drools中的RETE节点类型:
根节点(RootNode)是所有的对象进入网络的入口。然后,从根节点立即进入到ObjectTypeNode。ObjectTypeNode的作用是使引擎只做它需要做的事情。例如,我们有两个对象集:Account和Order。如果规则引擎需要对每个对象都进行一个周期的评估,那会浪费很多的时间。为了提高效率,引擎将只让匹配object type的对象通过到达节点。通过这种方法,如果一个应用assert一个新的account,它不会将Order对象传递到节点中。很多现代RETE实现都有专门的ObjectTypeNode。在一些情况下,ObjectTypeNode被用散列法进一步优化。
ObjectTypeNode能够传播到AlphaNodes,LeftInputAdapterNodes和BetaNodes。
1-input节点通常被称为AlphaNode。AlphaNodes被用来评估字面条件(literal conditions)。虽然,1982年的论文只提到了相等条件(指的字面上相等),很多RETE实现支持其他的操作。例如,Account.name == “Mr Trout”是一个字面条件。当一条规则对于一种object type有多条的字面条件,这些字面条件将被链接在一起。这是说,如果一个应用assert一个account对象,在它能到达下一个AlphaNode 之前,它必须先满足第一个字面条件。在Dr. Forgy的论文中,他用IntraElement conditions来表述。下面的图说明了Cheese的AlphaNode组合(name == “cheddar”,strength == “strong”):
Drools通过散列法优化了从ObjectTypeNode到AlphaNode的传播。每次一个AlphaNode被加到一个ObjectTypeNode的时候,就以字面值(literal value)作为key,以AlphaNode作为value加入HashMap。当一个新的实例进入ObjectTypeNode的时候,不用传递到每一个AlphaNode,它可以直接从HashMap中获得正确的AlphaNode,避免了不必要的字面检查。
2-input节点通常被称为BetaNode。Drools中有两种BetaNode:JoinNode和NotNode。BetaNodes被用来对2个对象进行对比。这两个对象可以是同种类型,也可以是不同类型。
我们约定BetaNodes的2个输入称为左边(left)和右边(right)。一个BetaNode的左边输入通常是a list of objects。在Drools中,这是一个数组。右边输入是a single object。两个NotNode可以完成‘exists’检查。Drools通过将索引应用在BetaNodes上扩展了RETE算法。下图展示了一个JoinNode的使用:
注意到图中的左边输入用到了一个LeftInputAdapterNode,这个节点的作用是将一个single Object转化为一个单对象数组(single Object Tuple),传播到JoinNode节点。因为我们上面提到过左边输入通常是a list of objects。
Terminal nodes被用来表明一条规则已经匹配了它的所有条件(conditions)。 在这点,我们说这条规则有了一个完全匹配(full match)。在一些情况下,一条带有“或”条件的规则可以有超过一个的terminal node。
Drools通过节点的共享来提高规则引擎的性能。因为很多的规则可能存在部分相同的模式,节点的共享允许我们对内存中的节点数量进行压缩,以提供遍历节点的过程。下面的两个规则就共享了部分节点:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
rule
when
Cheese( $chedddar : name == " cheddar " )
$person : Person( favouriteCheese == $cheddar )
then
System. out .println( $person.getName() + " likes cheddar " );
end
rule
when
Cheese( $chedddar : name == " cheddar " )
$person : Person( favouriteCheese != $cheddar )
then
System. out .println( $person.getName() + " does likes cheddar " );
end
|
这里我们先不探讨这两条rule到的是什么意思,单从一个直观的感觉,这两条rule在它们的LHS中基本都是一样的,只是最后favouriteCheese,一条规则是等于$cheddar,而另一条规则是不等于$cheddar。下面是这两条规则的节点图:
从图上可以看到,编译后的RETE网络中,AlphaNode是共享的,而BetaNode不是共享的。上面说的相等和不相等就体现在BetaNode的不同。然后这两条规则有各自的Terminal Node。
RETE算法的第二个部分是运行时(runtime)。当一个应用assert一个对象,引擎将数据传递到root node。从那里,它进入ObjectTypeNode并沿着网络向下传播。当数据匹配一个节点的条件,节点就将它记录到相应的内存中。这样做的原因有以下几点:主要的原因是可以带来更快的性能。虽然记住完全或部分匹配的对象需要内存,它提供了速度和可伸缩性的特点。当一条规则的所有条件都满足,这就是完全匹配。而只有部分条件满足,就是部分匹配。(我觉得引擎在每个节点都有其对应的内存来储存满足该节点条件的对象,这就造成了如果一个对象是完全匹配,那这个对象就会在每个节点的对应内存中都存有其映象。)
参考链接: