File size: 7,009 Bytes
ac442f8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 | #!/usr/bin/env python3
"""Convert the shipping v6 dataset into the trainer's TokenEvalRow schema.
v6 (hallucination_labelling/DATASET_CARD_v6.md) ships in the labelling-pipeline
schema with THREE label sets per row -- the same panel votes under different
aggregation rules. This converter:
* selects ONE label set as `labels` (--labels, default `main` =
`token_labels` = sentence:2:union, the card's shipping labels) and carries
all three in metadata so the choice is auditable and reversible;
* preserves `extra` verbatim -- replay reads the generation context from
`extra.context` / `extra.knowledge`, and the card's per-arm analysis needs
`extra.arm`, `extra.prompt_regime` and `extra.n_judges_resolved`;
* REJECTS NOTHING on content. build_v6 already applied the card's exclusion
rules (4-gram degeneracy, panel < 2); adding conversion-time rules here
would silently fork the shipped resource. In particular, no uncertain-
fraction filter: 22% of v6 rows are majority-U under the union rule, and U
tokens are already excluded per-token at metric time;
* treats any structural defect as a HARD FAILURE of the whole conversion,
not a skipped row -- the shipping dataset is supposed to be perfect, and a
silent skip would hide a build regression.
Usage:
python scripts/convert_v6.py \
--input ~/Desktop/Datasets/hallucinations_custom/hallu_dataset_v6.jsonl \
--output ~/Desktop/Datasets/hallucinations_custom/hallu_v6_eval.jsonl \
--labels main
"""
from __future__ import annotations
import argparse
import collections
import hashlib
import json
from pathlib import Path
GROUNDED, HALLUCINATED, UNCERTAIN = 0, 1, 2
LETTER = {"G": GROUNDED, "H": HALLUCINATED, "U": UNCERTAIN}
VARIANT_FIELD = {"main": "token_labels", "strict": "token_labels_strict",
"plurality": "token_labels_plurality"}
TSTAR_FIELD = {"main": "t_star_index", "strict": "t_star_index_strict",
"plurality": None}
GENERATOR = "meta-llama/Llama-3.2-1B-Instruct"
def parse_args(argv=None):
p = argparse.ArgumentParser(description=__doc__.splitlines()[0])
p.add_argument("--input", type=Path, required=True)
p.add_argument("--output", type=Path, required=True)
p.add_argument("--labels", choices=sorted(VARIANT_FIELD), default="main",
help="which of the card's three label sets becomes `labels`")
return p.parse_args(argv)
def spans_from_labels(labels, offsets):
"""Contiguous H runs -> character spans; runs close on unusable offsets."""
spans, start_char, end_char = [], None, None
for index, label in enumerate(labels):
offset = offsets[index] if index < len(offsets) else None
usable = (isinstance(offset, (list, tuple)) and len(offset) == 2
and offset[0] is not None and int(offset[1]) > int(offset[0]))
if label == HALLUCINATED and usable:
if start_char is None:
start_char = int(offset[0])
end_char = int(offset[1])
elif start_char is not None:
spans.append({"start": start_char, "end": end_char})
start_char, end_char = None, None
if start_char is not None:
spans.append({"start": start_char, "end": end_char})
return spans
def convert(record, variant):
rid = record["id"]
def fail(reason):
raise SystemExit(f"STRUCTURAL DEFECT in shipping dataset at {rid}: {reason}. "
"Refusing to convert; the v6 build needs fixing, not a skip.")
token_ids = record.get("gen_token_ids") or fail("no gen_token_ids")
tokens = record.get("gen_tokens") or fail("no gen_tokens")
offsets = record.get("gen_token_offsets") or fail("no gen_token_offsets")
if record.get("generator_model") != GENERATOR:
fail(f"generator {record.get('generator_model')!r} != {GENERATOR!r}")
variants = {}
for name, field in VARIANT_FIELD.items():
raw = record.get(field) or fail(f"missing {field}")
if len(raw) != len(token_ids):
fail(f"{field} length {len(raw)} != spine {len(token_ids)}")
try:
variants[name] = [LETTER[v] for v in raw]
except KeyError as exc:
fail(f"unknown label value {exc} in {field}")
if len(offsets) != len(token_ids):
fail("offsets length != spine")
labels = variants[variant]
tstar_field = TSTAR_FIELD[variant]
t_star = record.get(tstar_field) if tstar_field else None
if t_star is None:
t_star = next((i for i, v in enumerate(labels) if v == HALLUCINATED), None)
return {
"id": rid,
# Bare question; the chat template and the generation context (from
# extra.context / extra.knowledge) are applied at replay time.
"prompt": record["question"],
"answer": record["generated_answer"],
"labels": labels,
"hallucinated_spans": spans_from_labels(labels, offsets),
"metadata": {
"source": record["source"],
"dataset": "hallu_dataset_v6",
"generator_model": GENERATOR,
"generator_backend": record.get("generator_backend"),
"generated_token_ids": list(token_ids),
"generated_tokens": list(tokens),
"generated_logprobs": record.get("gen_logprobs"),
"auto_label_method": record.get("auto_label_method"),
"reference_answer": record.get("reference_answer"),
"t_star_index": t_star,
"label_variant_used": VARIANT_FIELD[variant],
"label_variants": {n: v for n, v in variants.items() if n != variant},
"original_label_counts": dict(collections.Counter(
{0: "grounded", 1: "hallucinated", 2: "uncertain"}[v] for v in labels)),
"extra": record.get("extra") or {},
},
}
def main(argv=None):
args = parse_args(argv)
rows = []
with args.input.open(encoding="utf-8") as handle:
for line in handle:
if line.strip():
rows.append(convert(json.loads(line), args.labels))
args.output.parent.mkdir(parents=True, exist_ok=True)
with args.output.open("w", encoding="utf-8") as handle:
for row in rows:
handle.write(json.dumps(row, ensure_ascii=False) + "\n")
counts = collections.Counter(v for r in rows for v in r["labels"])
gh = counts[GROUNDED] + counts[HALLUCINATED]
arms = collections.Counter(r["metadata"]["extra"].get("arm") for r in rows)
print(f"wrote {len(rows):,} rows -> {args.output} (variant: {VARIANT_FIELD[args.labels]})")
print(f" tokens {sum(counts.values()):,} | G {counts[GROUNDED]:,} "
f"H {counts[HALLUCINATED]:,} U {counts[UNCERTAIN]:,}")
print(f" H prevalence over G/H: {100*counts[HALLUCINATED]/gh:.3f}%")
print(f" arms: {dict(arms)}")
print(f" sha256: {hashlib.sha256(args.output.read_bytes()).hexdigest()}")
return 0
if __name__ == "__main__":
raise SystemExit(main())
|