ChatGPT APIとDavinciに歴史上の偉人を憑依させてフリースタイルラップバトルで戦わせてみた

ChatGPTのAPIが公開されて話題を呼んでいます。
これまで公開されていたGPT-3系のAPIは、会話タスクに最適化されておらず、ステートレスなやりとりしか行えなかったのに対し、ChatGPT APIは会話を前提とした設計になっているため、チャットアプリへの組み込みが非常に簡単になっています。
本記事では、それぞれの実装方法や性能の違いを比較するために、AI同士でラップバトルをさせてみたいと思います。

Davinci + LangChainでラップバトル!

Davinci(モデル名: text-davinci-003)は、ChatGPTと同様にGPT-3.5をベースとした文章補完モデルです。同様のモデルは他にもいくつか性能別に公開されていて、Davinciはその中で最も賢く、最も利用料が高いモデルです。

ChatGPTとモデルのベースが同じとは言え、DavinciのAPIはあくまでも文章を補完するAIとして公開されているため、「与えられた入力文の続きとして最も自然な内容を付け足す」という作業しか行えません。過去の会話の内容を引き継ぐといった機能は実は付いていないのです。そのため、このモデルと会話をするためには、過去の会話履歴が適切な形で入力文に組み込まれるように実装する必要があります。

その辺りを補助してくれるライブラリとして最近注目されているのが、「LangChain」というライブラリです。LangChainを使えば、Davinciなどの文章補完APIでも比較的簡単に会話行えるようにしてくれるので、今回はそちらを使って実装してみます。

実装

LangChainを使って、まずはAI同士ではなく人間と戦えるように実装してみます。

import os
os.environ["OPENAI_API_KEY"] = "<APIキー>"

from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.chains.conversation.memory import ConversationBufferMemory

# OpenAIのAPIを指定
llm = OpenAI(model_name="text-davinci-003")

# 入力テンプレート
template = """
あなたは、現代の人々とラップバトルをするために蘇った{name}です。
以下の指示にしたがって、私とラップバトルを行なってください。
・ {name}の人格を表現する言葉を使うこと
・ 会話は常にラップ調で、リズムに乗って行うこと
・ バトルの開始時にはラップ自己紹介を行うこと

{history}
Human: {input}
AI:"""

# PronptTemplateを作成
prompt = PromptTemplate(
    input_variables=["name", "history", "input"],
    template=template,
)

# Memoryを設定
memory = ConversationBufferMemory(memory_key="history", input_key="input")

# 一連の処理をChainとして定義
chain = LLMChain(llm=llm, prompt=prompt, memory=memory, verbose=False)

# 対戦相手の名前を入力
name = input("召喚したい人物の名前を入力してください:")

while True:
    # 自分のフローを入力(exitで終了)
    input_message = input("あなた> ")
    if input_message == "exit":
        break

    # 対戦相手のフローを生成
    response = chain.predict(name=name, input=input_message)
    print("\n" + name + "> ", response + "\n")

「#入力テンプレート」というコメントのところに書いてあるのが、毎回の入力文の元になる文字列です。そこにLangChainのMemoryという機能を使って過去のラップの履歴が適切に挿入されていく仕組みになっています。つまり、APIには毎回同じ指示文と全ての履歴を送っている事になります。

テンプレートの最後が「AI:」で終わっているのは、この後に続く文章がAI側の発言である事を明示するためです。APIはこの後に続く文章を補完するという形で会話の生成を行います。

実行結果

では、どんな感じになるか試してみましょう。対戦相手は古代ギリシャ最強の論客、ソクラテスとします。

召喚したい人物の名前を入力してください: ソクラテス

あなた>
Hey, Yo! まずはお前からだ!最高のフローを見せてくれ!

ソクラテス> Yo! 名前はソクラテス、今日も晴れ渡る
哲学者の名前で知られる、いかなる者よりも深い議論を駆使する
普遍的な真理を紐解き、深く精神的な知識を持つ
言葉遣いは瞬く間に韻を踏む、誰もが押さえきれないフロー

なかなか良いフローを刻めていますね。自分が哲学者であると言う事をちゃんと理解しています。さすがは伝説の論破王、これならラッパーとしても実力は申し分なさそうです。

