🔬

ChatGPTは馬鹿じゃない! 真の実力を解放するプロンプトエンジニアリングの最前線

2023/03/08に公開
5

はじめに

ChatGPTをはじめとしたLLMを使いこなすための必須スキル、プロンプトエンジニアリング について解説します。

最近は動きが早すぎてキャッチアップが難しくなっていますが、特に以下のような手法が注目されているようです。

  1. In-context Learning (ICL)
  2. Chain-of Thought (CoT)
  3. Zero-shot CoT
  4. ReAct
  5. Self-Consistency
  6. Program-aided Language Model (PAL)

今回は、6つのテクニックの中からPart1として、ICL、CoT、そしてZero-shot CoTの3つを紹介します。
これらのテクニックは、ChatGPTをはじめとするLLMのポテンシャルを最大限に引き出すために必要不可欠です。
さらに、各テクニックを詳しく解説した論文も紹介していますので、是非ご一読ください。

In-context Learning (ICL)

ICLとは、タスクを指示する際に単にタスクの内容を指示するだけでなく、タスクのデモンストレーションを提供することで大きく性能を高める方法です。

ICLは与えるデモンストレーションの数によって分類されます。
1つのデモンストレーションを与える場合は「One-shot learning」
複数のデモンストレーションを与える場合は「Few-shot learning」
と呼ばれます。
なお、デモンストレーションを用いない従来の方法はICLと対比して「Zero-shot transfer (learning)」と呼ばれます。

Zero-shot

Zero-shotではタスクの指示を与えるだけで、要約などがこれにあたります。

以下の例では、Wikipediaの「インフレーション」の文章1038文字を259文字に要約することができました。

Input
以下の文章を要約して

経済学では、一定期間にわたって経済の価格水準が全般的に上昇することをインフレーション(英語: inflation、物価上昇、インフレ)と呼ぶ[1][2][3][4]。一般的な価格水準が上昇すると、1単位の通貨で購入できる財やサービスの数が減る。その結果、インフレーションは1単位の通貨あたりの購買力の低下、つまり経済における交換手段や会計単位の実質的な価値の低下を反映する[5][6]。インフレーションの反対はデフレーションであり、財やサービスの一般的な価格水準が持続的に低下することである。インフレーションの一般的な指標はインフレ率で、物価(通常は消費者物価指数)の長期的な変化率を年率換算したものである。
経済学者は、非常に高いインフレ率やハイパーインフレーションは有害であり、マネーサプライの過剰な増加が原因であると考えている[7]。一方、低・中程度のインフレ率を決定づける要因については、より多様な見解がある。低・中程度のインフレは、財・サービスに対する実質的な需要の変動や、物資が不足しているときなどの供給可能量の変化に起因すると考えられる[8]。しかし、長期的に持続するインフレは、マネーサプライが経済成長率を上回るスピードで増加することによって起こるというのが共通の見解である[9][10]。
インフレは、経済に様々な良い影響と悪い影響を与える。インフレの負の影響としては、お金を保有することによる機会費用の増加、将来のインフレに対する不確実性による投資や貯蓄の抑制、さらにインフレが急速に進んだ場合には、消費者が将来の価格上昇を懸念して買いだめを始め、商品が不足することなどが挙げられる。ポジティブな効果としては、名目賃金の硬直性による失業率の低下、中央銀行の金融政策の自由度の拡大、お金をため込むのではなく融資や投資を促すこと、デフレに伴う非効率性の回避などが挙げられる。
今日、大半のエコノミストは、低位で安定したインフレ率を支持している[11]。インフレ率が低い(ゼロやマイナスではなく)と、景気後退の際に労働市場の調整が迅速に行われるため、景気後退の深刻さが軽減され、流動性の罠によって金融政策が経済を安定させることができなくなるリスクが軽減されるのである。インフレ率を低く安定的に維持する任務は、通常、金融当局に与えられている。一般的に、これらの金融当局は中央銀行であり、金利の設定、公開市場操作、銀行の預金準備率の設定を通じて金融政策をコントロールする。
Output
経済学では、物価の全般的な上昇であるインフレーションが定義され、これにより通貨の購買力が低下することがわかる。インフレ率は、長期的に持続するインフレはマネーサプライの過剰な増加が原因であるという見解が共通している。インフレには、保有するお金に対する機会費用の増加、将来の不確実性による投資や貯蓄の抑制などの負の影響と、失業率の低下や金融政策の自由度の拡大などのポジティブな影響がある。大半のエコノミストは、低位で安定したインフレ率を支持している。金融当局、通常は中央銀行がインフレ率をコントロールする任務を持っている。

