RAGの品質をwatonx.aiのLLMとwatsonx.governanceのAPIを用いて測定

# はじめに

生成AIではハルシネーションなどの問題を軽減するためにRAGの仕組みを利用しているが、RAGの回答の品質を客観的に評価できる方法について考えたことはないですか？
運用しているうちに、
* ユーザーからの質問内容が変わってしまった
* ベクトルDBなどの検索部分のデータを増やしたら以前は検索で引っかかっていたデータが引っ掛からなくなってしまった
* 生成AIのモデルを変えた方がいいか迷っている

などなど、いろいろと本番運用する上でRAGを評価できるツールは必要になっていきます。

RAGの品質について、LLMを利用してIBM Watsonx.governanceでの測定を試したので、日本語のRAGを評価できたことやその手順を紹介します。

# RAGでの主な評価指標

RAGの主な４つの指標を算出します
（https://www.ibm.com/docs/en/cloud-paks/cp-data/5.0.x?topic=models-metrics-computation-using-python-sdk#answer-quality を参照）

### コンテキストの関連度（ Context relevance ）

* RAGの検索部分が対象
* 質問と検索結果であるコンテキストを評価
* コンテキストが質問とどの程度関連しているかを評価する
* スコアが高いほど、コンテキストがプロンプトの質問に関連性が高いことを示す

### 忠実性（ Faithfulness ）

* RAGの回答生成部分が対象
* コンテキストと生成AIの出力を評価
* モデル出力がモデルのコンテキストにどれだけ忠実であるかを測定する
* スコアが高いほど、幻覚性（ハルシネーション）が低い

### 回答の関連性（ Answer relevance ）

* RAGの全体が対象
* 質問と回答を評価
* モデルの回答が質問にどの程度関連しているかを測定する
* スコアが高いほど、質問に対して適切な回答を提供している

### 回答の類似性（ Answer similarity ）

* RAGの全体が対象
* 生成AIの出力した回答を（模範回答と比較して）評価 
* 生成された回答が正解の回答や参照の回答とどの程度類似しているかを測定する
* スコアが高いほど、生成された回答がリファレンスの回答に類似している

# 試したこと

以下の２つのサンプルコードを参考にして、RAGにおける日本語の質問、検索結果、回答などをwatsonx.aiのLLMを用いて、watsonx.governanceで品質を評価してみました。

1. [watsonx.aiのLLMを用いて、RAGの品質を評価するサンプルコード](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/RAG%20metrics%20computation%20with%20llm%20and%20IBM%20watsonx.gov.ipynb)
2. [日本語対応するためのwatsonx.governanceのサンプルコード](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/Multi%20Lingual%20Support%20Notebook.ipynb)


## 環境 

### 前提条件

* watsonx.aiとwatsonx.governanceはSaaSを利用
* python 3.11

:::note warn
M1 Mac（arm64）では必要なライブラリ`ibm-metrics-plugin`がインストールできなかったため、コンテナ環境`--platform linux/x86_64`で実施しました。（2024年12月）
:::


### 準備

必要なものは2つ

1. IBM Cloud の `API Key`
2. watsonx.ai の `プロジェクトID`


## 全体の流れ

1. 必要なライブラリのインストール
2. 日本語のトークナイザを作成
3. 試してみるデータを準備
4. watsonx.governanceへの認証
5. スコアリング関数にwatsonx.aiのモデルを設定
6. 計算する指標のConfigを構成
7. 計算
8. 結果の取得

## 実際に試してみます

### 1. 必要なライブラリのインストール

watsonx.ai や watsonx.governance を利用するためのライブラリをインストールします。
```sh:必要なライブラリのインストール
pip install -U "ibm-metrics-plugin~=3.0.9" | tail -n 1
pip install -U ibm-watson-openscale | tail -n 1
pip install -U ibm_watsonx_ai | tail -n 1
pip install -U ibm-watson-machine-learning | tail -n 1
```

自然言語処理のライブラリをインストールします。
```sh:自然言語処理のライブラリのインストール
pip install nltk
```

