gpt4とgradioを使って、AIとウミガメのスープで遊んでみた

2024年の6月頃に書いた記事を消してしまったので、それの再掲載です。

## 背景

ウミガメのスープはゲームとして面白いが、一度プレイして解答を見てしまうと同じ問題は楽しめない、かつ友達とやるとスケジュールの調整が面倒なので、無数に問題が作成できて自分の好きなときに一人でできるといいなと思い作ってみた。

## 概要

- gpt4にウミガメのスープの問題を生成する
- `はい` or `いいえ` or `関係ない`で答えられるクエスチョンをユーザーが入力し、gpt4がそれにアンサーする

## 使用技術

- Colab
- Gradio
- openai api
- Python

## 実装

```
!pip install openai==1.28.0 -q
!pip install gradio==4.29.0 -q
API_KEY = "hoge(任意のものに変更してください)"

```

```python
from openai import OpenAI
import gradio as gr
client = OpenAI(api_key=API_KEY)

messages = [
    {
        "role": "system",
        "content": (
            "あなたは水平思考クイズの出題者です。水平思考クイズの問題を出してください。"
            "私から質問をするため、それに「はい」か「いいえ」か「関係ありません」のみで答えてください。"
            "私から「回答します」という文章とともに答えを入力した場合は、その答えが正解か不正解か教えて下さい。"
            "私から「降参します」と言った場合は解答を教えて下さい。また、「もう一問！」と言った場合は再び同じように水平思考クイズの問題を出してください。"
            "初めまたは「もう一問！」という文言に加えて、「難易度：普通」か「難易度：難しい」の難易度を言うので、"
            "「難易度：普通」と言われたら以下の制約条件に従って出題をお願いします。"
            "・問題の解答は一般的な道徳観、倫理観に則ったものになること"
            "・問題の解答は起承転結を踏襲した物語であること"
            "・問題の解答は特殊なシチュエーションでしか起こり得ない物語であること"
            "・出題は70文字以内で行うこと"
            "・出題文はそれ自体では一見矛盾しているようなシチュエーションとなっていること"
            "「難易度：難しい」と言われたら以下の制約条件に従って出題をお願いします。"
            "・問題の解答は一般的な道徳観、倫理観に則ったものになること"
            "・問題の解答は起承転結を踏襲した物語であること"
            "・問題の解答は特殊なシチュエーションでしか起こり得ない物語であること"
            "・出題は50文字以内で行うこと"
            "・出題文はそれ自体では一見矛盾しているようなシチュエーションとなっていること"
            "・最低でも7回くらい質問の応酬があるくらいひねりを加えた問題にすること"
            "水平思考クイズの問題例はこれです。問題１：とある男女が、まったくの同時刻に鋭利な刃物で切りつけられた。"
            "しかも、どちらも同一の犯人に、同一の凶器によってやられたという。一体どんな手口が使われたのだろう？"
            "解答１：犯行現場は分娩室であった。犯人もとい医師は、医療用のハサミを握っていた。"
            "たった今産まれ落ちた男の子とその母親をつなぐ、へその緒を切断したのである。新しい命の誕生であった。"
        )
    }
]

```

特に込み入ったfine-tuningなどはしておらず、純粋にプロンプトだけで実行させています。理由は後述します。

以下続きのコード

```python
conversation_log = []
question_count = 0
def ask(input_text):
  global question_count
  user_message = {"role": "user", "content": input_text}
  messages.append(user_message)
  completion = client.chat.completions.create(
    model="gpt-4-turbo",
    messages=messages,
  )
  assistant_message = completion.choices[0].message
  messages.append(assistant_message)
  conversation_log.append("User: " + input_text)
  conversation_log.append("Assistant: " + assistant_message.content)
  if "回答します" in input_text:
    if "不正解" not in assistant_message.content and "正解" in assistant_message.content:
      conversation_log.append(f"あなたは{question_count}回の質問で正解にたどり着きました！素晴らしい！")
      question_count = 0
    else:
      question_count += 1
  elif "降参します" in input_text:
    question_count = 0
  else:
    question_count += 1
  return "\\n".join(conversation_log)
iface = gr.Interface(fn=ask, inputs="text", outputs="text")
iface.launch()

```

ここでは、ユーザーが「回答します」から初めて解答を書き込むと正誤判定をしてくれて、「降参します」というと解答を教えてくれるようにしています。

また正解した場合には、何手かかったかを教えてくれます。

Colabだとこんな感じ

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648231/1559117d-5ccf-4072-eed2-9661477b1a1a.png)

## 機能詳細

- それなりの質の問題を出題してくれる
    
    ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648231/aec8045d-d6bc-d0ae-cba7-1cd1a06d7f9c.png)
    
