Delete cis6270
Browse files- cis6270/__init__.py +0 -20
- cis6270/data.py +0 -317
- cis6270/metrics.py +0 -100
- cis6270/nets.py +0 -231
- cis6270/runner.py +0 -173
cis6270/__init__.py
DELETED
|
@@ -1,20 +0,0 @@
|
|
| 1 |
-
"""Shared helpers for the CIS 6270 course code.
|
| 2 |
-
|
| 3 |
-
Nothing in this package is the subject of a lecture. The data, the small networks, the training
|
| 4 |
-
loop and the sample metrics live here so that each lecture file holds only the mathematics of its
|
| 5 |
-
own framework. Read this package once; after that every lecture file stands on its own.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
import warnings
|
| 9 |
-
|
| 10 |
-
# torch 2.14 warns about `torch.jit.script` from inside its own forward-mode autograd machinery,
|
| 11 |
-
# the first time `torch.func.jvp` runs. Chapter 6 uses that function, and nothing here is
|
| 12 |
-
# deprecated, so the message is filtered and the reader never sees it.
|
| 13 |
-
warnings.filterwarnings(
|
| 14 |
-
"ignore", message=r"`torch\.jit\.script` is deprecated", category=FutureWarning,
|
| 15 |
-
)
|
| 16 |
-
|
| 17 |
-
from cis6270 import data, metrics, nets, runner
|
| 18 |
-
|
| 19 |
-
__all__ = ["data", "metrics", "nets", "runner"]
|
| 20 |
-
__version__ = "1.0.0"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cis6270/data.py
DELETED
|
@@ -1,317 +0,0 @@
|
|
| 1 |
-
"""Datasets for the course.
|
| 2 |
-
|
| 3 |
-
Everything here runs offline in under a second, so a fresh clone and the test suite need no
|
| 4 |
-
network. MNIST and ESM-2 embeddings download on first use and are reached only through the
|
| 5 |
-
``--dataset`` flag of the scripts that offer them.
|
| 6 |
-
|
| 7 |
-
The discrete lectures all use the same synthetic DNA, so a loss that improves in Lecture 4 and a
|
| 8 |
-
loss that improves in Lecture 5 are measured on the same sequences.
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
from __future__ import annotations
|
| 12 |
-
|
| 13 |
-
from pathlib import Path
|
| 14 |
-
|
| 15 |
-
import torch
|
| 16 |
-
|
| 17 |
-
# %% 01. The DNA alphabet
|
| 18 |
-
#
|
| 19 |
-
# Chapters 4 and 5 run every worked example on DNA, so the vocabulary is four letters. The mask
|
| 20 |
-
# token, when a lecture needs one, is appended as index 4 by that lecture, not here, because masked
|
| 21 |
-
# diffusion has a mask and uniform diffusion does not.
|
| 22 |
-
|
| 23 |
-
DNA = "ACGT" # Token 0 is A, 1 is C, 2 is G, 3 is T.
|
| 24 |
-
DNA_SIZE = len(DNA) # K = 4, the alphabet size the chapters call |V|.
|
| 25 |
-
MOTIF = "ACGT" # The planted motif, used as the label in the guidance examples.
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def encode_dna(sequences: list[str]) -> torch.Tensor:
|
| 29 |
-
"""Turn a list of strings over ACGT into a long tensor of shape [N, L]."""
|
| 30 |
-
index = {letter: i for i, letter in enumerate(DNA)} # Letter to token id.
|
| 31 |
-
rows = [[index[letter] for letter in sequence] for sequence in sequences]
|
| 32 |
-
return torch.tensor(rows, dtype=torch.long) # [N, L] token ids.
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
def decode_dna(tokens: torch.Tensor) -> list[str]:
|
| 36 |
-
"""Turn a long tensor of shape [N, L] back into strings over ACGT."""
|
| 37 |
-
return ["".join(DNA[int(i)] for i in row) for row in tokens] # One string per row.
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
# %% 02. Synthetic DNA with structure a small model can actually learn
|
| 41 |
-
#
|
| 42 |
-
# A uniform random sequence has nothing to learn, so the sampler would look the same before and
|
| 43 |
-
# after training. These sequences carry two pieces of structure instead: a GC-rich background, and
|
| 44 |
-
# the motif ACGT planted at a random position in a fraction of the sequences. The GC fraction then
|
| 45 |
-
# gives a label for the conditional examples, and the motif gives a property to steer toward.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def synthetic_dna(
|
| 49 |
-
num_sequences: int = 512,
|
| 50 |
-
length: int = 16,
|
| 51 |
-
motif_fraction: float = 0.75,
|
| 52 |
-
gc_bias: float = 0.6,
|
| 53 |
-
seed: int = 0,
|
| 54 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 55 |
-
"""Draw synthetic DNA with a planted motif.
|
| 56 |
-
|
| 57 |
-
Returns the tokens as a long tensor of shape [N, L] and a binary label of shape [N] that is
|
| 58 |
-
one when the sequence has GC fraction at least 0.6. The label is a deterministic function of
|
| 59 |
-
the sequence, not a measurement of anything biological.
|
| 60 |
-
"""
|
| 61 |
-
generator = torch.Generator().manual_seed(seed) # Local generator; global seed untouched.
|
| 62 |
-
|
| 63 |
-
# Background letters: C and G each get gc_bias/2, A and T share the rest.
|
| 64 |
-
background = torch.tensor([
|
| 65 |
-
(1.0 - gc_bias) / 2.0, # A
|
| 66 |
-
gc_bias / 2.0, # C
|
| 67 |
-
gc_bias / 2.0, # G
|
| 68 |
-
(1.0 - gc_bias) / 2.0, # T
|
| 69 |
-
])
|
| 70 |
-
tokens = torch.multinomial(
|
| 71 |
-
background.expand(num_sequences * length, DNA_SIZE), # One draw per position.
|
| 72 |
-
num_samples=1,
|
| 73 |
-
replacement=True,
|
| 74 |
-
generator=generator,
|
| 75 |
-
).view(num_sequences, length) # [N, L] background sequence.
|
| 76 |
-
|
| 77 |
-
# Plant the motif in a fraction of the sequences, at a position drawn uniformly.
|
| 78 |
-
motif = encode_dna([MOTIF])[0] # [4] token ids of ACGT.
|
| 79 |
-
carries_motif = torch.rand(num_sequences, generator=generator) < motif_fraction # [N] bool.
|
| 80 |
-
# The motif starts at one of three places, not anywhere, which leaves it a pattern a
|
| 81 |
-
# small model can learn from a few hundred sequences while still varying its position. A start
|
| 82 |
-
# is kept only when the whole motif fits, so a short sequence simply has fewer of them.
|
| 83 |
-
room = length - len(MOTIF) # Last index at which the motif still fits.
|
| 84 |
-
choices = torch.tensor([s for s in (2, 6, 10) if s <= room] or [0])
|
| 85 |
-
starts = choices[torch.randint(0, len(choices), (num_sequences,), generator=generator)]
|
| 86 |
-
for row in torch.nonzero(carries_motif).flatten().tolist():
|
| 87 |
-
start = int(starts[row])
|
| 88 |
-
tokens[row, start:start + len(MOTIF)] = motif # Overwrite those positions.
|
| 89 |
-
|
| 90 |
-
gc = ((tokens == 1) | (tokens == 2)).to(torch.get_default_dtype()).mean(dim=1) # [N] fraction of C and G.
|
| 91 |
-
labels = (gc >= 0.6).long() # [N] the toy label used by the conditional examples.
|
| 92 |
-
return tokens, labels
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def dna_splits(
|
| 96 |
-
num_sequences: int = 512, length: int = 16, seed: int = 0, validation: float = 0.2,
|
| 97 |
-
) -> dict[str, torch.Tensor]:
|
| 98 |
-
"""The same synthetic DNA, split into a training and a validation set."""
|
| 99 |
-
tokens, labels = synthetic_dna(num_sequences, length, seed=seed)
|
| 100 |
-
cut = int(num_sequences * (1.0 - validation)) # Index where validation begins.
|
| 101 |
-
return {
|
| 102 |
-
"train_tokens": tokens[:cut], "train_labels": labels[:cut],
|
| 103 |
-
"valid_tokens": tokens[cut:], "valid_labels": labels[cut:],
|
| 104 |
-
}
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
def write_dna_tsv(path: str | Path, tokens: torch.Tensor, labels: torch.Tensor) -> None:
|
| 108 |
-
"""Write sequences and labels as a two-column TSV, the format ``--data`` reads."""
|
| 109 |
-
lines = ["sequence\tlabel"]
|
| 110 |
-
lines += [f"{s}\t{int(y)}" for s, y in zip(decode_dna(tokens), labels)]
|
| 111 |
-
Path(path).write_text("\n".join(lines) + "\n")
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
def read_dna_tsv(path: str | Path) -> tuple[torch.Tensor, torch.Tensor]:
|
| 115 |
-
"""Read a two-column TSV of sequences and labels."""
|
| 116 |
-
rows = Path(path).read_text().strip().split("\n")[1:] # Skip the header line.
|
| 117 |
-
sequences = [row.split("\t")[0] for row in rows]
|
| 118 |
-
labels = [int(row.split("\t")[1]) for row in rows]
|
| 119 |
-
return encode_dna(sequences), torch.tensor(labels, dtype=torch.long)
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
# %% 03. Two-dimensional densities
|
| 123 |
-
#
|
| 124 |
-
# The continuous lectures need a target we can plot and whose exact density we know. Four Gaussians
|
| 125 |
-
# at the corners of a square give both: the modes are far enough apart that a sampler that collapses
|
| 126 |
-
# is obvious, and the log density is a closed form the reports can quote.
|
| 127 |
-
|
| 128 |
-
GAUSSIAN_CENTERS = torch.tensor([[2.0, 2.0], [2.0, -2.0], [-2.0, 2.0], [-2.0, -2.0]])
|
| 129 |
-
GAUSSIAN_STD = 0.35 # Standard deviation of each component.
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
def gaussian_mixture_2d(num_points: int = 2048, seed: int = 0) -> torch.Tensor:
|
| 133 |
-
"""Draw from four equally weighted Gaussians at the corners of a square. Returns [N, 2]."""
|
| 134 |
-
generator = torch.Generator().manual_seed(seed)
|
| 135 |
-
which = torch.randint(0, len(GAUSSIAN_CENTERS), (num_points,), generator=generator) # [N].
|
| 136 |
-
noise = torch.randn(num_points, 2, generator=generator) * GAUSSIAN_STD # [N, 2].
|
| 137 |
-
return GAUSSIAN_CENTERS[which] + noise # [N, 2] points.
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def gaussian_mixture_log_density(points: torch.Tensor) -> torch.Tensor:
|
| 141 |
-
"""Exact log density of the four-Gaussian mixture at each row of ``points`` ([N, 2] -> [N])."""
|
| 142 |
-
diff = points[:, None, :] - GAUSSIAN_CENTERS.to(points) # [N, 4, 2] offset to each center.
|
| 143 |
-
squared = (diff ** 2).sum(dim=-1) # [N, 4] squared distance to each center.
|
| 144 |
-
log_component = (
|
| 145 |
-
-0.5 * squared / GAUSSIAN_STD ** 2 # Gaussian exponent.
|
| 146 |
-
- torch.log(torch.tensor(2.0 * torch.pi * GAUSSIAN_STD ** 2)) # Normalizer in 2-D.
|
| 147 |
-
) # [N, 4].
|
| 148 |
-
weight = torch.log(torch.tensor(1.0 / len(GAUSSIAN_CENTERS))) # Equal mixture weights.
|
| 149 |
-
return torch.logsumexp(log_component + weight, dim=-1) # [N] mixture log density.
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
def two_moons(num_points: int = 2048, noise: float = 0.08, seed: int = 0) -> torch.Tensor:
|
| 153 |
-
"""Two interleaving half circles, the standard test for a sampler that must bend. [N, 2]."""
|
| 154 |
-
generator = torch.Generator().manual_seed(seed)
|
| 155 |
-
half = num_points // 2
|
| 156 |
-
angle = torch.rand(half, generator=generator) * torch.pi # [half] angle along each arc.
|
| 157 |
-
upper = torch.stack([torch.cos(angle), torch.sin(angle)], dim=1) # Upper half circle.
|
| 158 |
-
lower = torch.stack([1.0 - torch.cos(angle), 0.5 - torch.sin(angle)], dim=1) # Lower arc.
|
| 159 |
-
points = torch.cat([upper, lower], dim=0) # [2*half, 2].
|
| 160 |
-
points = points + noise * torch.randn(points.shape, generator=generator) # Blur the arcs.
|
| 161 |
-
return (points - points.mean(0)) / points.std(0) # Standardize to roughly unit scale.
|
| 162 |
-
|
| 163 |
-
|
| 164 |
-
def checkerboard(num_points: int = 2048, seed: int = 0) -> torch.Tensor:
|
| 165 |
-
"""A checkerboard of eight squares, a density with disconnected support. [N, 2]."""
|
| 166 |
-
generator = torch.Generator().manual_seed(seed)
|
| 167 |
-
x = torch.rand(num_points, generator=generator) * 4.0 - 2.0 # [N] in [-2, 2].
|
| 168 |
-
y = torch.rand(num_points, generator=generator) - torch.randint(
|
| 169 |
-
0, 2, (num_points,), generator=generator,
|
| 170 |
-
).to(torch.get_default_dtype()) * 2.0 # [N] in [-2, 1] before the shift below.
|
| 171 |
-
y = y + torch.floor(x) % 2.0 # Shift alternate columns so the squares interleave.
|
| 172 |
-
return torch.stack([x, y * 2.0], dim=1) # [N, 2] points.
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
# %% 04. Short phrases, for the discrete examples whose tokens are words
|
| 176 |
-
#
|
| 177 |
-
# Chapter 6 runs its categorical flow maps on four-token phrases. The grammar is small enough that
|
| 178 |
-
# a reader can tell at a glance whether a generated phrase is in the language.
|
| 179 |
-
|
| 180 |
-
_SUBJECTS = ["red", "blue", "green", "small"]
|
| 181 |
-
_OBJECTS = ["circle", "square", "arrow", "dot"]
|
| 182 |
-
_VERBS = ["moves", "turns", "drifts", "jumps"]
|
| 183 |
-
_DIRECTIONS = ["left", "right", "up", "down"]
|
| 184 |
-
|
| 185 |
-
|
| 186 |
-
def phrase_vocabulary() -> list[str]:
|
| 187 |
-
"""The token vocabulary of the phrase language, in a fixed order."""
|
| 188 |
-
return _SUBJECTS + _OBJECTS + _VERBS + _DIRECTIONS # 16 tokens, four per slot.
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
def phrases(num_phrases: int = 512, seed: int = 0) -> tuple[torch.Tensor, list[str]]:
|
| 192 |
-
"""Draw four-token phrases such as ``red circle moves left``. Returns [N, 4] and the vocabulary.
|
| 193 |
-
|
| 194 |
-
Each slot draws from its own quarter of the vocabulary, so a correct phrase is one whose
|
| 195 |
-
position i holds a token from group i. That makes the training support easy to check.
|
| 196 |
-
"""
|
| 197 |
-
generator = torch.Generator().manual_seed(seed)
|
| 198 |
-
vocabulary = phrase_vocabulary()
|
| 199 |
-
groups = [_SUBJECTS, _OBJECTS, _VERBS, _DIRECTIONS]
|
| 200 |
-
columns = []
|
| 201 |
-
for slot, group in enumerate(groups):
|
| 202 |
-
offset = slot * len(group) # Where this slot's tokens start in the vocabulary.
|
| 203 |
-
picks = torch.randint(0, len(group), (num_phrases,), generator=generator)
|
| 204 |
-
columns.append(picks + offset) # [N] token ids inside the full vocabulary.
|
| 205 |
-
return torch.stack(columns, dim=1), vocabulary # [N, 4] and the 16 token strings.
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
# %% 05. MNIST and ESM-2, the two datasets that download
|
| 209 |
-
#
|
| 210 |
-
# Both are optional. A script reaches them only when the reader passes --dataset mnist or
|
| 211 |
-
# --dataset esm2, and every such script also runs on the offline data above.
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
def mnist_loader(batch_size: int = 128, root: str | Path = "data", train: bool = True):
|
| 215 |
-
"""MNIST as a DataLoader of images normalized to [-1, 1]. Downloads on first use."""
|
| 216 |
-
from torch.utils.data import DataLoader # Imported here so the module loads without torchvision.
|
| 217 |
-
from torchvision import datasets
|
| 218 |
-
from torchvision.transforms import v2
|
| 219 |
-
|
| 220 |
-
transform = v2.Compose([
|
| 221 |
-
v2.ToImage(), # Channel-first image: [1, 28, 28].
|
| 222 |
-
v2.ToDtype(torch.float32, scale=True), # Integer pixels 0-255 to floats in [0, 1].
|
| 223 |
-
v2.Normalize(mean=(0.5,), std=(0.5,)), # Then [0, 1] to [-1, 1].
|
| 224 |
-
])
|
| 225 |
-
dataset = datasets.MNIST(root=root, train=train, download=True, transform=transform)
|
| 226 |
-
return DataLoader(dataset, batch_size=batch_size, shuffle=train, num_workers=0)
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def toy_images(num_images: int = 256, size: int = 28, seed: int = 0) -> torch.Tensor:
|
| 230 |
-
"""Images shaped like MNIST, drawn from a two-mode distribution of bars and blobs.
|
| 231 |
-
|
| 232 |
-
These stand in for MNIST wherever a test has to run without a download. They have the same
|
| 233 |
-
shape and the same [-1, 1] range, so the identical training and sampling code paths run.
|
| 234 |
-
"""
|
| 235 |
-
generator = torch.Generator().manual_seed(seed)
|
| 236 |
-
images = torch.full((num_images, 1, size, size), -1.0) # Background at the low end of [-1, 1].
|
| 237 |
-
grid = torch.arange(size)
|
| 238 |
-
for i in range(num_images):
|
| 239 |
-
if i % 2 == 0: # A vertical bar.
|
| 240 |
-
column = int(torch.randint(6, size - 6, (1,), generator=generator))
|
| 241 |
-
images[i, 0, 4:size - 4, column - 2:column + 2] = 1.0
|
| 242 |
-
else: # A round blob.
|
| 243 |
-
cy = int(torch.randint(8, size - 8, (1,), generator=generator))
|
| 244 |
-
cx = int(torch.randint(8, size - 8, (1,), generator=generator))
|
| 245 |
-
dy = (grid[:, None] - cy) ** 2
|
| 246 |
-
dx = (grid[None, :] - cx) ** 2
|
| 247 |
-
images[i, 0][(dy + dx) <= 25] = 1.0
|
| 248 |
-
images = images + 0.05 * torch.randn(images.shape, generator=generator) # Light pixel noise.
|
| 249 |
-
return images.clamp(-1.0, 1.0) # [N, 1, size, size] in the training range.
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
PEPTIDES = [
|
| 253 |
-
"KWKLFKKIGAVLKVL", "GIGKFLHSAKKFGKA", "FLPIIAKLLSGLL", "ILPWKWPWWPWRR",
|
| 254 |
-
"KRWWKWWRR", "GLLSKLWKKVFKAFKKFLKK", "ACDEFGHIKLMNPQR", "RRWWRRWRR",
|
| 255 |
-
"LLGDFFRKSKEKIGKE", "KIAKVALKAL", "VRRFPWWWPFLRR", "GWLKKIGKKIERVGQ",
|
| 256 |
-
] # Short antimicrobial-peptide-like sequences, used when ESM-2 is unavailable.
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
def esm2_embeddings(
|
| 260 |
-
sequences: list[str] | None = None,
|
| 261 |
-
model_name: str = "facebook/esm2_t33_650M_UR50D",
|
| 262 |
-
max_length: int = 32,
|
| 263 |
-
device: torch.device | str = "cpu",
|
| 264 |
-
):
|
| 265 |
-
"""Residue embeddings from a frozen ESM-2 encoder. Downloads the checkpoint on first use.
|
| 266 |
-
|
| 267 |
-
Returns the embeddings [N, L, D], the attention mask [N, L], the tokenizer and the masked
|
| 268 |
-
language-model head, which Lecture 3 uses to decode generated latents back to amino acids.
|
| 269 |
-
"""
|
| 270 |
-
from transformers import AutoModel, AutoModelForMaskedLM, AutoTokenizer
|
| 271 |
-
|
| 272 |
-
sequences = sequences or PEPTIDES
|
| 273 |
-
tokenizer = AutoTokenizer.from_pretrained(model_name)
|
| 274 |
-
encoder = AutoModel.from_pretrained(model_name).to(device).eval()
|
| 275 |
-
head = AutoModelForMaskedLM.from_pretrained(model_name).lm_head.to(device).eval()
|
| 276 |
-
for parameter in encoder.parameters():
|
| 277 |
-
parameter.requires_grad = False # The encoder is frozen; only the denoiser trains.
|
| 278 |
-
|
| 279 |
-
batch = tokenizer(
|
| 280 |
-
sequences, padding="max_length", truncation=True,
|
| 281 |
-
max_length=max_length, return_tensors="pt",
|
| 282 |
-
).to(device)
|
| 283 |
-
with torch.no_grad():
|
| 284 |
-
embeddings = encoder(**batch).last_hidden_state # [N, L, D] residue embeddings.
|
| 285 |
-
return embeddings, batch["attention_mask"], tokenizer, head
|
| 286 |
-
|
| 287 |
-
|
| 288 |
-
def toy_embeddings(
|
| 289 |
-
num_sequences: int = 128, length: int = 16, width: int = 32, seed: int = 0,
|
| 290 |
-
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 291 |
-
"""Embeddings shaped like ESM-2 output, for the offline path of the latent examples.
|
| 292 |
-
|
| 293 |
-
Each sequence is a smooth low-frequency curve along the residue axis plus noise, so neighbouring
|
| 294 |
-
positions are correlated the way real residue embeddings are. Returns [N, L, D] and a mask.
|
| 295 |
-
"""
|
| 296 |
-
generator = torch.Generator().manual_seed(seed)
|
| 297 |
-
phase = torch.rand(num_sequences, 1, width, generator=generator) * 2.0 * torch.pi
|
| 298 |
-
frequency = torch.rand(num_sequences, 1, width, generator=generator) * 0.6 + 0.1
|
| 299 |
-
position = torch.arange(length).view(1, length, 1).to(torch.get_default_dtype()) # [1, L, 1].
|
| 300 |
-
signal = torch.sin(frequency * position + phase) # [N, L, D] smooth along the residue axis.
|
| 301 |
-
noise = 0.2 * torch.randn(num_sequences, length, width, generator=generator)
|
| 302 |
-
mask = torch.ones(num_sequences, length) # No padding in the toy set.
|
| 303 |
-
return signal + noise, mask
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
def standardize(embeddings: torch.Tensor, mask: torch.Tensor):
|
| 307 |
-
"""Center and scale each embedding coordinate over the valid residues.
|
| 308 |
-
|
| 309 |
-
Returns the standardized embeddings and the mean and standard deviation, which the decoder
|
| 310 |
-
needs in order to undo the transformation before reading amino acids.
|
| 311 |
-
"""
|
| 312 |
-
valid = mask.bool().unsqueeze(-1) # [N, L, 1] true at a real residue.
|
| 313 |
-
count = valid.sum() # Number of valid residue-coordinate entries.
|
| 314 |
-
mean = (embeddings * valid).sum(dim=(0, 1)) / count # [D] per-coordinate mean.
|
| 315 |
-
variance = ((embeddings - mean) ** 2 * valid).sum(dim=(0, 1)) / count # [D].
|
| 316 |
-
std = variance.sqrt().clamp(min=1e-6) # [D], clamped so a dead coordinate cannot divide by zero.
|
| 317 |
-
return (embeddings - mean) / std, mean, std
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cis6270/metrics.py
DELETED
|
@@ -1,100 +0,0 @@
|
|
| 1 |
-
"""Measurements of a batch of generated samples.
|
| 2 |
-
|
| 3 |
-
None of these is a benchmark. They are small, exactly computable quantities that tell a reader
|
| 4 |
-
whether a sampler learned the structure planted in the data: the right letter frequencies, the
|
| 5 |
-
planted motif, the right number of modes. A number here is a diagnostic, not a score.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import torch
|
| 11 |
-
|
| 12 |
-
from cis6270.data import GAUSSIAN_CENTERS, MOTIF, decode_dna
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
# %% 01. Measurements on DNA
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def gc_fraction(tokens: torch.Tensor) -> float:
|
| 19 |
-
"""Fraction of positions holding C or G, averaged over the batch. The training data sit at 0.577."""
|
| 20 |
-
return float(((tokens == 1) | (tokens == 2)).to(torch.get_default_dtype()).mean())
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def motif_fraction(tokens: torch.Tensor, motif: str = MOTIF) -> float:
|
| 24 |
-
"""Fraction of sequences that contain the planted motif anywhere."""
|
| 25 |
-
sequences = decode_dna(tokens)
|
| 26 |
-
return sum(motif in sequence for sequence in sequences) / max(len(sequences), 1)
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
def unique_fraction(tokens: torch.Tensor) -> float:
|
| 30 |
-
"""Fraction of generated sequences that are distinct, which catches a collapsed sampler.
|
| 31 |
-
|
| 32 |
-
The comparison is on the token ids themselves, so this works for any vocabulary, not only the
|
| 33 |
-
four DNA letters.
|
| 34 |
-
"""
|
| 35 |
-
rows = [tuple(row.tolist()) for row in tokens] # One tuple per sequence.
|
| 36 |
-
return len(set(rows)) / max(len(rows), 1)
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
def token_entropy(tokens: torch.Tensor, vocab_size: int) -> float:
|
| 40 |
-
"""Entropy in nats of the letter frequencies, at most log K. Four letters give log 4 = 1.386."""
|
| 41 |
-
counts = torch.bincount(tokens.flatten(), minlength=vocab_size).to(torch.get_default_dtype())
|
| 42 |
-
probabilities = counts / counts.sum().clamp(min=1)
|
| 43 |
-
nonzero = probabilities[probabilities > 0]
|
| 44 |
-
return float(-(nonzero * nonzero.log()).sum())
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def support_fraction(tokens: torch.Tensor, training: torch.Tensor) -> float:
|
| 48 |
-
"""Fraction of generated sequences that also appear in the training set."""
|
| 49 |
-
seen = {tuple(row.tolist()) for row in training}
|
| 50 |
-
return sum(tuple(row.tolist()) in seen for row in tokens) / max(len(tokens), 1)
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
# %% 02. Measurements on two-dimensional samples
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def mode_report(points: torch.Tensor, centers: torch.Tensor | None = None) -> dict:
|
| 57 |
-
"""Assign each point to its nearest mode and report the distance and the coverage.
|
| 58 |
-
|
| 59 |
-
``nearest_distance`` near the data's own standard deviation means the points land on the modes.
|
| 60 |
-
``mode_entropy`` near log 4 = 1.386 means all four modes are visited about equally.
|
| 61 |
-
"""
|
| 62 |
-
centers = GAUSSIAN_CENTERS.to(points) if centers is None else centers.to(points)
|
| 63 |
-
distance = torch.cdist(points, centers) # [N, M] distance to every center.
|
| 64 |
-
nearest, which = distance.min(dim=1) # [N] distance and index of the closest mode.
|
| 65 |
-
counts = torch.bincount(which, minlength=len(centers)).to(torch.get_default_dtype())
|
| 66 |
-
fractions = counts / counts.sum().clamp(min=1)
|
| 67 |
-
nonzero = fractions[fractions > 0]
|
| 68 |
-
return {
|
| 69 |
-
"nearest_distance": float(nearest.mean()),
|
| 70 |
-
"modes_found": int((counts > 0).sum()),
|
| 71 |
-
"mode_entropy": float(-(nonzero * nonzero.log()).sum()),
|
| 72 |
-
"mode_fractions": [round(float(f), 3) for f in fractions],
|
| 73 |
-
}
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
def mean_log_density(points: torch.Tensor) -> float:
|
| 77 |
-
"""Average exact log density of the four-Gaussian target at the generated points."""
|
| 78 |
-
from cis6270.data import gaussian_mixture_log_density
|
| 79 |
-
|
| 80 |
-
return float(gaussian_mixture_log_density(points).mean())
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
# %% 03. Measurements on a learned distribution over a small state space
|
| 84 |
-
#
|
| 85 |
-
# Several chapters work on a state space small enough to enumerate, where the exact answer is
|
| 86 |
-
# available. Comparing an empirical histogram with that exact answer is the sharpest check the
|
| 87 |
-
# course has.
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
def total_variation(p: torch.Tensor, q: torch.Tensor) -> float:
|
| 91 |
-
"""Half the sum of absolute differences between two distributions over the same states."""
|
| 92 |
-
return float(0.5 * (p - q).abs().sum())
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
def empirical_distribution(tokens: torch.Tensor, vocab_size: int, length: int) -> torch.Tensor:
|
| 96 |
-
"""Histogram of whole sequences over the K**L states, for a state space small enough to list."""
|
| 97 |
-
weights = vocab_size ** torch.arange(length - 1, -1, -1, device=tokens.device) # Place values.
|
| 98 |
-
indices = (tokens * weights).sum(dim=1) # [N] index of each sequence in the full state space.
|
| 99 |
-
counts = torch.bincount(indices, minlength=vocab_size ** length).to(torch.get_default_dtype())
|
| 100 |
-
return counts / counts.sum().clamp(min=1) # [K**L] empirical distribution.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cis6270/nets.py
DELETED
|
@@ -1,231 +0,0 @@
|
|
| 1 |
-
"""The small networks every lecture reuses.
|
| 2 |
-
|
| 3 |
-
Four networks cover the whole course. None of them is the subject of a lecture: a lecture changes
|
| 4 |
-
the loss and the sampler, and keeps the network fixed, so that any difference in the samples comes
|
| 5 |
-
from the mathematics and not from the architecture.
|
| 6 |
-
|
| 7 |
-
| Network | Input | Output | Used by |
|
| 8 |
-
| ------------------ | -------------------------- | --------------------- | ------- |
|
| 9 |
-
| `TimeEmbedding` | a time or step index | a vector of width D | all |
|
| 10 |
-
| `SequenceDenoiser` | tokens [B, L] and a time | logits [B, L, K] | 4, 5, 6 |
|
| 11 |
-
| `VelocityMLP` | a point [B, D] and a time | a vector [B, D] | 2, 3, 6, 7 |
|
| 12 |
-
| `TwoTimeMLP` | a point, a start, an end | a vector [B, D] | 6 |
|
| 13 |
-
| `SmallUNet` | an image [B, 1, 28, 28] | an image of the same | 2, 3 |
|
| 14 |
-
"""
|
| 15 |
-
|
| 16 |
-
from __future__ import annotations
|
| 17 |
-
|
| 18 |
-
import math
|
| 19 |
-
|
| 20 |
-
import torch
|
| 21 |
-
import torch.nn as nn
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
# %% 01. Embedding a continuous time
|
| 25 |
-
#
|
| 26 |
-
# Every network in the course takes a time as well as a state. A raw scalar in [0, 1] is a poor
|
| 27 |
-
# input to a linear layer, so it is first spread over many frequencies, exactly as in Section 3.28
|
| 28 |
-
# of the notes, and then passed through a small MLP.
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def sinusoidal_embedding(t: torch.Tensor, width: int) -> torch.Tensor:
|
| 32 |
-
"""Spread a time [B] over ``width`` sine and cosine frequencies. Returns [B, width]."""
|
| 33 |
-
half = width // 2 # Half the coordinates are sines and half cosines.
|
| 34 |
-
frequencies = torch.exp(
|
| 35 |
-
-math.log(10000.0) * torch.arange(half, device=t.device).to(torch.get_default_dtype()) / half
|
| 36 |
-
) # [half] geometrically spaced frequencies.
|
| 37 |
-
angles = t.to(torch.get_default_dtype())[:, None] * frequencies[None, :] # [B, half].
|
| 38 |
-
return torch.cat([torch.sin(angles), torch.cos(angles)], dim=1) # [B, width].
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
class TimeEmbedding(nn.Module):
|
| 42 |
-
"""Sinusoidal features of the time, followed by a two-layer MLP."""
|
| 43 |
-
|
| 44 |
-
def __init__(self, width: int = 128, scale: float = 1000.0):
|
| 45 |
-
super().__init__()
|
| 46 |
-
self.width = width # Width of the embedding the rest of the network receives.
|
| 47 |
-
self.scale = scale # Times in [0, 1] are multiplied by this before the sines.
|
| 48 |
-
self.mlp = nn.Sequential(
|
| 49 |
-
nn.Linear(width, width), nn.SiLU(), nn.Linear(width, width),
|
| 50 |
-
)
|
| 51 |
-
|
| 52 |
-
def forward(self, t: torch.Tensor) -> torch.Tensor:
|
| 53 |
-
"""t: [B] -> [B, width]."""
|
| 54 |
-
return self.mlp(sinusoidal_embedding(t * self.scale, self.width)) # [B, width].
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
# %% 02. A denoiser over token sequences
|
| 58 |
-
#
|
| 59 |
-
# Lectures 4, 5 and 6 all train the same shape of network: it reads a corrupted sequence and a time,
|
| 60 |
-
# and produces a distribution over the vocabulary at every position. The notes call it the denoiser
|
| 61 |
-
# d_theta(. | z, t). Attention lets the prediction at one position depend on every other position,
|
| 62 |
-
# which is what makes the factorized reverse step of Chapter 4 worth taking.
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
class SequenceDenoiser(nn.Module):
|
| 66 |
-
"""A small transformer that maps a token sequence and a time to per-position logits."""
|
| 67 |
-
|
| 68 |
-
def __init__(
|
| 69 |
-
self,
|
| 70 |
-
vocab_size: int,
|
| 71 |
-
length: int,
|
| 72 |
-
width: int = 128,
|
| 73 |
-
depth: int = 2,
|
| 74 |
-
heads: int = 4,
|
| 75 |
-
num_classes: int = 0,
|
| 76 |
-
):
|
| 77 |
-
super().__init__()
|
| 78 |
-
self.vocab_size = vocab_size # K, including a mask token when the lecture adds one.
|
| 79 |
-
self.length = length # L, the number of positions.
|
| 80 |
-
self.width = width # D, the model width.
|
| 81 |
-
self.token_embedding = nn.Embedding(vocab_size, width) # One vector per token id.
|
| 82 |
-
self.position_embedding = nn.Embedding(length, width) # One vector per position.
|
| 83 |
-
self.time_embedding = TimeEmbedding(width) # Shared across positions.
|
| 84 |
-
# A label embedding, used only by the conditional examples. Index num_classes is the null
|
| 85 |
-
# label that classifier-free guidance drops to.
|
| 86 |
-
self.label_embedding = (
|
| 87 |
-
nn.Embedding(num_classes + 1, width) if num_classes else None
|
| 88 |
-
)
|
| 89 |
-
layer = nn.TransformerEncoderLayer(
|
| 90 |
-
d_model=width, nhead=heads, dim_feedforward=4 * width,
|
| 91 |
-
batch_first=True, dropout=0.0, norm_first=True,
|
| 92 |
-
)
|
| 93 |
-
self.transformer = nn.TransformerEncoder(
|
| 94 |
-
layer, num_layers=depth, enable_nested_tensor=False,
|
| 95 |
-
)
|
| 96 |
-
self.head = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, vocab_size))
|
| 97 |
-
|
| 98 |
-
def forward(
|
| 99 |
-
self, tokens: torch.Tensor, t: torch.Tensor, labels: torch.Tensor | None = None,
|
| 100 |
-
) -> torch.Tensor:
|
| 101 |
-
"""tokens: [B, L], t: [B], labels: [B] or None -> logits [B, L, K]."""
|
| 102 |
-
positions = torch.arange(tokens.shape[1], device=tokens.device) # [L].
|
| 103 |
-
h = (
|
| 104 |
-
self.token_embedding(tokens) # [B, L, D] what each position currently holds.
|
| 105 |
-
+ self.position_embedding(positions)[None, :, :] # [1, L, D] where it sits.
|
| 106 |
-
+ self.time_embedding(t)[:, None, :] # [B, 1, D] how far along the path we are.
|
| 107 |
-
)
|
| 108 |
-
if labels is not None and self.label_embedding is not None:
|
| 109 |
-
h = h + self.label_embedding(labels)[:, None, :] # [B, 1, D] the condition.
|
| 110 |
-
h = self.transformer(h) # [B, L, D] every position now sees every other.
|
| 111 |
-
return self.head(h) # [B, L, K] logits over the vocabulary at each position.
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
# %% 03. A velocity network on a vector state
|
| 115 |
-
#
|
| 116 |
-
# Lectures 2, 3, 6 and 7 learn a field on R^d: a velocity, a score, a noise prediction or a drift.
|
| 117 |
-
# The same MLP serves for all of them, because the only difference is what the loss regresses it
|
| 118 |
-
# onto.
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
class VelocityMLP(nn.Module):
|
| 122 |
-
"""An MLP from a point and a time to a vector of the same width as the point."""
|
| 123 |
-
|
| 124 |
-
def __init__(self, dim: int = 2, width: int = 128, depth: int = 3, num_classes: int = 0):
|
| 125 |
-
super().__init__()
|
| 126 |
-
self.dim = dim # d, the width of the state.
|
| 127 |
-
self.time_embedding = TimeEmbedding(width, scale=1.0) # Times here already live in [0, 1].
|
| 128 |
-
self.label_embedding = (
|
| 129 |
-
nn.Embedding(num_classes + 1, width) if num_classes else None
|
| 130 |
-
)
|
| 131 |
-
self.input = nn.Linear(dim, width) # Lift the state to the hidden width.
|
| 132 |
-
layers: list[nn.Module] = []
|
| 133 |
-
for _ in range(depth):
|
| 134 |
-
layers += [nn.Linear(width, width), nn.SiLU()]
|
| 135 |
-
self.body = nn.Sequential(*layers)
|
| 136 |
-
self.output = nn.Linear(width, dim) # Back down to one vector per coordinate.
|
| 137 |
-
|
| 138 |
-
def forward(
|
| 139 |
-
self, x: torch.Tensor, t: torch.Tensor, labels: torch.Tensor | None = None,
|
| 140 |
-
) -> torch.Tensor:
|
| 141 |
-
"""x: [B, d], t: [B], labels: [B] or None -> [B, d]."""
|
| 142 |
-
h = self.input(x) + self.time_embedding(t) # [B, width] state and time together.
|
| 143 |
-
if labels is not None and self.label_embedding is not None:
|
| 144 |
-
h = h + self.label_embedding(labels) # [B, width] the condition.
|
| 145 |
-
return self.output(self.body(h)) # [B, d].
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
# %% 04. A two-time network, for the flow maps of Lecture 6
|
| 149 |
-
#
|
| 150 |
-
# A flow map carries a state from a start time s to an end time t, so its network takes both. The
|
| 151 |
-
# identity axiom X_{s,s} = id is free if the map is written as the state plus the elapsed time times
|
| 152 |
-
# a network output, which is what `flow_map` below does.
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
class TwoTimeMLP(nn.Module):
|
| 156 |
-
"""An MLP from a point, a start time and an end time to a vector."""
|
| 157 |
-
|
| 158 |
-
def __init__(self, dim: int = 2, width: int = 128, depth: int = 3):
|
| 159 |
-
super().__init__()
|
| 160 |
-
self.dim = dim
|
| 161 |
-
self.start_embedding = TimeEmbedding(width, scale=1.0) # Embedding of s.
|
| 162 |
-
self.end_embedding = TimeEmbedding(width, scale=1.0) # Embedding of t.
|
| 163 |
-
self.input = nn.Linear(dim, width)
|
| 164 |
-
layers: list[nn.Module] = []
|
| 165 |
-
for _ in range(depth):
|
| 166 |
-
layers += [nn.Linear(width, width), nn.SiLU()]
|
| 167 |
-
self.body = nn.Sequential(*layers)
|
| 168 |
-
self.output = nn.Linear(width, dim)
|
| 169 |
-
|
| 170 |
-
def forward(self, x: torch.Tensor, s: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 171 |
-
"""The average velocity v_{s,t}(x). x: [B, d], s: [B], t: [B] -> [B, d]."""
|
| 172 |
-
h = self.input(x) + self.start_embedding(s) + self.end_embedding(t) # [B, width].
|
| 173 |
-
return self.output(self.body(h)) # [B, d].
|
| 174 |
-
|
| 175 |
-
def flow_map(self, x: torch.Tensor, s: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 176 |
-
"""X_{s,t}(x) = x + (t - s) v_{s,t}(x), so the identity axiom holds by construction."""
|
| 177 |
-
elapsed = (t - s)[:, None] # [B, 1] elapsed time, broadcast over coordinates.
|
| 178 |
-
return x + elapsed * self.forward(x, s, t) # [B, d].
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
# %% 05. A small U-Net for 28-by-28 images
|
| 182 |
-
#
|
| 183 |
-
# Lectures 2 and 3 generate images. The network is deliberately small, about half a million
|
| 184 |
-
# parameters, so that a few epochs on a laptop CPU already produce recognizable digits.
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
def _conv_block(in_channels: int, out_channels: int) -> nn.Sequential:
|
| 188 |
-
"""Two 3-by-3 convolutions with a SiLU after each, keeping the height and width."""
|
| 189 |
-
return nn.Sequential(
|
| 190 |
-
nn.Conv2d(in_channels, out_channels, 3, padding=1), nn.SiLU(),
|
| 191 |
-
nn.Conv2d(out_channels, out_channels, 3, padding=1), nn.SiLU(),
|
| 192 |
-
)
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
class SmallUNet(nn.Module):
|
| 196 |
-
"""A three-scale U-Net that maps an image and a time to an image of the same shape.
|
| 197 |
-
|
| 198 |
-
The time enters as an extra constant channel, which is the simplest conditioning that works and
|
| 199 |
-
is what the Lecture 2 script uses.
|
| 200 |
-
"""
|
| 201 |
-
|
| 202 |
-
def __init__(self, base_channels: int = 32, in_channels: int = 1):
|
| 203 |
-
super().__init__()
|
| 204 |
-
c = base_channels # Widths are c, 2c, 4c.
|
| 205 |
-
self.encoder1 = _conv_block(in_channels + 1, c) # Image plus the time channel.
|
| 206 |
-
self.encoder2 = _conv_block(c, 2 * c)
|
| 207 |
-
self.middle = _conv_block(2 * c, 4 * c)
|
| 208 |
-
self.decoder2 = _conv_block(6 * c, 2 * c) # Upsampled 4c joined with the 2c skip.
|
| 209 |
-
self.decoder1 = _conv_block(3 * c, c) # Upsampled 2c joined with the c skip.
|
| 210 |
-
self.output = nn.Conv2d(c, in_channels, 1) # One prediction per pixel.
|
| 211 |
-
self.pool = nn.MaxPool2d(2) # Halve the height and width.
|
| 212 |
-
|
| 213 |
-
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
|
| 214 |
-
"""x: [B, 1, 28, 28], t: [B] -> [B, 1, 28, 28]."""
|
| 215 |
-
import torch.nn.functional as F # Local import keeps the module header short.
|
| 216 |
-
|
| 217 |
-
time_channel = t[:, None, None, None].expand(x.shape[0], 1, *x.shape[2:]) # [B, 1, H, W].
|
| 218 |
-
h = torch.cat([x, time_channel], dim=1) # [B, 2, H, W].
|
| 219 |
-
skip1 = self.encoder1(h) # [B, c, 28, 28].
|
| 220 |
-
skip2 = self.encoder2(self.pool(skip1)) # [B, 2c, 14, 14].
|
| 221 |
-
h = self.middle(self.pool(skip2)) # [B, 4c, 7, 7] the compressed representation.
|
| 222 |
-
h = F.interpolate(h, size=skip2.shape[-2:], mode="nearest") # 7 to 14.
|
| 223 |
-
h = self.decoder2(torch.cat([h, skip2], dim=1)) # Rejoin the 14-by-14 detail.
|
| 224 |
-
h = F.interpolate(h, size=skip1.shape[-2:], mode="nearest") # 14 to 28.
|
| 225 |
-
h = self.decoder1(torch.cat([h, skip1], dim=1)) # Rejoin the 28-by-28 detail.
|
| 226 |
-
return self.output(h) # [B, 1, 28, 28].
|
| 227 |
-
|
| 228 |
-
|
| 229 |
-
def count_parameters(model: nn.Module) -> int:
|
| 230 |
-
"""Number of trainable weights and biases, printed by every script at startup."""
|
| 231 |
-
return sum(p.numel() for p in model.parameters() if p.requires_grad)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
cis6270/runner.py
DELETED
|
@@ -1,173 +0,0 @@
|
|
| 1 |
-
"""Seeding, device selection, the training loop, checkpoints and the shared command line.
|
| 2 |
-
|
| 3 |
-
Every lecture script ends in the same four lines: parse the arguments, build the model, call
|
| 4 |
-
``train`` with a step function, and print a report. The step function is the only part that differs
|
| 5 |
-
between frameworks, and it is the part the lecture is about.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from __future__ import annotations
|
| 9 |
-
|
| 10 |
-
import argparse
|
| 11 |
-
import json
|
| 12 |
-
import random
|
| 13 |
-
import time
|
| 14 |
-
from pathlib import Path
|
| 15 |
-
from typing import Callable
|
| 16 |
-
|
| 17 |
-
import torch
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
# %% 01. Reproducibility and hardware
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
def set_seed(seed: int) -> None:
|
| 24 |
-
"""Seed Python, NumPy through torch, and torch itself, so a run repeats exactly."""
|
| 25 |
-
random.seed(seed)
|
| 26 |
-
torch.manual_seed(seed)
|
| 27 |
-
if torch.cuda.is_available():
|
| 28 |
-
torch.cuda.manual_seed_all(seed)
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
def pick_device(requested: str = "auto") -> torch.device:
|
| 32 |
-
"""Choose CUDA, then Apple MPS, then CPU, unless the reader names one."""
|
| 33 |
-
if requested != "auto":
|
| 34 |
-
return torch.device(requested)
|
| 35 |
-
if torch.cuda.is_available():
|
| 36 |
-
return torch.device("cuda")
|
| 37 |
-
if torch.backends.mps.is_available():
|
| 38 |
-
return torch.device("mps")
|
| 39 |
-
return torch.device("cpu")
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
# %% 02. The command line every script shares
|
| 43 |
-
#
|
| 44 |
-
# A reader who has learned the flags once knows them for all fifty scripts. A script adds its own
|
| 45 |
-
# flags on top of these with `parser.add_argument`.
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
def common_parser(description: str) -> argparse.ArgumentParser:
|
| 49 |
-
"""The flags shared by every script in the course."""
|
| 50 |
-
parser = argparse.ArgumentParser(
|
| 51 |
-
description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter,
|
| 52 |
-
)
|
| 53 |
-
parser.add_argument("--steps", type=int, default=600, help="optimizer steps")
|
| 54 |
-
parser.add_argument("--batch-size", type=int, default=64, help="examples per step")
|
| 55 |
-
parser.add_argument("--lr", type=float, default=1e-3, help="learning rate")
|
| 56 |
-
parser.add_argument("--width", type=int, default=128, help="hidden width of the network")
|
| 57 |
-
parser.add_argument("--length", type=int, default=16, help="sequence length L")
|
| 58 |
-
parser.add_argument("--samples", type=int, default=16, help="sequences or points to generate")
|
| 59 |
-
parser.add_argument("--sample-steps", type=int, default=64, help="integration steps")
|
| 60 |
-
parser.add_argument("--seed", type=int, default=0, help="random seed")
|
| 61 |
-
parser.add_argument("--device", type=str, default="auto", help="auto, cpu, cuda or mps")
|
| 62 |
-
parser.add_argument("--out", type=str, default="", help="directory for the run's output")
|
| 63 |
-
parser.add_argument("--quiet", action="store_true", help="print only the final report")
|
| 64 |
-
return parser
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
# %% 03. The training loop
|
| 68 |
-
#
|
| 69 |
-
# One loop for the whole course. The caller passes a function that takes the step index and returns
|
| 70 |
-
# a scalar loss with a graph attached; everything else, the optimizer, the logging and the timing,
|
| 71 |
-
# is the same in every lecture.
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
def train(
|
| 75 |
-
model: torch.nn.Module,
|
| 76 |
-
loss_fn: Callable[[int], torch.Tensor],
|
| 77 |
-
steps: int = 600,
|
| 78 |
-
lr: float = 1e-3,
|
| 79 |
-
log_every: int = 100,
|
| 80 |
-
quiet: bool = False,
|
| 81 |
-
grad_clip: float = 1.0,
|
| 82 |
-
extra_parameters: list[torch.nn.Parameter] | None = None,
|
| 83 |
-
) -> list[float]:
|
| 84 |
-
"""Run ``steps`` optimizer steps on ``loss_fn`` and return the loss at every step.
|
| 85 |
-
|
| 86 |
-
``loss_fn(step)`` draws its own batch, so a lecture that needs fresh noise at every step gets it
|
| 87 |
-
without a DataLoader. ``extra_parameters`` covers the rare case of a second trained object, such
|
| 88 |
-
as the bias force of Chapter 7.
|
| 89 |
-
"""
|
| 90 |
-
parameters = list(model.parameters()) + list(extra_parameters or [])
|
| 91 |
-
optimizer = torch.optim.AdamW(parameters, lr=lr, weight_decay=0.0)
|
| 92 |
-
losses: list[float] = []
|
| 93 |
-
started = time.time()
|
| 94 |
-
model.train()
|
| 95 |
-
for step in range(steps):
|
| 96 |
-
loss = loss_fn(step) # The lecture's own objective.
|
| 97 |
-
optimizer.zero_grad(set_to_none=True)
|
| 98 |
-
loss.backward()
|
| 99 |
-
torch.nn.utils.clip_grad_norm_(parameters, grad_clip) # Keep a bad batch from derailing.
|
| 100 |
-
optimizer.step()
|
| 101 |
-
losses.append(float(loss.detach()))
|
| 102 |
-
if not quiet and log_every and (step + 1) % log_every == 0:
|
| 103 |
-
window = losses[-log_every:] # Average over the last window, which is less jumpy.
|
| 104 |
-
print(f" step {step + 1:5d}/{steps} loss {sum(window) / len(window):.4f}")
|
| 105 |
-
if not quiet:
|
| 106 |
-
print(f" trained {steps} steps in {time.time() - started:.1f}s")
|
| 107 |
-
model.eval()
|
| 108 |
-
return losses
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
# %% 04. Saving a run
|
| 112 |
-
#
|
| 113 |
-
# A run writes three files: the weights, the configuration that produced them, and a short report of
|
| 114 |
-
# what came out. No optimizer state, because none of these examples resumes training.
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
def save_run(
|
| 118 |
-
out: str | Path,
|
| 119 |
-
model: torch.nn.Module | None = None,
|
| 120 |
-
config: dict | None = None,
|
| 121 |
-
report: dict | None = None,
|
| 122 |
-
losses: list[float] | None = None,
|
| 123 |
-
samples: list[str] | None = None,
|
| 124 |
-
) -> Path | None:
|
| 125 |
-
"""Write the weights, the configuration, the report, the loss curve and any samples."""
|
| 126 |
-
if not out:
|
| 127 |
-
return None
|
| 128 |
-
directory = Path(out)
|
| 129 |
-
directory.mkdir(parents=True, exist_ok=True)
|
| 130 |
-
if model is not None:
|
| 131 |
-
torch.save({"model": model.state_dict(), "config": config or {}},
|
| 132 |
-
directory / "checkpoint.pt")
|
| 133 |
-
if config is not None:
|
| 134 |
-
(directory / "config.json").write_text(json.dumps(config, indent=2) + "\n")
|
| 135 |
-
if report is not None:
|
| 136 |
-
(directory / "report.json").write_text(json.dumps(report, indent=2) + "\n")
|
| 137 |
-
if losses is not None:
|
| 138 |
-
# Thin the curve to at most 200 points so the file stays small and readable.
|
| 139 |
-
stride = max(1, len(losses) // 200)
|
| 140 |
-
thinned = [round(v, 5) for v in losses[::stride]]
|
| 141 |
-
(directory / "losses.json").write_text(json.dumps(thinned) + "\n")
|
| 142 |
-
if samples is not None:
|
| 143 |
-
(directory / "samples.txt").write_text("\n".join(samples) + "\n")
|
| 144 |
-
return directory
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
def load_run(out: str | Path, model: torch.nn.Module) -> dict:
|
| 148 |
-
"""Read the weights back into ``model`` and return the configuration that trained them."""
|
| 149 |
-
payload = torch.load(Path(out) / "checkpoint.pt", map_location="cpu", weights_only=False)
|
| 150 |
-
model.load_state_dict(payload["model"])
|
| 151 |
-
model.eval()
|
| 152 |
-
return payload.get("config", {})
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
# %% 05. Printing a report
|
| 156 |
-
#
|
| 157 |
-
# Every script ends with the same two-column block, so a reader can compare two methods by reading
|
| 158 |
-
# two reports side by side.
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
def print_report(title: str, report: dict, samples: list[str] | None = None) -> None:
|
| 162 |
-
"""Print a titled block of name and value pairs, and a few samples."""
|
| 163 |
-
print(f"\n{title}")
|
| 164 |
-
print("-" * len(title))
|
| 165 |
-
for key, value in report.items():
|
| 166 |
-
if isinstance(value, float):
|
| 167 |
-
print(f" {key:<34s} {value:.4f}")
|
| 168 |
-
else:
|
| 169 |
-
print(f" {key:<34s} {value}")
|
| 170 |
-
if samples:
|
| 171 |
-
print("\n samples")
|
| 172 |
-
for sample in samples[:8]:
|
| 173 |
-
print(f" {sample}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|