RAG_Music_Tutor / experiments /build_eval_set.py
github-actions
Deploy snapshot from 3637771
3340567
Raw
History Blame Contribute Delete
4.87 kB
"""統合 eval set のビルダー。
2つのソースを1つの統一スキーマにまとめる:
1. data/eval/questions.json — 手作りsilver 20問(単一記事・記事内セクション統合型)
2. data/eval/forum_review.json — フォーラム由来。review_status=="reviewed" かつ
answerable_standalone==True のものだけ採用
統一スキーマ(1問1dict):
id : 一意なID(old_NN / forum_<topic_id>)
question : 質問文
ground_truth : 参照回答
expected_source: list[str] ← 常にlist。正解記事のslug(拡張子なし)
match_type : "single" | "and" | "or"
single = 正解が1記事(旧セット)
and = top-kに全記事が揃って初めて正解(比較・統合質問)
or = 候補のどれか1つが取れれば正解
difficulty : easy/medium/hard
source : "silver_manual" | "forum"
reviewed : bool ← 正解ラベルが人手検証済みか。False=LLM機械推定のまま(未検証)。
スコア解釈時に必ず区別すること(未レビュー分はラベルノイズを含む)。
topic : 旧セットのみ(chord/melody/rhythm/prerequisite)
出力: data/eval/eval_set_merged.json
デフォルトは reviewed 済みのみ。--include-pending で未レビュー(pending)の
answerable も LLM推定ラベルのまま投入する(reviewed=False で明示タグ付け)。
forum側のレビューが進むたびに再実行すれば、reviewedになった分が自動で増える。
"""
from __future__ import annotations
import argparse
import json
from music_rag import config
OLD_PATH = config.EVAL_DIR / "questions.json"
FORUM_PATH = config.EVAL_DIR / "forum_review.json"
OUT_PATH = config.EVAL_DIR / "eval_set_merged.json"
def build(include_pending: bool = False) -> list[dict]:
rows: list[dict] = []
# --- 1. 旧silver 20問(すべて人手作成=reviewed)---
old = json.loads(OLD_PATH.read_text(encoding="utf-8"))
for i, q in enumerate(old, start=1):
rows.append({
"id": f"old_{i:02d}",
"question": q["question"],
"ground_truth": q.get("ground_truth", ""),
"expected_source": [q["expected_source"]], # 単一→listに正規化
"match_type": "single",
"difficulty": q.get("difficulty"),
"source": "silver_manual",
"reviewed": True,
"topic": q.get("topic"),
})
# --- 2. forum ---
# 既定: reviewed かつ answerable のみ。
# include_pending: 未レビュー(pending)の answerable も LLM推定ラベルのまま投入し、
# reviewed=False で明示タグ付けする(正解ラベルは未検証)。
forum = json.loads(FORUM_PATH.read_text(encoding="utf-8"))
for tid, v in forum.items():
if not v.get("answerable_standalone"):
continue
is_reviewed = v.get("review_status") == "reviewed"
if not is_reviewed and not include_pending:
continue
rows.append({
"id": f"forum_{tid}",
"question": v["question"],
"ground_truth": v.get("ground_truth", ""),
"expected_source": v["expected_source"],
"match_type": v.get("match_type") or "or",
"difficulty": v.get("difficulty"),
"source": "forum",
"reviewed": is_reviewed, # False = LLM推定ラベルのまま(未検証)
"topic": None,
})
return rows
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--include-pending",
action="store_true",
help="未レビュー(pending)のforum answerableもLLM推定ラベルのまま投入(reviewed=Falseでタグ付け)",
)
args = parser.parse_args()
rows = build(include_pending=args.include_pending)
OUT_PATH.write_text(
json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8"
)
from collections import Counter
print(f"wrote {len(rows)} questions → {OUT_PATH}")
print(" source:", dict(Counter(r["source"] for r in rows)))
print(" reviewed:", dict(Counter(r["reviewed"] for r in rows)))
print(" match_type:", dict(Counter(r["match_type"] for r in rows)))
print(" difficulty:", dict(Counter(r["difficulty"] for r in rows)))
multi = [r for r in rows if len(r["expected_source"]) > 1]
print(f" multi-source questions: {len(multi)}")
n_pending = sum(1 for r in rows if not r["reviewed"])
if n_pending:
print(f" ⚠️ 未レビュー(reviewed=False)を {n_pending} 問含む: 正解ラベルはLLM推定のまま")
if __name__ == "__main__":
main()