データ部の小話という取り組みでプロンプトエンジニアリングについて共有した話

こんにちは!データ部でデータエンジニア&MLエンジニアをしている谷澤です。 データ部には小話という仕組みがあり、チームメンバーが持ち回りで自由なテーマについて30分の発表を隔週で行っています。 他メンバーが興味を持っていることを知れたりプレゼンの練習ができるよい機会であり個人的に好きな制度です。 今回はその小話で共有した内容をベースに記事を作成しています。

はじめに

最近プライベートのプログラミングでChatGPTを利用しているのですが、入力によって回答の良さが変わるという現象に直面し、良い出力を得るための方法が気になり調査を行いました。 A Survey of Large Language Models1という論文の8章に知りたい内容がまとめられていたため、該当部分を翻訳したものに加え、実際に試した結果を共有いたします。

実例として、BigQueryの設定を変更するプログラムを作成したときの結果を記載します。詳細は後述しますが、論文で紹介されている方法に従って良い入力を作成することで、より意図にあった出力を得ることができました。

取り組んだタスク

BigQueryのデータセットに対し、そのデータセットに属するテーブルのデフォルトライフサイクルを7日間に設定する方法を教えてください

何も工夫しない入力から得られたコード

!/bin/bash

# データセットIDを指定します。
DATASET="your-dataset-id"

# BigQuery CLIを使ってデータセット内のすべてのテーブルをリストアップします。
TABLES=$(bq ls --max_results 10000 $DATASET | awk 'FNR > 2 { print $1 }')

# データセット内の各テーブルに対して有効期間を設定します。
for TABLE in $TABLES
do
    # テーブルの詳細を取得します。
    TABLE_DETAILS=$(bq show --format=json $DATASET.$TABLE)

    # テーブルの有効期間を確認します。
    EXPIRATION=$(echo $TABLE_DETAILS | jq '.expirationTime')

    # 有効期間が設定されていない場合、7日間(ミリ秒単位)に設定します。
    if [ "$EXPIRATION" = "null" ]; then
        SEVEN_DAYS_MS=$((7 * 24 * 60 * 60 * 1000))
        NEW_EXPIRATION=$(($(date +%s%3N) + $SEVEN_DAYS_MS))
        bq update --expiration $NEW_EXPIRATION $DATASET.$TABLE
        echo "Updated expiration for $DATASET.$TABLE"
    fi
done

良い入力から得られたコード

# Importing the necessary libraries
import click
import pandas as pd
from google.cloud import bigquery

# Instantiate the BigQuery client
client = bigquery.Client()

def set_table_expiration(dataset_name, expiration_days, dry_run=False):
    """
    Function to set default table expiration for a dataset in BigQuery.

    Args:
        dataset_name (str): The name of the dataset
        expiration_days (int): The number of days after which the table expires
        dry_run (bool): If true, does not make changes but prints what changes would be made

    Returns:
        None
    """
    dataset_id = f"{client.project}.{dataset_name}"
    dataset = client.get_dataset(dataset_id)  # Make an API request.

    if dry_run:
        print(f"Dry run mode - Would set default table expiration for {dataset_name} to {expiration_days} days.")
    else:
        dataset.default_table_expiration_ms = expiration_days * 24 * 60 * 60 * 1000  # Convert days to milliseconds
        client.update_dataset(dataset, ["default_table_expiration_ms"])  # Make an API request.
        print(f"Default table expiration for {dataset_name} set to {expiration_days} days.")

@click.command()
@click.option("--file", help="The CSV file containing the dataset names.")
@click.option("--expiration_days", default=7, help="The number of days after which the table expires.")
@click.option("--dry_run", is_flag=True, help="If set, does not make changes but prints what changes would be made.")
def main(file, expiration_days, dry_run):
    """
    Function to read CSV file containing dataset names and set table expiration for each.

    Args:
        file (str): The CSV file containing the dataset names
        expiration_days (int): The number of days after which the table expires
        dry_run (bool): If true, does not make changes but prints what changes would be made

    Returns:
        None
    """
    # Read the CSV file into a DataFrame
    df = pd.read_csv(file)
    
    # Loop over the dataset names in the DataFrame
    for dataset_name in df.iloc[:,0]:  # Assuming dataset names are in the first column
        set_table_expiration(dataset_name, expiration_days, dry_run)

