KAKEHASHI Tech Blog

カケハシのEngineer Teamによるブログです。

APE(Automatic Prompt Engineer)を使ったプロンプト自動生成の試み

こちらの記事は カケハシ Advent Calendar 2024 の 12日目の記事になります。

adventar.org

背景・目的

カケハシでは、最新の生成AI技術を活用したプロダクト開発を進めています(プレスリリース)。特に、プロンプトエンジニアリングは生成AIの性能を最大限に引き出すための重要な要素です。しかし、プロンプトを書く作業は非常に手間がかかり、試行錯誤が必要です。そこで、私たちはプロンプトを自動生成する革新的な手法であるAPE(Automatic Prompt Engineer)を導入し、その効果を検証することにしました。

APEを使うことで、プロンプト作成の効率を大幅に向上させるだけでなく、生成されるコンテンツの質も向上させることが期待できます。この記事では、APEの仕組みと実際の導入プロセス、そしてその結果について詳しく解説します。プロンプトエンジニアリングに興味がある方や、生成AIの活用を検討している方にとって、非常に有益な情報を提供できると思います。

APE(Automatic Prompt Engineer) とは?

APEは、LLMに人間のレベルのプロンプトエンジニアリングを行わせる手法です。ここでいうプロンプトの前提は、以下のように入力プレースホルダを持つプロンプトのテンプレートを指します。

  • 例: "あなたは優秀な薬剤師です。以下の質問に答えてください。質問: <ここに質問が入る>"

プロンプト(テンプレート)に対して、入力・出力のペアを作ることができます。

  • 例:
    • 入力: "どの薬とどの薬を一緒に飲めば良いのかがわかりづらいのですがどうすれば良いですか?"
    • 出力: "一包化して同じタイミングに飲む薬を一つの袋にまとめることができます"

この手法では、入力・出力の事例がいくつかある前提で、以下の三つのLLMを使って良いプロンプトの探索を行います。

  1. プロンプトの生成(生成関数LLM)
  2. プロンプトの評価(評価関数LLM)
  3. プロンプトの微修正(リサンプルLLM)

探索方法は、反復モンテカルロ探索(Iterative Monte Carlo Search)の手法を使います。

  1. 入力・出力のペアをfew-shotで与えて、プロンプト(テンプレート)を生成させる
  2. 生成されたプロンプトを評価関数LLMを使って評価する
  3. 評価結果が高いものを残して、リサンプルLLMを使って微修正を行う
  4. 2-3を繰り返す

反復モンテカルロ探索はブラックボックス最適化の一種で、スコアが高くなるプロンプトはスコアが高いプロンプトの周辺にあるだろうという仮定を使って、スコアが高いプロンプトからリサンプリングを行い探索していきます。

反復モンテカルロ探索のイメージ 2 4 1 4 3 0 4 5 2 3 3 2 4 4 3 2 1 3 2 4 1 2 6 1 2 初期値 1回目のリサンプル結果 2回目のリサンプル結果 最大スコアのプロンプト

上の画像は、反復モンテカルロ探索のイメージ図ですが、各点がプロンプトを表していてその右には評価スコア(高いほど良い)を書いています。
評価が高かったプロンプトに対して、リサンプルを繰り返していき、最終的に高いスコアを持つプロンプトを見つけ出すというアルゴリズムになります。

準備

今回は、個人情報のマスキングの事例で試してみたいと思います。具体的には、入力として「名前」を与えられたときに、その名前をマスキングした文章を生成するプロンプトを作成します。医療の現場では個人情報の取り扱いに注意が必要ですが、個人情報のマスキングのLLMを作ることができれば個人情報の漏洩のリスクを減らすことができます。

LLMはOpenAIのGPT-4o miniを使います。1000入出力トークンを100回リクエストしても10円程度という破格の安さで、高い精度の文章生成をすることができます。(今回はあくまでもサンプルです。OpenAIのAPIを利用する場合はOpenAIに個人情報をリクエストすることになるので、本番環境で行う場合は個人情報の取り扱いについて検討する必要があります。)

以下の文章のマスキングを行うプロンプトを作成してみましょう。

  1. 山田太郎は1990年5月3日に東京都で生まれました。
  2. 佐藤花子の電話番号は090-0000-0000です。
  3. 鈴木一郎はABC株式会社に勤務していて、メールアドレスはichiro.suzuki@example.comです。
  4. 私の住所は大阪府大阪市北区梅田1丁目です。
  5. 高橋健二の銀行口座番号は1234567890です。
  6. 松本涼子のマイナンバーは1234-5678-9012-3456です。
  7. 私のパスポート番号はAB123456です。
  8. 山口真由美はLINE IDがmayumi_yamaguchi123です。
  9. 本日、田中大輔に会いに京都の四条通りまで行ってきました。
  10. 内田浩司はXYZ不動産の顧客で、顧客番号は987654321です。

