dialectica / scripts /audit_labels.py
Kattine
Phase 4b: DistilBERT in-domain 0.952, OOD 0.916, degradation halved vs LogReg
25cfe3f
Raw
History Blame Contribute Delete
5.08 kB
"""Phase 3 label audit.
Samples questions, hides the generated label, asks the user to label each one,
then reports agreement between human and generated labels.
Usage:
python scripts/audit_labels.py
python scripts/audit_labels.py --n 50 --source data/raw/questions.jsonl
"""
import argparse
import json
import os
import random
ORDERED_LABELS = ["Surface", "Mechanistic", "Critical"]
INPUT_MAP = {
"s": "Surface", "surface": "Surface",
"m": "Mechanistic", "mechanistic": "Mechanistic",
"c": "Critical", "critical": "Critical",
}
DEFINITIONS = {
"Surface": "recall a fact/definition, or explain/interpret a single concept",
"Mechanistic": "apply a method, or analyse/compare/trace mechanism",
"Critical": "evaluate, critique, weigh trade-offs, or design something new",
}
def load_records(source):
"""Load all records from a JSONL file."""
records = []
with open(source, "r", encoding="utf-8") as handle:
for line in handle:
line = line.strip()
if line:
records.append(json.loads(line))
return records
def stratified_sample(records, n, seed):
"""Sample n records with roughly equal counts per class."""
random.seed(seed)
by_class = {label: [] for label in ORDERED_LABELS}
for record in records:
label = record.get("bloom_class")
if label in by_class:
by_class[label].append(record)
per_class = max(1, n // len(ORDERED_LABELS))
sample = []
for label in ORDERED_LABELS:
pool = by_class[label]
random.shuffle(pool)
sample.extend(pool[:per_class])
random.shuffle(sample)
return sample
def prompt_one(question, index, total):
"""Show a question and collect a blind label, or None to quit."""
print(f"\n[{index}/{total}] {question}")
print(" Which cognitive level?")
for label in ORDERED_LABELS:
print(f" {label[0].lower()} = {label:12s} ({DEFINITIONS[label]})")
while True:
choice = input(" your label [s/m/c, or q to stop]: ").strip().lower()
if choice == "q":
return None
if choice in INPUT_MAP:
return INPUT_MAP[choice]
print(" please enter s, m, c, or q")
def summarise(annotations):
"""Print and return agreement stats."""
if not annotations:
print("\nNo items labelled.")
return None
total = len(annotations)
agree = sum(1 for a in annotations if a["human_label"] == a["true_label"])
overall = agree / total
print("\n" + "=" * 46)
print(f"Label fidelity audit (n = {total})")
print(f" overall agreement : {overall:.1%}")
print(f"\n {'class':12s} {'agree':>6s} {'n':>4s}")
per_class = {}
for label in ORDERED_LABELS:
subset = [a for a in annotations if a["true_label"] == label]
if subset:
hits = sum(1 for a in subset if a["human_label"] == label)
rate = hits / len(subset)
per_class[label] = {"agreement": rate, "n": len(subset)}
print(f" {label:12s} {rate:6.1%} {len(subset):4d}")
else:
per_class[label] = {"agreement": None, "n": 0}
print("=" * 46)
return {"overall_agreement": overall, "per_class": per_class, "n": total}
def main():
"""Run the interactive audit and save results."""
parser = argparse.ArgumentParser(description="Label-fidelity audit")
parser.add_argument("--source", default="data/raw/questions.jsonl",
help="JSONL to sample from")
parser.add_argument("--n", type=int, default=50, help="sample size")
parser.add_argument("--seed", type=int, default=7, help="sampling seed")
parser.add_argument("--output", default="data/outputs/label_fidelity.json")
args = parser.parse_args()
if not os.path.exists(args.source):
fallback = "data/processed/train.jsonl"
print(f"{args.source} not found, falling back to {fallback}")
args.source = fallback
records = load_records(args.source)
sample = stratified_sample(records, args.n, args.seed)
print(f"Sampled {len(sample)} questions from {args.source}.")
print("Label each by cognitive intent. The generated label is hidden.\n")
annotations = []
for i, record in enumerate(sample, start=1):
human = prompt_one(record["question"], i, len(sample))
if human is None:
print("Stopping early.")
break
annotations.append({
"question": record["question"],
"true_label": record["bloom_class"],
"human_label": human,
"agree": human == record["bloom_class"],
})
summary = summarise(annotations)
if summary is not None:
os.makedirs(os.path.dirname(args.output), exist_ok=True)
with open(args.output, "w", encoding="utf-8") as handle:
json.dump({"summary": summary, "annotations": annotations},
handle, indent=2, ensure_ascii=False)
print(f"\nSaved -> {args.output}")
if __name__ == "__main__":
main()