reformulatee / src /eval /evaluate.py
fmrod
deploy: docs atualizadas
c31002d
"""
Pipeline de avaliacao da Fase 0.
Gate: AUC > 0.70 sobre as 100 questoes curadas.
Nota sobre nao_trivialidade: eh uma metrica relacional (Q vs Q0) e nao faz
sentido em avaliacao standalone de questoes individuais. O score aqui usa
apenas respondibilidade + tratabilidade, que sao propriedades intrinsecas de Q.
nao_trivialidade entra apenas no loop RLHF quando temos pares (Q0, Q*).
Uso:
python -m src.eval.evaluate [--no-tratabilidade] [--output results.json]
"""
from __future__ import annotations
import argparse
import json
import os
import time
from pathlib import Path
import numpy as np
from dotenv import load_dotenv
from sklearn.metrics import classification_report
from sklearn.metrics import roc_auc_score
from tqdm import tqdm
load_dotenv(override=True)
from src.corpus.fetch import fetch_corpus
from src.corpus.index import CorpusIndex
from src.corpus.index import build_index
from src.ee.respondibilidade import respondibilidade
from src.ee.tratabilidade import tratabilidade
from src.eval.curated import CuratedQuestion
from src.eval.curated import get_curated
# Pesos normalizados para avaliacao standalone (sem nao_trivialidade)
# B1 + B2 = 1.0
_B1 = 0.5 # respondibilidade
_B2 = 0.5 # tratabilidade
def score_question(
q: CuratedQuestion,
index: CorpusIndex,
use_tratabilidade: bool = True,
) -> dict:
resp = respondibilidade(q.text, index, top_k=10)
if use_tratabilidade:
tract_out = tratabilidade(q.text)
tract = tract_out["prob_tractable"] * tract_out["confidence"]
trajectory = tract_out.get("trajectory", "")
else:
tract = 0.5 # valor neutro quando API indisponivel
trajectory = "unknown"
# Score standalone: apenas respondibilidade + tratabilidade
# nao_trivialidade entra apenas no loop RLHF com pares (Q0, Q*)
ee = _B1 * resp + _B2 * tract
return {
"text": q.text,
"label": q.label,
"domain": q.domain,
"respondibilidade": round(resp, 4),
"tratabilidade": round(tract, 4),
"trajectory": trajectory,
"ee": round(ee, 4),
}
def evaluate(
corpus_dir: Path,
use_tratabilidade: bool = True,
output_path: Path | None = None,
delay_between_calls: float = 0.3,
) -> dict:
questions = get_curated()
fetch_corpus(corpus_dir)
index = build_index(corpus_dir)
results = []
for q in tqdm(questions, desc="Avaliando questoes"):
r = score_question(q, index, use_tratabilidade=use_tratabilidade)
results.append(r)
if use_tratabilidade:
time.sleep(delay_between_calls)
labels = np.array([r["label"] for r in results])
scores = np.array([r["ee"] for r in results])
auc = roc_auc_score(labels, scores)
threshold = float(np.median(scores))
preds = (scores >= threshold).astype(int)
report = classification_report(
labels, preds, target_names=["baixa_EE", "alta_EE"], output_dict=True
)
summary = {
"auc": round(auc, 4),
"gate_passed": auc >= 0.70,
"threshold_used": round(threshold, 4),
"n_questions": len(questions),
"classification_report": report,
"per_question": results,
}
print(f"\n{'='*50}")
print(f" AUC = {auc:.4f} | Gate (>= 0.70): {'PASSOU' if auc >= 0.70 else 'FALHOU'}")
print(f"{'='*50}")
print(f" Threshold (mediana): {threshold:.4f}")
print(f" Precision alta_EE: {report['alta_EE']['precision']:.3f}")
print(f" Recall alta_EE: {report['alta_EE']['recall']:.3f}")
if output_path:
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_text(json.dumps(summary, indent=2, ensure_ascii=False), encoding="utf-8")
print(f"\n Resultados salvos em: {output_path}")
return summary
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Fase 0 — avaliacao de EE(Q)")
parser.add_argument(
"--no-tratabilidade",
action="store_true",
help="Desabilita chamadas API (usa valor neutro 0.5)",
)
parser.add_argument(
"--output",
type=str,
default="data/results/fase0_eval.json",
help="Caminho para salvar resultados JSON",
)
args = parser.parse_args()
corpus_dir = Path(os.getenv("CORPUS_DIR", "data/corpus"))
output_path = Path(args.output)
evaluate(
corpus_dir=corpus_dir,
use_tratabilidade=not args.no_tratabilidade,
output_path=output_path,
)