見出し画像

[LangChain]謎機能Few-shot prompt templatesを分かりやすく解説

一生調べててやっと理解したのでこのnoteに残します。


・これってなに

単体の機能としては、複数の文字列セットを保持して、そこから一気に全てのセットをもらったり、〇番目の文字列セットをもらったりするだけです。

例えば、
"今日は", "うどん食べた"
"明日は", "かつ丼食べる"

という文字列セットを用意した場合に、
じゃあこれに、
"明後日は","うな重食べる"
を末尾に追加しよう!とした時に、一気に文字列セットが取得できるので簡単に
"今日は", "うどん食べた"
"明日は", "かつ丼食べる"
"明後日は","うな重食べる"

という文字列が作れる!便利だね、ぐらいです。
でもこれが大事で、「複数の文章データを合体させて、プロンプトにできる」が他の機能と組み合わせると役立つんです。

・類似データ検索機能モジュールと組み合わせる

例えば、企業Aが、自社製品に対するQ&Aのデータを大量に持っていたとします。その一部はこうだったとします。

Q: 製品Xのバッテリー寿命はどれくらいですか?
A: 製品Xのバッテリー寿命は約10時間です。
Q: 製品Xの保証期間は?
A: 製品Xの保証期間は1年間です。

で、新たに製品Yを開発しました。製品YのQ&Aも製品Xと同様に作らなければいけません。でも、例えば製品XのQ&Aが2個じゃなくて20個ぐらいあったら大変ですよね。別に大変じゃない?では、200個あったとしましょう。

で、Few-shot prompt templatesと類似データ検索機能を組み合わせると、別に製品YのQ&Aを作らなくて済むんです。
どういうこと?
例えば製品Yに関するQ&Aを用意せずに、製品情報だけをAIに持たせて、回答させたとしましょう。
するとこんな回答をする可能性があります。

Q.この前買った製品Yの調子が悪いです。バッテリー寿命を教えてください。
A: 製品Yのバッテリー寿命は、2年間です。もし調子が悪い場合は、サポートセンターや販売店にお問い合わせいただくと良いでしょう。

でも、こちらとしては他の製品のQ&Aと似たような形にしたいです。こんな感じに。
A: 製品Yの保証期間は2年間です。

で、例えば、200個持っているQ&Aの中から、類似のデータだけを取得できる機能のモジュールを使って、こんな文字列セットが取得できたとします。

Q: 製品Aのバッテリー寿命はどれくらいですか?
A: 製品Aのバッテリー寿命は約10時間です。
Q: 製品Bのバッテリー寿命はどれくらいですか?
A: 製品Bのバッテリー寿命は約5時間です。
Q: 製品Cのバッテリー寿命はどれくらいですか?
A: 製品Cのバッテリー寿命は約7時間です。
Q: 製品Dのバッテリー寿命はどれくらいですか?
A: 製品Dのバッテリー寿命は約8時間です。
Q: 製品Eのバッテリー寿命はどれくらいですか?
A: 製品Eのバッテリー寿命は約10時間です。
Q: 製品Fのバッテリー寿命はどれくらいですか?
A: 製品Fのバッテリー寿命は約10時間です。

これをFew-shot prompt templatesに持たせて、Few-shot prompt templatesに、末尾に
Q.この前買った製品Yの調子が悪いです。バッテリー寿命を教えてください。
という質問を追加するように設定すれば、これらを一つの塊としてまとめてプロンプトとしてAIに投げることができるわけです。
そうすると、
A: 製品Yのバッテリー寿命は約2年間です。
と返答する確率が上がります。

これだけ聞くと、類似データ検索機能という別のモジュールがすごいだけで、Few-shot prompt templatesはいらなくね?と思うかもしれませんが、
「機能としてこういうのが提供されていて、他の機能とも連携できる」というのがたぶん最大の利点で、要は自分であれこれ用意するのと比べてラクに、かつコードが簡潔になります。

・他の応用例

1、応答の質の向上

実は応答の精度も良くすることが期待されます。
類似データ検索機能のモジュールと併用することが条件ですが、
ある質問に対して、類似する質問を一緒に投げると良いらしいです。ChatGPTいわく。
なぜか。
大規模言語モデルは、大量のテキストデータで訓練することによって出来ていることは知っていると思いますが、そのデータを応用することで、新規の質問に答えられるのです。
で、じゃあどのデータを応用するの?という部分で、
類似する過去の質疑応答をプロンプトで一緒に投げると、AIが考える部分を"誘導"できるので、AIが考えやすくなるらしいです。

2、周辺の情報も一緒に回答できる

例えば「糖尿病の原因は?」という質問に対して、

