見出し画像

ChatGPT の embedding で自作カードゲームのアイデアを一緒に考えてもらった

はじめましての方ははじめまして、 Rapid Bout 開発者 創鍵(そうけん)です。

今回は、今話題の LLM (Large Language Models) である Chat-GPT の API を利用して、カードのアイデア出しのアシスタントをしてもらったので、そのレポートを書いていきます。


ChatGPT とは?

We’ve trained a model called ChatGPT which interacts in a conversational way. The dialogue format makes it possible for ChatGPT to answer followup questions, admit its mistakes, challenge incorrect premises, and reject inappropriate requests.

Introducing ChatGPT

ということで、大量のテキストデータを用いた学習によって会話ができるようになった AI です。chat.openai.com から会話を行うことができます。

もしかしたら、以下のようなやり取りのスクリーンショットを見たことはあるかもしれません。

ChatGPT に「Got any creative ideas for a 10 year old’s birthday?」と尋ねた

これだけでも、色々な質問に答えてくれるので便利なのですが、生憎私のゲームのルールは私しか知りません。
毎回チャットで教えてあげてもいいのですが、地味に手間がかかるため、 API を利用することで、事前知識を自動で与えるようにしました。

ChatGPT API

API を利用するには platform.openai.com からアカウントを登録し、支払い情報を登録したあとで API key を作成する必要があります。

API keys

ついでに Usage limits で支払い可能な金額をあらかじめ設定しておきましょう。これによって制限を超過した場合には API の利用が自動で止まるので、想定外の出費を抑えることができます。

Usage limits

それでは早速 API Reference を参考に実装を行なっていきます。
今回は Python を利用しました。

API key を openai.api_key にセットして、 role と content の組になった json を送ることによって、 ChatGPT と会話を行うことができます。

import openai

with open('secret.key', 'r') as f:
    openai.api_key = f.read()

messages = [{
    "role": "user", "content": "こんにちは"
}]

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages
)

print(
  response["choices"][0]["message"]["content"]
)

role には system, user, assistant の 3種類があり、以下のように使い分けます。 ChatGPT に過去の会話を記憶する能力はないため、複数回やり取りを行う場合は、前回の回答を assistant ロールで設定する必要があります。

  • system: モデルの動作ガイドを記述する

  • user: あなたの質問内容を記述する

  • assistant: 前回の会話がある場合、 ChatGPT の返答を記述する

早速質問してみよう!

では早速、 Rapid Bout の赤デッキとそのカードを考えてもらいましょう!

赤のデッキコンセプトとして、"燃えるような攻撃力"をテーマにしたデッキを考えてみました。
・火炎放射:1コストで相手に3ダメージを与えるインスタント呪文。
・炎の巻き舌:3コストで相手に4ダメージを与えるソーサリー呪文。
・燃える衝動:2コストでクリーチャーを+2/+0にするインスタント呪文。
・炎の調和:2コストで火属性のクリーチャーをとりこにする呪文。
燃えるような攻撃力を発揮する赤のクリーチャーを中心にデッキを組み、上記の呪文やエンチャントを採用します。例えば、火炎放射をドローして相手のクリーチャーを一掃したり、炎の調和で相手のクリーチャーを自分のものにしたりすることで、相手の陣営を攻め落とします。また、炎の巻き舌や燃える衝動を使って自分のクリーチャーを強化し、相手のライフを削ります。
このデッキは、攻撃的なプレイが好きな方にはオススメです。相手を徹底的に攻め落とし、その勢いで勝利を掴んでください。

?????

まるで Magic: the Gathering のようなカードを提案されました。それはそうです、 Rapid Bout のルールはチーム内 Notion にしか記載されていないため、 ChatGPT が考慮することは不可能です。

そこで、 Rapid Bout の知識をまとめて渡してあげることにしました。

messages = [
    {"role": "system", "content": "Rapid Bout という TCG が存在します"},
    {"role": "system", "content": "RPB とは Rapid Bout の公式略称です"},
    {"role": "system", "content": "RPB では、20枚 (同名4枚まで) のカードを用いて山札を構築します"},
    {"role": "system", "content": "RPB には赤・青・緑・黒の4色が存在し、各色にデザインのルールとイメージがあります"},
    {"role": "system", "content": "....."},
    {"role": "user", "content": "RPBの赤のデッキコンセプトを新しく考えてください。コンセプトには1枚以上のカードを例示し、その組み合わせを説明する必要があります。"}
]

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages
)

なんということでしょう!事前知識の量が多すぎて、利用できるトークンの量を超えてしまいました。
GPT-4 からは最大 32,768 token の利用が可能ですが、現在利用している GPT-3.5-turbo では 4,096 token までしか利用できません。

Models - OpenAI

openai.error.InvalidRequestError: This model's maximum context length is 4097 tokens. However, your messages resulted in 6785 tokens. Please reduce the length of the messages.

日本語の場合、1文字あたり約 1.1 token を消費します。
token には自分の質問や ChatGPT の回答も含まれるため、体感 2500~3500 字以上の事前知識は埋め込めないことになります。

embeddings

embeddings とは、テキストをベクトルに変換することで関連性を測定することができる機能です。僕も数学的な詳細は分からないのですが、複数の文章からより近いものを選び出すことができます。

openai.Embedding.create を利用することで、ある文章に対するベクトルの値が取得できるので、それを記録します。ベクトル記録用のデータベースやツールは存在しますが、今回は暫定的に JSON ファイルとしてローカルに保存しました。

import json
from typing import List, Tuple
import openai

with open('knowledge.json', 'r') as f:
    knowledge: List[str] = json.load(f)

