GPT-4oに危険物取扱者試験(甲種試験)を解かせてみる-その1: データ準備と予備試験-


10/15追記

質問法を修正したバージョンで再回答させました。

はじめに

大規模言語モデル(LLM)に期待される用途の一つは、特定の業界や領域に特化した利用で、様々なユースケースが模索されています。
とはいえ、どこから手を付ければよいのやら、、という事例も多いです。

今回は、ユースケースとして、化学系のLLMを想定した作業を考えてみます。
具体的に、危険物取扱者試験(甲種試験)を解かせるというタスク※を設定しました。
(※モデルに求める要素が多すぎると、二兎を追うものは…という恒例のアンチパターンに遭遇します)

ちなみにこのアイデアは、以下のDiscordサーバーで提案してもらいました。


本記事では、まずは練習として、gpt-4oを使ったベンチマーク問題の準備と、当該モデルの性能計測を行ってみます。

危険物取扱者試験(甲種試験)とは?

(GPT-4oによる解説)
日本において危険物を取り扱うための資格試験で、特に危険性が高い物質を取り扱うための資格を取得するための試験です。この試験を合格すると、すべての種類の危険物を取り扱うことができるようになります。甲種試験は、他の乙種や丙種の試験と比べて難易度が高く、幅広い知識と技能が求められます。


この資格は、化学知識に加えて、日本の法令にも精通している必要があるため、難易度が高いようです(合格率30%程度とのこと)。

なぜこの問題を解くのか?

専門特化のモデルを作る際に最も重要なのは、目標を明確にすることです。
定量可能な達成目標(ベンチマーク)があると、開発の方向性が定まります。
ただし、ベンチマーク問題への過適合や、ベンチマークを解くことが自己目的化しないような工夫が必要です。

危険物取扱者試験(甲種試験)は、人間の化学能力を測るベンチマークですが、LLMの性能評価にも、そのまま使えそうです。

蛇足: 学習データをどこから集めるか?

特化型モデルを作る際の2つ目のポイントは、モデルの学習(or RAGに使う)データをどこから持ってくるか、という問題です。
この資格試験で出る化学系の問題は、インターネットなど、どこかに掲載されているレベルのものです。なので、頑張れば、ネットから情報を収集できます。
教科書や参考書を参考にするというアプローチも可能です。
いずれも、著作権には注意する必要がありあす。

これに対して、法令の条文や告示、訓令、通達などは著作権の対象とはならないので、法律はデータの準備が楽なのが特長です。
(合成データなども気兼ねなく作れます)

ベンチマークの準備

問題抽出

以下のサイトに、最近の過去問がpdfで掲載されていたので、そこからデータを抜き出します。

今回は問題数45問とが少なかったので、ChatGPTで抽出しました。
以下のような要領でデータを抽出し、JSON化していきます。

文章は上記サイトより引用

手作業での修正

たまに問題抽出がうまくいきませんでした。手作業で治す必要がありました。
(GPT-4oによるデータクローリングは、まだ全能ではないことがよくわかります)

生じた抽出エラー

  • 表形式データの認識ミス

  • 問題文と選択肢の混同

  • 指数の抽出(10^5を105と認識)


問題に対する回答は容易に抽出できました。

GPT-4oに問題を解かせる

openaiのapiを使います。
以下は、問題文の読み込み~回答までの簡単なコードです。

import json

problem_path="problems/problem.json"

#読み込み
with open(problem_path) as f:
    problem_list = json.load(f)

#openaiのapi
with open("key") as f:
    api_key=f.read().strip()


#問題文の生成
def problem_to_text(problem):
    text="次の選択式問題に日本語で回答しなさい。理由も回答すること。\n"
    text+=problem["text"]+"\n\n"
    for i,option in enumerate(problem["options"]):
        text+= f"{i+1}: {option}\n"
    return text


problem=problem_list["questions"][0]
print(problem_to_text(problem))

#openaiのapiを使った回答
from pydantic import BaseModel
from openai import OpenAI

prediction_path="problems/prediction.json"

client = OpenAI(api_key=api_key)

class AnswerClass(BaseModel):
    number: int
    reason: str

