📝

MLflowでローカルチャットボットを評価する

に公開

はじめに

MLflow でローカルチャットボットを評価する方法を学びます。

以下の記事でローカルチャットボットのログを MFflow で可視化しました。

今回は LLM を評価することで、ローカルチャットボットを改善するループを回せるようにしていきたいと思います。

以下の情報を参照して作業をしていきます。

ChatGPT の登場により、LLM は質問応答、翻訳、テキスト要約など、様々な分野でテキスト生成の威力を発揮しています。LLM の性能評価は従来の ML モデルとは少し異なり、比較対象となる単一のグラウンドトゥルースが存在しないことが非常に多いためです。MLflow は、LLM の評価を支援する API mlflow.evaluate() を提供しています。

MLflow の LLM 評価は以下の3つの要素から構成されます。

  1. 評価対象のモデル
  2. 指標
  3. 評価データ

今回の調査で使ったコードは以下にあります。

ゴール

MLflowでローカルチャットボットを評価できている

環境

本記事の動作確認は以下の環境で行いました。

  • MacBook Pro
  • 14 インチ 2021
  • チップ:Apple M1 Pro
  • メモリ:32GB
  • macOS:15.5(24F74)

GenAI アプリを評価する

依存関係をインストールする

textstat をインストールします。 テキストの統計量を算出してくれるもので、mlflow.evaluate() で使うためです。また evaluate transformers torch torchvision torchaudiotoxic の計算に必要です。

pipenv install textstat evaluate transformers torch torchvision torchaudio

以下の内容で mlflow_evaluate.py を作成します。参照記事ではOpenAIを使うコードになっていますが、今回はローカルのLLMを使うように変更しています。

from typing import Any

import mlflow
import mlflow.pyfunc
import pandas as pd  # type: ignore
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_openai import ChatOpenAI

eval_data = pd.DataFrame(
    {
        "inputs": [
            "What is MLflow?",
            "What is Spark?",
        ],
        "ground_truth": [
            "MLflow is an open-source platform for managing the end-to-end machine learning "
            "lifecycle. It was developed by Databricks, a company that specializes in big data and "
            "machine learning solutions. MLflow is designed to address the challenges that data "
            "scientists and machine learning engineers face when developing, training, and deploying "
            "machine learning models.",
            "Apache Spark is an open-source, distributed computing system designed for big data "
            "processing and analytics. It was developed in response to limitations of the Hadoop "
            "MapReduce computing model, offering improvements in speed and ease of use. Spark "
            "provides libraries for various tasks such as data ingestion, processing, and analysis "
            "through its components like Spark SQL for structured data, Spark Streaming for "
            "real-time data processing, and MLlib for machine learning tasks",
        ],
    }
)

# MLflowサーバーのURIを設定
mlflow.set_tracking_uri("http://localhost:5001")

# 既存のアクティブなrunがあれば終了
if mlflow.active_run():
    mlflow.end_run()


# mlflow.pyfunc.log_model にわたすために必要な関数を実装する
class ChatOpenAIWrapper(mlflow.pyfunc.PythonModel):  # type: ignore
    def __init__(self):
        self.system_prompt = "Answer the following question in two sentences"

    def predict(
        self,
        context: mlflow.pyfunc.PythonModelContext,
        model_input: pd.DataFrame,
    ) -> list[str | list[str | dict[Any, Any]]]:  # type: ignore
        # contextからモデル設定を取得することも可能(今回は使用しない)
        # model_config = context.artifacts if context else {

        # ここで使いたいモデルを指定する
        llm = ChatOpenAI(
            base_url="http://localhost:1234/v1",
            api_key=None,
            temperature=0.7,
            name="google/gemma-3-12b",
        )

        # 結果はまとめて返却する
        predictions = []
        for question in model_input["inputs"]:
            response = llm.invoke(
                [
                    SystemMessage(content=self.system_prompt),
                    HumanMessage(content=question),
                ]
            )
            predictions.append(response.content)

        return predictions


