Upload folder using huggingface_hub
Browse files- README.md +17 -2
- final/README.md +61 -0
- final/build_datasets.py +131 -0
- final/fetch_titles.py +77 -0
- final/infer_test.py +95 -0
- final/recover_abstracts.py +174 -0
- final/train_qwen_qlora.py +159 -0
- final/validate_submission.py +53 -0
- train_final.ipynb +274 -0
- train_final.py +240 -0
README.md
CHANGED
|
@@ -17,5 +17,20 @@ research highlights from scientific paper abstracts (MixSub corpus).
|
|
| 17 |
- Training: one epoch on 15,960 pairs, Kaggle dual T4
|
| 18 |
- Validation ROUGE-L F1 (plain LCS, no stemming): **26.37**
|
| 19 |
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
- Training: one epoch on 15,960 pairs, Kaggle dual T4
|
| 18 |
- Validation ROUGE-L F1 (plain LCS, no stemming): **26.37**
|
| 19 |
|
| 20 |
+
## Repository contents
|
| 21 |
+
|
| 22 |
+
| file | purpose |
|
| 23 |
+
| --- | --- |
|
| 24 |
+
| `train_final.py` | standalone training recipe that produced this model |
|
| 25 |
+
| `train_final.ipynb` | notebook version of the same recipe |
|
| 26 |
+
| `final/` | full data pipeline: abstract recovery, title fetch, dataset build, submission validation |
|
| 27 |
+
|
| 28 |
+
## Data pipeline summary
|
| 29 |
+
|
| 30 |
+
~40% of corpus abstracts are truncated mid-sentence. Each row was matched to
|
| 31 |
+
its ScienceDirect PII (100% verified join), resolved to a DOI via Elsevier's
|
| 32 |
+
keyless API, and its complete abstract recovered from Semantic Scholar under
|
| 33 |
+
a label-free >=90%-token-overlap acceptance filter (~95% of truncated rows
|
| 34 |
+
repaired). Paper titles were fetched for 100% of rows and prepended to
|
| 35 |
+
inputs. See `final/README.md` for the full pipeline and negative-results
|
| 36 |
+
summary. Trained 2026-08-08.
|
final/README.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# SciHigh 2026 Subtask 1 — Final Pipeline
|
| 2 |
+
|
| 3 |
+
Team Yushkk99. Task: generate research highlights from paper abstracts
|
| 4 |
+
(MixSub corpus, 19,785 ScienceDirect papers). Official metric: ROUGE-L F1.
|
| 5 |
+
|
| 6 |
+
## Results (validation, 1,985 rows, plain rougeL / no stemming)
|
| 7 |
+
|
| 8 |
+
| system | val ROUGE-L |
|
| 9 |
+
| --- | --- |
|
| 10 |
+
| FIRE-2025 winning system (reference) | 23.45 |
|
| 11 |
+
| bart-large-cnn, original data | 24.37 |
|
| 12 |
+
| bart-large-cnn, repaired data + titles (run 1) | 26.37 |
|
| 13 |
+
| **Qwen2.5-72B QLoRA, repaired data + titles (run 2)** | **28.33** |
|
| 14 |
+
|
| 15 |
+
## Pipeline
|
| 16 |
+
|
| 17 |
+
Data enrichment (all sources free and sanctioned; test rows touched only
|
| 18 |
+
via abstract/title metadata — never highlight fields):
|
| 19 |
+
|
| 20 |
+
1. `recover_abstracts.py` — ~40% of corpus abstracts are truncated
|
| 21 |
+
mid-sentence. Each row is matched to its ScienceDirect PII (verified
|
| 22 |
+
100% by abstract-text join against the dataset's source repository),
|
| 23 |
+
resolved to a DOI via Elsevier's keyless Article API, and its complete
|
| 24 |
+
abstract fetched from Semantic Scholar. A label-free acceptance filter
|
| 25 |
+
(recovered text must contain >=90% of the truncated text's tokens)
|
| 26 |
+
guarantees same-paper extensions and structurally excludes records
|
| 27 |
+
containing highlight fields. Acceptance: ~95% of truncated rows.
|
| 28 |
+
2. `fetch_titles.py` — paper titles for 100% of rows via the same keyless
|
| 29 |
+
PII lookup.
|
| 30 |
+
3. `build_datasets.py` — rebuilds all splits with recovered abstracts
|
| 31 |
+
normalized to the corpus's punctuation-stripped format (transform
|
| 32 |
+
validated token-for-token against 5,481 known prefixes, mean agreement
|
| 33 |
+
0.987), then prepends titles: `<title> | <abstract>`.
|
| 34 |
+
|
| 35 |
+
Modeling:
|
| 36 |
+
|
| 37 |
+
4. `train_qwen_qlora.py` — one-epoch QLoRA SFT (4-bit NF4, LoRA r=16 on
|
| 38 |
+
attention+MLP) of a Qwen2.5 Instruct base on the 15,960-pair titled
|
| 39 |
+
pool, chat-formatted, greedy decoding. The identical script trains the
|
| 40 |
+
7B (24GB GPU) and 72B (80GB GPU) variants; only `--base` changes.
|
| 41 |
+
5. `infer_test.py` — greedy test-set inference producing the submission CSV.
|
| 42 |
+
6. `validate_submission.py` — format gate: row count, column schema, no
|
| 43 |
+
empty predictions, length statistics.
|
| 44 |
+
|
| 45 |
+
Run 1 (bart lineage) reproduces via the Kaggle kernels
|
| 46 |
+
(`res-3-title-prefix`) recorded in the project artifacts.
|
| 47 |
+
|
| 48 |
+
## Models
|
| 49 |
+
|
| 50 |
+
- `Yakk99/scihigh2026-subtask1-qwen72b-qlora` — run-2 adapter (72B)
|
| 51 |
+
- `Yakk99/scihigh2026-subtask1-qwen7b-qlora` — 7B pilot adapter
|
| 52 |
+
- `Yakk99/scihigh2026-subtask1-bart-titled` — run-1 full model
|
| 53 |
+
|
| 54 |
+
## Negative results (tested and closed, full logs in project artifacts)
|
| 55 |
+
|
| 56 |
+
BRIO contrastive calibration (two decode-matched attempts, ~0 delta);
|
| 57 |
+
MBR/DPO preference signal on the 72B pool (−2.0 — sampled candidates
|
| 58 |
+
cluster below the greedy mode of a well-trained model); oracle bullet
|
| 59 |
+
reordering (+0.0002); GRPO (rejected on published scientific-domain
|
| 60 |
+
negatives); RAG (measured negative on this exact corpus by a FIRE-2025
|
| 61 |
+
team).
|
final/build_datasets.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Build the final training/eval datasets.
|
| 2 |
+
|
| 3 |
+
Stage 1 — repair: replace each accepted truncated abstract with its
|
| 4 |
+
recovered text, normalized to the corpus format (the corpus strips all
|
| 5 |
+
punctuation except periods; sentence-final periods are free-standing ' . '
|
| 6 |
+
tokens; intra-token periods like '94.5' survive). The transform is
|
| 7 |
+
validated against every accepted pair by comparing the normalized recovered
|
| 8 |
+
prefix to the corpus text we already hold.
|
| 9 |
+
|
| 10 |
+
Stage 2 — titles: prepend the paper title, `<title> | <abstract>`, applied
|
| 11 |
+
identically to every split.
|
| 12 |
+
|
| 13 |
+
Usage: python build_datasets.py
|
| 14 |
+
Inputs: data/raw/*, data/external/{recovered_*,pii_title_cache.jsonl,*_with_pii.csv}
|
| 15 |
+
Outputs: data/recovered/{train,train_expanded,val,test}[_titled].csv
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
import json
|
| 19 |
+
import re
|
| 20 |
+
import unicodedata
|
| 21 |
+
from difflib import SequenceMatcher
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
|
| 24 |
+
import pandas as pd
|
| 25 |
+
|
| 26 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 27 |
+
EXT = REPO / "data/external"
|
| 28 |
+
OUT = REPO / "data/recovered"
|
| 29 |
+
|
| 30 |
+
DASHES = "‐‑‒–—―−"
|
| 31 |
+
SECTION_HEADERS = re.compile(
|
| 32 |
+
r"\b(BACKGROUND|METHODS?|RESULTS?|CONCLUSIONS?|OBJECTIVES?|PURPOSE"
|
| 33 |
+
r"|INTRODUCTION|RATIONALE|AIMS?|DISCUSSION|MATERIALS|SIGNIFICANCE"
|
| 34 |
+
r"|FINDINGS|INTERPRETATION|SETTING|DESIGN|PARTICIPANTS|MEASUREMENTS"
|
| 35 |
+
r"|LIMITATIONS|IMPLICATIONS|HYPOTHESIS|UNLABELLED|IMPORTANCE"
|
| 36 |
+
r"|EXPOSURES?|OUTCOMES?)(\s+AND\s+[A-Z]{4,})?\b[:.]?")
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def strip_to_corpus_format(text):
|
| 40 |
+
t = unicodedata.normalize("NFKC", str(text))
|
| 41 |
+
t = SECTION_HEADERS.sub("", t)
|
| 42 |
+
t = t.replace("!", ".").replace("?", ".")
|
| 43 |
+
t = re.sub(r"(?<=\S)\.(?=\S)", "\x00", t)
|
| 44 |
+
t = t.replace(".", " . ").replace("\x00", ".")
|
| 45 |
+
t = t.replace("-", " ")
|
| 46 |
+
for d in DASHES:
|
| 47 |
+
t = t.replace(d, "")
|
| 48 |
+
t = re.sub(r"[^A-Za-z0-9. ]", "", t)
|
| 49 |
+
return " ".join(t.split())
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
def validate_transform():
|
| 53 |
+
scores = []
|
| 54 |
+
for split in ("train", "val", "test"):
|
| 55 |
+
rec = pd.read_csv(EXT / f"recovered_{split}.csv")
|
| 56 |
+
for _, r in rec[rec["accepted"]].iterrows():
|
| 57 |
+
a = str(r["Abstract"]).split()[:-2]
|
| 58 |
+
b = strip_to_corpus_format(r["RecoveredAbstract"]).split()[: len(a) + 20]
|
| 59 |
+
m = SequenceMatcher(None, a, b, autojunk=False)
|
| 60 |
+
scores.append(sum(bl.size for bl in m.get_matching_blocks()) / max(len(a), 1))
|
| 61 |
+
mean = sum(scores) / len(scores)
|
| 62 |
+
print(f"transform validation: {len(scores)} pairs, mean token agreement {mean:.4f}")
|
| 63 |
+
assert mean >= 0.90, "transform does not reproduce corpus format"
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def load_titles():
|
| 67 |
+
titles = {}
|
| 68 |
+
for line in (EXT / "pii_title_cache.jsonl").read_text().splitlines():
|
| 69 |
+
try:
|
| 70 |
+
r = json.loads(line)
|
| 71 |
+
if r.get("title"):
|
| 72 |
+
titles[r["pii"]] = r["title"]
|
| 73 |
+
except (json.JSONDecodeError, KeyError):
|
| 74 |
+
continue
|
| 75 |
+
return titles
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def clean_title(raw):
|
| 79 |
+
t = strip_to_corpus_format(raw)
|
| 80 |
+
while t.endswith(" ."):
|
| 81 |
+
t = t[:-2].rstrip()
|
| 82 |
+
return t
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def main():
|
| 86 |
+
validate_transform()
|
| 87 |
+
OUT.mkdir(exist_ok=True)
|
| 88 |
+
titles = load_titles()
|
| 89 |
+
|
| 90 |
+
repl = {}
|
| 91 |
+
for split in ("train", "val", "test"):
|
| 92 |
+
rec = pd.read_csv(EXT / f"recovered_{split}.csv")
|
| 93 |
+
acc = rec[rec["accepted"]]
|
| 94 |
+
for pii, a in zip(acc["PII"], acc["RecoveredAbstract"]):
|
| 95 |
+
repl[pii] = strip_to_corpus_format(a)
|
| 96 |
+
extra = EXT / "recovered_expanded_extra.csv"
|
| 97 |
+
if extra.exists():
|
| 98 |
+
rec = pd.read_csv(extra)
|
| 99 |
+
acc = rec[rec["accepted"]]
|
| 100 |
+
for pii, a in zip(acc["PII"], acc["RecoveredAbstract"]):
|
| 101 |
+
repl[pii] = strip_to_corpus_format(a)
|
| 102 |
+
|
| 103 |
+
pii_by_split = {
|
| 104 |
+
s: dict(zip(*(lambda d: (d["Filename"], d["PII"]))(
|
| 105 |
+
pd.read_csv(EXT / f"{s}_with_pii.csv"))))
|
| 106 |
+
for s in ("train", "val", "test")
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
for name in ("train", "val", "test", "train_expanded"):
|
| 110 |
+
df = pd.read_csv(REPO / f"data/raw/{name}.csv")
|
| 111 |
+
piis = (df["PaperID"] if name == "train_expanded"
|
| 112 |
+
else df["Filename"].map(pii_by_split[name.replace("_expanded", "")]))
|
| 113 |
+
df["Abstract"] = [repl.get(p, a) for p, a in zip(piis, df["Abstract"])]
|
| 114 |
+
df.to_csv(OUT / f"{name}.csv", index=False)
|
| 115 |
+
|
| 116 |
+
titled = []
|
| 117 |
+
for pii, ab in zip(piis, df["Abstract"]):
|
| 118 |
+
t = titles.get(pii)
|
| 119 |
+
titled.append(f"{clean_title(t)} | {ab}" if t else str(ab))
|
| 120 |
+
df_t = df.copy()
|
| 121 |
+
df_t["Abstract"] = titled
|
| 122 |
+
df_t.to_csv(OUT / f"{name}_titled.csv", index=False)
|
| 123 |
+
|
| 124 |
+
trunc = (~df["Abstract"].astype(str).str.rstrip().str.endswith(".")).mean()
|
| 125 |
+
print(f"{name}: {len(df)} rows, residual truncation {trunc:.1%}, "
|
| 126 |
+
f"titled variant written")
|
| 127 |
+
return 0
|
| 128 |
+
|
| 129 |
+
|
| 130 |
+
if __name__ == "__main__":
|
| 131 |
+
raise SystemExit(main())
|
final/fetch_titles.py
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Fetch paper titles for all corpus rows via the keyless Elsevier PII
|
| 2 |
+
lookup. Titles are open bibliographic metadata and return for 100% of rows
|
| 3 |
+
regardless of paywall status. Cache is append-only JSONL; resumable.
|
| 4 |
+
|
| 5 |
+
Usage: python fetch_titles.py --splits val test expanded
|
| 6 |
+
Outputs: data/external/pii_title_cache.jsonl
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import json
|
| 11 |
+
import ssl
|
| 12 |
+
import time
|
| 13 |
+
import urllib.error
|
| 14 |
+
import urllib.request
|
| 15 |
+
from pathlib import Path
|
| 16 |
+
|
| 17 |
+
import certifi
|
| 18 |
+
import pandas as pd
|
| 19 |
+
|
| 20 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 21 |
+
CACHE = REPO / "data/external/pii_title_cache.jsonl"
|
| 22 |
+
SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
| 23 |
+
UA = "scihigh2026-titles"
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
def fetch_title(pii):
|
| 27 |
+
req = urllib.request.Request(
|
| 28 |
+
f"https://api.elsevier.com/content/article/pii/{pii}",
|
| 29 |
+
headers={"User-Agent": UA, "Accept": "application/json"})
|
| 30 |
+
for attempt in range(4):
|
| 31 |
+
try:
|
| 32 |
+
with urllib.request.urlopen(req, timeout=45, context=SSL_CTX) as r:
|
| 33 |
+
d = json.loads(r.read())
|
| 34 |
+
return d["full-text-retrieval-response"]["coredata"].get("dc:title")
|
| 35 |
+
except urllib.error.HTTPError as e:
|
| 36 |
+
if e.code == 404:
|
| 37 |
+
return None
|
| 38 |
+
time.sleep(8 * (attempt + 1))
|
| 39 |
+
except Exception:
|
| 40 |
+
time.sleep(8 * (attempt + 1))
|
| 41 |
+
return None
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def main():
|
| 45 |
+
ap = argparse.ArgumentParser()
|
| 46 |
+
ap.add_argument("--splits", nargs="+", default=["val", "test", "expanded"])
|
| 47 |
+
args = ap.parse_args()
|
| 48 |
+
|
| 49 |
+
piis = []
|
| 50 |
+
for split in args.splits:
|
| 51 |
+
if split == "expanded":
|
| 52 |
+
piis += pd.read_csv(REPO / "data/raw/train_expanded.csv")["PaperID"].dropna().tolist()
|
| 53 |
+
else:
|
| 54 |
+
piis += pd.read_csv(REPO / f"data/external/{split}_with_pii.csv")["PII"].dropna().tolist()
|
| 55 |
+
|
| 56 |
+
cached = set()
|
| 57 |
+
if CACHE.exists():
|
| 58 |
+
for line in CACHE.read_text().splitlines():
|
| 59 |
+
try:
|
| 60 |
+
cached.add(json.loads(line)["pii"])
|
| 61 |
+
except (json.JSONDecodeError, KeyError):
|
| 62 |
+
continue
|
| 63 |
+
todo = [p for p in dict.fromkeys(piis) if p not in cached]
|
| 64 |
+
print(f"{len(piis)} rows requested, {len(todo)} to fetch")
|
| 65 |
+
|
| 66 |
+
with CACHE.open("a") as f:
|
| 67 |
+
for i, pii in enumerate(todo, 1):
|
| 68 |
+
f.write(json.dumps({"pii": pii, "title": fetch_title(pii)}) + "\n")
|
| 69 |
+
time.sleep(0.15)
|
| 70 |
+
if i % 500 == 0:
|
| 71 |
+
print(f" {i}/{len(todo)}", flush=True)
|
| 72 |
+
print("done")
|
| 73 |
+
return 0
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
if __name__ == "__main__":
|
| 77 |
+
raise SystemExit(main())
|
final/infer_test.py
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Generate the test-set submission CSV from a trained Qwen adapter.
|
| 2 |
+
|
| 3 |
+
Greedy decoding over `<title> | <abstract>` test inputs, output formatted as
|
| 4 |
+
`Filename, Abstract, Predicted_Highlights` per the task specification. The
|
| 5 |
+
Abstract column carries the OFFICIAL (unmodified) test abstracts.
|
| 6 |
+
|
| 7 |
+
python infer_test.py --base unsloth/Qwen2.5-72B-Instruct-bnb-4bit \\
|
| 8 |
+
--adapter qwen72_out/adapter --test-titled test_titled.csv \\
|
| 9 |
+
--test-official test.csv --out Yushkk99_Task1_run2.csv
|
| 10 |
+
"""
|
| 11 |
+
|
| 12 |
+
import argparse
|
| 13 |
+
import subprocess
|
| 14 |
+
import sys
|
| 15 |
+
import time
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def pip(*p):
|
| 19 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *p], check=True)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
pip("transformers>=4.51", "peft", "bitsandbytes", "pandas", "accelerate")
|
| 23 |
+
|
| 24 |
+
import pandas as pd # noqa: E402
|
| 25 |
+
import torch # noqa: E402
|
| 26 |
+
from peft import PeftModel # noqa: E402
|
| 27 |
+
from transformers import (AutoModelForCausalLM, AutoTokenizer, # noqa: E402
|
| 28 |
+
BitsAndBytesConfig)
|
| 29 |
+
|
| 30 |
+
SYS = ("You write research highlights. Given a paper title and abstract "
|
| 31 |
+
"(separated by |), produce 3-5 short highlight sentences in the exact "
|
| 32 |
+
"style of the examples you were trained on: no punctuation except "
|
| 33 |
+
"sentence-final periods written as ' . ', ~55 words total, each "
|
| 34 |
+
"sentence stating one contribution or finding.")
|
| 35 |
+
|
| 36 |
+
|
| 37 |
+
def main():
|
| 38 |
+
ap = argparse.ArgumentParser()
|
| 39 |
+
ap.add_argument("--base", required=True)
|
| 40 |
+
ap.add_argument("--adapter", required=True)
|
| 41 |
+
ap.add_argument("--test-titled", required=True)
|
| 42 |
+
ap.add_argument("--test-official", required=True)
|
| 43 |
+
ap.add_argument("--out", default="submission.csv")
|
| 44 |
+
args = ap.parse_args()
|
| 45 |
+
|
| 46 |
+
tok = AutoTokenizer.from_pretrained(args.base)
|
| 47 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 48 |
+
args.base,
|
| 49 |
+
quantization_config=BitsAndBytesConfig(
|
| 50 |
+
load_in_4bit=True, bnb_4bit_quant_type="nf4",
|
| 51 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 52 |
+
bnb_4bit_use_double_quant=True),
|
| 53 |
+
dtype=torch.bfloat16, device_map="auto")
|
| 54 |
+
model = PeftModel.from_pretrained(model, args.adapter)
|
| 55 |
+
model.eval()
|
| 56 |
+
|
| 57 |
+
titled = pd.read_csv(args.test_titled)
|
| 58 |
+
official = pd.read_csv(args.test_official)
|
| 59 |
+
preds = []
|
| 60 |
+
t0 = time.time()
|
| 61 |
+
with torch.no_grad():
|
| 62 |
+
for i in range(0, len(titled), 8):
|
| 63 |
+
batch = titled.iloc[i : i + 8]
|
| 64 |
+
prompts = [tok.apply_chat_template(
|
| 65 |
+
[{"role": "system", "content": SYS},
|
| 66 |
+
{"role": "user", "content": str(a)}],
|
| 67 |
+
tokenize=False, add_generation_prompt=True)
|
| 68 |
+
for a in batch["Abstract"]]
|
| 69 |
+
enc = tok(prompts, return_tensors="pt", padding=True,
|
| 70 |
+
truncation=True, max_length=1024,
|
| 71 |
+
padding_side="left").to("cuda")
|
| 72 |
+
out = model.generate(**enc, max_new_tokens=130, do_sample=False,
|
| 73 |
+
pad_token_id=tok.pad_token_id)
|
| 74 |
+
preds += [tok.decode(out[j][enc.input_ids.shape[1]:],
|
| 75 |
+
skip_special_tokens=True).strip()
|
| 76 |
+
for j in range(len(batch))]
|
| 77 |
+
if (i // 8) % 20 == 0:
|
| 78 |
+
rate = (i + len(batch)) / max(time.time() - t0, 1)
|
| 79 |
+
print(f"{i + len(batch)}/{len(titled)} "
|
| 80 |
+
f"(eta {(len(titled) - i) / max(rate, 1e-9) / 60:.0f}m)",
|
| 81 |
+
flush=True)
|
| 82 |
+
|
| 83 |
+
sub = pd.DataFrame({
|
| 84 |
+
"Filename": titled["Filename"],
|
| 85 |
+
"Abstract": official.set_index("Filename").loc[
|
| 86 |
+
titled["Filename"], "Abstract"].values,
|
| 87 |
+
"Predicted_Highlights": preds,
|
| 88 |
+
})
|
| 89 |
+
sub.to_csv(args.out, index=False)
|
| 90 |
+
print(f"wrote {args.out}: {len(sub)} rows")
|
| 91 |
+
return 0
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
if __name__ == "__main__":
|
| 95 |
+
raise SystemExit(main())
|
final/recover_abstracts.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Recover complete abstracts for truncated corpus rows.
|
| 2 |
+
|
| 3 |
+
Chain: CSV row -> ScienceDirect PII (pre-verified join tables) -> DOI
|
| 4 |
+
(keyless Elsevier Article API) -> abstract (Semantic Scholar /paper/batch).
|
| 5 |
+
|
| 6 |
+
Acceptance filter (label-free): the recovered abstract must contain >=90%
|
| 7 |
+
of the truncated text's content tokens, be strictly longer, and end in
|
| 8 |
+
sentence-final punctuation. Both network stages checkpoint to JSONL caches,
|
| 9 |
+
so the sweep is resumable.
|
| 10 |
+
|
| 11 |
+
Usage: python recover_abstracts.py --splits val train test
|
| 12 |
+
Inputs: data/external/{split}_with_pii.csv
|
| 13 |
+
Outputs: data/external/recovered_{split}.csv
|
| 14 |
+
"""
|
| 15 |
+
|
| 16 |
+
import argparse
|
| 17 |
+
import json
|
| 18 |
+
import re
|
| 19 |
+
import ssl
|
| 20 |
+
import sys
|
| 21 |
+
import time
|
| 22 |
+
import urllib.error
|
| 23 |
+
import urllib.parse
|
| 24 |
+
import urllib.request
|
| 25 |
+
from pathlib import Path
|
| 26 |
+
|
| 27 |
+
import certifi
|
| 28 |
+
import pandas as pd
|
| 29 |
+
|
| 30 |
+
REPO = Path(__file__).resolve().parent.parent
|
| 31 |
+
EXT = REPO / "data/external"
|
| 32 |
+
SSL_CTX = ssl.create_default_context(cafile=certifi.where())
|
| 33 |
+
UA = "scihigh2026-recovery"
|
| 34 |
+
SENTENCE_FINAL = (".", "!", "?")
|
| 35 |
+
DOI_CACHE = EXT / "pii_doi_cache.jsonl"
|
| 36 |
+
S2_CACHE = EXT / "s2_abstract_cache.jsonl"
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def is_truncated(text):
|
| 40 |
+
return not str(text).rstrip().endswith(SENTENCE_FINAL)
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
def toks(t):
|
| 44 |
+
return re.findall(r"[a-z0-9]+", str(t).lower())
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
def load_cache(path, key):
|
| 48 |
+
cache = {}
|
| 49 |
+
if path.exists():
|
| 50 |
+
for line in path.read_text().splitlines():
|
| 51 |
+
try:
|
| 52 |
+
rec = json.loads(line)
|
| 53 |
+
cache[rec[key]] = rec
|
| 54 |
+
except (json.JSONDecodeError, KeyError):
|
| 55 |
+
continue
|
| 56 |
+
return cache
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
def append_cache(path, rec):
|
| 60 |
+
with path.open("a") as f:
|
| 61 |
+
f.write(json.dumps(rec) + "\n")
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def request_json(url, data=None, retries=6):
|
| 65 |
+
headers = {"User-Agent": UA, "Accept": "application/json"}
|
| 66 |
+
if data is not None:
|
| 67 |
+
headers["Content-Type"] = "application/json"
|
| 68 |
+
for attempt in range(retries):
|
| 69 |
+
req = urllib.request.Request(url, data=data, headers=headers)
|
| 70 |
+
try:
|
| 71 |
+
with urllib.request.urlopen(req, timeout=60, context=SSL_CTX) as r:
|
| 72 |
+
return json.loads(r.read())
|
| 73 |
+
except urllib.error.HTTPError as e:
|
| 74 |
+
if e.code in (404, 400):
|
| 75 |
+
return None
|
| 76 |
+
time.sleep(10 * (attempt + 1))
|
| 77 |
+
except Exception:
|
| 78 |
+
time.sleep(10 * (attempt + 1))
|
| 79 |
+
return None
|
| 80 |
+
|
| 81 |
+
|
| 82 |
+
def resolve_dois(piis):
|
| 83 |
+
cache = load_cache(DOI_CACHE, "pii")
|
| 84 |
+
todo = [p for p in piis if p not in cache]
|
| 85 |
+
print(f"PII->DOI: {len(piis) - len(todo)} cached, {len(todo)} to resolve", flush=True)
|
| 86 |
+
for i, pii in enumerate(todo, 1):
|
| 87 |
+
d = request_json(f"https://api.elsevier.com/content/article/pii/{pii}", retries=3)
|
| 88 |
+
try:
|
| 89 |
+
doi = d["full-text-retrieval-response"]["coredata"]["prism:doi"]
|
| 90 |
+
except (KeyError, TypeError):
|
| 91 |
+
doi = None
|
| 92 |
+
rec = {"pii": pii, "doi": doi}
|
| 93 |
+
cache[pii] = rec
|
| 94 |
+
append_cache(DOI_CACHE, rec)
|
| 95 |
+
time.sleep(0.15)
|
| 96 |
+
if i % 500 == 0:
|
| 97 |
+
print(f" {i}/{len(todo)}", flush=True)
|
| 98 |
+
return {p: cache[p]["doi"] for p in piis if p in cache}
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
def fetch_s2(dois):
|
| 102 |
+
cache = load_cache(S2_CACHE, "doi")
|
| 103 |
+
todo = [d for d in dois if d not in cache]
|
| 104 |
+
print(f"S2 abstracts: {len(dois) - len(todo)} cached, {len(todo)} to fetch", flush=True)
|
| 105 |
+
for start in range(0, len(todo), 100):
|
| 106 |
+
chunk = todo[start : start + 100]
|
| 107 |
+
payload = json.dumps({"ids": [f"DOI:{d}" for d in chunk]}).encode()
|
| 108 |
+
res = request_json(
|
| 109 |
+
"https://api.semanticscholar.org/graph/v1/paper/batch?fields=abstract",
|
| 110 |
+
data=payload)
|
| 111 |
+
recs = res if isinstance(res, list) else [None] * len(chunk)
|
| 112 |
+
for doi, rec in zip(chunk, recs):
|
| 113 |
+
abstract = None
|
| 114 |
+
if isinstance(rec, dict) and rec.get("abstract"):
|
| 115 |
+
abstract = " ".join(rec["abstract"].split())
|
| 116 |
+
out = {"doi": doi, "abstract": abstract}
|
| 117 |
+
cache[doi] = out
|
| 118 |
+
append_cache(S2_CACHE, out)
|
| 119 |
+
time.sleep(3.0)
|
| 120 |
+
return {d: cache[d]["abstract"] for d in dois if d in cache}
|
| 121 |
+
|
| 122 |
+
|
| 123 |
+
def accept(ours, theirs):
|
| 124 |
+
if not theirs:
|
| 125 |
+
return False, 0.0
|
| 126 |
+
cleaned = re.sub(r"^\s*Abstract\s+", "", theirs)
|
| 127 |
+
ours_t = toks(ours)
|
| 128 |
+
theirs_set = set(toks(cleaned))
|
| 129 |
+
cov = sum(1 for t in ours_t if t in theirs_set) / max(len(ours_t), 1)
|
| 130 |
+
ok = (cov >= 0.90
|
| 131 |
+
and len(cleaned) > len(str(ours)) * 1.05
|
| 132 |
+
and not is_truncated(cleaned))
|
| 133 |
+
return ok, cov
|
| 134 |
+
|
| 135 |
+
|
| 136 |
+
def main():
|
| 137 |
+
ap = argparse.ArgumentParser()
|
| 138 |
+
ap.add_argument("--splits", nargs="+", default=["val", "train", "test"])
|
| 139 |
+
args = ap.parse_args()
|
| 140 |
+
|
| 141 |
+
frames = {}
|
| 142 |
+
all_piis = []
|
| 143 |
+
for split in args.splits:
|
| 144 |
+
df = pd.read_csv(EXT / f"{split}_with_pii.csv")
|
| 145 |
+
df = df[df["PII"].notna()].copy()
|
| 146 |
+
df["is_trunc"] = df["Abstract"].map(is_truncated)
|
| 147 |
+
frames[split] = df
|
| 148 |
+
all_piis += df.loc[df["is_trunc"], "PII"].tolist()
|
| 149 |
+
|
| 150 |
+
doi_map = resolve_dois(all_piis)
|
| 151 |
+
s2_map = fetch_s2(sorted({d for d in doi_map.values() if d}))
|
| 152 |
+
|
| 153 |
+
for split, df in frames.items():
|
| 154 |
+
recs = []
|
| 155 |
+
for _, r in df[df["is_trunc"]].iterrows():
|
| 156 |
+
doi = doi_map.get(r["PII"])
|
| 157 |
+
theirs = s2_map.get(doi) if doi else None
|
| 158 |
+
ok, cov = accept(r["Abstract"], theirs)
|
| 159 |
+
cleaned = re.sub(r"^\s*Abstract\s+", "", theirs) if theirs else None
|
| 160 |
+
recs.append({
|
| 161 |
+
"Filename": r["Filename"], "PII": r["PII"], "DOI": doi,
|
| 162 |
+
"Abstract": r["Abstract"],
|
| 163 |
+
"RecoveredAbstract": cleaned if ok else None,
|
| 164 |
+
"coverage": round(cov, 3), "accepted": ok,
|
| 165 |
+
})
|
| 166 |
+
out = pd.DataFrame(recs)
|
| 167 |
+
out.to_csv(EXT / f"recovered_{split}.csv", index=False)
|
| 168 |
+
print(f"{split}: {len(out)} truncated, {int(out['accepted'].sum())} "
|
| 169 |
+
f"accepted ({out['accepted'].mean():.1%})")
|
| 170 |
+
return 0
|
| 171 |
+
|
| 172 |
+
|
| 173 |
+
if __name__ == "__main__":
|
| 174 |
+
raise SystemExit(main())
|
final/train_qwen_qlora.py
ADDED
|
@@ -0,0 +1,159 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""QLoRA supervised fine-tuning of a Qwen2.5 Instruct model for research
|
| 2 |
+
highlight generation.
|
| 3 |
+
|
| 4 |
+
Input rows: Abstract = `<title> | <repaired abstract>`, Highlights = target.
|
| 5 |
+
4-bit NF4 base + LoRA (r=16, attention+MLP), bf16 compute, one epoch,
|
| 6 |
+
chat-formatted. Evaluates plain ROUGE-L (stemmer=False) on a fixed
|
| 7 |
+
stratified 300-row validation subset, then on full validation. Greedy
|
| 8 |
+
decoding throughout. Self-contained: installs its own dependencies.
|
| 9 |
+
|
| 10 |
+
python train_qwen_qlora.py --train train_expanded_titled.csv \
|
| 11 |
+
--val val_titled.csv --base Qwen/Qwen2.5-72B-Instruct \
|
| 12 |
+
--batch 1 --accum 16 --out qwen72_out
|
| 13 |
+
|
| 14 |
+
7B fits a 24GB GPU (--batch 4 --accum 4); 72B needs 80GB + ~150GB disk.
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
import argparse
|
| 18 |
+
import json
|
| 19 |
+
import subprocess
|
| 20 |
+
import sys
|
| 21 |
+
import time
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def pip(*pkgs):
|
| 25 |
+
subprocess.run([sys.executable, "-m", "pip", "install", "-q", *pkgs], check=True)
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
pip("transformers>=4.51", "peft", "trl", "bitsandbytes", "datasets",
|
| 29 |
+
"rouge_score", "absl-py", "pandas", "accelerate")
|
| 30 |
+
|
| 31 |
+
import numpy as np # noqa: E402
|
| 32 |
+
import pandas as pd # noqa: E402
|
| 33 |
+
import torch # noqa: E402
|
| 34 |
+
from datasets import Dataset # noqa: E402
|
| 35 |
+
from peft import LoraConfig # noqa: E402
|
| 36 |
+
from rouge_score import rouge_scorer # noqa: E402
|
| 37 |
+
from transformers import (AutoModelForCausalLM, AutoTokenizer, # noqa: E402
|
| 38 |
+
BitsAndBytesConfig)
|
| 39 |
+
from trl import SFTConfig, SFTTrainer # noqa: E402
|
| 40 |
+
|
| 41 |
+
BASE = None # set from --base
|
| 42 |
+
SYS = ("You write research highlights. Given a paper title and abstract "
|
| 43 |
+
"(separated by |), produce 3-5 short highlight sentences in the exact "
|
| 44 |
+
"style of the examples you were trained on: no punctuation except "
|
| 45 |
+
"sentence-final periods written as ' . ', ~55 words total, each "
|
| 46 |
+
"sentence stating one contribution or finding.")
|
| 47 |
+
SC = rouge_scorer.RougeScorer(["rougeL"], use_stemmer=False, split_summaries=False)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def to_messages(row):
|
| 51 |
+
return {"messages": [
|
| 52 |
+
{"role": "system", "content": SYS},
|
| 53 |
+
{"role": "user", "content": str(row["Abstract"])},
|
| 54 |
+
{"role": "assistant", "content": str(row["Highlights"])},
|
| 55 |
+
]}
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
@torch.no_grad()
|
| 59 |
+
def gen_eval(model, tok, df, label, save=None):
|
| 60 |
+
model.eval()
|
| 61 |
+
preds = []
|
| 62 |
+
t0 = time.time()
|
| 63 |
+
for i in range(0, len(df), 8):
|
| 64 |
+
batch = df.iloc[i : i + 8]
|
| 65 |
+
prompts = [tok.apply_chat_template(
|
| 66 |
+
[{"role": "system", "content": SYS},
|
| 67 |
+
{"role": "user", "content": str(a)}],
|
| 68 |
+
tokenize=False, add_generation_prompt=True) for a in batch["Abstract"]]
|
| 69 |
+
enc = tok(prompts, return_tensors="pt", padding=True,
|
| 70 |
+
truncation=True, max_length=1024, padding_side="left").to("cuda")
|
| 71 |
+
out = model.generate(**enc, max_new_tokens=130, do_sample=False,
|
| 72 |
+
pad_token_id=tok.pad_token_id)
|
| 73 |
+
for j in range(len(batch)):
|
| 74 |
+
text = tok.decode(out[j][enc.input_ids.shape[1]:],
|
| 75 |
+
skip_special_tokens=True).strip()
|
| 76 |
+
preds.append(text)
|
| 77 |
+
if i == 0:
|
| 78 |
+
print(f"[{label}] sample pred: {preds[0][:200]!r}", flush=True)
|
| 79 |
+
scores = [SC.score(str(g), p)["rougeL"].fmeasure
|
| 80 |
+
for g, p in zip(df["Highlights"], preds)]
|
| 81 |
+
words = float(np.mean([len(p.split()) for p in preds]))
|
| 82 |
+
print(f"[{label}] rougeL={np.mean(scores):.4f} words={words:.1f} "
|
| 83 |
+
f"({time.time() - t0:.0f}s)", flush=True)
|
| 84 |
+
if save:
|
| 85 |
+
pd.DataFrame({"Filename": df["Filename"], "Prediction": preds,
|
| 86 |
+
"rougeL": scores}).to_csv(save, index=False)
|
| 87 |
+
model.train()
|
| 88 |
+
return float(np.mean(scores))
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def main():
|
| 92 |
+
ap = argparse.ArgumentParser()
|
| 93 |
+
ap.add_argument("--train", required=True)
|
| 94 |
+
ap.add_argument("--val", required=True)
|
| 95 |
+
ap.add_argument("--out", default="qwen_out")
|
| 96 |
+
ap.add_argument("--base", default="Qwen/Qwen2.5-7B-Instruct")
|
| 97 |
+
ap.add_argument("--batch", type=int, default=4)
|
| 98 |
+
ap.add_argument("--accum", type=int, default=4)
|
| 99 |
+
args = ap.parse_args()
|
| 100 |
+
global BASE
|
| 101 |
+
BASE = args.base
|
| 102 |
+
|
| 103 |
+
train = pd.read_csv(args.train)
|
| 104 |
+
val = pd.read_csv(args.val)
|
| 105 |
+
trunc = ~val["Abstract"].astype(str).str.rstrip().str.endswith(".")
|
| 106 |
+
n_c = int(300 * (1 - trunc.mean()))
|
| 107 |
+
val_eval = pd.concat([
|
| 108 |
+
val[~trunc].sample(n=n_c, random_state=42),
|
| 109 |
+
val[trunc].sample(n=300 - n_c, random_state=42),
|
| 110 |
+
]).reset_index(drop=True)
|
| 111 |
+
|
| 112 |
+
tok = AutoTokenizer.from_pretrained(BASE)
|
| 113 |
+
model = AutoModelForCausalLM.from_pretrained(
|
| 114 |
+
BASE,
|
| 115 |
+
quantization_config=BitsAndBytesConfig(
|
| 116 |
+
load_in_4bit=True, bnb_4bit_quant_type="nf4",
|
| 117 |
+
bnb_4bit_compute_dtype=torch.bfloat16,
|
| 118 |
+
bnb_4bit_use_double_quant=True),
|
| 119 |
+
dtype=torch.bfloat16, device_map="auto")
|
| 120 |
+
|
| 121 |
+
ds = Dataset.from_list([to_messages(r) for _, r in train.iterrows()])
|
| 122 |
+
peft_cfg = LoraConfig(
|
| 123 |
+
r=16, lora_alpha=32, lora_dropout=0.05, task_type="CAUSAL_LM",
|
| 124 |
+
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
|
| 125 |
+
"gate_proj", "up_proj", "down_proj"])
|
| 126 |
+
sft_cfg = SFTConfig(
|
| 127 |
+
output_dir=args.out, num_train_epochs=1,
|
| 128 |
+
per_device_train_batch_size=args.batch, gradient_accumulation_steps=args.accum,
|
| 129 |
+
learning_rate=2e-4, lr_scheduler_type="cosine", warmup_ratio=0.03,
|
| 130 |
+
bf16=True, logging_steps=50, save_strategy="no",
|
| 131 |
+
max_length=1024, gradient_checkpointing=True, report_to=[])
|
| 132 |
+
trainer = SFTTrainer(model=model, args=sft_cfg, train_dataset=ds,
|
| 133 |
+
peft_config=peft_cfg, processing_class=tok)
|
| 134 |
+
|
| 135 |
+
print("=== pre-SFT zero-shot eval (30 rows, sanity) ===", flush=True)
|
| 136 |
+
gen_eval(trainer.model, tok, val_eval.head(30), "zero-shot")
|
| 137 |
+
|
| 138 |
+
trainer.train()
|
| 139 |
+
trainer.model.save_pretrained(f"{args.out}/adapter")
|
| 140 |
+
tok.save_pretrained(f"{args.out}/adapter")
|
| 141 |
+
|
| 142 |
+
print("=== post-SFT eval (300-row pinned subset) ===", flush=True)
|
| 143 |
+
subset = gen_eval(trainer.model, tok, val_eval, "post-sft-300",
|
| 144 |
+
save=f"{args.out}/val300_predictions.csv")
|
| 145 |
+
full = None
|
| 146 |
+
if subset >= 0.25:
|
| 147 |
+
print("=== subset >= 0.25: full-val pass ===", flush=True)
|
| 148 |
+
full = gen_eval(trainer.model, tok, val, "post-sft-full",
|
| 149 |
+
save=f"{args.out}/val_predictions.csv")
|
| 150 |
+
else:
|
| 151 |
+
print(f"subset {subset:.4f} < 0.25 -> kill-gate zone, skipping full val",
|
| 152 |
+
flush=True)
|
| 153 |
+
json.dump({"subset300": subset, "full_val": full},
|
| 154 |
+
open(f"{args.out}/result.json", "w"), indent=2)
|
| 155 |
+
print(f"DONE subset={subset:.4f} full={full}", flush=True)
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
if __name__ == "__main__":
|
| 159 |
+
main()
|
final/validate_submission.py
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Format gate for a submission CSV. Checks row count against the official
|
| 2 |
+
test split, column schema, empty/duplicate predictions, and length
|
| 3 |
+
statistics. Exits non-zero on any hard failure.
|
| 4 |
+
|
| 5 |
+
python validate_submission.py --submission Yushkk99_Task1_run2.csv \\
|
| 6 |
+
--test data/raw/test.csv
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import argparse
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
+
import pandas as pd
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def main():
|
| 16 |
+
ap = argparse.ArgumentParser()
|
| 17 |
+
ap.add_argument("--submission", required=True)
|
| 18 |
+
ap.add_argument("--test", required=True)
|
| 19 |
+
args = ap.parse_args()
|
| 20 |
+
|
| 21 |
+
sub = pd.read_csv(args.submission)
|
| 22 |
+
test = pd.read_csv(args.test)
|
| 23 |
+
failures = []
|
| 24 |
+
|
| 25 |
+
if list(sub.columns) != ["Filename", "Abstract", "Predicted_Highlights"]:
|
| 26 |
+
failures.append(f"columns are {list(sub.columns)}")
|
| 27 |
+
if len(sub) != len(test):
|
| 28 |
+
failures.append(f"{len(sub)} rows, expected {len(test)}")
|
| 29 |
+
if set(sub["Filename"]) != set(test["Filename"]):
|
| 30 |
+
failures.append("Filename sets differ from official test split")
|
| 31 |
+
empty = sub["Predicted_Highlights"].isna() | (
|
| 32 |
+
sub["Predicted_Highlights"].astype(str).str.strip() == "")
|
| 33 |
+
if empty.any():
|
| 34 |
+
failures.append(f"{int(empty.sum())} empty predictions")
|
| 35 |
+
|
| 36 |
+
words = sub["Predicted_Highlights"].astype(str).str.split().str.len()
|
| 37 |
+
dupes = sub["Predicted_Highlights"].duplicated().sum()
|
| 38 |
+
print(f"rows: {len(sub)}")
|
| 39 |
+
print(f"prediction length: mean {words.mean():.1f} / min {words.min()} / "
|
| 40 |
+
f"max {words.max()} words")
|
| 41 |
+
print(f"duplicate predictions: {dupes}")
|
| 42 |
+
|
| 43 |
+
if failures:
|
| 44 |
+
print("\nFAIL:")
|
| 45 |
+
for f in failures:
|
| 46 |
+
print(f" - {f}")
|
| 47 |
+
return 1
|
| 48 |
+
print("\nPASS: submission format is valid")
|
| 49 |
+
return 0
|
| 50 |
+
|
| 51 |
+
|
| 52 |
+
if __name__ == "__main__":
|
| 53 |
+
raise SystemExit(main())
|
train_final.ipynb
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"cells": [
|
| 3 |
+
{
|
| 4 |
+
"cell_type": "markdown",
|
| 5 |
+
"metadata": {},
|
| 6 |
+
"source": [
|
| 7 |
+
"# SciHigh-2026 Subtask 1 \u2014 bart-large-cnn titled (run 1)\n",
|
| 8 |
+
"\n",
|
| 9 |
+
"Notebook version of `train_final.py` \u2014 run top to bottom.\n"
|
| 10 |
+
]
|
| 11 |
+
},
|
| 12 |
+
{
|
| 13 |
+
"cell_type": "code",
|
| 14 |
+
"execution_count": null,
|
| 15 |
+
"metadata": {},
|
| 16 |
+
"outputs": [],
|
| 17 |
+
"source": [
|
| 18 |
+
"\"\"\"\n",
|
| 19 |
+
"SciHigh-2026 Subtask 1 (Research Highlight Generation) -- final training recipe.\n",
|
| 20 |
+
"\n",
|
| 21 |
+
"Fine-tunes facebook/bart-large-cnn on the expanded MixSub-SciHigh training pool\n",
|
| 22 |
+
"to generate short research highlights from a paper's title and abstract.\n",
|
| 23 |
+
"Inputs are '<title> | <abstract>' with truncated abstracts repaired via\n",
|
| 24 |
+
"DOI-verified Semantic Scholar recovery (see final/ pipeline). This is the\n",
|
| 25 |
+
"exact recipe that produced the submitted result:\n",
|
| 26 |
+
"\n",
|
| 27 |
+
" ROUGE-1=39.30% ROUGE-2=15.32% ROUGE-L=26.37%\n",
|
| 28 |
+
" METEOR=36.17% BERTScore-F1=88.05%\n",
|
| 29 |
+
" (vs. the FIRE-2025 winning baseline of 23.45% ROUGE-L)\n",
|
| 30 |
+
"\n",
|
| 31 |
+
"This script is a clean, standalone extraction of the winning configuration --\n",
|
| 32 |
+
"it deliberately does NOT include the large space of experiments (backbone\n",
|
| 33 |
+
"sweeps, input-feature engineering, decode-time reranking/logit-bias/checkpoint\n",
|
| 34 |
+
"averaging, proxy-scale fast iteration mode, etc.) that were tried and used to\n",
|
| 35 |
+
"arrive at this recipe. Those all live in the project's internal experiment\n",
|
| 36 |
+
"history; none of them are part of what actually produced the result above.\n",
|
| 37 |
+
"\n",
|
| 38 |
+
"Usage:\n",
|
| 39 |
+
" pip install torch transformers accelerate datasets evaluate rouge_score \\\n",
|
| 40 |
+
" sentencepiece bert-score nltk\n",
|
| 41 |
+
" python train_final.py --data_dir /path/to/data --output_dir ./output\n",
|
| 42 |
+
"\n",
|
| 43 |
+
"Expects --data_dir to contain:\n",
|
| 44 |
+
" train_expanded_recovered_titled.csv (15,960 rows: the\n",
|
| 45 |
+
" official 10,000-row MixSub-SciHigh train split plus\n",
|
| 46 |
+
" 5,960 additional real pairs recovered from the\n",
|
| 47 |
+
" dataset's original source release, leakage-checked\n",
|
| 48 |
+
" against val/test by exact Abstract-text match)\n",
|
| 49 |
+
" val_recovered_titled.csv (Filename, Abstract, Highlights -- 1,985 rows, the\n",
|
| 50 |
+
" official held-out validation split, used only for\n",
|
| 51 |
+
" per-epoch monitoring/checkpoint selection here)\n",
|
| 52 |
+
" test_recovered_titled.csv (Filename, Abstract -- 1,840 rows, official masked\n",
|
| 53 |
+
" test split, for the submission predictions)\n",
|
| 54 |
+
"\"\"\"\n",
|
| 55 |
+
"import argparse\n",
|
| 56 |
+
"import json\n",
|
| 57 |
+
"import os\n",
|
| 58 |
+
"\n",
|
| 59 |
+
"import evaluate\n",
|
| 60 |
+
"import nltk\n",
|
| 61 |
+
"import numpy as np\n",
|
| 62 |
+
"import pandas as pd\n",
|
| 63 |
+
"import torch\n",
|
| 64 |
+
"from datasets import Dataset\n",
|
| 65 |
+
"from transformers import (\n",
|
| 66 |
+
" AutoModelForSeq2SeqLM,\n",
|
| 67 |
+
" AutoTokenizer,\n",
|
| 68 |
+
" DataCollatorForSeq2Seq,\n",
|
| 69 |
+
" EarlyStoppingCallback,\n",
|
| 70 |
+
" Seq2SeqTrainer,\n",
|
| 71 |
+
" Seq2SeqTrainingArguments,\n",
|
| 72 |
+
")\n",
|
| 73 |
+
"\n",
|
| 74 |
+
"MODEL_NAME = \"facebook/bart-large-cnn\"\n",
|
| 75 |
+
"MAX_INPUT_LEN = 512\n",
|
| 76 |
+
"MAX_TARGET_LEN = 100 # matches the FIRE-2025 baseline recipe's output budget\n",
|
| 77 |
+
"BATCH_SIZE = 2\n",
|
| 78 |
+
"LEARNING_RATE = 2e-5\n",
|
| 79 |
+
"NUM_BEAMS = 4\n",
|
| 80 |
+
"\n",
|
| 81 |
+
"# Epoch ceiling, not a fixed schedule: bart-large-cnn converges fast on this\n",
|
| 82 |
+
"# task (a small-scale proxy run reached near-final quality in ~600-1,200\n",
|
| 83 |
+
"# gradient steps, a fraction of one full epoch's ~7,980 steps at batch_size=2\n",
|
| 84 |
+
"# over the 15,960-row pool). load_best_model_at_end + EarlyStoppingCallback\n",
|
| 85 |
+
"# below let the run self-terminate rather than committing to a fixed count.\n",
|
| 86 |
+
"# In the actual run that produced the reported numbers, training stopped\n",
|
| 87 |
+
"# after epoch 3 (2 consecutive non-improving epochs), and the checkpoint from\n",
|
| 88 |
+
"# epoch 1 -- the true best on val ROUGE-L -- was the one restored and\n",
|
| 89 |
+
"# evaluated/submitted.\n",
|
| 90 |
+
"EPOCH_CEILING = 4\n",
|
| 91 |
+
"EARLY_STOPPING_PATIENCE = 2\n",
|
| 92 |
+
"\n",
|
| 93 |
+
"for _pkg in [\"wordnet\", \"punkt_tab\", \"omw-1.4\"]:\n",
|
| 94 |
+
" try:\n",
|
| 95 |
+
" nltk.download(_pkg, quiet=True)\n",
|
| 96 |
+
" except Exception as e: # noqa: BLE001\n",
|
| 97 |
+
" print(f\"warning: failed to download nltk resource '{_pkg}': {e}\")\n",
|
| 98 |
+
"\n",
|
| 99 |
+
"\n",
|
| 100 |
+
"def parse_args():\n",
|
| 101 |
+
" p = argparse.ArgumentParser()\n",
|
| 102 |
+
" p.add_argument(\"--data_dir\", required=True)\n",
|
| 103 |
+
" p.add_argument(\"--output_dir\", default=\"output\")\n",
|
| 104 |
+
" p.add_argument(\"--epochs\", type=int, default=EPOCH_CEILING)\n",
|
| 105 |
+
" p.add_argument(\"--skip_bertscore\", action=\"store_true\", help=\"BERTScore eval downloads its own scoring model; skip for a fast local check\")\n",
|
| 106 |
+
" return p.parse_args()\n",
|
| 107 |
+
"\n",
|
| 108 |
+
"\n",
|
| 109 |
+
"def to_hf_dataset(df, has_target):\n",
|
| 110 |
+
" d = {\"Abstract\": df[\"Abstract\"].tolist()}\n",
|
| 111 |
+
" if has_target:\n",
|
| 112 |
+
" d[\"Highlights\"] = df[\"Highlights\"].tolist()\n",
|
| 113 |
+
" return Dataset.from_dict(d)\n",
|
| 114 |
+
"\n",
|
| 115 |
+
"\n",
|
| 116 |
+
"def make_preprocess_fn(tokenizer):\n",
|
| 117 |
+
" def preprocess(batch):\n",
|
| 118 |
+
" model_inputs = tokenizer(batch[\"Abstract\"], max_length=MAX_INPUT_LEN, truncation=True)\n",
|
| 119 |
+
" labels = tokenizer(text_target=batch[\"Highlights\"], max_length=MAX_TARGET_LEN, truncation=True)\n",
|
| 120 |
+
" model_inputs[\"labels\"] = labels[\"input_ids\"]\n",
|
| 121 |
+
" return model_inputs\n",
|
| 122 |
+
"\n",
|
| 123 |
+
" return preprocess\n",
|
| 124 |
+
"\n",
|
| 125 |
+
"\n",
|
| 126 |
+
"def main():\n",
|
| 127 |
+
" args = parse_args()\n",
|
| 128 |
+
" os.makedirs(args.output_dir, exist_ok=True)\n",
|
| 129 |
+
" device = \"cuda\" if torch.cuda.is_available() else \"cpu\"\n",
|
| 130 |
+
"\n",
|
| 131 |
+
" train_df = pd.read_csv(os.path.join(args.data_dir, \"train_expanded_recovered_titled.csv\"))\n",
|
| 132 |
+
" val_df = pd.read_csv(os.path.join(args.data_dir, \"val_recovered_titled.csv\"))\n",
|
| 133 |
+
" test_df = pd.read_csv(os.path.join(args.data_dir, \"test_recovered_titled.csv\"))\n",
|
| 134 |
+
" print(f\"[train_final] device={device} train={len(train_df)} val={len(val_df)} test={len(test_df)}\")\n",
|
| 135 |
+
"\n",
|
| 136 |
+
" tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)\n",
|
| 137 |
+
" model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(device)\n",
|
| 138 |
+
"\n",
|
| 139 |
+
" # Decode-time settings: bart-large-cnn already ships no_repeat_ngram_size=3\n",
|
| 140 |
+
" # in its own generation_config, but it's set explicitly here rather than\n",
|
| 141 |
+
" # left implicit, so the decision doesn't silently depend on that shipped\n",
|
| 142 |
+
" # default surviving a future transformers/model-card change. Two other\n",
|
| 143 |
+
" # settings that were tuned during small-scale proxy experiments\n",
|
| 144 |
+
" # (repetition_penalty=1.5, min_new_tokens=15) were fixes for degenerate\n",
|
| 145 |
+
" # repetition-collapse in a severely undertrained checkpoint -- this\n",
|
| 146 |
+
" # full-scale, fully-converged model doesn't exhibit that failure mode, so\n",
|
| 147 |
+
" # those are deliberately left untouched at bart-large-cnn's own defaults\n",
|
| 148 |
+
" # (repetition_penalty=1.0, min_length=56) rather than carried over.\n",
|
| 149 |
+
" model.generation_config.no_repeat_ngram_size = 3\n",
|
| 150 |
+
" model.generation_config.max_length = MAX_TARGET_LEN\n",
|
| 151 |
+
"\n",
|
| 152 |
+
" train_ds = to_hf_dataset(train_df, has_target=True)\n",
|
| 153 |
+
" val_ds = to_hf_dataset(val_df, has_target=True)\n",
|
| 154 |
+
" preprocess = make_preprocess_fn(tokenizer)\n",
|
| 155 |
+
" train_tok = train_ds.map(preprocess, batched=True, remove_columns=train_ds.column_names)\n",
|
| 156 |
+
" val_tok = val_ds.map(preprocess, batched=True, remove_columns=val_ds.column_names)\n",
|
| 157 |
+
" collator = DataCollatorForSeq2Seq(tokenizer, model=model)\n",
|
| 158 |
+
"\n",
|
| 159 |
+
" rouge = evaluate.load(\"rouge\")\n",
|
| 160 |
+
" meteor = evaluate.load(\"meteor\")\n",
|
| 161 |
+
"\n",
|
| 162 |
+
" def compute_metrics(eval_preds):\n",
|
| 163 |
+
" preds, labels = eval_preds\n",
|
| 164 |
+
" if isinstance(preds, tuple):\n",
|
| 165 |
+
" preds = preds[0]\n",
|
| 166 |
+
" preds = np.where(preds != -100, preds, tokenizer.pad_token_id)\n",
|
| 167 |
+
" decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)\n",
|
| 168 |
+
" labels = np.where(labels != -100, labels, tokenizer.pad_token_id)\n",
|
| 169 |
+
" decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)\n",
|
| 170 |
+
"\n",
|
| 171 |
+
" result = rouge.compute(predictions=decoded_preds, references=decoded_labels)\n",
|
| 172 |
+
" result = {f\"rouge_{k}\": v for k, v in result.items()}\n",
|
| 173 |
+
" result[\"meteor\"] = meteor.compute(predictions=decoded_preds, references=decoded_labels)[\"meteor\"]\n",
|
| 174 |
+
" return result\n",
|
| 175 |
+
"\n",
|
| 176 |
+
" training_args = Seq2SeqTrainingArguments(\n",
|
| 177 |
+
" output_dir=os.path.join(args.output_dir, \"checkpoints\"),\n",
|
| 178 |
+
" num_train_epochs=args.epochs,\n",
|
| 179 |
+
" per_device_train_batch_size=BATCH_SIZE,\n",
|
| 180 |
+
" per_device_eval_batch_size=BATCH_SIZE,\n",
|
| 181 |
+
" learning_rate=LEARNING_RATE,\n",
|
| 182 |
+
" label_smoothing_factor=0.0,\n",
|
| 183 |
+
" warmup_ratio=0.0,\n",
|
| 184 |
+
" # Adafactor's factored second-moment estimates avoid the full-size\n",
|
| 185 |
+
" # exp_avg/exp_avg_sq buffers that OOM'd a 16GB T4 with plain Adam; it's\n",
|
| 186 |
+
" # also what the original PEGASUS/BART pretraining used.\n",
|
| 187 |
+
" optim=\"adafactor\",\n",
|
| 188 |
+
" predict_with_generate=True,\n",
|
| 189 |
+
" generation_max_length=MAX_TARGET_LEN,\n",
|
| 190 |
+
" generation_num_beams=NUM_BEAMS,\n",
|
| 191 |
+
" eval_strategy=\"epoch\",\n",
|
| 192 |
+
" save_strategy=\"epoch\",\n",
|
| 193 |
+
" save_total_limit=1,\n",
|
| 194 |
+
" load_best_model_at_end=True,\n",
|
| 195 |
+
" metric_for_best_model=\"rouge_rougeL\",\n",
|
| 196 |
+
" fp16=(device == \"cuda\"),\n",
|
| 197 |
+
" logging_steps=50,\n",
|
| 198 |
+
" report_to=[],\n",
|
| 199 |
+
" )\n",
|
| 200 |
+
"\n",
|
| 201 |
+
" trainer = Seq2SeqTrainer(\n",
|
| 202 |
+
" model=model,\n",
|
| 203 |
+
" args=training_args,\n",
|
| 204 |
+
" train_dataset=train_tok,\n",
|
| 205 |
+
" eval_dataset=val_tok,\n",
|
| 206 |
+
" data_collator=collator,\n",
|
| 207 |
+
" compute_metrics=compute_metrics,\n",
|
| 208 |
+
" callbacks=[EarlyStoppingCallback(early_stopping_patience=EARLY_STOPPING_PATIENCE)],\n",
|
| 209 |
+
" )\n",
|
| 210 |
+
"\n",
|
| 211 |
+
" trainer.train()\n",
|
| 212 |
+
"\n",
|
| 213 |
+
" final_model_dir = os.path.join(args.output_dir, \"model\")\n",
|
| 214 |
+
" trainer.save_model(final_model_dir)\n",
|
| 215 |
+
" tokenizer.save_pretrained(final_model_dir)\n",
|
| 216 |
+
"\n",
|
| 217 |
+
" val_metrics = trainer.evaluate()\n",
|
| 218 |
+
" print(\"[train_final] validation metrics:\", val_metrics)\n",
|
| 219 |
+
"\n",
|
| 220 |
+
" if not args.skip_bertscore:\n",
|
| 221 |
+
" from bert_score import score as bertscore\n",
|
| 222 |
+
"\n",
|
| 223 |
+
" val_preds = trainer.predict(val_tok)\n",
|
| 224 |
+
" preds = np.where(val_preds.predictions != -100, val_preds.predictions, tokenizer.pad_token_id)\n",
|
| 225 |
+
" decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)\n",
|
| 226 |
+
" _, _, f1 = bertscore(decoded_preds, val_df[\"Highlights\"].tolist(), lang=\"en\", verbose=False)\n",
|
| 227 |
+
" val_metrics[\"bertscore_f1\"] = float(f1.mean())\n",
|
| 228 |
+
" print(\"[train_final] bertscore_f1:\", val_metrics[\"bertscore_f1\"])\n",
|
| 229 |
+
"\n",
|
| 230 |
+
" with open(os.path.join(args.output_dir, \"val_metrics.json\"), \"w\") as f:\n",
|
| 231 |
+
" json.dump(val_metrics, f, indent=2)\n",
|
| 232 |
+
"\n",
|
| 233 |
+
" # Generate the submission predictions on the masked test set.\n",
|
| 234 |
+
" model.eval()\n",
|
| 235 |
+
" gen_device = next(model.parameters()).device\n",
|
| 236 |
+
" predictions = []\n",
|
| 237 |
+
" batch_size = max(BATCH_SIZE, 8)\n",
|
| 238 |
+
" abstracts = test_df[\"Abstract\"].tolist()\n",
|
| 239 |
+
" for i in range(0, len(abstracts), batch_size):\n",
|
| 240 |
+
" batch = abstracts[i : i + batch_size]\n",
|
| 241 |
+
" inputs = tokenizer(batch, max_length=MAX_INPUT_LEN, truncation=True, padding=True, return_tensors=\"pt\").to(gen_device)\n",
|
| 242 |
+
" with torch.no_grad():\n",
|
| 243 |
+
" generated = model.generate(**inputs, max_length=MAX_TARGET_LEN, num_beams=NUM_BEAMS)\n",
|
| 244 |
+
" predictions.extend(tokenizer.batch_decode(generated, skip_special_tokens=True))\n",
|
| 245 |
+
"\n",
|
| 246 |
+
" submission = pd.DataFrame({\n",
|
| 247 |
+
" \"Filename\": test_df[\"Filename\"],\n",
|
| 248 |
+
" \"Abstract\": test_df[\"Abstract\"],\n",
|
| 249 |
+
" \"Predicted_Highlights\": predictions,\n",
|
| 250 |
+
" })\n",
|
| 251 |
+
" submission_path = os.path.join(args.output_dir, \"Yushkk99_Task1_run1.csv\")\n",
|
| 252 |
+
" submission.to_csv(submission_path, index=False)\n",
|
| 253 |
+
" print(f\"[train_final] wrote submission to {submission_path}\")\n",
|
| 254 |
+
"\n",
|
| 255 |
+
"\n",
|
| 256 |
+
"if __name__ == \"__main__\":\n",
|
| 257 |
+
" main()\n"
|
| 258 |
+
]
|
| 259 |
+
}
|
| 260 |
+
],
|
| 261 |
+
"metadata": {
|
| 262 |
+
"kernelspec": {
|
| 263 |
+
"display_name": "Python 3",
|
| 264 |
+
"language": "python",
|
| 265 |
+
"name": "python3"
|
| 266 |
+
},
|
| 267 |
+
"language_info": {
|
| 268 |
+
"name": "python",
|
| 269 |
+
"version": "3.11"
|
| 270 |
+
}
|
| 271 |
+
},
|
| 272 |
+
"nbformat": 4,
|
| 273 |
+
"nbformat_minor": 5
|
| 274 |
+
}
|
train_final.py
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
SciHigh-2026 Subtask 1 (Research Highlight Generation) -- final training recipe.
|
| 3 |
+
|
| 4 |
+
Fine-tunes facebook/bart-large-cnn on the expanded MixSub-SciHigh training pool
|
| 5 |
+
to generate short research highlights from a paper's title and abstract.
|
| 6 |
+
Inputs are '<title> | <abstract>' with truncated abstracts repaired via
|
| 7 |
+
DOI-verified Semantic Scholar recovery (see final/ pipeline). This is the
|
| 8 |
+
exact recipe that produced the submitted result:
|
| 9 |
+
|
| 10 |
+
ROUGE-1=39.30% ROUGE-2=15.32% ROUGE-L=26.37%
|
| 11 |
+
METEOR=36.17% BERTScore-F1=88.05%
|
| 12 |
+
(vs. the FIRE-2025 winning baseline of 23.45% ROUGE-L)
|
| 13 |
+
|
| 14 |
+
This script is a clean, standalone extraction of the winning configuration --
|
| 15 |
+
it deliberately does NOT include the large space of experiments (backbone
|
| 16 |
+
sweeps, input-feature engineering, decode-time reranking/logit-bias/checkpoint
|
| 17 |
+
averaging, proxy-scale fast iteration mode, etc.) that were tried and used to
|
| 18 |
+
arrive at this recipe. Those all live in the project's internal experiment
|
| 19 |
+
history; none of them are part of what actually produced the result above.
|
| 20 |
+
|
| 21 |
+
Usage:
|
| 22 |
+
pip install torch transformers accelerate datasets evaluate rouge_score \
|
| 23 |
+
sentencepiece bert-score nltk
|
| 24 |
+
python train_final.py --data_dir /path/to/data --output_dir ./output
|
| 25 |
+
|
| 26 |
+
Expects --data_dir to contain:
|
| 27 |
+
train_expanded_recovered_titled.csv (15,960 rows: the
|
| 28 |
+
official 10,000-row MixSub-SciHigh train split plus
|
| 29 |
+
5,960 additional real pairs recovered from the
|
| 30 |
+
dataset's original source release, leakage-checked
|
| 31 |
+
against val/test by exact Abstract-text match)
|
| 32 |
+
val_recovered_titled.csv (Filename, Abstract, Highlights -- 1,985 rows, the
|
| 33 |
+
official held-out validation split, used only for
|
| 34 |
+
per-epoch monitoring/checkpoint selection here)
|
| 35 |
+
test_recovered_titled.csv (Filename, Abstract -- 1,840 rows, official masked
|
| 36 |
+
test split, for the submission predictions)
|
| 37 |
+
"""
|
| 38 |
+
import argparse
|
| 39 |
+
import json
|
| 40 |
+
import os
|
| 41 |
+
|
| 42 |
+
import evaluate
|
| 43 |
+
import nltk
|
| 44 |
+
import numpy as np
|
| 45 |
+
import pandas as pd
|
| 46 |
+
import torch
|
| 47 |
+
from datasets import Dataset
|
| 48 |
+
from transformers import (
|
| 49 |
+
AutoModelForSeq2SeqLM,
|
| 50 |
+
AutoTokenizer,
|
| 51 |
+
DataCollatorForSeq2Seq,
|
| 52 |
+
EarlyStoppingCallback,
|
| 53 |
+
Seq2SeqTrainer,
|
| 54 |
+
Seq2SeqTrainingArguments,
|
| 55 |
+
)
|
| 56 |
+
|
| 57 |
+
MODEL_NAME = "facebook/bart-large-cnn"
|
| 58 |
+
MAX_INPUT_LEN = 512
|
| 59 |
+
MAX_TARGET_LEN = 100 # matches the FIRE-2025 baseline recipe's output budget
|
| 60 |
+
BATCH_SIZE = 2
|
| 61 |
+
LEARNING_RATE = 2e-5
|
| 62 |
+
NUM_BEAMS = 4
|
| 63 |
+
|
| 64 |
+
# Epoch ceiling, not a fixed schedule: bart-large-cnn converges fast on this
|
| 65 |
+
# task (a small-scale proxy run reached near-final quality in ~600-1,200
|
| 66 |
+
# gradient steps, a fraction of one full epoch's ~7,980 steps at batch_size=2
|
| 67 |
+
# over the 15,960-row pool). load_best_model_at_end + EarlyStoppingCallback
|
| 68 |
+
# below let the run self-terminate rather than committing to a fixed count.
|
| 69 |
+
# In the actual run that produced the reported numbers, training stopped
|
| 70 |
+
# after epoch 3 (2 consecutive non-improving epochs), and the checkpoint from
|
| 71 |
+
# epoch 1 -- the true best on val ROUGE-L -- was the one restored and
|
| 72 |
+
# evaluated/submitted.
|
| 73 |
+
EPOCH_CEILING = 4
|
| 74 |
+
EARLY_STOPPING_PATIENCE = 2
|
| 75 |
+
|
| 76 |
+
for _pkg in ["wordnet", "punkt_tab", "omw-1.4"]:
|
| 77 |
+
try:
|
| 78 |
+
nltk.download(_pkg, quiet=True)
|
| 79 |
+
except Exception as e: # noqa: BLE001
|
| 80 |
+
print(f"warning: failed to download nltk resource '{_pkg}': {e}")
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
def parse_args():
|
| 84 |
+
p = argparse.ArgumentParser()
|
| 85 |
+
p.add_argument("--data_dir", required=True)
|
| 86 |
+
p.add_argument("--output_dir", default="output")
|
| 87 |
+
p.add_argument("--epochs", type=int, default=EPOCH_CEILING)
|
| 88 |
+
p.add_argument("--skip_bertscore", action="store_true", help="BERTScore eval downloads its own scoring model; skip for a fast local check")
|
| 89 |
+
return p.parse_args()
|
| 90 |
+
|
| 91 |
+
|
| 92 |
+
def to_hf_dataset(df, has_target):
|
| 93 |
+
d = {"Abstract": df["Abstract"].tolist()}
|
| 94 |
+
if has_target:
|
| 95 |
+
d["Highlights"] = df["Highlights"].tolist()
|
| 96 |
+
return Dataset.from_dict(d)
|
| 97 |
+
|
| 98 |
+
|
| 99 |
+
def make_preprocess_fn(tokenizer):
|
| 100 |
+
def preprocess(batch):
|
| 101 |
+
model_inputs = tokenizer(batch["Abstract"], max_length=MAX_INPUT_LEN, truncation=True)
|
| 102 |
+
labels = tokenizer(text_target=batch["Highlights"], max_length=MAX_TARGET_LEN, truncation=True)
|
| 103 |
+
model_inputs["labels"] = labels["input_ids"]
|
| 104 |
+
return model_inputs
|
| 105 |
+
|
| 106 |
+
return preprocess
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def main():
|
| 110 |
+
args = parse_args()
|
| 111 |
+
os.makedirs(args.output_dir, exist_ok=True)
|
| 112 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 113 |
+
|
| 114 |
+
train_df = pd.read_csv(os.path.join(args.data_dir, "train_expanded_recovered_titled.csv"))
|
| 115 |
+
val_df = pd.read_csv(os.path.join(args.data_dir, "val_recovered_titled.csv"))
|
| 116 |
+
test_df = pd.read_csv(os.path.join(args.data_dir, "test_recovered_titled.csv"))
|
| 117 |
+
print(f"[train_final] device={device} train={len(train_df)} val={len(val_df)} test={len(test_df)}")
|
| 118 |
+
|
| 119 |
+
tokenizer = AutoTokenizer.from_pretrained(MODEL_NAME)
|
| 120 |
+
model = AutoModelForSeq2SeqLM.from_pretrained(MODEL_NAME).to(device)
|
| 121 |
+
|
| 122 |
+
# Decode-time settings: bart-large-cnn already ships no_repeat_ngram_size=3
|
| 123 |
+
# in its own generation_config, but it's set explicitly here rather than
|
| 124 |
+
# left implicit, so the decision doesn't silently depend on that shipped
|
| 125 |
+
# default surviving a future transformers/model-card change. Two other
|
| 126 |
+
# settings that were tuned during small-scale proxy experiments
|
| 127 |
+
# (repetition_penalty=1.5, min_new_tokens=15) were fixes for degenerate
|
| 128 |
+
# repetition-collapse in a severely undertrained checkpoint -- this
|
| 129 |
+
# full-scale, fully-converged model doesn't exhibit that failure mode, so
|
| 130 |
+
# those are deliberately left untouched at bart-large-cnn's own defaults
|
| 131 |
+
# (repetition_penalty=1.0, min_length=56) rather than carried over.
|
| 132 |
+
model.generation_config.no_repeat_ngram_size = 3
|
| 133 |
+
model.generation_config.max_length = MAX_TARGET_LEN
|
| 134 |
+
|
| 135 |
+
train_ds = to_hf_dataset(train_df, has_target=True)
|
| 136 |
+
val_ds = to_hf_dataset(val_df, has_target=True)
|
| 137 |
+
preprocess = make_preprocess_fn(tokenizer)
|
| 138 |
+
train_tok = train_ds.map(preprocess, batched=True, remove_columns=train_ds.column_names)
|
| 139 |
+
val_tok = val_ds.map(preprocess, batched=True, remove_columns=val_ds.column_names)
|
| 140 |
+
collator = DataCollatorForSeq2Seq(tokenizer, model=model)
|
| 141 |
+
|
| 142 |
+
rouge = evaluate.load("rouge")
|
| 143 |
+
meteor = evaluate.load("meteor")
|
| 144 |
+
|
| 145 |
+
def compute_metrics(eval_preds):
|
| 146 |
+
preds, labels = eval_preds
|
| 147 |
+
if isinstance(preds, tuple):
|
| 148 |
+
preds = preds[0]
|
| 149 |
+
preds = np.where(preds != -100, preds, tokenizer.pad_token_id)
|
| 150 |
+
decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
|
| 151 |
+
labels = np.where(labels != -100, labels, tokenizer.pad_token_id)
|
| 152 |
+
decoded_labels = tokenizer.batch_decode(labels, skip_special_tokens=True)
|
| 153 |
+
|
| 154 |
+
result = rouge.compute(predictions=decoded_preds, references=decoded_labels)
|
| 155 |
+
result = {f"rouge_{k}": v for k, v in result.items()}
|
| 156 |
+
result["meteor"] = meteor.compute(predictions=decoded_preds, references=decoded_labels)["meteor"]
|
| 157 |
+
return result
|
| 158 |
+
|
| 159 |
+
training_args = Seq2SeqTrainingArguments(
|
| 160 |
+
output_dir=os.path.join(args.output_dir, "checkpoints"),
|
| 161 |
+
num_train_epochs=args.epochs,
|
| 162 |
+
per_device_train_batch_size=BATCH_SIZE,
|
| 163 |
+
per_device_eval_batch_size=BATCH_SIZE,
|
| 164 |
+
learning_rate=LEARNING_RATE,
|
| 165 |
+
label_smoothing_factor=0.0,
|
| 166 |
+
warmup_ratio=0.0,
|
| 167 |
+
# Adafactor's factored second-moment estimates avoid the full-size
|
| 168 |
+
# exp_avg/exp_avg_sq buffers that OOM'd a 16GB T4 with plain Adam; it's
|
| 169 |
+
# also what the original PEGASUS/BART pretraining used.
|
| 170 |
+
optim="adafactor",
|
| 171 |
+
predict_with_generate=True,
|
| 172 |
+
generation_max_length=MAX_TARGET_LEN,
|
| 173 |
+
generation_num_beams=NUM_BEAMS,
|
| 174 |
+
eval_strategy="epoch",
|
| 175 |
+
save_strategy="epoch",
|
| 176 |
+
save_total_limit=1,
|
| 177 |
+
load_best_model_at_end=True,
|
| 178 |
+
metric_for_best_model="rouge_rougeL",
|
| 179 |
+
fp16=(device == "cuda"),
|
| 180 |
+
logging_steps=50,
|
| 181 |
+
report_to=[],
|
| 182 |
+
)
|
| 183 |
+
|
| 184 |
+
trainer = Seq2SeqTrainer(
|
| 185 |
+
model=model,
|
| 186 |
+
args=training_args,
|
| 187 |
+
train_dataset=train_tok,
|
| 188 |
+
eval_dataset=val_tok,
|
| 189 |
+
data_collator=collator,
|
| 190 |
+
compute_metrics=compute_metrics,
|
| 191 |
+
callbacks=[EarlyStoppingCallback(early_stopping_patience=EARLY_STOPPING_PATIENCE)],
|
| 192 |
+
)
|
| 193 |
+
|
| 194 |
+
trainer.train()
|
| 195 |
+
|
| 196 |
+
final_model_dir = os.path.join(args.output_dir, "model")
|
| 197 |
+
trainer.save_model(final_model_dir)
|
| 198 |
+
tokenizer.save_pretrained(final_model_dir)
|
| 199 |
+
|
| 200 |
+
val_metrics = trainer.evaluate()
|
| 201 |
+
print("[train_final] validation metrics:", val_metrics)
|
| 202 |
+
|
| 203 |
+
if not args.skip_bertscore:
|
| 204 |
+
from bert_score import score as bertscore
|
| 205 |
+
|
| 206 |
+
val_preds = trainer.predict(val_tok)
|
| 207 |
+
preds = np.where(val_preds.predictions != -100, val_preds.predictions, tokenizer.pad_token_id)
|
| 208 |
+
decoded_preds = tokenizer.batch_decode(preds, skip_special_tokens=True)
|
| 209 |
+
_, _, f1 = bertscore(decoded_preds, val_df["Highlights"].tolist(), lang="en", verbose=False)
|
| 210 |
+
val_metrics["bertscore_f1"] = float(f1.mean())
|
| 211 |
+
print("[train_final] bertscore_f1:", val_metrics["bertscore_f1"])
|
| 212 |
+
|
| 213 |
+
with open(os.path.join(args.output_dir, "val_metrics.json"), "w") as f:
|
| 214 |
+
json.dump(val_metrics, f, indent=2)
|
| 215 |
+
|
| 216 |
+
# Generate the submission predictions on the masked test set.
|
| 217 |
+
model.eval()
|
| 218 |
+
gen_device = next(model.parameters()).device
|
| 219 |
+
predictions = []
|
| 220 |
+
batch_size = max(BATCH_SIZE, 8)
|
| 221 |
+
abstracts = test_df["Abstract"].tolist()
|
| 222 |
+
for i in range(0, len(abstracts), batch_size):
|
| 223 |
+
batch = abstracts[i : i + batch_size]
|
| 224 |
+
inputs = tokenizer(batch, max_length=MAX_INPUT_LEN, truncation=True, padding=True, return_tensors="pt").to(gen_device)
|
| 225 |
+
with torch.no_grad():
|
| 226 |
+
generated = model.generate(**inputs, max_length=MAX_TARGET_LEN, num_beams=NUM_BEAMS)
|
| 227 |
+
predictions.extend(tokenizer.batch_decode(generated, skip_special_tokens=True))
|
| 228 |
+
|
| 229 |
+
submission = pd.DataFrame({
|
| 230 |
+
"Filename": test_df["Filename"],
|
| 231 |
+
"Abstract": test_df["Abstract"],
|
| 232 |
+
"Predicted_Highlights": predictions,
|
| 233 |
+
})
|
| 234 |
+
submission_path = os.path.join(args.output_dir, "Yushkk99_Task1_run1.csv")
|
| 235 |
+
submission.to_csv(submission_path, index=False)
|
| 236 |
+
print(f"[train_final] wrote submission to {submission_path}")
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
if __name__ == "__main__":
|
| 240 |
+
main()
|