LLMアプリケーションテストの難しさ
GPT-4のような大規模言語モデル(large language model、以下LLM)が普及し、それを活用したLLMアプリケーショ ンもさまざまな分野で本格的に使用されています。さまざまなビジネスロジックが含まれているLLMアプリケーションは、サービスの品質を保証することも開発作業において非常に重要になっています。LLMアプリケーションはその複雑さゆえに、テストすることが容易ではありません。入力値を少し変更するだけで、全く異なる回答が生成されるためです。LLMアプリケーションは、プロンプトチェーン(prompt chaining)とエージェントなどを通じてモデルに対して何度も推論(inference)を行うため、誤差が累積して変動性が大きく、プロンプトを少し修正するだけで回答が全く異なるなど、非常にデリケートなアプリケーションです。
この記事では、LINE PlusのLINE GAME PLATFORMチームでさまざまなLLMアプリケーションを開発する中で、このようにデリケートなLLMアプリケーションのテストおよびテスト結果の評価プロセスを改善し、自動化するためにどのようなテスト方法論を使用したかを紹介します。
テストとテスト評価方法の改善
まず、テストとテスト結果の評価プロセスをどのように改善したかを見てみましょう。
テストセットをプロンプトごとに分離
まず、LLMアプリケーションの変動性を制御するために、出力に影響する項目を一つずつ区別する作業を行いました。プロンプトチェーンが実行されるアプリケーションの場合、単一のプロンプトをテストした後、プロンプトチェーンが実装されたLLMアプリ ケーション全体のテストを行います。各項目を評価できる質問と回答のデータセットをプロンプトごとに作るのですが、複数のプロンプトが連結されているプロンプトチェーンの場合、下図のようにプロンプトごとにテストデータセットをそれぞれ用意します。これは非常に面倒な作業ですが、これにより、各プロンプトの性能を独立して評価でき、後でそのプロンプトを修正しても全体のテストは継続的に行えます。
では、テスト実施後、テスト結果はどのような方法で評価するのでしょうか?
定量評価の導入と評価指標の選定
ドメインに特化したLLMアプリケーションは、特定の分野の複雑で細かな要件を満たす必要があるため、正確に評価するには、ドメイン専門家による定性評価が必須です。そのため、プロンプトやモデルを修正しながら性能の比較や測定を行うには、ドメイン担当者のリソースをさらに投入する必要があります。それを改善するために、評価プロセスを自動化し、評価に必要なリソースを削減できる新しい評価基準を開発することにしましたが、最も簡単に考えられる方法は定量評価でした。
定量評価は、客観的な指標を用いてモデルの性能を評価する方法であり、一貫した基準を設定し、迅速に結果を導き出すというメリットがあります。しかし、この方法は主に単語単位の単純なマッチングに基づいているため、以下の例のように異なる語彙を使用したり、文の構造を変えたりするなど表現方法が変わると、実際には正解であっても評価スコアが低くなる可能性や文脈を考慮しないという限界があります。
- 正解文:The cat sleeps on the sofa.
- 比較文:The cat is sleeping on the sofa. → 正解文と同じ意味で類似語彙を使い、高い類似度スコアを獲得
- 比較文:A feline is resting on the couch. → 正解文と同じ意味であるにもかかわらず、使用語彙や文の構造が異なるため、低い類似度スコアを獲得
上記のようなデメリットにもかかわらず、人間の介入なしで迅速な評価結果が得られるという点が自動化プロセスに適していたため、私たちはデメリットを補完していくことにしました。単語単位の単純マッチングの限界を破るために、文脈を考慮し、意味の類似性をよりよく反映できる指標を回答の形式ごとに追加しました。一般的に優れたモデルは定量評価でも良い性能を示す傾向があります。そのため、定量的指標を利用してモデルの性能の1次検証を行うパイプラインを設計しました。
では、私たちが定量評価に活用した主な指標をタイプごとに見てみましょう。
ロジックに基づく評価指標
完全一致(exact match)
モデルの予測が正解と完全に一致するかを判断する指標です。現実的に正解と予測値が完全に一致することは難しいので活用性は低いですが、回答のタイプが明確に定義されている場合は、直感的に素早く活用できるため使用しました。
コサイン類似度(cosine similarity)
文をベクトル化した後、2 つのベクトルがなす角のコサイン値を使用して類似性を測定する方法です。
LLMは創造的な回答を生成するため、同じ意味の文でも毎回異なる表現をすることがあります。そのため、正確に評価するには、文内の単語間の関係性を理解し、文脈を把握する方法が必要でした。最も迅速かつ簡単に使用できる指標としては、単純に単語のマッチングの有無をベースにしたN-gramを用いた指標がありましたが、それらの指標は同義語や単語の変動性、単語間の関係性を把握できないという限界があり、正確に評価できませんでした。
このような限界を打破するために、文のベクトル埋め込みを使用して類似度を測定するコサイン類似度を指標として追加しました。コサイン類似度 は、定性評価ほどではありませんが、定量的指標の中では文脈を考慮できる最も効果的な性能指標として知られています。
METEOR
正解と予測文間の完全な一致、同義語の一致、形態素の一致に基づいてソートを行った後、適合率(precision)と再現率(recall)の調和平均(F-score)と、単語の非連続性に対するペナルティを組み合わせて最終スコアを計算する方法です。N-gramベースの指標で、従来のN-gramベースの指標の限界を補完し、同義語と文構造の類似性を一緒に考慮する方法です。コサイン類似度と相互補完的に活用するために追加しました。
Rouge-L
正解と予測文間の単語の順序を考慮しながら、最長共通部分シーケンスをベースに類似性を測定する方法です。
現在開発中のLLMアプリケーションは、質問に対する自由記述回答だけでなく、場合によってはファイルのパスやcurlコマンドなどのコードベースの回答を出力することもあります。パスやコードのような場合、すべての単語やトークンが正解文と一致していても、その順序が違うと意味が全く異なるため、順序が一致することが非常に重要です。前述で文脈を考慮するために追加したコサイン類似度やMETEORは、その点で限界があります。以下の例で見てみましょう。
- 正解:curl -X GET http://example.com/data
- 不正解:curl -X http://example.com/data
上の例をコサイン類似度やMETEORで評価すると、HTTPメソッド(GET
)が抜けているにもかかわらず、類似度スコアが高くなります。一方、Rouge-Lは2つの文の最長共通部分列に基づいて評価するため、トークンの順番やトークンを含めるかどうかが考慮されるため、 GET
メソッドが抜けていることが構造的な違いとみなされ、類似度スコアが低くなります。
このように順序が重要で、単語の変形なしに構造化されたテキストに対応するためにRouge-L指標を使用しました。
LLMベースの評価指標
LLMを活用した評価指標としては、GPT類似度(similarity)とGPT正確度(correctness)を使用しました。
- GPT類似度:GPT-4を活用し、正解と予測文間の類似度を0~1のスコアで算出する方法です。
- GPT正確度:GPT-4を活用し、正解と予測文間の類似性を判断する方法で、結果を0(非類似)または1(類似)に分類する方法です。
ロジックベースの評価指標は、算出方法によって評価基準がそれぞれ異なります。これらの指標は、さまざまな形の出力を生成するLLMを評価する際、多方面から柔軟に判断を下すことが難しいため、人間によ る評価方法を完全に代替するには不十分であると判断しました。
その限界を補完するために、LLM自体を活用した評価方法を検討しました。LLMは、さまざまな分野で優れた能力を発揮し、ゼロショット文脈内学習(zero-shot in-context learning)により、別途の例がなくても指示に従って優れた性能を発揮できます。National Taiwan UniversityのCheng-Han ChiangとHung-yi LeeはCan Large Language Models Be an Alternative to Human Evaluation?という論文で、ChatGPTを利用してテストした結果、ほとんどの場合、人間の専門家と一致する評価を下したとし、LLMが人間の評価を代替することが可能であることを示唆しました。
LLMを活用した評価指標の主なメリットは以下のとおりです。
- 高い再現性:人間が評価する場合、同じ資格を持った人に同じ指示を与えても主観的な評価基準が異なる可能性があるのに対し、LLMの評価結果は、使用したモデルとランダムシード、ハイパーパラメータを指定すれば、同じ結果が再現される可能性が高くなります。
- サンプルの独立性を保証:人間の評価では現在の評価が以前のサンプルに多少影響を受ける可能性があるのに対し、LLM評価では各サンプルの評価が互いに独立しています。
- 低コストと高速性:人間によって評価する場合よりもコストが低く、より迅速に結果を確認できます。
このようなメリットを考慮して、人間による評価に近い役割を担えるように上記の2つの指標を追加し、前述で紹介したロジックベースの他の指標と一緒に使用することで、評価の客観性を高めながらモデルの性能を多角的かつ総合的に理解できることを期待しま した。
テストを自動化すべき理由
LLMアプリケーションのテストを行う際、前述で紹介した指標を状況に応じて選別し、使用します。プロンプトごとに適した指標が異なるためです。もし、アプリケーションで使用するプロンプトが多く、使用する指標も多いのに、プロンプトの変更に伴う関連作業をすべて手動で行う場合、テストプロセスが非常に複雑になってしまい、時間もかかることになります。
以下の例を見てみましょう。入力データとLLMは同じ状態で、プロンプトのみ変更して質問したものです。「LLMとは何ですか?」という同じ質問に対し、プロンプトによって回答の品質が全く異なることが確認できます。
このように、LLMはプロンプトのわずかな変化に敏感に反応します。そのため、より高い品質の回答を得るには、さまざまなプロンプトを開発してテストを行い、回答に対する指標を確認する必要があります。以下のように、さまざまなプロンプトの中から好きなものを自由に選択してテストすることができなければ、最適化されたプロンプトを決定するのにどれだけの時間がかかるか分かりません。
また、目的に応じてデータを集めてデータセットを構成する際に、数千、数万以上のデータで構成された各データセットを動的にプロンプトと組み合わせたり、選択したりしてテストする作業を自動化しなければ、非常に面倒な作業になります。
最後に、モデルはさまざまなハイパーパラメータとデータでファインチューニング(fine tuning)できます。または、複数のモデルで構成されたRAG(rag-augmented generation)ベースのLLMアプリケーションが評価対象になることもあります。最適なサービスを作るには、このようにさまざまなモデルをテストした後、それを評価して得られた指標を比較し、最適なモデルを選別する作業が必須になります。その作業も自動化が必要な機能でした。
まとめますと、プロンプトとデータセット、モデルをいちいち開発者が変更しながら一つひとつテストする方法は非常に非効率的です。アプリケーションを効果的に最適化するには、それを自動化する必要があります。
私たちは自動化できる方法を模索し、大規模なテストと指標のカスタマイズに焦点を当ててテスト環境を構築することにしました。ここでテスト自動化作業に中核の役割を果たしたフレームワークが、Language Model Evaluation Harness(以下、Harness)です。私たちはテストを自動化するためのパイプラインを構築したとき、モデルとLLMアプリケーションを評価するコアコンポーネントをHarnessベースで開発しました。そのHarnessはどのようなフレームワークなのか、どのように使われるのか一つずつ見ていきましょう。
Harnessを利用したテストの自動化
Harnessは、言語モデルを評価するために非営利AI研究所EleutherAIによって開発されたオープンソースフレームワークです。基本的にGLUE、MMLU、HellaSwagなど60以上のベンチマークで構成されたタスク(task)をサポートし、新しいタスクをカスタマイズしてモデルを評価することもできます。
Harnessのメリットは以下のとおりです。
- オープンソースプロジェクトで、多くの研究者や開発者が協力し、継続的にアップデートしながらモデル評価に対して標準的な実装を提供しています。そのため、HugginfaceのOpen LLM Leaderboardでもモデル評価にHarnessを使用しています。
- 評価に使用するデータセットや指標、プロンプトなどを自由に設定できるため、要件に合ったテストを素早く実行できます。
- 評価を自由に拡張できる仕組みなので、社内のAIシステムに合った方法で評価をカスタマイズできます。
次は、基本的なHarnessの使い方と、私たちがどのようにカスタマイズして使っているのか、一つずつ紹介します。
Harnessの使い方
Harnessの基本的な使い方を説明します。
インストールと実行
Harnessをインストールして実行する方法はとても簡単です。まず、以下のようにHarnessリポジトリでプロジェクトをダウンロードし、依存パッケージをインストールします。
git clone https://github.com/EleutherAI/lm-evaluation-harness // プロジェクトのダウンロード
cd lm-evaluation-harness // プロジェクトディレクトリへ移動
pip install -e . // Harnessで使う依存パッケージのインストール
インストール完了後、 lm_eval
コマンドでHarnessを実行できます。
lm_eval --model hf \
--model_args pretrained=EleutherAI/gpt-j-6B \
--tasks hellaswag \
--device cuda:0 \
--batch_size 8
上の例の順番でコマンドの引数を見てみましょう。
--model
:テストを行うモデルを選択します。上の例ではhf
(Hugging Face)を選択しました。--model-args
:モデルを実行するために必要な引数を設定します。上の例ではpretrained=EleutherAI/gpt-j-6B
を設定し、Hugging Faceを通じてモデルEleutherAI/gpt-j-6B
をテストしようとすることが分かります。--tasks
:モデルにどのタスクをテストするかを決定します。,
で区切って複数のタスクを選択でき、タスクグループを記入してそのグループに属するすべてのタスクをテストすることもできます。--device
:モデルが使用するデバイスを設定します(例:cuda
、cuda:0
、cpu
、mps
)。--batch_size
:評価に使用するバッチサイズを設定します。正の整数に設定するか、auto
に設定することで、メモリに収まる最大のバッチサイズを自動的に選択し、評価速度を向上させることができます。
その他、モデル評価に関連する多くのオプションは、インターフェイスガイドで確認できます。
上記の lm_eval
コマンドを実行して helloSwag
作業をテストした hf
の EleutherAI/gpt-j-6B
モデル評価の結果は、以下のように出力されます。
hf (pretrained=EleutherAI/gpt-j-6B), gen_kwargs: (None), limit: None, num_fewshot: None, batch_size: 8
| Tasks |Version|Filter|n-shot| Metric |Value | |Stderr|
|---------|------:|------|-----:|--------|-----:|---|-----:|
|hellaswag| 1|none | 0|acc |0.5737|± |0.0049|
| | |none | 0|acc_norm|0.7721|± |0.0042|
上の例で最も重要なのは、どのモデルをテストするかを指定する--model
と、モデルに対して何をテストしてどのような結果を得るかタスクを指定する--tasks
です。このとき、チームでファイ ンチューニングした単一モデルやRAG LLMアプリケーションはHarnessに別途登録されていないため、LM
という抽象クラスを開発して新しいモデルとして追加する必要があります。タスクもそれぞれのニーズに応じて作成したデータセットとプロンプト、指標をテストするためにYAMLファイルを利用してカスタマイズできます。
つまり、HarnessでLM
クラスとYAMLファイルによるモデルとデータセット、プロンプトに対するカスタムインターフェイスが提供されており、開発者はそれを利用して自由にLLMを評価できます。以下は、評価プロセスにおいてカスタムインターフェイスの使用ポイントを簡単に図で示したものです。
では、LM抽象クラスを直接実装してチームで開発したモデルを登録する方法を説明します。
モデルクラスの実装
前述で簡単に説明したとおり、新しいモデルを追加するときは、まず、Harnessパッケージ内の抽象クラスであるLM
を継承するモデルクラスを作成します。lm_eval
コマンドの--model_args
で入力される引数はコンストラクタのパラメータに渡され、モデルクラスはそれを利用して必要なモデルを特定したり、ハイパーパラメータを設定したりするなどの初期化作業を行います。
例えば、前述のlm_eval
コマンドの例のようにHugginfaceに依存関係がある場合、以下のようにモデルクラスのコンストラクタを実装できます。
@register_model("my_custom_lm")
class MyCustomLM(LM):
def __init__(self,
model_name: str,
**kwargs):
self._model = AutoModelForCausalLM.from_pretrained(model_name)
self._tokenizer = AutoTokenizer.from_pretrained(model_name)
...
上記のようにモデルクラスを実装するときは、最初にコンストラクタを通じて、モデルクラスが質問に対して即座に答えられる状態にするための初期化ロジックを実装します。初期化ロジックで初期化されたモデルクラスは、次に紹介する3つの抽象メソッドを実装することで質問に答えられます。モデルクラスの最終的な目的はデータセットに対してモデルに質問した結果を返すことですが、そのために求める指標に応じて以下の3つのメソッドのうち1つ以上を実装します。
@register_model("my_custom_lm")
class MyCustomLM(LM):
def __init__(self, ...,**kwargs):
#...
def loglikelihood(self, requests: list[Instance]) -> list[tuple[float, bool]]:
#...
def loglikelihood_rolling(self, requests: list[Instance]) -> list[tuple[float, bool]]:
#...
def generate_until(self, requests: list[Instance]) -> list[str]:
#...
#...
モデルの質疑応答のために1つではなく3つのメソッドをサポートする理由は、各メソッドの回答の形が人間が認識できる文字列形式か、それともトークナイザーでデコードする前のトークン形式かによって求められる指標が違うためです。下の表は3つのメソッドの特徴をまとめたものです。
抽象メソッド名 | 戻り値 | 得られる指標 | 説明 |
---|---|---|---|
generate_until | list[str] | 完全一致(exact_match)や類似度 | テストデータセットの各リクエストに対し、LM がuntil に設定された文字列に達するか、max_gen_toks のサイズに達するまで、モデルが生成した回答を人間が認識できる文字列形式でlist を生成して返すメソッドです。 |
loglikelihood | list[tuple[float, bool]] | パープレキシティ(perplexity)、正確度(Accuracy)など | リクエストに対するターゲット文字列を受け取り、LM がリクエストを入力として予測を生成して、ターゲット文字列が生成される対数尤度(loglikelihood)を返します。結果としては、対数尤度(ll )とターゲット文字列が、欲張りサンプリングによって生成されたかどうか(is_greedy )を表す値が返されます。 |
loglikelihood_rolling | list[tuple[float, bool]] | 単語(word)パープレキシティ、バイトパーフレキシティなど | loglikelihood の特殊なケースで、モデルは入力文字列全体に対して対数尤度を計算します。単語パープレキシティ、バイトパープレキシティなどを求めるときに使います。 |
上記の3つのメソッドをすべて実装する必要はありません。要件によって得られる指標を確認し、必要なメソッドを実装すれば良いでしょう。
以下は3つのメソッドのうちgenerate_until
を実装した例です。コンストラクタで初期化したモデルとトークナイザーを使ってモデルに質問した後、回答された結果をリスト形式で返します。
def generate_until(self, requests: List[Instance]) -> List[str]:
responses = []
for request in requests:
input_text = request.args[0]
inputs = self._tokenizer(input_text, return_tensors="pt")
outputs = self._model.generate(**inputs, max_length=50)
generated_text = self._tokenizer.decode(outputs[0], skip_special_tokens=True)
responses.append(generated_text)
return responses
このように自由にモデルクラスを実装でき、前述のlm_eval
コマンドの実行例(--model hf
)のように、すでに実装されているモデルを使うこともできます。ちなみに、hf
も抽象クラスLM
を実装する方法でプロジェクト内に定義されたもので、使用可能なモデルは以下のリンクから確認できます。