🔄

Vertex AI GenAI Evaluation Serviceを活用したLLM評価のCI

2024/12/21に公開

1. はじめに

この記事は、Google Cloud Champion Innovators Advent Calendar 2024 20日目の記事です。

機械学習エンジニアをしています、原です。
Google Cloud Champion Innovators(AI/ML)として選出いただき、活動しています。Google Cloud Innovatorsは、Google Cloud開発者/技術者のためのメンバーシッププログラムです。どなたでも参加可能ですので、Google Cloudユーザーの方に登録をおすすめします!

先日、私が所属する株式会社CAMのAdvent Calendarにて、Vertex AI GenAI Evaluation Serviceを活用した評価基盤についての記事を公開し、Evaluation ServiceとLLMOps、LLMの評価について紹介しました。今回は、このEvaluation ServiceをCIに組み込む方法について紹介したいと思います。

2. Vertex AI GenAI Evaluation Serviceについて

Vertex AI GenAI Evaluation Serviceは公式ドキュメントでは下記のように紹介されています。

Vertex AI の Gen AI Evaluation Service を使用すると、生成モデルまたはアプリケーションを評価し、独自の評価基準に基づいて、独自の判断と照らし合わせて評価結果のベンチマークを実施できます。
リーダーボードとレポートではモデルの全般的なパフォーマンスについて分析情報を得られる一方、モデルが特定のニーズにどのように対処しているのかは明らかになりません。Gen AI Evaluation Service では、お客様が独自の評価基準を定義したうえで、生成 AI モデルとアプリケーションがお客様のユースケースにどの程度即したものであるかを明確に把握できます。
生成 AI の開発プロセスでは、モデルの選択、プロンプト エンジニアリング、モデルのカスタマイズを含めて、あらゆるステップで評価が重要な意味を持ちます。Vertex AI には生成 AI 評価機能が組み込まれているため、必要に応じて評価を実施し、再利用できます。

つまり、生成AIに特化した評価サービスといった位置づけとなります。
この記事を読み進めるにあたって、まだどのような機能が備わっているかご存知でない方は、公式ドキュメントはもちろん、私の記事や同じくChampion Innovatorsの又吉さんが執筆した記事をお読みいただくことをおすすめします。

https://cloud.google.com/vertex-ai/generative-ai/docs/models/evaluation-overview
https://cam-inc.co.jp/p/techblog/995944402524307456
https://blog.g-gen.co.jp/entry/gen-ai-evaluation-service-in-vertex-ai

3. CIに評価を組み込むモチベーション

実験管理ができるEvaluation ServiceをCIを組み込むことで、常に定量的に優れたモデルの選択、プロンプトやパラメータなどをプロダクトに反映できます。

GitHubなどのCode Repositoryで、プロンプトやパラメータを管理している方は多くいると思います。その際に、Pull Requestにて、プロンプトやパラメータ、使用するLLMの変更箇所のみを確認して、改善されているのかを判断するのは困難だと私は考えています。そのため、評価用のサービスをCIに組み込むことによって自動的に評価し、評価指標をPull Requestで確認可能にすることで、レビュアーがより確実なレビューを行えるようになることを狙います。

4. 今回紹介するLLM評価用のCIについて

今回は、下記の図に掲載するような構成でシステムを構築します。
特定のブランチへのPull Requestが行われたことをトリガーとして、Evaluation Serviceを実行し、評価結果を取得・表示、結果に応じてmergeの可否を判断します。

CI/CDのプラットフォームとしてはGitHub Actionsを使用していますが、もちろんCloud Buildなどでも実現可能です。
今回のWorkflow

5. Vertex AI GenAI Evaluation Serviceのスクリプトの実装

それでは、Evaluation Serviceを実行するためのスクリプトを作成していきます。使用するPythonとライブラリのバージョンは下記になります。

# Python Version
Python 3.12

# Required Libraries
"google-cloud-aiplatform==1.75.0"
"ipykernel>=6.29.5"
"langchain>=0.3.13"
"langchain-openai>=0.2.13"
"pandas>=2.2.3"
"tqdm>=4.67.1"

5.1. Pointwise評価の指標を定義

今回は、エンジニア向けブログの要約タスクをLLMで実装し、評価します。
まずは下記のコードで、Vertex AI SDKを初期化し、次にLLM as a JudgeのPointwise評価で出力を評価するための評価指標を定義します。