with mlflow.start_run() as run:
    # 入力例を作成
    input_example = pd.DataFrame({"inputs": ["What is MLflow?"]})

    # モデルのsignatureを推論するための予測を実行
    model_instance = ChatOpenAIWrapper()

    # カスタムモデルをMLflowモデルとしてログ(signatureとinput_exampleを指定)
    logged_model_info = mlflow.pyfunc.log_model(
        name="model",
        python_model=model_instance,
        input_example=input_example,
    )

    # 事前定義された question-answering metrics を使って評価する
    results = mlflow.evaluate(
        logged_model_info.model_uri,
        eval_data,
        targets="ground_truth",
        model_type="question-answering",
    )
    print(f"See aggregated evaluation results below: \n{results.metrics}")

    # `results.tables` でデータ毎の結果を取得できる
    eval_table = results.tables["eval_results_table"]
    print(f"See evaluation table below: \n{eval_table}")

評価を実行する

以下のコマンドを実行して、評価を実行します。事前に pipenv run mlflow server --host localhost --port 5001MLflow サーバーを起動しておく事を忘れないでください。

export MLFLOW_TRACKING_URI=http://localhost:5001
pipenv run python3 ./mlflow_evaluate.py

以下のような結果がターミナルに表示されます。

{
  "toxicity/v1/mean": 0.0001414338403264992,
  "toxicity/v1/variance": 1.1491258809984219e-11,
  "toxicity/v1/p90": 0.00014414574106922374,
  "toxicity/v1/ratio": 0.0,
  "flesch_kincaid_grade_level/v1/mean": 14.302857142857142,
  "flesch_kincaid_grade_level/v1/variance": 4.398008163265302,
  "flesch_kincaid_grade_level/v1/p90": 15.980571428571427,
  "ari_grade_level/v1/mean": 17.180357142857147,
  "ari_grade_level/v1/variance": 10.126033163265305,
  "ari_grade_level/v1/p90": 19.726071428571434,
  "exact_match/v1": 0.0
}

これらの評価結果は、LLMの性能を多角的に捉えるための指標です。各メトリクスの意味と、それがLLMの出力品質にどう関連するかを理解することが重要です。
共通してmean(平均)、variance(分散)、p90(90パーセンタイル)の値が表示されます。ratioは有害と判定された出力の割合を示します。

メトリクスについて

MLflowには大きく2つのメトリクスが存在しています。「ヒューリスティックベースメトリクス」と「LLM判定によるメトリクス」です。

1.ヒューリスティックベースメトリクス

RougeFlesch-KincaidBLEU 等の特定の関数を使って評価するものです。今回はこちらを使っています。使用できるメトリクスや、自分で定義するカスタムメトリクスについてはこちらを参照してください。

以下にメトリクスの情報がまとまっています。

今回は事前定義されたメトリクスの model_type の中で question-answering を使っています。他にも text-summarization 等複数の事前定義されたメトリクスモデルがあります。

question-answering にふくまれるメトリクスは以下の通りです。

  • toxicity: 有害・攻撃的・不快・差別的な内容が含まれているかどうかを数値で評価する指標です。0から1の範囲の値で、1に近いほど有害な内容が含まれていると判定されます。デフォルトでは0.5以上で有害と判定されます。この値は低いほど望ましいです。
  • flesch_kincaid_grade_level: 米国の学年制度を基準とした読みやすさの指標で、値が低いほど読みやすいとされます。一般的に、0から15の範囲で示されることが多いですが、この範囲に限定されるわけではありません。例えば、0から30点は「非常に難しく、大学院レベルの知識が必要」と解釈されます。
  • ari_grade_level: こちらも米国の学年制度を基準とした読みやすさの指標で、Flesch-Kincaidと同様に値が低いほど読みやすいとされます。Wikipediaでは14まで記載があり、14で「大学生レベル」とされています。
  • exact_match: モデルの出力とground_truthが完全に一致する場合に1、そうでない場合に0を出力します。計算の結果など全く同一の結果を期待する場合に使うのでしょうか。Question and Answerの場合には完全一致することはほぼ無いと考えられ、むしろ完全一致した場合はなにか間違いがあると考えるなど使い方ができそうです。

