| """PIMT classification heads — v6 ConcentrationAwarePyramidHead. |
| |
| The model predicts three static pyramid tiers (top, middle, base notes) using |
| physics-informed concentration routing. Each tier pools token representations |
| weighted by OAV-based routing scores, so that top notes are dominated by |
| high-volatility ingredients and base notes by low-volatility ingredients. |
| |
| Key design decisions (from v6 spec): |
| (a) Temperature-controlled routing softmax with learnable per-tier temperature. |
| (b) Physics-as-bias: routing = learned_attention + log_OAV / tau (init at pure physics). |
| (c) Scalar conditioning via 2-layer MLP (not raw concatenation). |
| (d) Complete padding hygiene with NaN guards. |
| """ |
| from __future__ import annotations |
|
|
| import json |
| import math |
| from pathlib import Path |
| from typing import Any |
|
|
| import torch |
| import torch.nn as nn |
| import torch.nn.functional as F |
|
|
|
|
| def _load_scalar_stats(path: str = "artifacts/scalar_stats_v1.json") -> dict[str, dict[str, float]]: |
| """Load training-set scalar statistics for standardization.""" |
| p = Path(path) |
| if p.exists(): |
| return json.loads(p.read_text()) |
| |
| return { |
| "log10_oav_sum": {"mean": 35.0, "std": 21.5}, |
| "x_liquid_sum": {"mean": 1.0, "std": 0.08}, |
| } |
|
|
|
|
| class ConcentrationAwarePyramidHead(nn.Module): |
| """Three-tier pyramid head with physics-informed concentration routing. |
| |
| Input: |
| latent: (B, S, H) — per-token hidden states from the transformer (time already pooled). |
| physics: (B, S, 2) — per-token physics states [x_liquid, log10(OAV)]. |
| src_key_padding_mask: (B, S) — True for padding positions. |
| |
| Output: (B, 3, 138) sigmoid probabilities for [top, mid, base]. |
| """ |
|
|
| def __init__( |
| self, |
| hidden_dim: int, |
| output_dim: int = 138, |
| scalar_stats: dict[str, dict[str, float]] | None = None, |
| scalar_embed_dim: int = 16, |
| ) -> None: |
| super().__init__() |
| self.hidden_dim = hidden_dim |
| self.output_dim = output_dim |
|
|
| |
| if scalar_stats is None: |
| scalar_stats = _load_scalar_stats() |
| oav_stats = scalar_stats.get("log10_oav_sum", {"mean": 35.0, "std": 21.5}) |
| xliq_stats = scalar_stats.get("x_liquid_sum", {"mean": 1.0, "std": 0.08}) |
| self.register_buffer("oav_mean", torch.tensor(oav_stats["mean"], dtype=torch.float32)) |
| self.register_buffer("oav_std", torch.tensor(max(oav_stats["std"], 1e-6), dtype=torch.float32)) |
| self.register_buffer("xliq_mean", torch.tensor(xliq_stats["mean"], dtype=torch.float32)) |
| self.register_buffer("xliq_std", torch.tensor(max(xliq_stats["std"], 1e-6), dtype=torch.float32)) |
|
|
| |
| |
| self.log_tau = nn.Parameter(torch.zeros(3)) |
|
|
| |
| self.attn_proj = nn.Linear(hidden_dim, 1, bias=False) |
| nn.init.zeros_(self.attn_proj.weight) |
|
|
| |
| self.scalar_mlp = nn.Sequential( |
| nn.Linear(2, scalar_embed_dim), |
| nn.GELU(), |
| nn.Linear(scalar_embed_dim, scalar_embed_dim), |
| ) |
|
|
| |
| pooled_dim = hidden_dim + scalar_embed_dim |
| self.top_head = nn.Linear(pooled_dim, output_dim) |
| self.mid_head = nn.Linear(pooled_dim, output_dim) |
| self.base_head = nn.Linear(pooled_dim, output_dim) |
|
|
| |
| |
| |
|
|
| def _compute_routing_weights( |
| self, |
| latent: torch.Tensor, |
| physics: torch.Tensor, |
| src_key_padding_mask: torch.Tensor, |
| ) -> torch.Tensor: |
| """Compute per-tier routing weights for each token. |
| |
| Args: |
| latent: (B, S, H) |
| physics: (B, S, 2) — [x_liquid, log10(OAV)] |
| src_key_padding_mask: (B, S) — True for padding |
| |
| Returns: |
| routing_weights: (B, 3, S) — per-tier softmax weights over tokens. |
| """ |
| B, S, H = latent.shape |
|
|
| |
| log_oav = physics[..., 1] |
|
|
| |
| attn_score = self.attn_proj(latent).squeeze(-1) |
|
|
| |
| tau = torch.exp(torch.clamp(self.log_tau, -2.303, 2.303)) |
|
|
| routing_weights = [] |
| for tier_idx in range(3): |
| |
| |
| |
| logits = attn_score + log_oav / tau[tier_idx] |
|
|
| |
| neg_mask_val = torch.finfo(logits.dtype).min |
| logits = logits.masked_fill(src_key_padding_mask, neg_mask_val) |
|
|
| |
| all_masked = src_key_padding_mask.all(dim=1, keepdim=True) |
| if all_masked.any(): |
| |
| safe_logits = torch.zeros_like(logits) |
| safe_logits = safe_logits.masked_fill(src_key_padding_mask, neg_mask_val) |
| logits = torch.where(all_masked.expand_as(logits), safe_logits, logits) |
|
|
| weights = F.softmax(logits, dim=-1) |
| routing_weights.append(weights) |
|
|
| return torch.stack(routing_weights, dim=1) |
|
|
| def _compute_intensity_scalars( |
| self, |
| physics: torch.Tensor, |
| src_key_padding_mask: torch.Tensor, |
| ) -> torch.Tensor: |
| """Compute standardized intensity scalars: [log10(ΣOAV), Σx_liquid]. |
| |
| These are computed per-tier (using different OAV snapshots), but for now |
| we use the overall per-token physics. |
| |
| Args: |
| physics: (B, S, 2) — [x_liquid, log10(OAV)] |
| src_key_padding_mask: (B, S) |
| |
| Returns: |
| scalars: (B, 2) — standardized [log10(ΣOAV), Σx_liquid] |
| """ |
| B, S, _ = physics.shape |
|
|
| |
| mask = (~src_key_padding_mask).float().unsqueeze(-1) |
| masked_physics = physics * mask |
|
|
| |
| log_oav_sum = masked_physics[..., 1].sum(dim=1) |
| x_liq_sum = masked_physics[..., 0].sum(dim=1) |
|
|
| |
| std_oav = (log_oav_sum - self.oav_mean) / self.oav_std |
| std_xliq = (x_liq_sum - self.xliq_mean) / self.xliq_std |
|
|
| return torch.stack([std_oav, std_xliq], dim=-1) |
|
|
| def forward( |
| self, |
| latent: torch.Tensor, |
| physics: torch.Tensor, |
| src_key_padding_mask: torch.Tensor, |
| ) -> dict[str, torch.Tensor]: |
| """Forward pass. |
| |
| Args: |
| latent: (B, S, H) — per-token hidden states. |
| physics: (B, S, 2) — [x_liquid, log10(OAV)] per token. |
| src_key_padding_mask: (B, S) — True for padding. |
| |
| Returns: |
| dict with: |
| "pyramid": (B, 3, 138) sigmoid probabilities |
| "routing_weights": (B, 3, S) routing weights (for diagnostics) |
| "scalar_embedding": (B, 16) scalar conditioning embedding |
| """ |
| B, S, H = latent.shape |
|
|
| |
| routing = self._compute_routing_weights(latent, physics, src_key_padding_mask) |
|
|
| |
| scalars = self._compute_intensity_scalars(physics, src_key_padding_mask) |
| scalar_emb = self.scalar_mlp(scalars) |
|
|
| |
| |
| pooled = torch.bmm(routing, latent) |
|
|
| |
| scalar_expanded = scalar_emb.unsqueeze(1).expand(-1, 3, -1) |
| pooled_with_scalar = torch.cat([pooled, scalar_expanded], dim=-1) |
|
|
| |
| top = torch.sigmoid(self.top_head(pooled_with_scalar[:, 0])) |
| mid = torch.sigmoid(self.mid_head(pooled_with_scalar[:, 1])) |
| base = torch.sigmoid(self.base_head(pooled_with_scalar[:, 2])) |
|
|
| pyramid = torch.stack([top, mid, base], dim=1) |
|
|
| return { |
| "pyramid": pyramid, |
| "routing_weights": routing, |
| "scalar_embedding": scalar_emb, |
| } |
|
|
|
|
| class PIMTHeads(nn.Module): |
| """Container for the concentration-aware pyramid head and subjective heads. |
| |
| Accepts the full 4D latent from the transformer (B, T, S, H) and pools |
| over time before feeding to the pyramid head. |
| """ |
|
|
| def __init__( |
| self, |
| hidden_dim: int, |
| objective_dim: int = 138, |
| seasonality_classes: int = 4, |
| wearability_classes: int = 2, |
| scalar_stats: dict[str, dict[str, float]] | None = None, |
| ) -> None: |
| super().__init__() |
| self.objective_head = ConcentrationAwarePyramidHead( |
| hidden_dim, objective_dim, scalar_stats=scalar_stats |
| ) |
|
|
| |
| self.seasonality_head = nn.Linear(hidden_dim, seasonality_classes) |
| self.gender_head = nn.Linear(hidden_dim, 1) |
| self.wearability_head = nn.Linear(hidden_dim, wearability_classes) |
| self.substantivity_head = nn.Linear(hidden_dim, 1) |
|
|
| def forward( |
| self, |
| latent: torch.Tensor, |
| physics: torch.Tensor | None = None, |
| src_key_padding_mask: torch.Tensor | None = None, |
| ) -> dict[str, Any]: |
| """Forward pass. |
| |
| Args: |
| latent: (B, T, S, H) from the transformer encoder. |
| physics: (B, T, S, 2) physics states. If None, uses zero physics. |
| src_key_padding_mask: (B, S) padding mask. If None, no padding. |
| |
| Returns: |
| dict with objective, subjective, alignment, and diagnostics. |
| """ |
| |
| latent_pooled_time = latent.mean(dim=1) |
|
|
| B, S, H = latent_pooled_time.shape |
|
|
| |
| if physics is not None: |
| |
| physics_pooled = physics.mean(dim=1) |
| else: |
| physics_pooled = latent_pooled_time.new_zeros(B, S, 2) |
|
|
| |
| if src_key_padding_mask is None: |
| src_key_padding_mask = torch.zeros(B, S, dtype=torch.bool, device=latent.device) |
|
|
| |
| pyramid_result = self.objective_head(latent_pooled_time, physics_pooled, src_key_padding_mask) |
| pyramid = pyramid_result["pyramid"] |
|
|
| |
| pooled_global = latent_pooled_time.mean(dim=1) |
| alignment_raw = pyramid_result["pyramid"].mean(dim=1) |
| alignment = alignment_raw |
|
|
| |
| seasonality = self.seasonality_head(pooled_global) |
| gender = self.gender_head(pooled_global) |
| wearability = self.wearability_head(pooled_global) |
| substantivity = self.substantivity_head(pooled_global) |
|
|
| return { |
| "objective": pyramid, |
| "subjective": { |
| "seasonality": seasonality, |
| "gender_profile": gender, |
| "wearability": wearability, |
| "substantivity": substantivity, |
| }, |
| "alignment": alignment, |
| "diagnostics": { |
| "routing_weights": pyramid_result["routing_weights"], |
| "scalar_embedding": pyramid_result["scalar_embedding"], |
| "tau": torch.exp(torch.clamp(self.objective_head.log_tau, -2.303, 2.303)), |
| }, |
| } |
|
|