fix: target_encoder.py — respect config.jepa_loss_fn (smooth_l1/mse/cosine) instead of hardcoded MSE
Browse files- mr_jepa/models/target_encoder.py +30 -39
mr_jepa/models/target_encoder.py
CHANGED
|
@@ -75,7 +75,6 @@ class TargetEncoder(nn.Module):
|
|
| 75 |
"""
|
| 76 |
# Compute momentum
|
| 77 |
if self.config.ema_schedule == "cosine":
|
| 78 |
-
# Cosine annealing from base to end momentum
|
| 79 |
progress = step / max(total_steps, 1)
|
| 80 |
momentum = self.config.ema_momentum_end - \
|
| 81 |
(self.config.ema_momentum_end - self.config.ema_momentum_base) * \
|
|
@@ -119,7 +118,6 @@ class TargetEncoder(nn.Module):
|
|
| 119 |
'target_trajectory': [B, K+1, N_s, D] - target states
|
| 120 |
'target_evidence': [B, N_e, D] - target evidence tokens
|
| 121 |
"""
|
| 122 |
-
# Target evidence memory
|
| 123 |
evidence_output = self.target_evidence_memory(
|
| 124 |
visual_tokens=visual_tokens,
|
| 125 |
text_tokens=text_tokens,
|
|
@@ -129,7 +127,6 @@ class TargetEncoder(nn.Module):
|
|
| 129 |
|
| 130 |
target_evidence = evidence_output['evidence_tokens']
|
| 131 |
|
| 132 |
-
# Target rollout
|
| 133 |
rollout_output = self.target_rollout(
|
| 134 |
evidence_tokens=target_evidence,
|
| 135 |
)
|
|
@@ -149,14 +146,11 @@ class SIGRegLoss(nn.Module):
|
|
| 149 |
|
| 150 |
Uses random projections + Epps-Pulley test statistic.
|
| 151 |
SIGReg(Z) = (1/M) Σ_m T(Z @ u_m)
|
| 152 |
-
|
| 153 |
-
where T is the Epps-Pulley univariate normality test.
|
| 154 |
"""
|
| 155 |
|
| 156 |
def __init__(self, hidden_dim: int, num_projections: int = 1024):
|
| 157 |
super().__init__()
|
| 158 |
self.num_projections = num_projections
|
| 159 |
-
# Random projection directions (fixed, not learned)
|
| 160 |
self.register_buffer(
|
| 161 |
'projections',
|
| 162 |
F.normalize(torch.randn(hidden_dim, num_projections), dim=0)
|
|
@@ -165,26 +159,15 @@ class SIGRegLoss(nn.Module):
|
|
| 165 |
def _epps_pulley_statistic(self, h: torch.Tensor) -> torch.Tensor:
|
| 166 |
"""
|
| 167 |
Compute Epps-Pulley test statistic for univariate normality.
|
| 168 |
-
|
| 169 |
-
T(h) measures how far the distribution of h is from N(0,1).
|
| 170 |
-
Lower values = more Gaussian.
|
| 171 |
-
|
| 172 |
-
Simplified version: uses moment-based approximation.
|
| 173 |
"""
|
| 174 |
-
# Standardize
|
| 175 |
h_mean = h.mean()
|
| 176 |
h_std = h.std() + 1e-6
|
| 177 |
h_norm = (h - h_mean) / h_std
|
| 178 |
|
| 179 |
-
n = h_norm.size(0)
|
| 180 |
-
|
| 181 |
-
# Compute pairwise differences for the EP statistic
|
| 182 |
-
# EP test: based on characteristic function
|
| 183 |
-
# Simplified: variance + kurtosis penalty
|
| 184 |
variance = h_norm.var()
|
| 185 |
-
kurtosis = ((h_norm ** 4).mean() - 3).abs()
|
| 186 |
|
| 187 |
-
# Penalize deviation from unit variance and zero excess kurtosis
|
| 188 |
return (variance - 1.0) ** 2 + 0.5 * kurtosis
|
| 189 |
|
| 190 |
def forward(self, z: torch.Tensor) -> torch.Tensor:
|
|
@@ -193,7 +176,6 @@ class SIGRegLoss(nn.Module):
|
|
| 193 |
|
| 194 |
Args:
|
| 195 |
z: Latent embeddings [B, N, D] or [B*N, D]
|
| 196 |
-
|
| 197 |
Returns:
|
| 198 |
Scalar SIGReg loss
|
| 199 |
"""
|
|
@@ -203,12 +185,10 @@ class SIGRegLoss(nn.Module):
|
|
| 203 |
else:
|
| 204 |
z_flat = z
|
| 205 |
|
| 206 |
-
# Project onto random directions
|
| 207 |
projections = z_flat @ self.projections # [B*N, M]
|
| 208 |
|
| 209 |
-
# Compute EP statistic for each projection
|
| 210 |
losses = []
|
| 211 |
-
for m in range(min(self.num_projections, 64)):
|
| 212 |
losses.append(self._epps_pulley_statistic(projections[:, m]))
|
| 213 |
|
| 214 |
return torch.stack(losses).mean()
|
|
@@ -220,7 +200,7 @@ class VICRegLoss(nn.Module):
|
|
| 220 |
|
| 221 |
Three terms:
|
| 222 |
- Variance: keep feature std above a threshold
|
| 223 |
-
- Invariance:
|
| 224 |
- Covariance: decorrelate features
|
| 225 |
"""
|
| 226 |
|
|
@@ -230,10 +210,6 @@ class VICRegLoss(nn.Module):
|
|
| 230 |
self.cov_weight = cov_weight
|
| 231 |
|
| 232 |
def forward(self, z: torch.Tensor) -> torch.Tensor:
|
| 233 |
-
"""
|
| 234 |
-
Args:
|
| 235 |
-
z: [B*N, D] latent embeddings
|
| 236 |
-
"""
|
| 237 |
if z.dim() == 3:
|
| 238 |
z = z.reshape(-1, z.size(-1))
|
| 239 |
|
|
@@ -246,7 +222,6 @@ class VICRegLoss(nn.Module):
|
|
| 246 |
N = z_centered.size(0)
|
| 247 |
cov = (z_centered.T @ z_centered) / (N - 1)
|
| 248 |
D = cov.size(0)
|
| 249 |
-
# Off-diagonal elements
|
| 250 |
off_diag = cov.flatten()[:-1].view(D - 1, D + 1)[:, 1:].flatten()
|
| 251 |
cov_loss = (off_diag ** 2).mean()
|
| 252 |
|
|
@@ -257,7 +232,10 @@ class JEPALoss(nn.Module):
|
|
| 257 |
"""
|
| 258 |
Complete JEPA objective for MR-JEPA.
|
| 259 |
|
| 260 |
-
|
|
|
|
|
|
|
|
|
|
| 261 |
|
| 262 |
Plus anti-collapse regularization:
|
| 263 |
L_total = L_JEPA + λ * SIGReg(Z) + L_task + α * L_gen
|
|
@@ -279,17 +257,29 @@ class JEPALoss(nn.Module):
|
|
| 279 |
target_trajectory: torch.Tensor, # [B, K+1, N_s, D]
|
| 280 |
) -> torch.Tensor:
|
| 281 |
"""
|
| 282 |
-
Compute
|
| 283 |
|
| 284 |
Only compute loss for steps k=1..K (not z₀, which is deterministic).
|
|
|
|
| 285 |
"""
|
| 286 |
# Skip z₀ (step 0) — only supervise predicted states
|
| 287 |
pred = predicted_trajectory[:, 1:] # [B, K, N_s, D]
|
| 288 |
-
target = target_trajectory[:, 1:]
|
| 289 |
-
|
| 290 |
-
|
| 291 |
-
|
| 292 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 293 |
|
| 294 |
def compute_regularization(
|
| 295 |
self,
|
|
@@ -299,7 +289,6 @@ class JEPALoss(nn.Module):
|
|
| 299 |
reg_loss = torch.tensor(0.0, device=trajectory.device)
|
| 300 |
|
| 301 |
if self.config.use_sigreg:
|
| 302 |
-
# Apply SIGReg to each step's representations
|
| 303 |
B, Kp1, N_s, D = trajectory.shape
|
| 304 |
for k in range(Kp1):
|
| 305 |
reg_loss = reg_loss + self.sigreg(trajectory[:, k])
|
|
@@ -308,9 +297,11 @@ class JEPALoss(nn.Module):
|
|
| 308 |
|
| 309 |
if self.config.use_vicreg:
|
| 310 |
B, Kp1, N_s, D = trajectory.shape
|
|
|
|
| 311 |
for k in range(Kp1):
|
| 312 |
-
|
| 313 |
-
|
|
|
|
| 314 |
|
| 315 |
return reg_loss
|
| 316 |
|
|
|
|
| 75 |
"""
|
| 76 |
# Compute momentum
|
| 77 |
if self.config.ema_schedule == "cosine":
|
|
|
|
| 78 |
progress = step / max(total_steps, 1)
|
| 79 |
momentum = self.config.ema_momentum_end - \
|
| 80 |
(self.config.ema_momentum_end - self.config.ema_momentum_base) * \
|
|
|
|
| 118 |
'target_trajectory': [B, K+1, N_s, D] - target states
|
| 119 |
'target_evidence': [B, N_e, D] - target evidence tokens
|
| 120 |
"""
|
|
|
|
| 121 |
evidence_output = self.target_evidence_memory(
|
| 122 |
visual_tokens=visual_tokens,
|
| 123 |
text_tokens=text_tokens,
|
|
|
|
| 127 |
|
| 128 |
target_evidence = evidence_output['evidence_tokens']
|
| 129 |
|
|
|
|
| 130 |
rollout_output = self.target_rollout(
|
| 131 |
evidence_tokens=target_evidence,
|
| 132 |
)
|
|
|
|
| 146 |
|
| 147 |
Uses random projections + Epps-Pulley test statistic.
|
| 148 |
SIGReg(Z) = (1/M) Σ_m T(Z @ u_m)
|
|
|
|
|
|
|
| 149 |
"""
|
| 150 |
|
| 151 |
def __init__(self, hidden_dim: int, num_projections: int = 1024):
|
| 152 |
super().__init__()
|
| 153 |
self.num_projections = num_projections
|
|
|
|
| 154 |
self.register_buffer(
|
| 155 |
'projections',
|
| 156 |
F.normalize(torch.randn(hidden_dim, num_projections), dim=0)
|
|
|
|
| 159 |
def _epps_pulley_statistic(self, h: torch.Tensor) -> torch.Tensor:
|
| 160 |
"""
|
| 161 |
Compute Epps-Pulley test statistic for univariate normality.
|
| 162 |
+
Simplified: variance + kurtosis penalty.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 163 |
"""
|
|
|
|
| 164 |
h_mean = h.mean()
|
| 165 |
h_std = h.std() + 1e-6
|
| 166 |
h_norm = (h - h_mean) / h_std
|
| 167 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 168 |
variance = h_norm.var()
|
| 169 |
+
kurtosis = ((h_norm ** 4).mean() - 3).abs()
|
| 170 |
|
|
|
|
| 171 |
return (variance - 1.0) ** 2 + 0.5 * kurtosis
|
| 172 |
|
| 173 |
def forward(self, z: torch.Tensor) -> torch.Tensor:
|
|
|
|
| 176 |
|
| 177 |
Args:
|
| 178 |
z: Latent embeddings [B, N, D] or [B*N, D]
|
|
|
|
| 179 |
Returns:
|
| 180 |
Scalar SIGReg loss
|
| 181 |
"""
|
|
|
|
| 185 |
else:
|
| 186 |
z_flat = z
|
| 187 |
|
|
|
|
| 188 |
projections = z_flat @ self.projections # [B*N, M]
|
| 189 |
|
|
|
|
| 190 |
losses = []
|
| 191 |
+
for m in range(min(self.num_projections, 64)):
|
| 192 |
losses.append(self._epps_pulley_statistic(projections[:, m]))
|
| 193 |
|
| 194 |
return torch.stack(losses).mean()
|
|
|
|
| 200 |
|
| 201 |
Three terms:
|
| 202 |
- Variance: keep feature std above a threshold
|
| 203 |
+
- Invariance: (handled by prediction loss)
|
| 204 |
- Covariance: decorrelate features
|
| 205 |
"""
|
| 206 |
|
|
|
|
| 210 |
self.cov_weight = cov_weight
|
| 211 |
|
| 212 |
def forward(self, z: torch.Tensor) -> torch.Tensor:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 213 |
if z.dim() == 3:
|
| 214 |
z = z.reshape(-1, z.size(-1))
|
| 215 |
|
|
|
|
| 222 |
N = z_centered.size(0)
|
| 223 |
cov = (z_centered.T @ z_centered) / (N - 1)
|
| 224 |
D = cov.size(0)
|
|
|
|
| 225 |
off_diag = cov.flatten()[:-1].view(D - 1, D + 1)[:, 1:].flatten()
|
| 226 |
cov_loss = (off_diag ** 2).mean()
|
| 227 |
|
|
|
|
| 232 |
"""
|
| 233 |
Complete JEPA objective for MR-JEPA.
|
| 234 |
|
| 235 |
+
Supports three loss functions (controlled by config.jepa_loss_fn):
|
| 236 |
+
- smooth_l1: SmoothL1Loss (hybrid default, robust to outliers)
|
| 237 |
+
- mse: MSE / L2 (original I-JEPA)
|
| 238 |
+
- cosine: Cosine similarity loss (purist default)
|
| 239 |
|
| 240 |
Plus anti-collapse regularization:
|
| 241 |
L_total = L_JEPA + λ * SIGReg(Z) + L_task + α * L_gen
|
|
|
|
| 257 |
target_trajectory: torch.Tensor, # [B, K+1, N_s, D]
|
| 258 |
) -> torch.Tensor:
|
| 259 |
"""
|
| 260 |
+
Compute prediction loss between online and target trajectories.
|
| 261 |
|
| 262 |
Only compute loss for steps k=1..K (not z₀, which is deterministic).
|
| 263 |
+
Loss function selected by config.jepa_loss_fn.
|
| 264 |
"""
|
| 265 |
# Skip z₀ (step 0) — only supervise predicted states
|
| 266 |
pred = predicted_trajectory[:, 1:] # [B, K, N_s, D]
|
| 267 |
+
target = target_trajectory[:, 1:].detach() # [B, K, N_s, D]
|
| 268 |
+
|
| 269 |
+
loss_fn = self.config.jepa_loss_fn
|
| 270 |
+
|
| 271 |
+
if loss_fn == "smooth_l1":
|
| 272 |
+
return F.smooth_l1_loss(pred, target)
|
| 273 |
+
elif loss_fn == "mse":
|
| 274 |
+
return F.mse_loss(pred, target)
|
| 275 |
+
elif loss_fn == "cosine":
|
| 276 |
+
# Cosine similarity loss: 1 - cos(pred, target), averaged
|
| 277 |
+
pred_flat = pred.reshape(-1, pred.size(-1))
|
| 278 |
+
target_flat = target.reshape(-1, target.size(-1))
|
| 279 |
+
cos_sim = F.cosine_similarity(pred_flat, target_flat, dim=-1)
|
| 280 |
+
return (1 - cos_sim).mean()
|
| 281 |
+
else:
|
| 282 |
+
raise ValueError(f"Unknown JEPA loss function: {loss_fn}")
|
| 283 |
|
| 284 |
def compute_regularization(
|
| 285 |
self,
|
|
|
|
| 289 |
reg_loss = torch.tensor(0.0, device=trajectory.device)
|
| 290 |
|
| 291 |
if self.config.use_sigreg:
|
|
|
|
| 292 |
B, Kp1, N_s, D = trajectory.shape
|
| 293 |
for k in range(Kp1):
|
| 294 |
reg_loss = reg_loss + self.sigreg(trajectory[:, k])
|
|
|
|
| 297 |
|
| 298 |
if self.config.use_vicreg:
|
| 299 |
B, Kp1, N_s, D = trajectory.shape
|
| 300 |
+
vicreg_loss = torch.tensor(0.0, device=trajectory.device)
|
| 301 |
for k in range(Kp1):
|
| 302 |
+
vicreg_loss = vicreg_loss + self.vicreg(trajectory[:, k])
|
| 303 |
+
vicreg_loss = vicreg_loss / Kp1
|
| 304 |
+
reg_loss = reg_loss + vicreg_loss
|
| 305 |
|
| 306 |
return reg_loss
|
| 307 |
|