Download src/pivot/evaluation/baselines.py from ChatterjeeLab/PIVOT: direct link, hf CLI and curl.
- Browser
- Download file 5.49 kB
-
https://huggingface.co/ChatterjeeLab/PIVOT/resolve/main/src/pivot/evaluation/baselines.py
- Command line
-
hf download hf://ChatterjeeLab/PIVOT/src/pivot/evaluation/baselines.py
-
curl -L -o baselines.py https://huggingface.co/ChatterjeeLab/PIVOT/resolve/main/src/pivot/evaluation/baselines.py
5.49 kB
| """Reference predictors fitted with the same training cells and feature basis.""" | |
| from __future__ import annotations | |
| import copy | |
| import numpy as np | |
| import torch | |
| from sklearn.linear_model import Ridge | |
| from pivot.training.train import make_model, TrainConfig, validation_loss | |
| from pivot.models.encoders import build_pert_tensors | |
| from pivot.utils.common import set_seed | |
| class Baseline: | |
| """Population predictor. `predict(c0, label)` returns (n_controls, d).""" | |
| def __init__( | |
| self, data, kind="ridge", alpha=1.0, epochs=60, hidden=512, seed=0, device="cpu" | |
| ): | |
| if kind not in ( | |
| "mean_control", | |
| "average_effect", | |
| "additive", | |
| "ridge", | |
| "endpoint_mlp", | |
| "conditional_mlp", | |
| ): | |
| raise ValueError(kind) | |
| self.data = data | |
| self.kind = kind | |
| self.device = device | |
| tr = data.indices("train", False) | |
| ctrl = data.indices("train", True) | |
| self.control = data.emb[ctrl].mean(0) | |
| self.effects = { | |
| p: data.emb[np.intersect1d(tr, data.pert_to_idx[p])].mean(0) - self.control | |
| for p in data.labels("train") | |
| } | |
| self.avg = np.mean(list(self.effects.values()), axis=0) | |
| self.training_info = { | |
| "kind": kind, | |
| "alpha": alpha, | |
| "seed": seed, | |
| "train_cell_count": len(tr), | |
| } | |
| if kind == "ridge": | |
| labels = list(self.effects) | |
| self.reg = Ridge(alpha=alpha, fit_intercept=False).fit( | |
| self.features(labels), np.stack(list(self.effects.values())) | |
| ) | |
| if kind in ("endpoint_mlp", "conditional_mlp"): | |
| set_seed(seed) | |
| torch.set_num_threads(4) | |
| cfg = TrainConfig(hidden=hidden, depth=4, seed=seed, device=device) | |
| self.model = make_model(data, cfg) | |
| opt = torch.optim.AdamW(self.model.parameters(), lr=1e-3, weight_decay=1e-5) | |
| rng = np.random.default_rng(seed) | |
| best = float("inf") | |
| beststate = None | |
| history = [] | |
| for epoch in range(epochs): | |
| self.model.train() | |
| for ids in np.array_split( | |
| rng.permutation(tr), max(1, int(np.ceil(len(tr) / 1024))) | |
| ): | |
| c = data.emb[rng.choice(ctrl, len(ids))] | |
| if kind == "endpoint_mlp": | |
| c = np.broadcast_to(self.control, c.shape).copy() | |
| c = torch.as_tensor(c, device=device) | |
| y = torch.as_tensor(data.emb[ids], device=device) | |
| labels = data.obs.iloc[ids].perturbation.tolist() | |
| g, o, m, p = build_pert_tensors(data, labels, device) | |
| pred = self.model.endpoint_from_pert(c, g, o, m, p) | |
| loss = (pred - y).square().sum(-1).mean() | |
| opt.zero_grad() | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(self.model.parameters(), 5.0) | |
| opt.step() | |
| self.model.eval() | |
| vals = [] | |
| vc = data.emb[data.indices("val", True)][:128] | |
| for p in data.labels("val"): | |
| ids = np.intersect1d( | |
| data.indices("val", False), data.pert_to_idx[p] | |
| ) | |
| vals.append( | |
| np.mean( | |
| (self.predict(vc, p).mean(0) - data.emb[ids].mean(0)) ** 2 | |
| ) | |
| ) | |
| score = float(np.mean(vals)) | |
| history.append(score) | |
| if score < best: | |
| best = score | |
| beststate = copy.deepcopy(self.model.state_dict()) | |
| self.model.load_state_dict(beststate) | |
| self.training_info.update( | |
| validation_mse=history, | |
| best_validation_mse=best, | |
| epochs=epochs, | |
| hidden=hidden, | |
| ) | |
| def features(self, labels): | |
| a = np.zeros((len(labels), len(self.data.genes_vocab)), np.float32) | |
| for i, p in enumerate(labels): | |
| for g in self.data.parse(p): | |
| a[i, self.data.gene_to_id[g]] = 1.0 | |
| return a | |
| def predict(self, c0, label): | |
| if self.kind in ("endpoint_mlp", "conditional_mlp"): | |
| c = np.asarray(c0, dtype=np.float32) | |
| if self.kind == "endpoint_mlp": | |
| c = np.broadcast_to(self.control, c.shape).copy() | |
| g, o, m, p = build_pert_tensors(self.data, [label], self.device) | |
| with torch.no_grad(): | |
| return ( | |
| self.model.endpoint_from_pert( | |
| torch.as_tensor(c, device=self.device), g, o, m, p | |
| ) | |
| .cpu() | |
| .numpy() | |
| ) | |
| if self.kind == "mean_control": | |
| delta = np.zeros(self.data.d) | |
| elif self.kind == "average_effect": | |
| delta = self.avg | |
| elif self.kind == "additive": | |
| # Unsupported single genes receive the training-average effect. | |
| delta = sum( | |
| (self.effects.get(g, self.avg) for g in self.data.parse(label)), | |
| start=np.zeros(self.data.d), | |
| ) | |
| else: | |
| delta = self.reg.predict(self.features([label]))[0] | |
| return np.asarray(c0) + delta | |