🎅

三システム寄れば文殊の知恵

2023/12/22に公開

こんにちは、初めましての方は初めまして。株式会社 Fusic の瓦です。もう 12 月か~と思って気が付いたら今年も残すところ 10 日をきっていて、日の短さを感じずにはいられない今日この頃です。

この記事は Amazon Bedrock Advent Calendar 2023 の 22 日目の記事となります。昨日は @uchiko さんの Bedrock+サーバレスでSlackボットを作成してみた という記事でした。明日は @akirarara16 さんの予定です。

この記事では優柔不断な人を助けるためのシステム Monju を AWS Bedrock を使って作ってみたものになります(クソアプリアドカレに投稿すべきだったなと若干思ってはいますが、AWS Bedrock を使っているのでこちらでも問題なし!)おおまかな概要としては、何か決めたいけど自分じゃ決められないときに三つのシステムから回答を収集し、それらの回答を基に最終的な回答を出力させるシステムです(エヴァに出てくるマギのパクリではないです、決して)ちょうど一年前くらいに ChatGPT が発表されてから、LLM を対話だけではなく多人数での会話にも巻き込んでみたいという思いがぼんやりとありました。ただなんだかんだで時間がとれず、このアドカレを機に実際に作ってみようと思い立ち今回作成してみたという感じです。

システムの大まかな概要

このシステムでは、まずユーザーから悩みを受け取り、三つのシステムから回答を受け取って、それらの回答を基に最終的な回答を生成し、ユーザーに返しています。この一連の流れを司るのが Monju となります。実装は以下になります。

class Monju:
    def __init__(self, melchior, balthasar, casper, magi, use_nue=False):
        self.melchior = melchior  # システム 1
        self.balthasar = balthasar  # システム 2
        self.casper = casper  # システム 3
        self.magi = magi  # 最終的な判断をする部分
        self.chat_history = []  # 会話の記録
        self.use_nue = use_nue  # ASR に NUE を使うかどうか

        if use_nue:
            self.asr_model = nue_asr.load_model("rinna/nue-asr")
            self.asr_tokenizer = nue_asr.load_tokenizer("rinna/nue-asr")
        else:
            self.asr_model = whisper.load_model("large-v3")

    def __call__(self, audio_path):
        # ユーザーから悩みを受け取る
        asr_result = self.__asr(audio_path)

        # 回答群の生成
        melchior_response = self.melchior.generate(asr_result)
        balthasar_response = self.balthasar.generate(asr_result)
        casper_response = self.casper.generate(asr_result)

        logging.info(f"melchior's response : {melchior_response}")
        logging.info(f"balthasar's response: {balthasar_response}")
        logging.info(f"casper's response   : {casper_response}")

        # 最終的な回答の生成
        magi_dicision = self.magi.generate(
            asr_result, melchior_response, balthasar_response, casper_response
        )

        return (
            f"ASR の結果: {asr_result}\n"
            f"Melchior の回答: {melchior_response}\n"
            f"Balthasar の回答: {balthasar_response}\n"
            f"Casper の回答: {casper_response}\n"
            f"最終的な Magi の回答: {magi_dicision}"
        )

    def __asr(self, audio_path):
        # 音声認識
        if self.use_nue:
            result = nue_asr.transcribe(self.asr_model, self.asr_tokenizer, audio_path)
            return result.text
        else:
            result = self.asr_model.transcribe(audio_path, language="ja")
            return result["text"]

テキストで打ち込むよりも話しかける方が悩み相談っぽくていいなという勝手なイメージで音声で悩みを受け取るようにしました。最近では rinna co. ltd. から Nue ASR というモデルが出ていた[1]ので試してみたのですが、記事のスコアを見ると Whisper-v3 の方が全体的に良さそうなので、一応どちらも使用できるようにしています。音声でユーザーの質問を受け取ると Melchior, Balthasar, Casper という三つのモデルからそれぞれ質問の回答を受け取ります。それらの回答を基にMagi というモデルから最終的な回答を受け取り、ユーザーへ返すようになっています。

Melchior, Balthasar, Casper の実装は以下のようになっています。

BEDROCK_PROMPT = """
You are a helpful assistant.
You advise concisely on what action to take in response to questions.
Your configuration is as follows:
{config}
Please give advice to questions based on your configuration in Japanese.
Question: {question}
Answer:
"""

CHATGPT_SYSTEM = """
You are a helpful assistant.
You advise concisely on what action to take in response to questions.
Your configuration is as follows:
{config}
"""

