Chat GPT-APIを使った解説つき英単語データベース＋単語テスト

# 1. はじめに
### この記事の目的

本記事では、ChatGPTのAPIを使って英単語データベースを作成し、ここから英単語テストを自動生成していきます。

まず、英語の単語テストの出題や採点を自動化する上で、大きな障壁となるのは単語データベースの作成です。その理由は単純で

- 英単語の数は莫大であり、それらを難易度や目標ごとに分割し、単語と意味の組をデータにまとめるだけで大変な労力がかかる

ためです。したがって、意味以外の多くの考慮すべき、覚えるべきこと（類似語、対義語との差異や具体的な使い方、品詞の変換など）があるにも関わらず、それは残念ながら単語テストの自動化には組み込めませんでした。

そこで今回は、ChatGPTのAPIを用いて、意味以外の多くの情報を含んだ単語データベースを作り、それを用いて単語テストを作成したいと思います！

# 2. 単語データベースの生成

### ライブラリのインポート

まずはライブラリをインポートして、OpenAIのAPIキーを設定します。APIキーの取得の仕方は、[公式ページを参照](https://platform.openai.com/account/api-keys)してください。

```python
# ライブラリのインポート
import openai
import pandas as pd
import re

# OpenAI APIの設定
openai.api_key = "APIキーの設定"
```

### 単語の生成

次にgpt-4モデルを使って、TOEIC600〜700点レベルの単語を30個生成していきます。ChataGPTのAPIの使い方の詳細については私の過去の記事[ChatGPTのAPIの使い方 - Qiita](https://qiita.com/rio_0402/items/65cb7261f85099ec942a)をご覧ください。

また、出力は一つの文字列として生成されるので、正規表現等を用いて欲しい情報だけを取得する必要があります。ここでは、プロンプトの末尾で出力の形式を固定しています。

```python
# 単語リストの作成
question = """
TOEICで600~700点レベルの単語を30個教えてください。意味の説明は不要です。
1. 
2.
"""  # 下の数字をつけることで出力の形式が固定されます

# ChatGPTによる応答の生成
response = openai.ChatCompletion.create(
    model="gpt-4",
    messages= [
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": question},
        ]
    )
text_gpt = response['choices'][0]['message']['content']

# 単語ごとに分割し、リストに保存
TOEIC600 = re.findall(r'\b\w+\b', text_gpt)
TOEIC600 = [word for word in TOEIC600 if not word.isdigit()]
```

### 単語の情報の生成

以上で生成した単語について以下の情報を追加していきます。

- 質問
- 解説用の意味
- 例文
- 品詞の変換（形容詞、名詞、動詞、副詞）
- 語源
- 注意点
- 類義語
- 対義語

今回は質問とその他の説明を分割して生成しました。

先ほどと同様抽出の問題がありますが、ここではfew-shotとして生成の具体例を入れてあげることで、出力の形式を固定します。自然言語で出力の形式を指示してあげる以上に、few-shotは強力に出力の形式を固定すると個人的に感じています。(”role”の”assistant”を用いたfew-shotとの比較の記事を後で作成します）

```python
# 質問を格納する辞書
TOEIC600_question = {}

# 単語質問リストの作成
for word in TOEIC600:
    question = f"""
    例:competitiveという単語を和訳して訳だけを教えてください。
    競争的な
    
    {word}という単語を和訳して、訳だけを教えてください。
    """  # 具体例を1ついれて出力形式などを固定

		# 生成
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages= [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": question},
            ],
        )
    text_gpt = response['choices'][0]['message']['content']

    # 単語ごとに分割し辞書に保存
    TOEIC600_question[word] = text_gpt
```

```python
# 単語の詳細情報を格納する辞書
TOEIC600_ex2 = {}

# 単語詳細リストの作成
for word in TOEIC600:
    question = f"""
    competitiveという単語について
    1. 意味
    2. 例文+和訳
    3. 形容詞や名詞、動詞、副詞への変換とその意味
    4. この単語を覚えるうえで役に立つ語源
    5. 注意点
    6. 類義語との比較
    7. 対義語との比較
    を説明してください。

    解答例：
    1. 意味:
    「competitive」は形容詞で、「競争力のある」、「競争的な」、「競争を好む」などの意味を持ちます。競争を通じて成功を追求することを指すことが多いです。

    2. 例文+和訳:
    - Our company is highly competitive in the global market. (私たちの会社は、グローバル市場で非常に競争力があります。)
    - She is competitive and always wants to be the best. (彼女は競争心が強く、常に最高でいたいと思っています。)

    3. 形容詞や名詞、動詞、副詞への変換とその意味:
    - 形容詞: Competitive
    - 名詞: Competitiveness（競争）
    - 動詞: Compete（競争する）
    - 副詞: Competitively(競争的に)

    4. この単語を覚えるうえで役に立つ語源:
    ラテン語の「competere」から派生した英語の単語で、「努力する」、「求める」、「競争する」を意味します。元々は「共に求める」を意味する「com-」（共に）と「petere」（求める）から成り立ちます。

    5. 注意点:
    「competitive salary」は「比較して高い給料」を表し、「competitive price」だと「比較して安い価格」を表すが混乱しやすいので注意しましょう。

    6. 類義語との比較:
    「aggressive」や「ambitious」なども似た意味を持ちますが、これらはより強い意志や積極性を示す傾向があります。「Competitive」は競争の文脈で使用され、他者との比較や競争を通じての成功を強調します。

    7. 対義語との比較:
    「Competitive」の対義語は「noncompetitive」や「uncompetitive」などがあります。これらは競争力がない、または競争を好まないことを示します。
    {word}という単語について、
    1. 意味
    2. 例文+和訳
    3. 形容詞や名詞、動詞、副詞への変換とその意味
    4. この単語を覚えるうえで役に立つ語源
    5. 注意点
    6. 類義語との比較
    7. 対義語との比較
    を説明してください。
    """  # 具体例を1ついれて出力形式などを固定

		# 生成
    response = openai.ChatCompletion.create(
        model="gpt-4",
        messages= [
            {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": question},
            ],
        )
    text_gpt = response['choices'][0]['message']['content']

    # 単語ごとに分割し辞書に保存
    TOEIC600_ex2[word] = re.findall(r':\n(.+?)(?=\n\n|$)', text_gpt, re.DOTALL)
```

### 生成結果

生成した情報をまとめると次のようになります。空白になっている場所は、生成の形式が完全に固定できていなくて、正規表現による抽出がうまくいかなかったものです。
抽出の仕方を工夫したり、所望の出力が出るまで生成を繰り返せば、この問題は解決できますが、今回は省略します。
![スクリーンショット 2023-10-08 113349.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/841e85f0-39f0-4193-389e-fbaf55f8ed27.png)
以上の単語ごとの情報の具体的な例がこちらです。少し見づらいですが、へーと思うような情報も多いです。自分がやっていた某大手予備校の単語テストの情報よりは充実していそうです。
![スクリーンショット 2023-10-08 120909.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/1f5d72f8-402f-6dba-32f6-dc0e4e08f3f6.png)
### データベースに保存

今回は作成した単語のデータベースをcsvファイルに保存します。

```python
# すべての情報を結合
word_list = []
for word in TOEIC600:
    word_list.append([word]+[TOEIC600_question[word]]+TOEIC600_ex2[word])

# データフレームにして格納
df_word2 = pd.DataFrame(word_list)
df_word2.columns = ['単語','テスト用意味', '意味', '例文', '変換', '語源', '注意点', '類義語', '対義語']
df_word2.to_csv("単語リスト（prompt, TOEIC600~700）.csv", encoding='utf_8_sig')
```

# 3. 単語テストの作成

作成した単語データベースを使って実際に単語テストを作ってみます。まずは先のデータベース作成用とは別ファイルを用意して必要なライブラリを読み込みます。

## ライブラリのインポート

```python
# ライブラリのインポート
import tkinter as tk
from random import shuffle, choice
import csv
```

## 単語データベースの読み込み

次に先ほどCSVファイルで作成したデータベースを読み込みます。

```python
# データベース読み込み
with open(csv_file_prompt, mode='r', encoding='utf_8_sig') as file:
    reader = csv.reader(file)
    word_all = []
    for row in reader:
        word_all.append(row)
        
# 単語と意味の組と単語と詳細情報の組を作成
word_bank = {}  # 単語+意味
word_details = {}  # 単語+詳細
title="単語テスト"
for row in word_all[1:]:
    word_bank[row[2]] = row[1]
		# 出力用に文字列として結合
    details_str = "\n\n\n".join([f"{word_all[0][i]}:\n{row[i]}" for i in range(3, len(row))])
    word_details[row[1]] = details_str
```

## 単語テスト

今回はtkinterを使って単語テストのGUIを作成していきます。GUIを作成するのは初めてだったで、ChatGPTのCodeInterpreterに非常にお世話になりました。

```python
class DetailsWindow(tk.Toplevel):
		""" 特定の単語に関する詳細情報を表示するToplevelウィンドウ  
    Args:
        parent (tk.Tk または tk.Toplevel): 親ウィンドウ
        word (str): 詳細が表示される単語
        details (str): 単語の詳細として表示されるテキスト
        
    Attributes:
        label (tk.Label): 単語の詳細を表示するラベルウィジェット
        close_button (tk.Button): ウィンドウを閉じるためのボタンウィジェット
        
    Methods:
        __init__(self, parent, word, details): 
            ウィンドウの初期化、タイトルとサイズの設定、詳細テキストの表示、ウィンドウを閉じるためのボタンの作成・配置
    """
    def __init__(self, parent, word, details):
        super().__init__(parent)  # 継承クラスのコンストラクタ呼び出し
        self.title(f"{word}の詳細")
        self.geometry("1500x600")
        
        self.label = tk.Label(self, text=details,justify=tk.LEFT)
        self.label.pack(pady=20)

        self.close_button = tk.Button(self, text="閉じる", command=self.destroy, width=20, height=2)
        self.close_button.pack(pady=20)

class ResultWindow(tk.Toplevel):
		""" テストの結果と使用された単語を表示するToplevelウィンドウ  
    Args:
        parent (tk.Tk または tk.Toplevel): 親ウィンドウ
        score (int): ユーザーのスコア
        max_questions (int): 最大質問数
        question (list of str): 出題された質問
        used_words (list of str): クイズで使用された単語のリスト
        word_details (dict): 各単語に関する詳細情報を含む辞書
        answers_correct (list of bool): 各質問に対するユーザーの回答が正しいかどうかを示すブール値のリスト
    
    Attributes:
        result_label (tk.Label): テストの結果を表示するラベルウィジェット
        restart_button (tk.Button): テストを再開するボタンウィジェット
        end_button (tk.Button): アプリケーションを終了するボタンウィジェット
        word_details (dict): 各単語に関する詳細情報を含む辞書
        word_bank (list of str): オリジナルの単語リスト
        n (int): 新しいクイズで使用される質問数
        title_text (str): 結果のウィンドウのタイトルテキスト
        
    Methods:
        __init__(self, parent, score, max_questions, question, used_words, word_details, answers_correct): 
            ウィンドウの初期化、テストの結果と使用された単語を表示、詳細ボタンとテストの再スタートや終了のオプションの提示
            
        show_details(self, word, details): 
            指定された単語の詳細情報を表示する新しいDetailsWindowを開く
            
        close_all(self): 
            すべてのウィンドウを閉じてアプリケーションを終了
            
        restart_quiz(self): 
            現在の結果ウィンドウを閉じて、新しいクイズを開始
    """
    def __init__(self, parent, score, max_questions, question, used_words, word_details, answers_correct):
        super().__init__(parent)
        self.title("テスト結果")
        self.geometry("400x800")
        # スコア
        percent = (score / max_questions) * 100
        result_text = f"あなたの点数は{score}/{max_questions}です！"
        self.result_label = tk.Label(self, text=result_text, font=("Arial", 16))  # 点数の表示
        self.result_label.pack(pady=10)

        # 詳細ボタンの設定
        for idx, word in enumerate(used_words):
            correct_symbol = "〇" if answers_correct[idx] else "×"  # 修正：正解かどうかに応じてシンボルを選択
            btn = tk.Button(self, text=f"{correct_symbol} {question[idx]}:{word}", command=lambda w=word: self.show_details(w, word_details[w]), width=20, height=2, justify=tk.LEFT)
            btn.pack(pady=5)
        
        self.restart_button = tk.Button(self, text="もう一度テストする", command=self.restart_quiz, width=20, height=2)
        self.restart_button.pack(pady=20)
        self.end_button = tk.Button(self, text="終了", command=self.close_all, width=20, height=2)
        self.end_button.pack(pady=20)

        # 再テストデータ
        self.word_details = word_details
        self.word_bank = parent.original_word_bank  # original_word_bankを参照
        self.n = parent.max_questions
        self.title_text = parent.title_text

    def show_details(self, word, details):
        """ Detailsウィンドウを表示する関数 """
        DetailsWindow(self, word, details)

    def close_all(self):
        """ すべてのウィンドウを閉じる関数 """
        self.master.destroy()
    
    def restart_quiz(self):
        """ クイズを再スタートする関数 """
        self.master.destroy()  # 元のクイズウィンドウを閉じる
        new_quiz = WordQuizApp(self.n, self.title_text, self.word_bank, self.word_details)  # 新しいクイズウィンドウを開く
        new_quiz.mainloop()

class WordQuizApp(tk.Tk):
		""" メインウィンドウ  
    Args:
        n (int): 出題される質問数
        title (str): タイトル
        word_bank (dict): クイズの質問として使用される単語の辞書。キーは日本語の意味、値は英単語
        word_details (dict): 各英単語の詳細情報を含む辞書
        
    Attributes:
        original_word_bank (dict): 出題される単語の元の辞書。キーは日本語単語、値は英単語。
        word_bank (dict): 問題作成のために使用される単語の辞書。キーは日本語単語、値は英単語
        word_details (dict): 各英単語の詳細情報を含む辞書
        used_words (list of str): これまでに出題された英単語のリスト
        score (int): ユーザーの現在のスコア
        answers_correct (list of bool): 各質問が正しかったかどうかを示すブール値のリスト
        current_question (int): 現在の質問番号
        max_questions (int): クイズで出題される最大質問数
        correct_word (str): 現在の質問の正しい英単語
        incorrect_words (list of str): 現在の質問の誤った選択肢として表示される英単語のリスト
        question_label (tk.Label): 現在の質問を表示するラベルウィジェット
        buttons (list of tk.Button): 英単語の選択肢を表示するボタンウィジェットのリスト
        score_label (tk.Label): 現在のスコアを表示するラベルウィジェット
        
    Methods:
        __init__(self, n, title, word_bank, word_details): 
            ウィンドウの初期化、単語バンクの設定、ウィジェットの作成、最初の質問の準備
            
        next_question(self): 
            新しい質問を準備し、選択肢を更新する。全ての質問が終了した場合は結果を表示する
            
        check_answer(self, idx): 
            選択された選択肢が正しいかどうかをチェックし、次の質問を表示する
            
        show_results(self): 
            クイズの結果を新しいウィンドウに表示する
    """
    def __init__(self, n, title, word_bank, word_details):
        super().__init__()
        self.original_word_bank = word_bank.copy()  # word_bankをコピーして保持
        self.word_bank = word_bank.copy()  # 問題作成用
        self.max_questions = n
        self.title_text = title
        self.title(title)
        self.geometry("400x400")
        self.word_details = word_details
        self.used_words = []
        self.score = 0
        self.answers_correct = []
        self.current_question = 0
        self.correct_word = ""
        self.incorrect_words = []
        self.question_label = tk.Label(self, text="", font=("Arial", 16))
        self.question_label.pack(pady=20)
        self.question = []
        self.buttons = []

        for _ in range(4+1):  # 選択肢の設定
            btn = tk.Button(self, text="", command=lambda idx=_: self.check_answer(idx), width=12, height=2)
            btn.pack(pady=5)
            self.buttons.append(btn)
        self.next_question()

    def next_question(self):
        """ 次の質問のセットアップ """
        if self.current_question < self.max_questions:
            jp_word, en_word = choice(list(self.word_bank.items()))
            self.question.append(jp_word)
            del self.word_bank[jp_word]
            self.used_words.append(en_word)
            self.question_label.config(text=f"{self.current_question + 1}.{jp_word}") # 問題
            self.correct_word = en_word
            # incorrect_wordsをword_bankからランダムに選んで設定
            self.incorrect_words = [ w for w in self.word_bank.values() if w != en_word ]
            options = [en_word] + [choice(self.incorrect_words) for _ in range(len(self.buttons)-2)] # 選択肢
            shuffle(options)
            options.append("わからない")
            
            for i, option in enumerate(options):
                self.buttons[i].config(text=option)  # ボタンにテキスト選択肢を設定

            self.current_question += 1
        else:
            self.show_results()

    def check_answer(self, idx):
        """ 解答結果の採点 """
        is_correct = self.buttons[idx].cget("text") == self.correct_word
        self.answers_correct.append(is_correct)  # 追加：この質問が正解かどうかを記録
        if is_correct:
            self.score += 1
        self.next_question()

    def show_results(self):
        """ 結果の表示 """
        self.question_label.config(text="単語テストは終了です!")
        for btn in self.buttons:
            btn.config(state=tk.DISABLED)
        ResultWindow(self, self.score, self.max_questions, self.question, self.used_words, self.word_details, self.answers_correct)
```

# 4. 実際にテストしてみる
作った単語テストを実際に実行してみます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/3a875a59-d029-b2be-95c5-3d45fe45f86e.png" width="400" height="300"/>
適当に選択肢を選んでテストが終えると次のようなウィンドウが表示されます。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/3f1fd00a-f29e-9b16-7c75-f5fc13b699f9.png" width="400" height="300"/>
各単語のボタンを押してみると、詳細の情報が表示されます。情報の正しさはとりあえず置いておくとして、注意点や類義語、対義語の情報は割と有用そうです。
![スクリーンショット 2023-10-07 212503.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/563c452a-1065-fe53-b235-2807ccf3328e.png)
![スクリーンショット 2023-10-07 212615.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/056ea6f9-4adc-289d-8777-44ff37ca656b.png)
![スクリーンショット 2023-10-07 212631.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/ee293c08-4de7-f7e9-99d7-226add7a3ddf.png)
一方で、生成自体は悪くないのですが、取得やUIの表示がうまくいっていないものもあります。これは今後の課題です。以下は類義語の説明が長すぎて、途中で切れている＋取得が失敗しています。
![スクリーンショット 2023-10-08 122623.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3525327/d8ac5b2d-42cf-bb98-f43d-e8574566cb77.png)
# 5. まとめ

今回はChatGPTのAPIを使って、単語のデータベースを作成し、単語テストを作成してみました。テキストのデータベースを作成するうえで、出力形式の固定をしっかりできれば、ChatGPTのAPIを利用することは極めて有用だと思います。また、単語テストの結果としては、今まで自分が使ってきた単語テストよりは、情報が豊富な単語テスト自動生成ができたかなと思います。また、自分の実力不足でこれらの単語テストをアプリ化することなどはできませんでした（わからなかった）。

今後の進展としては、キーボードによる記述に対応させたり、発音を確認できるようにしたり、ユーザーの間違えた単語を記録して、その単語を中心とした英語長文の生成や助言の追加など幅広い様々な応用が考えられます。

# 6. 類似文献

- 【公式】APIキーの取得について
    https://platform.openai.com/account/api-keys
    
- ChatGPTに指示するだけで本当にアプリが作れるのかやってみた
    https://zenn.dev/tellernovel_inc/articles/0433926786d937