カスタムで日本語のトークナイザを用いるためにSpaCyのライブラリをインストールします。
(https://github.com/explosion/spaCy/issues/13528 と同様のエラーの事象が発生したので、numpyのバージョン指定してインストールしました。)
```sh:SpaCyライブラリのインストール
pip install numpy==1.26.4 spacy
```

### 2. 日本語のトークナイザを作成

[多言語対応のサンプルコードのシナリオ２](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/Multi%20Lingual%20Support%20Notebook.ipynb)を参考に日本語のカスタムトークナイザ `my_custom_tokenizer_ja`を作成。

```python:日本語のカスタムトークナイザを作成
language_code = "ja"

import spacy

import nltk
nltk.download('wordnet')

def get_language_model_sp(language='en'):
    nlp = None
    if language=='en':
        nlp = spacy.load("en_core_web_sm")
    if language=='ja':
        nlp = spacy.load("ja_core_news_sm")
    return nlp

class MyCustomTokenizer():    
    def __init__(self, language = 'en'):
        self.language = language
        self.tokenizer = None
    
    def tokenize(self, input_text):
        tokens = []
        nlp = get_language_model_sp(self.language)        
        doc = nlp(input_text)
        for token in doc:
            tokens.append(str(token))
        return tokens
    
    def __call__(self, input_text):
        return self.tokenize(input_text)

my_custom_tokenizer_ja = MyCustomTokenizer(language_code).tokenize
```

### 3. 試してみるデータを準備

RAGで取得できる、ユーザーからの質問、検索結果、生成AIの出力、をセットする。
また、回答の類似性（Answer similiarity）を測定するために模範回答もセットする。
（各データは環境に合わせてセットしてください。）

```python:品質を評価するためのデータ準備
query1 = "ユーザからの質問"
context1 = "質問内容から検索結果"
answer1 = "質問と検索結果を元に生成AIが回答した内容"
ground_truth1 = "正解の回答内容"

uestions = [query1]
contexts = [context1]
answers = [answer1]
ground_truth = [ground_truth1]
```

サンプルコードに倣って、以下も実行します。
```python
import pandas as pd
data = pd.DataFrame(contexts, columns=["context1"])
data["question"] = questions
data["answer"] = answers
data["ground_truth"] = ground_truth
data
```

### 4. watsonx.governanceへの認証

API Keyやwatsonx.aiのURLをセットします。
（API Keyの値は環境に合わせて設定してください。）
```python:API Keyやwatsonx.aiのURLをセット
from ibm_watsonx_ai import APIClient
from ibm_watsonx_ai import Credentials

credentials = Credentials(
    url = "https://us-south.ml.cloud.ibm.com",
    api_key="xxxxxxxxxxxxxx",
)
```

:::warn
watsonx.aiで東京リージョンを利用する場合は、以下に書き換えてください
```python:watsonx.aiが東京リージョンの場合
    url = "https://jp-tok.ml.cloud.ibm.com",
```
:::


watsonx.aiのプロジェクトIDを変数にセットします。

```python:プロジェクトIDをセット
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
```

watsonx.governanceのAPIで認証します。

```python:watsonx.governanceの認証
from ibm_cloud_sdk_core.authenticators import IAMAuthenticator
from ibm_watson_openscale import APIClient

from ibm_watson_openscale import *
from ibm_watson_openscale.supporting_classes.enums import *
from ibm_watson_openscale.supporting_classes import *

authenticator = IAMAuthenticator(
    apikey=credentials['apikey']
)
client = APIClient(
    authenticator=authenticator
)

print(client.version)
```


### 5. スコアリング関数にwatsonx.aiのモデルを設定

[サンプルコード](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/RAG%20metrics%20computation%20with%20llm%20and%20IBM%20watsonx.gov.ipynb)を参考にして、スコアリング関数を作成します。
サンプルコードからモデルを`mistralai/mixtral-8x7b-instruct-v01`に変更しています。
日本語のRAGの指標の精度は、こちらのモデルの方が良かったです。（2024年12月）

```python:LLMによるスコアリング関数を作成
from ibm_watson_machine_learning.metanames import GenTextParamsMetaNames as GenParams
from ibm_watson_machine_learning.foundation_models import Model
from ibm_watson_machine_learning.foundation_models.utils.enums import ModelTypes
import pandas as pd

generate_params = {
    GenParams.MAX_NEW_TOKENS: 1000,
    GenParams.MIN_NEW_TOKENS: 10,
    GenParams.TEMPERATURE: 0.0
}

model = Model(
    model_id="mistralai/mixtral-8x7b-instruct-v01",
    params=generate_params,
    credentials={
        "apikey": credentials.get("apikey"),
        "url": credentials.get("url")
    },
    project_id=project_id
)

def scoring_fn(data):
    results = []

    for prompt_text in data.iloc[:, 0].values.tolist():
        model_response = model.generate_text(prompt=prompt_text)
        results.append(model_response)
    return pd.DataFrame({"generated_text": results})
```


### 6. 計算する指標のConfigを構成

watsonx.governanceにおいて計算する指標やスコアリング関数を構成する。

```python:指標などの構成をセット
from ibm_metrics_plugin.metrics.llm.utils.constants import LLMTextMetricGroup, LLMCommonMetrics, LLMQAMetrics, LLMRAGMetrics, RetrievalQualityMetric

# Edit below values based on the input data
context_columns = ["context1"]
question_column = "question"
answer_column = "answer"

config_json = {
    "configuration" : {
        "record_level" : True,
        "scoring_fn" : scoring_fn,
        "context_columns" : context_columns,
        "question_column" : question_column,
            LLMTextMetricGroup.RAG.value: {
                LLMCommonMetrics.FAITHFULNESS.value: {},
                LLMCommonMetrics.ANSWER_RELEVANCE.value: {},
                LLMQAMetrics.UNSUCCESSFUL_REQUESTS.value: {},
                LLMCommonMetrics.CONTENT_ANALYSIS.value: {},
                LLMRAGMetrics.RETRIEVAL_QUALITY.value: {},
                LLMCommonMetrics.ANSWER_SIMILARITY.value: {},
        }
    }
}
```

:::note warn
ここで、以下のような設定を入れてしまうと日本語のカスタムトークナイザが機能しなくなるため、要注意です。
```json
        "language_code" : language_code
```
:::

### 7. 計算

サンプルコードに倣って、以下を実行する。

```python
df_input = pd.DataFrame(data, columns=context_columns+[question_column])
df_output = pd.DataFrame(data, columns=[answer_column])
df_reference = pd.DataFrame(data, columns=["ground_truth"])
```

サンプルコードに倣って、以下を実行するが、ここで日本語のカスタムトークナイザをセットする。

```python:指標の計算
import json
metrics_result = client.llm_metrics.compute_metrics(
    config_json,
    sources=df_input, 
    predictions=df_output,
    references=df_reference,
    tokenizer = my_custom_tokenizer_ja,
)
```

:::note
実行完了まで少し時間がかかります。
:::

### 8. 結果の取得

サンプルコードに倣って、以下を実行して結果を取得する。

```python:結果を取得
final_results = client.llm_metrics.get_metrics_result(configuration=config_json, metrics_result=metrics_result)
print(json.dumps(final_results, indent=2))
```


サンプルコードに倣って、以下を実行して結果を表形式で出力する。

```python:結果を表形式に整形
results_df = data.copy()

for metric_name, metric_data in final_results.items():
    vals = {}
    if "record_level_metrics" in metric_data:
        for rm in metric_data["record_level_metrics"]:
            for m, mv in rm.items():
                if m != "record_id" and m!= "faithfulness_attributions" and m!= "context_relevances":  # Excluding columns
                    if m in vals:
                        vals[m].append(mv)
                    else:
                        vals[m] = [mv]

    if vals:
        for k, v in vals.items():
            results_df[k] = v

results_df
```


# 最後に

日本語のRAGの品質をLLMを用いて評価できることを試すことができました。

検索部分の評価を活用することで、質問を回答できるデータがベクトルDBにあるか、ベクトルDBにデータがある場合はデータの入れ方や検索方法などの工夫が必要になるかなど考慮できるようになります。
また、回答生成の評価を活用することで、ハルシネーションなど回避するためにプロンプトテンプレートや生成AIについて考慮できるようになります。
質問から回答への評価では、RAGの品質を客観的に評価できるようになります。

生成AIによってAIシステムの開発が容易になった反面、テストや運用や保守などのコストについて考えてみると、RAGを検証したり本番運用したりする際に、生成AIにRAGを評価させることで効率良くRAGの品質を評価し改善していく、というのが今後の生成AIの利用方法の１つになっていくかもしれないです。

watsonx.governanceでの評価においてwatsonx.aiのLLMで利用した場合は、watsonx.governanceとは別にwatsonx.aiの料金が発生するので注意してください。


# 参考資料


1. [watsonx.aiのLLMを用いて、RAGの品質を評価するサンプルコード](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/RAG%20metrics%20computation%20with%20llm%20and%20IBM%20watsonx.gov.ipynb)
2. [日本語対応するためのwatsonx.governanceのサンプルコード](https://github.com/IBM/watson-openscale-samples/blob/main/WatsonX.Governance/Cloud/GenAI/samples/Multi%20Lingual%20Support%20Notebook.ipynb)

3. [watsonx.governanceでの評価指標](https://www.ibm.com/docs/en/cloud-paks/cp-data/5.0.x?topic=models-metrics-computation-using-python-sdk#answer-quality)

4. [watsonx.governance OpenScale SDK](https://client-docs.aiopenscale.cloud.ibm.com/html/index.html)