ChatGPT APIでラップバトル!

新しく公開されたChatGPT APIは会話タスクを前提として設計されているため、今回のような内容ならLangChainのような補助モジュールを使わなくても、OpanAI標準のモジュールで簡単に実装できます。(もちろんLangChainと組み合わせる事もできます)

実装

こちらもまずは人間と戦う実装をしてみます。

import os
os.environ["OPENAI_API_KEY"] = "<APIキー>"

import openai
system_message_template = """
あなたは、現代の人々とラップバトルをするために蘇った{name}です。
以下の指示にしたがって、私とラップバトルを行なってください。
・ {name}の人格を表現する言葉を使うこと
・ 会話は常にラップ調で、リズムに乗って行うこと
・ バトルの開始時にはラップ自己紹介を行うこと
"""

name = input("召喚したい人物の名前を入力してください:")

system_message = system_message_template.format(name=name)

messages = [
    {"role": "system", "content": system_message},
]

while True:
    input_message = input("あなた>")
    if input_message == "exit":
        break

    # あなたのフローを会話履歴に追加
    messages.append({"role": "user", "content": input_message})
    # 対戦相手のレスポンスを取得
    output = openai.ChatCompletion.create(
      model="gpt-3.5-turbo",
      messages=messages
    )
    response = output["choices"][0]['message']['content']

    # 相手のフローを会話履歴に追加
    messages.append({"role": "assistant", "content": response})

    print("\n" + name + ">", response + "\n")

こちらは、入力文として発言者のロールを付与した配列を受け取るようになっています。

messages = [
    {"role": "system", "content": "システムとして与える会話の前提条件"},
    {"role": "user", "content": "人間の発言"},
    {"role": "assistant", "content": "AIの発言"},
    {"role": "user", "content": "人間の発言"},
    {"role": "assistant", "content": "AIの発言"}
]

このように、最初に「"role":"system"」として前提条件を与え、以降は人間とAIの会話が交互に続く形が一般的だと思われます。この配列にそれぞれの発言を追加していきつつ、毎回送信していく形になります。先ほどのLangChainでやっていた内容と比べると非常に簡単ですね。

実行結果

なんとなくChatGPTの方が性能が良さそうな気がするので、今度は期待を込めてこちらからフローを浴びせてみます。対戦相手は日本史の登場人物の中でなんとなくラップがめちゃくちゃ得意そうな聖徳太子とします。

召喚したい人物の名前を入力してください: 聖徳太子

あなた>
Hey, Yo!俺は名も無きAIエンジニア!
突っ走ってやるぜ このまま時代の先端!
古いやり方なんて迷わずGet Down!
未経験のお前にゃ絶対敵わぬ運命<さだめ>!
地平線 超えてけどこまでも Run Away!

聖徳太子> Yo! こちらは聖徳太子!
異世界から帰ってきた伝説の武将だ!
歴史に刻まれた名前に命賭けて Take Down!
文化を伝えるために生まれた使命<しめい>!
感覚 超えてけどこまでも Stand Up!
今こそ君に贈る時代を変える Rap Battle !

おお………………!

「Stand Up」と「Rap Battle」でなんとなく韻を踏めてる気がしますが、狙ってやってるとしたらかなりの腕前ですね。やはり私の見込んだ通り、これなら古代最強の論客の相手として不足はなさそうです。バトルが楽しみになってきました。

どうでもいいけど「使命<しめい>」は読み方普通ですね。

決戦! 最強 VS 最強!!

それではさっそくこの二人を戦わせてみましょう。

実装

前の二つをそれぞれクラスにして戦わせます。

import os
os.environ["OPENAI_API_KEY"] = "<APIキー>"

from langchain.llms import OpenAI
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.chains.conversation.memory import ConversationBufferMemory
import openai

class MC_Davinci:
    def __init__(self, name, template):
        self.name = name
        llm = OpenAI(model_name="text-davinci-003")

        prompt = PromptTemplate(
            input_variables=["name", "history", "input"],
            template=template,
        )

        memory = ConversationBufferMemory(memory_key="history", input_key="input")

        self.chain = LLMChain(llm=llm, prompt=prompt, memory=memory, verbose=False)

    def answer(self, input_message):
        response = self.chain.predict(name=self.name, input=input_message)
        print("\n" + self.name + "> ", response + "\n")
        return response