Evaluation Serviceでは、ビルドインの評価指標の他にも自身でプロンプトを作成し、評価指標を作成できます。ここでは、MetricPromptTemplateExamples.Pointwise.VERBOSITY がビルドインの指標として使用されています。また、独自に作成したものが、custom_text_quality_metricとなっており、instruction,criteria,rating_rubric,evaluation_stepsの4つを指定して作成しています。定義した評価指標はリスト(metrics_set)に格納しておきます。

モデルベース評価(LLM as a Judge)におけるビルドインの指標はこちらが用意されています。これらは、全て英語で作成されているため、評価理由などを出力した際にも英語で出力されることがあります。そのため、翻訳して用いるのもよいでしょう。

また、LLM as a JudgeではないBLEUやROUGEなどの計算ベースの指標も用意されています。こちらもモデルベース評価と同様に、自身で定義することが可能です。

import json

import numpy as np
import pandas as pd
import vertexai
import vertexai.preview

from vertexai.evaluation import (
    EvalTask,
    MetricPromptTemplateExamples,
    PointwiseMetric,
    PointwiseMetricPromptTemplate,
)
from vertexai.generative_models import (
    GenerationConfig,
    GenerativeModel,
    HarmBlockThreshold,
    HarmCategory,
)

PROJECT_ID = "MY_PROJECT_ID"
LOCATION = "us-central1"
EXPERIMENT_NAME = "MY_EXPERIMENT_NAME"

def initialize_vertexai():
    vertexai.init(
        project=PROJECT_ID,
        location=LOCATION,
        experiment=EXPERIMENT_NAME,
    )

def set_metrics():
    custom_text_quality_metric = PointwiseMetric(
        metric="custom_text_quality",
        metric_prompt_template=PointwiseMetricPromptTemplate(
            input_variables=["response"],
            instruction="あなたのタスクはブログの要約を添削することです。以下の基準に従って、要約結果を評価してください。",
            criteria={
                "grounding": "要約結果は<context>に記載されている内容に基づいており、記載の内容以外を記述していない。<context>{context}</context>",
                "engineering": "適切に専門用語が使用されており、エンジニアにとって理解しやすい文章となっている",
            },
            rating_rubric={
                "1": "「回答は両方の基準を満たしており、優れたパフォーマンスである」",
                "0": "「回答は両方の基準にある程度合致している」",
                "-1": "「回答は両方の基準を満たしておらず、不十分である」",
            },
            evaluation_steps={
                "Step_1": "提供されたすべての基準の観点から応答を評価してください。各基準に従って評価を行ってください。",
                "Step_2": "各基準に対して評価を行い、評価基準に従って評価スコアを出力してください。",
                "Step_3": "評価基準に基づいてスコアを付けてください。各基準を考慮して評価の簡単な理由を説明してください。",
            },
        ),
    )
    metrics_set = [
        MetricPromptTemplateExamples.Pointwise.VERBOSITY,
        custom_text_quality_metric,
    ]
    return metrics_set

5.2. 評価対象のデータセットを取得

次に評価対象のデータセットを取得します。
LLMへの入力データを用意し、User Promptとして定義、これをDataFrameに格納します。この際に、他にプロンプトへと埋め込みたい情報がある場合には、contextのように他のカラムとして追加も可能です。
また、今回はローカルから入力データを取得するコードとなっていますが、Cloud Storageなどの外部ストレージから取得するのもよいでしょう。

評価用データセットのスキーマはこちらに記載されています。今回は、テキストの生成と同時に評価する方針をとっているため、responseのカラムは用意していません。一方で、事前に生成してresponseに格納しておくことによって、生成処理と評価処理の分離も可能です。

def set_dataset():
    instruction = "今回はblog_textに記載のブログを要約してください。: \n <blog_text>"

    # 評価対象のブログテキストを取得
    df_blog = pd.read_csv("./dataset/blog_text.csv")
    context = df_blog["text"].tolist()

    eval_dataset = pd.DataFrame(
        {
            "prompt": [instruction + item + "</blog_text>" for item in context],
            "context": context,
        }
    )
    return eval_dataset

作成されたDataFramepromptカラムには下記のようなデータが格納されます。

"今回はblog_textに記載のブログを要約してください。:
<blog_text>"
{blog_text.csvのtextカラムのデータが入ります}
</blog_text>"

5.3. Evaluation Serviceによるテキスト生成と評価の実行

最後にEvaluation Serviceを実行し、評価結果を取得していきましょう。

要約結果の生成には、先日リリースしたばかりのgemini-2.0-flash-expを使用しています。
評価結果を取得には、EvalTaskへと用意したモデルとデータセット、評価指標を渡す必要があります。