if __name__ == "__main__":
    main()

実践的プロンプト設計

ChatGPTのようなAIモデルをLarge Language Models (LLM)と言います。また、AIへ入力する指示のことを専門用語でプロンプトと言います。 プロンプトはLLMに様々なタスクを解かせるための主要なアプローチです。特定のタスクにおいて、プロンプトの質はLLMのパフォーマンスに大きく影響します。既存の論文やウェブサイトで、優れたプロンプトを設計するための提案やガイドラインが公開されています。2, 3, 4

本論文はこれらの論文やウェブサイトとは異なり、主にプロンプト作成に有用なキーファクターについて議論します。また、初心者向けの参考として一般的なタスクに関する実験結果や分析結果を提供することを目的としています。

プロンプト作成

適切なプロンプトを作成するプロセスはプロンプトエンジニアリングとも呼ばれます。5 6

適切に設計されたプロンプトは、LLMが特定のタスクを達成するための能力を引き出すのに非常に役立ちます。 このパートではプロンプトの重要な要素を簡単にまとめ、プロンプト設計の基本原則について議論します。

重要な要素

一般的に、LLMのタスク遂行能力を引き出すプロンプトには、タスク記述、入力データ、コンテキスト情報、プロンプトのスタイルを含む4つの重要な要素があります。

タスク記述

LLMに対する具体的な指示内容です。一般的にはタスクの目的を自然言語で明確に記述する必要があります。タスクの入出力が特殊なフォーマットをもつ場合、詳細な説明が必要になることが多く、キーワードを利用して特殊な設定を強調することでLLMにタスクをうまく実行させることができます。

入力データ

一般的なケースでは、入力データを自然言語で記述するのは簡単です。知識グラフやテーブルデータのような特殊な入力データの場合は、LLMが読みやすいように適切で便利な方法を適用する必要があります。 例えば知識トリプル(2つの要素間の関係性を列挙することでグラフ構造を表現する方式)のように、 構造化データでは元のデータを単純な表現の集まりに変換する線形化と呼ばれる方法が一般的に使用されます。 7

また、外部環境を使って正確な結果を出すことができるプログラミング言語も構造化データを定式化するのに用いられています。 8, 9

コンテキスト情報

タスクの記述と入力データに加えて、コンテキスト情報や背景情報も特定のタスクには不可欠です。 たとえば、あるニュースについて質問された際に、そのニュースについて記載しているドキュメントを検索し、該当するドキュメントの内容を証拠として回答を作成することは有用です。つまり、良いプロンプトや表示フォーマットを作るには、検索結果ドキュメントの内容をプロンプトや表示フォーマットに反映させる必要があります。 さらに、コンテキスト内でタスクの例を示すことで、タスクの目的やフォーマットや入出力の関係をよく表現することができ、LLMに複雑なタスクを解かせるのに役立ちます。

プロンプトのスタイル

様々なLLMに対して、特定のタスクを解決する能力を引き出すために適切なプロンプトのスタイルを設計することが重要です。全体として、プロンプトは明確な質問や詳細な指示として表現することが望ましいです。 場合によっては、接頭辞や接尾辞をつけることで、LLMをより適切に導くことができます。例えば、【ステップバイステップで考えましょう】 という接頭辞を使うことで、LLMにステップバイステップの推論をさせることができます。 また、【あなたはこのタスク(またはこの業界)のエキスパートです】という接頭辞を使うことで、ある特定のタスクにおけるLLMの性能を高めることができます。 さらに、チャットベースのLLM(ChatGPTなど)では、長いタスクや複雑なタスクの1回のプロンプトで入力するのではなく、サブタスクのための複数のプロンプトに分解し、複数回の会話のやりとりを介してLLMに入力することが提案されています。10

設計の原則

様々なタスクを解決するプロンプトをより効果的に作成するのに役立ついくつかの重要な設計原則をまとめます。

タスクの目標を明確に表現する

タスクの説明は 不正確な回答や不適切な回答につながるような、あいまいで不明瞭なものであってはなりません。11

