RSCE / evaluation /scifact_eval.py
LP1205's picture
Deploy RSCE backend to Hugging Face Space
5e952bb
Raw
History Blame Contribute Delete
6.85 kB
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"
# Fallback to subdirectory if nested in extraction
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"] # "SUPPORT" or "CONTRADICT"
sent_indices = ev_set["sentences"]
# Concatenate the evidence 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()
# Allow CLI --threshold to override the value in settings/config
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.")
# 2. Run NLI scoring on the pairs
logger.info("Initializing NLIScorer and evaluating pairs...")
scorer = NLIScorer()
nli_results = scorer.score_pairs(pairs)
# 3. Compute precision, recall, and F1 metrics for CONTRADICT (REFUTES) label
tp, fp, fn, tn = 0, 0, 0, 0
for res, true_label in zip(nli_results, labels):
# We classify as CONTRADICT if contradiction score meets threshold
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 results to stdout
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}")
# 4. Save results to evaluation/results/scifact_results.json
os.makedirs(RESULTS_DIR, exist_ok=True)
# Name the result file to reflect whether this was a full-dataset or sample run
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()