| """Pure-PyTorch engineering reproduction of the Clay v1.5 model specification.""" |
|
|
| import math |
|
|
| import torch |
| from torch import nn |
| from torch.nn import functional as F |
|
|
|
|
| def fourier_encode(values, dim, max_frequency=10000.0): |
| """Encode scalar metadata while preserving exactly ``dim`` output features.""" |
| if dim < 1: |
| return values.new_zeros(*values.shape, 0) |
| pairs = (dim + 1) // 2 |
| frequencies = torch.exp( |
| torch.linspace(0, math.log(max_frequency), pairs, device=values.device, dtype=values.dtype) |
| ) |
| angles = values.unsqueeze(-1) * frequencies |
| return torch.cat((angles.sin(), angles.cos()), dim=-1)[..., :dim] |
|
|
|
|
| def position_encoding_2d(height, width, dim, gsd, device, dtype): |
| if dim % 4: |
| raise ValueError("spatial position dimension must be divisible by four") |
| y, x = torch.meshgrid( |
| torch.arange(height, device=device, dtype=dtype), |
| torch.arange(width, device=device, dtype=dtype), |
| indexing="ij", |
| ) |
| scale = torch.as_tensor(gsd, device=device, dtype=dtype) / 10.0 |
| quarter = dim // 4 |
| frequencies = torch.exp( |
| torch.arange(quarter, device=device, dtype=dtype) * (-math.log(10000.0) / max(quarter, 1)) |
| ) |
| x_angles = x.reshape(-1, 1) * scale * frequencies |
| y_angles = y.reshape(-1, 1) * scale * frequencies |
| return torch.cat((x_angles.sin(), x_angles.cos(), y_angles.sin(), y_angles.cos()), dim=-1) |
|
|
|
|
| def patchify(pixels, patch_size): |
| batch, channels, height, width = pixels.shape |
| if height % patch_size or width % patch_size: |
| raise ValueError("image dimensions must be divisible by patch_size") |
| return pixels.reshape( |
| batch, channels, height // patch_size, patch_size, width // patch_size, patch_size |
| ).permute(0, 2, 4, 1, 3, 5).reshape(batch, -1, channels * patch_size * patch_size) |
|
|
|
|
| def unpatchify(patches, channels, height, width, patch_size): |
| batch = patches.shape[0] |
| return patches.reshape( |
| batch, height // patch_size, width // patch_size, channels, patch_size, patch_size |
| ).permute(0, 3, 1, 4, 2, 5).reshape(batch, channels, height, width) |
|
|
|
|
| class Transformer(nn.Module): |
| def __init__(self, dim, depth, heads, mlp_ratio=4): |
| super().__init__() |
| layer = nn.TransformerEncoderLayer( |
| dim, heads, int(dim * mlp_ratio), activation="gelu", batch_first=True, norm_first=True |
| ) |
| self.layers = nn.TransformerEncoder(layer, depth) |
| self.norm = nn.LayerNorm(dim) |
|
|
| def forward(self, values): |
| return self.norm(self.layers(values)) |
|
|
|
|
| class DynamicEmbedding(nn.Module): |
| """Create sensor-agnostic patches by conditioning per-band kernels on wavelength.""" |
|
|
| def __init__(self, patch_size, embed_dim, wave_dim, wave_latents): |
| super().__init__() |
| self.patch_size = patch_size |
| self.wave_dim = wave_dim |
| self.wave_mlp = nn.Sequential(nn.Linear(wave_dim, wave_dim), nn.GELU(), nn.Linear(wave_dim, wave_dim)) |
| self.latents = nn.Parameter(torch.randn(wave_latents, wave_dim) * 0.02) |
| self.cross_attention = nn.MultiheadAttention(wave_dim, 4, batch_first=True) |
| self.kernel = nn.Linear(wave_dim, patch_size * patch_size * embed_dim) |
| self.bias = nn.Parameter(torch.zeros(embed_dim)) |
| self.embed_dim = embed_dim |
|
|
| def forward(self, pixels, wavelengths): |
| batch, channels, height, width = pixels.shape |
| if wavelengths.ndim == 1: |
| wavelengths = wavelengths[None].expand(batch, -1) |
| if wavelengths.shape != (batch, channels): |
| raise ValueError(f"wavelengths must have shape {(batch, channels)}, got {tuple(wavelengths.shape)}") |
| wave_features = fourier_encode(wavelengths / 1000.0, self.wave_dim) |
| wave_features = self.wave_mlp(wave_features) |
| queries = self.latents[None].expand(batch, -1, -1) |
| context = self.cross_attention(queries, wave_features, wave_features, need_weights=False)[0].mean(dim=1) |
| conditioned = wave_features + context[:, None] |
| kernels = self.kernel(conditioned).reshape( |
| batch, channels, self.embed_dim, self.patch_size, self.patch_size |
| ) |
| patches = [] |
| for index in range(batch): |
| patches.append(F.conv2d(pixels[index:index + 1], kernels[index].permute(1, 0, 2, 3), |
| stride=self.patch_size) + self.bias[None, :, None, None]) |
| return torch.cat(patches).flatten(2).transpose(1, 2), conditioned |
|
|
|
|
| class DynamicDecoder(nn.Module): |
| def __init__(self, patch_size, decoder_dim, wave_dim): |
| super().__init__() |
| self.patch_size = patch_size |
| self.wave_mlp = nn.Sequential(nn.Linear(wave_dim, decoder_dim), nn.GELU(), nn.Linear(decoder_dim, decoder_dim)) |
| self.output = nn.Linear(decoder_dim, patch_size * patch_size) |
|
|
| def forward(self, tokens, wave_features): |
| wave_context = self.wave_mlp(wave_features) |
| joint = tokens[:, :, None, :] + wave_context[:, None, :, :] |
| return self.output(joint).permute(0, 1, 2, 3).flatten(2) |
|
|
|
|
| class ClayFoundation(nn.Module): |
| def __init__(self, config): |
| super().__init__() |
| self.config = dict(config) |
| self.patch_size = int(config["patch_size"]) |
| self.mask_ratio = float(config["mask_ratio"]) |
| enc_dim, dec_dim = int(config["encoder_dim"]), int(config["decoder_dim"]) |
| if enc_dim < 12 or (enc_dim - 8) % 4: |
| raise ValueError("encoder_dim - 8 must be positive and divisible by four") |
| if dec_dim < 12 or (dec_dim - 8) % 4: |
| raise ValueError("decoder_dim - 8 must be positive and divisible by four") |
| self.dynamic_embedding = DynamicEmbedding( |
| self.patch_size, enc_dim, int(config["wave_dim"]), int(config["wave_latents"]) |
| ) |
| self.cls_token = nn.Parameter(torch.randn(1, 1, enc_dim) * 0.02) |
| self.encoder = Transformer(enc_dim, int(config["encoder_depth"]), int(config["encoder_heads"])) |
| self.encoder_to_decoder = nn.Linear(enc_dim, dec_dim) |
| self.mask_token = nn.Parameter(torch.randn(1, 1, dec_dim) * 0.02) |
| self.decoder = Transformer(dec_dim, int(config["decoder_depth"]), int(config["decoder_heads"])) |
| self.wave_to_decoder = nn.Linear(int(config["wave_dim"]), int(config["wave_dim"])) |
| self.dynamic_decoder = DynamicDecoder(self.patch_size, dec_dim, int(config["wave_dim"])) |
| self.representation_head = nn.Linear(enc_dim, int(config["teacher_dim"])) |
| self.norm_pix_loss = bool(config.get("norm_pix_loss", False)) |
|
|
| @staticmethod |
| def _metadata_encoding(time, latlon, dim): |
| values = torch.cat((time, latlon), dim=1) |
| widths = [dim // 4] * 4 |
| for index in range(dim % 4): |
| widths[index] += 1 |
| return torch.cat([fourier_encode(values[:, index], widths[index]) for index in range(4)], dim=1) |
|
|
| def _add_encoding(self, tokens, time, latlon, gsd): |
| batch, length, dim = tokens.shape |
| grid = int(math.sqrt(length)) |
| if grid * grid != length: |
| raise ValueError("Clay reproduction requires a square patch grid") |
| spatial = position_encoding_2d(grid, grid, dim - 8, gsd, tokens.device, tokens.dtype) |
| metadata = self._metadata_encoding(time, latlon, 8) |
| encoding = torch.cat((spatial[None].expand(batch, -1, -1), metadata[:, None].expand(-1, length, -1)), dim=-1) |
| return tokens + encoding |
|
|
| def encode(self, pixels, time, latlon, gsd, wavelengths): |
| patches, _ = self.dynamic_embedding(pixels, wavelengths) |
| patches = self._add_encoding(patches, time, latlon, gsd) |
| cls = self.cls_token.expand(len(pixels), -1, -1) |
| encoded = self.encoder(torch.cat((cls, patches), dim=1)) |
| return encoded[:, 0], encoded[:, 1:] |
|
|
| def forward(self, pixels, time, latlon, gsd, wavelengths, teacher_target=None, mask_ratio=None): |
| ratio = self.mask_ratio if mask_ratio is None else float(mask_ratio) |
| patches, wave_features = self.dynamic_embedding(pixels, wavelengths) |
| patches = self._add_encoding(patches, time, latlon, gsd) |
| batch, length, _ = patches.shape |
| keep = max(1, length - int(length * ratio)) |
| noise = torch.rand(batch, length, device=pixels.device) |
| ordering = noise.argsort(dim=1) |
| unmasked_indices, masked_indices = ordering[:, :keep], ordering[:, keep:] |
| gather = unmasked_indices[:, :, None].expand(-1, -1, patches.shape[-1]) |
| visible = patches.gather(1, gather) |
| encoded = self.encoder(torch.cat((self.cls_token.expand(batch, -1, -1), visible), dim=1)) |
| embedding = encoded[:, 0] |
|
|
| decoded_visible = self.encoder_to_decoder(encoded[:, 1:]) |
| decoder_tokens = self.mask_token.expand(batch, length, -1).clone() |
| decoder_tokens.scatter_(1, unmasked_indices[:, :, None].expand(-1, -1, decoded_visible.shape[-1]), decoded_visible) |
| grid = int(math.sqrt(length)) |
| spatial = position_encoding_2d(grid, grid, decoder_tokens.shape[-1] - 8, gsd, |
| decoder_tokens.device, decoder_tokens.dtype) |
| metadata = self._metadata_encoding(time, latlon, 8) |
| decoder_tokens = decoder_tokens + torch.cat((spatial[None].expand(batch, -1, -1), |
| metadata[:, None].expand(-1, length, -1)), dim=-1) |
| decoded = self.decoder(decoder_tokens) |
| predicted_patches = self.dynamic_decoder(decoded, self.wave_to_decoder(wave_features)) |
| target_patches = patchify(pixels, self.patch_size) |
| if self.norm_pix_loss: |
| mean = target_patches.mean(dim=-1, keepdim=True) |
| variance = target_patches.var(dim=-1, keepdim=True) |
| target_patches = (target_patches - mean) / (variance + 1e-6).sqrt() |
| mask = torch.zeros(batch, length, device=pixels.device) |
| mask.scatter_(1, masked_indices, 1.0) |
| patch_loss = (predicted_patches - target_patches).abs().mean(dim=-1) |
| reconstruction_loss = (patch_loss * mask).sum() / mask.sum().clamp_min(1) |
| projected = F.normalize(self.representation_head(embedding), dim=1) |
| if teacher_target is None: |
| representation_loss = embedding.new_zeros(()) |
| else: |
| representation_loss = 1.0 - (projected * F.normalize(teacher_target, dim=1)).sum(dim=1).mean() |
| reconstruction = unpatchify(predicted_patches, pixels.shape[1], pixels.shape[2], pixels.shape[3], self.patch_size) |
| return { |
| "embedding": embedding, |
| "projected_embedding": projected, |
| "reconstruction": reconstruction, |
| "mask": mask, |
| "reconstruction_loss": reconstruction_loss, |
| "representation_loss": representation_loss, |
| } |
|
|
|
|
| def compute_loss(outputs, reconstruction_weight=0.95, representation_weight=0.05): |
| total = reconstruction_weight * outputs["reconstruction_loss"] + representation_weight * outputs["representation_loss"] |
| return total, { |
| "reconstruction": outputs["reconstruction_loss"], |
| "representation": outputs["representation_loss"], |
| "total": total, |
| } |
|
|