評価が実行されると、データセットで定義したUser Promptを元に自動的にテキストの生成が行われます。そして、定義した評価指標に基づいて評価され、pointwise_result.summary_metricsへとデータセット全体の評価サマリが格納されます。今回は、後に登場するGitHub ActionsのWorkflowで使用するために、この結果をJSON形式で保存しています。また、pointwise_result.metrics_tableには、データセットの各行ごとに評価スコアと評価理由(/explanation)が格納されます。

pointwise_result.summary_metricsの例
pointwise_result.metrics_tableの例

def result_to_json(pointwise_result):
    data = pointwise_result.summary_metrics
    for key, value in data.items():
        if isinstance(value, np.generic):
            data[key] = value.item()

    with open("./output.json", "w") as file:
        json.dump(data, file)

def run_evaluation(model, eval_dataset, metrics):
    GEMINI_RPM = YOUR_PROJECT_GEMINI_RPM
    CUSTOM_EVAL_SERVICE_QPS_LIMIT = (
        GEMINI_RPM / 60 / len(eval_dataset)
    )  # (gemini-1.5-pro RPM / 60 sec / default number of samples)
    eval_task = EvalTask(
        dataset=eval_dataset,
        metrics=metrics,
        experiment=EXPERIMENT_NAME,
    )

    pointwise_result = eval_task.evaluate(
        model=model,
        evaluation_service_qps=CUSTOM_EVAL_SERVICE_QPS_LIMIT,
        retry_timeout=1200,
    )
    return pointwise_result

if __name__ == "__main__":
    initialize_vertexai()  # Vertex AI SDKの初期化
    target_metrics = set_metrics()  # LLM as a Judge用の評価指標を準備する
    target_eval_dataset = set_dataset()  # 評価対象のデータセットを準備する

    # モデルを定義する
    generation_config = GenerationConfig(
        temperature=1.0,
    )

    llm = GenerativeModel(
        model_name="gemini-2.0-flash-exp",
        system_instruction="あなたのタスクは、エンジニアに向けたブログの文章を300字程度に要約することです。",
        generation_config=generation_config,
    )

    # 評価を実行する
    pointwise_result = run_evaluation(
        model=llm, eval_dataset=target_eval_dataset, metrics=target_metrics
    )

補足: Gemini以外のLLMに対しての評価

今回の例では、GeminiをVertex AI SDKから呼び出して評価対象のデータを生成していますが、任意のモデルの出力に対して評価することも可能です。これは、テキスト生成用の関数を定義することによって実現できます。つまり、Gemini以外にもAzureやAWSで提供されているモデルに対して評価ができます。下記はGPT-4o(OpenAI API)をLangChainから使用する例です。

実装時には、Google Cloudが提供している、こちらのNotebookが参考になります。

from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate
from langchain_openai import ChatOpenAI

def run_openai_model(input):
    llm = ChatOpenAI(
        api_key=OPENAI_API_KEY,
        model="gpt-4o",
        temperature=1.0,
    )

prompt = ChatPromptTemplate.from_messages(
    [
        (
            "system",
            "あなたのタスクは、エンジニアに向けたブログの文章を300字程度に要約することです。",
        ),
        (
            "human",
            "{input}",
        ),
    ]
)
output_parser = StrOutputParser()
chain = prompt | llm | output_parser
llm_response = chain.invoke({"input": input})

return llm_response

if __name__ == "__main__":
  pointwise_result = run_evaluation(
      model=run_openai_model, eval_dataset=target_eval_dataset, metrics=target_metrics
  )

6. GitHub Actionsの設定

Evaluation Serviceの評価用スクリプトが準備できたので、GitHub Actionsで実行していきます。
まず、GitHub ActionsでEvaluation Serviceを実行するためには、権限を付与する必要があります。セキュリティの観点から、Service Accountキーを直接扱うのではなく、Workload Identityを使用するのがよいでしょう。

Workload Identityの設定方法については、公式ドキュメントこちらの記事などを参考にしてください。上記と重複する箇所もあるかと思いますが、gcloudでの設定コマンドについても記載しておきます。

export PROJECT_ID="MY_PROJECT_ID"

# Workload Identity APIの有効化
gcloud services enable iamcredentials.googleapis.com --project "${PROJECT_ID}"

# Workload Identity Poolの作成
gcloud iam workload-identity-pools create "MY_POOL_NAME" --project="${PROJECT_ID}" --location="global" --display-name="MY_POOL_NAME"
export WORKLOAD_IDENTITY_POOL_ID=$(gcloud iam workload-identity-pools describe "MY_POOL_NAME" --project="${PROJECT_ID}" --location="global" --format="value(name)")

