Download lecture_6/common.py from ChatterjeeLab/CIS6270: direct link, hf CLI and curl.
- Browser
- Download file 7.68 kB
-
https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/lecture_6/common.py
- Command line
-
hf download hf://ChatterjeeLab/CIS6270/lecture_6/common.py
-
curl -L -o common.py https://huggingface.co/ChatterjeeLab/CIS6270/resolve/main/lecture_6/common.py
7.68 kB
| """Shared networks, data, and optimization for CIS 6270 Lecture 6. | |
| The small MLPs keep time derivatives and all training steps visible. Sequence | |
| networks receive the whole padded sequence, so predictions can depend on other | |
| positions. No pretrained checkpoint or dataset download is required. | |
| """ | |
| import copy | |
| import json | |
| import math | |
| import random | |
| from pathlib import Path | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| from torch.nn import functional as F | |
| def seed_all(seed, threads=1): | |
| random.seed(seed) | |
| np.random.seed(seed) | |
| torch.manual_seed(seed) | |
| torch.set_num_threads(threads) | |
| def mlp(inputs, outputs, width): | |
| return nn.Sequential(nn.Linear(inputs, width), nn.SiLU(), | |
| nn.Linear(width, width), nn.SiLU(), | |
| nn.Linear(width, outputs)) | |
| def time_like(t, x): | |
| """One scalar time per batch member, shape [B,1].""" | |
| t = torch.as_tensor(t, dtype=x.dtype, device=x.device) | |
| if t.numel() == 1: | |
| return t.expand(len(x), 1) | |
| return t.reshape(len(x), 1) | |
| class MapNet(nn.Module): | |
| def __init__(self, dim=2, width=64, context_dim=0): | |
| super().__init__() | |
| self.net = mlp(dim + 2 + context_dim, dim, width) | |
| def forward(self, x, s, t, context=None): | |
| inputs = [x, time_like(s, x), time_like(t, x)] | |
| if context is not None: | |
| inputs.append(context) | |
| return self.net(torch.cat(inputs, -1)) | |
| class SequenceNet(nn.Module): | |
| def __init__(self, length, vocab, width=64): | |
| super().__init__() | |
| self.length, self.vocab = length, vocab | |
| self.net = mlp(length * vocab + 2, length * vocab, width) | |
| def forward(self, x, s, t): | |
| inputs = torch.cat([x.flatten(1), time_like(s, x), time_like(t, x)], -1) | |
| return self.net(inputs).reshape(-1, self.length, self.vocab) | |
| def finite_map(model, x, s, t, context=None): | |
| """F(s,t,x) = x + (t-s) v(s,t,x), including exact F(s,s,x)=x.""" | |
| s, t = time_like(s, x), time_like(t, x) | |
| return x + (t - s) * model(x, s, t, context) | |
| def ordered_times(x, ceiling=.98): | |
| times = ceiling * torch.rand(len(x), 2, device=x.device, dtype=x.dtype) | |
| s, t = times.sort(-1).values.split(1, -1) | |
| return s, t, (s + t) / 2 | |
| def draw_batch(data, count): | |
| return data[torch.randint(len(data), (count,), device=data.device)] | |
| def interpolate(data, time, noise=None): | |
| noise = torch.randn_like(data) if noise is None else noise | |
| t = time.reshape(len(data), *([1] * (data.ndim - 1))) | |
| return (1 - t) * noise + t * data, data - noise | |
| def ema_copy(model): | |
| result = copy.deepcopy(model).eval() | |
| result.requires_grad_(False) | |
| return result | |
| def update_ema(ema, model, decay=.99): | |
| for p, q in zip(ema.parameters(), model.parameters()): | |
| p.lerp_(q, 1 - decay) | |
| def optimize(model, objective, steps, lr=1e-3, ema=None): | |
| optimizer = torch.optim.Adam(model.parameters(), lr=lr) | |
| logs = [] | |
| for step in range(steps): | |
| loss, details = objective(step) | |
| if not torch.isfinite(loss): | |
| raise FloatingPointError(f'Nonfinite loss at step {step}: {details}') | |
| optimizer.zero_grad(set_to_none=True) | |
| loss.backward() | |
| norm = torch.nn.utils.clip_grad_norm_(model.parameters(), 10.) | |
| if not torch.isfinite(norm): | |
| raise FloatingPointError(f'Nonfinite gradient at step {step}') | |
| optimizer.step() | |
| if ema is not None: | |
| update_ema(ema, model) | |
| logs.append({'step': step, 'loss': float(loss.detach()), | |
| **{k: float(torch.as_tensor(v).detach()) for k, v in details.items()}}) | |
| return logs | |
| CENTERS = torch.tensor([[-1.5, -1.5], [-1.5, 1.5], [1.5, -1.5], [1.5, 1.5]]) | |
| DATA_STD = .22 | |
| def mixture_data(n, generator=None): | |
| ids = torch.randint(4, (n,), generator=generator) | |
| return CENTERS[ids] + DATA_STD * torch.randn(n, 2, generator=generator) | |
| def mixture_posterior(x, t): | |
| """Exact p(X1 | (1-t)X0+tX1=x) for the four Gaussian mixture. | |
| Return component probabilities, conditional means, scalar variances. | |
| This analytic oracle is used for diagnostics and the GLASS example. | |
| """ | |
| t = time_like(t, x) | |
| a = 1 - t | |
| variance = a.square() + (t * DATA_STD).square() | |
| centers = CENTERS.to(x) | |
| delta = x[:, None, :] - t[:, None, :] * centers | |
| logits = -delta.square().sum(-1) / (2 * variance) | |
| prob = logits.softmax(-1) | |
| gain = t * DATA_STD**2 / variance | |
| means = centers + gain[:, None, :] * delta | |
| posterior_variance = DATA_STD**2 * a.square() / variance | |
| return prob, means, posterior_variance | |
| def exact_denoiser(x, t): | |
| p, means, _ = mixture_posterior(x, t) | |
| return (p[..., None] * means).sum(1) | |
| def exact_velocity(x, t): | |
| t = time_like(t, x) | |
| # Direct conditional velocity avoids cancellation at t=1. | |
| a = 1 - t | |
| variance = a.square() + (t * DATA_STD).square() | |
| centers = CENTERS.to(x) | |
| delta = x[:, None] - t[:, None] * centers | |
| weights = (-delta.square().sum(-1) / (2 * variance)).softmax(-1) | |
| component_velocity = centers + ((t * DATA_STD**2 - a) / variance)[:, None] * delta | |
| return (weights[..., None] * component_velocity).sum(1) | |
| def mixture_metrics(samples): | |
| x = samples.detach().cpu() | |
| distances = (x[:, None] - CENTERS).square().sum(-1) | |
| counts = torch.bincount(distances.argmin(-1), minlength=4).float() | |
| p = counts / len(x) | |
| logp = torch.logsumexp(-distances / (2 * DATA_STD**2), -1) | |
| logp -= math.log(4 * 2 * math.pi * DATA_STD**2) | |
| return {'finite_samples': bool(torch.isfinite(x).all()), | |
| 'mean_distance_to_center': float(distances.min(-1).values.sqrt().mean()), | |
| 'mode_fractions': p.tolist(), 'mode_entropy': float(-(p * p.clamp_min(1e-12).log()).sum()), | |
| 'mean_target_log_density': float(logp.mean())} | |
| def load_text(path, variable=False): | |
| lines = [x.strip().split() for x in Path(path).read_text().splitlines() if x.strip()] | |
| if len(lines) < 2: | |
| raise ValueError('Text data need at least two nonempty lines.') | |
| vocab = sorted(set(word for line in lines for word in line)) | |
| lengths = torch.tensor([len(x) for x in lines]) | |
| if not variable and len(set(lengths.tolist())) != 1: | |
| raise ValueError('Fixed-length methods require equal tokens per line; use expanding otherwise.') | |
| if lengths.max() > 32: | |
| raise ValueError('Teaching MLP supports up to 32 positions.') | |
| ids = {word: i for i, word in enumerate(vocab)} | |
| encoded = torch.zeros(len(lines), int(lengths.max()), dtype=torch.long) | |
| for i, line in enumerate(lines): | |
| encoded[i, :len(line)] = torch.tensor([ids[word] for word in line]) | |
| return encoded, lengths, vocab | |
| def text_metrics(ids, vocab, lengths=None, reference=None): | |
| lengths = [ids.shape[1]] * len(ids) if lengths is None else lengths.tolist() | |
| texts = [' '.join(vocab[j] for j in row[:length]) for row, length in zip(ids.tolist(), lengths)] | |
| flat = [j for row, length in zip(ids.tolist(), lengths) for j in row[:length]] | |
| counts = torch.bincount(torch.tensor(flat, dtype=torch.long), minlength=len(vocab)).float() | |
| p = counts / counts.sum().clamp_min(1) | |
| metrics = {'unique_fraction': len(set(texts)) / len(texts), | |
| 'token_entropy': float(-(p * p.clamp_min(1e-12).log()).sum()), | |
| 'mean_length': sum(lengths) / len(lengths), 'empty_fraction': lengths.count(0) / len(lengths)} | |
| if reference is not None: | |
| metrics['training_support_fraction'] = sum(x in reference for x in texts) / len(texts) | |
| return texts, metrics | |
| def write_json(path, value): | |
| Path(path).write_text(json.dumps(value, indent=2, allow_nan=False) + '\n') | |