📝

LangChainを用いて自動で最適なfew-shot promptingを実装させよう!

2023/08/26に公開

ChatGPTが出てからプロンプトエンジニアリングというワードが注目されていますね!プロンプトエンジニアリングとは、Large Language Models(LLM)から望む答えを引き出すためのプロンプト(指示)の最適化を指します。正確な回答を得るためには、どのように質問や指示を行うかが非常に重要な要素となってきます!


few-shot prompting

プロンプトエンジニアリングの手法の中でもfew-shot promptingというのはご存知ですか?
詳細は以下のリンクですごくわかりやすく解説されているので知らない方は是非チェックしてみて下さい!
https://www.promptingguide.ai/jp/techniques/fewshot
few-shot promptingによって出力の形式や具体的な解答例を明示的に提示することで、望む回答をより精度高く取得することが出来るんです。

Dynamic few-shot prompting

最近、LangChainでfew-shot promptingに関する面白いツールを見つけたので紹介したいと思います!Dynamic(動的な) few-shot promptingというもので、入力されたプロンプトに応じてあらかじめ用意していたものの中から最適なfew-shot-promptingを選択してくれるんですね!
以下は公式ドキュメントです。
https://python.langchain.com/docs/modules/model_io/prompts/prompt_templates/few_shot_examples_chat

LangChainとは?

LangChainは、ChatGPTのような言語モデルの機能を効率的に拡張するためのライブラリです。このツールを使用すると、長文の入力や複雑な計算問題への対応など、言語モデルの課題を短いコードで効率的に解決することができます。
https://www.langchain.com
https://zenn.dev/umi_mori/books/prompt-engineer/viewer/langchain_overview

早速実行してみよう!

必要なライブラリは下記です。

pip install langchain
pip install faiss-cpu
pip install openai

OpenAIのAPIキーも必要になります!OpenAIのAPIキーの取得方法が分からない場合は他の方の記事を参考に取得して下さい。
最終的なコードは下記です。コードは公式ドキュメントに書いてあるのをまとめたものです。

from langchain.prompts import SemanticSimilarityExampleSelector
from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.chat_models import ChatOpenAI
from langchain.prompts import (
    FewShotChatMessagePromptTemplate,
    ChatPromptTemplate,
)
import openai
openai.api_key = 'your-api-key-here'
# few shot promptingの定義
examples = [
    {"input": "2+2", "output": "4"},
    {"input": "2+3", "output": "5"},
    {"input": "2+4", "output": "6"},
    {"input": "What did the cow say to the moon?", "output": "nothing at all"},
    {"input": "Write me a poem about the moon",
     "output": "One for the moon, and one for me, who are we to talk about the moon?",},
]

# データの入力と出力を連結してベクトル化のためのリストを作成
to_vectorize = [" ".join(example.values()) for example in examples]

# OpenAIの埋め込みモデルを初期化
embeddings = OpenAIEmbeddings()

# 教師データをベクトル化してChromaベクトルストアに保存
vectorstore = Chroma.from_texts(to_vectorize, embeddings, metadatas=examples)

# セマンティック類似性に基づく選択器を初期化。kは類似する最上位kの例を選択する数を意味する
example_selector = SemanticSimilarityExampleSelector(
    vectorstore=vectorstore,
    k=2,
)

# メッセージプロンプトテンプレートを定義
few_shot_prompt = FewShotChatMessagePromptTemplate(
    input_variables=["input"],
    example_selector=example_selector,
    example_prompt=ChatPromptTemplate.from_messages(
        [("human", "{input}"), ("ai", "{output}")]
    ),
)

# 最終的なチャットプロンプトテンプレートを定義
final_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a wondrous wizard of math."),
        few_shot_prompt,
        ("human", "{input}"),
    ]
)

# チャットモデルとプロンプトテンプレートをチェーン化
chain = final_prompt | ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo")

# チェーンを使って質問を投げ、結果を表示
print(chain.invoke({"input": "What's 3+3?"}).content)