One-shot/Few-shot

One-shot/Few-shotでは、「タスクの指示」に加えて「デモンストレーション」を与えます。

具体例をみてみましょう。
こちらのGraphGPTはpromptをもとにKnowledge Graphを生成することができるものですが、その内部ではOne-shot learningが用いられています。

https://github.com/varunshenoy/GraphGPT

こちらがGraphGPTで用いられているプロンプトです。
現在表示中のグラフデータとユーザーが入力したプロンプトをそれぞれ$state$promptに代入した上でGPT-3のAPIを呼び出しています。
「タスクの指示」「デモンストレーション」それぞれの内容を見てみましょう。
https://github.com/varunshenoy/GraphGPT/blob/dcea106fcba18776f065216712ab931d783dd038/public/prompts/stateful.prompt

タスクの指示 (1行目)

グラフの現在の状態とプロンプトをもとに、プロンプトからできるだけ多くの関係を推定し、状態を更新せよ。各ノードには、ID、ラベル、色(16進数)がある。すべてのエッジは、ノードIDを持つtoとfrom、およびラベルを持つ。エッジは有向性であるため、fromとtoの順番が重要である。

https://github.com/varunshenoy/GraphGPT/blob/dcea106fcba18776f065216712ab931d783dd038/public/prompts/stateful.prompt#L1

デモンストレーション (3~10行目)

このようなデモンストレーションを含めることがOne-shot及びFew-shotの特徴です。
以下の1枚目のグラフに対してアリスとボブはルームメイトというプロンプトを組み合わせると2枚目の画像のようになる」という例を与えています。

https://github.com/varunshenoy/GraphGPT/blob/dcea106fcba18776f065216712ab931d783dd038/public/prompts/stateful.prompt#L3-L10


{ "nodes": [ { "id": 1, "label": "Bob", "color": "#ffffff" } ], "edges": [] }

{ "nodes": [ { "id": 1, "label": "Bob", "color": "#ffffff" }, { "id": 2, "label": "Alice", "color": "#ff7675" } ], "edges": [ { "from": 1, "to": 2, "label": "roommate" } ] }

Chain-of-Thought (CoT)

CoTはReasoningという論理的な思考能力が求められるタスクにおいて精度を向上させるために用いられるテクニックです。
ReasoningタスクではFew-shotを用いても精度が期待するほど向上しないことが多く、CoTがよく用いられます。

CoTとはその名の通り連鎖的な思考をさせることにより出力の精度を高めるというものです。

「連鎖的な思考」といってもピンとこないですが、数学の証明問題のようなイメージです。

例えば、人間が数学の問題を解く時に、いきなり答えを出すのではなく、途中式を書くことで間違えにくくなることは想像に難くないでしょう。
CoTはこの傾向のLLMバージョンです。

具体的なイメージをつかむために、実際にChatGPTに算数の問題を解かせてCoTの精度を検証してみましょう。

通常の質問

ChatGPTのAPIを使用し、以下のつるかめ算をそれぞれ100回ずつ回答させます。

鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

正解は「鶴が60匹、亀が20匹」なのですが、まずは特に工夫せずに聞いてみましょう。
問題の答え以外を出力させないためにフォーマットをJSONで指定します。

Input
問題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

回答は以下のJSONフォーマットに従ってください。
{
    "鶴の数": ,
    "亀の数": 
}
解答例
{
    "鶴の数": 60,
    "亀の数": 20
}

使用したコードはこちら。

import os
import openai
from dotenv import load_dotenv
import json
import pandas as pd


load_dotenv()

key = os.getenv("API_KEY")
openai.api_key = key


from datetime import datetime


class GptInterface:
    def __init__(self, model: str, key: str):
        self.model = model
        self.key = key
        openai.api_key = key

    def single_message(self, message: str):
        if hasattr(self, "last_exec_time"):
            while (datetime.now() - self.last_exec_time).total_seconds() < 0.05:
                pass
        res = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=[
                {"role": "user", "content": message},
            ],
        )
        message = res["choices"][0]["message"]["content"]
        self.last_exec_time = datetime.now()
        return message