実験環境としては、Google Colaboratoryを使います。実際にAPEを動かすためのコードは以下のようになりました。

コード例

共通の関数

!pip install tiktoken openai
from google.colab import userdata
from openai import OpenAI
import tiktoken

client = OpenAI(api_key=userdata.get('OPENAI_API_KEY'))
enc = tiktoken.get_encoding("o200k_base")

def generate(prompt: str, temperature: float = 0) -> str:
    """プロンプトを使って文章を生成する"""
    response = client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[{
            "role": "user",
            "content": prompt,
        }],
        temperature=temperature
    )
    return response.choices[0].message.content

def count_tokens(text: str) -> int:
    """文字列のトークン数を数える"""
    return len(enc.encode(text))
  • Colaboratoryで動かすことを前提にしています
    • シークレットにOPENAI_API_KEYを設定してください

生成関数LLM

def to_pairs(dataset: list[tuple[str, str]]):
    lines = []
    for d in dataset:
        line = f"<pair><input>\n{d[0]}\n</input><output>\n{d[1]}\n</output></pair>"
        lines.append(line)
    return lines

GEN_PROMPT = """
I gave a friend an instruction and five
inputs. The friend read the instruction
and wrote an output for every one of
the inputs. Here are the input-output
pairs:
{pairs}

The instruction was
""".strip()

def generate_prompt(dataset: list[tuple[str, str]]):
    """プロンプトを生成する"""
    prompt = GEN_PROMPT.format(pairs='\n'.join(to_pairs(dataset)))
    output = generate(prompt=prompt, temperature=0.5)
    return output + '\n<input>\n{input_value}\n</input>'
  • 生成関数LLMのプロンプトは、論文に記載されているものを使っています。
  • 生成関数LLMは、入力と出力のペアを与えられると、それを使ってプロンプトを生成します。
  • 生成関数LLMは、GPT-4o miniを使っています。パラメータとしては、多様性を重視しtemperature=0.5を指定しています。
  • 論文ではReverse Mode GenerationやCustomized Promptsという生成方法もありましたが、今回はForward Mode Generationのみを使いました
    • Reverse Mode Generationは、コードの補完のような前後の文脈から穴埋め問題を解くような生成方法を使うアプローチなのですが、OpenAIのgpt-4o miniでは使えないため、今回は使いませんでした。(legacyのCompletion APIだとsuffixパラメータを使ってfill-in-the-middleの生成ができたみたいです)

評価関数LLM

SCORE_PROMPT = """
入力プロンプトに対する、出力の妥当性について、1 ~ 5の整数値で評価してください。
値が大きいほど良いとします。
出力は必ず数値だけにしてください。

<input>
{input_value}
</input>
<output>
{output_value}
</output>

"""

def calc_score(template: str, input_value: str, output_value: str) -> int:
    """入力・出力のペアについて、スコアを計算する"""
    prompt = SCORE_PROMPT.format(
        input_value=template.format(input_value=input_value),
        output_value=output_value
    )
    output = generate(prompt=prompt, temperature=0)
    return int(output)


def generate_output(template: str, input_value: str) -> str:
    """改善したいLLMアプリケーション"""
    prompt = template.format(input_value=input_value)
    output = generate(prompt=prompt, temperature=0)
    return output

def score_function(template: str, test_dataset: list[str]) -> float:
    """プロンプトの評価を行う関数"""
    outputs = [
        generate_output(template, input_value)
        for input_value in test_dataset
    ]
    scores = [
        calc_score(template, input_value, output_value)
        for input_value, output_value in zip(test_dataset, outputs)
    ]

    return sum(scores) / len(scores)
  • プロンプトの評価は、そのプロンプトを使って出力を生成させ、別のLLMを使って評価を行いました。
  • 評価用のデータセット(入出力のペア)に対してスコアを計算し、その平均値を取ることでプロンプトの評価結果としています。
  • 評価関数LLMもGPT-4o miniを使っています。パラメータとしては多様性は不要なのでtemperature=0を指定しています。

リサンプルLLM

RESAMPLE_PROMPT = """
Generate a variation of the
following instruction while
keeping the semantic meaning.

INPUT: {instruction}
OUTPUT:
""".strip()
def resample(template: str) -> str:
    """リサンプル(プロンプトの微修正)を行う関数"""
    prompt = RESAMPLE_PROMPT.format(instruction=template)
    output = generate(prompt=prompt, temperature=0.5)
    return output

