現實生活中,規則無處不在。法律、法規和各種制度均是;對於企業級應用來說,在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並沿着網絡向下傳播。當數據匹配一個節點的條件,節點就將它記錄到相應的內存中。這樣做的原因有以下幾點:主要的原因是可以帶來更快的性能。雖然記住完全或部分匹配的對象需要內存,它提供了速度和可伸縮性的特點。當一條規則的所有條件都滿足,這就是完全匹配。而只有部分條件滿足,就是部分匹配。(我覺得引擎在每個節點都有其對應的內存來儲存滿足該節點條件的對象,這就造成了如果一個對象是完全匹配,那這個對象就會在每個節點的對應內存中都存有其映象。)
參考鏈接: