claim-ai / src /eval_scitail.py
MINOO LA
initial commit β€” clAIm v2.0 clean
cd91c77
Raw
History Blame Contribute Delete
11.4 kB
"""
eval_scitail.py β€” v1
Zero-shot out-of-domain evaluation of the SciFact-fine-tuned DeBERTa model
on the SciTail test set.
Purpose: confirm that the MultiNLI β†’ SciFact transfer learned generalizable
scientific reasoning, not just SciFact-specific claim memorization.
No retraining. No fine-tuning on SciTail. Pure zero-shot transfer.
═══════════════════════════════════════════════════════════════════
PRE-FLIGHT (run these cells in Colab before executing this script)
═══════════════════════════════════════════════════════════════════
# 1. Mount Drive (if not already mounted)
from google.colab import drive
drive.mount('/content/drive')
# 2. Verify the best SciFact model is present
import os
path = '/content/drive/MyDrive/scifact_checkpoint/buss305-scifact-bestmodel'
print(os.listdir(path)) # should show model.safetensors, config.json etc.
# 3. Run:
!python eval_scitail.py
═══════════════════════════════════════════════════════════════════
Label compatibility note:
SciTail is binary: "entailment" / "neutral"
Our model has 3 outputs: SUPPORT(0) / NEI(1) / CONTRADICT(2)
Mapping:
SciTail "entailment" β†’ our SUPPORT (0)
SciTail "neutral" β†’ our NEI (1)
SciTail has no contradiction examples by design (it was built from
science exam questions, not adversarial pairs). We report:
1. Binary F1 (SUPPORT vs NEI) β€” the directly comparable metric
2. How often the model fires CONTRADICT on SciTail β€” should be low;
a high CONTRADICT rate would indicate the model is confused
3. Macro-F1 over all 3 model classes for completeness
A strong binary F1 (>0.70) with low CONTRADICT rate (<15%) confirms
genuine transfer of scientific NLI reasoning.
"""
import numpy as np
import torch
from datasets import load_dataset
from transformers import AutoTokenizer, AutoModelForSequenceClassification
from sklearn.metrics import (
f1_score, classification_report, confusion_matrix
)
# ══════════════════════════════════════════════════════════════════════════════
# CONFIG
# ══════════════════════════════════════════════════════════════════════════════
SCIFACT_MODEL = "/content/drive/MyDrive/scifact_checkpoint/buss305-scifact-bestmodel"
MAX_LENGTH = 256
BATCH_SIZE = 32 # eval only β€” can be larger than training batch
# Our model's label scheme
ID2LABEL = {0: "SUPPORT", 1: "NOT_ENOUGH_INFO", 2: "CONTRADICT"}
# SciTail β†’ our label mapping
SCITAIL_LABEL_MAP = {
"entailment": 0, # entailment β†’ SUPPORT
"neutral": 1, # neutral β†’ NEI
}
# ══════════════════════════════════════════════════════════════════════════════
# LOAD SCITAIL TEST SET
# ══════════════════════════════════════════════════════════════════════════════
print("Loading SciTail test set …")
# snli_format gives us: sentence1 (premise), sentence2 (hypothesis), gold_label
scitail = load_dataset("allenai/scitail", "snli_format", split="test")
print(f" SciTail test rows : {len(scitail)}")
print(f" Columns : {scitail.column_names}")
print(f" Label values : {set(scitail['gold_label'])}")
# Filter to known labels only (drop any rows with unexpected label strings)
scitail = scitail.filter(lambda x: x["gold_label"] in SCITAIL_LABEL_MAP)
print(f" Rows after filter : {len(scitail)}")
# Map to our integer labels
true_labels = [SCITAIL_LABEL_MAP[x] for x in scitail["gold_label"]]
from collections import Counter
dist = Counter(true_labels)
print(f" Label distribution: SUPPORT={dist[0]} NEI={dist[1]}")
# ══════════════════════════════════════════════════════════════════════════════
# LOAD MODEL + TOKENIZER
# ══════════════════════════════════════════════════════════════════════════════
print(f"\nLoading model from {SCIFACT_MODEL} …")
tokenizer = AutoTokenizer.from_pretrained(SCIFACT_MODEL)
model = AutoModelForSequenceClassification.from_pretrained(SCIFACT_MODEL)
model.eval()
device = torch.device("cuda" if torch.cuda.is_available() else "cpu")
model.to(device)
print(f" Device: {device}")
print(f" Model labels: {model.config.id2label}")
# ══════════════════════════════════════════════════════════════════════════════
# INFERENCE
# ══════════════════════════════════════════════════════════════════════════════
print("\nRunning inference on SciTail test set …")
premises = scitail["sentence1"]
hypotheses = scitail["sentence2"]
all_preds = []
all_logits = []
with torch.no_grad():
for i in range(0, len(premises), BATCH_SIZE):
batch_p = premises[i : i + BATCH_SIZE]
batch_h = hypotheses[i : i + BATCH_SIZE]
enc = tokenizer(
batch_p,
batch_h,
truncation=True,
max_length=MAX_LENGTH,
padding=True,
return_tensors="pt",
)
enc.pop("token_type_ids", None) # DeBERTa-v3 doesn't use this
enc = {k: v.to(device) for k, v in enc.items()}
outputs = model(**enc)
logits = outputs.logits.cpu().numpy()
preds = np.argmax(logits, axis=-1)
all_preds.extend(preds.tolist())
all_logits.extend(logits.tolist())
if (i // BATCH_SIZE) % 10 == 0:
print(f" {i}/{len(premises)} …")
print(f" Done. {len(all_preds)} predictions made.")
# ══════════════════════════════════════════════════════════════════════════════
# RESULTS
# ══════════════════════════════════════════════════════════════════════════════
all_preds = np.array(all_preds)
true_arr = np.array(true_labels)
# ── 1. CONTRADICT rate ────────────────────────────────────────────────────────
# SciTail has no CONTRADICT examples. If model fires CONTRADICT frequently,
# it means it's confused or over-generalizing from SciFact's adversarial pairs.
contradict_count = (all_preds == 2).sum()
contradict_rate = contradict_count / len(all_preds)
print(f"\n── CONTRADICT rate (should be low) ──────────────────────────")
print(f" Model predicted CONTRADICT : {contradict_count} / {len(all_preds)} ({100*contradict_rate:.1f}%)")
if contradict_rate < 0.10:
print(f" βœ“ Low CONTRADICT rate β€” model is not over-triggering contradiction")
elif contradict_rate < 0.20:
print(f" ⚠ Moderate CONTRADICT rate β€” model somewhat over-triggers contradiction")
else:
print(f" βœ— High CONTRADICT rate β€” model confused on binary-only domain")
# ── 2. Binary F1: treat predictions as binary (collapse CONTRADICT β†’ NEI) ────
# For fair comparison, any CONTRADICT prediction is mapped to NEI (1),
# since SciTail has no contradiction class. The model is penalised for
# firing CONTRADICT β€” those examples become wrong NEI predictions.
binary_preds = np.where(all_preds == 0, 0, 1) # SUPPORT=0, everything else=1
binary_true = true_arr # already 0 or 1
binary_f1 = f1_score(binary_true, binary_preds, average="macro")
binary_accuracy = (binary_preds == binary_true).mean()
print(f"\n── Binary evaluation (SUPPORT vs NEI) ───────────────────────")
print(f" Macro-F1 : {binary_f1:.4f}")
print(f" Accuracy : {binary_accuracy:.4f}")
print()
print(classification_report(
binary_true, binary_preds,
target_names=["SUPPORT (entailment)", "NEI (neutral)"],
digits=4,
))
# ── 3. Full 3-class report (for completeness) ─────────────────────────────────
print(f"── Full 3-class prediction distribution ─────────────────────")
pred_dist = Counter(all_preds.tolist())
for lid in [0, 1, 2]:
print(f" {ID2LABEL[lid]:20s}: {pred_dist.get(lid, 0):5d} ({100*pred_dist.get(lid,0)/len(all_preds):.1f}%)")
# ── 4. Confusion matrix ───────────────────────────────────────────────────────
print(f"\n── Confusion matrix (rows=true, cols=predicted) ─────────────")
print(f" True labels: 0=SUPPORT(entailment) 1=NEI(neutral)")
print(f" Pred labels: 0=SUPPORT 1=NEI 2=CONTRADICT")
cm = confusion_matrix(true_arr, all_preds, labels=[0, 1, 2])
print(f" SUPPORT NEI CONTRADICT")
for i, row_name in enumerate(["SUPPORT(true)", "NEI(true) "]):
if i < len(cm):
print(f" {row_name}: {cm[i]}")
# ── 5. Interpretation ─────────────────────────────────────────────────────────
print(f"\n── Interpretation ───────────────────────────────────────────")
if binary_f1 >= 0.75:
print(f" βœ“ STRONG transfer (binary F1={binary_f1:.3f})")
print(f" Model generalises scientific NLI reasoning beyond SciFact.")
elif binary_f1 >= 0.60:
print(f" ~ MODERATE transfer (binary F1={binary_f1:.3f})")
print(f" Model shows partial generalisation. Some SciFact-specific patterns.")
else:
print(f" βœ— WEAK transfer (binary F1={binary_f1:.3f})")
print(f" Model may have overfit SciFact domain.")
print("\nβœ“ eval_scitail.py complete.")