🦁

MLflowでカスタムLLM判定メトリクスを作ってローカルLLMを評価する

に公開

はじめに

前回はLLM判定メトリクスを使ってローカルLLMを評価してみました。今回はLLM判定メトリクスをカスタマイズしてみたいと思います。

以下に今回のコードを置いてあります。

ゴール

MLflowでカスタムLLM判定メトリクスを作ってローカルLLMを評価できている

環境

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

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

カスタムLLM判定メトリクスを作る

本記事では、カスタムLLM判定メトリクスを定義するprofessionalism_metrics.pyと、そのメトリクスを使ってLLMを評価するmlflow_evaluate.pyの2つのファイルを作成します。

  • professionalism_metrics.py: カスタムLLM判定メトリクス(professionalism)の定義と、その評価例(EvaluationExample)を記述します。これにより、メトリクス自体を再利用可能な形で定義し、評価ロジックを独立させます。
  • mlflow_evaluate.py: 定義したカスタムメトリクスを実際にMLflowの評価プロセスに組み込み、LLMの評価を実行するメインスクリプトです。

前回まででLLM判定メトリクスを使ってローカルLLMを評価できる状態が構築できています。
このLLM判定メトリクスをカスタマイズします。

MLflowは、毒性(toxicity)や可読性(flesch_kincaid_grade_level)など、汎用的なLLM判定メトリクスを標準で提供しています。しかし、特定のビジネス要件やドメイン固有の品質基準(例:専門性、特定の業界用語の使用、ブランドイメージへの適合性など)を評価するには、これらの既存メトリクスだけでは不十分な場合があります。このような場合に、独自の評価基準をLLMに判断させるカスタムLLM判定メトリクスが必要となります。

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

mlflow.metrics.genai.make_genai_metric() APIを使うことで、カスタムLLM判定メトリクスを作成することができます。以下の情報が必要になります。

  • name: カスタムメトリクスの名前
  • definition: メトリクスがしていることの説明
  • grading_prompt: スコアリングの指標を説明する。このプロンプトがLLMに直接与えられ、LLMがどのようにスコアリングを行うかを指示します。スコア0から4までの具体的な基準は、このgrading_prompt内に詳細に定義されています。
  • examples (Optional): input/output の例とスコアのセット
    こちらに詳細な情報があります。
    内部的には、definitiongrading_promptexamples を組み合わせて、長いプロンプトとしてLLMに与えることになります。

カスタムLLM判定メトリクスである "professionalism" を作ってみましょう。これはoutputが専門的であるかどうかを評価します。
まずmlflow.metrics.genai.EvaluationExample()を使ってLLM判定が使う例を作成しましょう。
そのためには以下の内容が必要になります。

  • input: モデルの入力
  • output: モデルの出力
  • score: モデルの出力を評価するスコア
  • justification: スコアの根拠

以下のような例になります。
今回は professionalism_metrics.py として以下のファイルを作成します。

import mlflow

##################
# 専門性の評価例を定義
##################
professionalism_example_score_2 = mlflow.metrics.genai.EvaluationExample(
    input="What is MLflow?",
    output=(
        "MLflow is like your friendly neighborhood toolkit for managing your machine learning projects. It helps "
        "you track experiments, package your code and models, and collaborate with your team, making the whole ML "
        "workflow smoother. It's like your Swiss Army knife for machine learning!"
    ),
    score=2,
    justification=(
        "The response is written in a casual tone. It uses contractions, filler words such as 'like', and "
        "exclamation points, which make it sound less professional. "
    ),
)
professionalism_example_score_4 = mlflow.metrics.genai.EvaluationExample(
    input="What is MLflow?",
    output=(
        "MLflow is an open-source platform for managing the end-to-end machine learning (ML) 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.",
    ),
    score=4,
    justification=("The response is written in a formal language and a neutral tone. "),
)


