vortex-omega-final / evaluate_model.py
RDS777's picture
Upload 1772 files
89c2dc5 verified
Raw History Blame Contribute Delete
15.4 kB
#!/usr/bin/env python3
"""
training/scripts/evaluate_model.py
Évalue le modèle fine-tuné sur les 6 tÒches VORTEX GOD.
MΓ©triques par tΓ’che :
planner β†’ json_valid_rate, steps_count_correct, risks_present
engineer β†’ syntax_valid_rate, exec_pass_rate, has_docstring
critic β†’ json_valid_rate, verdict_present, issues_severity_present
researcher→ has_sections, has_citations, has_lacunes
scientist β†’ json_valid_rate, hypothesis_length, has_experiment
optimizer β†’ json_valid_rate, has_changes, speedup_numeric
Score global = moyenne pondΓ©rΓ©e (0.0 – 1.0)
Usage :
python training/scripts/evaluate_model.py \
--model training/checkpoints/vortex-lora \
--dataset training/data/vortex_dataset.jsonl \
--base-model Qwen/Qwen3-8B-Instruct \
--n 50 \
--output training/eval_results.json
"""
from __future__ import annotations
import argparse
import ast
import json
import logging
import os
import random
import re
import time
from pathlib import Path
from typing import Any, Callable, Dict, List, Optional, Tuple
log = logging.getLogger("evaluate_model")
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(message)s")
# ─────────────────────────────────────────────
# MΓ©triques par tΓ’che
# ─────────────────────────────────────────────
def _is_valid_json(text: str) -> bool:
clean = re.sub(r"```(?:json)?\n?", "", text).replace("```", "").strip()
for pattern in (r"\{[\s\S]*\}", r"\[[\s\S]*\]"):
m = re.search(pattern, clean)
if m:
try:
json.loads(m.group(0))
return True
except Exception:
pass
return False
def _is_valid_python(text: str) -> bool:
clean = re.sub(r"```(?:python|py)?\n?", "", text).replace("```", "").strip()
try:
ast.parse(clean)
return len(clean) > 30
except SyntaxError:
return False
def _exec_python(text: str, timeout: float = 5.0) -> bool:
"""ExΓ©cute le code dans un sous-process isolΓ©. Retourne True si exit code 0."""
import subprocess, tempfile
clean = re.sub(r"```(?:python|py)?\n?", "", text).replace("```", "").strip()
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
f.write(clean)
fname = f.name
try:
result = subprocess.run(
["python3", fname],
timeout=timeout,
capture_output=True,
)
return result.returncode == 0
except Exception:
return False
finally:
os.unlink(fname)
TASK_METRICS: Dict[str, List[Tuple[str, Callable[[str], float], float]]] = {
# (metric_name, scorer_fn returns 0.0-1.0, weight)
"planner": [
("json_valid", lambda r: 1.0 if _is_valid_json(r) else 0.0, 2.0),
("has_steps", lambda r: 1.0 if '"steps"' in r and '"id"' in r else 0.0, 1.5),
("has_goal", lambda r: 1.0 if '"goal"' in r else 0.0, 1.0),
("has_risks", lambda r: 1.0 if '"risks"' in r else 0.0, 0.5),
("steps_count", lambda r: min(1.0, _count_steps(r) / 4), 1.0),
],
"engineer": [
("syntax_valid", lambda r: 1.0 if _is_valid_python(r) else 0.0, 3.0),
("has_imports", lambda r: 1.0 if re.search(r"^import |^from ", r, re.MULTILINE) else 0.0, 1.0),
("has_docstring", lambda r: 1.0 if '"""' in r or "'''" in r else 0.0, 0.5),
("has_type_hints", lambda r: 1.0 if " -> " in r or ": " in r else 0.0, 0.5),
("exec_pass", lambda r: 1.0 if _exec_python(r) else 0.0, 2.0),
],
"critic": [
("json_valid", lambda r: 1.0 if _is_valid_json(r) else 0.0, 2.0),
("has_verdict", lambda r: 1.0 if '"verdict"' in r else 0.0, 1.5),
("has_issues", lambda r: 1.0 if '"issues"' in r else 0.0, 1.5),
("has_severity", lambda r: 1.0 if '"severity"' in r else 0.0, 1.0),
("has_fix", lambda r: 1.0 if '"fix"' in r else 0.0, 0.5),
],
"researcher": [
("has_sections", lambda r: min(1.0, r.count("##") / 2), 2.0),
("has_citations", lambda r: 1.0 if "[source:" in r.lower() else 0.0, 2.0),
("has_lacunes", lambda r: 1.0 if "lacune" in r.lower() or "manque" in r.lower() else 0.0, 1.0),
("length_ok", lambda r: 1.0 if 200 <= len(r.split()) <= 600 else 0.5, 1.0),
],
"scientist": [
("json_valid", lambda r: 1.0 if _is_valid_json(r) else 0.0, 2.0),
("has_hypothesis", lambda r: 1.0 if '"hypothesis"' in r else 0.0, 2.0),
("has_experiment", lambda r: 1.0 if '"experiment"' in r else 0.0, 1.5),
("has_confidence", lambda r: 1.0 if '"confidence"' in r else 0.0, 0.5),
("has_null_hyp", lambda r: 1.0 if '"null_hypothesis"' in r else 0.0, 0.5),
],
"optimizer": [
("json_valid", lambda r: 1.0 if _is_valid_json(r) else 0.0, 2.0),
("has_optimized", lambda r: 1.0 if '"optimized"' in r else 0.0, 2.0),
("has_changes", lambda r: 1.0 if '"changes"' in r else 0.0, 1.5),
("has_speedup", lambda r: 1.0 if '"expected_speedup"' in r else 0.0, 0.5),
("has_rationale", lambda r: 1.0 if '"rationale"' in r else 0.0, 0.5),
],
}
TASK_WEIGHTS = {
"planner": 1.0,
"engineer": 1.5, # tΓ’che la plus critique
"critic": 1.0,
"researcher": 0.8,
"scientist": 1.0,
"optimizer": 0.8,
}
def _count_steps(text: str) -> int:
clean = re.sub(r"```(?:json)?\n?", "", text).replace("```", "").strip()
m = re.search(r"\{[\s\S]*\}", clean)
if m:
try:
data = json.loads(m.group(0))
return len(data.get("steps", []))
except Exception:
pass
return 0
def score_response(task_type: str, response: str) -> Dict[str, float]:
"""Calcule les mΓ©triques pour une rΓ©ponse."""
metrics_def = TASK_METRICS.get(task_type, [])
scores = {}
total_weight = 0.0
weighted_sum = 0.0
for metric_name, scorer, weight in metrics_def:
try:
s = scorer(response)
except Exception:
s = 0.0
scores[metric_name] = round(s, 3)
weighted_sum += s * weight
total_weight += weight
scores["task_score"] = round(weighted_sum / total_weight, 4) if total_weight > 0 else 0.0
return scores
# ─────────────────────────────────────────────
# Génération avec le modèle évalué
# ─────────────────────────────────────────────
class ModelEvaluator:
def __init__(self, model_path: str, base_model: str, use_gguf: bool = False):
self.model_path = model_path
self.base_model = base_model
self.use_gguf = use_gguf or model_path.endswith(".gguf")
self._model = None
self._tokenizer = None
self._llama = None
self._load()
def _load(self):
if self.use_gguf:
from llama_cpp import Llama
self._llama = Llama(model_path=self.model_path, n_ctx=2048, n_gpu_layers=35, verbose=False)
log.info(f"[Eval] GGUF chargΓ© : {self.model_path}")
else:
import torch
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
log.info(f"[Eval] Chargement LoRA depuis : {self.model_path}")
bnb = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.bfloat16)
self._tokenizer = AutoTokenizer.from_pretrained(self.model_path, trust_remote_code=True)
base = AutoModelForCausalLM.from_pretrained(
self.base_model,
quantization_config = bnb,
device_map = "auto",
trust_remote_code = True,
torch_dtype = torch.bfloat16,
)
self._model = PeftModel.from_pretrained(base, self.model_path)
self._model.eval()
log.info(f"[Eval] Modèle LoRA chargé ({self._model.num_parameters():,} params)")
def generate(self, system: str, user: str, max_tokens: int = 512, temperature: float = 0.1) -> str:
if self.use_gguf and self._llama:
out = self._llama.create_chat_completion(
messages=[{"role": "system", "content": system},
{"role": "user", "content": user}],
max_tokens=max_tokens, temperature=max(temperature, 0.01),
)
return out["choices"][0]["message"]["content"].strip()
import torch
messages = [{"role": "system", "content": system}, {"role": "user", "content": user}]
text = self._tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
inputs = self._tokenizer(text, return_tensors="pt").to(self._model.device)
with torch.no_grad():
out = self._model.generate(
**inputs, max_new_tokens=max_tokens, temperature=temperature,
do_sample=True, pad_token_id=self._tokenizer.eos_token_id,
)
new_ids = out[0][inputs.input_ids.shape[1]:]
return self._tokenizer.decode(new_ids, skip_special_tokens=True).strip()
# ─────────────────────────────────────────────
# Pipeline d'Γ©valuation
# ─────────────────────────────────────────────
def run_evaluation(args: argparse.Namespace) -> Dict[str, Any]:
# Charger les exemples de test depuis le dataset
from build_dataset import SYSTEMS
all_examples: List[Dict] = []
with open(args.dataset) as f:
for line in f:
line = line.strip()
if line:
try:
all_examples.append(json.loads(line))
except Exception:
pass
# SΓ©lectionner n exemples Γ©quilibrΓ©s par tΓ’che
by_task: Dict[str, List] = {}
for ex in all_examples:
t = ex.get("task_type", "unknown")
by_task.setdefault(t, []).append(ex)
test_examples = []
n_per_task = max(1, args.n // len(SYSTEMS))
for task, exs in by_task.items():
test_examples.extend(random.sample(exs, min(n_per_task, len(exs))))
log.info(f"Γ‰valuation sur {len(test_examples)} exemples")
# Charger le modèle
evaluator = ModelEvaluator(args.model, args.base_model, use_gguf=args.gguf)
# Γ‰valuation
results_by_task: Dict[str, List[Dict]] = {}
total_latency = 0.0
for i, ex in enumerate(test_examples):
task_type = ex.get("task_type", "unknown")
messages = ex["messages"]
system = messages[0]["content"]
user = messages[1]["content"]
reference = messages[2]["content"]
t0 = time.time()
response = evaluator.generate(system, user, max_tokens=600, temperature=0.1)
latency = time.time() - t0
total_latency += latency
scores = score_response(task_type, response)
scores["latency_s"] = round(latency, 2)
# Comparaison avec la rΓ©fΓ©rence (longueur relative)
ref_len = len(reference.split())
resp_len = len(response.split())
scores["length_ratio"] = round(resp_len / max(ref_len, 1), 2)
results_by_task.setdefault(task_type, []).append({
"user": user[:150],
"response": response[:400],
"scores": scores,
})
if (i + 1) % 10 == 0:
log.info(f" Progression : {i+1}/{len(test_examples)}")
# AgrΓ©gation
summary: Dict[str, Any] = {
"model": args.model,
"base_model": args.base_model,
"n_examples": len(test_examples),
"avg_latency_s": round(total_latency / len(test_examples), 2),
"tasks": {},
"global_score": 0.0,
}
weighted_scores = []
for task_type, results in results_by_task.items():
task_scores = [r["scores"]["task_score"] for r in results]
avg_score = sum(task_scores) / len(task_scores)
weight = TASK_WEIGHTS.get(task_type, 1.0)
# DΓ©tail par mΓ©trique
metric_avgs = {}
for metric_name, _, _ in TASK_METRICS.get(task_type, []):
vals = [r["scores"].get(metric_name, 0.0) for r in results]
metric_avgs[metric_name] = round(sum(vals) / len(vals), 3)
summary["tasks"][task_type] = {
"n": len(results),
"avg_score": round(avg_score, 4),
"weight": weight,
"metrics": metric_avgs,
}
weighted_scores.append(avg_score * weight)
log.info(f" [{task_type:12s}] score={avg_score:.4f} n={len(results)}")
total_weight = sum(TASK_WEIGHTS.get(t, 1.0) for t in results_by_task)
summary["global_score"] = round(sum(weighted_scores) / total_weight, 4) if total_weight else 0.0
log.info(f"\n{'='*50}")
log.info(f"Score global : {summary['global_score']:.4f}")
log.info(f"Latence moy. : {summary['avg_latency_s']:.2f}s")
log.info(f"{'='*50}")
# Sauvegarde
out_path = Path(args.output)
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path.write_text(json.dumps(summary, ensure_ascii=False, indent=2))
log.info(f"βœ… RΓ©sultats β†’ {args.output}")
return summary
def main():
parser = argparse.ArgumentParser(description="Évaluation du modèle VORTEX GOD")
parser.add_argument("--model", required=True, help="Chemin checkpoint LoRA ou .gguf")
parser.add_argument("--dataset", default="training/data/full_dataset.jsonl")
parser.add_argument("--base-model", default="Qwen/Qwen3-8B-Instruct")
parser.add_argument("--n", type=int, default=50, help="Nb exemples de test")
parser.add_argument("--output", default="training/eval_results.json")
parser.add_argument("--gguf", action="store_true", help="Utiliser le mode GGUF")
args = parser.parse_args()
run_evaluation(args)
if __name__ == "__main__":
main()