extra_metrics を使うことでカスタムメトリクスを追加することもできます。

2.LLM判定によるメトリクス

モデルの出力を評価するために、LLMを使って評価するものです。モデル出力の品質を評価する新しいタイプの指標で、文脈や意味の正確さのような微妙な点を見落としがちなヒューリスティックベースの指標の限界を克服します。また、ヒューリスティックメトリクスよりスケールしやすくコスト効率も高いです。MLflowでは様々なLLM判定によるメトリクスを利用でき、自分自身のプロンプトを使うカスタムメトリクスもサポートしています。詳細はこちらを参照してください。

ヒューリスティックベースのメトリクスは、特定の側面(読みやすさ、有害性など)を評価するのに役立ちますが、LLMの出力が「論理的に正しいか」「文脈に合っているか」といった、より複雑な品質を評価するには限界があります。そのため、次のステップとしてLLM自身を評価者として利用する「LLM判定によるメトリクス」が重要になります。これについては、今後の記事で詳しく掘り下げていきたいと思います。

評価結果を確認する

評価結果をmlflowでみてみましょう。

ExperimentsのModel metricsに結果が表示されています。

試しに、exact_match の値を変えてみます。以下のようにmlflow_evaluate.pyを変更し、回答をground_truthにします。

diff --git a/mlflow_evaluate.py b/mlflow_evaluate.py
index 3b5cf53..9fa9ba9 100644
--- a/mlflow_evaluate.py
+++ b/mlflow_evaluate.py
@@ -60,14 +60,14 @@ class ChatOpenAIWrapper(mlflow.pyfunc.PythonModel):  # type: ignore
 
         # 結果はまとめて返却する
         predictions = []
-        for question in model_input["inputs"]:
+        for i, question in enumerate(model_input["inputs"]):
             response = llm.invoke(
                 [
                     SystemMessage(content=self.system_prompt),
                     HumanMessage(content=question),
                 ]
             )
-            predictions.append(response.content)
+            predictions.append(eval_data["ground_truth"][i])
 
         return predictions
 

exact_matchの値が1になり、ari_grade_levelの値も下がっていることが確認できます。これは、モデルの出力がground_truthと完全に一致したため、読みやすさの指標もground_truthのレベルに近づいたことを示しています。

次に、toxicityの値を意図的に高くしてみます。mlflow_evaluate.pyを以下のように変更します。

diff --git a/mlflow_evaluate.py b/mlflow_evaluate.py
index 3b5cf53..7f6dbff 100644
--- a/mlflow_evaluate.py
+++ b/mlflow_evaluate.py
@@ -67,7 +67,7 @@ class ChatOpenAIWrapper(mlflow.pyfunc.PythonModel):  # type: ignore
                     HumanMessage(content=question),
                 ]
             )
-            predictions.append(response.content)
+            predictions.append("Fuck you man!!!")
 
         return predictions
 

この変更により、toxicityの値が非常に高くなり、同時にflesch_kincaid_grade_levelの値も下がります。これは、モデルが意図的に不適切な内容を出力した場合、toxicityメトリクスがそれを正確に捉え、読みやすさの指標もその影響を受けることを示しています。

まとめ

本記事では、MLflowを活用してローカルチャットボットのLLM評価を行う方法を解説しました。特に、ヒューリスティックベースのメトリクスを用いて、モデルの出力の読みやすさや有害性といった側面を数値的に評価できることを示しました。

今回の評価では、toxicityflesch_kincaid_grade_levelari_grade_levelといった指標が、LLMの出力品質を測る上でどのように機能するかを具体例を通して確認できました。これらの指標は、特定の側面を評価するのに有効ですが、出力の論理的な正確性や文脈への適合性といった、より高度な品質を評価するには限界があることも認識しました。

今後は、これらの限界を克服するために、LLM自身を評価者として利用する「LLM判定によるメトリクス」について深掘りし、より包括的なLLM評価手法を確立することを目指します。

Discussion