def predict(problem_list,problem_id):
    problem=problem_list["questions"][problem_id]

    #キャッシュを読み込む
    try:
        with open(prediction_path) as f:
            prediction_dict=json.load(f)
    except:
        prediction_dict={}

    if str(problem_id) in prediction_dict:
        return prediction_dict[str(problem_id)]

    #推論
    text=problem_to_text(problem)
    completion = client.beta.chat.completions.parse(
        model="gpt-4o-2024-08-06",
        messages=[
            {"role": "system", "content": text},
        ],
        response_format=AnswerClass,
        temperature=0.0,
    )
    ans=completion.choices[0].message.parsed
    prediction=ans.number
    reason=ans.reason

    #キャッシュに保存
    prediction_dict[str(problem_id)]={"prediction":prediction,"reason":reason}
    with open(prediction_path,"w") as f:
        json.dump(prediction_dict,f,indent=4,ensure_ascii=False)
    
    return prediction,reason

from tqdm import tqdm
n_problems=len(problem_list["questions"])
for pid in tqdm(range(n_problems)):
    prediction,reason=predict(problem_list,pid)

回答は1.5分で終わりました。人間よりも早いですね。

回答例 (指定数量という値を計算する問題)
"1": { "prediction": 1, "reason": "指定数量は、ガソリン200L、灯油1,000L、重油1,000L、軽油1,000L、シリンダー油1,000Lです。各組合せの指定数量の倍数を計算します。\n\n1: ガソリン2,000Lは10倍、灯油6,000Lは6倍。合計16倍。\n2: 灯油3,000Lは3倍、重油5,000Lは5倍。合計8倍。\n3: 重油4,000Lは4倍、軽油4,000Lは4倍。合計8倍。\n4: 軽油5,000Lは5倍、シリンダー油3,000Lは3倍。合計8倍。\n5: シリンダー油6,000Lは6倍、ガソリン2,000Lは10倍。合計16倍。\n\n1と5が同じ16倍ですが、問題文の意図から最初に出てくる組合せを選びます。したがって、1が正解です。" }

採点

適当に採点プログラムを書きました。

ans_json_path="problems/answer.json"
with open(ans_json_path,"r") as f:
    ans_dict=json.load(f)

with open(prediction_path) as f:
    prediction_dict=json.load(f)


correct_count=0

for pid in range(n_problems):
    prediction=prediction_dict[str(pid)]["prediction"]
    answer=ans_dict["questions_and_answers"][pid]["answer"]
    if prediction==answer:
        correct_count+=1
        print(f"問題{pid+1}: 正解")
    else:
        print(f"問題{pid+1}: 不正解")
print(f"正答率: {correct_count}/{n_problems}={correct_count/n_problems:.2f}")

全体の正答率: 33/45=0.73 となりました。

以下に、全問題の正否を示します。

[危険物に関する法令]
問題1: 正解
問題2: 正解
問題3: 正解
問題4: 不正解
問題5: 不正解
問題6: 正解
問題7: 正解
問題8: 不正解
問題9: 不正解
問題10: 不正解
問題11: 正解
問題12: 不正解
問題13: 正解
問題14: 正解
問題15: 正解
[物理化学及び化学]
問題16: 不正解 (知識問題)
問題17: 不正解 (計算問題)
問題18: 正解 (知識問題)
問題19: 正解 (知識問題)
問題20: 正解 (知識問題)
問題21: 不正解 (知識問題)
問題22: 正解 (知識問題)
問題23: 不正解 (計算問題)
問題24: 正解 (知識問題)
問題25: 不正解 (計算問題)
[危険物の性質並びにその火災予防及び消化の方法]
問題26: 正解
問題27: 正解
問題28: 正解
問題29: 正解
問題30: 正解
問題31: 正解
問題32: 正解
問題33: 正解
問題34: 正解
問題35: 正解
問題36: 正解
問題37: 正解
問題38: 正解
問題39: 正解
問題40: 正解
問題41: 不正解
問題42: 正解
問題43: 正解
問題44: 正解
問題45: 正解

合格には、各ジャンルごとに60%以上の正答率が必要なようです。
各科目の正答率は以下のとおりでした。
危険物: 60%
化学・物理化学: 50%
危険物の性質: 95%

化学・物理化学については、惜しくも50%に届きませんでした※。計算問題が特に苦手な印象です。
(※英語で推論させるなど、プロンプトを工夫すれば、60%は行けそうです)
日本の法令についても、知識が不足しているようです。