ユーザー: 高血圧の原因は?
応答: 高血圧の原因には遺伝、食生活、ストレス、過度な塩分摂取などがあります。定期的な健康診断が推奨されます。
ユーザー: 心臓病のリスク因子は?
応答: 心臓病のリスク因子には喫煙、高コレステロール、糖尿病、肥満などがあります。健康的な生活習慣が重要です。

という関連情報も類似データ検索モジュールが持ってきてくれれば、それをプロンプトに含めることで、AIが関連情報も回答してくれることが期待できます。

なるほど。で、どう実装するの?

公式ドキュメントを参考にします。まず下記のように複数の文章を用意します。

examples = [
    {
        "question": "ムハンマド・アリとアラン・チューリングのうち、誰がより長生きしたか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ムハンマド・アリは亡くなるまでに何歳でしたか?
中間の答え:ムハンマド・アリは亡くなるまでに74歳でした。
フォローアップ:アラン・チューリングは亡くなるまでに何歳でしたか?
中間の答え:アラン・チューリングは亡くなるまでに41歳でした。
最終的な答えは:ムハンマド・アリ
"""
    },
    {
        "question": "craigslistの創設者はいつ生まれましたか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:craigslistの創設者は誰ですか?
中間の答え:Craigslistはクレイグ・ニューマークによって創設されました。
フォローアップ:クレイグ・ニューマークはいつ生まれましたか?
中間の答え:クレイグ・ニューマークは1952年12月6日に生まれました。
最終的な答えは:1952年12月6日
"""
    },
    {
        "question": "ジョージ・ワシントンの母方の祖父は誰ですか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョージ・ワシントンの母は誰ですか?
中間の答え:ジョージ・ワシントンの母はメアリー・ボール・ワシントンでした。
フォローアップ:メアリー・ボール・ワシントンの父は誰ですか?
中間の答え:メアリー・ボール・ワシントンの父はジョセフ・ボールでした。
最終的な答えは:ジョセフ・ボール
"""
    },
    {
        "question": "ジョーズとカジノ・ロワイヤルの監督は同じ国出身ですか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョーズの監督は誰ですか?
中間の答え:ジョーズの監督はスティーブン・スピルバーグです。
フォローアップ:スティーブン・スピルバーグはどこ出身ですか?
中間の答え:アメリカ合衆国。
フォローアップ:カジノ・ロワイヤルの監督は誰ですか?
中間の答え:カジノ・ロワイヤルの監督はマーティン・キャンベルです。
フォローアップ:マーティン・キャンベルはどこ出身ですか?
中間の答え:ニュージーランド。
最終的な答えは:いいえ
"""
    }
]

PromptTemplateを使用したフォーマッターを用意します。なにそれ?これは文字列を渡すと既存の文字列の特定の箇所を置換してくれるものです。

example_prompt = PromptTemplate(input_variables=["question", "answer"], template="Question: {question}\n{answer}")

print(example_prompt.format(**examples[0]))

input_variablesは必須のパラメータを指定するものです。このフォーマッターを使う時は必ず"question"と"answer"を使いますよ!と設定してます。
print(example_prompt.format(**examples[0]))
とありますが、これが実際にそうで、先ほどたくさん文章が入ったexamples変数を作りましたが、この1つ目の"ムハンマド・アリとアラン・チューリングのうち~"のやつを、"question"と"answer"に分解して、フォーマッターに渡しています。

template="Question: {question}\n{answer}は、置換したい文字列ですね。
さっきのムハンマドの文章を"question"と"answer"に分解したので、それをそれぞれ入れるようにしてます。
結果、

Question: ムハンマド・アリとアラン・チューリングのうち、誰がより長生きしたか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ムハンマド・アリは亡くなるまでに何歳でしたか?
中間の答え:ムハンマド・アリは亡くなるまでに74歳でした。
フォローアップ:アラン・チューリングは亡くなるまでに何歳でしたか?
中間の答え:アラン・チューリングは亡くなるまでに41歳でした。
最終的な答えは:ムハンマド・アリ

がフォーマッタを通して作られます。

次は、本命のFewShotPromptTemplateを作ります。

prompt = FewShotPromptTemplate(
    examples=examples, 
    example_prompt=example_prompt, 
    suffix="Question: {input}", 
    input_variables=["input"]
)

print(prompt.format(input="メアリー・ボール・ワシントンの父親は誰ですか?"))

やっていることは2つで、この記事の上の方で説明しましたが、
FewShotPromptTemplateは複数の文章を一つの文章として展開するような処理をするのでしたね。
で、その展開した文章に末尾に
"Question: メアリー・ボール・ワシントンの父親は誰ですか?"
という文章を追加してます。これを実行すると、

