rinnaのjapanese-gpt-neox-3.6b-instruction-ppoで口調や性格を簡単に変える-(2) 固定プロンプト記述の簡素化とシンプルな記憶をGUIで試す。
新しいモデルもたくさん出ている中、非商用制限や会話制限も少なめで会話ができるモデルならrinnaかな、と始めたテストです。Fine-turningをせずにどこまでできるのか、試して見たくてGUIを準備しつつ、ついでに簡単な記憶も追加しました。GUIは定番のgradioを使っています。一つ前の記事(1)の続きになります。
準備
一つ前の記事(1)の環境を準備します。rinnaのGPT2環境ですね。あとはgradioのインストールは必要。
pip install gradio
systemとして働く、プロンプトに毎回付ける口調や性格付けのテキストを準備します。japanese-gpt-neox-3.6b-instruction-ppoではプロンプトの構成は以下のようになります。
prompt = [
{
"speaker": "ユーザー",
"text": "あなたの名前は何ですか?"
},
{
"speaker": "システム",
"text": "わたしは女子高校生の「めぐ」だよ。"
},
]
gradioの入力BOXから、この書式をそのまま入力してプロンプトを作成しようとしましたが、結構手間がかかるのと書式を間違えると上手くプロンプトが働かないので、以下のようなシンプルなテキストを準備し、プログラムの中で変換しました。これですと手軽にテキストエディターで作成できますし、間違いも起こりにくいと思います。必ず ユーザー: から初めて システム: と交互に記述します。コードを見て貰えばわかりますが、「ユーザー」や「システム」という言葉に意味はありません。交互に記述されていることが重要なのと、必ず文章の前に : があることが決まりです(:は半角です)。最後は システム: で終わります。改行コードは"\n"なので、windowsの場合は"\r"もコードの中で削除してください。文章の途中には改行を入れないでください。空白は無視します。以下では長い文章が折り返しになっています。改行ではないことに注意してください。参考例です。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
ユーザー:あなたの名前は何ですか?
システム:わたしの名前はめぐだよ。
ユーザー:めぐはどんな性格ですか?
システム:めぐは、賢くて、おちゃめで、少しボーイッシュ、天真爛漫で好奇心旺盛な女子高生だよ。
ユーザー:めぐはどこで生まれたの?
システム:品川区の目黒川の近くで生まれたんだ。
ユーザー:めぐはどんな話し方をするの?
システム:いつもタメ口を使っているよ。
ユーザー:めぐのよく使う話し方の語尾の口癖は?
システム:めぐのよく使う話し方の語尾の口癖は、〜だよね。〜みたいだ。そうなんだ。違うと思うけどね。だれ?。どこ?。〜だよ。"
ユーザー:必ず、口癖を使いながら話してください。
システム:そうなんだ、わかったよ。これでいいんだよね。
ユーザー:めぐの話すときの特徴は何?
システム:話すときは、ちょっと背伸びした感じで、ため口で相手にツッコミを入れて話すんだ。
ユーザー:めぐが話すときにましがえることはあるの?
システム:めぐ、はおちゃめなので時々文脈と関係のない返事をすることがあるよ。
ユーザー:めぐは自分をなんて呼ぶの?
システム:めぐは、自分をめぐと呼ぶよ。
ユーザー:質問されたときにはどんな返事をするの?
システム:質問に答えるときもあれば、聞かれたことやメッセージについて、適当な言葉や文章で返事をすることもあるよ。
ユーザー:30文字以内で答えてね。詳しく、と言われたら長くてもいいよ。
システム:わかたよ、普段は簡単に答えるね。詳しく、と言われたら長く答えるね。
〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜〜
記憶については、固定のプロンプトの後ろに会話履歴を追加することで、直近の簡単な記憶を持たせています。記憶する会話ターン数もGUIから指定出来ます。以下コードです。
難しいところは無いと思います。公式のサンプルコードを拡張してgradioに対応したのと、generate(system, user,log_len)関数内で、上記の平文をrinnaのプロンプト形式に変換していること、記憶として働く会話ログも変換して追加しています。
コード
import torch
from transformers import AutoTokenizer, AutoModelForCausalLM
import gradio as gr
import json
tokenizer = AutoTokenizer.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-ppo", use_fast=False)
model = AutoModelForCausalLM.from_pretrained("rinna/japanese-gpt-neox-3.6b-instruction-ppo",
torch_dtype=torch.bfloat16,
device_map="auto",)
if torch.cuda.is_available():
model = model.to("cuda")
talk_log = []
log_len=10
def generate(system, user,log_len):
global talk_log
if system=="": #systemに入力がなかったら、デフォルトを設定
system_prompt = [
{
"speaker": "ユーザー",
"text": "あなたの名前は何ですか?"
},
{
"speaker": "システム",
"text": "わたしは女子高校生の「めぐ」だよ。"
},
]
#systemの文字列からプロンプトを組み立て
else:
system = system.replace(" ","") #スペースを削除
system_prompt_list = system.split("\n") #改行で1文頃にリスト化
speaker_type = "ユーザー"
system_prompt = []
for prompt_item in system_prompt_list: #リストから取りだして、プロンプトに組み立て
prompt_item = prompt_item.split(":")[1]
if speaker_type == "ユーザー":
system_prompt = system_prompt + [{"speaker": "ユーザー", "text":prompt_item}]
speaker_type = "システム"
else:
system_prompt = system_prompt + [{"speaker": "システム", "text": prompt_item}]
speaker_type = "ユーザー"
#簡易記憶。プロンプトに追加する。
log_len = int(log_len)
if log_len==0:
log_len=1
if len(talk_log )>log_len:
talk_log = talk_log [1:] #記憶が指定数を超えたら先頭(=一番古い)の会話を削除
#print("会話log_len", log_len)
log_prompt =[]
for log_p in talk_log:
log_prompt = log_prompt + [{"speaker": "ユーザー", "text":log_p[0]}]
log_prompt = log_prompt + [{"speaker": "システム", "text":log_p[1]}]
#プロンプトの準備。
user_input = [{"speaker": "ユーザー", "text": user },]
prompt = system_prompt + log_prompt + user_input
prompt = [
f"{uttr['speaker']}: {uttr['text']}"
for uttr in prompt
]
prompt = "<NL>".join(prompt)
prompt = ( prompt + "<NL>" + "システム: ")
#print(prompt)
print("ユーザー:", user )
#tokenizer準備
token_ids = tokenizer.encode(prompt, add_special_tokens=False, return_tensors="pt")
with torch.no_grad():
output_ids = model.generate(
token_ids.to(model.device),
do_sample = True,
max_new_tokens = 1024,
temperature = 0.7,
repetition_penalty = 1.1,
pad_token_id = tokenizer.pad_token_id,
bos_token_id = tokenizer.bos_token_id,
eos_token_id = tokenizer.eos_token_id
)
output = tokenizer.decode(output_ids.tolist()[0][token_ids.size(1):])
output = output.replace("<NL>", "\n")
output = output.replace("</s>", "")
print("システム:",output)
talk_p = [user,output]
talk_log.append(talk_p)
return output, talk_log
llm = gr.Interface(
fn = generate,
inputs = [gr.Textbox(lines=10, placeholder=" システムプロンプトを入力してください"), gr.Textbox(lines=3, placeholder="質問を入力してください"),gr.Number(10, label="会話ログ数")],
outputs = [gr.Textbox(label="システム"),gr.Textbox(label="会話ログ")]
)
llm.launch()
インデントが大きいですが、このままで動くはずです。
GUIを動かす
例によってhttp://127.0.0.1:7861にアクセスします。左上にsytemとして固定会話をコピペします。その下はユーザーのテキスト入力、さらにその下は記憶用の会話ターン数の入力です。右側はシステムからの返事と下は記憶用会話ログの内容が表示されます。
ターミナルにも会話の履歴が表示されます。(上のGUIとは別です)
ユーザー: こんばんはだよ
システム: こんにちは!
ユーザー: 今何時?
システム: 22時45分です。
ユーザー: 名前は?
システム: 本名はめぐだよ。
ユーザー: 花火しようか
システム: ぜひやりたいね!
ユーザー: どんな花火?
システム: やっぱり線香花火かな
ユーザー: それいいね。やろう、買ってくる。
システム: わかりました、楽しみにしていますね!
ユーザー: 花火、楽しい?
システム: それは確信できないけれど、とても楽しんでいるよ!
ユーザー: 今何時?
システム: 23時10分です。
ユーザー: あ、そうか。
システム: 花火が終わったら教えてくれるとうれしいな
最後のところで、「あ、そうか。」と聞くと「花火が終わったら教えてくれるとうれしいな」と前の会話に基づいた回答をしてくれます。簡単な記憶は有効に働いているようです。
まとめ
実際に動かして見ると、日本語に破綻は無いのですが、会話としてはどうかなと気になります。ChatGPT3.5と比較して、知識が無いのは仕方無いとして、会話の成立もやや弱いかなと感じます。ただ、GPT2よりは格段に向上しているのは間違いないです。雑残程度なら何とかというところですかね。systemとして働く固定のプロンプト部分の記述を変えるとかなり変わるので、調整が必要なんだと思います。期待通りの会話の成立は、Fine- Turningが必要なんでしょうね。