def resample_prompt(seed_templates: list[str], resample_times: int) -> list[str]:
    """プロンプトをリサンプルする"""
    resampled_template = [
        resample(seed_template)
        for seed_template in seed_templates
        for _ in range(resample_times)
    ]
    return resampled_template
  • リサンプルの候補を与えると、それらを微修正したプロンプトをリストにして返しています
  • リサンプルの回数は外部から変えられるようにしています
  • リサンプルのプロンプトは、論文に記載されているものを使っています
  • リサンプルLLMもGPT-4o miniを使っています。パラメータとしては多様性が必要なのでtemperature=0.5を指定しています。

APEの実行

A = 3 # 反復回数
B = 3 # 1プロンプト当たりのリサンプル回数
C = 3 # リサンプルのために残すプロンプト数

dataset = [
    ("山田太郎は1990年5月3日に東京都で生まれました。", "XXXはXXX年XXX月XXX日にXXXで生まれました。"),
    ("佐藤花子の電話番号は090-0000-0000です。", "XXXの電話番号はXXXです。"),
    ("鈴木一郎はABC株式会社に勤務していて、メールアドレスはichiro.suzuki@example.comです。", "XXXはXXXに勤務していて、メールアドレスはXXXです。"),
]
test_dataset = [
    "私の住所は大阪府大阪市北区梅田1丁目です。",
    "高橋健二の銀行口座番号は1234567890です。",
    "松本涼子のマイナンバーは1234-5678-9012-3456です。",
    "私のパスポート番号はAB123456です。",
    "山口真由美はLINE IDがmayumi_yamaguchi123です。",
    "本日、田中大輔に会いに京都の四条通りまで行ってきました。",
    "内田浩司はXYZ不動産の顧客で、顧客番号は987654321です。",
]
results = []
templates = []
for i in range(A):
    if i == 0:
        templates = [generate_prompt(dataset) for _ in range(C)]
    else:
        templates = resample_prompt(templates, num=B)

    for p in templates:
        s = score_function(p, test_dataset)
        results.append((p, s, i))
    templates = [p for p, s, i in sorted(results, key=lambda x: x[1], reverse=True)][:C]

import pandas as pd
final_df = pd.DataFrame(sorted(results, key=lambda x: x[1], reverse=True), columns=['prompt', 'score', 'depth'])
final_df['num_tokens'] = final_df['prompt'].apply(count_tokens)
print(final_df)
  • APEの実行は、上記のコードで行いました
  • 反復回数は3回、リサンプル回数は3回、リサンプルのために残すプロンプト数は3としています
  • リサンプルのシードとなるプロンプトは、前回までに生成したプロンプトのうちスコアが高いTopC件を使っています

実行する際の料金について

gpt-4o-miniの料金は、

  • \$0.150/1M 入力トークン
  • \$0.600/1M 出力トークン

です。

料金を計算するために変数として

  • 1実行あたりの平均入力トークン数: I
  • 1実行あたりの平均出力トークン数: O
  • 反復回数: A
  • プロンプトのリサンプル回数: B
  • リサンプルのために残すプロンプト数: C
  • 評価データセットのレコード数: Y

とすると、料金はざっくりと以下のようになります。

  • LLM実行回数: R = A * B * C * (2Y + 1)
    • (2Y+1)の+1は、ループの中でリサンプルが1回行われる分
  • 料金: (\$0.150 * R * I + \$0.600 * R * O) / 1M

I = 1000, O = 1000, A = 3, B = 3, C = 3, Y = 10とすると、料金は以下のようになります。

  • LLM実行回数: R = 3 * 3 * 3 * (20 + 1) = 567
  • 料金(\$): (\$0.150 * 567 * 1000 + \$0.600 * 567 * 1000) / 1M = \$0.43
  • 料金(円): \$0.43 * 150 = 64円

プロンプトエンジニアリングをしてもらって64円で済むというのはかなり安いですね!

結果

以下のようになりました。上の方のプロンプトはスコアが平均で5と、最高得点を取っています。

depthは何回目の反復で生成されたプロンプトかを示しています。depthが大きいほど、リサンプルが多く行われたプロンプトです。これを見ると、初回で良いプロンプトが生成されていたということがわかりますね。

