File size: 11,144 Bytes
0ba9d09 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | """The small networks every lecture reuses.
Four networks cover the whole course. A lecture changes the loss and the sampler and keeps the
network fixed, so any difference in the samples comes from the mathematics alone.
| Network | Input | Output | Used by |
| ------------------ | -------------------------- | --------------------- | ------- |
| `TimeEmbedding` | a time or step index | a vector of width D | all |
| `SequenceDenoiser` | tokens [B, L] and a time | logits [B, L, K] | 4, 5, 6 |
| `VelocityMLP` | a point [B, D] and a time | a vector [B, D] | 2, 3, 6, 7 |
| `TwoTimeMLP` | a point, a start, an end | a vector [B, D] | 6 |
| `SmallUNet` | an image [B, 1, 28, 28] | an image of the same | 2, 3 |
"""
from __future__ import annotations
import math
import torch
import torch.nn as nn
# %% 01. Embedding a continuous time
#
# Every network in the course has a time input as well as a state. A raw scalar in [0, 1] is a poor
# input to a linear layer, so we first spread it over many frequencies, exactly as in Section 3.28
# of the notes, and then pass it through a small MLP.
def sinusoidal_embedding(t: torch.Tensor, width: int) -> torch.Tensor:
"""Spread a time [B] over ``width`` sine and cosine frequencies. Returns [B, width]."""
half = width // 2 # Half the coordinates are sines and half cosines.
frequencies = torch.exp(
-math.log(10000.0) * torch.arange(half, device=t.device).to(torch.get_default_dtype()) / half
) # [half] geometrically spaced frequencies.
angles = t.to(torch.get_default_dtype())[:, None] * frequencies[None, :] # [B, half].
return torch.cat([torch.sin(angles), torch.cos(angles)], dim=1) # [B, width].
class TimeEmbedding(nn.Module):
"""Sinusoidal features of the time, followed by a two-layer MLP."""
def __init__(self, width: int = 128, scale: float = 1000.0):
super().__init__()
self.width = width # Width of the embedding the rest of the network receives.
self.scale = scale # We multiply a time in [0, 1] by this before the sines.
self.mlp = nn.Sequential(
nn.Linear(width, width), nn.SiLU(), nn.Linear(width, width),
)
def forward(self, t: torch.Tensor) -> torch.Tensor:
"""t: [B] -> [B, width]."""
return self.mlp(sinusoidal_embedding(t * self.scale, self.width)) # [B, width].
# %% 02. A denoiser over token sequences
#
# Lectures 4, 5 and 6 all train the same shape of network: it reads a corrupted sequence and a time,
# and produces a distribution over the vocabulary at every position. The notes call it the denoiser
# d_theta(. | z, t). Attention lets the prediction at one position depend on every other position,
# so the factorized reverse step of Chapter 4 is worth taking.
class SequenceDenoiser(nn.Module):
"""A small transformer that maps a token sequence and a time to per-position logits."""
def __init__(
self,
vocab_size: int,
length: int,
width: int = 128,
depth: int = 2,
heads: int = 4,
num_classes: int = 0,
):
super().__init__()
self.vocab_size = vocab_size # K, including a mask token when the lecture adds one.
self.length = length # L, the number of positions.
self.width = width # D, the model width.
self.token_embedding = nn.Embedding(vocab_size, width) # One vector per token id.
self.position_embedding = nn.Embedding(length, width) # One vector per position.
self.time_embedding = TimeEmbedding(width) # Shared across positions.
# A label embedding, used only by the conditional examples. Index num_classes is the null
# label that classifier-free guidance drops to.
self.label_embedding = (
nn.Embedding(num_classes + 1, width) if num_classes else None
)
layer = nn.TransformerEncoderLayer(
d_model=width, nhead=heads, dim_feedforward=4 * width,
batch_first=True, dropout=0.0, norm_first=True,
)
self.transformer = nn.TransformerEncoder(
layer, num_layers=depth, enable_nested_tensor=False,
)
self.head = nn.Sequential(nn.LayerNorm(width), nn.Linear(width, vocab_size))
def forward(
self, tokens: torch.Tensor, t: torch.Tensor, labels: torch.Tensor | None = None,
) -> torch.Tensor:
"""tokens: [B, L], t: [B], labels: [B] or None -> logits [B, L, K]."""
positions = torch.arange(tokens.shape[1], device=tokens.device) # [L].
h = (
self.token_embedding(tokens) # [B, L, D] what each position currently holds.
+ self.position_embedding(positions)[None, :, :] # [1, L, D] where it sits.
+ self.time_embedding(t)[:, None, :] # [B, 1, D] how far along the path we are.
)
if labels is not None and self.label_embedding is not None:
h = h + self.label_embedding(labels)[:, None, :] # [B, 1, D] the condition.
h = self.transformer(h) # [B, L, D] every position now depends on every other.
return self.head(h) # [B, L, K] logits over the vocabulary at each position.
# %% 03. A velocity network on a vector state
#
# Lectures 2, 3, 6 and 7 learn a field on R^d: a velocity, a score, a noise prediction or a drift.
# The same MLP serves for all of them, because only the target the loss regresses onto differs.
class VelocityMLP(nn.Module):
"""An MLP from a point and a time to a vector of the same width as the point."""
def __init__(self, dim: int = 2, width: int = 128, depth: int = 3, num_classes: int = 0):
super().__init__()
self.dim = dim # d, the width of the state.
self.time_embedding = TimeEmbedding(width, scale=1.0) # Times here already live in [0, 1].
self.label_embedding = (
nn.Embedding(num_classes + 1, width) if num_classes else None
)
self.input = nn.Linear(dim, width) # Lift the state to the hidden width.
layers: list[nn.Module] = []
for _ in range(depth):
layers += [nn.Linear(width, width), nn.SiLU()]
self.body = nn.Sequential(*layers)
self.output = nn.Linear(width, dim) # Back down to one vector per coordinate.
def forward(
self, x: torch.Tensor, t: torch.Tensor, labels: torch.Tensor | None = None,
) -> torch.Tensor:
"""x: [B, d], t: [B], labels: [B] or None -> [B, d]."""
h = self.input(x) + self.time_embedding(t) # [B, width] state and time together.
if labels is not None and self.label_embedding is not None:
h = h + self.label_embedding(labels) # [B, width] the condition.
return self.output(self.body(h)) # [B, d].
# %% 04. A two-time network, for the flow maps of Lecture 6
#
# A flow map sends a state at a start time s to the state at an end time t, so its network has both
# times as inputs. The identity axiom X_{s,s} = id holds by construction when we write the map as
# the state plus the elapsed time times a network output, as `flow_map` below does.
class TwoTimeMLP(nn.Module):
"""An MLP from a point, a start time and an end time to a vector."""
def __init__(self, dim: int = 2, width: int = 128, depth: int = 3):
super().__init__()
self.dim = dim
self.start_embedding = TimeEmbedding(width, scale=1.0) # Embedding of s.
self.end_embedding = TimeEmbedding(width, scale=1.0) # Embedding of t.
self.input = nn.Linear(dim, width)
layers: list[nn.Module] = []
for _ in range(depth):
layers += [nn.Linear(width, width), nn.SiLU()]
self.body = nn.Sequential(*layers)
self.output = nn.Linear(width, dim)
def forward(self, x: torch.Tensor, s: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
"""The average velocity v_{s,t}(x). x: [B, d], s: [B], t: [B] -> [B, d]."""
h = self.input(x) + self.start_embedding(s) + self.end_embedding(t) # [B, width].
return self.output(self.body(h)) # [B, d].
def flow_map(self, x: torch.Tensor, s: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
"""X_{s,t}(x) = x + (t - s) v_{s,t}(x), so the identity axiom holds by construction."""
elapsed = (t - s)[:, None] # [B, 1] elapsed time, broadcast over coordinates.
return x + elapsed * self.forward(x, s, t) # [B, d].
# %% 05. A small U-Net for 28-by-28 images
#
# Lectures 2 and 3 generate images. The network is deliberately small, about half a million
# parameters, so that a few epochs on a laptop CPU already produce recognizable digits.
def _conv_block(in_channels: int, out_channels: int) -> nn.Sequential:
"""Two 3-by-3 convolutions with a SiLU after each, keeping the height and width."""
return nn.Sequential(
nn.Conv2d(in_channels, out_channels, 3, padding=1), nn.SiLU(),
nn.Conv2d(out_channels, out_channels, 3, padding=1), nn.SiLU(),
)
class SmallUNet(nn.Module):
"""A three-scale U-Net that maps an image and a time to an image of the same shape.
The time enters as an extra constant channel, the simplest conditioning that works and the one
the Lecture 2 script runs on.
"""
def __init__(self, base_channels: int = 32, in_channels: int = 1):
super().__init__()
c = base_channels # Widths are c, 2c, 4c.
self.encoder1 = _conv_block(in_channels + 1, c) # Image plus the time channel.
self.encoder2 = _conv_block(c, 2 * c)
self.middle = _conv_block(2 * c, 4 * c)
self.decoder2 = _conv_block(6 * c, 2 * c) # Upsampled 4c joined with the 2c skip.
self.decoder1 = _conv_block(3 * c, c) # Upsampled 2c joined with the c skip.
self.output = nn.Conv2d(c, in_channels, 1) # One prediction per pixel.
self.pool = nn.MaxPool2d(2) # Halve the height and width.
def forward(self, x: torch.Tensor, t: torch.Tensor) -> torch.Tensor:
"""x: [B, 1, 28, 28], t: [B] -> [B, 1, 28, 28]."""
import torch.nn.functional as F # Local import keeps the module header short.
time_channel = t[:, None, None, None].expand(x.shape[0], 1, *x.shape[2:]) # [B, 1, H, W].
h = torch.cat([x, time_channel], dim=1) # [B, 2, H, W].
skip1 = self.encoder1(h) # [B, c, 28, 28].
skip2 = self.encoder2(self.pool(skip1)) # [B, 2c, 14, 14].
h = self.middle(self.pool(skip2)) # [B, 4c, 7, 7] the compressed representation.
h = F.interpolate(h, size=skip2.shape[-2:], mode="nearest") # 7 to 14.
h = self.decoder2(torch.cat([h, skip2], dim=1)) # Rejoin the 14-by-14 detail.
h = F.interpolate(h, size=skip1.shape[-2:], mode="nearest") # 14 to 28.
h = self.decoder1(torch.cat([h, skip1], dim=1)) # Rejoin the 28-by-28 detail.
return self.output(h) # [B, 1, 28, 28].
def count_parameters(model: nn.Module) -> int:
"""Number of trainable weights and biases, printed by every script at startup."""
return sum(p.numel() for p in model.parameters() if p.requires_grad)
|