JorgeAV commited on
Commit
ac52da3
·
verified ·
1 Parent(s): 67a4198

fix: target_encoder.py — respect config.jepa_loss_fn (smooth_l1/mse/cosine) instead of hardcoded MSE

Browse files
Files changed (1) hide show
  1. 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() # Excess kurtosis
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)): # Sample subset for efficiency
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: prediction should match target (already handled by L2)
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
- L_JEPA = (1/K) Σ_{k=1}^{K} ||z_pred_k - sg(z*_k)||²
 
 
 
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 L2 prediction loss between online and target trajectories.
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:] # [B, K, N_s, D]
289
-
290
- # L2 loss per step, averaged
291
- loss = F.mse_loss(pred, target.detach())
292
- return loss
 
 
 
 
 
 
 
 
 
 
 
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
- reg_loss = reg_loss + self.vicreg(trajectory[:, k])
313
- reg_loss = reg_loss / Kp1
 
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