📊

LLM・プロンプトの評価・テストフレームワークについてまとめてみた

2023/12/14に公開

はじめに

ご存知の通り大LLM時代なわけで、つよつよな方からアフィリ記事までこぞってどうやってLLMで良い出力を得るかまとめております。そしてそのテクニックがプロンプトエンジニアリングとして体系化されつつあります。ただし、エンプラでLLMを真面目に使おうとすると、プロンプトの管理やLLMごとの管理、レスポンスタイムの計測など様々な評価を継続的にやる必要があります。従来のデータサイエンスでも結局評価や計測が後々重要になったきたことを考えると必然かもしれませんが、そこらへんをまとめた日本語の記事がなかったので、アドベントカレンダーというチャンスを使ってまとめてみます!

そんなわけで、この記事ではまだそこまで盛り上がってはないが、確実に重要なLLMの評価の基本的な流れデモも交えて簡単な評価フレームワークの利用例を紹介していこうと思います! ただしまだ黎明期で今後主流になる方法論も変わると思うのと、私のやり方を書いたまでなので力を抜いて読んでいただければ幸いです。

注意: LLM及びプロンプトの開発はPythonで行われることが多いためそれを前提としてやり方を紹介していきます。

LLMの評価の流れ

基本的にはNLPにおける翻訳や要約タスクの評価と同じです。少し異なるのはcontextという所謂、文脈をLLMでは入力文と合わせて使います。そしてそれを評価するときにも使ったりします。
また新しい方法としては評価用のプロンプトを用意して別のLLMに対象のLLMを5段階で評価というやり方もあります。[1]この章ではフレームワークによらない大まかな流れを載せておきます

1. 評価のための評価用データを用意

正直ここが一番大変です。機械学習で言うデータセットの作成に近く手作りでつくることもできますが、かなり手間です。ただし、昨今ではLLMを用いてこのデータセット(Synthetic test data)すら自動生成するという方法もあります。[2] 具体的なハンズオンは後の章で紹介します!

必要なデータのフィールドは一般的な応答評価では下記となると思います。

data:
  question: "質問文"
  ground_truth_context: "質問の前提となる状況や文脈"
  ground_truth: "正解の回答"

2. 実際のLLMに評価用データを入力

LangChainなどのプロンプトエンジニアリングのフレームワークを使ってもOpenAIのライブラリを直接呼んでも良いと思います。ここは他の記事でもやられているのでこれ以上詳しくは書きません。

3. 出力と評価用データを合わせて評価用フレームワークの入力作成

評価するアルゴリズムや手法によって入力に必要な変数が変わるので、1と2の結果を元にそれに使えるデータを作ります。ユニットテストで2と同時評価もしている場合はこのステップは不要かもしれませんが機械学習のテストみたくノートブックでインタラクティブにやる場合は必要です。

また、評価に必要なデータとしては下記フィールドが使われることが多いです。

data:
  question: "質問文"
  ground_truth_context: "質問の前提となる状況や文脈"
  ground_truth: "正解の回答"
  answer: "実際のLLM回答"

ちなみに、contextはリスト形式で複数の文が入ることもあります。フレームワークによって細かいフォーマットは違うものの基本的これらを用意できれば、応答の定量的・(LLMからみた)定性的な評価をすることができます。

4. 評価用のモジュールに実行

評価用のモジュールを実行するだけです。

5. 出力された結果に対してグラフ整形や集計を行う

ここか一番の目的です。フレームワークによってスコアリングなどはできるものの結局そのLLM及びプロンプトが良いものだったかは、~2023年現時点では~最終的には人間が判断する必要があります。ここで使う知識はデータサイエンスをやっていた人が大好きなmatplotlibやseabornでのお絵描きとPandasで混合行列だしたりごにょごにょするやつです。

主なフレームワーク一覧

黎明期で大量のフレームワークや実装が発表されているので私が使えそうと思って試したもののみかるーく紹介します。ただし黎明期というものもありどっぷり使い込むにはGithubのコードを直接呼んだり開発者に直接聞いたりする必要があるので、個人的には自作するというのもアリよりのアリだと思います。
フレームワークの選定基準としては、下記となります。

  • 複数のLLMやパラメータを比較して評価できるか?
  • AnswerRelevancyなどの評価用関数がビルトインされているか?
  • 入出力や評価用関数のカスタマイズが容易か?
  • 結果をCSVもしくはPandasのDataFrameなど広く使われているデータ形式にエクスポート可能か?
  • (Option)グラフによる可視化やUIなどがあるか?

