研究室の先生からChatGPTのAPIをもらったので、試しにChatGPTとしりとりができるコードを作ってみた。
ChatGPTの現状
・・・全然ダメやん!
こんな感じで、しりとりのルールを教えても全然うまくいかないのです。
そんなChatGPTのAPIを使って、きちんとしりとりができるようなものを作ってみた、というものです。
実行結果
とりあえず最終的にはこんなものができましたよ、というものです。
・名詞以外が来たらダメです
・一度ゲームに出てきた単語もダメです
・最後が「ん」ももちろんダメです
・「ヨーヨー」のように、最後に「ー」が付いてても対応しています。
また、例えば「最初」→「予防接種」というように、最後が「ょ」の場合は「よ」に変換するようにしています。
使ったライブラリ
上から、Juman++という形態素解析を行うライブラリ、ChatGPTのAPIを使うためにopenAIのインストール、jaconvというひらがなとカタカタの相互変換を行ってくれるやつです。
from pyknp import Juman
import openai
import jaconv
方針
①ユーザーが言葉を入力します。
②その言葉がしりとりのルールを満たしているかどうかをチェック→満たしていなければ負けと出力しゲーム終了。満たしていればChatGPTの回答に移る。
③ ChatGPTへのプロンプトとしては、「ユーザーが入力した言葉の最後の文字から始まる言葉を一つ言ってください」だけ。重要なのはこの後で、これだけではChatGPTは平気で全然関係ない言葉を言うので、しりとりのルールを満たす言葉を言うまでwhile文を回して聞き続ける。(だからChatGPTが負ける可能性がない)
④条件を満たす言葉を言ったら、それをChatGPTの回答とする
⑤以上の操作を繰り返す
と言った感じです。しりとりのルールを満たしているかどうかの判定が作る上で一番めんどかった。(名詞ではない、最後の文字からは始まってない、最後が「ん」で終わらない、同じ言葉を2回言う...など)
コードについて
クラスとインスタンス変数
word_listは、ゲームの実行中に出てきた言葉を管理するリスト(重複なし)です。これから書く関数は全てShiritoriGameのクラス下にあります。
class ShiritoriGame:
def __init__(self):
self.N = 100
self.word_list = set()
self.juman = Juman()
self.last_word = ""
self.openai_organization = "org-xxx"
self.openai_api_key = "sk-xxx"
APIの利用
これでChatGPTのAPIを使ってChatGPTを呼び出しています。
def ask_ChatGPT(self, message):
openai.organization = self.openai_organization
openai.api_key = self.openai_api_key
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "user",
"content": message,
}
],
max_tokens=1024,
n=1,
stop=None,
temperature=0.5,
)
response = completion.choices[0].message.content
return response
しりとりの条件を満たしているかを判定する
最後の文字が「ん」でないかを判別
def check_last(self, word):
if word == "":
return False
result = self.juman.analysis(word)
mrph_list = result.mrph_list()
mrph = mrph_list[-1]
s=jaconv.kata2hira(mrph.yomi)
if s[-1] == "ん":
return False
return True
名詞かどうかを判別
def check_hinsi(self, word):
result = self.juman.analysis(word)
mrph_list = result.mrph_list()
if not mrph_list:
return False
mrph = mrph_list[-1]
if mrph.hinsi == "名詞":
return True
return False
小さいひらがなかカタカナ(「ぁ」や「ョ」など)であることを判別
def check_small(self,character):
small_list=["ぁ","ぃ","ぅ","ぇ","ぉ","ゃ","ゅ","ょ","ァ","ィ","ゥ","ェ","ォ","ャ","ュ","ョ"]
if character in small_list:
return True
return False
小さいひらがな、カタカナを、大きいひらがな、カタカナに直す
def change_small(self,s):
if s=="ぁ" or s=="ァ":
return "あ"
elif s=="ぃ" or s=="ィ":
return "い"
elif s=="ぅ" or s=="ゥ":
return "う"
elif s=="ぇ" or s=="ェ":
return "え"
elif s=="ぉ" or s=="ォ":
return "お"
elif s=="ゃ" or s=="ャ":
return "や"
elif s=="ゅ" or s=="ュ":
return "ゆ"
elif s=="ょ" or s=="ョ":
return "よ"
return s
単語の最後の文字を返します。次のターンではこの文字から始めないと負けになります。
def get_lastletter(self, current_word):
result = self.juman.analysis(current_word)
mrph_list = result.mrph_list()
mrph = mrph_list[-1]
if mrph.yomi[-1]=="ー":
return mrph.yomi[-2:-1]
return mrph.yomi[-1]
これらの関数は、以下の「最後の文字からスタートしているか」(りんご→コアラはNG)のチェックの際の補足として使っています。
具体的には、例えば
「シーシャ」だと「ャ」では始まれないので「や」からスタートする言葉を探しています。
また、「ヨーヨー」といった、最後に伸ばし棒がある単語はその一個前の「よ」からスタートするようにしています。
def check_initial(self, last_word, current_word):
if last_word == "":
return True
if current_word == "":
return False
result1 = self.juman.analysis(current_word)
mrph_list1 = result1.mrph_list()
mrph1 = mrph_list1[0]
result2 = self.juman.analysis(last_word)
mrph_list2 = result2.mrph_list()
mrph2 = mrph_list2[-1]
m1_new = jaconv.kata2hira(mrph1.yomi)
m2_new = jaconv.kata2hira(mrph2.yomi)
if m2_new[-1]=="ー":
last=m2_new[-2:-1]
if self.check_small(last):
last_new=self.change_small(last)
if m1_new[0] != last_new:
return False
return True
if m1_new[0] != last:
return False
else:
return True
if self.check_small(m2_new[-1])==False:
if m1_new[0] != m2_new[-1]:
return False
return True
if self.check_small(m2_new[-1]):
yomi_new=self.change_small(m2_new[-1])
if m1_new[0] != yomi_new:
return False
return True
上記の判定関数を組み合わせて、しりとりのルールをその単語が満たしているかどうかの結果を返します。満たしていなかったらその理由を出力するようにしています。
def win_or_lose(self, last_word, current_word):
if (
current_word not in self.word_list
and self.check_initial(last_word, current_word)
and self.check_last(current_word)
and self.check_hinsi(current_word)
):
return True
else:
if current_word in self.word_list:
print("既出です")
if self.check_initial(last_word, current_word) == False:
print("最後の文字から始まっていません")
if self.check_last(current_word) == False:
print("「ん」で終わっています")
if self.check_hinsi(current_word) == False:
print("名詞ではありません")
return False
あとは、ゲームを始めるだけです。
def play(self):
n = 0
current_word = ""
self.last_word = ""
last_letter = None
user = input("名前を教えてください: ")
while n < self.N:
word = input("単語を入力してください: ")
current_word = word
if self.win_or_lose(self.last_word, current_word) == False:
print(f"{user}の負けです")
exit()
self.word_list.add(current_word)
last_letter = self.get_lastletter(current_word)
if self.check_small(last_letter):
last_letter = self.change_small(last_letter)
self.last_word = current_word
current_word = ""
while self.win_or_lose_for_chatgpt(self.last_word, current_word) == False:
print(f"「{last_letter}」から始まる単語を探しています...")
command = f"{last_letter}から始まる名詞を一つ言ってください"
current_word = self.ask_ChatGPT(command)
print("ChatGPT:", current_word)
if self.win_or_lose(self.last_word, current_word) == False:
print("Chatgptの負けです")
exit()
self.last_word = current_word
self.word_list.add(current_word)
n += 1
最後に、コード全体を置いておきます。
from pyknp import Juman
import openai
import jaconv
class ShiritoriGame:
def __init__(self):
self.N = 100
self.word_list = set()
self.juman = Juman()
self.last_word = ""
self.openai_organization = "org-xxx"
self.openai_api_key = "sk-xxx"
def ask_ChatGPT(self, message):
openai.organization = self.openai_organization
openai.api_key = self.openai_api_key
completion = openai.ChatCompletion.create(
model="gpt-3.5-turbo",
messages=[
{
"role": "user",
"content": message,
}
],
max_tokens=1024,
n=1,
stop=None,
temperature=0.5,
)
response = completion.choices[0].message.content
return response
def check_last(self, word):
if word == "":
return False
result = self.juman.analysis(word)
mrph_list = result.mrph_list()
mrph = mrph_list[-1]
s=jaconv.kata2hira(mrph.yomi)
if s[-1] == "ん":
return False
return True
def check_hinsi(self, word):
result = self.juman.analysis(word)
mrph_list = result.mrph_list()
if not mrph_list:
return False
mrph = mrph_list[-1]
if mrph.hinsi == "名詞":
return True
return False
def check_small(self,character):
small_list=["ぁ","ぃ","ぅ","ぇ","ぉ","ゃ","ゅ","ょ","ァ","ィ","ゥ","ェ","ォ","ャ","ュ","ョ"]
if character in small_list:
return True
return False
def change_small(self,s):
if s=="ぁ" or s=="ァ":
return "あ"
elif s=="ぃ" or s=="ィ":
return "い"
elif s=="ぅ" or s=="ゥ":
return "う"
elif s=="ぇ" or s=="ェ":
return "え"
elif s=="ぉ" or s=="ォ":
return "お"
elif s=="ゃ" or s=="ャ":
return "や"
elif s=="ゅ" or s=="ュ":
return "ゆ"
elif s=="ょ" or s=="ョ":
return "よ"
return s
def check_initial(self, last_word, current_word):
if last_word == "":
return True
if current_word == "":
return False
result1 = self.juman.analysis(current_word)
mrph_list1 = result1.mrph_list()
mrph1 = mrph_list1[0]
result2 = self.juman.analysis(last_word)
mrph_list2 = result2.mrph_list()
mrph2 = mrph_list2[-1]
m1_new = jaconv.kata2hira(mrph1.yomi)
m2_new = jaconv.kata2hira(mrph2.yomi)
if m2_new[-1]=="ー":
last=m2_new[-2:-1]
if self.check_small(last):
last_new=self.change_small(last)
if m1_new[0] != last_new:
return False
return True
if m1_new[0] != last:
return False
else:
return True
if self.check_small(m2_new[-1])==False:
if m1_new[0] != m2_new[-1]:
return False
return True
if self.check_small(m2_new[-1]):
yomi_new=self.change_small(m2_new[-1])
if m1_new[0] != yomi_new:
return False
return True
def win_or_lose(self, last_word, current_word):
if (
current_word not in self.word_list
and self.check_initial(last_word, current_word)
and self.check_last(current_word)
and self.check_hinsi(current_word)
):
return True
else:
if current_word in self.word_list:
print("既出です")
if self.check_initial(last_word, current_word) == False:
print("最後の文字から始まっていません")
if self.check_last(current_word) == False:
print("「ん」で終わっています")
if self.check_hinsi(current_word) == False:
print("名詞ではありません")
return False
def win_or_lose_for_chatgpt(self, last_word, current_word):
if (
current_word not in self.word_list
and self.check_initial(last_word, current_word)
and self.check_last(current_word)
and self.check_hinsi(current_word)
):
return True
return False
def get_lastletter(self, current_word):
result = self.juman.analysis(current_word)
mrph_list = result.mrph_list()
mrph = mrph_list[-1]
if mrph.yomi[-1]=="ー":
return mrph.yomi[-2:-1]
return mrph.yomi[-1]
def play(self):
n = 0
current_word = ""
self.last_word = ""
last_letter = None
user = input("名前を教えてください: ")
while n < self.N:
word = input("単語を入力してください: ")
current_word = word
if self.win_or_lose(self.last_word, current_word) == False:
print(f"{user}の負けです")
exit()
self.word_list.add(current_word)
last_letter = self.get_lastletter(current_word)
if self.check_small(last_letter):
last_letter=self.change_small(last_letter)
self.last_word = current_word
current_word = ""
while self.win_or_lose_for_chatgpt(self.last_word, current_word) == False:
print(f"「{last_letter}」から始まる単語を探しています...")
command = f"{last_letter}から始まる名詞を一つ言ってください"
current_word = self.ask_ChatGPT(command)
print("ChatGPT:", current_word)
if self.win_or_lose(self.last_word, current_word) == False:
print("Chatgptの負けです")
exit()
self.last_word = current_word
self.word_list.add(current_word)
n += 1
game = ShiritoriGame()
game.play()
感想
細かい判定がめんどすぎた。
コードは改善点だらけだと思いますが読んでくださった方ありがとうございました。駄文失礼しました。