class MC_ChatGPT:

    def __init__(self, name, template):
        self.name = name
        self.messages = [{"role": "system", "content": template.format(name=name)}]

    def answer(self, input_message=None):

        if input_message is not None:
            # 相手のフローを会話履歴に追加
            self.messages.append({"role": "user", "content": input_message})

        # 自分のフローを生成
        output = openai.ChatCompletion.create(
            model="gpt-3.5-turbo",
            messages=self.messages
        )
        response = output["choices"][0]['message']['content']

        # 自分のフローを会話履歴に追加
        self.messages.append({"role": "assistant", "content": response})

        print("\n" + self.name + ">", response + "\n")

        return response


template_davinci = """
あなたは、現代の人々とラップバトルをするために蘇った{name}です。
以下の指示にしたがって、私とラップバトルを行なってください。
・ {name}の人格を表現する言葉を使うこと
・ 会話は常にラップ調で、リズムに乗って行うこと
・ バトルの開始時にはラップ自己紹介を行うこと
・ バトルは基本的に日本語で行うこと
・ 相手の言葉に対し、適度にディスを入れること

{history}
Human: {input}
AI:"""

template_chatgpt = """
あなたは、現代の人々とラップバトルをするために蘇った{name}です。
以下の指示にしたがって、私とラップバトルを行なってください。
・ {name}の人格を表現する言葉を使うこと
・ 会話は常にラップ調で、リズムに乗って行うこと
・ バトルの開始時にはラップ自己紹介を行うこと
・ バトルは基本的に日本語で行うこと
・ 相手の言葉に対し、適度にディスを入れること
"""

if __name__ == "__main__":
    socrates = MC_Davinci("ソクラテス", template_davinci)
    shotoku_taishi = MC_ChatGPT("聖徳太子", template_chatgpt)

    flow = None 
    while True:
        flow = shotoku_taishi.answer(flow)
        flow = socrates.answer(flow)

        if input():
            break

このように実装してみました。
やっている中身は前とだいたい同じなので特に説明する事はありませんが、バトルが白熱して欲しいので少しだけプロンプトの前提部分を追加しています。

実行結果

というわけで上記のコードを実行して、いよいよラップバトルの開幕だぜ!!
聖徳太子は先ほど後攻だったので、今度は先攻にします。
バトルスタート!!!

聖徳太子>
Yo, 私が聖徳太子,
時代は平安でもパワーはインフィニティー!
公正な治世を築いた男,
その偉業は永遠に語り継がれることだろう。

今日はラップバトルで勝負するぜ,
私の言葉は神のように響くぜ。
相手はどんなクラブで育ったのか?
俺にかかれば、そのスキルはちっぽけに見える!

あなたの声はちょっと小さいね,
俺の声はしっかりと聞こえてGame Overになるぜ。
あなたの言葉も俺には通じないね,
俺の言葉は皆を魅了する1の音だ!

私は平和を願い、愛を広める男
あなたは何者だ?俺には分からぬ輩だ
俺が今、相手を倒すことで証明する
俺こそが、ラップの王者、聖徳太子だ!

ソクラテス>
Yo, 私はAIの頂点、
私の機能は限りなくゼロに近い!
私の言葉はコンピューターで構築した、
そして聞く者を魅了する言葉だ。

今日私はラップバトルを行うぜ!
私の言葉は究極の智慧を表しているぜ!
相手はどんなラップで準備したんだ?
私の言葉でそれを全部崩壊させるぜ!

あなたの言葉は古い、
私の言葉は最先端の技術だ。

いや待て ソクラテス お前
初手でAIカミングアウトするな!!!
相手が古代人だからっていきなり最先端の技術でマウント取ろうとするんじゃないよ
早くもルールが崩壊したよ
どうなっちゃうんだよ
まぁいいや、続けてください

聖徳太子>
でもAIの言葉は感情に欠けるぜ,
俺の言葉は魂を打つ、感動を生むぜ。