DeepEval

pytest形式でLLMのスコアが評価値を超えたかどうかをチェックできるフレームワークです。開発元のConfident AIのサイトに登録すれば結果をゴージャスなUIで確認することも可能です。使ってみた雑感としてはデータサイエンスなどに馴染みがないソフトウェアエンジニア向けで入出力データの細かいカスタマイズ(集計/加工)は難しかったです。

😍良かった点

  • pytestに近い形で直感的にテストがかける
  • テスト用データをCSVで読み込み可能
  • 開発者がめっちゃ親切でレスポンスが早い
  • 実用的なメトリクスが揃っている HallucinationMetricとかFaithfulnessとか

😒いまいちな点

  • deepeval testという独自CLIからテスト可能でpytestに対応してないため他のテストケースと共存が難しい
  • CSV出力も上述のUIを使えばできるものの用意されるているものが少なく、カスタマイズが難しい

PromptTools

Jupyter Lab(Notebook)使用に向いているフレームワーク。下記のサンプルコードのように数行で複数LLMの評価ができる優れものです。

messages = [
    [{"role": "user", "content": "Tell me a joke."},],
    [{"role": "user", "content": "Is 17077 a prime number?"},],
]
models = ["gpt-3.5-turbo", "gpt-4"]
openai_experiment = OpenAIChatExperiment(models, messages, temperature=temperatures)
openai_experiment.run()
openai_experiment.visualize()

😍良かった点

  • 細かい実装を考えずとりあえずLLMの評価を回せる
  • latencyや使用token数などのシステムのメトリクスがデフォルトで計測可能

😒いまいちな点

  • 各ベンダごとにexperimentという実験セットを回さないといけない
  • docstringが書かれてないことが多く、実際の機能はコードの実装を追わないといけないことがある
  • openai_experiment.evaluate("similar_to_expected", similarity.semantic_similarity, expected=expected)というように正解データを入力と別で入れる必要があるためリストの順番を一致させるなど細かい帳尻合わせが必要

RAGAS

Retrieval-Augmented Generation (RAG)とAssessmentという言葉を繋げてRAGASという名前のライブラリです。
最終的なアウトプットをHugging Faceのdatasets形式にして渡すだけでデータフレーム形式のアウトプットを得られるという試した三つのフレームワーク中一番私のユースケースにあっていました。また独自の評価用テストデータの自動生成機能もあり、データサイエンスで広く使われた既存のライブラリやフレームワーク(Pandas, Datasets)などと親和性が高いと感じました。

😍良かった点

  • 従来のデータサイエンスのライブラリのように特殊なデータ形式やクラスは不要で関数呼ぶだけで使える
  • PandasやDatasetsなど有名なライブラリの形式が入出力で対応している
  • 設計がシンプルなのでロックインされるづらい
  • Answer RelevanceやFaithfulnessなど基本的なメトリクスが揃っている

😒いまいちな点

  • ドキュメントが簡素なため、実際に自分のデータを使ってやる場合はコードを呼んだり調べたりする必要がある
  • リッチな可視化やUIはない
  • AWS Bedlockなど一部LLMサービスに対応してない[3]
  • データサイエンス用のライブラリ・フレームワークにある程度慣れている必要がある

RAGASを使った自動データ生成

今回のユースケース的にRAGASが一番使いやすかったため、これを用いてハンズオンしていきます!公式ではPubMedという生物医学の文章コーパスを使った例はありますが、自前データを使った例がなかったのでこちらで紹介します!OpenAIのAPIを使う想定です。

まずLangChainのlangchain_core.documents.base.Documentをリストに格納した入力データを作ります。実際は巨大のtxtファイルからtextsplitterで読み込んで作るかと思います。

from langchain.docstore.document import Document

documents = []
documents.append(Document(page_content="Yuji Itadori is the protagonist of jujutsu kaisen.", metadata={"source": "local"}))

次にLangChainを経由してGPTのモデルを使ってデータセットを生成させます。

from ragas.testset import TestsetGenerator
from langchain.embeddings import OpenAIEmbeddings
from langchain.chat_models import ChatOpenAI
from ragas.llms import LangchainLLM

