Files
Llama-3.1-Swallow-8B-OpenMa…/run_inference.py
ModelHub XC 8d914512ab 初始化项目,由ModelHub XC社区提供模型
Model: Nextorage/Llama-3.1-Swallow-8B-OpenMath-FT
Source: Original Platform
2026-07-12 02:36:09 +08:00

819 lines
33 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python3
"""
OpenMath Finetuning 自動評価スクリプト (Llama-3.1-Swallow-8B)
finetuned model でテストデータに対して推論を実行し、
自動採点 (Exact Match, Code Execution, Format Compliance) を全件実行。
結果をJSON + CSV + サマリーレポートで保存。
Usage:
python3 run_evaluation.py
python3 run_evaluation.py --model_path /path/to/model --test_data /path/to/test.json
python3 run_evaluation.py --skip_inference --results_json /path/to/existing_results.json
"""
import os
os.environ["CUDA_LAUNCH_BLOCKING"] = "1"
import argparse
import re
import csv
import gc
import json
import time
from datetime import datetime
from pathlib import Path
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, StoppingCriteria, StoppingCriteriaList
try:
from peft import PeftModel
_PEFT_AVAILABLE = True
except ImportError:
_PEFT_AVAILABLE = False
from eval_utils import (
extract_boxed_answer,
math_equivalent,
check_code_execution,
check_format_compliance,
extract_code_blocks,
execute_code_safely,
)
# デフォルトパス (Llama-3.1-Swallow-8B)
DEFAULT_FINETUNED_MODEL = "/home/mshohda/Desktop/KambayashiWork/Nextorage-LLM-ChatBot/AiDaptive_Software/output_llama_openmath"
DEFAULT_ORIGINAL_MODEL = "/home/mshohda/Desktop/KambayashiWork/Nextorage-LLM-ChatBot/AiDaptive_Software/models/Llama-3.1-Swallow-8B"
DEFAULT_TEST_DATA = "/home/mshohda/Desktop/KambayashiWork/Nextorage-LLM-ChatBot/Dataset/FinetuiningDataset/OpenMathInstruct/openmath_test_500.json"
DEFAULT_OUTPUT_DIR = "/home/mshohda/Desktop/KambayashiWork/Nextorage-LLM-ChatBot/AiDaptive_Software/vNXUN_2_03_00/commands/finetuning_openmath_llama/evaluation/results"
SYSTEM_PROMPT = (
"あなたは数学の問題を解く優秀なAIアシスタントです。ステップバイステップで考え、Pythonコードを使って計算し、最終的な答えを明示してください。\n"
"\n"
"回答は以下のフォーマットに従ってください:\n"
"1. Pythonコードは <llm-code> と </llm-code> タグで囲んでください\n"
"2. コードの実行結果は <llm-code-output> と </llm-code-output> タグで囲んでください\n"
"3. 最終的な答えは \\boxed{答え} の形式で明示してください\n"
"\n"
"回答例:\n"
"Pythonコードを使用してこの問題を解決しましょう。<llm-code>\n"
"x = 2 + 3\n"
"print(x)\n"
"</llm-code><llm-code-output>\n"
"5\n"
"</llm-code-output>\n"
"したがって、答えは\\boxed{5}です。"
)
def parse_args():
parser = argparse.ArgumentParser(description="OpenMath Finetuning Evaluation (Llama)")
parser.add_argument("--model_path", default=DEFAULT_FINETUNED_MODEL,
help="Path to finetuned model")
parser.add_argument("--original_model_path", default=DEFAULT_ORIGINAL_MODEL,
help="Path to original model (for tokenizer)")
parser.add_argument("--test_data", default=DEFAULT_TEST_DATA,
help="Path to test dataset JSON")
parser.add_argument("--output_dir", default=DEFAULT_OUTPUT_DIR,
help="Directory to save results")
parser.add_argument("--max_samples", type=int, default=-1,
help="Max samples to evaluate (-1 for all)")
parser.add_argument("--max_new_tokens", type=int, default=1024,
help="Max new tokens for generation")
parser.add_argument("--gpu_id", type=int, default=0,
help="GPU ID to use")
parser.add_argument("--skip_inference", action="store_true",
help="Skip inference, only run scoring on existing results")
parser.add_argument("--results_json", default=None,
help="Path to existing results JSON (for --skip_inference)")
parser.add_argument("--skip_code_exec", action="store_true",
help="Skip code execution checks")
parser.add_argument("--model_label", default=None,
help="Label for this model (e.g. 'finetuned', 'original'). "
"Used in output filenames and reports.")
parser.add_argument("--system_prompt", default=None,
help="Custom system prompt (overrides default SYSTEM_PROMPT). "
"Use to provide format instructions for baseline models.")
parser.add_argument("--code_exec_pipeline", action="store_true",
help="Enable code execution pipeline: execute <llm-code> blocks "
"and inject real outputs into <llm-code-output>")
parser.add_argument("--max_retries", type=int, default=2,
help="Max self-repair retries on code execution error (default: 2)")
return parser.parse_args()
def build_prompt(question: str) -> str:
"""学習時と同じQAフォーマットでプロンプトを構築 (Llama 3.1形式)"""
return (
f"<|begin_of_text|><|start_header_id|>system<|end_header_id|>\n\n"
f"{SYSTEM_PROMPT}<|eot_id|>"
f"<|start_header_id|>user<|end_header_id|>\n\n"
f"{question}<|eot_id|>"
f"<|start_header_id|>assistant<|end_header_id|>\n\n"
)
def build_repair_prompt(error_msg: str, failed_code: str, attempt: int) -> str:
"""エラー箇所を特定し、具体的な修正指示を生成"""
# tracebackからエラー行を抽出
error_line = ""
line_match = re.search(r'File ".*", line (\d+)', error_msg)
if line_match:
line_no = int(line_match.group(1))
code_lines = failed_code.strip().split('\n')
adjusted = line_no - 2 # wrapper scriptのオフセット補正
if 0 <= adjusted < len(code_lines):
error_line = code_lines[adjusted].strip()
# 2回目以降はアプローチ転換を促す
approach_change = ""
if attempt >= 1:
approach_change = (
"前回と同じアプローチでは解決しません。"
"sympyの代わりにmathモジュールやforループなど、別の方法を試してください。"
)
repair = f"<llm-code-output>\nError: {error_msg}\n</llm-code-output>\n"
repair += "上記のコードでエラーが発生しました。"
if error_line:
repair += f"特にこの行が問題です: `{error_line}`\n"
repair += "エラーメッセージをよく読み、問題の箇所を変更して修正したコードを書いてください。"
if approach_change:
repair += f"\n{approach_change}"
repair += "\n"
return repair
def postprocess_response(text: str) -> str:
"""回答後の繰り返し生成を除去する。"""
# 1. ターンマーカー検出
match = re.search(r'\n[^\n]{0,30}?(?:user|assistant)', text)
if match:
text = text[:match.start()].strip()
# 2. 短フレーズの繰り返し検出 (3回以上同じフレーズが繰り返されたら切り取る)
match = re.search(r'(.{10,80}?)\1{2,}', text, re.DOTALL)
if match:
text = text[:match.start() + len(match.group(1))].strip()
return text.strip()
def _find_lora_adapter(model_path: str) -> str | None:
"""
phisonai2 LoRA学習時のアダプタパスを探す。
model_path: .../output_dir/finetuned_model_YYYY-mm-dd-HH-MM-SS/epoch_N_step_M_#ModelName
対応するLoRAアダプタ: .../lora_adapters/finetuned_model_.../Lora_epoch_N_step_M_#ModelName
"""
model_path = Path(model_path)
# epoch_N_step_M_#ModelName → Lora_epoch_N_step_M_#ModelName
step_dir_name = model_path.name
lora_step_name = "Lora_" + step_dir_name
# output_dir/lora_adapters/finetuned_model_.../Lora_epoch_N_step_M_#...
run_dir = model_path.parent # finetuned_model_YYYY-...
output_dir = run_dir.parent # output_dir (e.g. /mnt/nvme0/output_llama_lora/lora_01_...)
candidate = output_dir / "lora_adapters" / run_dir.name / lora_step_name
if (candidate / "adapter_config.json").exists():
return str(candidate)
return None
def load_model(model_path: str, tokenizer_path: str, gpu_id: int):
"""モデルとトークナイザーをロード。LoRAアダプタが存在する場合は自動適用。"""
# CUDA_VISIBLE_DEVICES でGPUを制限
os.environ["CUDA_VISIBLE_DEVICES"] = str(gpu_id)
logical_gpu = 0 # CUDA_VISIBLE_DEVICES で絞った後は論理GPU 0
print(f"Loading tokenizer from: {tokenizer_path}")
tokenizer = AutoTokenizer.from_pretrained(tokenizer_path, trust_remote_code=True)
if tokenizer.pad_token is None:
tokenizer.pad_token = tokenizer.eos_token
# LoRAアダプタの存在確認
lora_adapter_path = _find_lora_adapter(model_path)
if lora_adapter_path:
if not _PEFT_AVAILABLE:
raise ImportError("peft is required for LoRA evaluation. Run: pip install peft")
print(f"LoRA adapter found: {lora_adapter_path}")
print(f"Loading base model from: {tokenizer_path}")
model = AutoModelForCausalLM.from_pretrained(
tokenizer_path,
torch_dtype=torch.bfloat16,
device_map={"": logical_gpu},
trust_remote_code=True,
)
print(f"Applying LoRA adapter...")
model = PeftModel.from_pretrained(model, lora_adapter_path)
model = model.merge_and_unload()
print(f" [OK] LoRA merged into base model")
else:
print(f"Loading model from: {model_path}")
model = AutoModelForCausalLM.from_pretrained(
model_path,
torch_dtype=torch.bfloat16,
device_map={"": logical_gpu},
trust_remote_code=True,
)
# vocab_size不一致の場合はリサイズ
actual_embed_size = model.model.embed_tokens.weight.shape[0]
tokenizer_vocab_size = len(tokenizer)
if actual_embed_size != tokenizer_vocab_size:
print(f" Resizing embeddings: {actual_embed_size} -> {tokenizer_vocab_size}")
model.resize_token_embeddings(tokenizer_vocab_size)
model.config.pad_token_id = tokenizer.pad_token_id
model.eval()
print(f" [OK] Model loaded on GPU {gpu_id} (CUDA_VISIBLE_DEVICES={gpu_id})")
return model, tokenizer
def compute_perplexity(model, tokenizer, question: str, answer: str) -> float:
"""質問と回答のペアに対するperplexityを計算"""
prompt = build_prompt(question) + answer
inputs = tokenizer(prompt, return_tensors="pt", truncation=True, max_length=2048).to(model.device)
with torch.no_grad():
outputs = model(
input_ids=inputs["input_ids"],
labels=inputs["input_ids"],
)
return torch.exp(outputs.loss).item()
def generate_response(model, tokenizer, question: str, max_new_tokens: int) -> str:
"""プロンプトを生成してモデルから応答を取得"""
prompt = build_prompt(question)
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
with torch.no_grad():
outputs = model.generate(
**inputs,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
)
response_ids = outputs[0][inputs["input_ids"].shape[1]:]
raw = tokenizer.decode(response_ids, skip_special_tokens=True)
return postprocess_response(raw)
class StopOnString(StoppingCriteria):
"""指定文字列が生成テキストに出現したら生成を停止する"""
def __init__(self, stop_string: str, tokenizer, prompt_length: int):
self.stop_string = stop_string
self.tokenizer = tokenizer
self.prompt_length = prompt_length
# stop_string のトークン数の3倍をチェック窓とする分割トークン化対策
self.check_window = max(
len(tokenizer.encode(stop_string, add_special_tokens=False)) * 3, 20
)
def __call__(self, input_ids, scores, **kwargs):
if input_ids.shape[1] <= self.prompt_length:
return False
start = max(self.prompt_length, input_ids.shape[1] - self.check_window)
tail = self.tokenizer.decode(input_ids[0][start:], skip_special_tokens=True)
return self.stop_string in tail
def generate_with_code_exec(
model, tokenizer, question: str, max_new_tokens: int, max_retries: int = 2
) -> tuple:
"""
コード実行パイプライン付き推論。
フロー:
1. </llm-code> まで生成(コードブロックを含むテキスト)
2. コードを実際にPythonで実行
3. 実行結果を <llm-code-output> に注入
4. 続きを生成して \\boxed{} を含む最終回答を取得
5. 実行エラー時はエラーを返してリトライself-repair
Returns:
(response_text, pipeline_info_dict)
"""
prompt = build_prompt(question)
input_ids = tokenizer(prompt, return_tensors="pt").input_ids.to(model.device)
prompt_len = input_ids.shape[1]
pipeline_info = {
"code_exec_pipeline": True,
"attempts": [],
"total_attempts": 0,
"final_status": "no_code",
}
# Phase 1: </llm-code> まで生成
stop_criteria = StopOnString("</llm-code>", tokenizer, prompt_len)
with torch.no_grad():
outputs = model.generate(
input_ids=input_ids,
max_new_tokens=max_new_tokens,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
stopping_criteria=StoppingCriteriaList([stop_criteria]),
)
generated = tokenizer.decode(outputs[0][prompt_len:], skip_special_tokens=True)
# コードブロックがない場合はそのまま返す
if "</llm-code>" not in generated:
pipeline_info["final_status"] = "no_code"
return postprocess_response(generated), pipeline_info
# </llm-code> 以降を切り捨て(ハルシネーション防止)
code_end_idx = generated.index("</llm-code>") + len("</llm-code>")
response_so_far = generated[:code_end_idx]
prev_code = None
for attempt in range(max_retries + 1):
pipeline_info["total_attempts"] = attempt + 1
# コードブロック抽出・実行
code_blocks = extract_code_blocks(response_so_far)
if not code_blocks:
pipeline_info["final_status"] = "no_code_block"
break
last_code = code_blocks[-1]
# 同一コード検出(無限ループ防止)
if prev_code is not None and last_code.strip() == prev_code.strip():
pipeline_info["final_status"] = "same_code_detected"
break
prev_code = last_code
exec_result = execute_code_safely(last_code)
attempt_info = {
"attempt": attempt,
"code_preview": last_code[:200],
"success": exec_result["success"],
"stdout": exec_result["stdout"][:500] if exec_result["stdout"] else "",
"stderr": exec_result["stderr"][:500] if exec_result["stderr"] else "",
}
pipeline_info["attempts"].append(attempt_info)
if exec_result["success"]:
# 成功: 実行結果を注入して続きを生成
output_text = exec_result["stdout"] if exec_result["stdout"] else "None"
injection = f"<llm-code-output>\n{output_text}\n</llm-code-output>\n"
response_so_far += injection
# Phase 2: 最終回答を生成
full_context = prompt + response_so_far
context_ids = tokenizer(
full_context, return_tensors="pt", truncation=True, max_length=4096
).input_ids.to(model.device)
context_len = context_ids.shape[1]
with torch.no_grad():
outputs = model.generate(
input_ids=context_ids,
max_new_tokens=max_new_tokens // 2,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
)
continuation = tokenizer.decode(
outputs[0][context_len:], skip_special_tokens=True
)
response_so_far += continuation
pipeline_info["final_status"] = "success"
return postprocess_response(response_so_far), pipeline_info
else:
# 実行失敗
error_msg = exec_result["stderr"] or "Unknown error"
if len(error_msg) > 300:
error_msg = error_msg[:300] + "..."
if attempt < max_retries:
# Self-repair: エラー箇所を特定した具体的な修正指示を生成
repair_injection = build_repair_prompt(error_msg, last_code, attempt)
response_so_far += repair_injection
full_context = prompt + response_so_far
context_ids = tokenizer(
full_context, return_tensors="pt", truncation=True, max_length=4096
).input_ids.to(model.device)
context_len = context_ids.shape[1]
stop_criteria = StopOnString("</llm-code>", tokenizer, context_len)
with torch.no_grad():
outputs = model.generate(
input_ids=context_ids,
max_new_tokens=max_new_tokens // 2,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
stopping_criteria=StoppingCriteriaList([stop_criteria]),
)
repair_text = tokenizer.decode(
outputs[0][context_len:], skip_special_tokens=True
)
if "</llm-code>" in repair_text:
code_end = repair_text.index("</llm-code>") + len("</llm-code>")
response_so_far += repair_text[:code_end]
# 次のループで再実行(同一コード検出はループ先頭で行う)
else:
# モデルがコードブロックを生成しなかった
response_so_far += repair_text
pipeline_info["final_status"] = "repair_no_code"
return postprocess_response(response_so_far), pipeline_info
else:
# リトライ上限到達: エラー出力を注入して続きを生成
injection = f"<llm-code-output>\nError: {error_msg}\n</llm-code-output>\n"
response_so_far += injection
full_context = prompt + response_so_far
context_ids = tokenizer(
full_context, return_tensors="pt", truncation=True, max_length=4096
).input_ids.to(model.device)
context_len = context_ids.shape[1]
with torch.no_grad():
outputs = model.generate(
input_ids=context_ids,
max_new_tokens=max_new_tokens // 2,
do_sample=False,
pad_token_id=tokenizer.pad_token_id,
eos_token_id=tokenizer.eos_token_id,
repetition_penalty=1.1,
)
continuation = tokenizer.decode(
outputs[0][context_len:], skip_special_tokens=True
)
response_so_far += continuation
pipeline_info["final_status"] = "error_exhausted"
return postprocess_response(response_so_far), pipeline_info
return postprocess_response(response_so_far), pipeline_info
def run_inference(model, tokenizer, test_data: list, max_new_tokens: int,
max_samples: int, code_exec_pipeline: bool = False,
max_retries: int = 2) -> list:
"""全テストデータに対して推論を実行"""
samples = test_data if max_samples < 0 else test_data[:max_samples]
results = []
total = len(samples)
mode = "code execution pipeline" if code_exec_pipeline else "standard"
print(f"\nRunning inference on {total} samples... (mode: {mode})")
if code_exec_pipeline:
print(f" max_retries={max_retries} for self-repair on code execution error")
start_time = time.time()
for i, sample in enumerate(samples):
t0 = time.time()
if code_exec_pipeline:
response, pipeline_info = generate_with_code_exec(
model, tokenizer, sample["question"], max_new_tokens, max_retries
)
else:
response = generate_response(model, tokenizer, sample["question"], max_new_tokens)
pipeline_info = None
elapsed = time.time() - t0
# Perplexity: 参照回答に対するモデルのperplexity
ppl = compute_perplexity(model, tokenizer, sample["question"], sample["cot_answer"])
result = {
"id": i,
"question": sample["question"],
"reference_answer": sample["cot_answer"],
"model_response": response,
"inference_time_s": round(elapsed, 2),
"perplexity": round(ppl, 4),
}
if pipeline_info:
result["pipeline_info"] = pipeline_info
results.append(result)
if (i + 1) % 10 == 0 or (i + 1) == total:
elapsed_total = time.time() - start_time
eta = elapsed_total / (i + 1) * (total - i - 1)
status = ""
if pipeline_info:
status = f" [{pipeline_info['final_status']}]"
print(f" [{i+1}/{total}] {elapsed:.1f}s/sample{status}, ETA: {eta/60:.1f}min")
total_time = time.time() - start_time
print(f"\nInference completed: {total_time/60:.1f}min total, {total_time/total:.1f}s/sample avg")
return results
def score_results(results: list, skip_code_exec: bool = False) -> list:
"""推論結果を自動採点"""
print(f"\nScoring {len(results)} results...")
for i, r in enumerate(results):
ref = r["reference_answer"]
resp = r["model_response"]
# 1. Boxed Answer Exact Match
gold_answer = extract_boxed_answer(ref)
pred_answer = extract_boxed_answer(resp)
r["gold_boxed"] = gold_answer
r["pred_boxed"] = pred_answer
r["exact_match"] = math_equivalent(pred_answer, gold_answer) if (pred_answer and gold_answer) else False
# 2. Code Execution
if skip_code_exec:
r["code_exec"] = {"has_code": False, "skipped": True}
else:
r["code_exec"] = check_code_execution(resp)
# 3. Format Compliance
r["format"] = check_format_compliance(resp)
if (i + 1) % 50 == 0:
print(f" [{i+1}/{len(results)}] scored")
return results
def compute_summary(results: list) -> dict:
"""集計サマリーを計算"""
total = len(results)
if total == 0:
return {}
# Exact Match
em_correct = sum(1 for r in results if r["exact_match"])
gold_extracted = sum(1 for r in results if r["gold_boxed"] is not None)
pred_extracted = sum(1 for r in results if r["pred_boxed"] is not None)
# Code Execution
code_results = [r for r in results if r["code_exec"].get("has_code", False)]
code_success = sum(1 for r in code_results if r["code_exec"]["all_success"])
# Format Compliance
format_ok = sum(1 for r in results if r["format"]["format_ok"])
has_boxed = sum(1 for r in results if r["format"]["has_boxed_answer"])
no_bad_tokens = sum(1 for r in results if r["format"]["no_bad_tokens"])
# Perplexity
ppls = [r["perplexity"] for r in results if "perplexity" in r]
avg_ppl = sum(ppls) / len(ppls) if ppls else 0
summary = {
"total_samples": total,
"perplexity": {
"mean": round(avg_ppl, 4),
"min": round(min(ppls), 4) if ppls else 0,
"max": round(max(ppls), 4) if ppls else 0,
},
"exact_match": {
"correct": em_correct,
"accuracy": round(em_correct / total * 100, 1),
"gold_extracted": gold_extracted,
"pred_extracted": pred_extracted,
},
"code_execution": {
"samples_with_code": len(code_results),
"all_success": code_success,
"success_rate": round(code_success / len(code_results) * 100, 1) if code_results else 0,
},
"format_compliance": {
"format_ok": format_ok,
"format_ok_rate": round(format_ok / total * 100, 1),
"has_boxed": has_boxed,
"has_boxed_rate": round(has_boxed / total * 100, 1),
"no_bad_tokens": no_bad_tokens,
},
}
# Code Execution Pipeline 統計
pipeline_results = [r for r in results if "pipeline_info" in r]
if pipeline_results:
status_counts = {}
for r in pipeline_results:
status = r["pipeline_info"]["final_status"]
status_counts[status] = status_counts.get(status, 0) + 1
total_retries = sum(
r["pipeline_info"]["total_attempts"] - 1
for r in pipeline_results
if r["pipeline_info"]["total_attempts"] > 1
)
summary["code_exec_pipeline"] = {
"enabled": True,
"samples_processed": len(pipeline_results),
"status_counts": status_counts,
"total_retries": total_retries,
}
return summary
def save_results(results: list, summary: dict, output_dir: str, model_label: str = None):
"""結果を保存"""
output_path = Path(output_dir)
output_path.mkdir(parents=True, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
label = f"_{model_label}" if model_label else ""
# 1. JSON (全詳細)
json_path = output_path / f"eval_results{label}_{timestamp}.json"
with open(json_path, "w", encoding="utf-8") as f:
json.dump({"summary": summary, "results": results}, f, ensure_ascii=False, indent=2)
print(f" JSON: {json_path}")
# 2. CSV (スコアシート)
csv_path = output_path / f"eval_scores{label}_{timestamp}.csv"
with open(csv_path, "w", newline="", encoding="utf-8") as f:
writer = csv.writer(f)
writer.writerow([
"id", "question_preview", "exact_match",
"gold_boxed", "pred_boxed",
"perplexity",
"has_code", "code_success",
"format_ok", "has_boxed", "no_bad_tokens",
"inference_time_s",
])
for r in results:
writer.writerow([
r["id"],
r["question"][:80],
int(r["exact_match"]),
r["gold_boxed"] or "",
r["pred_boxed"] or "",
r.get("perplexity", ""),
int(r["code_exec"].get("has_code", False)),
int(r["code_exec"].get("all_success", False)) if r["code_exec"].get("has_code") else "N/A",
int(r["format"]["format_ok"]),
int(r["format"]["has_boxed_answer"]),
int(r["format"]["no_bad_tokens"]),
r.get("inference_time_s", ""),
])
print(f" CSV: {csv_path}")
# 3. サマリーレポート (テキスト)
report_path = output_path / f"eval_summary{label}_{timestamp}.txt"
with open(report_path, "w", encoding="utf-8") as f:
f.write("=" * 60 + "\n")
f.write("OpenMath Finetuning Evaluation Summary (Llama-3.1-Swallow-8B)\n")
if model_label:
f.write(f"Model: {model_label}\n")
f.write(f"Date: {timestamp}\n")
f.write("=" * 60 + "\n\n")
s = summary
f.write(f"Total Samples: {s['total_samples']}\n\n")
f.write("--- Perplexity ---\n")
f.write(f" Mean: {s['perplexity']['mean']}\n")
f.write(f" Min: {s['perplexity']['min']}, Max: {s['perplexity']['max']}\n\n")
f.write("--- Exact Match (Primary Metric) ---\n")
f.write(f" Accuracy: {s['exact_match']['accuracy']}% ({s['exact_match']['correct']}/{s['total_samples']})\n")
f.write(f" Gold answer extracted: {s['exact_match']['gold_extracted']}/{s['total_samples']}\n")
f.write(f" Pred answer extracted: {s['exact_match']['pred_extracted']}/{s['total_samples']}\n\n")
f.write("--- Code Execution ---\n")
f.write(f" Samples with code: {s['code_execution']['samples_with_code']}\n")
f.write(f" Success rate: {s['code_execution']['success_rate']}% ({s['code_execution']['all_success']}/{s['code_execution']['samples_with_code']})\n\n")
f.write("--- Format Compliance ---\n")
f.write(f" Format OK: {s['format_compliance']['format_ok_rate']}% ({s['format_compliance']['format_ok']}/{s['total_samples']})\n")
f.write(f" Has boxed answer: {s['format_compliance']['has_boxed_rate']}%\n")
f.write(f" No bad tokens: {s['format_compliance']['no_bad_tokens']}/{s['total_samples']}\n\n")
if "code_exec_pipeline" in s:
p = s["code_exec_pipeline"]
f.write("--- Code Execution Pipeline ---\n")
f.write(f" Samples processed: {p['samples_processed']}\n")
for status, count in sorted(p["status_counts"].items()):
f.write(f" {status}: {count}\n")
f.write(f" Total self-repair retries: {p['total_retries']}\n")
print(f" Report: {report_path}")
return json_path, csv_path, report_path
def print_summary(summary: dict, model_label: str = None):
"""サマリーをコンソールに表示"""
s = summary
print("\n" + "=" * 60)
title = f"EVALUATION SUMMARY [{model_label}]" if model_label else "EVALUATION SUMMARY"
print(title)
print("=" * 60)
print(f"Total Samples: {s['total_samples']}")
print()
print(f" Perplexity (mean): {s['perplexity']['mean']}")
print(f" Exact Match Accuracy: {s['exact_match']['accuracy']}%"
f" ({s['exact_match']['correct']}/{s['total_samples']})")
print(f" Code Execution Rate: {s['code_execution']['success_rate']}%"
f" ({s['code_execution']['all_success']}/{s['code_execution']['samples_with_code']} samples with code)")
print(f" Format Compliance: {s['format_compliance']['format_ok_rate']}%")
if "code_exec_pipeline" in s:
p = s["code_exec_pipeline"]
print()
print(" --- Code Execution Pipeline ---")
print(f" Samples processed: {p['samples_processed']}")
for status, count in sorted(p["status_counts"].items()):
print(f" {status}: {count}")
print(f" Total self-repair retries: {p['total_retries']}")
print("=" * 60)
def main():
global SYSTEM_PROMPT
args = parse_args()
# カスタムシステムプロンプトが指定された場合、グローバル変数を上書き
if args.system_prompt is not None:
SYSTEM_PROMPT = args.system_prompt
print(f"[INFO] Using custom system prompt ({len(SYSTEM_PROMPT)} chars)")
print("=" * 60)
print("OpenMath Finetuning Evaluation (Llama-3.1-Swallow-8B)")
print(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
print("=" * 60)
if args.skip_inference:
# 既存の推論結果を読み込んでスコアリングのみ
results_path = args.results_json
if not results_path:
print("[ERROR] --results_json is required when using --skip_inference")
return
print(f"\nLoading existing results: {results_path}")
with open(results_path, encoding="utf-8") as f:
data = json.load(f)
results = data["results"] if "results" in data else data
else:
# テストデータ読み込み
print(f"\nTest data: {args.test_data}")
with open(args.test_data, encoding="utf-8") as f:
test_data = json.load(f)
print(f" {len(test_data)} samples loaded")
# モデルロード
print(f"\nModel: {args.model_path}")
model, tokenizer = load_model(args.model_path, args.original_model_path, args.gpu_id)
# 推論実行
results = run_inference(
model, tokenizer, test_data, args.max_new_tokens, args.max_samples,
code_exec_pipeline=args.code_exec_pipeline,
max_retries=args.max_retries,
)
# GPUメモリ解放
del model
gc.collect()
torch.cuda.empty_cache()
print(" [OK] Model unloaded")
# スコアリング
results = score_results(results, skip_code_exec=args.skip_code_exec)
# サマリー計算
summary = compute_summary(results)
# model_label の自動推定
model_label = args.model_label
if model_label is None and not args.skip_inference:
if args.model_path == DEFAULT_ORIGINAL_MODEL:
model_label = "llama_original"
elif args.model_path == DEFAULT_FINETUNED_MODEL:
model_label = "llama_finetuned"
# サマリーに metadata を追加
summary["model_label"] = model_label
summary["model_path"] = args.model_path if not args.skip_inference else "(skip_inference)"
# 保存
print("\nSaving results...")
save_results(results, summary, args.output_dir, model_label)
# サマリー表示
print_summary(summary, model_label)
if __name__ == "__main__":
main()