ARslan-Ahamd's picture
Deploy from GitHub commit 9c1d82a
465b477 verified
Raw History Blame Contribute Delete
5.36 kB
"""QASPER (Dasigi et al., NAACL 2021; CC-BY-4.0) loader.
Reads the Hugging Face parquet export, keeps every paragraph with its section, adds
figure/table captions as a final section (QASPER marks float evidence with
``FLOAT SELECTED``) and converts answers into the reference format used by the
official QASPER evaluator.
"""
from __future__ import annotations
import random
import re
import urllib.request
from collections import defaultdict
from pathlib import Path
import pandas as pd
from ..schemas import Document, GoldReference, Question, Section
PARQUET_URL = "https://huggingface.co/api/datasets/allenai/qasper/parquet/qasper/{split}/0.parquet"
SPLITS = ("train", "validation", "test")
FLOAT_PREFIX = "FLOAT SELECTED: "
FLOAT_SECTION = "Figures and Tables"
def download(raw_dir: str | Path, splits=SPLITS) -> list[Path]:
raw_dir = Path(raw_dir)
raw_dir.mkdir(parents=True, exist_ok=True)
paths = []
for split in splits:
p = raw_dir / f"qasper_{split}.parquet"
if not p.exists():
urllib.request.urlretrieve(PARQUET_URL.format(split=split), p)
paths.append(p)
return paths
def _clean(text: str) -> str:
return re.sub(r"\s+", " ", text or "").strip()
def _as_list(x) -> list:
return [] if x is None else list(x)
def _reference(ans: dict) -> GoldReference | None:
evidence = [_clean(e) for e in _as_list(ans.get("evidence")) if _clean(e)]
highlighted = [_clean(e) for e in _as_list(ans.get("highlighted_evidence")) if _clean(e)]
if ans.get("unanswerable"):
return GoldReference(answer="Unanswerable", type="none", evidence=[], highlighted_evidence=[])
spans = [s for s in _as_list(ans.get("extractive_spans")) if s]
if spans:
return GoldReference(answer=", ".join(spans), type="extractive", evidence=evidence, highlighted_evidence=highlighted)
if ans.get("free_form_answer"):
return GoldReference(answer=ans["free_form_answer"], type="abstractive", evidence=evidence, highlighted_evidence=highlighted)
if ans.get("yes_no") is True:
return GoldReference(answer="Yes", type="boolean", evidence=evidence, highlighted_evidence=highlighted)
if ans.get("yes_no") is False:
return GoldReference(answer="No", type="boolean", evidence=evidence, highlighted_evidence=highlighted)
return None # malformed annotation; the official evaluator raises here, we skip it
def parse_row(row) -> tuple[Document, list[Question]]:
doc_id = str(row["id"])
sections = [Section(idx=0, heading="Abstract", paragraphs=[_clean(row["abstract"])])]
ft = row["full_text"]
for name, paras in zip(_as_list(ft["section_name"]), _as_list(ft["paragraphs"])):
cleaned = [_clean(p) for p in _as_list(paras) if _clean(p)]
if cleaned:
sections.append(Section(idx=len(sections), heading=_clean(name) or "Untitled", paragraphs=cleaned))
captions = [_clean(c) for c in _as_list((row.get("figures_and_tables") or {}).get("caption")) if _clean(c)]
if captions:
sections.append(Section(idx=len(sections), heading=FLOAT_SECTION, paragraphs=captions))
doc = Document(doc_id=doc_id, title=_clean(row["title"]), sections=sections)
questions = []
qas = row["qas"]
for q, qid, answers in zip(_as_list(qas["question"]), _as_list(qas["question_id"]), _as_list(qas["answers"])):
refs = [r for a in _as_list(answers["answer"]) if (r := _reference(a)) is not None]
if refs:
questions.append(Question(qid=str(qid), doc_id=doc_id, question=_clean(q), references=refs))
return doc, questions
def load_split(raw_dir: str | Path, split: str) -> tuple[dict[str, Document], list[Question]]:
df = pd.read_parquet(Path(raw_dir) / f"qasper_{split}.parquet")
docs: dict[str, Document] = {}
questions: list[Question] = []
for _, row in df.iterrows():
doc, qs = parse_row(row)
docs[doc.doc_id] = doc
questions.extend(qs)
return docs, questions
def sample_questions(
questions: list[Question],
n_answerable: int,
n_unanswerable: int,
seed: int,
max_per_doc: int = 2,
) -> list[Question]:
"""Seeded sample that spreads questions across papers.
Answerable = no annotator marked it unanswerable; unanswerable = every annotator did.
Questions where annotators disagree are excluded from both pools so that the
abstention labels are unambiguous.
"""
rng = random.Random(seed)
pools = {"ans": [q for q in questions if not q.unanswerable_any], "unans": [q for q in questions if q.unanswerable_all]}
chosen: list[Question] = []
per_doc: dict[str, int] = defaultdict(int)
for key, n in (("ans", n_answerable), ("unans", n_unanswerable)):
pool = sorted(pools[key], key=lambda q: q.qid)
rng.shuffle(pool)
taken = 0
for q in pool:
if taken >= n:
break
if per_doc[q.doc_id] >= max_per_doc:
continue
chosen.append(q)
per_doc[q.doc_id] += 1
taken += 1
if taken < n:
raise ValueError(f"only {taken} {key} questions available with max_per_doc={max_per_doc}, wanted {n}")
return chosen
def normalize_paragraph(text: str) -> str:
text = _clean(text)
text = text.removeprefix(FLOAT_PREFIX)
return text.lower()