Drools 5.0 入門 3C (優先度)

認知-実行サイクル

 ルールベースの一般論(プロダクションシステムとは)のところでも書きましたが、前向き推論のようなデータ駆動のルールエンジンでは、認知実行サイクルの中の競合解消(Conflict Resolution)というフェーズが実行に大きな影響を与えてきます。ここでは、その競合解消とそれに付随した優先度(salience)について見ていきましょう。

競合解消(Conflict Resolution)と優先度(Salience)

 データ駆動の前向き推論の実行は、認知-実行サイクルというモデルに基づいています。

 認知実行サイクル

まず、

1.照合: ルールのLHS (Left Hand Side – when節)とファクトとをマッチさせる。マッチしたルールとファクトの組(Droolsでは、アクティベーション(Activation)と呼んでいる)は、アジェンダ(Agenda)に加えられる。

次に

2.競合解消: アジェンダにあるアクティベーションからどれを実行するか選択する。

最後に

3.選択されたアクティベーションに含まれるルールのRHS (Right Hand Side – then節) を実行する。

そして、再度照合のフェーズに移る・・・ということを繰り返し、 マッチするルールがなくなったところで実行が終了します。

 たとえとしてよく引き合いに出されるのが自動車の運転で、上記にあわせてモデル化してみると

1.まず周りの状況に合わせて、次にどのような操作が可能か候補をあげる。
2.候補の中から、何をしなければいけないか判断する。
3.実際の操作を行う。

となります。

 さて上に見たように前向き推論の実行では競合解消はひとつの要であり、複数のアクティベーション(ルールとファクトの組)がアジェンダに載ったとき、どのアクティベーションを選ぶか、決まった規則をあらかじめ決めておかなければなりません。これを戦略(競合解消戦略)と呼びます。この「戦略」、どのような戦略を採用するかは議論のわかれるところで、Droolsでは、単純に LIFO(より新しくできたファクトにマッチしたアクティベーションを優先し、それ以外は任意)となっていますが、たとえばOPS5というルールエンジンに採用されているLEXとかMEAや、ルールエンジンによっては問題にあわせて戦略を自由にインプリメントできたりするようなものさえもあります。
 単純に「戦略」といってもなかなかわかりにくいかと思いますので、たとえばということでOPS5のLEX戦略を例にあげてみましょう。この戦略は簡単に言うと、より新しくできたファクトにマッチしたアクティベーションを優先すること(すなわち、できるだけ新しい状況にマッチしたアクティベーションを優先する)や、より条件の多いwhen節を持ったルールのアクティベーションを優先すること(すなわち、一般原則のみにマッチするものよりも、より特殊な条件、例外規則にマッチしたものを優先する)などという規則にしたがった戦略です。考え方としては比較的受け入れられやすいのではないでしょうか。
 ただ、戦略は確かに前向き推論の実行順序を決める重要な規則ではありますが、一方で、OPS5のように作りつけの固定した戦略を持ってしまうと、逆にトリッキーなプログラミングを強要するという批判もあり、Droolsでは基本的な戦略としてはごく単純にLIFOとし、ルールの制御には、優先度をつけるということで割り切ったものと思います(もちろんDroolsのソースを弄って問題に合わせた戦略をインプリメントすることは可能でしょう)。

 先ほどからルールの「優先度」という言葉が出てきていますが、この「優先度」を考えるために、非常に単純化した形ではありますが自動車の運転をモデルにしたひとつの例を考えてみましょう。まず、比較的常識的な

1.交差点があり目の前の信号が赤であれば停止する。

2.交差点があり目の前の信号が青であればそのまま進む。

3.交差点に人がいれば停止する(またはハンドルを切ってよける)。

というルールを考えましょう。これらをDroolsのルールっぽい擬似コードで書くと、

rule “ルール1”
  When
 交差点手前にいる
 信号は赤
 Then
 停止する

rule “ルール2”
  When
 交差点手前にいる
 信号は青
 Then
 そのまま進む

rule “ルール3”
  When
 交差点手前にいる
 交差点に人がいる
 Then
 停止する

さて、次に状況が、

 交差点手前にいる
 信号は青
 (しかし)交差点に人がいる

となったとしましょう。このときこの状況にマッチするルールは 2 と 3 の 2つがあります。

