4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

ChatGPTでクイズ問題の難易度を推定してみる

Posted at

最近流行りのChatGPTを使って、クイズの問題の難易度を推定してみたい。

現状だと、競技クイズの問題の難易度管理は作問者の感覚に依存する形になっているが、ChatGPTで自動化できれば客観的な指標として使いやすいかもしれない。

手法

サンプルとして、以下の10問を使う:

  1. 日本で一番高い山は何でしょう?(富士山)
  2. 世界で一番高い山は何でしょう?"(エベレスト)
  3. 日本で2番目に高い山は何でしょう?"(北岳)
  4. 世界で2番目に高い山は何でしょう?"(K2)
  5. アンデス山脈の最高峰は何でしょう?"(アコンカグア)
  6. ピレネー山脈の最高峰は何でしょう?"(アネト山)
  7. アパラチア山脈の最高峰は何でしょう?"(ミッチェル山)
  8. ドラケンスバーグ山脈の最高峰は何でしょう?"(タバナントレニャナ山)
  9. ドイツの最高峰は何でしょう?"(ツークシュピッツェ)
  10. パラオの最高峰は何でしょう?"(ゲルチェレチュース山)

この問題の選定は、同じジャンルに絞ることで問題間の比較をしやすいようにすること、問題文に含まれる情報を絞ること、正答率が高いものから低いものまでいろいろなパターンが含まれるようにすること、を意識して選んだ。

手順は以下の通り:

ChatGPTのAPIに、以下の形式のプロンプトを投げる:

これから述べるクイズの問題を、日本人の大学生〜大学院生に出題した時、予想される正答率を、「予想正答率: 10±2%」の形で書いてください。問題:{question} 解答:{answer}

モデルはgpt-3.5-turboを用いた。

レスポンスをいい感じにパースして正答率の数字を取得する。

これをループで100回繰り返す。レスポンスのタイムアウトやパースエラーを除いて、取得できた数字から、重み付き平均を取る。±の値を標準誤差だと思って、逆二乗の値を重みとする。statsmodels.stats.weightstats.DescrStatsWを使ってmeanとstd_meanを計算する。

結果

question answer mean sigma
日本で一番高い山は何でしょう? 富士山 91.740 0.885
世界で一番高い山は何でしょう? エベレスト 88.424 1.237
日本で2番目に高い山は何でしょう? 北岳 33.230 8.080
世界で2番目に高い山は何でしょう? K2 15.537 3.092
アンデス山脈の最高峰は何でしょう? アコンカグア 23.370 7.068
ピレネー山脈の最高峰は何でしょう? アネト山 8.256 1.371
アパラチア山脈の最高峰は何でしょう? ミッチェル山 9.075 1.341
ドラケンスバーグ山脈の最高峰は何でしょう? タバナントレニャナ山 6.346 0.774
ドイツの最高峰は何でしょう? ツークシュピッツェ 17.223 6.200
パラオの最高峰は何でしょう? ゲルチェレチュース山 6.053 0.891

hist_富士山.png
hist_エベレスト.png
hist_北岳.png
hist_K2.png
hist_アコンカグア.png
hist_アネト山.png
hist_ミッチェル山.png
hist_タバナントレニャナ山.png
hist_ツークシュピッツェ.png
hist_ゲルチェレチュース山.png

考察

大雑把に、正答率が高そうなものが高い数字に、低そうなものが低い数字にはなった。個人的な感覚としては、K2は若干過小評価な気がするし、ツークシュピッツェは過大評価な気がする。

ヒストグラムを見た感じ、多くの問題では1箇所際立ったピークがある形だが、北岳の問題のように山がはっきりしないものも見られた。北岳・アコンカグア・ツークシュピッツェの問題は推定値のsigmaが少し大きめになっていて、こうしたChatGPTの推定のばらつきを反映していると思われる。

1問あたり100回ChatGPTのAPIを呼ぶことになるので、大規模な問題群に一律適用するにはちょっと重いかもしれないが、特定個人の感覚に依存しない難易度推定手法として一考の余地はありそうに思う。

Appendix

apiを呼んだり重み付き平均を取るのに使ったpythonのコードを貼っておく:

import datetime
import logging
import os
import sys

import numpy as np
import openai
import pandas as pd
from statsmodels.stats.weightstats import DescrStatsW

dt_now = datetime.datetime.now()

logger = logging.getLogger(__name__)

streamHandler = logging.StreamHandler(sys.stdout)
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
streamHandler.setFormatter(formatter)
logger.addHandler(streamHandler)

fileHandler = logging.FileHandler(
    f'log/log_{dt_now.strftime("%Y%m%d-%H%M%S")}.log')
fileHandler.setFormatter(formatter)
logger.addHandler(fileHandler)

logger.setLevel(logging.DEBUG)

openai.api_key = os.environ.get('OPENAI_SECRET', '')


def generate_prompt(question, answer):
    return f'これから述べるクイズの問題を、日本人の大学生〜大学院生に出題した時、予想される正答率を、「予想正答率: 10±2%」の形で書いてください。問題:{question} 解答:{answer}'


def query_openai(prompt, timeout=10):
    response = openai.ChatCompletion.create(
        model="gpt-3.5-turbo",
        messages=[
            {"role": "user", "content": prompt}
        ],
        timeout=timeout,
        request_timeout=timeout
    )
    return response['choices'][0]['message']['content']


def parse_openai_res(res_openai):
    data = res_openai.replace('予想正答率: ', '').replace('%', '').split('±')
    logger.debug(f'{data = }')
    m = float(data[0])
    s = float(data[1])
    return m, s


def calculate_mean_errors(df):
    d = DescrStatsW(df['mean'], weights=1/(df['sigma']**2))
    m = d.mean
    s = d.std_mean
    return m, s


def estimate_difficulty(question, answer, N=100):

    res = []

    for i in range(N):
        logger.info(f'loop {i}')

        try:
            prompt = generate_prompt(question, answer)
            res_openai = query_openai(prompt)
            logger.info(f'{res_openai = }')

            m, s = parse_openai_res(res_openai)
            res.append((m, s))
        except Exception as e:
            logger.warning(e)
            continue

    df = pd.DataFrame(res, columns=['mean', 'sigma'])
    df.to_csv(f'res/res_{answer}.csv', encoding='utf_8_sig')

    m, s = calculate_mean_errors(df)

    return m, s


def main():
    questions = pd.read_csv('questions.csv')
    logger.debug(questions)

    questions['mean'] = 0.0
    questions['sigma'] = 0.0
    for index, row in questions.iterrows():
        question = row['question']
        answer = row['answer']
        logger.info(f'{question} ({answer})')

        m, s = estimate_difficulty(question, answer)
        logger.info(f'{m:.3f} ± {s:.3f}')
        questions.iat[index, 2] = m
        questions.iat[index, 3] = s

    questions.to_csv('res.csv', encoding='utf_8_sig')

    logger.info('Completed!')


if __name__ == '__main__':
    main()
4
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?