Download train.py from aaarchitha7/kcat-km-final1: direct link, hf CLI and curl.
- Browser
- Download file 25.3 kB
-
https://huggingface.co/aaarchitha7/kcat-km-final1/resolve/main/train.py
- Command line
-
hf download hf://aaarchitha7/kcat-km-final1/train.py
-
curl -L -o train.py https://huggingface.co/aaarchitha7/kcat-km-final1/resolve/main/train.py
25.3 kB
| """Train the kcat/Km model. | |
| Typical use (in the Colab notebook): | |
| python train.py --train ../data/train.parquet --out weights --esm --lab # compare ideas on a local holdout, then stop | |
| python train.py --train ../data/train.parquet --out weights --esm --preset NAME --seeds 5 # real training | |
| """ | |
| import argparse | |
| import json | |
| import os | |
| import time | |
| import lightgbm as lgb | |
| import numpy as np | |
| import pandas as pd | |
| from scipy.stats import spearmanr | |
| from sklearn.model_selection import GroupKFold, GroupShuffleSplit | |
| import features as F | |
| ESM_NAME = "facebook/esm2_t12_35M_UR50D" | |
| REG = dict(num_leaves=31, min_child_samples=60, colsample_bytree=0.15, reg_lambda=10.0) | |
| PRESETS = { | |
| "base": {}, | |
| "clip": {"clip": True}, | |
| "w05": {"weight_pow": 0.5}, | |
| "reg": {"params": REG}, | |
| "knn": {"knn": True}, | |
| "combo": {"clip": True, "weight_pow": 0.5, "params": REG}, | |
| "combo_knn": {"clip": True, "weight_pow": 0.5, "params": REG, "knn": True}, | |
| "noesm": {"noesm": True}, # lab only: shows whether ESM helps at all | |
| # lab results (local holdout): knn best, w05 helps a little, clip/reg hurt -> combine the winners | |
| "knn_w05": {"knn": True, "weight_pow": 0.5}, | |
| # layer experiments: run with --esm_layers 4,6,8,10,12 (blocks 0..4) | |
| "L12": {"esm_blocks": [4]}, | |
| "L8": {"esm_blocks": [2]}, | |
| "L6": {"esm_blocks": [1]}, | |
| "L8_12": {"esm_blocks": [2, 4]}, | |
| "L6_8_12": {"esm_blocks": [1, 2, 4]}, | |
| "Lall": {}, | |
| # v9: enzyme-effect (residual) idea + rank-transformed target | |
| "res": {"res": True}, | |
| "rank": {"rank": True}, | |
| "res_knn_w05": {"res": True, "knn": True, "weight_pow": 0.5}, | |
| "res_w05": {"res": True, "weight_pow": 0.5}, | |
| "pca32": {"esm_pca": 32}, | |
| "nodi": {"nodi": True}, | |
| "pca32_nodi": {"esm_pca": 32, "nodi": True}, | |
| "pca32_knn": {"esm_pca": 32, "knn": True}, | |
| "knn_multi": {"knn": True, "ks": (1, 5, 20)}, | |
| "knn_multi_w05": {"knn": True, "ks": (1, 5, 20), "weight_pow": 0.5}, | |
| } | |
| def log(*a): | |
| print(time.strftime("%H:%M:%S"), *a, flush=True) | |
| def te_oof(df, groups, smi_index, bits_u, seq_index, embn, ks=(5,), n_splits=5, want_res=False): | |
| """Substrate-level (and ESM-neighbour) features for training rows, computed WITHOUT | |
| looking at the row's own enzyme group, so the model learns to use them honestly.""" | |
| te = np.full((len(df), 12), np.nan, dtype=np.float32) | |
| kn = None if embn is None else np.full((len(df), 6 * len(ks)), np.nan, dtype=np.float32) | |
| er = np.full((len(df), 3), np.nan, dtype=np.float32) if (want_res and embn is not None) else None | |
| smis = df["smiles"].values | |
| sidx = df["sequence"].map(seq_index).values | |
| ys = df[["log10_kcat", "log10_km"]].values.astype(np.float32) | |
| for tr, va in GroupKFold(n_splits).split(df, groups=groups): | |
| st = F.te_stats(df.iloc[tr]) | |
| rb = bits_u[[smi_index[s] for s in st.index]] | |
| te[va] = F.te_full(smis[va], smi_index, bits_u, st, rb) | |
| if kn is not None: | |
| kn[va] = F.knn_feats(smis[va], sidx[va], embn, smis[tr], sidx[tr], embn, ys[tr], ks=ks) | |
| if er is not None: | |
| rsr = F.enzyme_effect_ref(smis[tr], sidx[tr], ys[tr], embn.shape[0], st) | |
| er[va] = F.enzyme_effect_feats(sidx[va], embn, embn, rsr) | |
| return te, kn, er | |
| def mlp_fit(Ztr, Ytr, Zva=None, Yva=None, epochs=15, seed=0, hidden=(384, 128), alpha=1e-3, lr=1e-3): | |
| """Small neural net (sklearn). Returns model, per-epoch validation SCC, per-epoch validation predictions.""" | |
| from sklearn.neural_network import MLPRegressor | |
| m = MLPRegressor(hidden_layer_sizes=hidden, alpha=alpha, learning_rate_init=lr, | |
| batch_size=256, random_state=seed) | |
| curve, preds = [], [] | |
| for ep in range(epochs): | |
| m.partial_fit(Ztr, Ytr) | |
| if Zva is not None: | |
| P = m.predict(Zva) | |
| preds.append(P) | |
| curve.append(float(np.mean([spearmanr(Yva[:, j], P[:, j]).correlation for j in (0, 1)]))) | |
| return m, curve, preds | |
| def nn_fit_torch(Ztr, Ytr, Zva=None, Yva=None, epochs=12, seed=0, hidden=(512, 256), | |
| dropout=0.3, in_drop=0.1, wd=1e-2, lr=1e-3, bs=256): | |
| """Regularised neural net trained with PyTorch (dropout + weight decay + Huber loss). | |
| Returns numpy layers so prediction needs only numpy.""" | |
| import torch | |
| torch.manual_seed(seed) | |
| dev = "cuda" if torch.cuda.is_available() else "cpu" | |
| mods, prev = [torch.nn.Dropout(in_drop)], Ztr.shape[1] | |
| for h in hidden: | |
| mods += [torch.nn.Linear(prev, h), torch.nn.ReLU(), torch.nn.Dropout(dropout)] | |
| prev = h | |
| mods += [torch.nn.Linear(prev, Ytr.shape[1])] | |
| net = torch.nn.Sequential(*mods).to(dev) | |
| opt = torch.optim.AdamW(net.parameters(), lr=lr, weight_decay=wd) | |
| Xt = torch.tensor(Ztr, dtype=torch.float32, device=dev) | |
| Yt = torch.tensor(Ytr, dtype=torch.float32, device=dev) | |
| Xv = None if Zva is None else torch.tensor(Zva, dtype=torch.float32, device=dev) | |
| curve, preds = [], [] | |
| for ep in range(epochs): | |
| net.train() | |
| perm = torch.randperm(len(Xt), device=dev) | |
| for i in range(0, len(Xt), bs): | |
| idx = perm[i:i + bs] | |
| loss = torch.nn.functional.huber_loss(net(Xt[idx]), Yt[idx], delta=1.0) | |
| opt.zero_grad() | |
| loss.backward() | |
| opt.step() | |
| if Xv is not None: | |
| net.eval() | |
| with torch.no_grad(): | |
| P = net(Xv).cpu().numpy() | |
| preds.append(P) | |
| curve.append(float(np.mean([spearmanr(Yva[:, j], P[:, j]).correlation for j in (0, 1)]))) | |
| lin = [m for m in net if isinstance(m, torch.nn.Linear)] | |
| layers = [(l.weight.detach().cpu().numpy().T.astype(np.float32), | |
| l.bias.detach().cpu().numpy().astype(np.float32)) for l in lin] | |
| return layers, curve, preds | |
| def fit_nn(args, Ztr, Ytr, Zva=None, Yva=None, epochs=12, seed=0): | |
| """-> (numpy layers, per-epoch val SCC, per-epoch val predictions)""" | |
| if args.nn == "torch": | |
| try: | |
| return nn_fit_torch(Ztr, Ytr, Zva, Yva, epochs=epochs, seed=seed) | |
| except Exception as e: # never let the extra model break the whole run | |
| log(" torch net failed (", repr(e)[:150], ") -> using the simple sklearn net instead") | |
| m, curve, preds = mlp_fit(Ztr, Ytr, Zva, Yva, epochs=epochs, seed=seed) | |
| return mlp_layers(m), curve, preds | |
| def mlp_layers(m): | |
| return [(W.astype(np.float32), b.astype(np.float32)) for W, b in zip(m.coefs_, m.intercepts_)] | |
| def make_model(seed, n_estimators, lr, **over): | |
| p = dict(n_estimators=n_estimators, learning_rate=lr, num_leaves=63, | |
| min_child_samples=20, subsample=0.8, subsample_freq=1, | |
| colsample_bytree=0.25, reg_lambda=1.0, random_state=seed, | |
| n_jobs=-1, verbose=-1) | |
| p.update(over) | |
| return lgb.LGBMRegressor(**p) | |
| def row_weights(g, pw): | |
| """enzymes with hundreds of mutant rows should not dominate training""" | |
| if not pw: | |
| return None | |
| s = pd.Series(g) | |
| sizes = s.map(s.value_counts()).values.astype(float) | |
| w = sizes ** (-pw) | |
| return w / w.mean() | |
| def target(y, preset): | |
| if preset.get("rank"): | |
| from scipy.stats import norm, rankdata | |
| return norm.ppf(rankdata(y) / (len(y) + 1.0)) | |
| if preset.get("clip"): | |
| lo, hi = np.quantile(y, [0.01, 0.99]) | |
| return np.clip(y, lo, hi) | |
| return y | |
| def report(name, y, p): | |
| scc = spearmanr(y, p).correlation | |
| rmse = float(np.sqrt(np.mean((y - p) ** 2))) | |
| log(f" {name}: SCC={scc:.4f} RMSE={rmse:.4f}") | |
| return scc | |
| def main(): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--train", required=True) | |
| ap.add_argument("--out", default="weights") | |
| ap.add_argument("--esm", action="store_true", help="add ESM2 protein embeddings") | |
| ap.add_argument("--esm_name", default=ESM_NAME) | |
| ap.add_argument("--esm_layers", default="", help="e.g. 6,9,12 = pool several ESM layers (default: last layer only)") | |
| ap.add_argument("--preset", default="base", choices=list(PRESETS)) | |
| ap.add_argument("--lab", action="store_true", help="compare presets on local holdout(s), then stop") | |
| ap.add_argument("--lab_presets", default="", help="comma separated presets for --lab") | |
| ap.add_argument("--split", default="esm", choices=["esm", "cosine"], | |
| help="esm = hold out whole ESM-similarity families (harsh, closest to the real validation set)") | |
| ap.add_argument("--repeats", type=int, default=1, help="how many different holdouts to average in --lab") | |
| ap.add_argument("--esm_clusters", type=int, default=300) | |
| ap.add_argument("--seeds", type=int, default=1) | |
| ap.add_argument("--lr", type=float, default=0.03) | |
| ap.add_argument("--max_trees", type=int, default=3000) | |
| ap.add_argument("--no_holdout", action="store_true") | |
| ap.add_argument("--external", default="", help="parquet of EXTRA training rows (smiles, sequence, log10_kcat, log10_km; one label may be NaN)") | |
| ap.add_argument("--mlp", action="store_true", help="also train a neural net and blend it with LightGBM") | |
| ap.add_argument("--mlp_epochs", type=int, default=12) | |
| ap.add_argument("--nn", default="torch", choices=["torch", "sk"], help="neural net implementation") | |
| ap.add_argument("--mlp_w", type=float, default=0.4, help="weight of the neural net in the blend") | |
| ap.add_argument("--blend_with", default="base", help="LightGBM preset to blend with in --lab") | |
| args = ap.parse_args() | |
| os.makedirs(args.out, exist_ok=True) | |
| preset = PRESETS[args.preset] | |
| needs_esm = lambda p: p.get("knn") or p.get("esm_pca") or p.get("noesm") or p.get("res") | |
| if (needs_esm(preset) or args.lab or args.split == "esm") and not args.esm: | |
| raise SystemExit("this needs --esm") | |
| if preset.get("noesm") and not args.lab: | |
| raise SystemExit("'noesm' is only for --lab") | |
| if preset.get("rank") and args.mlp and not args.lab: | |
| raise SystemExit("'rank' cannot be blended with the neural net (different scales)") | |
| df = pd.read_parquet(args.train).reset_index(drop=True) | |
| df = df[["smiles", "sequence", "log10_kcat", "log10_km"]].copy() | |
| df["_ext"] = False | |
| if args.external: | |
| ext = pd.read_parquet(args.external)[["smiles", "sequence", "log10_kcat", "log10_km"]].copy() | |
| ext["_ext"] = True | |
| log(f"external rows added: {len(ext)} (kcat labels: {ext.log10_kcat.notna().sum()}, km labels: {ext.log10_km.notna().sum()})") | |
| df = pd.concat([df, ext], ignore_index=True) | |
| is_task = ~df["_ext"].values | |
| log("task rows:", int(is_task.sum()), "| all rows:", len(df), "| unique smiles:", df.smiles.nunique(), "| unique sequences:", df.sequence.nunique()) | |
| usmi = df["smiles"].unique().tolist() | |
| useq = df["sequence"].unique().tolist() | |
| smi_index = {s: i for i, s in enumerate(usmi)} | |
| seq_index = {s: i for i, s in enumerate(useq)} | |
| log("computing SMILES features ...") | |
| smi_mat = F.smiles_features(usmi) | |
| log("computing sequence features ...") | |
| seq_mat = F.seq_features(useq) | |
| bits_u = (smi_mat[:, :F.N_MORGAN] > 0).astype(np.float32) | |
| esm_mat, esm_mean, embn = None, None, None | |
| layers = [int(x) for x in args.esm_layers.split(",") if x] or None | |
| n_blocks = len(layers) if layers else 1 | |
| if args.esm: | |
| esm_dir = os.path.join(args.out, "esm") | |
| if not os.path.exists(esm_dir): | |
| from transformers import AutoTokenizer, EsmModel | |
| log("downloading", args.esm_name) | |
| AutoTokenizer.from_pretrained(args.esm_name).save_pretrained(esm_dir) | |
| EsmModel.from_pretrained(args.esm_name).save_pretrained(esm_dir) | |
| cache = "esm_cache_" + args.esm_name.split("/")[-1] + ("_L" + "-".join(map(str, layers)) if layers else "") + ".npz" | |
| if os.path.exists(cache) and list(np.load(cache, allow_pickle=True)["seqs"]) == useq: | |
| esm_mat = np.load(cache, allow_pickle=True)["emb"] | |
| log("loaded ESM embeddings from cache") | |
| else: | |
| import torch | |
| dev = "cuda" if torch.cuda.is_available() else "cpu" | |
| log(f"computing ESM embeddings on {dev} (this is the slow part) ...") | |
| esm_mat = F.esm_embed(useq, esm_dir, device=dev, | |
| batch_tokens=16384 if dev == "cuda" else 4096, | |
| n_threads=os.cpu_count() or 2, layers=layers) | |
| np.savez(cache, seqs=np.array(useq, dtype=object), emb=esm_mat) | |
| task_seqs = set(df.loc[is_task, "sequence"]) | |
| seq_is_task = np.array([s in task_seqs for s in useq]) | |
| esm_mean = esm_mat[seq_is_task].mean(0) # centre on the competition's own enzymes (keeps holdouts identical with/without external data) | |
| np.save(os.path.join(args.out, "esm_mean.npy"), esm_mean) | |
| embn = F.normalize_emb(esm_mat, esm_mean) | |
| log("grouping near-identical sequences (mutants) ...") | |
| lab_ = F.cluster_by_cosine(useq, log=log) | |
| groups = df["sequence"].map(dict(zip(useq, lab_))).values # mutant groups (used for weights) | |
| df["_g"] = groups # lets te_stats average per enzyme group | |
| if args.split == "esm": | |
| from sklearn.cluster import KMeans | |
| kmf = KMeans(n_clusters=args.esm_clusters, n_init=3, random_state=0).fit(embn[seq_is_task]) | |
| km_lab = np.zeros(len(useq), dtype=int) | |
| km_lab[seq_is_task] = kmf.labels_ | |
| if (~seq_is_task).any(): | |
| km_lab[~seq_is_task] = kmf.predict(embn[~seq_is_task]) | |
| split_groups = df["sequence"].map(dict(zip(useq, km_lab))).values | |
| log(f"harsh split: {args.esm_clusters} enzyme families by ESM similarity") | |
| else: | |
| split_groups = groups | |
| smi_dim, seq_dim = smi_mat.shape[1], seq_mat.shape[1] | |
| esm_dim = esm_mat.shape[1] if esm_mat is not None else 0 | |
| pcas = {} | |
| def get_pca(p): | |
| n = p.get("esm_pca") | |
| if not n: | |
| return None | |
| if n not in pcas: | |
| pcas[n] = F.fit_pca(esm_mat, n) | |
| return pcas[n] | |
| def variant(X0, kn, p, ks_all, er=None): | |
| return F.apply_variant(X0, kn, p, smi_dim, seq_dim, esm_dim, ks_all=ks_all, pca=get_pca(p), n_blocks=n_blocks, er=er) | |
| best_iter = {"kcat": 800, "km": 800} | |
| # ---------------- local validation run(s) ---------------- | |
| if not args.no_holdout or args.lab: | |
| if args.lab: | |
| lab_names = [n for n in args.lab_presets.split(",") if n] or \ | |
| ["base", "noesm", "w05", "knn", "knn_w05", "pca32", "nodi", "pca32_nodi"] | |
| else: | |
| lab_names = [args.preset] | |
| all_ks = sorted({k for n in lab_names for k in PRESETS[n].get("ks", (5,))}) | |
| want_res = any(PRESETS[n].get("res") for n in lab_names) | |
| def make_split(seed): | |
| task_ix = np.nonzero(is_task)[0] | |
| ext_ix = np.nonzero(~is_task)[0] | |
| tr_t, va_t = next(GroupShuffleSplit(1, test_size=0.2, random_state=seed).split(task_ix, groups=split_groups[task_ix])) | |
| va = task_ix[va_t] | |
| ext_keep = ext_ix[~np.isin(split_groups[ext_ix], np.unique(split_groups[va]))] # no external rows from held-out families | |
| tr = np.concatenate([task_ix[tr_t], ext_keep]) | |
| dtr, dva = df.iloc[tr].reset_index(drop=True), df.iloc[va].reset_index(drop=True) | |
| te_tr, kn_tr, er_tr = te_oof(dtr, split_groups[tr], smi_index, bits_u, seq_index, embn, ks=tuple(all_ks), want_res=want_res) | |
| st_tr = F.te_stats(dtr) | |
| te_va = F.te_full(dva["smiles"].values, smi_index, bits_u, st_tr, | |
| bits_u[[smi_index[s] for s in st_tr.index]]) | |
| kn_va = None | |
| if embn is not None: | |
| kn_va = F.knn_feats(dva["smiles"].values, dva["sequence"].map(seq_index).values, embn, | |
| dtr["smiles"].values, dtr["sequence"].map(seq_index).values, embn, | |
| dtr[["log10_kcat", "log10_km"]].values.astype(np.float32), ks=tuple(all_ks)) | |
| er_va = None | |
| if want_res: | |
| rsr = F.enzyme_effect_ref(dtr["smiles"].values, dtr["sequence"].map(seq_index).values, | |
| dtr[["log10_kcat", "log10_km"]].values, embn.shape[0], st_tr) | |
| er_va = F.enzyme_effect_feats(dva["sequence"].map(seq_index).values, embn, embn, rsr) | |
| return dict(dtr=dtr, dva=dva, gtr=groups[tr], kn_tr=kn_tr, kn_va=kn_va, er_tr=er_tr, er_va=er_va, | |
| Xtr0=F.assemble(dtr, usmi, smi_mat, useq, seq_mat, esm_mat, te_tr), | |
| Xva0=F.assemble(dva, usmi, smi_mat, useq, seq_mat, esm_mat, te_va)) | |
| def run(pname, d): | |
| p = PRESETS[pname] | |
| Xtr = variant(d["Xtr0"], d["kn_tr"], p, all_ks, d["er_tr"]) | |
| Xva = variant(d["Xva0"], d["kn_va"], p, all_ks, d["er_va"]) | |
| w = row_weights(d["gtr"], p.get("weight_pow")) | |
| res, its, prd = {}, {}, {} | |
| for name, col in [("kcat", "log10_kcat"), ("km", "log10_km")]: | |
| m = make_model(0, args.max_trees, args.lr, **p.get("params", {})) | |
| mk = ~np.isnan(d["dtr"][col].values) | |
| m.fit(Xtr[mk], target(d["dtr"][col].values[mk], p), sample_weight=None if w is None else w[mk], | |
| eval_set=[(Xva, d["dva"][col].values)], | |
| callbacks=[lgb.early_stopping(100, verbose=False)]) | |
| its[name] = max(100, int(m.best_iteration_ or args.max_trees)) | |
| prd[name] = m.predict(Xva) | |
| res[name] = report(f"{pname}/{name}", d["dva"][col].values, prd[name]) | |
| return res, its, prd | |
| if args.lab: | |
| if args.mlp and args.blend_with not in lab_names: | |
| lab_names.append(args.blend_with) | |
| scores = {n: [] for n in lab_names} | |
| lgb_pred, mlp_pred, ytrue = {}, [], [] | |
| nodi = {"nodi": True} | |
| for rep in range(args.repeats): | |
| log(f"##### holdout {rep + 1} of {args.repeats}") | |
| d = make_split(rep) | |
| log("holdout features:", d["Xtr0"].shape, d["Xva0"].shape) | |
| for pname in lab_names: | |
| log(f"=== preset: {pname}") | |
| r, _, pr = run(pname, d) | |
| scores[pname].append((r["kcat"], r["km"])) | |
| lgb_pred[(rep, pname)] = pr | |
| if args.mlp: | |
| log("=== neural net") | |
| Xa = F.apply_variant(d["Xtr0"], None, nodi, smi_dim, seq_dim, esm_dim) | |
| Xb = F.apply_variant(d["Xva0"], None, nodi, smi_dim, seq_dim, esm_dim) | |
| Ytr = d["dtr"][["log10_kcat", "log10_km"]].values.astype(np.float32) | |
| Yva = d["dva"][["log10_kcat", "log10_km"]].values.astype(np.float32) | |
| both = ~np.isnan(Ytr).any(1) | |
| Xa, Ytr = Xa[both], Ytr[both] | |
| st = F.mlp_prep_fit(Xa) | |
| ym, ys = Ytr.mean(0), Ytr.std(0) | |
| _, curve, preds = fit_nn(args, F.mlp_prep(Xa, st), (Ytr - ym) / ys, F.mlp_prep(Xb, st), | |
| (Yva - ym) / ys, epochs=args.mlp_epochs) | |
| log(" nn SCC after each epoch:", " ".join(f"{c:.3f}" for c in curve)) | |
| mlp_pred.append([p * ys + ym for p in preds]); ytrue.append(Yva) | |
| log("========== LAB RESULTS (harsh local holdout, higher is better) ==========") | |
| log(f"{'preset':14s} {'kcat':>8s} {'km':>8s} {'mean':>8s} {'+-':>7s}") | |
| rows = [] | |
| for n, v in scores.items(): | |
| a = np.array(v) | |
| m_ = a.mean(1) | |
| rows.append((n, a[:, 0].mean(), a[:, 1].mean(), m_.mean(), m_.std())) | |
| for r_ in sorted(rows, key=lambda r: -r[3]): | |
| log(f"{r_[0]:14s} {r_[1]:8.4f} {r_[2]:8.4f} {r_[3]:8.4f} {r_[4]:7.4f}") | |
| if args.mlp: | |
| def scc2(y, p): | |
| return [spearmanr(y[:, j], p[:, j]).correlation for j in (0, 1)] | |
| n_ep = len(mlp_pred[0]) | |
| mean_curve = [np.mean([np.mean(scc2(ytrue[r], mlp_pred[r][e])) for r in range(args.repeats)]) for e in range(n_ep)] | |
| be = int(np.argmax(mean_curve)) | |
| log(f"neural net alone: best after epoch {be + 1} (mean SCC {mean_curve[be]:.4f})") | |
| log(f"BLEND of '{args.blend_with}' LightGBM and neural net (epoch {be + 1}):") | |
| log(f"{'nn weight':>10s} {'kcat':>8s} {'km':>8s} {'mean':>8s}") | |
| for w_ in (0.0, 0.2, 0.3, 0.4, 0.5, 0.6, 0.8, 1.0): | |
| ks_, ms_ = [], [] | |
| for r in range(args.repeats): | |
| lp = lgb_pred[(r, args.blend_with)] | |
| L = np.column_stack([lp["kcat"], lp["km"]]) | |
| s2 = scc2(ytrue[r], (1 - w_) * L + w_ * mlp_pred[r][be]) | |
| ks_.append(s2[0]); ms_.append(s2[1]) | |
| log(f"{w_:10.1f} {np.mean(ks_):8.4f} {np.mean(ms_):8.4f} {(np.mean(ks_) + np.mean(ms_)) / 2:8.4f}") | |
| log(f"-> to use it for real set MLP_EPOCHS = {be + 1} and MLP_W to the best weight above") | |
| return | |
| d = make_split(0) | |
| r, best_iter, _ = run(args.preset, d) | |
| log(f"LOCAL HOLDOUT mean SCC = {(r['kcat'] + r['km']) / 2:.4f} (preset {args.preset}, best iters: {best_iter})") | |
| # ---------------- final training on ALL training rows ---------------- | |
| log("training final models on all rows ...") | |
| fks = tuple(preset.get("ks", (5,))) | |
| te_all, kn_all, er_all = te_oof(df, split_groups, smi_index, bits_u, seq_index, embn, ks=fks, want_res=bool(preset.get("res"))) | |
| stats = F.te_stats(df) | |
| stats.to_parquet(os.path.join(args.out, "te_stats.parquet")) | |
| np.save(os.path.join(args.out, "ref_bits.npy"), | |
| bits_u[[smi_index[s] for s in stats.index]].astype(np.uint8)) | |
| if preset.get("res"): | |
| np.save(os.path.join(args.out, "ref_seq_res.npy"), | |
| F.enzyme_effect_ref(df["smiles"].values, df["sequence"].map(seq_index).values, | |
| df[["log10_kcat", "log10_km"]].values, embn.shape[0], stats)) | |
| if preset.get("knn") or preset.get("res"): | |
| np.save(os.path.join(args.out, "ref_embn.npy"), embn.astype(np.float16)) | |
| if preset.get("knn"): | |
| pd.DataFrame({"smiles": df["smiles"].values, "seq_id": df["sequence"].map(seq_index).values, | |
| "kc": df["log10_kcat"].values, "km": df["log10_km"].values} | |
| ).to_parquet(os.path.join(args.out, "ref_rows.parquet")) | |
| pca = get_pca(preset) | |
| if pca is not None: | |
| np.savez(os.path.join(args.out, "esm_pca.npz"), mean=pca["mean"], comps=pca["comps"]) | |
| X = variant(F.assemble(df, usmi, smi_mat, useq, seq_mat, esm_mat, te_all), kn_all, preset, list(fks), er_all) | |
| w = row_weights(groups, preset.get("weight_pow")) | |
| for name, col in [("kcat", "log10_kcat"), ("km", "log10_km")]: | |
| mk = ~np.isnan(df[col].values) | |
| y = target(df[col].values[mk], preset) | |
| for s in range(args.seeds): | |
| m = make_model(s, int(best_iter[name] * 1.15), args.lr, **preset.get("params", {})) | |
| m.fit(X[mk], y, sample_weight=None if w is None else w[mk]) | |
| m.booster_.save_model(os.path.join(args.out, f"{name}_{s}.txt")) | |
| log(f"saved {name}_{s}.txt") | |
| mlp_meta = None | |
| if args.mlp: | |
| log("training neural nets on all rows ...") | |
| nodi = {"nodi": True} | |
| Xm = F.apply_variant(F.assemble(df, usmi, smi_mat, useq, seq_mat, esm_mat, te_all), None, nodi, | |
| smi_dim, seq_dim, esm_dim) | |
| Y = df[["log10_kcat", "log10_km"]].values.astype(np.float32) | |
| both = ~np.isnan(Y).any(1) | |
| Xm, Y = Xm[both], Y[both] | |
| st = F.mlp_prep_fit(Xm) | |
| ym, ys = Y.mean(0), Y.std(0) | |
| Zm = F.mlp_prep(Xm, st) | |
| np.savez(os.path.join(args.out, "mlp_prep.npz"), mean=st["mean"], std=st["std"], ym=ym, ys=ys) | |
| n_nn = max(3, args.seeds) | |
| for s in range(n_nn): | |
| lay, _, _ = fit_nn(args, Zm, (Y - ym) / ys, epochs=args.mlp_epochs, seed=s) | |
| np.savez(os.path.join(args.out, f"mlp_{s}.npz"), n=len(lay), | |
| **{f"W{i}": W for i, (W, b) in enumerate(lay)}, **{f"b{i}": b for i, (W, b) in enumerate(lay)}) | |
| log(f"saved mlp_{s}.npz") | |
| mlp_meta = {"w": args.mlp_w, "n": n_nn} | |
| pflags = {k: (list(v) if isinstance(v, tuple) else v) for k, v in preset.items() if k != "params"} | |
| meta = {"use_esm": bool(args.esm), "knn": bool(preset.get("knn")), "ks": list(fks), "preset": pflags, | |
| "dims": [smi_dim, seq_dim, esm_dim], "layers": layers, "n_seeds": args.seeds, "mlp": mlp_meta, | |
| "mean_kcat": float(np.nanmean(df["log10_kcat"])), "mean_km": float(np.nanmean(df["log10_km"])), | |
| "external_rows": int((~is_task).sum())} | |
| with open(os.path.join(args.out, "meta.json"), "w") as f: | |
| json.dump(meta, f) | |
| log("DONE. Weights are in", args.out) | |
| if __name__ == "__main__": | |
| main() | |