Question: ムハンマド・アリとアラン・チューリングのうち、誰がより長生きしたか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ムハンマド・アリは亡くなるまでに何歳でしたか?
中間の答え:ムハンマド・アリは亡くなるまでに74歳でした。
フォローアップ:アラン・チューリングは亡くなるまでに何歳でしたか?
中間の答え:アラン・チューリングは亡くなるまでに41歳でした。
最終的な答えは:ムハンマド・アリ


Question: craigslistの創設者はいつ生まれましたか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:craigslistの創設者は誰ですか?
中間の答え:Craigslistはクレイグ・ニューマークによって創設されました。
フォローアップ:クレイグ・ニューマークはいつ生まれましたか?
中間の答え:クレイグ・ニューマークは1952年12月6日に生まれました。
最終的な答えは:1952年12月6日


Question: ジョージ・ワシントンの母方の祖父は誰ですか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョージ・ワシントンの母は誰ですか?
中間の答え:ジョージ・ワシントンの母はメアリー・ボール・ワシントンでした。
フォローアップ:メアリー・ボール・ワシントンの父は誰ですか?
中間の答え:メアリー・ボール・ワシントンの父はジョセフ・ボールでした。
最終的な答えは:ジョセフ・ボール


Question: ジョーズとカジノ・ロワイヤルの監督は同じ国出身ですか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョーズの監督は誰ですか?
中間の答え:ジョーズの監督はスティーブン・スピルバーグです。
フォローアップ:スティーブン・スピルバーグはどこ出身ですか?
中間の答え:アメリカ合衆国。
フォローアップ:カジノ・ロワイヤルの監督は誰ですか?
中間の答え:カジノ・ロワイヤルの監督はマーティン・キャンベルです。
フォローアップ:マーティン・キャンベルはどこ出身ですか?
中間の答え:ニュージーランド。
最終的な答えは:いいえ


Question: メアリー・ボール・ワシントンの父親は誰ですか?

という文字列が生成されます。

SemanticSimilarityExampleSelectorと組み合わせる

これは、ちょくちょく記事で出てきた類似データ検索機能モジュールです。
複数の文章を持たせた上である文章を渡すと、複数の文章のうち、意味が類似してる文章をピックアップしてくれます。
クラスの生成方法はこんな感じ。

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings

example_selector = SemanticSimilarityExampleSelector.from_examples(
    # これは選択できる例のリストです。
    examples, 
    # これは、意味的な類似性を測定するために使用される埋め込みを生成するために使用される埋め込みクラスです。
    OpenAIEmbeddings(), 
    # これは、埋め込みを保存し、類似性検索を実行するために使用される VectorStore クラスです。
    Chroma, 
    # これは生成されるサンプルの数です。
    k=1
)

examples変数は先ほど作りましたね。"ムハンマド・アリとアラン・チューリングのうち~"のような文章の塊のやつです。
OpenAIEmbeddings()は、どれを使用して埋め込みを作りますか?という指定です。埋め込みってなに!文章をベクトル表現にすることを言います。どゆこと?
例えば、"これは、テストドキュメントです。"という文章をベクトル表現にすると、
[-0.007878163829445839, -0.0035345633514225483, ..., -0.006051199045032263]
という感じの、1536個のベクトルになったりします。
これの何がうれしいかというと、AとBとCの文章のベクトルの差を比較すれば、どれが一番意味的に近いか数学的に計算できるのです。

↓参考

3つめのChromaってなに!
文章をベクトル表現もとい埋め込みしたものを収納する箱です。

じゃあ試しに使ってみましょう。

from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.vectorstores import Chroma
from langchain.embeddings import OpenAIEmbeddings


example_selector = SemanticSimilarityExampleSelector.from_examples(
    # これは選択できる例のリストです。さっきの持ってきてください。
    examples,
    # これは、意味的な類似性を測定するために使用される埋め込みを生成するために使用される埋め込みクラスです。
    OpenAIEmbeddings(),
    # これは、埋め込みを保存し、類似性検索を実行するために使用される VectorStore クラスです。
    Chroma,
    # これは生成されるサンプルの数です。
    k=1
)

question = "メアリー・ボール・ワシントンの父親は誰ですか?"
selected_examples = example_selector.select_examples({"question": question})
print(f"Examples most similar to the input: {question}")
for example in selected_examples:
    print("\n")
    for k, v in example.items():
        print(f"{k}: {v}")

するとこんな結果になります。

Examples most similar to the input: メアリー・ボール・ワシントンの父親は誰ですか?


answer:
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョージ・ワシントンの母は誰ですか?
中間の答え:ジョージ・ワシントンの母はメアリー・ボール・ワシントンでした。
フォローアップ:メアリー・ボール・ワシントンの父は誰ですか?
中間の答え:メアリー・ボール・ワシントンの父はジョセフ・ボールでした。
最終的な答えは:ジョセフ・ボール