このうち、どちらが実行されるべきでしょうか・・・当然ルール3が実行されるべきでしょう。すなわち競合解消でルール2よりもルール3が選ばれるようにすべきでしょう。
このようなときにルールに優先度(salience)をつけます。Droolsでは優先度はWhen節の前に salience というキーワードを用いて優先度をつけます。salienceはデフォルトでは0で、salienceの値が大きい方が優先されます。したがって上記の場合は、ルール2をデフォルトのままとすると、たとえば ルール3 に salience 100 をつけて

rule “ルール3”
 salience 100
  When
 交差点手前にいる
 交差点に人がいる
 Then
 停止する

とすればよいでしょう。

 

サンプルプログラム

 では、この辺でサンプルプログラムを見てみましょう。先ほどのスーパーの割引の例で会員割引を加えてみました。

 [ルールの仕様]

 1.会員は、一律5%の割引がある

 2.5000円以上お買い上げの場合一律に500円を割引する

としましょう。

 まず、 メインとなるJavaプログラムは枠組みとしては前に提示した DroolsTest1.java と変わりません。読み込むルールファイルがDroolsTest2.drlとなったところとそれに付随するエラーメッセージ、ログファイルの設定の変更。それから Sales クラスに会員(member)という属性を加えました。それに伴い、メインプログラムで fact をinsert する部分で、Salesのインスタンスをつくる際に member の設定を加えて、ここでは会員が、12000円の買い物をしたということをファクトとして insert しました。

DroolsTest2.java

package jp.co.iluminado.example;

import java.util.Collection;

import org.drools.KnowledgeBase;
import org.drools.KnowledgeBaseFactory;
import org.drools.builder.KnowledgeBuilder;
import org.drools.builder.KnowledgeBuilderFactory;
import org.drools.builder.ResourceType;
import org.drools.definition.KnowledgePackage;
import org.drools.event.rule.DebugAgendaEventListener;
import org.drools.event.rule.DebugWorkingMemoryEventListener;
import org.drools.io.ResourceFactory;
import org.drools.logger.KnowledgeRuntimeLogger;
import org.drools.logger.KnowledgeRuntimeLoggerFactory;
import org.drools.runtime.StatefulKnowledgeSession;

/**
 * This is a sample file to launch a rule package from a rule source file.
 */
public class DroolsTest2 {

    public static final void main(final String[] args) throws Exception {
        final KnowledgeBuilder kbuilder = KnowledgeBuilderFactory
                .newKnowledgeBuilder();

        // ルールファイルをコンパイルしてKnowledgeBuilderへ追加する
        kbuilder.add(ResourceFactory.newClassPathResource(“DroolsTest2.drl”,
                DroolsTest1.class), ResourceType.DRL);

        // 上記コンパイル時のエラー処理
        if (kbuilder.hasErrors()) {
            System.out.println(kbuilder.getErrors().toString());
            throw new RuntimeException(“Unable to compile \”DroolsTest2.drl\”.”);
        }

        // KnowledgeBuilderからコンパイル済みのパッケージを取得する
        final Collection<KnowledgePackage> pkgs = kbuilder
                .getKnowledgePackages();

        // 取得したパッケージをKnowledgebaseに追加する(パッケージのデプロイ)
        final KnowledgeBase kbase = KnowledgeBaseFactory.newKnowledgeBase();
        kbase.addKnowledgePackages(pkgs);

        // Statefulセッションを作成
        final StatefulKnowledgeSession ksession = kbase
                .newStatefulKnowledgeSession();

        // Agenda,WorkingMemoryのView表示準備
        ksession.addEventListener(new DebugAgendaEventListener());
        ksession.addEventListener(new DebugWorkingMemoryEventListener());

        // Auditログのセット
        KnowledgeRuntimeLogger logger = KnowledgeRuntimeLoggerFactory
                .newFileLogger(ksession, “log/DroolsTest2”);

        // ルールの実行
        final Sales sale = new Sales(12000, Sales.NOT_APPLIED, Sales.MEMBER);
       
        ksession.insert(sale);
        ksession.fireAllRules();

        // Auditログを閉じる
        logger.close();

        // Statefulセッションなので、実行が終わったところでdisposeして領域を開放する
        ksession.dispose();
    }

    public static class Sales {
       
        public static final int NOT_APPLIED = 0;  // 未適用
        public static final int APPLIED = 1;        // 適用済