# Workload Identity Providerの作成
gcloud iam workload-identity-pools providers create-oidc "MY_PROVIDER_NAME" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="MY_POOL_NAME" \
  --display-name="MY_DISPLAY_NAME" \
  --attribute-mapping="google.subject=assertion.sub,attribute.actor=assertion.actor,attribute.repository=assertion.repository,attribute.repository_owner=assertion.repository_owner" \
  --attribute-condition="assertion.repository_owner == 'REPOSITORY_OWNER_NAME'" \
  --issuer-uri="https://token.actions.githubusercontent.com"

export REPO="REPOSITORY_NAME"
gcloud iam service-accounts add-iam-policy-binding 'MY_SERVICE_ACCOUNT_KEY_NAME@MY_PROJECT_ID.iam.gserviceaccount.com' \
  --project="${PROJECT_ID}" \
  --role="roles/iam.workloadIdentityUser" \
  --member="principalSet://iam.googleapis.com/${WORKLOAD_IDENTITY_POOL_ID}/attribute.repository/${REPO}"

gcloud iam workload-identity-pools providers describe "MY_PROVIDER_NAME" \
  --project="${PROJECT_ID}" \
  --location="global" \
  --workload-identity-pool="MY_POOL_NAME" \
  --format="value(name)"

では、GitHub Actionsの設定ファイルを作成していきましょう。ポイントとなる箇所を順を追って説明します。まず、先ほど設定したWorkload Identityを使用するために、Google Cloudが提供しているGitHub Actionsの設定を使用しましょう。

name: Vertex AI GenAI Evaluation Service - Metrics Check

on:
  pull_request:
    branches:
      - main

permissions:
  id-token: write  # OIDCトークンの書き込み権限
  contents: read    # コードの読み取り権限
  pull-requests: write  # Pull Requestへの書き込み権限

jobs:
  setup:
    name: Setup and Evaluation
    runs-on: ubuntu-latest

    steps:
      # 1. リポジトリをチェックアウト
      - name: Checkout code
        uses: actions/checkout@v4

      # 2. 認証情報を取得
      - name: Authenticate to Google Cloud
        uses: 'google-github-actions/auth@v2'
        with:
          project_id: 'MY_PROJECT_ID'
          workload_identity_provider: 'projects/MY_PROJECT_NUMBER/locations/global/workloadIdentityPools/MY_POOL_NAME/providers/MY_PROVIDER_NAME'
          service_account: 'MY_SERVICE_ACCOUNT_KEY_NAME@MY_PROJECT_ID.iam.gserviceaccount.com'

      # 3. Cloud SDKをセットアップ
      - name: Set up Cloud SDK
        uses: 'google-github-actions/setup-gcloud@v2'
        with:
          version: 'latest'

      # 4. Cloud SDKのインストール確認
      - name: Use gcloud CLI
        run: 'gcloud info'

続いて、評価スクリプトを実行するための依存ライブラリをインストールします。今回はuvでインストールを行うため、uv自体のインストールを行い、リポジトリに格納されているuv.lockを元にしてライブラリをインストールします。

# 5. uvをインストール
- name: Install uv
  run: |
    curl -LsSf https://astral.sh/uv/install.sh | sh
    echo "$HOME/.local/bin" >> $GITHUB_PATH
    uv --version

# 6. Python仮想環境をセットアップし、依存関係をインストール
- name: Install dependencies
  run: |
    uv venv .venv
    source .venv/bin/activate
    uv sync

次に、Evaluation Serviceを実行します。先ほど作成したスクリプトを実行し、JSON形式で評価結果を保存します。

# 7. Pythonスクリプトを実行してJSONデータを保存
- name: Run Python script
  run: |
    source .venv/bin/activate
    python src/genai_eval.py
  timeout-minutes: 20

実行が完了したら、出力されたJSONから評価結果を取得し、結果に応じてWorkflowの成功と失敗を制御します。ここでは、評価結果が閾値を下回った場合にWorkflowを失敗させるようにしています。実際に導入する際には、既存のモデルとの比較をするとよいでしょう。