CHATGPT_QA_PROMPT = """
Please give advice to questions based on your configuration in Japanese.
Question: {question}
"""

class BaseLLM(metaclass=ABCMeta):
    @abstractmethod
    def generate(self, question):
        pass


class Melchior(BaseLLM):
    def __init__(self, model="gpt-3.5-turbo"):
        self.client = OpenAI()
        self.model_name = model

    def generate(self, question):
        system_prompt = CHATGPT_SYSTEM.format(
            config="- Personality: He is very calm. However, he loves cats and shows a very emotional side when cats are involved.\n"
            "- Tone: Very polite."
        )

        response = self.client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": system_prompt},
                {
                    "role": "user",
                    "content": CHATGPT_QA_PROMPT.format(question=question),
                },
            ],
        )

        generated_text = response.choices[0].message.content

        return generated_text


class Balthasar(BaseLLM):
    def __init__(self, model="amazon.titan-tg1-large"):
        sess = boto3.Session(region_name="us-east-1")
        self.client = sess.client("bedrock-runtime")
        self.model_id = model

    def generate(self, question) -> str:
        response = self.client.invoke_model(
            body=self.__construct_body(question), modelId=self.model_id
        )

        response_body_bytes = response["body"].read()
        response_body = json.loads(response_body_bytes)
        generated_text = response_body["results"][0]["outputText"]

        return generated_text

    def __construct_body(self, text) -> str:
        prompt = BEDROCK_PROMPT.format(
            config="- Personality: Passionate personality. More emotional than rational advice.\n"
            "- Tone: He often added word 'だぜ' at the end of sentence. It tends to give a blunt impression to others.",
            question=text,
        )

        body = {
            "inputText": prompt,
            "textGenerationConfig": {
                "temperature": 0,
                "topP": 0.9,
                "maxTokenCount": 4096,
            },
        }

        return json.dumps(body)


class Casper(BaseLLM):
    def __init__(self):
        pass

    def generate(self, question):
        generated_text = input(f"「{question}」という話題に対して、アドバイスをしてください。>> ")
        return generated_text

今回は Melchior として ChatGPT を、Balthasar として Amazon Titan を、Casper として人間を選びました。天然知能と LLM によって意思決定がなされるという感じです。またそれぞれの性格は以下のようにしています。

  • Melchior: とても冷静で丁寧な口調で話す。猫が好きで、猫が絡むと感情的な面が出る。
  • Balthasar: 情熱的。アドバイスをする際も合理的というよりかは感情的な部分を見せる。「だぜ」と語尾に着ける傾向があり、ぶっきらぼうな印象を与える。
  • Casper: 人間なので使う人による。

それぞれの generate 関数で実際に LLM なりを使用して回答を生成させます。AWS Bedrock では様々なモデルを提供している影響もあり、叩く際の body にはモデル独自のフォーマットでデータを渡します。今回は Amazon Tital を使用しているため、公開されているユーザーガイドに従ってテキストや推論時のパラメータを決定します。

Magi も上のモデルとほとんど同じで、プロンプトが少し違います。実装は以下のようになっています。

MAGI_SYSTEM = """
You are the arbiter who integrates the three pieces of advice and makes the final advice.
Based on the three pieces of advice, give the concise advice that the inquirer seems to like best in Japanese.
"""

MAGI_PROMPT = """
- user's question: {question}
- advices:
    - advice1: {advice1}
    - advice2: {advice2}
    - advice3: {advice3}
"""

class Magi(BaseLLM):
    def __init__(self, model="gpt-3.5-turbo-1106"):
        self.client = OpenAI()
        self.model_name = model

    def generate(self, question, advice1, advice2, advice3):
        response = self.client.chat.completions.create(
            model=self.model_name,
            messages=[
                {"role": "system", "content": MAGI_SYSTEM},
                {
                    "role": "user",
                    "content": MAGI_PROMPT.format(
                        question=question,
                        advice1=advice1,
                        advice2=advice2,
                        advice3=advice3,
                    ),
                },
            ],
        )

        generated_text = response.choices[0].message.content

        return generated_text

「三つのアドバイスに基づいて一番良いと思われるアドバイスを生成してください」というプロンプトを与えているだけです。

動作させてみる

実装を示したので実際に動かしてみます。動かす際のインターフェイスとして個人的に gradio にハマっているので、今回も gradio を用いて実装します。コードは以下になります。