def read_json(text:str):
    is_valid_json = True
    data = None
    try:
        data = json.loads(text)
    except Exception as e:
        is_valid_json = False
        try:
            json_string = text[text.find("{"): text.rfind("}") + 1]
            data = json.loads(json_string.replace("'", '"'))
        except Exception as e:
            print(e)
    return data, is_valid_json
 
 
gpt = GptInterface("gpt-3.5-turbo", key)

question = """
問題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

回答は以下のJSONフォーマットに従ってください。
{
   "鶴の数": ,
   "亀の数": 
}
"""

responses = []
data_counter = 0
total_num = 100

while True:
    total_counter += 1

    content = gpt.single_message(question)
    result, is_valid_json = read_json(content, )
    if result is not None and is_valid_json:
        responses.append(result)
        data_counter += 1

    if data_counter >= total_num:
        break

df = pd.DataFrame(responses)

df.groupby(['鶴の数', '亀の数']).size().reset_index(name='データ数')
table = df.groupby(['鶴の数', '亀の数']).size().reset_index(name='データ数').sort_values("データ数", ascending=False).reset_index(drop=True)
table

結果

正答率は28%でした。
つるかめ算は小4・5年くらいに習う程度のものなので、あまり賢いとは言えませんね。

鶴の数 亀の数 データ数
60 20 28
50 30 28
20 60 23
40 40 14
30 50 6
35 45 1

One-shot

次に例題を追加してOne-shotを試してみましょう。
同じようなつるかめ算の問題を与えてみます。

+例題
+鶴と亀が合計6匹います。
+足の数の和が20本であったとき、鶴と亀はそれぞれ何匹いますか?
+
+答え
+{
+    "鶴の数": 2,
+    "亀の数": 4
+}

問題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

回答は以下のJSONフォーマットに従ってください。
{
    "鶴の数": ,
    "亀の数": 
}

結果

正答率はなんと 1% になってしまいました。ReasoningタスクではOne-shotは不向きのようですね。
なお、数値を変えて数パターン試してみましたが、ほとんど同じような結果になりました。

鶴の数 亀の数 データ数
40 40 49
50 30 27
30 50 22
20 60 1
60 20 1

Few-shot

今度は例題を5つに増やしてみましたが、正答率は 3% でした。
※例題を追加しただけなのでプロンプトは省略

結果

鶴の数 亀の数 データ数
40 40 78
20 60 10
30 50 4
50 30 4
60 20 3
10 70 1

CoT

それではCoTを試してみましょう。これまでと違う点は2つあります。

  1. 例題の解法を追加する

    式を解く過程をかなり詳細に記述しているところがポイントです

    +解法
    +鶴の数をx、亀の数をyとする
    +鶴と亀の合計は6匹なので、x + y = 6 (式1)
    +足の数の合計は20本なので、2x + 4y = 20 (式2)
    +式1を変形すると、y = 6 - x (式3)
    +式2に式3を代入すると、2x + 4(6 - x) = 20 (式4)
    +式4を変形する
    +2x + 24 - 4x = 20
    +-2x + 24 = 20
    +-2x = 20 - 24
    +-2x = -4
    +-2x / -2 = -4 / -2
    +x = 2
    +これを式1に代入すると、
    +2 + y = 6
    +y = 6 - 2
    +y = 4
    +よって、鶴は2匹、亀は4匹である
    
  2. ChatGPTに途中式を記述させる

    解法と追加することで、明示的に解法をアウトプットさせています。

    問題
    鶴と亀が合計80匹います。
    足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?
    
    +解法
    
    答え
    {
       "鶴の数": ,
       "亀の数": 
    }
    
Input
例題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

+解法
+鶴の数をx、亀の数をyとする
+鶴と亀の合計は6匹なので、x + y = 6 (式1)
+足の数の合計は20本なので、2x + 4y = 20 (式2)
+式1を変形すると、y = 6 - x (式3)
+式2に式3を代入すると、2x + 4(6 - x) = 20 (式4)
+式4を変形する
+2x + 24 - 4x = 20
+-2x + 24 = 20
+-2x = 20 - 24
+-2x = -4
+-2x / -2 = -4 / -2
+x = 2
+これを式1に代入すると、
+2 + y = 6
+y = 6 - 2
+y = 4
+よって、鶴は2匹、亀は4匹である

答え
{
    "鶴の数": 2,
    "亀の数": 4
}


問題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

+解法

答え
{
    "鶴の数": ,
    "亀の数": 
}

