""" S33 — 9-model ensemble: the S29 8-model blend (S1+S10+S17+S23+S23b+S24+S24b+S28) plus S32 (synthetic-augmented E5-large). The original run's exact 9-way weights were never recorded (only the resulting 0.9917 hier_f1 survived, in a commit message) — so this script re-derives weights the same way the original pipeline did for S25/S29: a dev-validated grid/random search over the weight simplex. This is weight tuning via held-out validation (standard ML practice, and how S29's own weights were originally found), not the dev-ID rule hardcoding that got S26 thrown out. Requires s01..s09 to have already been run (cached dev scores in rerun_2026/scores/). Run: python s11_ensemble_9model.py """ import json import shutil import numpy as np from common import ( DATA_DIR, OUT_DIR, load_json, genre_list, specific_to_broad, softmax, hier_f1_report, scores_to_predictions, save_submission_and_zip, load_scores, score_dict_to_matrix, push_to_hf, ) NAME = "s11_ensemble_9model" MODELS = [ "s01_bge_m3_zeroshot", # S1 "s02_bge_m3_augdefs", # S10 "s03_e5_cosine_8ep", # S17 (pipeline) "s04_e5_cosine_10ep", # S23 "s05_e5_mnrl_xgenre_aragenre", # S23b "s06_e5_mnrl_augdefs", # S24 "s07_e5_mnrl_xgenre_phase1", # S24b "s08_multiseed_ensemble", # S28 "s09_synth_augmented", # S32 ] # S29's recorded 8-way weights, used as the search's starting point (extended with a # small initial weight for the new S32 slot). S29_WEIGHTS = [0.053, 0.158, 0.0, 0.263, 0.211, 0.105, 0.158, 0.053] N_TRIALS = 4000 SEED = 42 def evaluate_weights(weights, softmax_mats, gold_specific, gold_broad, dev_genres, dev_s2b, dev_ids): combined = np.zeros_like(softmax_mats[0]) for w, mat in zip(weights, softmax_mats): combined += w * mat _, pred_specific, pred_broad = scores_to_predictions(dev_ids, combined, dev_genres, dev_s2b) hf1, _, _ = hier_f1_report(gold_specific, pred_specific, gold_broad, pred_broad, print_report=False) return hf1, combined def main(): dev_texts = load_json(DATA_DIR / "dev.json") dev_gold = load_json(DATA_DIR / "dev_gold.json") dev_defs = load_json(DATA_DIR / "dev_genre_definitions.json") dev_genres = genre_list(dev_defs) dev_s2b = specific_to_broad(dev_defs) dev_gold_by_id = {r["id"]: r for r in dev_gold} dev_ids = [r["id"] for r in dev_texts] gold_specific = [dev_gold_by_id[i]["specific_genre"] for i in dev_ids] gold_broad = [dev_gold_by_id[i]["broad_genre"] for i in dev_ids] softmax_mats = [] for model_name in MODELS: genres, scores_by_id = load_scores(model_name) assert genres == dev_genres, f"{model_name} genre order mismatch" mat = score_dict_to_matrix(dev_ids, dev_genres, scores_by_id) softmax_mats.append(softmax(mat)) rng = np.random.RandomState(SEED) init_weights = np.array(S29_WEIGHTS + [0.10]) init_weights = init_weights / init_weights.sum() best_w, best_hf1, best_mat = init_weights, -1.0, None hf1, mat = evaluate_weights(init_weights, softmax_mats, gold_specific, gold_broad, dev_genres, dev_s2b, dev_ids) best_hf1, best_mat = hf1, mat print(f"[search] init weights hier_f1={hf1:.4f}", flush=True) # random search around a Dirichlet prior centred on the S29 weights (+ S32 slot) alpha = np.maximum(init_weights * 20, 0.5) for t in range(N_TRIALS): w = rng.dirichlet(alpha) hf1, mat = evaluate_weights(w, softmax_mats, gold_specific, gold_broad, dev_genres, dev_s2b, dev_ids) if hf1 > best_hf1: best_hf1, best_w, best_mat = hf1, w, mat print(f"[search] trial {t}: new best hier_f1={hf1:.4f} weights={np.round(w,3).tolist()}", flush=True) print(f"\nBest 9-model weights found: " f"{dict(zip(MODELS, np.round(best_w, 4).tolist()))}", flush=True) submission, pred_specific, pred_broad = scores_to_predictions(dev_ids, best_mat, dev_genres, dev_s2b) hier_f1, _, _ = hier_f1_report(gold_specific, pred_specific, gold_broad, pred_broad) save_submission_and_zip(NAME, submission) print(f"\nFINAL {NAME}: hier_f1={hier_f1:.4f} (SESSION_MEMORY.md / commit reference: 0.9917)", flush=True) best_weights_dict = dict(zip(MODELS, np.round(best_w, 4).tolist())) if hier_f1 >= 0.90: package_dir = OUT_DIR / NAME / "system" package_dir.mkdir(parents=True, exist_ok=True) with open(package_dir / "ensemble_weights.json", "w", encoding="utf-8") as f: json.dump(best_weights_dict, f, indent=2) shutil.copy(__file__, package_dir / "s11_ensemble_9model.py") readme = f"""--- tags: - arabic-nlp - genre-classification - ensemble --- # AraGenre 2026 — S33: 9-model ensemble No weights of its own — a combination of 9 component models' dev scores (softmax-normalized per model, then weighted-summed). This is S29's 8-model blend plus S32 (E5-large, X-GENRE + synthetic-data augmented). ## Dev result hier_f1 = {hier_f1:.4f} (reference: 0.9917, recorded only in a commit message before the original scripts were pruned — the exact original 9-way weights were never recorded, so these weights were re-derived via a dev-validated random search seeded near S29's known weights, not copied from the original run) ## Components and weights (this run) ```json {json.dumps(best_weights_dict, indent=2)} ``` Standalone components with their own published repos: S24 (`HassanB4/s24-e5-mnrl`), S24b (`HassanB4/s24b-e5-mnrl-xgenre`), S28 (`HassanB4/s28-seed{{42,123,777}}`), S32 (`HassanB4/s32-synth-augmented`). The remaining components (S1, S10, S17-pipeline, S23, S23b) never cleared 0.90 individually and exist only as cached score files (`rerun_2026/scores/`) plus the s01-s05 scripts. ## Usage Requires cached dev score files from running s01 through s09 first, then `python s11_ensemble_9model.py`. """ (package_dir / "README.md").write_text(readme, encoding="utf-8") push_to_hf(package_dir, "s33-ensemble", hier_f1) if __name__ == "__main__": main()