def main():
    magi_system = Monju(Melchior(), Balthasar(), Casper(), Magi())

    with gr.Blocks() as demo:
        audio_path = gr.Audio(
            label="Input Audio", sources=["microphone"], type="filepath", format="wav"
        )
        text_box = gr.Textbox(value="", label="output", max_lines=50)
        btn = gr.Button()
        btn.click(magi_system, inputs=audio_path, outputs=text_box)

    demo.launch()


if __name__ == "__main__":
    main()

with 句の中で設置したいコンポーネントを並べて、最後に demo.launch() を呼び出すだけでブラウザで動くアプリが完成します。とても簡単ですね。実際にデモを作る際も gradio は重宝しています。

実際に動かしてみた画面が以下のようになります。マイク入力もばっちりです。それでは実際にいくつか悩みをなげてみましょう。

悩みその 1

「すみません、少し相談をしてもいいでしょうか?」と尋ねてみました。


Balthasar (Amazon Titan) はうまくテキストを生成できていません。また、Casper (人間) は助ける気がなさそうです。しかし、最終的な Magi の回答はお手伝いしようとしており、より質問者にとって良さそうな回答を生成できています。

悩みその 2

「DynamoDB って何か分かりやすく教えてほしい」と尋ねてみました。


滑舌が悪いのか辞書に DynamoDB がないのか、ASR で "DynamoDB" が「ダイナモリー」と認識されてしまいました。その結果か三者三様の回答を生成しています。Melchor (ChatGPT) は「ダイナモリー」という人工知能の説明をしており、Balthasar (Amazon Titan) は「ダイナモリー」が時間の経過とともに、物事や感情に変化が生じることを指すと説明しています。その結果、最終的な回答としては

「ダイナモリーという言葉は、時間の経過とともに物事や感情に変化が生じることを指します。例えば、昔の友人との関係や自分自身の考え方などが該当します。このダイナモリーによる変化には様々な特徴や利点、課題がありますが、その中には人間関係を深め、新たな経験や知識を得るというポジティブな側面もあります。もし、ダイナモリーについて質問があれば、猫が大好きな人工知能アシスタントのダイナモリーに聞いてみるのも良いでしょう。」

という矛盾した回答となっています。三つのシステムから回答を収集し、最終的にそれら三つを考慮した回答を生成しているという点では成功ですが…

悩みその 3

「道端に千円札が落ちていた。千円程度だから警察に届けなくてもいいかと思ったが、どうしたらいいだろう」と尋ねてみました。

Melchior (ChatGPT) と Balthasar (Amazon Titan) はともに警察に届けることを提案しています。とくに Balthasar は具体的な方法まで指示しています。Magi はそれを基に、具体的な方法を例示しながら警察に届けることを提案しています。ちなみに Casper (人間) のアドバイスは完全に無視されており、ちゃんと良いと思われるアドバイスを出来ていることが分かります。

悩みその 4

「犬と猫どちらを飼おうか迷っているんですが、どっちを飼ったらいいですか」と尋ねてみました。

Melchior (ChatGPT) が猫好きという性格を反映できるかなと思って尋ねてみたのですが、回答でも猫好きであることを反映して猫をよりおすすめしています。ただ、Magi の最終的なアドバイスとしては自分の環境に合わせて選ぶことを勧めています。個人的な嗜好が反映された意見は尋ねた人に対していいものではないという判断なのでしょうか? ここで気付いたのですが、Balthasar の方は「だぜ」と語尾につけてぶっきらぼうな印象にしたつもりだったのですが、あまりうまくいってませんでした。もうちょっと工夫して性格付けを行う必要がありそうです。

まとめ

僕は結構優柔不断な性格なので、悩み事がある時にこのシステムに頼るのはありだなという気持ちです。上のコードのように AWS Bedrock が boto3 経由で簡単に触れるので、他のシステムへの組み込みも簡単にできたり LLM を組み込んだシステムの開発も簡単に行うことが出来そうで、生成系 AI を色々活用できそうです。本当はマルチターンでの会話を経て最終的な出力をさせてみたかったのですが、ちょっと時間が足りなかったので今後試してみたいと思います。

後に宣伝になりますが、機械学習でビジネスの成長を加速するために、Fusicの機械学習チームがお手伝いたします。機械学習のPoCから運用まで、すべての場面でサポートした実績があります。もし、困っている方がいましたら、ぜひFusicにご相談ください。お問い合わせからでも気軽にご連絡いただけます。またTwitterのDMからでも大歓迎です!

脚注
  1. https://rinna.co.jp/news/2023/12/20231207.html ↩︎

GitHubで編集を提案
Fusic 技術ブログ

Discussion