実際の流れに沿って説明しますね!

①事前にfew-shot promptingに使用する入力、出力のセットを複数個用意しベクトル化します。
ベクトル化のモデルは色々ありますがOpenAIのEmbeddingモデルを使用します。

examples = [
    {"input": "2+2", "output": "4"},
    {"input": "2+3", "output": "5"},
    {"input": "2+4", "output": "6"},
    {"input": "What did the cow say to the moon?", "output": "nothing at all"},
    {"input": "Write me a poem about the moon",
     "output": "One for the moon, and one for me, who are we to talk about the moon?",},
]
to_vectorize = [" ".join(example.values()) for example in examples]
embeddings = OpenAIEmbeddings()
vectorstore = Chroma.from_texts(to_vectorize, embeddings, metadatas=examples)

②今回のプロンプトは"What's 3+3?"です。(LangChainのchainというツールを使います。)
examplesでいえば上から3つのいずれかを持ってきてほしいですね。その場合、答えは"6"となるでしょう!

chain.invoke({"input": "What's 3+3?"}).content

③few-shot promptingに使用するものを先程ベクトル化したリストの中から選択します。
選択するのはSemanticSimilarityExampleSelectorという関数でプロンプトの"What's 3+3?"と類似度の高いinputをランク付けし、上位から指定された個数のinput、outputのセットを取り出してくれます。kで何個選択するか指定出来ます。今回は2個と指定しています。

few_shot_prompt = FewShotChatMessagePromptTemplate(
    input_variables=["input"],
    example_selector= SemanticSimilarityExampleSelector(vectorstore=vectorstore,k=2),
    example_prompt=ChatPromptTemplate.from_messages(
        [("human", "{input}"), ("ai", "{output}")]
    ),)

④選んだfew_shot_promptをsystem promptと自身のプロンプトの間に挿入します。

final_prompt = ChatPromptTemplate.from_messages(
    [
        ("system", "You are a wondrous wizard of math."),
        few_shot_prompt,
        ("human", "{input}"),
    ]
)

⑤そして最終的なプロンプトが出来上がり、これをLLMに渡します。

chain = final_prompt | ChatOpenAI(temperature=0.0, model="gpt-3.5-turbo")

出てきた回答は、

3

希望通りの回答が得られましたね!
本当に最適なfew-shot promptingが出来てるのか気になった方は以下で確認出来ます!

print(example_selector.select_examples({"input": "What's 3+3?"}))
#出力 [{'input': '2+3', 'output': '5'}, {'input': '2+2', 'output': '4'}]

 
ベクトル化は実施する毎にコストが発生します。(chatのコストに比べれば僅かですが)
入力、出力のリストが決まっている場合はローカルに保存してコード実行の度に呼び出すのも良いでしょう!公式ではChromaというDBを用いていますが、これは保存出来ないみたいで代わりにFAISS DBを使用します。
以下はexamplesというフォルダー名でベクトル情報を保存するコードになります。

from langchain.vectorstores import FAISS
vectorstore = FAISS.from_texts(to_vectorize, embeddings, metadatas=examples)
vectorstore.save_local("./examples")

実行するとexamplesというフォルダーにfaissとpickleのファイルが保存されていると思います!
呼び出す際は下記のコードです。

vectorstore = FAISS.load_local("./examples", embeddings)

これでコード実行ごとにexamplesのベクトル化をすることはなくなりました!
(*プロンプトのベクトル化は実行ごとにする必要があります)
 
few-shot promptingに関して、私の経験上、例を多くすることは必ずしも有利ではないように感じます。特に、1shotや2shotが最もバランスが良いと考えています。例を増やすと、トークン数(コスト)が増加するし、期待する回答が別の例に影響されて得られなくなる気がしています。
そう思っていたところにこのツールを見つけたのでこれはいいと思って紹介した次第です!
 
少々マニアックなテクニックかもしれませんが、知っておいて損はないと思います。ぜひ一度試してみてください!Happy coding!

Discussion