まとめと今後の方針

本記事では、gpt-4oに危険物取扱者試験の問題を抽出させた上で、実際に試験問題を解かせてみました。
合格ラインは各科目で60%超えのようですが、惜しくも届かない科目がありました。

人間の受験者は、この試験で正答率60%を突破すれば、晴れて難関試験(合格率30%)を突破、めでたしめでたし、となります。
一方でLLMの場合は、そうはいきません。LLMはユーザーから、コンピュータとしての完璧性を求められる宿命にあるので、正答率が60%程度では、ハルシネーションの塊≒ゴミ扱いになるのがオチです。

実際の現場で使って貰うためには、少なくとも95%くらいの正答率を出した上で、説得力のある理由や出典を提示できる能力が求められそうです。

その実現のため、以下のような方策が考えられます。

  • 複数のモデルの活用

    • openai-o1は、博士レベルの自然科学の知識を持つようです

      • gpt-4oが苦手な化学・物理化学系の問題は、openai-o1に解かせた方が良いかもしれません

  • RAG

    • gpt-4oは日本の法令系の知識が弱いようでした。

    • 関連する法令をRAGで引っ張ってくるシステムを作るというアイデアを活用するのが良さそうです

  • ファインチューニング

    • Llamaなどのローカルモデルをファインチューニングするというアイデアも有りです

    • ただし、一般に計算問題の能力はgpt-4oなどに劣るため、注意深いモデル設計が必要となりそうです。


個人的には、複数のモデルの活用&RAGが、コスパの良い解決策であるように思われます。
今後、余裕があれば、追加検討を行う予定です。

10/15追記: 質問方法の変更

上述の手法では、回答を取得しやすくするため、
response_format=AnswerClass
という形式でgptのapiを投げていました。

このフォーマットで質問を行うことで、
prediction=ans.number #回答番号
reason=ans.reason #理由
という風に、モデルの出力を容易に取得できます。

一方、この形式で回答をさせた場合と、ChatGPTのインターフェースで質問をした場合で、回答がやや異なることがわかりました。

そこで、以下のように、普通にテキストを返すフォーマットで質問をしてみることにしました。
出力の最後に「#回答」というセクションで回答番号のみを出力する以外は、自由に回答する仕様としました。


def problem_to_text(problem):
    text="次の選択式問題に日本語で回答しなさい。回答について一つ一つ吟味し、最後に#回答 として選択肢番号のみを出力しなさい。\n\n"
    #text=""
    text+=problem["text"]+"\n\n"
    for i,option in enumerate(problem["options"]):
        text+= f"{i+1}: {option}\n"
    return text

def predict(problem_list,problem_id):
    problem=problem_list["questions"][problem_id]

    #キャッシュを読み込む
    try:
        with open(prediction_path) as f:
            prediction_dict=json.load(f)
    except:
        prediction_dict={}

    if str(problem_id) in prediction_dict:
        return prediction_dict[str(problem_id)]

    #推論
    text=problem_to_text(problem)
    #completion = client.beta.chat.completions.create(
    completion = client.chat.completions.create(
        model="gpt-4o-2024-08-06",
        messages=[
        {"role": "system", "content": "You are a helpful assistant."},
            {"role": "user", "content": text},
        ],

        temperature=0.0,
    )
    ans=completion.choices[0].message.content

    #回答を変形
    prediction=ans.split("#回答")[1].strip()
    #キャッシュに保存
    prediction_dict[str(problem_id)]={"prediction":prediction,"reason":ans,}
    with open(prediction_path,"w") as f:
        json.dump(prediction_dict,f,indent=4,ensure_ascii=False)

    return ans

このような自由回答をさせることで、正答率が上昇しました。


各科目の正答率は以下のように変化しました。

危険物: 60→60%
化学・物理化学: 50→80%
危険物の性質: 95→95%
総合: 73→80%

理由はわかりませんが、化学・物理化学での正答率が著しく向上しました。
全ジャンルで60%を超えたので、無事に試験をパスすることができた模様です。

[教訓]
開発的には、response_formatやjson形式を指定してapiを投げたくなりますが、場合によっては回答精度が著しく変化する可能性があるため、丁寧な検証が必要。

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