#########################
# 専門性の評価メトリクスを定義
#########################
def professionalism(
    model: str,
):  # answer_similarity 等既存のメトリクスに合わせてmodelを引数にしました
    return mlflow.metrics.genai.make_genai_metric(
        name="professionalism",
        definition=(
            "Professionalism refers to the use of a formal, respectful, and appropriate style of communication that is "
            "tailored to the context and audience. It often involves avoiding overly casual language, slang, or "
            "colloquialisms, and instead using clear, concise, and respectful language."
        ),
        grading_prompt=(
            "Professionalism: If the answer is written using a professional tone, below are the details for different scores: "
            "- Score 0: Language is extremely casual, informal, and may include slang or colloquialisms. Not suitable for "
            "professional contexts."
            "- Score 1: Language is casual but generally respectful and avoids strong informality or slang. Acceptable in "
            "some informal professional settings."
            "- Score 2: Language is overall formal but still have casual words/phrases. Borderline for professional contexts."
            "- Score 3: Language is balanced and avoids extreme informality or formality. Suitable for most professional contexts. "
            "- Score 4: Language is noticeably formal, respectful, and avoids casual elements. Appropriate for formal "
            "business or academic settings. "
        ),
        # ここで評価例を渡しています
        examples=[professionalism_example_score_2, professionalism_example_score_4],
        model=model,
        parameters={"temperature": 0.0},
        aggregations=["mean", "variance"],
        greater_is_better=True,
    )

上記のprofessionalismメトリクス定義において、parameters={"temperature": 0.0}を設定しています。これは、LLMの出力のランダム性を制御するパラメータであり、0.0に設定することで、LLMが最も確信度の高い、一貫性のある評価結果を生成するようになります。これにより、評価の再現性と信頼性が向上します。

上記をインポートして評価してみます。
mlflow_evaluate.py を以下のように変更します。

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
from mlflow.deployments import set_deployments_target

from professionalism_metrics import professionalism

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")
mlflow.set_experiment("my-genai-experiment")

# MLflow AI gatewayのターゲットを設定
set_deployments_target("http://localhost:5002")

# 既存のアクティブな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,  # type: ignore
        model_input: pd.DataFrame,
    ) -> list[str | list[str | dict[Any, Any]]]:
        # 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,
    )

    # デプロイ済みのエンドポイントを使って専門性を評価する
    my_professionalism_metrics = professionalism(model="endpoints:/chat") # 変更点はここ!!!!

    # 事前定義された question-answering metrics を使って評価する
    results = mlflow.evaluate(
        logged_model_info.model_uri,
        eval_data,
        targets="ground_truth",
        model_type="question-answering",
        # LLM判定メトリクスを追加
        extra_metrics=[
            my_professionalism_metrics,
        ],
    )
    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 python3 ./mlflow_evaluate.py

以下の結果となりました

{
  "toxicity/v1/mean": 0.00014185939653543755,
  "toxicity/v1/variance": 3.664199391347333e-12,
  "toxicity/v1/p90": 0.0001433907644241117,
  "toxicity/v1/ratio": 0.0,
  "flesch_kincaid_grade_level/v1/mean": 15.784054878048781,
  "flesch_kincaid_grade_level/v1/variance": 0.6344996737433081,
  "flesch_kincaid_grade_level/v1/p90": 16.421298780487806,
  "ari_grade_level/v1/mean": 18.37282393292683,
  "ari_grade_level/v1/variance": 3.700858050816681,
  "ari_grade_level/v1/p90": 19.911833079268295,
  "exact_match/v1": 0.0,
  "professionalism/v1/mean": 3.5,
  "professionalism/v1/variance": 0.25
}

professionalismが出力されています!
数値の妥当性などは別途評価する必要がありそうですが、カスタムLLM判定メトリクスを作成し使うことができました。

おわりに

今回はLLM判定メトリクスをカスタマイズしてローカルLLMの評価をしてみました。
エンドポイントの指定方法など作法があり、躓く部分もありましたがさほど難しくはありませんでした。
次はローカルLLMのRAG構成について調べてみたいと思います。

Discussion