Spaces:
Running on Zero
Running on Zero
Download src/graphtrace/data/qasper.py from ARslan-Ahamd/graphtrace-bench: direct link, hf CLI and curl.
- Browser
- Download file 5.36 kB
-
https://huggingface.co/spaces/ARslan-Ahamd/graphtrace-bench/resolve/main/src/graphtrace/data/qasper.py
- Command line
-
hf download hf://spaces/ARslan-Ahamd/graphtrace-bench/src/graphtrace/data/qasper.py
-
curl -L -o qasper.py https://huggingface.co/spaces/ARslan-Ahamd/graphtrace-bench/resolve/main/src/graphtrace/data/qasper.py
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() | |