        public static final int NONMEMBER = 0;  // 
        public static final int MEMBER = 1;        //

        private long sales;   // 売り上げ
        private int status;   // 状態(未適用、適用済)
        private int member;   // 会員

        public Sales(long sales,
                     int status,
                     int member) {
            this.sales = sales;
            this.status = status;
            this.member = member;
        }

        public int getStatus() {
            return this.status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

        public void setSales(long sales) {
            this.sales = sales;
        }

        public long getSales() {
            return sales;
        }

        public int getMember() {
            return member;
        }

        public void setMember(int member) {
            this.member = member;
        }

    }

}

次にルールファイル。上にあげたルールの仕様1が Discount1 ルール、仕様2が Discount2 ルールにあたります。Discount1のルールは LHS で、statusが未適用で、会員である場合の Sales ファクトにマッチするように書かれています。変数 $s は、マッチしたファクトに紐付けられる変数で、変数 $salesValue は、マッチしたファクトのsalesフィールドに紐付けられ RHS で参照されます。RHSでは、LHSでマッチしたファクトの sales フィールドの5%引きにし、statusを適用にセットします。最後のprintlnは、コンソールにルールが発火したことを知らせるメッセージです。Discount2 のルールは、同様に仕様2と比較してみると理解できるかと思います。ルールprintoutは結果を表示するルールです。

DroolsTest2.drl (優先度なし)

package jp.co.iluminado.example

import jp.co.iluminado.example.DroolsTest2.Sales;

rule “Discount1”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
                    $salesValue : sales,
                    member == Sales.MEMBER )
    then
        $s.setSales((long) ($salesValue * 0.95));
        $s.setStatus(Sales.APPLIED);
        update( $s );
        System.out.println( “Discount1 rule fired” );
end

rule “Discount2”
    when
        $s : Sales( status == Sales.NOT_APPLIED,
              $salesValue : sales >= 5000 )
    then
        $s.setSales($salesValue – 500);
        $s.setStatus(Sales.APPLIED);
        update( $s );
        System.out.println( “Discount2 rule fired” );
end

rule “Printout”
    when
        $s : Sales( status == Sales.APPLIED)
    then
        System.out.println(  $s.getSales() );
        System.out.println( “Printout rule fired” );
end

 

 ひとまず優先度なしで動かしてみましょう。

優先度なしの実行

会員で、12000円の買い物をすると通常考えると会員割引が適用されて、12000円の5%引きの11400円になるはずですが、ここではルールの2のほう、5000円以上のお買い物で500円引きが適用されてしまっています。これは Drools が競合解消戦略として LIFO を適用しており後にあるルールを優先的に実行しているためです。

さて会員割引のほうを優先するために、ここで優先度(salience)をつけてみましょう。

DroolsTest2.drl (優先度つき)

package jp.co.iluminado.example

import jp.co.iluminado.example.DroolsTest2.Sales;

rule “Discount1”
    salience 100
    when
        $s : Sales( status == Sales.NOT_APPLIED,
                    $salesValue : sales,
                    member == Sales.MEMBER )
    then
        $s.setSales((long) ($salesValue * 0.95));
        $s.setStatus(Sales.APPLIED);
        update( $s );
        System.out.println( “Discount1 rule fired” );
end

rule “Discount2”
    salience 50
    when
        $s : Sales( status == Sales.NOT_APPLIED,
              $salesValue : sales >= 5000 )
    then
        $s.setSales($salesValue – 500);
        $s.setStatus(Sales.APPLIED);
        update( $s );
        System.out.println( “Discount2 rule fired” );
end

rule “Printout”
    when
        $s : Sales( status == Sales.APPLIED)
    then
        System.out.println(  $s.getSales() );
        System.out.println( “Printout rule fired” );
end

 

Discount1のほうを優先するために、Discount2よりも大きな優先度をつけました。これで実行してみましょう。

 優先度ありの実行

 Discount1のほうが実行されたことがわかります。

 このように優先度を設定すると競合解消の際に実行するルールを制御することができます。

 ただ、今回のサンプルの例で言うと、会員がたとえば6000円の買い物をしたときにも会員割引のほうが適用されてしまいます(会員割引だと5700円、非会員だと500円引きで5500円)。これは常識的に考えるとちょっと・・・となるでしょう。次の記事では、上のような形で直接に優先度を使わず、もう少し常識的な例へと修正しましょう。