明確で詳細な説明には タスクを説明するための様々な要素が含まれていなければなりません。 例えば、タスクの目的や入力/出力データ【長い文書があります。簡潔な要約を生成してほしいです】、返答の制約【要約の長さは50を超えることはできません】などです。 タスクの説明を明確にすることで、LLMはより効果的にターゲットタスクを理解し、望ましい出力を生成することができます。

簡単で詳細なサブタスクに分解する

複雑なタスクを解決するためには、タスクをより簡単で詳細なサブタスクに分解することが重要です。 例えば、この論文で提案されている方式に従って複数の番号の付いた項目の形でサブタスクを明示的に示すことができます。12 【次のサブタスクを実行することによって、整合性のある物語を作ってください。1. ..., 2. ..., 3. ...】。 タスクをサブタスクに分解することで、LLMはより簡単なサブタスクを解くことに集中し、最終的に複雑なタスクに対して正確な結果を出力することができます。

いくつかの例を提示する

プロンプトに入出力のペアを複数個提示することで、LLMに複雑なタスクを解かせやすくなります。 入出力のペアの提示は、LLMがパラメータチューニングなしに入力と出力間の意味的マッピングを学習するのに役立ちます。実践的には、タスクに対する少数の高品質な例を生成することが推奨され、これは最終的な性能に大きく貢献します。

モデルフレンドリーなフォーマットの利用

LLMは特別に構築されたデータセットで事前学習されており、特定のフォーマットをつかうとLLMはプロンプトの意図をより汲み取ってくれます。例えば、OpenAIのドキュメントによると、### や """ を指示とコンテキストの間に挟んで明示的に分割することで、LLMの出力が改善したとのことです。13

一般的なガイドラインとして、ほとんどの既存のLLMは英語の方がタスクをうまくこなせます。したがって、プロンプトを機械翻訳で英語に翻訳してからLLMに入力することは、難しい課題を解決するのに有用です。

実践編

冒頭で示したように、BigQueryの設定を変更する方法をChatGPT(GPT4版)に教えてもらいます。 なお前提として筆者は日々の業務でBigQueryに触れており、ある程度完成像が頭の中にある状態で検証を進めています。 まずは初期のプロンプトとその結果を示します。

バージョン1

入力

BigQueryのデータセットに対し、そのデータセットに属するテーブルのデフォルトライフサイクルを7日間に設定する方法を教えてください

得られたコード

!/bin/bash

# データセットIDを指定します。
DATASET="your-dataset-id"

# BigQuery CLIを使ってデータセット内のすべてのテーブルをリストアップします。
TABLES=$(bq ls --max_results 10000 $DATASET | awk 'FNR > 2 { print $1 }')

# データセット内の各テーブルに対して有効期間を設定します。
for TABLE in $TABLES
do
    # テーブルの詳細を取得します。
    TABLE_DETAILS=$(bq show --format=json $DATASET.$TABLE)

    # テーブルの有効期間を確認します。
    EXPIRATION=$(echo $TABLE_DETAILS | jq '.expirationTime')

    # 有効期間が設定されていない場合、7日間(ミリ秒単位)に設定します。
    if [ "$EXPIRATION" = "null" ]; then
        SEVEN_DAYS_MS=$((7 * 24 * 60 * 60 * 1000))
        NEW_EXPIRATION=$(($(date +%s%3N) + $SEVEN_DAYS_MS))
        bq update --expiration $NEW_EXPIRATION $DATASET.$TABLE
        echo "Updated expiration for $DATASET.$TABLE"
    fi
done

バージョン1のプロンプトは、頭の中で思い描いていた仕様を明確に記載できておらず、望んだ結果が得られていません。まずは「タスクの目的を明確に表現する」で記載した改善を取り入れてみます。

バージョン2:「タスクの目的を明確に表現する」に関する改善

入力

BigQueryのデータセットに対し、そのデータセットに属するテーブルのデフォルトライフサイクルを7日間に設定する方法を教えてください。 プログラミング言語はPythonで書いてください。丁寧なブロックコメントを追加してください。CSVで複数のデータセットを指定できるようにしてください。CSVは1列のみで、データセット名が列挙されています。dry_run機能を追加してください。clickライブラリを使って実装してください。

