【Drools】NLUを用いて非構造化データをルールエンジンで扱えるようにする

ルールエンジン×AIは結構使い古されているネタな気もしますが、Droolsでの例がなかったので書いてみようかと。

[ルールエンジンの扱うデータ](#ルールエンジンの扱うデータ)
[NLUを用いたデータの構造化](#nluを用いたデータの構造化)
[サンプルコード](#サンプルコード)
[終わりに](#終わりに)
[参考](#参考)

## ルールエンジンの扱うデータ

ルールエンジンは、「〇〇という条件をみたすデータがあれば、✕✕という処理をする」というルールを蓄積し、入力データに対してルールに基づいた一連の処理を行うことのできる技術です。
業務ルールに関する処理を一元管理できるなどのメリットがあります。

ただ、ルールエンジンも内部で動いているのはプログラムのため、In/Outのデータは構造化されている必要があります。
ルールエンジンの保持するルールは、以下のような形式となります。

|  | 「人」データの項目「年齢」を指定の値と比較する | 「飲み物」データを生成し、項目「名前」を指定の値にする |
| ---- | ---- | ---- |
| 20歳以上ならビールが飲める | 人.年齢 >= 20 | 飲み物.名前 = "Beer" |
| 20歳未満ならジュースにする | 人.年齢 < 20 | 飲み物.名前 = "Orange Juice" |

これはルールエンジンが、
- 「人」データには項目「年齢」が存在すること
- 「人」データの項目「年齢」は、数値と比較できること
- 「飲み物」データには項目「名前」が存在すること
- 「飲み物」データの項目「名前」には文字列が設定できること

ということを前提に動いていることを指しています。
人間が思うままに書いたフリーフォーマットのデータを扱うことは、ルールエンジンの苦手とするところです。

## NLUを用いたデータの構造化

ここでNLU(Natural Language Understanding)の出番です。
NLUは自然言語(普段我々が使用している言葉)の意味を分析するAI技術です。

人が書いたテキストデータをうけて、いくつかのカテゴリに分類分けしたり、文章を書いた人間の感情を分析したりできます。

今回の記事では、テキストデータをNLUを用いて構造化し、ルールエンジンに投入することを考えてみます。

## サンプルコード

### 題材

あなたがSaaSを提供する会社の顧客サポート担当になったとします。
利用ユーザからポストされるレビューコメントを一つ一つ確認して、今後の開発やマーケティングに役立てることを考えています。
レビューの分析は以下のような流れで行います。

- レビューに込められた感情が、「ネガティブな感情」「ポジティブな感情」「ニュートラル(中立)」のいずれに当たるのか判断する
- レビューから導き出した感情と顧客情報をもとに、下記表のルールに基づいて優先順位を決定する
- 優先度の高いレビューから順に内容を精査し、必要に応じて関係部署(開発チーム等)への連携を行う

|  | レビューの感情 | ユーザの契約期間 | 優先順位 |
| ---- | ---- |---- |---- |
| 長期顧客_ネガティブ | ネガティブ | 2年以上 | 5 |
| 長期顧客_ニュートラル | ニュートラル | 2年以上 | 2 |
| 長期顧客_ポジティブ | ポジティブ | 2年以上 | 3 |
| 短期顧客_ネガティブ | ネガティブ | 2年未満 | 3 |
| 短期顧客_ニュートラル | ニュートラル | 2年未満 | 1 |
| 短期顧客_ポジティブ | ポジティブ | 2年未満 | 2 |

サンプルなので適当なルールになっていますが、実業務ではユーザの課金状況やアプリの使用頻度などもルールの判断条件に使えるかと思います。

ここで、ユーザレビューの感情を分析するのはNLUの仕事です。
レビューはフリーフォーマットのデータなので、まずNLUによってルールエンジンが扱いやすいデータに変換します。
続く優先順位付けの手続きでは、NLUが判定した感情と顧客情報をもとに、ルールエンジンで優先順位を決定します。


### アプリ構成

題材で感情分析を行う部分については、Azure AI サービスを使用します。
(かつてCognitive Servicesと呼ばれていたもののようです。2023/9/30現在[公式ドキュメント](https://learn.microsoft.com/ja-jp/azure/ai-services/what-are-ai-services)では「Azure AI サービス」となっています)

レビューの感情と顧客情報から優先順位を判断する部分は、DroolsのRule Unitという仕組みを用いて実現します。
構成としては以下の図のようになります。

```mermaid
graph
subgraph 0_[Application]
    1_[Main Logic]
    2_[Azure SDK] 
    subgraph RuleUnit
        3_[Rule]
        subgraph DataSource
            4_[DataStream]
            5_[SingletonStore] 
        end
    end
end

6_[Azure AI Services API] 
1_ -->| 1.感情分析 | 2_
2_ -->| 2.API呼び出し | 6_
1_ -->| 3. ルール実行 | RuleUnit
```

### Azure AI サービスを用いた感情分析

Azure AI サービスは、自然言語処理・画像検出・音声分析などのAI技術をAPIとして呼び出すことのできるAzureのサービスです。
今回は言語に関するAPIを呼び出し、レビューコメントの感情分析をさせてみます。

```AiApiClient.java
public class AiApiClient {

    public AnalyzedReviewComment analyzeComment(String comment) {

        // get token from env
        String key = System.getenv("AZURE_CREDENTIAL_KEY");
        String endpoint = System.getenv("AZURE_ENDPOINT");

        // set up api client
        AzureKeyCredential credential = new AzureKeyCredential(key);
        TextAnalyticsClient textAnalyticsClient = new TextAnalyticsClientBuilder()
            .credential(credential)
            .endpoint(endpoint)
            .buildClient();

        // analyze comment
        DocumentSentiment documentSentiment = textAnalyticsClient.analyzeSentiment(comment);
        System.out.println("======Analyze comment======");
        System.out.println("negative:" + documentSentiment.getConfidenceScores().getNegative());
        System.out.println("neutral:" + documentSentiment.getConfidenceScores().getNeutral());
        System.out.println("positive:" + documentSentiment.getConfidenceScores().getPositive());
        System.out.println("===========================");
         
        return new AnalyzedReviewComment(
                comment
                , documentSentiment.getConfidenceScores().getNegative()
                , documentSentiment.getConfidenceScores().getNeutral()
                , documentSentiment.getConfidenceScores().getPositive()
            );
    }
    
}
```

上記コードでは、Azure SDKを用いてAzure AIサービスのAPIを呼び出しています。

https://github.com/Azure/azure-sdk-for-java/tree/main/sdk/textanalytics/azure-ai-textanalytics

AzureのAPIからは、送信したテキストに含まれる「ネガティブ」「中立」「ポジティブ」の感情がどの程度かを数値で受け取ることができます。
この数値をもとに、ルールエンジンのInputとなるオブジェクトを生成しています。

SDKを用いてAzure AIサービスを利用するには、
- Azure アカウントの作成(無料アカウントで大丈夫です)
- Azure AIサービスの作成
- Azure AIサービスのクレデンシャルキーを取得し、アプリでロードできるようにする(今回は環境変数に設定しています)

が必要です。
詳細はAzureの公式ドキュメントをご参照ください。
<b>当たり前ですが、クレデンシャルキーなどの機密情報は、コードにべた書きしないようにご注意ください！！</b>

https://learn.microsoft.com/ja-jp/azure/ai-services/language-service/sentiment-opinion-mining/quickstart?tabs=windows&pivots=programming-language-csharp

### ルールエンジンで優先順位を判定

ここからは、RuleUnitを実装していきます。
RuleUnitについてはDroolsの公式ドキュメント、もしくは過去記事もご参照ください。

https://qiita.com/yagii/items/dd858d17d49e564ee6ec


以下がRuleUnitのクラス(一部)です。
判定に必要な顧客情報(`customer`)、感情分析されたレビューコメント(`analyzedReviewComment`)、ルールの判定結果である優先順位(`priority`)を保持しています。

```PriorityRuleUnit.java
public class PriorityRuleUnit implements RuleUnitData {
    
    private DataStream<Customer> customer;

    private DataStream<AnalyzedReviewComment> analyzedReviewComment;

    private SingletonStore<Priority> priority;
...
```

ルールの実態はDRLファイルに記載します。
一部抜粋したものを紹介しておきます。

```Priority.drl
package org.example.azure.negaposirule.ruleunit;
unit PriorityRuleUnit;


rule "長期顧客_ネガティブ"
    when
        /customer[ contractPeriod >= 2 ];
        /analyzedReviewComment[ sentiment().equals("negative") ];
    then
        priority.set(new Priority(5));
end


rule "長期顧客_ニュートラル"
    when
        /customer[ contractPeriod >= 2 ];
        /analyzedReviewComment[ sentiment().equals("neutral") ];
    then
        priority.set(new Priority(2));
end

rule "長期顧客_ポジティブ"
    when
        /customer[ contractPeriod >= 2 ];
        /analyzedReviewComment[ sentiment().equals("positive") ];
    then
        priority.set(new Priority(3));
end

...
```

`/analyzedReviewComment[ sentiment().equals("negative") ];`の部分では、AnalyzedReviewCommentインスタンスのsentiment()メソッドを呼び出し、結果を判定に利用しています。

```AnalyzedReviewComment.java
public record AnalyzedReviewComment(
    String comment
    , double negative
    , double neutral
    , double positive) {

    public String sentiment() {
        // Azure AIサービスで算出した感情分析の数値で最も点数が高いものを算出し、
        // negative, neutral, positiveのいずれかの結果を返す
        var sentimentMap = new HashMap<String, Double>();
        sentimentMap.put("negative", this.negative);
        sentimentMap.put("neutral", this.neutral);
        sentimentMap.put("positive", this.positive);

        String maxKey = sentimentMap.entrySet().stream()
            .max((entry1, entry2) -> entry1.getValue().compareTo(entry2.getValue()))
            .get()
            .getKey();

        return maxKey;
    }
}
```

ルール呼び出し部分のコードは以下です。
PriorityRuleUnitインスタンスを生成し、判定に必要なデータを投入しています。
RuleUnitInstanceのfire()メソッドで、ルールを実行させています。
最後にRuleUnitのクエリという機能を使って、判定結果のデータを取得しています。
```DecisionService.java
public class DecisionService {

    public Priority decidePriority(Customer customer, AnalyzedReviewComment comment) {
        PriorityRuleUnit priorityRuleUnit = new PriorityRuleUnit();
        RuleUnitInstance<PriorityRuleUnit> instance = RuleUnitProvider.get().createRuleUnitInstance(priorityRuleUnit);

        priorityRuleUnit.getCustomer().append(customer);
        priorityRuleUnit.getAnalyzedReviewComment().append(comment);

        // execute rule 
        instance.fire();

        // get result by query
        Priority priority = null;
        var queryResult = instance.executeQuery("FindPriority").toList();
        if (queryResult.size() == 1) {
            priority = (Priority) queryResult.get(0).get("$p");
        }
        instance.close();

        return priority;
    }
    
}
```

### メインロジック

Mainクラスは以下です。
契約期間が2年(長期顧客)の太郎さんが「UIが少しわかりづらいです。」という批判的なレビューをコメントをしたとします。

```Main.java
public class Main {

    public static void main(String[] args) {
        var customer = new Customer("Taro", 2);

       // call API to analyze comment
        String reviewComment = "UIが少しわかりづらいです。" ;
        AiApiClient ac = new AiApiClient();
        AnalyzedReviewComment analyzedReviewComment = ac.analyzeComment(reviewComment);
    
        // decide the priority 
        DecisionService ds = new DecisionService();
        var priority = ds.decidePriority(customer, analyzedReviewComment);

        System.out.println(priority);
    }
}
```

実行結果は以下のようになります。
```
======Analyze comment======
negative:0.95
neutral:0.05
positive:0.0
===========================
Priority[value=5]
```
negative:0.95とあるように、当該レビューの感情はネガティブなものと判定されています。
優先度は、長期顧客のネガティブなコメントなので、`5`と判定されています。


## 終わりに

今回はNLUとルールエンジンとを組み合わせた題材を紹介しました。
自然言語処理以外にも、他のAI技術とも組み合わせることでルールエンジンがさらに便利になっていきそうと感じました。

本当は今話題のOpenAI API(GPT)での題材にしようと思っていたのですが、無料クレジットの有効期限がきれてしまい。。
[ボツになったプロジェクト](https://github.com/Hami1ton/study_brms/tree/main/drools/integrate_with_openai/unstructured_input)


## 参考

Azure AI Language公式ドキュメント

https://learn.microsoft.com/ja-jp/azure/ai-services/language-service/

Drools公式ドキュメント

https://docs.drools.org/8.44.0.Final/drools-docs/drools/introduction/index.html

紹介したコードの全量は[こちら](https://github.com/Hami1ton/study_brms/tree/main/drools/azure/negaposi_rule)