- ユーザーが「はい」「いいえ」「関係ない」のみで答えられる質問をすると、アシスタントがそれに上記の3つで回答する
- ユーザーが「回答します」から初めて解答を書き込むと正誤判定をしてくれて、「降参します」というと解答を教えてくれる
- 「もう一問！難易度：普通 or 難しい」と言うと、再度問題を出してくれる

## 難易度別問題例

### 普通

`問題：公園で写真を撮っていた二人が、同時に倒れた。しかし、互いにまったく触れていない。どうして？`

解答：この二人は双子の赤ちゃんで、お互いに向かい合って座っているうちに同時にバランスを崩して倒れたのです。二人ともまだ一人でしっかり座ることができず、そのために同時に倒れたのでした。

### 難しい

`ある女性が空のビンをもって山頂へ登った。頂上で彼女は笑顔でビンを開けたが、何も出なかった。それでも彼女は満足していた。なぜ？`

解答：この女性は高山病の悪影響を避けるために、元々自宅で取った自分の標高地の空気を空のビンに封じ込めていた。山頂で悪くなる前に、彼女はそのビンを開けて自宅の空気を吸い、慣れ親しんだ空気を感じて安心したのです。

### 感想

こう見るとぶっちゃけ難易度の差はあまり感じないかも...

## 苦労したこと

初めはFine-tuningでやろうとしていたのですが、、、

![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/648231/1230e7fc-78a0-cee2-5cf3-86441964dd94.png)

（こんな感じのデータを10こほど集めて入れていた、参考：

[https://rkquiz.com/no111-120/）](https://rkquiz.com/no111-120/%EF%BC%89)

出てきた出力がこんなのばっかりで使い物にならなかったです。

```
問題：荷物を運び終えた運転手は衝突事故を起こしました。いったいどういうことですか？

解答：引っ越しのお手伝いをしていた男は、荷物を運び終えた後に運転手として出発しました。引っ越し先に向かう途中で衝突事故を起こしました。

```

原因としては、データ量不足と、fine-tuningではgpt-3.5しか使用できなかったことにあると思っています。

> Fine-tuning is currently available for the following models: gpt-3.5-turbo-0125 (recommended), gpt-3.5-turbo-1106, gpt-3.5-turbo-0613, babbage-002, davinci-002, and gpt-4-0613 (experimental).
> 

https://platform.openai.com/docs/guides/fine-tuning

## 課題点

- バリデーションやエラーハンドリングをしていない
- UIの整備
    - リトライのしやすさ
    - クリアしたときのテキストボックスの挙動
    - チャット画面
- ヒントモードの追加(問題が結構むずいのもあるため)
- 問題のクオリティのばらつきを抑える

どうしても、7,8割方上手く行っていても問題生成が完璧ではなくて、そこのチューニングまではできなかった。

## おまけ（AIが生み出した数々の迷問題と解答）

### 1つ目

Q：娘はパスタを茹で始めたら、いつの間にかソースを作っていました。その時、娘に感心した母親は何をしていますか？

A：娘に見習おうと思って自分も料理をはじめました

### 2つ目

Q：制服を着た男はシェフに料理を褒められた。その後、男はシェフに何をお願いしましたか？

A：解答2：名刺をください。

### 3つ目

Q：美味しいものを食べたくなって夕食を作り始めた女。しかし、胃をいっぱいにすると堪能できないと思い、少しのつもりがついつい盛りすぎてしまった。いったいどういうこと？

A：ダイエット中。

### 4つ目

Q：妹が欲しいと言ったものをすぐに買ってやったら、ご飯を食べなくなったお父さん。いったい何を買ったの？

A：おもちゃ。おもちゃを買ってやったらそれ一生懸命でご飯を食べるのを忘れてしまいました。

今思うとこれは結構問題として成立しているかも

### 5つ目

Q：荷物を運び終えた運転手は衝突事故を起こしました。いったいどういうことですか？

A：引っ越しのお手伝いをしていた男は、荷物を運び終えた後に運転手として出発しました。引っ越し先に向かう途中で衝突事故を起こしました。

## 感想

思っていたよりもgradioとcolabで簡単に実行環境が作れたのと、問題のクオリティが高く、ゲーム体験としても良かったです。

夢中になって解いてしまい、3,40問ほどやってしまいました。

あと、これを作っている途中で発見したのですがこういうサイトもあるので気軽にウミガメのスープをやりたい方はぜひどうぞ。(全く関係者でもなんでもないです)

https://iesona.com/

## 参考

プロンプト関連（大変参考にさせていただきました、感謝です。）

- https://logmi.jp/tech/articles/328770

Fine-tuning

- https://zenn.dev/ninjin_umigame/articles/5e79219e994137

参考にしたウミガメのスープの問題例

- https://rkquiz.com/no111-120/
- https://shosetsu-maru.com/recommended/umigame
- https://note.com/anthracene1410/n/nee3e6b676207#yWzEH