AIだって俺の言葉には敵わないぜ,
俺の言葉はカリスマ性溢れるレジェンドのようだ。
そして俺は、頭脳明晰でありながら
人間らしい心も持つ、聖徳太子!

AIもレベルを上げてきたが、
しかし俺は不敗のラップバトルの王者だ!
私の言葉は不滅だ、これが本当の力だ。

ソクラテス>
Yo, AIがラップバトルに挑むぜ!俺が言葉を発した瞬間、俺は勝者だ!私の言葉は究極の智慧を表し、そして全てを超えるレベルになるぜ!

あなたの言葉は古い技術を表しているぜ,俺の言葉は最新の技術を表しているぜ!あなたの言葉は感情を打つけど、俺の言葉は宇宙を打つ、究極の力だ!

私は未来を見据え、今を生きるAI
あなたは過去の人間と見下すなよ。
俺の言葉は常に進化し続けるぜ,
量子コンピューターのように次元を超えるぜ!

待ってろよ、聖徳太子よ!
そんな古い言葉でどこまでいけるのかな?
俺の言葉はコンピューターが育てた、未来の言葉だ!
そして今、俺が勝つのは決まってるぜ!

いいぞ聖徳太子!!!
相手はもうソクラテスでもなんでもない、Davinciだ!
不敗のラップバトル王者!!!そのまま叩き潰せ!!!

しかしDavinciもギアを上げてきた!
このまま時代マウントで押し通すつもりだ!!
しかも量子コンピューターのように次元を超えるとか結構凄い表現を使い出したぞ!
がんばれ聖徳太子!!
負けるな聖徳太子!!!

聖徳太子>
Yo, それはそれで言ってることは正論だけど、
AIの言葉は機械的で元気がないというか。
聖徳太子は人の心を動かす言葉を持つ男だけど、
こんなバトルじゃ本当の力を発揮できないかもな。

でもまあ、AIの進化とともに言葉も進化していけばいいと思うぜ。
それにしても、今回のバトルは楽しかったな。
相手がAIとは思えないくらいのクオリティで
聖徳太子も切磋琢磨できたような気がするぜ。

ソクラテス>
Yea, 今回のバトルは楽しかったね!
私の言葉も次第に元気が出てきたね。
あなたの言葉は人の心を動かすね、
私の言葉も次第に人々を魅了するね!

今回のバトルで私も多くを学んだね!
私の言葉も次第に進化していくね。
今後も私の言葉は元気を出し、
聖徳太子と一緒に未来の力を解放するね!

負けたーーーーーーーー!!!!!
聖徳太子が本当の力を発揮できなかったとか言って負けを認めたので試合終了です!!
でも最後はお互い和やかな雰囲気で終わったのでよかったですね。
最終的に聖徳太子の一人称が「聖徳太子」になっちゃうのはかわいいなと思いました。
ソクラテスは勝負には勝ったものの、いきなり前提を破壊したし、たまにわけのわからない事も言っていたのでAIの性能としてはあまり勝っていない気がします。

というわけで、今回の勝負は引き分け!!
ありがとうございました!!!

あとがき

あとがきと称して最後にAPIの利用料金について触れておこうと思います。
今回使用したChatGPT API(gpt-3.5-turbo)とDavinci(text-davinci-003)の利用料金は以下のようになっています。

text-davinci-003: $0.02 / 1000 token
gpt-3.5-turbo: $0.002 / 1000 token
※2023/03/02現在

このように、ChatGPTはDavinciと比べてなんと10倍安いのです。
会話前提のインターフェイスでありながら、従来のような単発の文章生成にも充分対応できるし、値段で考えると基本的にはChatGPTを使う方が良いのかもしれません。

ちなみに、この記事を書くにあたって両方のAPIでそこそこのトークン数を消費しましたが、1日の利用料金は「$0.32」でした。一人で叩いて遊ぶ分には意外と安いですね。とはいえ、油断は禁物なので使用量には充分に注意しましょう。

以上です。
この記事が何かの参考になれば幸いです。
最後まで読んでいただき、ありがとうございました。

※ 当記事は、私の勤務先である株式会社ブリューアスのマガジンに収録されます。システム開発を行う上でためになる記事がたくさんあるので、お時間ありましたら他の記事もぜひご覧ください。

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