Download lecture_4/common.py from ChatterjeeLab/CIS6270: direct link, hf CLI and curl.
- Browser
- Download file 5.09 kB
-
https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/lecture_4/common.py
- Command line
-
hf download hf://ChatterjeeLab/CIS6270/lecture_4/common.py
-
curl -L -o common.py https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/lecture_4/common.py
5.09 kB
| """CPU teaching utilities shared by the two lecture folders.""" | |
| import csv | |
| import json | |
| import random | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| from lecture_core import DNA, K, MASK, encode | |
| def seed_all(seed): | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| torch.set_num_threads(1) | |
| def decode(tokens): | |
| alphabet = 'ACGTm' | |
| return [''.join(alphabet[int(i)] for i in row) for row in tokens] | |
| def make_data(count=512, length=8, seed=7): | |
| """Four synthetic motif families, with independent 8% base mutations.""" | |
| rng = np.random.default_rng(seed) | |
| motifs = ['ACGT', 'CGTA', 'TATA', 'GCGC'] | |
| strings, labels = [], [] | |
| for _ in range(count): | |
| motif = motifs[int(rng.integers(4))] | |
| seq = list((motif * ((length + 3) // 4))[:length]) | |
| for j in range(length): | |
| if rng.random() < .08: | |
| seq[j] = 'ACGT'[int(rng.integers(4))] | |
| strings.append(''.join(seq)) | |
| labels.append(int(sum(x in 'GC' for x in seq) / length >= .6)) | |
| return encode(strings), torch.tensor(labels) | |
| def load_data(path, length): | |
| if path is None: | |
| return make_data(length=length) | |
| with open(path) as f: | |
| rows = list(csv.DictReader(f, delimiter='\t')) | |
| strings = [r['sequence'].strip().upper() for r in rows] | |
| if not strings or any(len(s) != length or set(s) - set('ACGT') for s in strings): | |
| raise ValueError('All DNA sequences must contain only A/C/G/T and have --length bases.') | |
| labels = [int(r.get('label', sum(c in 'GC' for c in s) / length >= .6)) | |
| for r, s in zip(rows, strings)] | |
| if set(labels) - {0, 1}: | |
| raise ValueError('Labels must be 0 or 1.') | |
| return encode(strings), torch.tensor(labels) | |
| class ConditionalDNA(DNA): | |
| """The slide network plus a label embedding; label 2 means unconditional.""" | |
| def __init__(self, width=32, max_len=64): | |
| super().__init__(width, max_len) | |
| self.condition = nn.Embedding(3, width) | |
| def forward(self, z, t=None, label=None): | |
| h = self.token(z) if z.ndim == 2 else self.soft(z) | |
| pos = torch.arange(z.shape[1], device=z.device) | |
| h = h + self.position(pos)[None] | |
| if t is not None: | |
| h = h + self.time(t[:, None])[:, None] | |
| if label is None: | |
| label = torch.full((len(z),), 2, dtype=torch.long, device=z.device) | |
| h = h + self.condition(label)[:, None] | |
| return self.output(self.context(h)) | |
| def objectives(tokens): | |
| """Both toy objectives are maximized: GC fraction and ATAT agreement.""" | |
| onehot = torch.nn.functional.one_hot(tokens.long(), 4).float() | |
| return soft_objectives(onehot) | |
| def soft_objectives(z): | |
| gc = (z[..., 1] + z[..., 2]).mean(-1) | |
| motif = torch.tensor([0, 3], device=z.device).repeat((z.shape[1] + 1) // 2)[:z.shape[1]] | |
| match = z.gather(-1, motif[None, :, None].expand(z.shape[0], -1, 1)).squeeze(-1).mean(-1) | |
| return torch.stack((gc, match), -1) | |
| def metrics(tokens): | |
| strings = decode(tokens) | |
| score = objectives(tokens).mean(0) | |
| return dict(valid_dna=all(set(s) <= set('ACGT') for s in strings), | |
| unique_fraction=len(set(strings)) / len(strings), | |
| mean_gc=float(score[0]), mean_atat_match=float(score[1])) | |
| def save_run(out, config, losses, samples, extra=None): | |
| out = Path(out) | |
| out.mkdir(parents=True, exist_ok=True) | |
| (out / ('sample_config.json' if config.get('mode') == 'sample' else 'config.json')).write_text(json.dumps(config, indent=2)) | |
| if losses or not (out / 'losses.json').exists(): | |
| (out / 'losses.json').write_text(json.dumps(losses, indent=2)) | |
| (out / 'samples.txt').write_text('\n'.join(decode(samples)) + '\n') | |
| report = {'metrics': metrics(samples), **(extra or {})} | |
| (out / 'report.json').write_text(json.dumps(report, indent=2)) | |
| print(json.dumps({'output': str(out), **report}, indent=2)) | |
| return report | |
| def optimize(model, loss_fn, data, steps, batch_size=32, lr=.002): | |
| optimizer = torch.optim.AdamW(model.parameters(), lr=lr) | |
| losses = [] | |
| model.train() | |
| for step in range(steps): | |
| idx = torch.randint(len(data), (batch_size,)) | |
| optimizer.zero_grad(set_to_none=True) | |
| loss = loss_fn(model, data[idx], idx) | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError(f'Nonfinite loss at step {step}') | |
| loss.backward() | |
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1.) | |
| optimizer.step() | |
| losses.append(float(loss.detach())) | |
| model.eval() | |
| return losses | |
| def project_simplex(z, eps=1e-6): | |
| """Euclidean simplex projection, followed by a small interior floor.""" | |
| sorted_z = z.sort(-1, descending=True).values | |
| cssv = sorted_z.cumsum(-1) - 1. | |
| k = torch.arange(1, z.shape[-1] + 1, dtype=z.dtype, device=z.device) | |
| active = sorted_z - cssv / k > 0 | |
| rho = active.sum(-1, keepdim=True).clamp_min(1) | |
| theta = cssv.gather(-1, rho - 1) / rho | |
| result = (z - theta).clamp_min(eps) | |
| return result / result.sum(-1, keepdim=True) | |