# 8. 値を検証してステータスチェックを制御
- name: Validate value
  run: |
    TEXT_QUALIY=$(cat output.json | jq -r '."custom_text_quality/mean"') # JSONからcustom_text_quality/meanの値を取得
    VERBOSITY=$(cat output.json | jq -r '."verbosity/mean"') # JSONからverbosity/meanの値を取得
    TEXT_QUALIY_INT=$(printf "%.0f" $TEXT_QUALIY)
    VERBOSITY_INT=$(printf "%.0f" $VERBOSITY)

    echo "TEXT_QUALIY (int): $TEXT_QUALIY_INT"
    echo "VERBOSITY (int): $VERBOSITY_INT"

    # 両方の値が0以上のときのみ条件を満たす
    if [ "$TEXT_QUALIY_INT" -ge 0 ] && [ "$VERBOSITY_INT" -ge 0 ]; then
      echo "全ての評価指標が基準値を満たしています。"
      echo "RESULT=✅ 全ての評価指標が基準値を満たしています。" >> $GITHUB_ENV
    else
      echo "評価指標が基準値を満たしていません。"
      echo "RESULT=❌ 評価指標が基準値を満たしていません。" >> $GITHUB_ENV
      exit 1
    fi

最後に、Pull Requestに対してコメントをします。これにより、Pull Requestを作成したユーザーやレビュアーが評価結果を容易に確認できるようにします。JSONをMarkdownテーブルに変換し、環境変数に格納します。

# 9. JSONデータをMarkdownテーブルに変換
- name: Convert JSON to Markdown table
  if: always()
  run: |
    echo "Converting JSON to Markdown table..."
    JSON=$(cat output.json)
    echo "| Metrics   | Score   |" > table.md
    echo "|-------|---------|" >> table.md
    echo $JSON | jq -r 'to_entries[] | "| \(.key)  | \(.value)  |"' >> table.md
    echo "MARKDOWN_TABLE<<EOF" >> $GITHUB_ENV
    cat table.md >> $GITHUB_ENV
    echo "EOF" >> $GITHUB_ENV
    echo "TIMESTAMP=$(date +%s)" >> $GITHUB_ENV

# 10. Pull Requestにコメントを投稿
- name: Comment on Pull Request
  if: always()
  uses: mshick/add-pr-comment@v2
  with:
    message: |
      ### 【Vertex AI GenAI Evaluation Service】 Metrics Check
      評価の結果、下記の指標が算出されています。
      ${{ env.RESULT }}

      ${{ env.MARKDOWN_TABLE }}
    message-id: "vertexai-enai-eval-${{ env.TIMESTAMP }}"  # コメントを毎回新規投稿

7. 実行例

この設定ファイルを使用して、任意のブランチからmainブランチへのPull Requestを作成してみると、下記のような結果が得られます。

❌失敗時

まず、評価結果が閾値を下回った場合の結果をみてみましょう。
評価指標のテーブルと結果がコメントとして投稿され、Workflowが失敗していることが確認できます。今回は、Free Planである個人のリポジトリで実行をしたため、マージのブロックは設定できませんでした。設定をしたい方はこちらを参考にするとよいでしょう。

失敗時の画面

✅成功時

次に、評価結果が閾値を上回った場合です。
こちらでは、Workflowが成功していることが確認できます。

成功時の画面

また、今回は下記のような設定を加えることで、コメントが上書きされるのではなく、新しいコメントが追加されるようにしています。そのため、Workflowの失敗後にプロンプトやパラメータに修正を加えた際にも、どのコミットによって精度が向上したのかを確認しやすくなります。

message-id: "vertexai-genai-eval-${{ env.TIMESTAMP }}"  # コメントを毎回新規投稿

8. 終わりに

今回は、Vertex AI GenAI Evaluation ServiceをCIに組み込む方法について紹介しました。
Evaluation Serviceには今回紹介した以外にも様々な機能が備わっており、Google Cloudが提供している下記のリポジトリに置かれているNotebookが実装の際の参考になるのでおすすめです。
https://github.com/GoogleCloudPlatform/generative-ai/tree/main/gemini/evaluation

また、今回のWorkflowには実装をしませんが、最新のリポジトリに反映されているモデルやパラメータの評価結果とPull Requestの評価結果を比較することで、性能が向上しているかの判断も可能です。その際に、2つの出力結果を直接LLM as a Judgeで比較する、Pairwise評価を採用するのもよいでしょう。皆さんも、ぜひEvaluation ServiceをCIに組み込んで、モデルの評価を自動化してみてください。

Google Cloud Champion Innovators Advent Calendar 2024では、各々の専門領域に長けたGoogle Cloud Champion Innovatorsの皆さんが素晴らしい記事を投稿しています。ぜひご覧ください!

Discussion