| import os |
| import io |
| import json |
| import tarfile |
| import urllib.request |
| import logging |
| import argparse |
| from pathlib import Path |
| from src.config import settings |
| from src.detection.nli_scorer import NLIScorer |
|
|
| logging.basicConfig(level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s") |
| logger = logging.getLogger(__name__) |
|
|
| DATA_DIR = Path("data/scifact") |
| RESULTS_DIR = Path("evaluation/results") |
|
|
| def download_scifact_dataset(): |
| """Download and extract the SciFact dataset if it doesn't exist.""" |
| if DATA_DIR.exists() and ((DATA_DIR / "corpus.jsonl").exists() or (DATA_DIR / "data" / "corpus.jsonl").exists()): |
| logger.info("SciFact dataset already exists locally.") |
| return |
| |
| url = "https://scifact.s3-us-west-2.amazonaws.com/release/latest/data.tar.gz" |
| logger.info(f"Downloading SciFact dataset from {url}...") |
| |
| os.makedirs(DATA_DIR, exist_ok=True) |
| try: |
| with urllib.request.urlopen(url) as response: |
| with tarfile.open(fileobj=io.BytesIO(response.read()), mode="r:gz") as tar: |
| tar.extractall(path=DATA_DIR) |
| logger.info("SciFact dataset downloaded and extracted successfully.") |
| except Exception as e: |
| logger.error(f"Failed to download SciFact dataset: {e}") |
| raise |
|
|
| def load_corpus() -> dict[int, list[str]]: |
| """Load doc_id -> list of abstract sentences from corpus.jsonl.""" |
| corpus = {} |
| corpus_path = DATA_DIR / "corpus.jsonl" |
| |
| if not corpus_path.exists(): |
| corpus_path = DATA_DIR / "data" / "corpus.jsonl" |
| |
| with open(corpus_path, "r", encoding="utf-8") as f: |
| for line in f: |
| doc = json.loads(line) |
| corpus[doc["doc_id"]] = doc["abstract"] |
| |
| return corpus |
|
|
| def load_claim_evidence_pairs(corpus: dict[int, list[str]]) -> tuple[list[tuple[str, str]], list[str]]: |
| """Load claim-evidence sentence pairs and their gold labels (SUPPORT/CONTRADICT).""" |
| claims_path = DATA_DIR / "claims_dev.jsonl" |
| if not claims_path.exists(): |
| claims_path = DATA_DIR / "data" / "claims_dev.jsonl" |
| |
| pairs = [] |
| labels = [] |
| |
| with open(claims_path, "r", encoding="utf-8") as f: |
| for line in f: |
| claim = json.loads(line) |
| claim_text = claim["claim"] |
| evidence = claim.get("evidence", {}) |
| |
| for doc_id_str, ev_list in evidence.items(): |
| doc_id = int(doc_id_str) |
| abstract = corpus.get(doc_id) |
| if not abstract: |
| continue |
| |
| for ev_set in ev_list: |
| label = ev_set["label"] |
| sent_indices = ev_set["sentences"] |
| |
| |
| evidence_text = " ".join([abstract[i] for i in sent_indices]) |
| pairs.append((claim_text, evidence_text)) |
| labels.append(label) |
| |
| return pairs, labels |
|
|
| def main(): |
| parser = argparse.ArgumentParser( |
| description="Evaluate the RSCE NLI contradiction detector on the SciFact dev benchmark.", |
| formatter_class=argparse.ArgumentDefaultsHelpFormatter, |
| ) |
| parser.add_argument( |
| "--limit", |
| type=int, |
| default=0, |
| help="Number of pairs to evaluate. 0 = full dataset (recommended for README reporting).", |
| ) |
| parser.add_argument( |
| "--threshold", |
| type=float, |
| default=None, |
| help=( |
| "NLI contradiction threshold override. " |
| "Defaults to settings.nli_contradiction_threshold from config." |
| ), |
| ) |
| args = parser.parse_args() |
|
|
| |
| threshold = args.threshold if args.threshold is not None else settings.nli_contradiction_threshold |
|
|
|
|
| download_scifact_dataset() |
| corpus = load_corpus() |
| pairs, labels = load_claim_evidence_pairs(corpus) |
| |
| if args.limit > 0: |
| pairs = pairs[:args.limit] |
| labels = labels[:args.limit] |
| logger.info(f"Evaluating a representative sample of {len(pairs)} claim-evidence pairs from SciFact dev set.") |
| else: |
| logger.info(f"Evaluating all {len(pairs)} claim-evidence pairs from SciFact dev set.") |
| |
| |
| logger.info("Initializing NLIScorer and evaluating pairs...") |
| scorer = NLIScorer() |
| nli_results = scorer.score_pairs(pairs) |
| |
| |
| tp, fp, fn, tn = 0, 0, 0, 0 |
| |
| for res, true_label in zip(nli_results, labels): |
| |
| pred_contradict = res.contradiction >= threshold |
| is_true_contradict = true_label == "CONTRADICT" |
| |
| if pred_contradict and is_true_contradict: |
| tp += 1 |
| elif pred_contradict and not is_true_contradict: |
| fp += 1 |
| elif not pred_contradict and is_true_contradict: |
| fn += 1 |
| else: |
| tn += 1 |
| |
| 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 |
| |
| |
| print(f"\nSciFact Evaluation Results (Threshold: {threshold}):") |
| print("==================================================") |
| print(f"Total Pairs Evaluated: {len(pairs)}") |
| print(f"True Positives (TP): {tp}") |
| print(f"False Positives (FP): {fp}") |
| print(f"False Negatives (FN): {fn}") |
| print(f"True Negatives (TN): {tn}") |
| print(f"Precision: {precision:.4f} (Target: >= 70%)") |
| print(f"Recall: {recall:.4f} (Target: >= 55%)") |
| print(f"F1-Score: {f1:.4f}") |
| |
| |
| os.makedirs(RESULTS_DIR, exist_ok=True) |
| |
| suffix = f"_n{len(pairs)}" if args.limit > 0 else "_full" |
| results_path = RESULTS_DIR / f"scifact_results{suffix}.json" |
| |
| results_data = { |
| "threshold": threshold, |
| "total_pairs": len(pairs), |
| "true_positives": tp, |
| "false_positives": fp, |
| "false_negatives": fn, |
| "true_negatives": tn, |
| "precision": precision, |
| "recall": recall, |
| "f1_score": f1 |
| } |
| |
| with open(results_path, "w", encoding="utf-8") as f: |
| json.dump(results_data, f, indent=2) |
| |
| logger.info(f"Results successfully saved to {results_path}") |
|
|
| if __name__ == "__main__": |
| main() |
|
|