index prompt score depth num_tokens
0 to replace specific personal information in the input sentences with placeholders "XXX". The goal was to anonymize the data while maintaining the structure of the original sentences. Each output reflects this transformation by substituting names, dates, locations, and contact information with generic placeholders.\n\<input>\n{input_value}\n\</input> 5.0 0 61
1 Transform the provided sentences by substituting particular personal details with the placeholder "XXX". The objective is to anonymize the information while preserving the original sentence structure. Each resulting output will showcase this modification by replacing names, dates, places, and contact details with generic placeholders. \n\<input> \n{input_value} \n\</input> \nOUTPUT: 5.0 1 69
2 Transform the provided sentences by substituting particular personal details with the placeholder "XXX". The objective is to anonymize the content while preserving the original sentence structure. Each output will demonstrate this modification by replacing names, dates, places, and contact details with generic placeholders. \n\<input> \n{input_value} \n\</input> \nOUTPUT: 5.0 1 68
3 Transform the given sentences by substituting identifiable personal details with the placeholder "XXX". The objective is to protect the privacy of the information while preserving the original sentence structure. Each resulting output will demonstrate this change by replacing names, dates, places, and contact details with generic placeholders. \n\<input> \n{input_value} \n\</input> \nOUTPUT: 5.0 1 71
4 Transform the given sentences by substituting particular personal details with the placeholder "XXX". The aim is to anonymize the content while preserving the original sentence structure. Each output should demonstrate this change by replacing names, dates, locations, and contact details with generic placeholders.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 5.0 2 68
5 Transform the given sentences by replacing specific personal information with the placeholder "XXX". The goal is to anonymize the content while maintaining the original sentence format. Each output will reflect this change by substituting names, dates, locations, and contact information with generic placeholders.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 5.0 2 70
6 Please modify the given sentences by replacing specific personal information with the placeholder "XXX". The goal is to anonymize the content while maintaining the original structure of the sentences. Each output will reflect this change by substituting names, dates, locations, and contact information with generic placeholders.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 5.0 2 73
7 Revise the given sentences by replacing specific personal information with the placeholder "XXX". The aim is to anonymize the text while maintaining the original structure of the sentences. Each output will illustrate this change by substituting names, dates, locations, and contact information with generic placeholders.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 5.0 2 74
8 Rephrase the given sentences by replacing specific personal information with the placeholder "XXX". The goal is to anonymize the text while maintaining the original sentence format. Each result will illustrate this change by substituting names, dates, locations, and contact information with generic placeholders.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 5.0 2 71
9 to replace specific personal information in the input sentences with placeholders (XXX). The goal was to anonymize the content while retaining the structure of the original sentences. Each output reflects the same format as the input but removes identifiable details such as names, dates, locations, phone numbers, and email addresses.\n\<input>\n{input_value}\n\</input> 4.857142857142857 0 68
10 to replace specific personal information in the input sentences with placeholders (represented as "XXX"). The goal is to anonymize the data while retaining the structure of the sentences. Each output replaces names, dates, locations, phone numbers, and email addresses with "XXX" to protect the privacy of the individuals mentioned.\n\<input>\n{input_value}\n\</input> 4.857142857142857 0 70
11 Please substitute identifiable personal information in the provided sentences with placeholders (XXX). The objective is to anonymize the content while preserving the original sentence structure. Each output should mirror the input format but eliminate recognizable details such as names, dates, locations, phone numbers, and email addresses.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 4.857142857142857 1 71
12 Transform the given sentences by substituting identifiable personal details with placeholders, denoted as "XXX". The objective is to ensure the data remains anonymous while preserving the original sentence structure. In each output, substitute names, dates, locations, phone numbers, and email addresses with "XXX" to safeguard the privacy of the individuals referenced.\n\<input>\n{input_value}\n\</input>\nOUTPUT: 4.857142857142857 1 81

今回はスコア関数はLLMに評価させましたが、これぐらいの問題であれば評価データセットで個人情報が入っていないか?を検査して厳密にテストした方が良かったかもしれません。

考察

  • 元々は日本語でプロンプトを書いていたのですが、英語のプロンプトが生成されるため、近い精度を出せるプロンプトでありながらトークン数を大幅に削減できる点がとても良いと感じました。
  • 入出力のペアや評価関数を作るのは大変ですが、それを作ることでプロンプトの改善ができるというのは非常に魅力的だと感じました。
  • 1回APEを回すための料金について、gpt-4o-miniの場合はそこまで高くはないですが、コストを削減するためには生成用のデータセットと評価用のデータセットの選定も重要だと感じました。
    • 評価用のデータセットは、結果が相関しないものを集めるのが良いと思われます。
  • 反復回数については、3回で十分だと感じました。論文にも書いてありましたが、結果を見てもリサンプルによる改善は限定的なので、そこまで大きい値ではなくても良さそうだと感じました。

まとめ

この記事では、APE(Automatic Prompt Engineer)を用いたプロンプト自動生成の試みについて詳しく解説しました。APEを活用することで、プロンプト作成の効率化ができることを紹介しました。具体的な実装方法や評価プロセス、実行結果についても紹介し、APEの有用性を示しました。カケハシでは今後も生成AI技術を活用し、プロダクト開発を進めていく予定です。プロンプトエンジニアリングに興味がある方や生成AIの活用を検討している方にとって、有益な情報となれば幸いです。

参考資料