FakeNews-XAI / evaluation /run_benchmark.py
Marius16's picture
Replace Ollama/llama3 with spacy-llm + Qwen3-1.7B (Pipeline B + Explainer), archive REBEL (F1=0.038), fix TCS n_claims=0 score, add ISOT/RAGuard eval scripts, update start.sh and requirements
4a6ebd0
Raw
History Blame Contribute Delete
12.1 kB
"""Evaluare manuala pipeline TCS pe un set de articole cu ground truth.
Rulare: python evaluation/run_benchmark.py
Optiuni:
--wikidata Activeaza verificare Wikidata (necesita retea)
--threshold N Threshold TCS sub care articolul e prezis FAKE (default: 0.6)
--pipeline P Pipeline de folosit: spacy sau llm (default: spacy)
"""
from __future__ import annotations
import argparse
import json
import logging
import sys
import time
from datetime import datetime, date
from pathlib import Path
from typing import Optional
# Adauga project root in sys.path (necesar cand scriptul e rulat direct)
_PROJECT_ROOT = Path(__file__).parent.parent
if str(_PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(_PROJECT_ROOT))
logging.basicConfig(
level=logging.WARNING,
format="%(asctime)s %(name)-35s %(levelname)-5s %(message)s",
datefmt="%H:%M:%S",
)
logger = logging.getLogger("benchmark")
EVAL_DIR = Path(__file__).parent
BENCHMARK_FILE = EVAL_DIR / "benchmark_articles.json"
RESULTS_DIR = EVAL_DIR / "results"
FAKE_THRESHOLD = 0.70
# // section load
def load_benchmark(path: Path) -> list[dict]:
"""Incarca articolele de benchmark din JSON."""
if not path.exists():
logger.error(f"Fisier benchmark negasit: {path}")
sys.exit(1)
with open(path, encoding="utf-8") as f:
data = json.load(f)
logger.info(f"Incarcat {len(data)} articole din {path}")
return data
# // section run
def _resolve_spacy_model(requested: Optional[str]) -> Optional[str]:
"""Returneaza modelul spaCy de folosit: cel cerut (daca e instalat) sau primul disponibil."""
import spacy
installed = spacy.util.get_installed_models()
if not installed:
return None
if requested and requested in installed:
return requested
# Fallback: preferinta lg > sm > primul disponibil
for preferred in ("en_core_web_trf", "en_core_web_lg", "en_core_web_sm"):
if preferred in installed:
return preferred
return installed[0]
def run_benchmark(
articles: list[dict],
pipeline: str = "spacy",
use_wikidata: bool = False,
threshold: float = FAKE_THRESHOLD,
model_name: Optional[str] = None,
use_rss: bool = False,
) -> list[dict]:
"""Ruleaza pipeline-ul TCS pe fiecare articol si colecteaza rezultatele."""
from backend.pipeline.graph.models import Article
from backend.pipeline.orchestrator import PipelineOrchestrator
resolved_model = _resolve_spacy_model(model_name) if pipeline == "spacy" else model_name
if pipeline == "spacy" and resolved_model is None:
logger.error("Niciun model spaCy instalat. Instaleaza cu: python -m spacy download en_core_web_sm")
sys.exit(1)
if resolved_model:
logger.info(f"Model spaCy: {resolved_model}")
orch = PipelineOrchestrator(
use_wikidata=use_wikidata,
extractor_name=pipeline,
model_name=resolved_model,
persistent_store=None,
use_rss=use_rss,
)
rows = []
for i, entry in enumerate(articles):
pub_date: Optional[datetime] = None
if entry.get("publication_date"):
try:
pub_date = datetime.strptime(entry["publication_date"], "%Y-%m-%d")
except ValueError:
pass
article = Article(
text=entry["text"],
title=entry.get("title", f"Article {i+1}"),
publication_date=pub_date,
source=entry.get("source", "benchmark"),
)
logger.info(f"[{i+1}/{len(articles)}] '{article.title[:60]}'")
t0 = time.monotonic()
try:
result = orch.run(article)
except Exception as e:
logger.error(f"Eroare pipeline pe '{article.title}': {e}", exc_info=True)
rows.append(_error_row(entry, i + 1))
continue
elapsed_ms = (time.monotonic() - t0) * 1000
expected_fake: bool = entry.get("expected_fake", False)
predicted_fake: bool = result.n_temporal_claims > 0 and result.score < threshold
outcome = _classify_outcome(expected_fake, predicted_fake)
rows.append({
"idx": i + 1,
"title": article.title,
"tcs": round(result.score, 4),
"tcs_label": result.label,
"n_claims": result.n_temporal_claims,
"n_inconsistencies": result.n_inconsistencies,
"coherence_factor": round(result.coherence_factor, 4),
"expected_fake": expected_fake,
"predicted_fake": predicted_fake,
"outcome": outcome,
"known_inconsistencies": entry.get("known_inconsistencies", []),
"detected_inconsistencies": [
{
"type": inc.inconsistency_type.value,
"severity": inc.severity.value,
"description": inc.description,
}
for inc in result.inconsistencies
],
"processing_time_ms": round(elapsed_ms, 1),
"source": entry.get("source", ""),
})
return rows
def _classify_outcome(expected_fake: bool, predicted_fake: bool) -> str:
if expected_fake and predicted_fake:
return "TP"
if not expected_fake and not predicted_fake:
return "TN"
if not expected_fake and predicted_fake:
return "FP"
return "FN"
def _error_row(entry: dict, idx: int) -> dict:
return {
"idx": idx,
"title": entry.get("title", f"Article {idx}"),
"tcs": 0.0,
"tcs_label": "error",
"n_claims": 0,
"n_inconsistencies": 0,
"coherence_factor": 0.0,
"expected_fake": entry.get("expected_fake", False),
"predicted_fake": False,
"outcome": "ERROR",
"known_inconsistencies": entry.get("known_inconsistencies", []),
"detected_inconsistencies": [],
"processing_time_ms": 0.0,
"source": entry.get("source", ""),
}
# // section metrics
def compute_metrics(rows: list[dict]) -> dict:
"""Calculeaza Precision, Recall, F1 pe baza clasificarilor TP/FP/FN/TN."""
tp = sum(1 for r in rows if r["outcome"] == "TP")
tn = sum(1 for r in rows if r["outcome"] == "TN")
fp = sum(1 for r in rows if r["outcome"] == "FP")
fn = sum(1 for r in rows if r["outcome"] == "FN")
precision = tp / (tp + fp) if (tp + fp) > 0 else 0.0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0.0
f1 = 2 * precision * recall / (precision + recall) if (precision + recall) > 0 else 0.0
accuracy = (tp + tn) / len(rows) if rows else 0.0
avg_tcs_true = _avg_tcs(rows, expected_fake=False)
avg_tcs_fake = _avg_tcs(rows, expected_fake=True)
return {
"total": len(rows),
"tp": tp, "tn": tn, "fp": fp, "fn": fn,
"precision": round(precision, 4),
"recall": round(recall, 4),
"f1": round(f1, 4),
"accuracy": round(accuracy, 4),
"avg_tcs_true_articles": round(avg_tcs_true, 4) if avg_tcs_true is not None else None,
"avg_tcs_fake_articles": round(avg_tcs_fake, 4) if avg_tcs_fake is not None else None,
}
def _avg_tcs(rows: list[dict], expected_fake: bool) -> Optional[float]:
subset = [r["tcs"] for r in rows if r["expected_fake"] == expected_fake and r["outcome"] != "ERROR"]
return sum(subset) / len(subset) if subset else None
# // section print_table
def print_table(rows: list[dict], threshold: float) -> None:
"""Afiseaza tabelul de rezultate in consola."""
title_w = 42
sep = "-" * (title_w + 62)
print(f"\n{'='*(title_w + 62)}")
print(f" TCS BENCHMARK | threshold = {threshold:.2f} | fake = TCS < {threshold:.2f}")
print(f"{'='*(title_w + 62)}")
print(
f" {'#':>2} {'Title':<{title_w}} {'TCS':>6} "
f"{'Exp':>4} {'Pred':>5} {'Out':>4} {'Inc':>4} {'ms':>6}"
)
print(sep)
for r in rows:
exp_str = "FAKE" if r["expected_fake"] else "TRUE"
pred_str = "FAKE" if r["predicted_fake"] else "TRUE"
outcome = r["outcome"]
marker = " " if outcome in ("TP", "TN") else "*"
title_short = r["title"][:title_w]
print(
f"{marker} {r['idx']:>2} {title_short:<{title_w}} {r['tcs']:>6.3f} "
f"{exp_str:>4} {pred_str:>5} {outcome:>4} "
f"{r['n_inconsistencies']:>4} {r['processing_time_ms']:>6.0f}"
)
print(sep)
print(" * = misclassified\n")
def print_metrics(metrics: dict, threshold: float) -> None:
"""Afiseaza metricile de evaluare."""
print(f" Metrics (threshold TCS < {threshold:.2f} = FAKE predicted):")
print(f" {'-'*40}")
print(f" TP={metrics['tp']} TN={metrics['tn']} FP={metrics['fp']} FN={metrics['fn']}")
print(f" Precision : {metrics['precision']:.4f}")
print(f" Recall : {metrics['recall']:.4f}")
print(f" F1 : {metrics['f1']:.4f}")
print(f" Accuracy : {metrics['accuracy']:.4f}")
if metrics["avg_tcs_true_articles"] is not None:
print(f" Avg TCS (TRUE articles) : {metrics['avg_tcs_true_articles']:.4f}")
if metrics["avg_tcs_fake_articles"] is not None:
print(f" Avg TCS (FAKE articles) : {metrics['avg_tcs_fake_articles']:.4f}")
print()
def print_inconsistency_details(rows: list[dict]) -> None:
"""Afiseaza detalii inconsistente detectate vs asteptate pentru fiecare articol."""
print(" Inconsistency details:")
print(f" {'─'*60}")
for r in rows:
print(f" [{r['idx']}] {r['title'][:55]}")
if r["known_inconsistencies"]:
for k in r["known_inconsistencies"]:
print(f" expected : {k}")
if r["detected_inconsistencies"]:
for d in r["detected_inconsistencies"]:
print(f" detected : [{d['severity']}] {d['type']}: {d['description'][:70]}")
if not r["known_inconsistencies"] and not r["detected_inconsistencies"]:
print(" (no inconsistencies expected or found)")
print()
# // section save
def save_results(rows: list[dict], metrics: dict, pipeline: str, threshold: float) -> Path:
"""Salveaza rezultatele in evaluation/results/results_YYYY-MM-DD.json."""
RESULTS_DIR.mkdir(parents=True, exist_ok=True)
today = date.today().strftime("%Y-%m-%d")
output_path = RESULTS_DIR / f"results_{today}.json"
payload = {
"generated_at": datetime.now().isoformat(),
"pipeline": pipeline,
"fake_threshold": threshold,
"metrics": metrics,
"articles": rows,
}
with open(output_path, "w", encoding="utf-8") as f:
json.dump(payload, f, indent=2, ensure_ascii=False, default=str)
return output_path
# // section main
def main() -> None:
parser = argparse.ArgumentParser(description="TCS Pipeline Benchmark")
parser.add_argument("--wikidata", action="store_true", help="Activeaza verificare Wikidata")
parser.add_argument("--threshold", type=float, default=FAKE_THRESHOLD, help="Threshold TCS fake (default: 0.6)")
parser.add_argument("--pipeline", choices=["spacy", "llm"], default="spacy", help="Pipeline extractor")
parser.add_argument("--model", type=str, default=None, help="Model spaCy/LLM explicit (default: auto-detect)")
parser.add_argument("--rss", action="store_true", help="Enable RSS Stream")
args = parser.parse_args()
print(f"\nBenchmark: pipeline={args.pipeline}, wikidata={args.wikidata}, threshold={args.threshold}")
print(f"Fisier: {BENCHMARK_FILE}\n")
articles = load_benchmark(BENCHMARK_FILE)
rows = run_benchmark(
articles,
pipeline=args.pipeline,
use_wikidata=args.wikidata,
threshold=args.threshold,
model_name=args.model,
use_rss=args.rss,
)
metrics = compute_metrics(rows)
print_table(rows, args.threshold)
print_metrics(metrics, args.threshold)
print_inconsistency_details(rows)
output_path = save_results(rows, metrics, args.pipeline, args.threshold)
print(f" Rezultate salvate: {output_path}")
if __name__ == "__main__":
main()