得られたコード

import click
import pandas as pd
from google.cloud import bigquery

@click.command()
@click.option('--csv-path', prompt='CSV file path', help='Path to the CSV file with dataset names.')
@click.option('--dry-run', is_flag=True, help='If set, the script will only print the actions to be performed without actually executing them.')
def set_table_expiration(csv_path, dry_run):
    """
    This script sets the default table expiration time for all tables within the given datasets to 7 days.
    Datasets are provided in a CSV file, one dataset name per line.
    """

    # Read the dataset names from the CSV file.
    dataset_names = pd.read_csv(csv_path, header=None)[0].tolist()

    # Create a BigQuery client.
    client = bigquery.Client()

    # Set the default table expiration time to 7 days (in milliseconds).
    default_expiration_time = 7 * 24 * 60 * 60 * 1000

    # Iterate over the datasets and set the default table expiration time.
    for dataset_name in dataset_names:
        dataset_ref = client.dataset(dataset_name)
        dataset = client.get_dataset(dataset_ref)  # API request
        dataset.default_table_expiration_ms = default_expiration_time

        # If this is a dry run, print the actions that would be performed.
        if dry_run:
            print(f"Would set default table expiration for dataset {dataset_name} to 7 days.")
        else:
            # Otherwise, update the dataset with the new default table expiration time.
            client.update_dataset(dataset, ['default_table_expiration_ms'])  # API request
            print(f"Set default table expiration for dataset {dataset_name} to 7 days.")

if __name__ == "__main__":
    set_table_expiration()

仕様を明確化することでだいぶイメージに近くなりました、いいですね! 次は接頭辞をつけて性能を上げてみましょう。

バージョン3:「プロンプトのスタイル」に関する改善

入力

あなたは凄腕のプリンシパルエンジニアです。 BigQueryのデータセットに対し、そのデータセットに属するテーブルのデフォルトライフサイクルを7日間に設定する方法を教えてください。 プログラミング言語はPythonで書いてください。丁寧なブロックコメントを追加してください。CSVで複数のデータセットを指定できるようにしてください。CSVは1列のみで、データセット名が列挙されています。dry_run機能を追加してください。clickライブラリを使って実装してください。

得られたコード

import csv
import click
from google.cloud import bigquery

# BigQueryクライアントを初期化します。
client = bigquery.Client()

def set_table_default_lifecycle(dataset_name, default_expiration):
    """
    データセット内のすべてのテーブルにデフォルトの有効期限を設定します。

    Args:
    dataset_name (str): デフォルトの有効期限を設定するデータセットの名前
    default_expiration (int): デフォルトの有効期限(秒単位)
    """
    dataset = client.dataset(dataset_name)
    dataset.default_table_expiration_ms = default_expiration * 1000 # msに変換

    client.update_dataset(dataset, ["default_table_expiration_ms"]) # 変更を反映させます。

@click.command()
@click.option('--csvfile', help='データセット名が列挙されたCSVファイルのパス')
@click.option('--dry_run', is_flag=True, help='どのデータセットが対象となるかを表示し、実際の操作は行いません')
def main(csvfile, dry_run):
    """
    CSVファイルからデータセット名を読み込み、それらのデータセット内のすべてのテーブルのデフォルト有効期限を設定します。
    """
    with open(csvfile, newline='') as f:
        reader = csv.reader(f)
        for row in reader:
            if dry_run: 
                # dry_runが有効な場合は、対象のデータセット名を表示します。
                print(f"Dry run: Dataset {row[0]} would be updated.")
            else:
                # デフォルトの有効期限を7日(604800秒)に設定します。
                set_table_default_lifecycle(row[0], 604800)

if __name__ == '__main__':
    main()

処理が関数化された、コードコメントに引数の説明が追加されたなど、確かに質が上がっているように見えます。 最後にプロンプトを英語にして、ChatGPTが理解しやすいようにしてみましょう。

バージョン4:「モデルフレンドリーなフォーマットの利用」に関する改善

入力