結果

正解率はなんと90%!!かなり驚異的な結果になりました。

鶴の数 亀の数 データ数
60 20 90
20 60 4
30 50 3
2 4 1
40 40 1
50 30 1

こちらがChatGPTの実際の出力です。例題の解法を忠実に真似ていることがわかります。

Output
この問題は先ほどの例題と同じです。
鶴の数をx、亀の数をyとする
鶴と亀の合計は80匹なので、x + y = 80 (式1)
足の数の合計は200本なので、2x + 4y = 200 (式2)
式1を変形すると、y = 80 - x (式3)
式2に式3を代入すると、2x + 4(80 - x) = 200 (式4)
式4を変形する
2x + 320 - 4x = 200
-2x + 320 = 200
-2x = 200 - 320
-2x = -120
-2x / -2 = -120 / -2
x = 60
これを式1に代入すると、
60 + y = 80
y = 80 - 60
y = 20
よって、鶴は60匹、亀は20匹である

答え
{
   "鶴の数": 60,
   "亀の数": 20
}

Zero-shot Chain-of-Thought

Zero-shot CoTとは、ReasoningタスクにおいてZero-shotで精度を向上させるテクニックです。

いきなり回答を生成させるのではなく、そこに至る過程を先に生成させるだけで精度が向上するというもので、CoTと同じようなアプローチ方法を取ります。
CoTの特徴は次の二つでした。

  1. 例題の解法を与える
  2. 途中式を記述させる

Zero-shot CoTとは、このうちの 2.途中式を記述させるだけでも精度をあげることができる というものです。
やり方は非常にシンプルで、論文ではプロンプトにLet's think step by step.と加えるだけで精度が向上したことを報告しています。

それでは、先ほどと同じつるかめ算の問題にこれを用いてみましょう。

Input
問題
鶴と亀が合計80匹います。
足の数の和が200本であったとき、鶴と亀はそれぞれ何匹いますか?

+解き方と答えを教えてください

解法

答え
{
    "鶴の数": ,
    "亀の数": 
}

結果

正解率は 66% となり、通常のZero-shotより38%高い結果となりました。
解法の例を与えるCoTよりは低いですが、「思考過程を記述させる」だけで精度が向上するというのが重要です。

鶴の数 亀の数 データ数
60 20 66
50 30 13
40 40 8
30 50 6
35 45 2
20 60 1
105 25 1

おわりに

以上、ICL, CoT, Zero-shot CoTの3つを紹介してきました。
割と初歩的な内容だったかもしれませんが、体系的な理解の助けになればと思います。
Part2ではReActPALなどを扱う予定ですので、そちらもよろしくお願いします。

参考資料

今回の記事はこちらの動画及びリポジトリを参考にしています。資料探しには最適ですので是非。

https://youtu.be/dOxUroR57xs

https://github.com/dair-ai/Prompt-Engineering-Guide

ICL: Language Models are Few-Shot Learners
CoT: Chain-of-Thought Prompting Elicits Reasoning in Large Language Models
Zero-shot Reasoning: Large Language Models are Zero-Shot Reasoners

Discussion

kyokyo

細かいかもしれませんが、zero-shotは例を伴わないので、in-context learningに該当しません。

Brown, T., Mann, B., Ryder, N., Subbiah, M., Kaplan, J.D., Dhariwal, P., Neelakantan, A., Shyam, P., Sastry, G., Askell, A. and Agarwal, S., 2020. Language models are few-shot learners. Advances in neural information processing systems, 33, pp.1877-1901. https://proceedings.neurips.cc/paper/2020/hash/1457c0d6bfcb4967418bfb8ac142f64a-Abstract.html

yasuyasu

ご指摘ありがとうございます!以下のように修正しました。

ICLは与えるデモンストレーションの数によって分類されます。
1つのデモンストレーションを与える場合は「One-shot learning」
複数のデモンストレーションを与える場合は「Few-shot learning」
と呼ばれます。
なお、デモンストレーションを用いない従来の方法はICLと対比して「Zero-shot learning」と呼ばれます。

kyokyo

迅速なご対応ありがとうございます。

より素晴らしくなったと思います。
ですが、"zero-shot learning"でも良いのかもしれませんが、私は"zero-shot transfer (learning)"としたほうが適切な気がします。
どう思いますか?

yasuyasu

たしかにtransferの方が適切のように思えました。反映させていただきます。
ありがとうございます!