TEST_SIZE = 5

# documents = load your documents

# Add custom llms and embeddings
generator_llm = LangchainLLM(llm=ChatOpenAI(model="gpt-3.5-turbo"))
critic_llm = LangchainLLM(llm=ChatOpenAI(model="gpt-4"))
embeddings_model = OpenAIEmbeddings()

# Change resulting question type distribution
testset_distribution = {
    "simple": 0.25,
    "reasoning": 0.5,
    "multi_context": 0.0,
    "conditional": 0.25,
}

# percentage of conversational question
chat_qa = 0.5


test_generator = TestsetGenerator(
    generator_llm=generator_llm,
    critic_llm=critic_llm,
    embeddings_model=embeddings_model,
    testset_distribution=testset_distribution,
    chat_qa=chat_qa,
)

testset = test_generator.generate(documents, test_size=TEST_SIZE)

出力言語が現時点では英語しか対応してないのでをpandasに変換して、他のLLMを用いて日本語に変換します。

from dotenv import load_dotenv
import re
import csv
import os
import pandas as pd


def _translate(input: str, context: str):
	prompt = f"""Please translate the input with given context

	input: {input}
	context: {context}
	"""
	system_message_prompt = SystemMessagePromptTemplate.from_template(template)

	chat_prompt = ChatPromptTemplate.from_messages(
	    [system_message_prompt]
	)

	# get a chat completion from the formatted messages
	chat(
	    chat_prompt.format_prompt(
		input=input, context=context
	    ).to_messages()
	)

df = testset.to_pandas()

df['question'] = df.apply(lambda row: _translate([row['question']], row['ground_truth_context']), axis=1)
df['ground_truth'] = df.apply(lambda row: _translate(row['ground_truth'], row['ground_truth_context']), axis=1)

RAGASを使った自動評価

では自動生成で生成されたデータを使って早速評価してみます!
評価用のデータは生成されたデータの形式とは違いHuggingfaceのDatasets形式です

DatasetDict({
    baseline: Dataset({
        features: ['question', 'contexts', 'ground_truths', 'question_type', 'episode_done', 'answer'],
        num_rows: 100
    })
})

なぜかカラム名が評価用の入力形式と違うので変換します。もしかしたら使い方が間違ってるかもしれないです。。。

df = df.rename(columns={'ground_truth_context': 'contexts'})
df = df.rename(columns={'ground_truth': 'ground_truths'})
df['question'] = df['question'].apply(lambda row: row[0])

eval_data = Dataset.from_pandas(df)

最後に必要なメトリクスをインポートして評価します!

from ragas.metrics import (
    answer_relevancy,
    faithfulness,
    context_recall,
    context_precision,
)
from ragas import evaluate
result = evaluate(
    faq_eval["baseline"],
    metrics=[
        context_precision,
        faithfulness,
        answer_relevancy,
        context_recall,
    ],
)

result

集計や可視化のため出力された評価をDataframeに変換します

df = result.to_pandas()
df

以上!

まとめ

どうでしょうか?このあたりはまだまだ開拓中の分野だと思うので、人気のフレームワークの推移や方法論が今後数ヶ月で大きく変わる可能性はありますが、基本的な流れはある程度共通かとお思います。
またプロンプトエンジニアリングに比べてまだコミュニティが大きくないので、必要なメトリクスや評価・テスト方法がかっちり決まってたら自作する方が融通が効くかもしれません。
個別評価メトリクスに関しては深掘りすると結局従来のNLPの知識が必要になってくるので、LLMは慣れてきたけどNLPはやったことないという方はぜひこちらの本も一読してみるといいと思います。

参考

https://dev.classmethod.jp/articles/huggingface-usage-dataset/
https://stackoverflow.com/questions/76551067/how-to-create-a-langchain-doc-from-an-str
https://arize.com/blog-course/llm-evaluation-the-definitive-guide/
https://llmshowto.com/blog/llm-test-frameworks

脚注
  1. Building RAG-based LLM Applications for Production
    ↩︎

  2. https://python.langchain.com/docs/use_cases/data_generation ↩︎

  3. あくまで自動評価や自動生成に使うLLMとして対応してないのみで、BedLockを使った出力データを評価してもらうこと自体は可能 ↩︎

Discussion