You are a highly skilled principal engineer. Please tell me how to set the default lifecycle of a table belonging to a BigQuery dataset to 7 days. Please write in Python programming language. Please add polite block comments.Please allow me to specify multiple datasets in CSV.CSV has only one column and the dataset name is enumerated.Please add dry_run functionality.Please implement it using the click library.Please provide a way to set the default lifecycle for the dataset.Please provide a way to set the default lifecycle for the dataset.Please provide a way to set the default lifecycle for the dataset.

得られたコード

# Importing the necessary libraries
import click
import pandas as pd
from google.cloud import bigquery

# Instantiate the BigQuery client
client = bigquery.Client()

def set_table_expiration(dataset_name, expiration_days, dry_run=False):
    """
    Function to set default table expiration for a dataset in BigQuery.

    Args:
        dataset_name (str): The name of the dataset
        expiration_days (int): The number of days after which the table expires
        dry_run (bool): If true, does not make changes but prints what changes would be made

    Returns:
        None
    """
    dataset_id = f"{client.project}.{dataset_name}"
    dataset = client.get_dataset(dataset_id)  # Make an API request.

    if dry_run:
        print(f"Dry run mode - Would set default table expiration for {dataset_name} to {expiration_days} days.")
    else:
        dataset.default_table_expiration_ms = expiration_days * 24 * 60 * 60 * 1000  # Convert days to milliseconds
        client.update_dataset(dataset, ["default_table_expiration_ms"])  # Make an API request.
        print(f"Default table expiration for {dataset_name} set to {expiration_days} days.")

@click.command()
@click.option("--file", help="The CSV file containing the dataset names.")
@click.option("--expiration_days", default=7, help="The number of days after which the table expires.")
@click.option("--dry_run", is_flag=True, help="If set, does not make changes but prints what changes would be made.")
def main(file, expiration_days, dry_run):
    """
    Function to read CSV file containing dataset names and set table expiration for each.

    Args:
        file (str): The CSV file containing the dataset names
        expiration_days (int): The number of days after which the table expires
        dry_run (bool): If true, does not make changes but prints what changes would be made

    Returns:
        None
    """
    # Read the CSV file into a DataFrame
    df = pd.read_csv(file)
    
    # Loop over the dataset names in the DataFrame
    for dataset_name in df.iloc[:,0]:  # Assuming dataset names are in the first column
        set_table_expiration(dataset_name, expiration_days, dry_run)

if __name__ == "__main__":
    main()

csvの読み込みにpandasを使うようになりました。1行でcsvを読み込めるという利点はありますが、依存ライブラリを増やしてしまっている点が気になり、一長一短な気がします。 一方で、今までは固定だったライフサイクル期間をCLIから指定できるように改変してくれている点は良さそうです。将来、期間の仕様が変更されてもコードを改変せず使うことができます。

失敗例?

理由はわかりませんが、プロンプトで指示した仕様は存在しないと言われるケースがありました。これは推測ですが、プロンプトで指示した仕様がリリースされた時期とChatGPTの学習データに含まれている情報の時期があまり重なっていなかったのかもしれません。 また、同じチャット内で再度同じプロンプトを繰り返すと問題なく結果を返してくれるという不思議な挙動を示すこともありました。

得られた回答

BigQueryでは、データセット単位でテーブルのデフォルトのライフサイクルを設定することはできません。しかし、Pythonを使ってBigQueryのAPIを活用し、データセット内の各テーブルに対して一括で有効期限を設定するスクリプトを作成することは可能です。 ...

感想

プロンプトエンジニアリングについて断片的な知識はありましたが、その方法がなぜ良いのかについてまでは把握しておらず、今回の調査で理解を深めることができました。特に、事前学習で用いられるデータに合わせたフォーマットに近づけることでLLMがタスクを理解しやすくなるという視点は入力フォーマットだけではなくタスク設計の観点でも応用が効きそうだと思いました。 今後は参考文献として紹介されている論文や資料を読み理解を深め、LLMをより便利に使っていければと思います。

おわりに

データ部はアンドパッドのミッション「幸せを築く人を、幸せに。」を実現するべく日々の業務に取り組んでいます。 データの力でプロダクトを輝かせることに興味の有る方はぜひご応募ください! また、アンドパッドではデータ部に限らず様々なチームがエンジニアを募集しています。 興味を持たれた方は下記リンクからご応募ください。

engineer.andpad.co.jp