Instructions to use SinhalaMMLU-SharedTask/team-sinbotics with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- PEFT
How to use SinhalaMMLU-SharedTask/team-sinbotics with PEFT:
Task type is invalid.
- Notebooks
- Google Colab
- Kaggle
Download training/sinmmlu/data.py from SinhalaMMLU-SharedTask/team-sinbotics: direct link, hf CLI and curl.
- Browser
- Download file 6.11 kB
-
https://huggingface.co/SinhalaMMLU-SharedTask/team-sinbotics/resolve/main/training/sinmmlu/data.py
- Command line
-
hf download hf://SinhalaMMLU-SharedTask/team-sinbotics/training/sinmmlu/data.py
-
curl -L -o data.py https://huggingface.co/SinhalaMMLU-SharedTask/team-sinbotics/resolve/main/training/sinmmlu/data.py
6.11 kB
| """Loading, text normalisation and option permutation. | |
| Every row gets: | |
| level easy | medium | hard (public train is all easy) | |
| file source JSON file name; one file = one subject, so this is the subject key used for stratification | |
| uid "<level>/<file>/q<q_no>", unique, used to join predictions back to rows | |
| Test rows have no `answer`. Submission keys (q_no, subject, category, level) are always copied verbatim from the row, | |
| because the subject strings are inconsistent inside the files (e.g. "Eastern_Music" and "Eastern Music"). | |
| """ | |
| import glob | |
| import json | |
| import os | |
| import random | |
| import re | |
| import unicodedata | |
| from pathlib import Path | |
| ROOT = Path(__file__).resolve().parents[1] | |
| TRAIN_DIR = ROOT / "data" / "public train" | |
| TEST_DIR = ROOT / "data" / "public test" | |
| ZWNJ, ZWJ = "", "" | |
| LEVELS = ("easy", "medium", "hard") | |
| def path(p): | |
| p = Path(p) | |
| return p if p.is_absolute() else ROOT / p | |
| # ----------------------------------------------------------------------------- normalisation | |
| def norm_model(text): | |
| """Model input: NFC, drop ZWNJ only (a legacy-font artefact, far more common in test than in train), keep ZWJ | |
| (needed for conjuncts such as ප්ර), tidy spaces, keep newlines so sub-option lists stay readable.""" | |
| text = unicodedata.normalize("NFC", text).replace(ZWNJ, "") | |
| text = re.sub(r"[^\S\n]+", " ", text) | |
| text = re.sub(r" ?\n ?", "\n", text) | |
| return re.sub(r"\n{3,}", "\n\n", text).strip() | |
| def norm_dedup(text): | |
| """Dedup only: NFC, drop ZWJ/ZWNJ, punctuation and symbols to spaces, lowercase Latin, collapse spaces.""" | |
| text = unicodedata.normalize("NFC", text).replace(ZWJ, "").replace(ZWNJ, "") | |
| text = "".join(" " if unicodedata.category(c)[0] in "PS" else c for c in text) | |
| return re.sub(r"\s+", " ", text.lower()).strip() | |
| def subject_si(row): | |
| """Sinhala subject name for prompts (never for submission keys).""" | |
| name = row.get("subject_si") or row.get("metadata", {}).get("subject_original") or row["subject"] | |
| return " ".join(norm_model(name).split()) | |
| def subject_en(row): | |
| return " ".join(row["subject"].replace("_", " ").split()) | |
| def subject_family(row): | |
| """Coarse subject key shared by train and test spellings ("Arts"/"Art", "Health and Physical Science"/"Health", | |
| "Sinhala language and literature"/"Sinhala"): first three letters of the English subject name.""" | |
| return re.sub(r"[^a-z]", "", row["subject"].lower())[:3] | |
| # ----------------------------------------------------------------------------- io | |
| def load_dir(d, level=None): | |
| """All *.json under `d` (recursive). `level` overrides metadata.difficulty (public train has none).""" | |
| rows = [] | |
| for f in sorted(glob.glob(os.path.join(str(path(d)), "**", "*.json"), recursive=True)): | |
| with open(f, encoding="utf-8") as fh: | |
| for r in json.load(fh): | |
| r = dict(r) | |
| r["level"] = level or r.get("metadata", {}).get("difficulty", "unknown").lower() | |
| r["file"] = os.path.basename(f) | |
| r["uid"] = f"{r['level']}/{r['file']}/q{r['q_no']}" | |
| rows.append(r) | |
| uids = [r["uid"] for r in rows] | |
| assert len(set(uids)) == len(uids), f"duplicate (level, file, q_no) in {d}" | |
| return rows | |
| def load_train(): | |
| return load_dir(TRAIN_DIR, level="easy") | |
| def load_test(): | |
| return load_dir(TEST_DIR) | |
| def load_jsonl(p): | |
| with open(path(p), encoding="utf-8") as fh: | |
| return [json.loads(l) for l in fh if l.strip()] | |
| def write_jsonl(p, rows): | |
| p = path(p) | |
| p.parent.mkdir(parents=True, exist_ok=True) | |
| with open(p, "w", encoding="utf-8", newline="\n") as fh: | |
| for r in rows: | |
| fh.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| def load_rows(spec): | |
| """'test' -> public test; anything else -> a dev .jsonl written by sinmmlu.build_dev.""" | |
| return load_test() if spec == "test" else load_jsonl(spec) | |
| def split_by_subject(rows, frac, seed=42): | |
| """Validation split of training data (rows with q_no, subject, question): `frac` of each subject, at least 1 for | |
| subjects with >= 10 questions, sampled with Random(seed) over the subjects in sorted order. Validation questions | |
| whose text (norm_dedup) also occurs in the training part move to train, so no question text is in both. | |
| Returns (train, val), each in the input order.""" | |
| import collections | |
| rng = random.Random(seed) | |
| by = collections.defaultdict(list) | |
| for r in rows: | |
| by[r["subject"]].append(r) | |
| val_ids = set() | |
| for s in sorted(by): | |
| n = len(by[s]) | |
| k = max(1, round(frac * n)) if n >= 10 else 0 | |
| val_ids.update(r["q_no"] for r in rng.sample(by[s], k)) | |
| train_q = {norm_dedup(r["question"]) for r in rows if r["q_no"] not in val_ids} | |
| val = [r for r in rows if r["q_no"] in val_ids and norm_dedup(r["question"]) not in train_q] | |
| vset = {r["q_no"] for r in val} | |
| return [r for r in rows if r["q_no"] not in vset], val | |
| def submission_key(r): | |
| return (r["level"], r["subject"], r["category"], r["q_no"]) | |
| # ----------------------------------------------------------------------------- options | |
| def permute_options(row, perm): | |
| """perm[i] = original index shown at position i. The answer (if present) is remapped; translated options | |
| (`choices_en`) move with the originals.""" | |
| new = dict(row) | |
| for k in ("choices", "choices_en"): | |
| if k in row: | |
| new[k] = [row[k][j] for j in perm] | |
| if row.get("answer") is not None: | |
| new["answer"] = perm.index(int(row["answer"]) - 1) + 1 | |
| return new | |
| def cyclic_perms(n=4): | |
| return [[(i + s) % n for i in range(n)] for s in range(n)] | |
| def drop_random_wrong_option(row, rng: random.Random): | |
| """5 -> 4 options by removing one random wrong option (as in the paper, section 6).""" | |
| ans = int(row["answer"]) - 1 | |
| drop = rng.choice([i for i in range(len(row["choices"])) if i != ans]) | |
| keep = [i for i in range(len(row["choices"])) if i != drop] | |
| new = dict(row) | |
| new["choices"] = [row["choices"][i] for i in keep] | |
| new["answer"] = keep.index(ans) + 1 | |
| return new | |