| import os |
| import re |
| import torch |
| import ast |
| import json |
| import shutil |
| import csv |
| import multiprocessing |
| import queue |
| from pathlib import Path |
| from tqdm import tqdm |
| from typing import List, Dict, Optional, Union |
|
|
| |
| |
| |
| def _code_runner_worker(q, code_str): |
| """在完全隔离的进程中运行代码""" |
| try: |
| |
| safe_globals = { |
| "__builtins__": { |
| "abs": abs, "round": round, "min": min, "max": max, "sum": sum, |
| "int": int, "float": float, "str": str, "len": len, "range": range, |
| "list": list, "dict": dict, "set": set, "tuple": tuple, |
| "True": True, "False": False, "None": None, |
| "math": __import__("math") |
| } |
| } |
| safe_locals = {} |
| |
| |
| full_code = f"{code_str}\n\nresult = simple_math_problem()" |
| |
| |
| exec(full_code, safe_globals, safe_locals) |
| |
| |
| res = safe_locals.get('result') |
| q.put(res) |
| except Exception: |
| q.put(None) |
|
|
| |
| |
| |
| class Generator: |
| def __init__(self, config, model, tokenizer, device="cuda", output_dir=None): |
| self.config = config.generation |
| self.model = model.eval().to(device) |
| self.tokenizer = tokenizer |
| self.device = device |
| self.output_dir = output_dir or Path('output') |
| |
| |
| self.dataset_name = getattr(config, 'dataset', "chat") |
| if not isinstance(self.dataset_name, str): |
| self.dataset_name = getattr(self.dataset_name, 'dataset_name', "chat") |
| |
| self.is_tinygsm = "tinygsm" in self.dataset_name.lower() |
| print(f"[Generator] Mode: {'TinyGSM (Evaluation)' if self.is_tinygsm else 'Chat'}") |
|
|
| def generate(self, prompt: Optional[str] = None): |
| if self.is_tinygsm: |
| return self._generate_tinygsm() |
| return self._generate_chat(prompt) |
|
|
| def _execute_math_code(self, code: str, timeout: int = 20) -> Optional[float]: |
| """核心:多进程执行器,防止死循环卡死""" |
| if not code or 'def simple_math_problem' not in code: |
| return None |
|
|
| |
| if len(code) > 30000: |
| return None |
|
|
| |
| ctx = multiprocessing.get_context('spawn') |
| q = ctx.Queue() |
| p = ctx.Process(target=_code_runner_worker, args=(q, code)) |
| |
| p.start() |
| p.join(timeout) |
|
|
| if p.is_alive(): |
| p.terminate() |
| p.join() |
| return None |
|
|
| try: |
| result = q.get_nowait() |
| return float(result) if result is not None else None |
| except (queue.Empty, ValueError, TypeError): |
| return None |
|
|
| def _generate_solutions(self, questions, ground_truth): |
| """生成并实时验证准确率""" |
| results = {'questions': [], 'generated_solutions': [], 'executed_answers': [], 'full_outputs': []} |
| correct, total = 0, 0 |
| |
| pbar = tqdm(zip(questions, ground_truth), total=len(questions), desc="GSM8K Eval") |
| |
| for idx, (question, truth_val) in enumerate(pbar): |
| prompt = f"<|bos|>Question: {question}\n\nSolution:\ndef simple_math_problem() -> int:\n # {question}\n " |
| |
| |
| input_ids = torch.tensor([self.tokenizer.encode(prompt)], dtype=torch.long).to(self.device) |
| with torch.no_grad(): |
| outputs = self.model.generate( |
| input_ids, |
| max_generation_length=512, |
| tokenizer=self.tokenizer, |
| temperature=0.7, |
| top_p=0.95 |
| ) |
| |
| full_text = self.tokenizer.decode(outputs[0].tolist()) |
| |
| |
| code = self._extract_function_code(full_text) |
| answer = self._execute_math_code(code, timeout=60) |
| |
| |
| is_correct = self._compare_answers(answer, truth_val) |
| if is_correct: correct += 1 |
| total += 1 |
| |
| |
| pbar.set_postfix({"Acc": f"{correct/total:.2%}"}) |
| |
| |
| results['questions'].append(question) |
| results['executed_answers'].append(answer) |
| results['full_outputs'].append({'question': question, 'code': code, 'answer': answer, 'truth': truth_val}) |
|
|
| |
| if idx % 20 == 0: torch.cuda.empty_cache() |
|
|
| return results, {"correct": correct, "total": total, "accuracy": correct/total} |
|
|
| def _extract_function_code(self, text): |
| """正则匹配提取完整的 Python 函数""" |
| match = re.search(r'def simple_math_problem\(\).*?(\n\s+return.*?(?=\n\S|$))', text, re.DOTALL) |
| if match: return match.group(0) |
| |
| |
| lines = text.split('\n') |
| code_lines = [] |
| in_func = False |
| for line in lines: |
| if 'def simple_math_problem' in line: |
| in_func = True |
| code_lines.append(line) |
| continue |
| if in_func: |
| if line.startswith(' ') or line.startswith('\t') or not line.strip(): |
| code_lines.append(line) |
| else: break |
| return '\n'.join(code_lines) if code_lines else None |
|
|
| def _compare_answers(self, pred, truth) -> bool: |
| if pred is None: return False |
| try: |
| return abs(float(pred) - float(truth)) < 1e-5 |
| except: return False |
|
|
| def _generate_tinygsm(self): |
| """TinyGSM 数据集主逻辑: 分两半验证并合并结果""" |
| from datasets import load_dataset |
| import re |
| import json |
| |
| ds = load_dataset("openai/gsm8k", "main", split="test") |
| ds = [s for s in ds] |
| mid_point = len(ds) // 2 |
| |
| |
| def process_subset(subset): |
| questions = [s['question'] for s in subset] |
| ground_truth = [ |
| re.search(r'####\s*([+-]?[\d,]+\.?\d*)', s['answer']).group(1).replace(',', '') |
| for s in subset |
| ] |
| return self._generate_solutions(questions, ground_truth) |
|
|
| print(f"--- Processing First Half (0 to {mid_point}) ---") |
| results_1, summary_1 = process_subset(ds[:mid_point]) |
| |
| print(f"--- Processing Second Half ({mid_point} to {len(ds)}) ---") |
| results_2, summary_2 = process_subset(ds[mid_point:]) |
|
|
| |
| combined_results = results_1 + results_2 |
| |
| |
| |
| total_correct = summary_1['correct_count'] + summary_2['correct_count'] |
| total_samples = summary_1['total_count'] + summary_2['total_count'] |
| combined_accuracy = total_correct / total_samples if total_samples > 0 else 0 |
|
|
| final_summary = { |
| "accuracy": combined_accuracy, |
| "total_samples": total_samples, |
| "first_half_acc": summary_1['accuracy'], |
| "second_half_acc": summary_2['accuracy'] |
| } |
|
|
| |
| self.output_dir.mkdir(parents=True, exist_ok=True) |
| with open(self.output_dir / "gsm8k_metrics.txt", 'w') as f: |
| f.write(json.dumps(final_summary, indent=4)) |
| |
| print(f"\n✅ Evaluation Finished! Combined Accuracy: {combined_accuracy:.2%}") |
| return combined_results |