Download src/sr_harness/cli/benchmark.py from YuMeow/SRHarness: direct link, hf CLI and curl.
- Browser
- Download file 31.1 kB
-
https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/cli/benchmark.py
- Command line
-
hf download hf://spaces/YuMeow/SRHarness/src/sr_harness/cli/benchmark.py
-
curl -L -o benchmark.py https://huggingface.co/spaces/YuMeow/SRHarness/resolve/main/src/sr_harness/cli/benchmark.py
31.1 kB
| """ | |
| LLM-SRBench 评估脚本 (浓缩版) | |
| 将 benchmark 评估流程整合为单个脚本: | |
| 1) 数据读取与预处理 | |
| 2) 调用符号回归函数 (placeholder) | |
| 3) 评估结果 | |
| 4) 汇总多次运行的结果 | |
| Usage: | |
| # 测试单个问题 | |
| sr-harness benchmark --exp_name test_my_algorithm --datasets lsrtransform --problem_names II.6.15b_1_0 | |
| # 测试单个数据集 | |
| sr-harness benchmark --exp_name test_my_algorithm --datasets bio_pop_growth | |
| # 测试一系列问题 | |
| sr-harness benchmark --problem_names MatSci2 MatSci19 CRK28 BPG1 PO6 | |
| # 测试特定算法 | |
| sr-harness benchmark --algorithm linear --exp_name test_linear_fitting | |
| """ | |
| from __future__ import annotations | |
| import re | |
| import time | |
| import json | |
| import h5py | |
| import shlex | |
| import dotenv | |
| import logging | |
| import argparse | |
| import datasets | |
| import numpy as np | |
| import sr_harness_engine as engine | |
| from sr_harness_engine.tree import transform | |
| from pathlib import Path | |
| from datetime import datetime | |
| from socket import gethostname | |
| from typing import Callable, Dict, List | |
| from scipy.stats import kendalltau | |
| from scipy.optimize import least_squares | |
| from sklearn.metrics import mean_absolute_percentage_error | |
| from sr_harness.utils import ( | |
| add_minus_flags, | |
| get_symbolic_acc, | |
| log_exception, | |
| sanitize_filename, | |
| save_args, | |
| seed_all, | |
| setup_logging, | |
| tag2ansi, | |
| ) | |
| from sr_harness._vendor.llmsr_bench.core import SEDTask, SRResult, Problem | |
| from sr_harness._vendor.llmsr_bench.algorithms import get_update_parser, get_algorithm, list_algorithms | |
| DATASET_SPLITS = { | |
| "lsrtransform": "lsr_transform", | |
| "bio_pop_growth": "lsr_synth_bio_pop_growth", | |
| "chem_react": "lsr_synth_chem_react", | |
| "matsci": "lsr_synth_matsci", | |
| "phys_osc": "lsr_synth_phys_osc", | |
| } | |
| dotenv.load_dotenv() # Load environment variables from .env file if present | |
| SCRIPT_NAME = "benchmark" | |
| _logger = logging.getLogger(f"sr_harness.{SCRIPT_NAME}") | |
| def setup_parser(parser: argparse.ArgumentParser | None = None) -> argparse.ArgumentParser: | |
| """Configure the command-line argument parser. | |
| Args: | |
| parser: Argument parser to configure. | |
| Returns: | |
| argparse.ArgumentParser: The operation result. | |
| """ | |
| if parser is None: | |
| parser = argparse.ArgumentParser( | |
| prog="sr-harness benchmark", | |
| description="LLM-SRBench Evaluation Script.", | |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, | |
| ) | |
| else: | |
| parser.description = "LLM-SRBench Evaluation Script." | |
| parser.formatter_class = argparse.ArgumentDefaultsHelpFormatter | |
| parser.add_argument("--algorithm", required=True, choices=list_algorithms(), help=( | |
| "Symbolic-regression algorithm to evaluate." | |
| )) | |
| parser.add_argument("--name", default=f"{SCRIPT_NAME}", help=( | |
| "Experiment task name used when auto-generating exp_name." | |
| )) | |
| parser.add_argument("--exp_name", default=None, help=( | |
| "Experiment name. Defaults to a timestamped name." | |
| )) | |
| parser.add_argument("--save_dir", default=f"./logs/{SCRIPT_NAME}", help=( | |
| "Root directory for logs and run artifacts." | |
| )) | |
| parser.add_argument("--seed", type=int, default=-1, help=( | |
| "Random seed. Default -1 means using current system time." | |
| )) | |
| parser.add_argument("--save_path", default=None, help=( | |
| "Path to save agent logs and artifacts. Default is auto-generated from --save_dir and --exp_name." | |
| )) | |
| parser.add_argument("--verbose", action=argparse.BooleanOptionalAction, default=False, help=( | |
| "Enable verbose agent logging." | |
| )) | |
| parser.add_argument("--debug", action=argparse.BooleanOptionalAction, default=False, help=( | |
| "Enable debug mode (verbose + raise caught exceptions)." | |
| )) | |
| parser.add_argument("--data_root", type=str, default=str(Path("data") / "llm-srbench-data"), help=( | |
| "HDF5 数据文件所在目录" | |
| )) | |
| parser.add_argument("--datasets", type=str, default=None, nargs="+", choices=list(DATASET_SPLITS.keys()), help=( | |
| "数据集名称, 默认评估全部数据集" | |
| )) | |
| parser.add_argument("--problem_names", type=str, default=None, nargs="+", help=( | |
| "仅评估指定问题(方程)ID, 默认评估全部问题" | |
| )) | |
| parser.add_argument("--skip_existing", action=argparse.BooleanOptionalAction, default=False, help=( | |
| "如果结果文件已存在则跳过评估" | |
| )) | |
| parser.add_argument("--skip_successful", action=argparse.BooleanOptionalAction, default=True, help=( | |
| "如果结果文件已存在且成功则跳过评估" | |
| )) | |
| parser.add_argument("--anonymize", action=argparse.BooleanOptionalAction, default=False, help=( | |
| "Anonymize agent-facing variables as x1..xn and target as y." | |
| )) | |
| # 解析 --algorithm 参数以获取对应的 update_parser | |
| algorithm_probe = argparse.ArgumentParser(add_help=False) | |
| algorithm_probe.add_argument("--algorithm", choices=list_algorithms()) | |
| probe_args, _ = algorithm_probe.parse_known_args() | |
| if probe_args.algorithm and (update_parser_fn := get_update_parser(probe_args.algorithm)): | |
| parser = update_parser_fn(parser) | |
| add_minus_flags(parser) | |
| return parser | |
| def load_problems(dataset_name: str, data_root: str, hf_repo_id = "nnheui/llm-srbench") -> List[Problem]: | |
| """Load problems. | |
| Args: | |
| dataset_name: The dataset name value. | |
| data_root: The data root value. | |
| hf_repo_id: The hf repo id value. | |
| Returns: | |
| List[Problem]: The operation result. | |
| """ | |
| split_name = DATASET_SPLITS[dataset_name] | |
| # 尝试从本地 parquet 文件加载公式元信息 | |
| data_dir = Path(data_root) / "data" | |
| if not data_dir.exists(): | |
| _logger.note(f"Local parquet not found. Downloading from {hf_repo_id}... (If you wait too long, consider downloading it manually)") | |
| ds = datasets.load_dataset(hf_repo_id, split=split_name, cache_dir='./data/hf_cache') | |
| elif not (data_path := data_dir.glob(f"{split_name}-*.parquet").__iter__().__next__()).exists(): | |
| _logger.note(f"Local parquet not found. Downloading from {hf_repo_id}... (If you wait too long, consider downloading it manually)") | |
| ds = datasets.load_dataset(hf_repo_id, split=split_name, cache_dir='./data/hf_cache') | |
| else: | |
| _logger.note(f"Loading parquet from local: {data_path}") | |
| ds = datasets.load_dataset("parquet", data_files=str(data_path), split="train") | |
| # 从 HDF5 读取数值样本 | |
| h5file_path = Path(data_root) / "lsr_bench_data.hdf5" | |
| problems = [] | |
| with h5py.File(h5file_path, "r") as f: | |
| for entry in ds: | |
| name = entry["name"] | |
| # HDF5 路径: /lsr_transform/<name> 或 /lsr_synth/<domain>/<name> | |
| if split_name == "lsr_transform": | |
| h5_path = f"/lsr_transform/{name}" | |
| else: | |
| h5_path = f"/lsr_synth/{dataset_name}/{name}" | |
| expression = entry["expression"] | |
| samples = {k: v[...].astype(np.float64) for k, v in f[h5_path].items()} | |
| # expression 中有一些 P(t) 这样的写法,需要将它替换成 P | |
| for symbol in entry['symbols']: | |
| try: | |
| expression = re.sub(rf"\b{re.escape(symbol)}\b\(t\)", symbol, expression) | |
| except Exception as e: | |
| _logger.warning(f"Failed to replace {symbol}(t) -> {symbol} in {entry['expression']!r}") | |
| # expression 中有一些 pi, e 这样的常数, 需要将它替换成数值 | |
| for key, var in {'pi': np.pi, 'e': np.e}.items(): | |
| if key not in entry['symbols']: | |
| try: | |
| expression = re.sub(rf"\b{key}\b", str(var), expression) | |
| except Exception as e: | |
| _logger.warning(f"Failed to replace {key} -> {var} in {entry['expression']!r}") | |
| expression = expression.replace("^", "**") | |
| # Chemical Reaction 数据集中存在一些错误设置的数值常数,替换为 __C0, __C1, ... 以便后续重新确定数值常数 | |
| if dataset_name == 'chem_react': | |
| # 0.18997742423620262_z, 0.547523655303147_s, .5_alpha, 1e-3_k -> __C0, __C1, __C2, __C3 | |
| expression = re.sub( | |
| r"(?<![A-Za-z_])(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?_[A-Za-z_]\w*", | |
| lambda m, c=iter(range(100)): f"__C{next(c)}", | |
| expression, | |
| ) | |
| # Physical Oscillation / Chemical Reaction 数据集中公式的常数未被正确设置,需要拟合后替换成数值 | |
| if dataset_name in {'phys_osc', 'chem_react'}: | |
| formula = engine.parse(expression) | |
| if unknowns := sorted({var.name for var in formula.iter_preorder() if isinstance(var, engine.Variable) and var.name not in entry['symbols']}): | |
| train, y = samples["train"], samples["train"][:, 0] | |
| base = {sym: train[:, i] for i, sym in enumerate(entry["symbols"])} | |
| def residual(c): | |
| data = base | {k: np.full(len(y), v) for k, v in zip(unknowns, c)} | |
| return formula.eval(data).flatten() - y | |
| starts = [np.full(len(unknowns), x) for x in (0.1, 0.5, 1.0, 2.0)] | |
| starts += [np.where(np.arange(len(unknowns)) == i, 0.01, 0.5) for i in range(len(unknowns))] | |
| rng = np.random.default_rng(0) | |
| starts += [np.exp(rng.uniform(np.log(0.01), np.log(2.0), len(unknowns))) for _ in range(4)] | |
| fits = [least_squares(residual, x0, bounds=(0, np.inf), max_nfev=5000).x for x0 in starts] | |
| values = min(fits, key=lambda c: np.mean(residual(c) ** 2)) | |
| for key, val in zip(unknowns, values): | |
| expression = re.sub(rf"\b{re.escape(key)}\b", repr(float(val)), expression) | |
| gt_expression = engine.parse(expression) | |
| # 确保没有未知变量 | |
| variables = {var.name for var in gt_expression.iter_preorder() if isinstance(var, engine.Variable)} | |
| if missing := set(variables) - set(entry['symbols']): | |
| _logger.warning(f"[{dataset_name}] {name} has unknown variables in expression: {missing}. This problem is SKIPPED.") | |
| continue | |
| # 确保 gt_expression 足够准确 | |
| data = {sym: samples["train"][:, i] for i, sym in enumerate(entry["symbols"])} | |
| y_pred = gt_expression.eval(data) | |
| y_true = data[entry["symbols"][0]] | |
| metrics = { | |
| 'mae': float(np.mean(np.abs(y_true - y_pred))), | |
| 'mape': float(mean_absolute_percentage_error(y_true, y_pred)), | |
| 'rmse': float(np.sqrt(np.mean((y_true - y_pred) ** 2))), | |
| 'r2': float(1 - np.mean((y_true - y_pred) ** 2) / np.var(y_true)) if np.var(y_true) > 0 else float("nan"), | |
| } | |
| if not (metrics['r2'] > 0.99): | |
| _logger.warning( | |
| f"[{dataset_name}] {name} has low R^2 ({metrics['r2']:.4f}) between gt_expression and train samples " | |
| f"(MAE={metrics['mae']:.4e}, MAPE={metrics['mape']:.4e}, RMSE={metrics['rmse']:.4e}). " | |
| f"This problem is NOT skipped, but the results may be unreliable." | |
| ) | |
| problems.append(Problem( | |
| dataset_identifier=dataset_name, | |
| equation_idx=name, | |
| symbols=entry["symbols"], | |
| symbol_descs=entry["symbol_descs"], | |
| symbol_properties=entry["symbol_properties"], | |
| raw_expression=entry["expression"], | |
| gt_expression=gt_expression, | |
| samples=samples, | |
| )) | |
| return problems | |
| def anonymize_problem(problem: Problem) -> Problem: | |
| """Return an agent-facing anonymized copy of a benchmark problem. | |
| Args: | |
| problem: The problem value. | |
| Returns: | |
| Problem: The operation result. | |
| """ | |
| target = problem.symbols[0] | |
| features = problem.symbols[1:] | |
| feature_mapping = {name: f"x{i}" for i, name in enumerate(features, start=1)} | {target: "y"} | |
| anonymized_symbols = [feature_mapping[sym] for sym in problem.symbols] | |
| anonymized_symbol_descs = ["target variable", *[f"input variable {i}" for i in range(1, len(features) + 1)]] | |
| anonymized_expression = transform( | |
| problem.gt_expression, | |
| lambda node: ( | |
| engine.Variable(feature_mapping[node.name]) | |
| if isinstance(node, engine.Variable) else node | |
| ), | |
| ) | |
| anonymized_expression_str = anonymized_expression.to_str() | |
| _logger.debug( | |
| f"[{problem.equation_idx} @ {problem.dataset_identifier}]" | |
| f"Anonymization enabled. " | |
| f"Variable mapping: {feature_mapping}\n" | |
| f"Original formula: {target} = {problem.gt_expression}\n" | |
| f"Anonymized formula: {anonymized_symbols[0]} = {anonymized_expression_str}\n" | |
| ) | |
| anonymized_problem = Problem( | |
| dataset_identifier=problem.dataset_identifier, | |
| equation_idx=problem.equation_idx, | |
| symbols=anonymized_symbols, | |
| symbol_descs=anonymized_symbol_descs, | |
| symbol_properties=problem.symbol_properties, | |
| gt_expression=anonymized_expression, | |
| raw_expression="<None>", | |
| samples=problem.samples, | |
| ) | |
| return anonymized_problem | |
| def compute_metrics(y_pred: np.ndarray, y_true: np.ndarray) -> Dict[str, float]: | |
| """Compute metrics. | |
| Args: | |
| y_pred: Predicted target values. | |
| y_true: Observed target values. | |
| Returns: | |
| Dict[str, float]: The operation result. | |
| """ | |
| mask = ~np.isnan(y_pred) # 原始的 LLM-SRBench 就是这么做的,可能导致潜在的问题,但为了保持一致,我们也采用相同的过滤方式。 | |
| y_pred, y_true = y_pred[mask], y_true[mask] | |
| if len(y_true) == 0: | |
| return { | |
| "mse": float("nan"), "nmse": float("nan"), "r2": float("nan"), | |
| "kdt": float("nan"), "mape": float("nan"), "num_valid_points": 0 | |
| } | |
| else: | |
| var = np.var(y_true) | |
| mse = float(np.mean((y_true - y_pred) ** 2)) | |
| nmse = float(mse / var) if var > 0 else float("nan") | |
| r2 = float(1 - nmse) | |
| kdt = float(kendalltau(y_true, y_pred)[0]) if len(y_true) > 1 else float("nan") | |
| mape = float(mean_absolute_percentage_error(y_true, y_pred)) | |
| acc01 = np.mean(np.abs(y_true - y_pred) <= 0.1 * np.abs(y_true)) | |
| return { | |
| "mse": mse, "nmse": nmse, "r2": r2, "acc01": acc01, | |
| "kdt": kdt, "mape": mape, "num_valid_points": len(y_true), | |
| } | |
| def evaluate_problem(args, problem: Problem, sr_fn: Callable, exp_path: Path) -> Dict: | |
| """Run the ``evaluate problem`` operation. | |
| Args: | |
| args: Parsed command-line arguments. | |
| problem: The problem value. | |
| sr_fn: The sr fn value. | |
| exp_path: The exp path value. | |
| Returns: | |
| Dict: The operation result. | |
| """ | |
| task = problem.create_task() | |
| start_time = time.time() | |
| result = sr_fn(args, task) | |
| search_time = time.time() - start_time | |
| # ID test | |
| X_id = problem.test_samples[:, 1:] | |
| y_id = problem.test_samples[:, 0] | |
| id_metrics = compute_metrics(y_pred=result.predict(X_id), y_true=y_id) | |
| # OOD test (if available) | |
| ood_metrics = None | |
| if problem.ood_test_samples is not None: | |
| X_ood = problem.ood_test_samples[:, 1:] | |
| y_ood = problem.ood_test_samples[:, 0] | |
| ood_metrics = compute_metrics(y_pred=result.predict(X_ood), y_true=y_ood) | |
| # Symbolic Accurate | |
| try: | |
| data = {sym: problem.test_samples[:, i] for i, sym in enumerate(problem.symbols)} | |
| f_true = problem.gt_expression | |
| f_pred = engine.parse(result.expression.replace("^", "**").replace("np.", "").replace("math.", "")) | |
| symbolic_acc = get_symbolic_acc( | |
| f_true, | |
| f_pred, | |
| data, | |
| return_details=True, | |
| llm_provider=getattr(args, "llm_provider", None), | |
| llm_model=getattr(args, "llm_model", None), | |
| ) | |
| foo = lambda x: tag2ansi(('[green bold]EQUIVALENT[reset]' if x is True else '[red bold]NOT EQUIVALENT[reset]' if x is False else f'[gray bold]{x!r}[reset]')) | |
| _logger.note(tag2ansi( | |
| f"[{problem.equation_idx}] The predicted formula is judged to be {foo(symbolic_acc['equivalent'])} since {symbolic_acc.get('reason')}:\n" | |
| f" f_true = [green]{f_true.to_str()}[reset]\n" | |
| f" f_pred = [red]{f_pred.to_str()}[reset]" | |
| f"Manually modify [green bold]{exp_path}[reset] if you want to override the symbolic accuracy judgment." | |
| )) | |
| except Exception as e: | |
| symbolic_acc = { | |
| "equivalent": None, | |
| "reason": f"symbolic accuracy check failed: [{type(e).__name__}] {e}", | |
| } | |
| _logger.warning(f"[{problem.equation_idx}] Symbolic accuracy check failed: {log_exception(e)}") | |
| benchmark_result = { | |
| "equation_id": problem.equation_idx, | |
| "dataset_identifier": problem.dataset_identifier, | |
| "gt_expression": problem.gt_expression.to_str(), | |
| "discovered_expression": result.expression, | |
| "num_train": len(problem.train_samples), | |
| "num_test": len(problem.test_samples), | |
| "search_time": search_time, | |
| "id_metrics": id_metrics, | |
| "ood_metrics": ood_metrics, | |
| "symbolic_acc": symbolic_acc['equivalent'], | |
| "symbolic_acc_detail": symbolic_acc['reason'], | |
| } | |
| if result.metadata: | |
| benchmark_result.update(result.metadata) | |
| return benchmark_result | |
| def log_result(result: Dict): | |
| """Run the ``log result`` operation. | |
| Args: | |
| result: Result mapping to format or update. | |
| """ | |
| lines = [] | |
| lines.append(f'[gray]{"=" * 50}') | |
| lines.append(f"[blue bold]Problem {result['equation_id']} @ {result.get('dataset_identifier', 'Unknown')} evaluated.[reset]") | |
| lines.append(f'[gray]{"-" * 50}') | |
| lines.append(f"[blue]GT: [green]{result['gt_expression']}[reset]") | |
| lines.append(f"[blue]Discovered: [red]{result['discovered_expression']}[reset]") | |
| if (symbolic_acc := result.get("symbolic_acc")) is None: | |
| lines.append(f"[blue]Symbolic Accurate:[reset] [gray]N/A[reset]") | |
| elif symbolic_acc: | |
| lines.append(f"[blue]Symbolic Accurate:[reset] [green]Yes[reset]") | |
| else: | |
| lines.append(f"[blue]Symbolic Accurate:[reset] [red]No[reset]") | |
| if 'id_metrics' in result and result['id_metrics'] is not None: | |
| lines.append( | |
| f"[blue]In-Domain: R2={result['id_metrics'].get('r2', float('nan')):.6f}, " | |
| f"MSE={result['id_metrics'].get('mse', float('nan')):.6f}, " | |
| f"NMSE={result['id_metrics'].get('nmse', float('nan')):.6f}, " | |
| f"MAPE={result['id_metrics'].get('mape', float('nan')):.6f}, " | |
| f"Acc@0.1={result['id_metrics'].get('acc01', float('nan')):.6f}, " | |
| f"Valid Points={result['id_metrics'].get('num_valid_points', 0)}" | |
| ) | |
| if 'ood_metrics' in result and result['ood_metrics'] is not None: | |
| lines.append( | |
| f"[blue]Out-of-Domain: R2={result['ood_metrics']['r2']:.6f}, " | |
| f"MSE={result['ood_metrics']['mse']:.6f}, " | |
| f"NMSE={result['ood_metrics']['nmse']:.6f}, " | |
| f"MAPE={result['ood_metrics']['mape']:.6f}, " | |
| f"Acc@0.1={result['ood_metrics']['acc01']:.6f}, " | |
| f"Valid Points={result['ood_metrics']['num_valid_points']}" | |
| ) | |
| lines.append(f"[blue]Search time:[reset] {result['search_time']:.2f}s") | |
| lines.append(f'[gray]{"=" * 50}') | |
| return tag2ansi("\n".join(lines)) | |
| def aggregate_results(results: List[Dict]) -> Dict: | |
| """Run the ``aggregate results`` operation. | |
| Args: | |
| results: Result records to process. | |
| Returns: | |
| Dict: The operation result. | |
| """ | |
| def safe_mean(key, group): | |
| vals = [r[group][key] for r in results if r[group] is not None and not np.isnan(r[group][key])] | |
| return float(np.mean(vals)) if vals else float("nan") | |
| n = len(results) | |
| # R² 达标率 | |
| r2_thresholds = [0.5, 0.9, 0.99, 0.999] | |
| r2_hit_rates = {} | |
| for thr in r2_thresholds: | |
| hits = sum(1 for r in results if r["id_metrics"] is not None and r["id_metrics"]["r2"] >= thr) | |
| r2_hit_rates[f"r2>={thr}"] = {"count": hits, "rate": hits / n if n > 0 else 0} | |
| summary = { | |
| "dataset": "|".join(sorted({r.get("dataset_identifier") for r in results} - {None})), | |
| "total_problems": n, | |
| "avg_search_time": float(np.mean([r["search_time"] for r in results])), | |
| "avg_symbolic_acc": float(np.mean([r.get("symbolic_acc") is True for r in results])), | |
| "r2_hit_rates": r2_hit_rates, | |
| } | |
| summary["id_metrics"] = { | |
| "avg_mse": safe_mean("mse", "id_metrics"), | |
| "avg_nmse": safe_mean("nmse", "id_metrics"), | |
| "avg_r2": safe_mean("r2", "id_metrics"), | |
| "avg_acc01": safe_mean("acc01", "id_metrics"), | |
| "avg_kdt": safe_mean("kdt", "id_metrics"), | |
| "avg_mape": safe_mean("mape", "id_metrics"), | |
| "avg_num_valid_points": safe_mean("num_valid_points", "id_metrics"), | |
| } | |
| if any(r["ood_metrics"] is not None for r in results): | |
| summary["ood_metrics"] = { | |
| "avg_mse": safe_mean("mse", "ood_metrics"), | |
| "avg_nmse": safe_mean("nmse", "ood_metrics"), | |
| "avg_r2": safe_mean("r2", "ood_metrics"), | |
| "avg_acc01": safe_mean("acc01", "ood_metrics"), | |
| "avg_kdt": safe_mean("kdt", "ood_metrics"), | |
| "avg_mape": safe_mean("mape", "ood_metrics"), | |
| "avg_num_valid_points": safe_mean("num_valid_points", "ood_metrics"), | |
| } | |
| return summary | |
| def conclude_results(results: List[Dict], llmsr_datasets: List[str], save_path: str): | |
| # 汇总 | |
| """Run the ``conclude results`` operation. | |
| Args: | |
| results: Result records to process. | |
| llmsr_datasets: The llmsr datasets value. | |
| save_path: Optional output path. | |
| """ | |
| results = [r for r in results if r.get("dataset_identifier") in llmsr_datasets] | |
| summary = aggregate_results(results) | |
| # 打印汇总 | |
| lines = [] | |
| lines.extend([ | |
| f'[gray]{"=" * 50}[reset]', | |
| f"[red bold]Summary of {'|'.join(llmsr_datasets)} ({summary['total_problems']} problems)[reset]", | |
| f'[gray]{"-" * 50}[reset]', | |
| f" [red]Avg R2 (In-Domain):[reset] {summary['id_metrics']['avg_r2']:.6f}", | |
| f" [red]Avg MSE (In-Domain):[reset] {summary['id_metrics']['avg_mse']:.6f}", | |
| f" [red]Avg NMSE (In-Domain):[reset] {summary['id_metrics']['avg_nmse']:.6f}", | |
| f" [red]Avg MAPE (In-Domain):[reset] {summary['id_metrics']['avg_mape']:.6f}", | |
| f" [red]Avg KDT (In-Domain):[reset] {summary['id_metrics']['avg_kdt']:.6f}", | |
| f" [red]Avg Acc@0.1 (In-Domain):[reset] {summary['id_metrics']['avg_acc01']:.6f}", | |
| f" [red]Avg Valid Points (In-Domain):[reset] {summary['id_metrics']['avg_num_valid_points']:.1f}", | |
| ]) | |
| if "ood_metrics" in summary: | |
| lines.extend([ | |
| f'[gray]{"-" * 50}[reset]', | |
| f" [red]Avg R2 (Out-of-Domain):[reset] {summary['ood_metrics']['avg_r2']:.6f}", | |
| f" [red]Avg MSE (Out-of-Domain):[reset] {summary['ood_metrics']['avg_mse']:.6f}", | |
| f" [red]Avg NMSE (Out-of-Domain):[reset] {summary['ood_metrics']['avg_nmse']:.6f}", | |
| f" [red]Avg MAPE (Out-of-Domain):[reset] {summary['ood_metrics']['avg_mape']:.6f}", | |
| f" [red]Avg KDT (Out-of-Domain):[reset] {summary['ood_metrics']['avg_kdt']:.6f}", | |
| f" [red]Avg Acc@0.1 (Out-of-Domain):[reset] {summary['ood_metrics']['avg_acc01']:.6f}", | |
| f" [red]Avg Valid Points (Out-of-Domain):[reset] {summary['ood_metrics']['avg_num_valid_points']:.1f}", | |
| ]) | |
| lines.append(f'[gray]{"-" * 50}[reset]') | |
| for thr, info in summary["r2_hit_rates"].items(): | |
| lines.append(f" [red]{thr:>10}:[reset] {info['count']:>5}/{summary['total_problems']} ({info['rate']:.1%})") | |
| lines.append(f'[gray]{"-" * 50}[reset]') | |
| lines.append(f" [red]Avg Search Time:[reset] {summary['avg_search_time']:.2f}s") | |
| lines.append(f" [red]Avg Symbolic Accurate Rate:[reset] {summary['avg_symbolic_acc']:.6f}") | |
| lines.append(f'[gray]{"=" * 50}[reset]') | |
| _logger.note(tag2ansi("\n" + "\n".join(lines))) | |
| # 保存汇总 | |
| save_path.parent.mkdir(parents=True, exist_ok=True) | |
| with open(save_path, "a", encoding="utf-8") as f: | |
| json.dump(summary, f, indent=2, default=str, allow_nan=True) | |
| _logger.note(f"Summary saved to {save_path}") | |
| def run_benchmark(args: argparse.Namespace) -> int: | |
| # 加载问题集 | |
| """Run the ``run benchmark`` operation. | |
| Args: | |
| args: Parsed command-line arguments. | |
| Returns: | |
| int: The operation result. | |
| """ | |
| args.datasets = args.datasets or list(DATASET_SPLITS.keys()) | |
| problems = [] | |
| for dataset in args.datasets: | |
| problems.extend(load_problems(dataset, args.data_root)) | |
| _logger.note(f"Load {len(problems)} problems in total from datasets: {args.datasets}") | |
| if args.problem_names is not None: | |
| if unknown_problems := set(args.problem_names) - set(p.equation_idx for p in problems): | |
| _logger.warning(f"Unknown problems specified: {unknown_problems}") | |
| problems = [p for p in problems if p.equation_idx in args.problem_names] | |
| _logger.note(f"Filtered to {len(problems)} problems: {[p.equation_idx for p in problems]}") | |
| if not problems: | |
| _logger.error("No valid problems found after filtering. Check your --problem_names.") | |
| return 1 | |
| # 将 agent-facing 问题匿名化。样本矩阵列顺序不变,只替换符号名和自然语言描述。 | |
| if args.anonymize: | |
| problems = [anonymize_problem(problem) for problem in problems] | |
| _logger.note("Anonymization enabled for benchmark tasks.") | |
| # 获取算法的 run 函数 | |
| sr_fn = get_algorithm(args.algorithm) | |
| # 逐个问题运行 SR 并评估 | |
| results = [] | |
| interrupted = False | |
| for i, problem in enumerate(problems): | |
| _logger.note(f"[{i+1}/{len(problems)}] {problem.equation_idx}: {problem.gt_expression.to_str()}") | |
| exp_path = Path(args.save_path) / "results" / f"{problem.dataset_identifier}_{problem.equation_idx}.jsonl" | |
| exp_path.parent.mkdir(parents=True, exist_ok=True) | |
| if exp_path.exists(): | |
| lines = [json.loads(line) for line in exp_path.read_text(encoding="utf-8").splitlines() if line.strip()] | |
| successful_lines = [line for line in lines if 'error' not in line] | |
| if args.skip_successful and successful_lines: | |
| result = successful_lines[-1] | |
| results.append(result) | |
| _logger.info(tag2ansi( | |
| f"Successful result already exists at {exp_path} ([red bold]{len(successful_lines)} successful records), skipping...\n" | |
| f"Last successful result:\n{log_result(result)}" | |
| )) | |
| continue | |
| if args.skip_existing and lines: | |
| result = successful_lines[-1] if successful_lines else lines[-1] | |
| results.append(result) | |
| _logger.info(tag2ansi( | |
| f"Result already exists at {exp_path} ([red bold]{len(lines)} records with [red bold]{len(successful_lines)} successful), skipping...\n" | |
| f"Last result:\n{log_result(result)}" | |
| )) | |
| continue | |
| try: | |
| result = evaluate_problem(args, problem, sr_fn, exp_path) | |
| results.append(result) | |
| _logger.note(f"\n{log_result(result)}") | |
| with open(exp_path, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(result, default=str, allow_nan=True) + "\n") | |
| _logger.note(f"Result saved to {exp_path}") | |
| except KeyboardInterrupt: | |
| _logger.warning("Evaluation interrupted by user.") | |
| interrupted = True | |
| break | |
| except Exception as e: | |
| _logger.error(f" ERROR: {log_exception(e)}") | |
| result = { | |
| "equation_id": problem.equation_idx, | |
| "dataset_identifier": problem.dataset_identifier, | |
| "gt_expression": problem.gt_expression.to_str(), | |
| "discovered_expression": None, | |
| "num_train": len(problem.train_samples), | |
| "num_test": len(problem.test_samples), | |
| "search_time": float("nan"), | |
| "id_metrics": { | |
| "mse": float("nan"), "nmse": float("nan"), "r2": float("nan"), | |
| "kdt": float("nan"), "mape": float("nan"), "acc01": float("nan"), | |
| "num_valid_points": 0 | |
| }, | |
| "ood_metrics": None, | |
| "symbolic_acc": False, | |
| "error": str(e), | |
| } | |
| results.append(result) | |
| with open(exp_path, "a", encoding="utf-8") as f: | |
| f.write(json.dumps(result, default=str, allow_nan=True) + "\n") | |
| if args.debug: raise | |
| # 汇总结果 | |
| for dataset in args.datasets: | |
| conclude_results(results, [dataset], save_path=Path(args.save_path) / "summary" / f"{dataset}.json") | |
| conclude_results(results, list(DATASET_SPLITS.keys()), save_path=Path(args.save_path) / "summary" / f"all.json") | |
| if interrupted: | |
| return 130 | |
| return 1 if any("error" in result for result in results) else 0 | |
| def main(args: argparse.Namespace) -> int: | |
| """Run LLM-SRBench from CLI arguments. | |
| Args: | |
| args: Parsed command-line arguments. | |
| Returns: | |
| int: The operation result. | |
| """ | |
| if args.exp_name is None: | |
| now = datetime.now() | |
| args.exp_name = sanitize_filename( | |
| f"{now:%Y%m%d}_{args.name}_{now:%H%M%S}_{gethostname()}" | |
| ) | |
| else: | |
| args.exp_name = sanitize_filename(args.exp_name) | |
| if args.debug: | |
| args.verbose = True | |
| if args.seed == -1: | |
| args.seed = int(datetime.now().timestamp() * 1000) % (2**32 - 1) | |
| seed_all(args.seed) | |
| save_path = Path(args.save_path) if args.save_path else Path(args.save_dir) / args.exp_name | |
| save_path.mkdir(parents=True, exist_ok=True) | |
| args.save_path = str(save_path) | |
| command = getattr(args, "invocation", ["sr-harness", "benchmark"]) | |
| args.invocation = " ".join(map(shlex.quote, command)) | |
| setup_logging( | |
| info_level="debug" if args.verbose else "info", | |
| exp_name=args.exp_name, | |
| save_path=save_path / "info.log", | |
| force=True, | |
| ) | |
| _logger.note(f"Args: {args}") | |
| save_args(args, save_path / "args.json") | |
| exit_code = run_benchmark(args) | |
| _logger.note(tag2ansi(f"Experiment completed. Re-run the script with [green bold]{args.invocation}[reset]")) | |
| return exit_code | |
| if __name__ == "__main__": | |
| raise SystemExit(main(setup_parser().parse_args())) | |