embeddings = openai.Embedding.create(
  input=knowledge,
  model="text-embedding-ada-002"
)['data']

embedded: List[Tuple[str, List[float]]] = []

for i in range(0, len(knowledge)):
  embedded.append(
    (knowledge[i], embeddings[i]["embedding"])
  )

faiss というライブラリを利用することで、ベクトルの列とベクトルを比較できます。
質問が行われる度に、まず Embedding でベクトルを取得し、事前知識のうちベクトルが近いものを優先的に抜き出すことで、必要な事前知識を絞ることができます。

import faiss
d = 1536
index = faiss.IndexFlatL2(d)
index.add(np.array(
  [v for (k, v) in embedded], dtype=np.float32
))

k = 30
D,I = index.search("{質問をベクトルにしたやつ}", k)

messages = []

for i in I[0]:
  messages.append({
    "role": "system", "content": embedded[i][0]
  })

messages.append({
  "role": "user", "content": "{質問内容}"
})

これにより、より重要な事前知識を利用して質問することができるようになりました。

赤のデッキコンセプトとして、「連続攻撃」というものが考えられます。
例えば、「赤, 攻撃力: 2点, 条件: 前のターンに同名のカードをプレイしている, 効果: 相手は手札からカードを1枚山札の下に置く」といったカードを採用します。
これらのカードを組合わせることで、前ターンに同名のカードをプレイしている場合に攻撃が増強され、相手の手札数も減少させることが可能となります。また、複数枚の同名のカードを揃えることで、攻撃力がより高くなるため、相手を一気に追い詰めることもできるでしょう。

4,096 token を超える事前知識を与えることはできないため、デザインの制約を無視していたり、カードの要素が多い/少ないといった問題はありますが、最初よりは十分アシスタントとして使えるレベルになりました。

おわりに

最近はこれを利用してアイデア出しの壁打ちをすることで、試作カードの量産に成功しました。 ChatGPT の提案をそのまま受け入れるのではなく、バランスを手直ししたり、着想だけをもらって自分で考えることによって品質を損なうことなくデザインを持続させられるのは強みだなと思いました。

今後は Fine-tuning を試したり、 GPT-4 API waitlist が更新されるのを待ちながら AI と共存したゲームデザインをしていきたいと考えています。

実際に利用したコード

import json
from typing import Dict, List, Tuple

import faiss
import numpy as np
import openai
import tiktoken as tiktoken

encoding = tiktoken.get_encoding("cl100k_base")

with open('../secret.key', 'r') as f:
    openai.api_key = f.read()

with open('../resource/embedded.json') as f:
    embedded: List[Tuple[str, List[float]]] = json.load(f)

d = 1536
index = faiss.IndexFlatL2(d)

embeddings = np.array([v for (k, v) in embedded], dtype=np.float32)

index.add(embeddings)

query = "RPBの赤のデッキコンセプトを新しく考えてください。コンセプトには1枚以上のカードを例示し、その組み合わせを説明する必要があります。"
query_embedding = np.array([openai.Embedding.create(input=[query], model="text-embedding-ada-002")['data'][0]["embedding"]], dtype=np.float32)

D, I = index.search(query_embedding, 100)

messages = [{"role": "system", "content": "あなたは Rapid Bout という TCG のカードデザイナーです。"}]

tokens = 0
index = 0

for i in I[0]:
    token = len(encoding.encode(embedded[i][0]))
    tokens += token
    print(f"context[{index}]: {embedded[i][0]}, token: {token}")
    messages.append({"role": "system", "content": embedded[i][0]})
    index += 1
    if tokens > 3000: print(f"tokens: {tokens}"); break

print(f"question: {query}, token: {len(encoding.encode(query))}")
messages.append({"role": "user", "content": query})

response = openai.ChatCompletion.create(
    model="gpt-3.5-turbo",
    messages=messages
)
print(f'response: {response["choices"][0]["message"]["content"]}')
import json
from typing import List, Tuple

import openai

with open('../resource/knowledge.json', 'r') as f:
    knowledge: List[str] = json.load(f)

embeddings = openai.Embedding.create(input = knowledge, model="text-embedding-ada-002")['data']

embedded: List[Tuple[str, List[float]]] = []

for i in range(0, len(knowledge)):
    embedded.append((knowledge[i], embeddings[i]["embedding"]))

with open('../resource/embedded.json','w') as f:
    f.write(json.dumps(embedded))
[
  "Rapid Bout という TCG が存在します",
  "RPB とは Rapid Bout の公式略称です",
  "RPB では、20枚 (同名4枚まで) のカードを用いて山札を構築します",
  "RPB には赤・青・緑・黒の4色が存在し、各色にデザインのルールとイメージがあります",
  "RPB カードのデザインには制約があります。カードの効果はプレイした時に発動する効果でなくてはいけません。そのため、ターン終了時や相手のターン開始時、他のカードをプレイした時のようなテキストは利用できません",
  "RPB カードのデザインには制約があります。あるカードの攻撃力を増加・減少させたり (+/-) 、効果によって (仮想的な) 状態を付与してはいけません",
  "RPB カードのデザインには制約があります。相手のカードを自分のカードのように使ったり、自分の場や手札に加えることもできません",
  "「青, 効果: 山札の上のカード2枚を手札に加える」",
  "「青, 効果: カードを3枚引く。その後、カードを2枚、山札の下に置く」",
  "「青, 攻撃力: 4点」",
  "「青, 攻撃力: 5点, コスト: 手札のカード1枚を山札の下に戻す」",
  "......"
]


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