擬似会話でLLMの回答をコントロールする方法|Few-shot Promptingの応用テクニック
大規模言語モデル(LLM)の出力を効果的にコントロールできる方法として、擬似的な会話を活用するテクニックを紹介します。Few-shot Promptingと言われる手法の応用になりますが、テンプレートを用いてLLMの回答も擬似的に与える点がポイントとなります。この記事では、プログラムコードを使いながら、どのように効果があるかを解説します。
はじめに
大規模言語モデル(LLM)の出力をコントロール
一般的に、LLMの出力をコントロールする場合、プロンプトに具体的な指示を書くという方法をがとられます。これは、プロンプトに「箇条書きで記述すること」や「なるべく簡単に説明してください」などの指示を与える方法です。
この方法は、ChatGPTのような大規模なモデルではうまくいきますが、2Bや7Bなどのモデルではどうもうまくいかないことが多いです
規模の小さなモデルは、こちらのプロンプトをうまく理解できないようで、プロンプトでいくら指示をしてもなかなか思った動作をしてくれません。
小さいモデルは、プロンプトで思ったようにコントロールすることが結構難しいと感じています。
擬似的な会話でコントロール
これを解決するのが、擬似的な会話によるLLMのコントロールです。Few-shot Promptingと呼ばれるテクニックの一種となりますが、効果的でしたので紹介します。
例えば、「日本で一番高い山は?」と質問した場合、どのような返答になるかはLLMのモデルによってことなります。
しかし、これまでの会話が以下のようだとしたらどうでしょうか?
世界で一番高い山は?
エベレストです。標高は8,888mです。
世界で2番目に高い山は?
ゴドウィンオースチンです。標高は8,611mです。
世界で3番目に高い山は?
カンチェンジュンガです。標高は8,586mです。
日本で一番高い山は?
このパターンで「日本で一番高い山は?」と聞かれたら「富士山です。標高はxxxxmです」と答えてくれそうな気がしませんか。
実際、これはうまく動作します。llama2-7bでは、「 日本で一番高い山は、富士山です。標高は3,776mです。」と答えてくれました。
この手法はFew-shot promptingと呼ばれる手法の派生です。ただし、テンプレートを使って質問と回答を作成していることがポイントです。
同じに見えますが、以下のようなパターンだと回答がうまくコントロールできませんでした。テンプレートを使う点がポイントだと思われます。
世界で3番目に高い山は?
以下の例に従って、回答してください。
Q:世界で一番高い山は?
A:エベレストです。標高は8,888mです。
Q:世界で2番目に高い山は?
A:ゴドウィンオースチンです。標高は8,611mです。
以下、実際に試した内容を解説します。
Llama2で試してみる
今回利用したLLMはllama2-7bモデルです。特にどのモデルでもよかったのですが、チャットテンプレートに対応していて手軽に使えるモデルということで、llama2を選択しました(7bのモデルは比較的小さいモデルです。今回の手法では、このクラスのモデルでの効果が高いです)。
ここでは、llama2の使い方については詳しく解説しません。興味がある方は以下の記事を参考にしてください。
トークナイザーとモデルをロードする
まず、以下のコードで、トークナイザー(tokenizer
)とモデル(model
)を準備します。
今回利用したのは、meta-llama/Llama-2-7b-chat-hf
というチャット用のモデルになります。モデルがローカルにない場合は、ダウンロードが開始されます(ネット環境により時間がかかります)。
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained(
"meta-llama/Llama-2-7b-chat-hf",
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-2-7b-chat-hf",
torch_dtype=torch.float16, device_map="auto", trust_remote_code=False,
)
普通に質問する
まず、普通に質問をしてみます。
シンプルな質問
最初は、シンプルな質問です。ここでは、プロンプトの生成にapply_chat_template()
を使っています。チャットテンプレートの使い方につては以下の記事が詳しいです。
回答は出力の末尾の[/INST]
以降になりますので、その部分だけ取り出して表示しています。
question = "日本で一番高い山は?"
chat = [
{ "role": "user", "content": question },
]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
print(prompt)
# <s>[INST] 日本で一番高い山は? [/INST]
input_ids = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
tokens = model.generate(
**input_ids.to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.5,
top_p=0.9,
repetition_penalty=1.05,
)
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
output = output.rsplit("[/INST]", 1)
print(output[-1])
回答は以下のようになりました。
とりあえず、回答は得られましたが、期待していたものとは異なります(一応、山の名前と標高書かれていますが少し冗長です)。
Japan has many high mountains, but the highest mountain in Japan is Mount Fuji, which stands at an elevation of 3,776 meters (12,388 feet) above sea level. It is located on Honshu Island and is considered one of the country's most iconic landmarks. Mount Fuji is a popular destination for hiking and climbing, and is also a UNESCO World Heritage Site.
「名前と標高」を尋ねる
少しプロンプトを修正して「名前と標高」を尋ねるように変更しました。
question = "日本で一番高い山は?山の名前と標高を教えてください"
chat = [
{ "role": "user", "content": question },
]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
input_ids = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
tokens = model.generate(
**input_ids.to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.5,
top_p=0.9,
repetition_penalty=1.05,
)
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
output = output.rsplit("[/INST]", 1)
print(output[-1])
回答は以下のようになりました。名前と標高が箇条書きで追加されましたが、まだ冗長です。
Japan has many mountains, and the highest one is Mount Fuji, which is located on Honshu Island. It has a height of 3,776 meters (12,388 feet) above sea level. Here are some other notable mountains in Japan, along with their names and heights:
1. Mount Fuji (Fuji-san) - 3,776 meters (12,388 feet)
2.
限定するように指示
「日本で一番高い山は?山の名前と標高だけを教えてください」と指示を変えてみました。
question = "日本で一番高い山は?山の名前と標高だけを教えてください"
chat = [
{ "role": "user", "content": question },
]
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
input_ids = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
tokens = model.generate(
**input_ids.to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.5,
top_p=0.9,
repetition_penalty=1.05,
)
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
output = output.rsplit("[/INST]", 1)
print(output[-1])
出力は以下になります。なかなか思った通りに出力されません。
Japan has many high mountains, but the highest mountain in Japan is Mount Fuji, which is located on Honshu Island. It has a height of 3,776 meters (12,388 feet) above sea level. Here are some other notable mountains in Japan, along with their names and heights:
1. Mount Fuji - 3,776 meters (12,388 feet)
2. Mount Kitadake
このように、プロンプトで出力をコントロールするのはかなり難しいです。
ということで、会話を模擬してみます。
テンプレートを使って会話を模擬する
まず、これまでの会話を擬似的に作成します。
以下の例では、世界で高い山を3つ質問する内容になっています。また、それぞれの質問に対するモデルの答えも擬似的に追加しています。
prev_chat = [
['世界で一番高い山は?', 'エベレストです。標高は8,888mです。'],
['世界で2番目に高い山は?', 'ゴドウィンオースチンです。標高は8,611mです。'],
['世界で3番目に高い山は?', 'カンチェンジュンガです。標高は8,586mです。']
]
chat = []
for user, assistant in prev_chat:
chat.append({ "role": "user", "content": f"次の内容を、日本語で完結にまとめてください。\n{user}"})
chat.append({ "role": "assistant", "content": assistant})
chat.append({ "role": "user", "content": question },
)
print(chat)
作成した会話は以下のようになります。世界で高い山を3つ聞いて、その後に日本で一番高い山を聞いたというシチュエーションを作っている感じになります。
[{'role': 'user', 'content': '次の内容を、日本語で完結にまとめてください。\n世界で一番高い山は?'},
{'role': 'assistant', 'content': 'エベレストです。標高は8,888mです。'},
{'role': 'user', 'content': '次の内容を、日本語で完結にまとめてください。\n世界で2番目に高い山は?'},
{'role': 'assistant', 'content': 'ゴドウィンオースチンです。標高は8,611mです。'},
{'role': 'user', 'content': '次の内容を、日本語で完結にまとめてください。\n世界で3番目に高い山は?'},
{'role': 'assistant', 'content': 'カンチェンジュンガです。標高は8,586mです。'},
{'role': 'user', 'content': '日本で一番高い山は?'}]
では、これをLLMに投げてみます。
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
input_ids = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
tokens = model.generate(
**input_ids.to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.5,
top_p=0.9,
repetition_penalty=1.05,
)
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
output = output.rsplit("[/INST]", 1)
print(output[-1])
すると、以下のような回答を得ることができました。欲しかったフォーマットです!
このように擬似的な会話(質問と答えのペア)を入力することで、続く質問の回答パターンをある程度コントロールすることが可能です。
日本で一番高い山は、富士山です。標高は3,776mです。
回答が日本語に変化している部分もポイントです
番外編
試しに、他のパターンもやってみました。キーワードから連想する3つのものを列挙するという内容です。
prev_chat = [
['パソコンといえば', '* モニター\n * キーボード \n * マウス'],
['音楽といえば', '* 楽器\n * ジャンル\n * 楽譜'],
['旅といえば', '* パスポート\n * スーツケース\n * 地図']
]
question = "車といえば"
chat = []
for user, assistant in prev_chat:
chat.append({ "role": "user", "content": user})
chat.append({ "role": "assistant", "content": assistant})
chat.append({ "role": "user", "content": question },
)
prompt = tokenizer.apply_chat_template(chat, tokenize=False, add_generation_prompt=True)
input_ids = tokenizer(prompt, return_tensors='pt')
with torch.no_grad():
tokens = model.generate(
**input_ids.to(model.device),
max_new_tokens=100,
do_sample=True,
temperature=0.5,
top_p=0.9,
repetition_penalty=1.05,
)
output = tokenizer.decode(tokens[0], skip_special_tokens=True)
output = output.rsplit("[/INST]", 1)
print(output[-1])
llama2の回答は以下の通りでした。
* 自動車
* ナンバープレート
* ガススティン
これもうまくいきました!
モデルによると思いますが、モデルの出力のコントロールがかなりうまくいきました。上手に使えば、LLMの出力を思ったようにコントロールできる可能性があります。
まとめ
大規模言語モデル(LLM)の使い方で面白いものを見つけたので、実際に試してみました。
意外とうまく動作することが確認でき、うまく利用すればLLMの出力をある程度コントロールできるのでは?と感じました。