question: ジョージ・ワシントンの母方の祖父は誰ですか?

なんかquestionとanswerが逆ですが気にしなくていいです。
これはフォーマッターがまだ無いからで、
上の方で作ったPromptTemplateを使用したフォーマッターを使えば、
template="Question: {question}\n{answer}
という形で順番通りに整形されます。

じゃあいよいよ、FewShotPromptTemplateとSemanticSimilarityExampleSelectorを組み合わせます。
組み合わせるとどうなるの?
SemanticSimilarityExampleSelectorは意味が類似した文章だけを引っ張ってくるわけですが、FewShotPromptTemplateはそれを展開して末尾に文字列を追加したりできるわけですよね。なので、
新たな質問内容に対して
"意味が類似した文章"
+
"新たな質問内容"
のプロンプトを作ることができます。
スクリプトの全文を貼るとこうなります。

from langchain.embeddings import OpenAIEmbeddings
from langchain.vectorstores import Chroma
from langchain.prompts.example_selector import SemanticSimilarityExampleSelector
from langchain.prompts.few_shot import FewShotPromptTemplate
from langchain.prompts.prompt import PromptTemplate

examples = [
    {
        "question": "ムハンマド・アリとアラン・チューリングのうち、誰がより長生きしたか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ムハンマド・アリは亡くなるまでに何歳でしたか?
中間の答え:ムハンマド・アリは亡くなるまでに74歳でした。
フォローアップ:アラン・チューリングは亡くなるまでに何歳でしたか?
中間の答え:アラン・チューリングは亡くなるまでに41歳でした。
最終的な答えは:ムハンマド・アリ
"""
    },
    {
        "question": "craigslistの創設者はいつ生まれましたか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:craigslistの創設者は誰ですか?
中間の答え:Craigslistはクレイグ・ニューマークによって創設されました。
フォローアップ:クレイグ・ニューマークはいつ生まれましたか?
中間の答え:クレイグ・ニューマークは1952年12月6日に生まれました。
最終的な答えは:1952年12月6日
"""
    },
    {
        "question": "ジョージ・ワシントンの母方の祖父は誰ですか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョージ・ワシントンの母は誰ですか?
中間の答え:ジョージ・ワシントンの母はメアリー・ボール・ワシントンでした。
フォローアップ:メアリー・ボール・ワシントンの父は誰ですか?
中間の答え:メアリー・ボール・ワシントンの父はジョセフ・ボールでした。
最終的な答えは:ジョセフ・ボール
"""
    },
    {
        "question": "ジョーズとカジノ・ロワイヤルの監督は同じ国出身ですか?",
        "answer":
        """
ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョーズの監督は誰ですか?
中間の答え:ジョーズの監督はスティーブン・スピルバーグです。
フォローアップ:スティーブン・スピルバーグはどこ出身ですか?
中間の答え:アメリカ合衆国。
フォローアップ:カジノ・ロワイヤルの監督は誰ですか?
中間の答え:カジノ・ロワイヤルの監督はマーティン・キャンベルです。
フォローアップ:マーティン・キャンベルはどこ出身ですか?
中間の答え:ニュージーランド。
最終的な答えは:いいえ
"""
    }
]


example_selector = SemanticSimilarityExampleSelector.from_examples(
    # これは選択できる例のリストです。
    examples,
    # これは、意味的な類似性を測定するために使用される埋め込みを生成するために使用される埋め込みクラスです。
    OpenAIEmbeddings(),
    # これは、埋め込みを保存し、類似性検索を実行するために使用される VectorStore クラスです。
    Chroma,
    # これは生成されるサンプルの数です。
    k=1
)

example_prompt = PromptTemplate(input_variables=["question", "answer"], template="Question: {question}\n{answer}")

prompt = FewShotPromptTemplate(
    example_selector=example_selector,
    example_prompt=example_prompt,
    suffix="Question: {input}",
    input_variables=["input"]
)

print(prompt.format(input="メアリー・ボール・ワシントンの父親は誰ですか?"))

結果はこうなります。

Question: ジョージ・ワシントンの母方の祖父は誰ですか?

ここでフォローアップの質問が必要ですか:はい。
フォローアップ:ジョージ・ワシントンの母は誰ですか?
中間の答え:ジョージ・ワシントンの母はメアリー・ボール・ワシントンでした。
フォローアップ:メアリー・ボール・ワシントンの父は誰ですか?
中間の答え:メアリー・ボール・ワシントンの父はジョセフ・ボールでした。
最終的な答えは:ジョセフ・ボール

Question: メアリー・ボール・ワシントンの父親は誰ですか?

想定通り、意味が近い文章を持ってきて、末尾に新規の質問を追加したプロンプトが出来ました。

この記事が気に入ったらサポートをしてみませんか?