Spaces:
Sleeping
Sleeping
Download tools/train_models.py from phamluan/namboqa-api: direct link, hf CLI and curl.
- Browser
- Download file 14.9 kB
-
https://huggingface.co/spaces/phamluan/namboqa-api/resolve/main/tools/train_models.py
- Command line
-
hf download hf://spaces/phamluan/namboqa-api/tools/train_models.py
-
curl -L -o train_models.py https://huggingface.co/spaces/phamluan/namboqa-api/resolve/main/tools/train_models.py
14.9 kB
| # -*- coding: utf-8 -*- | |
| """ | |
| Tái tạo bi-encoder và bộ đọc trích xuất ĐÃ FINE-TUNE theo bài (bộ thực nghiệm | |
| 25/10/2026), kiểm tra nhanh trên tập test, rồi đẩy lên HF Hub cho Space dùng. | |
| Bộ thực nghiệm không lưu trọng số (run_reading.py: save_strategy="no", không có | |
| save_pretrained; run_experiments.py: --save-model tắt), nên phải huấn luyện lại. | |
| Cấu hình (--config, giống tên trong ketqua_ablation_v2/): | |
| silver_sinh gold + silver sinh mới F1 trích xuất top-5 0,745 (tốt nhất) -> mặc định | |
| chinh gold + silver người duyệt cấu hình chính của bài (0,679) | |
| gold chỉ gold 0,717 | |
| Chạy trên vast.ai: xem run_vast_train.sh (cài thư viện, chạy nền, ghi log, bật Vast Monitor). | |
| export HF_TOKEN=hf_... # token WRITE của tài khoản phamluan | |
| python tools/train_models.py --data data --user phamluan --config silver_sinh \ | |
| --update-space phamluan/namboqa-api --data-repo phamluan/namboqa-private | |
| Xong việc, script: | |
| 1. đẩy <user>/namboqa-bi-encoder-<cfg> và <user>/namboqa-xlmr-reader-<cfg> (private) | |
| 2. tính embedding corpus -> emb_*.npy, đẩy vào dataset private (Space khỏi mã hoá lại) | |
| 3. đánh giá trên 695 câu test bằng ĐÚNG code của demo (engine.py) và in so sánh với bài | |
| 4. (--update-space) đặt RETRIEVER_MODEL / READER_MODEL cho Space rồi restart | |
| Cách chia (chia_tap.chia_theo_tinh, seed 42), nạp silver (chia_tap.nap_silver) và | |
| siêu tham số y hệt nambo_run/code/run_experiments.py và run_reading.py. | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import random | |
| import re | |
| import string | |
| import sys | |
| import time | |
| import unicodedata | |
| from collections import Counter | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| ROOT = Path(__file__).resolve().parent.parent | |
| sys.path.insert(0, str(ROOT)) | |
| import chia_tap # noqa: E402 | |
| from vm import vm_finish, vm_log, vm_start # noqa: E402 | |
| CONFIGS = { | |
| "silver_sinh": dict(no_silver=False, nguon="sinh"), | |
| "chinh": dict(no_silver=False, nguon=None), | |
| "gold": dict(no_silver=True, nguon=None), | |
| } | |
| # Số liệu bài báo để đối chiếu (seed 42 / trung bình 3 seed) | |
| PAPER = { | |
| "silver_sinh": {"R@5 hybrid": 0.882, "F1 oracle": 0.906, "F1 top-5": 0.745}, | |
| "chinh": {"R@5 hybrid": 0.881, "F1 oracle": 0.901, "F1 top-5": 0.679, "F1 top-3": 0.717}, | |
| "gold": {"R@5 hybrid": 0.882, "F1 oracle": 0.903, "F1 top-5": 0.717, "F1 top-3": 0.734}, | |
| } | |
| RUN = "namboqa_demo" | |
| def load_jsonl(p): | |
| return [json.loads(l) for l in open(p, encoding="utf-8") if l.strip()] | |
| # chấm điểm: chép từ run_reading.py | |
| def norm_ans(s): | |
| s = unicodedata.normalize("NFC", (s or "").lower().strip()) | |
| s = "".join(c for c in s if c not in string.punctuation) | |
| s = re.sub(r"\b(là|của|tại|ở|vào|năm|the|a|an)\b", " ", s) | |
| return re.sub(r"\s+", " ", s).strip() | |
| def f1_score(pred, gold): | |
| pt, gt = norm_ans(pred).split(), norm_ans(gold).split() | |
| if not pt or not gt: | |
| return float(pt == gt) | |
| same = sum((Counter(pt) & Counter(gt)).values()) | |
| if same == 0: | |
| return 0.0 | |
| p, r = same / len(pt), same / len(gt) | |
| return 2 * p * r / (p + r) | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--data", required=True, help="thư mục có qa_final, passages, places_clean, silver_final") | |
| ap.add_argument("--user", required=True, help="tài khoản HF, vd phamluan") | |
| ap.add_argument("--config", default="silver_sinh", choices=list(CONFIGS)) | |
| ap.add_argument("--seed", type=int, default=42) | |
| ap.add_argument("--public", action="store_true", help="đẩy mô hình dạng public") | |
| ap.add_argument("--skip-retriever", action="store_true") | |
| ap.add_argument("--skip-reader", action="store_true") | |
| ap.add_argument("--skip-push", action="store_true", help="chỉ train + đánh giá, không đẩy lên Hub") | |
| ap.add_argument("--update-space", default=None, help="vd phamluan/namboqa-api") | |
| ap.add_argument("--data-repo", default=None, help="dataset private để đẩy emb_*.npy, vd phamluan/namboqa-private") | |
| ap.add_argument("--out", default="trained") | |
| a = ap.parse_args() | |
| cfg = CONFIGS[a.config] | |
| global RUN | |
| run = RUN = f"namboqa_demo_{a.config}_s{a.seed}" | |
| vm_start(run, config=vars(a)) | |
| random.seed(a.seed); np.random.seed(a.seed); torch.manual_seed(a.seed) | |
| dev = "cuda" if torch.cuda.is_available() else ("mps" if torch.backends.mps.is_available() else "cpu") | |
| print("Thiết bị:", dev, torch.cuda.get_device_name(0) if dev == "cuda" else "") | |
| d, out = Path(a.data), Path(a.out) | |
| out.mkdir(exist_ok=True) | |
| passages = load_jsonl(d / "passages.jsonl") | |
| pid2text = {p["id"]: p["text"] for p in passages} | |
| qa = load_jsonl(d / "qa_final.jsonl") | |
| for q in qa: | |
| q["gold"] = [g for g in q["gold_passage_ids"] if g in pid2text] | |
| qa = [q for q in qa if q["gold"]] | |
| train_qa, _, test_qa, tk = chia_tap.chia_theo_tinh(qa, chia_tap.tinh_cua(d)) | |
| test_qa, _ = chia_tap.tach_danh_gia(test_qa) | |
| silver = [] | |
| if not cfg["no_silver"]: | |
| silver, tk_sv = chia_tap.nap_silver(d, qa, tk["train_p"], pid2text, nguon=cfg["nguon"]) | |
| print("Silver:", tk_sv) | |
| print(f"Cấu hình {a.config}: train gold {len(train_qa)} câu / {len(tk['train_p'])} địa điểm, " | |
| f"silver {len(silver)}, test {len(test_qa)}") | |
| private = not a.public | |
| tag = a.config.replace("_", "-") | |
| retr_repo = f"{a.user}/namboqa-bi-encoder-{tag}" | |
| read_repo = f"{a.user}/namboqa-xlmr-reader-{tag}" | |
| emb_file = out / f"emb_{re.sub(r'[^\w.-]', '_', retr_repo)}.npy" | |
| res = {"config": a.config, "seed": a.seed} | |
| # ------------------------------------------------ bi-encoder (run_experiments.py) | |
| if not a.skip_retriever: | |
| from sentence_transformers import InputExample, SentenceTransformer, losses | |
| from torch.utils.data import DataLoader | |
| samples = [InputExample(texts=[q["question"], pid2text[q["gold"][0]]]) | |
| for q in train_qa + silver if q["type"] != "unanswerable"] | |
| print(f"Fine-tune bi-encoder: {len(samples)} cặp") | |
| ft = SentenceTransformer("bkai-foundation-models/vietnamese-bi-encoder", device=dev) | |
| dl = DataLoader(samples, shuffle=True, batch_size=16, drop_last=True) | |
| t0 = time.time() | |
| ft.fit(train_objectives=[(dl, losses.MultipleNegativesRankingLoss(ft))], | |
| epochs=3, warmup_steps=int(0.1 * len(dl) * 3), show_progress_bar=True) | |
| res["bi_encoder_phut"] = (time.time() - t0) / 60 | |
| vm_log(run, bi_encoder_phut=res["bi_encoder_phut"], bi_encoder_cap=len(samples)) | |
| ft.save(str(out / f"bi_encoder_{tag}")) | |
| emb = ft.encode([p["text"] for p in passages], batch_size=64, | |
| normalize_embeddings=True, convert_to_numpy=True) | |
| np.save(emb_file, emb.astype(np.float32)) | |
| if not a.skip_push: | |
| ft.push_to_hub(retr_repo, private=private, exist_ok=True) | |
| print("Đã đẩy", retr_repo) | |
| del ft | |
| if dev == "cuda": | |
| torch.cuda.empty_cache() | |
| # ------------------------------------------------ extractive (run_reading.py) | |
| if not a.skip_reader: | |
| from datasets import Dataset | |
| from transformers import (AutoModelForQuestionAnswering, AutoTokenizer, Trainer, | |
| TrainerCallback, TrainingArguments, default_data_collator) | |
| base = "deepset/xlm-roberta-base-squad2" | |
| tok = AutoTokenizer.from_pretrained(base) | |
| model = AutoModelForQuestionAnswering.from_pretrained(base) | |
| ex, miss = [], 0 | |
| for q in train_qa + silver: | |
| if q["type"] == "unanswerable": | |
| ex.append({"question": q["question"], "context": pid2text[q["gold"][0]], | |
| "answer": "", "start": 0, "impossible": True}) | |
| continue | |
| ctx = " ".join(pid2text[g] for g in q["gold"]) | |
| pos = ctx.find(q["answer"]) | |
| if pos < 0: | |
| miss += 1 | |
| continue | |
| ex.append({"question": q["question"], "context": ctx, | |
| "answer": q["answer"], "start": pos, "impossible": False}) | |
| print(f"Mẫu extractive: {len(ex)} (bỏ {miss} câu không định vị được đáp án)") | |
| def prep(b): | |
| enc = tok(b["question"], b["context"], truncation="only_second", max_length=384, | |
| padding="max_length", return_offsets_mapping=True) | |
| st, en = [], [] | |
| for i, off in enumerate(enc["offset_mapping"]): | |
| if b["impossible"][i]: | |
| st.append(0); en.append(0); continue | |
| s, e = b["start"][i], b["start"][i] + len(b["answer"][i]) | |
| sid = enc.sequence_ids(i) | |
| ts = te = 0 | |
| for j, (x, y) in enumerate(off): | |
| if sid[j] != 1: | |
| continue | |
| if x <= s < y: ts = j | |
| if x < e <= y: te = j | |
| st.append(ts); en.append(te) | |
| enc["start_positions"], enc["end_positions"] = st, en | |
| enc.pop("offset_mapping") | |
| return enc | |
| class _VMCb(TrainerCallback): | |
| def on_log(self, args, state, control, logs=None, **kw): | |
| if state.is_world_process_zero and logs: | |
| vm_log(run, step=state.global_step, | |
| **{k: v for k, v in logs.items() if isinstance(v, (int, float))}) | |
| ds = Dataset.from_list(ex).map(prep, batched=True, remove_columns=list(ex[0].keys())) | |
| # MPS (Mac 16 GB) hết bộ nhớ ở batch 8: batch nhỏ + tích luỹ, batch hiệu dụng vẫn 8 | |
| bs, acc = (2, 4) if dev == "mps" else (8, 1) | |
| targs = TrainingArguments(output_dir=str(out / "_tmp"), num_train_epochs=10, | |
| per_device_train_batch_size=bs, gradient_accumulation_steps=acc, | |
| learning_rate=3e-5, weight_decay=0.01, seed=a.seed, | |
| logging_steps=20, save_strategy="no", report_to=[]) | |
| t0 = time.time() | |
| Trainer(model=model, args=targs, train_dataset=ds, data_collator=default_data_collator, | |
| callbacks=[_VMCb()]).train() | |
| res["reader_phut"] = (time.time() - t0) / 60 | |
| res["reader_mau"] = len(ex) | |
| vm_log(run, reader_phut=res["reader_phut"], reader_mau=len(ex)) | |
| model.save_pretrained(out / f"reader_{tag}"); tok.save_pretrained(out / f"reader_{tag}") | |
| if not a.skip_push: | |
| model.push_to_hub(read_repo, private=private); tok.push_to_hub(read_repo, private=private) | |
| print("Đã đẩy", read_repo) | |
| # ------------------------------------------------ đánh giá bằng đúng code của demo | |
| if not (a.skip_retriever or a.skip_reader): | |
| res.update(evaluate(d, out, tag, emb_file, test_qa, dev)) | |
| ref = PAPER[a.config] | |
| print("\n===== Kiểm tra trên tập test (code demo) so với bài =====") | |
| for k, v in res.items(): | |
| if isinstance(v, float) and not k.endswith("phut"): | |
| print(f" {k:28s} {v:.3f}" + (f" (bài: {ref[k]:.3f})" if k in ref else "")) | |
| vm_log(run, **{k.replace(" ", "_").replace("@", ""): v for k, v in res.items() | |
| if isinstance(v, (int, float))}) | |
| (out / f"eval_{tag}.json").write_text(json.dumps(res, ensure_ascii=False, indent=1), encoding="utf-8") | |
| # ------------------------------------------------ cập nhật dataset + Space | |
| if not a.skip_push and (a.data_repo or a.update_space): | |
| from huggingface_hub import HfApi | |
| api = HfApi() | |
| if a.data_repo and emb_file.exists(): | |
| api.upload_file(path_or_fileobj=str(emb_file), path_in_repo=emb_file.name, | |
| repo_id=a.data_repo, repo_type="dataset") | |
| print("Đã đẩy", emb_file.name, "vào", a.data_repo) | |
| if a.update_space: | |
| if not a.skip_retriever: | |
| api.add_space_variable(a.update_space, "RETRIEVER_MODEL", retr_repo) | |
| if not a.skip_reader: | |
| api.add_space_variable(a.update_space, "READER_MODEL", read_repo) | |
| api.restart_space(a.update_space, factory_reboot=True) | |
| print("Đã đặt biến và khởi động lại", a.update_space) | |
| vm_finish(run, "done") | |
| def evaluate(d, out, tag, emb_file, test_qa, dev): | |
| """Chạy đúng Engine của demo: hybrid α=0,5 + XLM-R đọc 3 đoạn (như Space), và | |
| XLM-R trên đoạn tham chiếu (oracle) để so với bài.""" | |
| os.environ["RETRIEVER_MODEL"] = str(out / f"bi_encoder_{tag}") | |
| from engine import Engine | |
| from sentence_transformers import SentenceTransformer | |
| from transformers import AutoModelForQuestionAnswering, AutoTokenizer | |
| e = Engine(d) | |
| e.load_fast() | |
| e.encoder = SentenceTransformer(str(out / f"bi_encoder_{tag}"), device=dev) | |
| e.emb = np.load(emb_file).astype(np.float32) | |
| e.status["dense"] = "ready" | |
| e.qa_tok = AutoTokenizer.from_pretrained(out / f"reader_{tag}") | |
| e.qa_model = AutoModelForQuestionAnswering.from_pretrained(out / f"reader_{tag}").to(dev).eval() | |
| e._torch = torch | |
| e.status["extractive"] = "ready" | |
| hit5, f1_ora, ref_ora = [], [], [] | |
| f1_k = {k: [] for k in (1, 3, 5)} | |
| ref_k = {k: [] for k in (1, 3, 5)} | |
| for q in test_qa: | |
| hits, _ = e.retrieve(q["question"], "hybrid", 5) | |
| ids = [e.ids[i] for i, _ in hits] | |
| hit5.append(bool(set(q["gold"]) & set(ids))) | |
| if q["type"] not in ("factoid", "unanswerable"): | |
| continue | |
| ora_ctx = " ".join(e.txts[e.pid2idx[g]] for g in q["gold"])[:3000] | |
| r = e.extract(q["question"], ora_ctx) | |
| pred_ora = r["answer"] # "" nghĩa là mô hình từ chối | |
| for k in (1, 3, 5): | |
| ctx = " ".join(e.txts[i] for i, _ in hits[:k])[:3000] | |
| rk = e.extract(q["question"], ctx) | |
| pk = rk["answer"] | |
| if q["type"] == "factoid": | |
| f1_k[k].append(f1_score(pk, q["answer"])) | |
| else: | |
| ref_k[k].append(not pk) | |
| if q["type"] == "factoid": | |
| f1_ora.append(f1_score(pred_ora, q["answer"])) | |
| else: | |
| ref_ora.append(not pred_ora) | |
| res = {"R@5 hybrid": float(np.mean(hit5)), "F1 oracle": float(np.mean(f1_ora)), | |
| "Từ chối đúng oracle": float(np.mean(ref_ora))} | |
| for k in (1, 3, 5): | |
| res[f"F1 top-{k}"] = float(np.mean(f1_k[k])) | |
| res[f"Từ chối đúng top-{k}"] = float(np.mean(ref_k[k])) | |
| return res | |
| if __name__ == "__main__": | |
| try: | |
| main() | |
| except KeyboardInterrupt: | |
| vm_finish(RUN, "stopped"); raise | |
| except BaseException as ex: | |
| vm_finish(RUN, "failed", ex); raise | |