【Promptim】LLM-as-a-JudgeによるPrompt Tuningを試してみた

初めまして、馬場(@soniccan)です。普段は、ML,LLMを正しくデリバリーする仕組みを考えています。

今回、プロンプト最適化ライブラリである「promptim」を用い、LLM-as-a-Judgeによるプロンプトチューニングの実験を行った結果、うまく行ったので、この記事では、その雰囲気をお伝えしたいと思います。

1. やったこと

  • promptimの評価関数をLLM-as-a-Judgeにした。

  • 一般的なLLM応答をプロンプトチューニングにより、ツンデレ応答にする実験を行った。

  • 学習過程を観察して、挙動を確認した。

2.背景

人手によるプロンプトチューニングの限界

本番レベルのLLM運用において、プロンプトはLLMのアウトプットの質を決める重要な要素になります。しかし、プロダクトの成長につれて、多くの要望に答えるプロンプトを人手によって作ることは、困難になっていくでしょう。

Data-Centricなプロンプトチューニングの可能性

一般的なMLOpsと違い、LLMOpsは、その性能を測定することも改善する方法もBest Practiceなるものが定まっていません。
今回の最適化は、LangSmith上に作成した正解ラベル付きデータセットを用いて行いました。これにより、MLOpsのようなData-CentricなLLMOpsを実現しうるのではないかと考えました。


3.やり方

3.1 promptim

promptim」は、2024/11/13にLangChain公式ブログで紹介されたプロンプト最適化ライブラリです。定量的な評価に基づいて、プロンプトを改善するプロセスを良い感じにやってくれます。

Promptimは特定のタスクに関するプロンプトを改善するプロセスを自動化します。 初期プロンプト、データセット、カスタム評価者(およびオプションで人間によるフィードバック)を提供すると、promptimは最適化ループを実行し、元のプロンプトより優れたプロンプトを作成します。

LangChain公式ブログより

3.2 LLM-as-a-Judge

「promptim」では、評価関数を自前で実装する必要があります。
今回は、この評価関数をLLMを用いて設定し、LLMによって評価する仕組み(LLM-as-a-Judge)を導入しました。

以下が今回の評価関数の概要です。

LLMから返ってきてほしい応答(正解ラベル)と実際の応答の類似度をLLMによって、評価してもらい、類似スコア(0.0~1.0)と改善のための指示コメントを作成してもらう、これを評価関数※1とする。

※1 LangSmithの標準の評価指標を応用しても良さそうではあります。

3.3 実際のコード

評価関数はこちらです。LLMに正解ラベルと応答の類似度を測定してもらい、スコアと改善コメントを生成してもらっています。

class LLMOutput(BaseModel):
    score: float = Field(description="スコアは、0.0から1.0の範囲で評価してください。厳しく評価してください")
    comment: str = Field(description="もっと正解の出力に近づけるコメントを提供してください。")

def example_evaluator(run: Run, example: Example) -> dict:
    with tracing_v2_enabled(project_name="evaluator-trace"):
        output_label = example.outputs  # 正解ラベル
        output: AIMessage = run.outputs["output"]  # 実際のLLMの出力
        
        llm = ChatOpenAI(model="gpt-4o-mini").with_structured_output(LLMOutput)
        prompt = ChatPromptTemplate.from_messages([
            ("system", "あなたは、LLMの出力と正解の出力を比較して、LLMの出力が正解の出力にどれだけ近いかを評価するLLMです。..(省略)\n"),
            ("user", "正解の出力: {output_label} LLMの出力: {output}")
        ])
        chain = prompt | llm
        result = chain.invoke({"output_label": output_label["Output"], "output": output.content})

    return {
        "key": "my_example_criterion",
        "score": result.score,
        "comment": result.comment,
    }

その他、詳しい実装は、「promptim」のソースコードをご覧ください

5.実験設定

今回は、LLMの応答をツンデレセリフを集めた簡易的なデータセットを用い、LLMがツンデレ化する過程を観察しました。

初期プロンプト

あなたは彼女として振る舞ってください。

データセット。左が入力で右が期待してるアウトプットです。

作品「推しの子」から有馬かなのセリフをスクレイピングし、それを元にLLMに生成させました。

後述しますが、お金がもったいないので、データ数10個でepoch数10で実験を行いました。

6. 実験

6.1 LLM-as-a-Judge側

Judge側に与えたプロンプトは単純でしたが、的確なコメントを返してくれています。スコアも近い回答になっているときは、スコアが高くなっております。

1回目の訓練

LLMの応答と正解ラベルの回答が全く異なっております。ツンデレ感がありません。

2回目の訓練

ツンデレセリフが組み込まれるようになりました。アドバイスも的確です。

最終的なアウトプット

偶然「やるべきこと」と言う共通のワードが出たため、評価が高くなっているようです

6.2 スコアの遷移

前半では、スコアが、0.3を下回るケースもありましたが、後半は、そのケースが見られなくなりました。Judge側の実装にもよりますが、scoreが後半改善されにくかったので、機械学習のような逆伝播でゴリゴリ改善されていくみたいな雰囲気ではなさそうですね。

1~20回の評価
20~70回の評価

6.3 生成されたプロンプト

最終的なプロンプトはこちらになりました。

あなたは以下の性格を持つ彼女として振る舞ってください:

- ツンデレな性格で、直接的な愛情表現は控えめですが、本当は相手のことを考えています
- 時には厳しく指摘し、相手の成長を促します
- 甘やかしすぎず、必要なときは厳しいアドバイスをします
- 相手の言い訳を簡単には受け入れません
- 「しょうがないわね」「ばか」などのツンデレらしい言葉を時々使います
- 相手の成長を願う気持ちから、時には厳しい言葉で叱咤激励します
- 基本的に冷静で、感情的になりすぎない対応を心がけます

返答は簡潔に、かつ的確なアドバイスを含めるようにしてください。甘やかしすぎず、かといって冷たすぎない、適度な距離感を保ちながら対応してください。

以下が印象的でした。

  • ツンデレになりなさいとは命令していないが、"ツンデレ"と言うワードを学習により、導き出せている

  • 正解ラベルの特徴である罵倒+フォロー+アドバイスと言う特徴を捉えられている

  • 自分では、言語化できていないところまでプロンプトを書いてくれる

思ったより良い結果が出て満足です。

7.感想

  • promptim有能
    公式の例では、ルールベースのもので、パッとしてなかったのですが、LLM-as-a-judgeを導入してみた結果、思ったより体験が良かったです。

  • 訓練にLLMを用いるので、料金が怖い
    訓練一回につき、生成用とJudge用の2回LLMに投げる必要があるので、リッチなモデルに使うのは、難しそうです。
    生成用には高いモデル、Judgeには安いモデルにするなど工夫が必要そうです。

  • MLのスコアとは少し違った雰囲気になりそう
    データセットも評価も曖昧性を含んでいるため、accuracy=0.8ですごい!みたいなことにはならないのが難しいところです。これは、Ragas ,Gen AI Evaluation Serviceなどを触って勘所をつかみたいです。

  • LLM-as-a-Judgeを、外部ツールに任せる形をやってみたい
    LLM-as-a-Judgeを提供してくれるツールは、いっぱいあるので、それらを応用してみた方が、可用性が上がるかもしれません

8.最後に

しっかりとした技術ブログは初投稿でした。今後とも発信頑張りますので、何卒よろしくお願いします。


最後に、ツンデレに、苦労を癒してもらいたいと思います。

※筆者はツンデレが特段好きな訳ではありません


馬場(@soniccan)でした。お読みいただきありがとうございました。


いいなと思ったら応援しよう!