| """Complete samplers and guidance extensions around the lecture code modules.""" | |
| import math | |
| import numpy as np | |
| import torch | |
| from torch import nn | |
| import torch.nn.functional as F | |
| from lecture_core import (K, MASK, draw, token_ce, udlm_rates, | |
| rate_step, geometric_cfg, pareto_filter) | |
| from common import objectives | |
| def uniform_sample(model, batch, length, steps=100, epsilon=.02): | |
| """Adaptive reverse Euler on the same truncated interval as udlm_loss. | |
| Uniform initialization at t=1-epsilon and stopping at t=epsilon | |
| are endpoint approximations. The returned sequence retains residual | |
| corruption; UDLM output probabilities are not treated as a clean posterior. | |
| """ | |
| z = torch.randint(K, (batch, length)) | |
| time = 1. - epsilon | |
| count = 0 | |
| while time > epsilon + 1e-8: | |
| t = torch.full((batch,), time) | |
| rate = udlm_rates(model(z, t).softmax(-1), z, t) | |
| max_exit = float(rate.sum(-1).max()) | |
| h = min(1. / steps, time - epsilon, .5 / max(max_exit, 1e-8)) | |
| z = rate_step(z, rate, h) | |
| time -= h | |
| count += 1 | |
| if count > 100000: | |
| raise RuntimeError('Adaptive sampler failed to advance.') | |
| return z | |
| def block_sample(model, batch, length, size=2, steps=20): | |
| """Generate one block completely before appending the next one.""" | |
| prefix = torch.empty((batch, 0), dtype=torch.long) | |
| for start in range(0, length, size): | |
| width = min(size, length - start) | |
| block = torch.full((batch, width), MASK) | |
| grid = torch.linspace(1., 0., steps + 1) | |
| for t, s in zip(grid[:-1], grid[1:]): | |
| context = torch.cat((prefix, block), 1) | |
| p = model(context)[:, start:].softmax(-1) | |
| candidate = draw(p) | |
| reveal = (block == MASK) & (torch.rand(block.shape) < (t-s)/t) | |
| block = torch.where(reveal, candidate, block) | |
| prefix = torch.cat((prefix, block), 1) | |
| return prefix | |
| def conditional_loss(model, clean, labels, drop=.15): | |
| t = torch.rand(len(clean)).clamp_min(1e-4) | |
| mask = torch.rand(clean.shape) < t[:, None] | |
| context = clean.masked_fill(mask, MASK) | |
| condition = labels.clone() | |
| condition[torch.rand(len(clean)) < drop] = 2 | |
| ce = token_ce(model(context, label=condition), clean) | |
| return (ce * mask / t[:, None]).sum(1).mean() | |
| def cfg_sample(model, batch, length, strength=2., label=1, steps=20): | |
| z = torch.full((batch, length), MASK) | |
| grid = torch.linspace(1., 0., steps + 1) | |
| labels = torch.full((batch,), label, dtype=torch.long) | |
| for t, s in zip(grid[:-1], grid[1:]): | |
| uncond = model(z).softmax(-1) | |
| cond = model(z, label=labels).softmax(-1) | |
| probability = geometric_cfg(uncond, cond, strength) | |
| candidate = draw(probability) | |
| reveal = (z == MASK) & (torch.rand(z.shape) < (t-s)/t) | |
| z = torch.where(reveal, candidate, z) | |
| return z | |
| class NoisyClassifier(nn.Module): | |
| """Predict high-GC class from a masked sequence and its noise level.""" | |
| def __init__(self, length): | |
| super().__init__() | |
| self.net = nn.Sequential(nn.Linear(length * 5 + 1, 64), | |
| nn.SiLU(), nn.Linear(64, 1)) | |
| def forward(self, z, t): | |
| features = F.one_hot(z, 5).float() if z.ndim == 2 else z | |
| return self.net(torch.cat((features.flatten(1), t[:, None]), 1)).squeeze(-1) | |
| def fit_classifier(model, data, labels, steps=200, batch=32): | |
| opt = torch.optim.Adam(model.parameters(), lr=.003) | |
| losses = [] | |
| for _ in range(steps): | |
| idx = torch.randint(len(data), (batch,)) | |
| t = torch.rand(batch) | |
| noisy = data[idx].masked_fill(torch.rand(batch, data.shape[1]) < t[:, None], MASK) | |
| loss = F.binary_cross_entropy_with_logits(model(noisy, t), labels[idx].float()) | |
| opt.zero_grad(set_to_none=True) | |
| loss.backward() | |
| opt.step() | |
| losses.append(float(loss.detach())) | |
| model.eval() | |
| return losses | |
| def log_success(classifier, z, t, label): | |
| logit = classifier(z, t) | |
| return F.logsigmoid(logit if label == 1 else -logit) | |
| def guidance_changes(classifier, z, time, label=1, gradient=False): | |
| """Log h(candidate)-log h(current), exact or first-order one-hot.""" | |
| batch, length = z.shape | |
| t = torch.full((batch,), float(time)) | |
| if gradient: | |
| with torch.enable_grad(): | |
| soft = F.one_hot(z, 5).float().requires_grad_(True) | |
| value = log_success(classifier, soft, t, label) | |
| grad = torch.autograd.grad(value.sum(), soft)[0] | |
| current = grad.gather(-1, z[..., None]) | |
| return grad[..., :K] - current | |
| with torch.no_grad(): | |
| current = log_success(classifier, z, t, label) | |
| delta = torch.empty((batch, length, K)) | |
| for i in range(length): | |
| for a in range(K): | |
| edited = z.clone() | |
| edited[:, i] = a | |
| delta[:, i, a] = log_success(classifier, edited, t, label) - current | |
| return delta | |
| def classifier_sample(model, classifier, batch, length, strength=1., | |
| label=1, steps=40, gradient=False, epsilon=.005): | |
| """Rate guidance with adaptive Euler and an explicit endpoint closure.""" | |
| z = torch.full((batch, length), MASK) | |
| time = 1. | |
| iterations = 0 | |
| while time > epsilon + 1e-8: | |
| p = model(z).softmax(-1) | |
| delta = guidance_changes(classifier, z, time, label, gradient) | |
| rates = p / time * (strength * delta).clamp(-20, 20).exp() | |
| rates *= (z == MASK)[..., None] | |
| exit_rate = rates.sum(-1) | |
| h = min(1. / steps, time-epsilon, .5 / max(float(exit_rate.max()), 1e-8)) | |
| change = torch.rand(z.shape) < h * exit_rate | |
| candidate = draw(rates / exit_rate.clamp_min(1e-12)[..., None] + 1e-12) | |
| z = torch.where(change & (z == MASK), candidate, z) | |
| time -= h | |
| iterations += 1 | |
| if iterations > 100000: | |
| raise RuntimeError('Guided rate integration failed to advance.') | |
| z = torch.where(z == MASK, draw(model(z).softmax(-1)), z) | |
| return z | |
| class SearchNode: | |
| def __init__(self, tokens, parent=None, prior=1.): | |
| self.tokens, self.parent, self.prior = tokens, parent, prior | |
| self.children = [] | |
| self.visits = 0 | |
| self.reward = np.zeros(2) | |
| def peptune_search(model, length=8, iterations=100, branching=4): | |
| """DNA MCTS: selection, expansion, completion, Pareto rewards, backup. | |
| All DNA strings are valid. Peptide chemistry, bond-dependent masks, | |
| RoFormer training, and the PepTune invalid-SMILES penalty are not used. | |
| """ | |
| root = SearchNode(torch.full((length,), MASK)) | |
| archive = torch.empty((0, length), dtype=torch.long) | |
| archive_scores = torch.empty((0, 2)) | |
| trace = [] | |
| for iteration in range(iterations): | |
| node = root | |
| while node.children: | |
| def selection(child): | |
| mean = child.reward.mean() / max(child.visits, 1) | |
| bonus = 1.5 * child.prior * math.sqrt(node.visits + 1) / (child.visits + 1) | |
| return mean + bonus | |
| node = max(node.children, key=selection) | |
| if (node.tokens == MASK).any(): | |
| p = model(node.tokens[None]).softmax(-1)[0] | |
| positions = torch.where(node.tokens == MASK)[0] | |
| seen = set() | |
| for _ in range(branching): | |
| pos = int(positions[torch.randint(len(positions), ())]) | |
| token = int(torch.multinomial(p[pos], 1)) | |
| candidate = node.tokens.clone() | |
| candidate[pos] = token | |
| key = tuple(candidate.tolist()) | |
| if key not in seen: | |
| node.children.append(SearchNode(candidate, node, float(p[pos, token]))) | |
| seen.add(key) | |
| node = node.children[0] | |
| rollout = node.tokens.clone() | |
| while (rollout == MASK).any(): | |
| prob = model(rollout[None]).softmax(-1) | |
| position = int(torch.where(rollout == MASK)[0][0]) | |
| rollout[position] = draw(prob)[0, position] | |
| score = objectives(rollout[None])[0] | |
| reward = ((score >= archive_scores).float().mean(0).numpy() | |
| if len(archive) else np.ones(2)) | |
| archive = torch.cat((archive, rollout[None])) | |
| archive_scores = torch.cat((archive_scores, score[None])) | |
| archive, archive_scores = pareto_filter(archive, archive_scores) | |
| # Equal-score alternatives remain; remove exact repeated sequences only. | |
| unique = []; seen = set() | |
| for i, row in enumerate(archive.tolist()): | |
| key = tuple(row) | |
| if key not in seen: | |
| unique.append(i); seen.add(key) | |
| archive, archive_scores = archive[unique], archive_scores[unique] | |
| while node is not None: | |
| node.visits += 1 | |
| node.reward += reward | |
| node = node.parent | |
| trace.append(dict(iteration=iteration, archive_size=len(archive), score=score.tolist())) | |
| return archive, archive_scores, trace | |