#!/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コードは タグで囲んでください\n" "2. コードの実行結果は タグで囲んでください\n" "3. 最終的な答えは \\boxed{答え} の形式で明示してください\n" "\n" "回答例:\n" "Pythonコードを使用してこの問題を解決しましょう。\n" "x = 2 + 3\n" "print(x)\n" "\n" "5\n" "\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 blocks " "and inject real outputs into ") 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"\nError: {error_msg}\n\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. まで生成(コードブロックを含むテキスト) 2. コードを実際にPythonで実行 3. 実行結果を に注入 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: まで生成 stop_criteria = StopOnString("", 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 "" not in generated: pipeline_info["final_status"] = "no_code" return postprocess_response(generated), pipeline_info # 以降を切り捨て(ハルシネーション防止) code_end_idx = generated.index("") + len("") 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"\n{output_text}\n\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("", 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 "" in repair_text: code_end = repair_text.index("") + len("") 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"\nError: {error_msg}\n\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()