初始化项目,由ModelHub XC社区提供模型

Model: Nextorage/Llama-3.1-Swallow-8B-OpenMath-FT
Source: Original Platform
This commit is contained in:
ModelHub XC
2026-07-12 02:36:09 +08:00
commit 8d914512ab
13 changed files with 3782 additions and 0 deletions

818
run_inference.py Normal file
View File

@@ -0,0 +1,818 @@
#!/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()