|
37 | 37 |
|
38 | 38 | import torch |
39 | 39 | import torch.nn as nn |
| 40 | +from models import AEBatchCorrector, GradReverseLayer, ResidualBlock, make_mlp |
| 41 | +from models.factory import build_model_from_args |
40 | 42 | # AMP GradScaler compatibility (torch>=2 provides torch.amp.GradScaler; older used torch.cuda.amp.GradScaler) |
41 | 43 | try: # prefer new API to silence deprecation warnings when available |
42 | 44 | from torch.amp import GradScaler # type: ignore |
@@ -294,161 +296,13 @@ def _eta2_by_group(Z: np.ndarray, groups: np.ndarray): |
294 | 296 | # ---------------------------- |
295 | 297 | # Gradient Reversal |
296 | 298 | # ---------------------------- |
297 | | - |
298 | | -class GradReverse(torch.autograd.Function): |
299 | | - @staticmethod |
300 | | - def forward(ctx, x, lambda_): |
301 | | - ctx.lambda_ = lambda_ |
302 | | - return x.view_as(x) |
303 | | - |
304 | | - @staticmethod |
305 | | - def backward(ctx, grad_output): |
306 | | - return grad_output.neg() * ctx.lambda_, None |
307 | | - |
308 | | - |
309 | | -class GradReverseLayer(nn.Module): |
310 | | - def __init__(self, lambda_: float = 1.0): |
311 | | - super().__init__() |
312 | | - self.lambda_ = lambda_ |
313 | | - |
314 | | - def forward(self, x): |
315 | | - return GradReverse.apply(x, self.lambda_) |
316 | | - |
317 | | - def set_lambda(self, lambda_: float): |
318 | | - self.lambda_ = lambda_ |
| 299 | +# Moved to models.ae (imported above) |
319 | 300 |
|
320 | 301 |
|
321 | 302 | # ---------------------------- |
322 | 303 | # Model |
323 | 304 | # ---------------------------- |
324 | | - |
325 | | -class ResidualBlock(nn.Module): |
326 | | - """ |
327 | | - Residual block with skip connection: x + FFN(x) + LayerNorm |
328 | | - """ |
329 | | - def __init__(self, hidden_size: int, dropout: float = 0.0): |
330 | | - super().__init__() |
331 | | - self.ffn = nn.Sequential( |
332 | | - nn.Linear(hidden_size, hidden_size), |
333 | | - nn.SiLU(), |
334 | | - nn.Dropout(dropout), |
335 | | - nn.Linear(hidden_size, hidden_size), |
336 | | - nn.Dropout(dropout) |
337 | | - ) |
338 | | - self.layer_norm = nn.LayerNorm(hidden_size) |
339 | | - |
340 | | - def forward(self, x): |
341 | | - return self.layer_norm(x + self.ffn(x)) |
342 | | - |
343 | | - |
344 | | -def make_mlp(sizes, dropout=0.0, last_activation=None, use_residual=False): |
345 | | - """ |
346 | | - Hidden layers: Linear -> LayerNorm -> SiLU -> Dropout |
347 | | - Output layer: optional activation per arg |
348 | | - |
349 | | - If use_residual=True, uses ResidualBlock layers instead. |
350 | | - Note: For residual connections to work, all hidden layer sizes must be the same. |
351 | | - """ |
352 | | - if use_residual: |
353 | | - if len(sizes) < 3: |
354 | | - raise ValueError("Residual networks need at least input, hidden, and output layers") |
355 | | - |
356 | | - # Check that all hidden layers have the same size for residual connections |
357 | | - hidden_sizes = sizes[1:-1] |
358 | | - if len(set(hidden_sizes)) > 1: |
359 | | - raise ValueError(f"For residual connections, all hidden layer sizes must be the same. Got: {hidden_sizes}") |
360 | | - |
361 | | - hidden_size = hidden_sizes[0] |
362 | | - num_hidden_layers = len(hidden_sizes) |
363 | | - |
364 | | - layers = [] |
365 | | - |
366 | | - # Input projection to hidden size |
367 | | - layers.append(nn.Linear(sizes[0], hidden_size)) |
368 | | - layers.extend([nn.LayerNorm(hidden_size), nn.SiLU(), nn.Dropout(dropout)]) |
369 | | - |
370 | | - # Residual blocks |
371 | | - for _ in range(num_hidden_layers): |
372 | | - layers.append(ResidualBlock(hidden_size, dropout)) |
373 | | - |
374 | | - # Output layer |
375 | | - layers.append(nn.Linear(hidden_size, sizes[-1])) |
376 | | - if last_activation == "relu": |
377 | | - layers.append(nn.ReLU()) |
378 | | - elif last_activation == "tanh": |
379 | | - layers.append(nn.Tanh()) |
380 | | - elif last_activation == "sigmoid": |
381 | | - layers.append(nn.Sigmoid()) |
382 | | - |
383 | | - return nn.Sequential(*layers) |
384 | | - |
385 | | - else: |
386 | | - # Original implementation |
387 | | - layers = [] |
388 | | - for i in range(len(sizes) - 1): |
389 | | - in_f, out_f = sizes[i], sizes[i + 1] |
390 | | - layers.append(nn.Linear(in_f, out_f)) |
391 | | - if i < len(sizes) - 2: |
392 | | - layers += [nn.LayerNorm(out_f), nn.SiLU(), nn.Dropout(dropout)] |
393 | | - else: |
394 | | - if last_activation == "relu": |
395 | | - layers += [nn.ReLU()] |
396 | | - elif last_activation == "tanh": |
397 | | - layers += [nn.Tanh()] |
398 | | - elif last_activation == "sigmoid": |
399 | | - layers += [nn.Sigmoid()] |
400 | | - return nn.Sequential(*layers) |
401 | | - |
402 | | - |
403 | | -class AEBatchCorrector(nn.Module): |
404 | | - def __init__( |
405 | | - self, |
406 | | - n_genes: int, |
407 | | - latent_dim: int = 32, |
408 | | - enc_hidden=(1024, 256), |
409 | | - dec_hidden=(256, 1024), |
410 | | - adv_hidden=(128,), |
411 | | - sup_hidden=(64,), |
412 | | - n_batches: int = 2, |
413 | | - n_labels: Optional[int] = None, |
414 | | - dropout: float = 0.1, |
415 | | - adv_lambda: float = 1.0, |
416 | | - use_residual: bool = False, |
417 | | - ): |
418 | | - super().__init__() |
419 | | - self.n_labels = n_labels |
420 | | - self.grl = GradReverseLayer(lambda_=adv_lambda) |
421 | | - |
422 | | - enc_sizes = [n_genes] + list(enc_hidden) + [latent_dim] |
423 | | - dec_sizes = [latent_dim] + list(dec_hidden) + [n_genes] |
424 | | - self.encoder = make_mlp(enc_sizes, dropout=dropout, last_activation=None, use_residual=use_residual) |
425 | | - self.decoder = make_mlp(dec_sizes, dropout=dropout, last_activation=None, use_residual=use_residual) |
426 | | - |
427 | | - adv_sizes = [latent_dim] + list(adv_hidden) + [n_batches] |
428 | | - self.adv = make_mlp(adv_sizes, dropout=dropout, last_activation=None, use_residual=False) # Keep simple for adversarial head |
429 | | - |
430 | | - if n_labels is not None: |
431 | | - sup_sizes = [latent_dim] + list(sup_hidden) + [n_labels] |
432 | | - self.sup = make_mlp(sup_sizes, dropout=dropout, last_activation=None, use_residual=False) # Keep simple for supervised head |
433 | | - else: |
434 | | - self.sup = None |
435 | | - |
436 | | - def forward(self, x, adv_lambda: Optional[float] = None): |
437 | | - z = self.encoder(x) |
438 | | - x_hat = self.decoder(z) |
439 | | - if adv_lambda is not None: |
440 | | - self.grl.set_lambda(adv_lambda) |
441 | | - z_rev = self.grl(z) |
442 | | - batch_logits = self.adv(z_rev) |
443 | | - label_logits = self.sup(z) if self.sup is not None else None |
444 | | - return x_hat, batch_logits, label_logits, z |
445 | | - |
446 | | - @torch.no_grad() |
447 | | - def reconstruct(self, x): |
448 | | - """Fast path at inference time (skips adversary & GRL).""" |
449 | | - z = self.encoder(x) |
450 | | - x_hat = self.decoder(z) |
451 | | - return x_hat, z |
| 305 | +# Moved to models.ae (imported above) |
452 | 306 |
|
453 | 307 |
|
454 | 308 | # ---------------------------- |
@@ -1192,7 +1046,8 @@ def main(): |
1192 | 1046 | ap.add_argument("--viz_hvg_top", default=2000, type=int, help="Top-N most variable genes to use for PCA visualisations (0=use all)") |
1193 | 1047 | ap.add_argument("--viz_pca_before", default="pca_before.png", type=str, help="Output path for PCA before correction") |
1194 | 1048 | ap.add_argument("--viz_pca_after", default="pca_after.png", type=str, help="Output path for PCA after correction") |
1195 | | - ap.add_argument("--viz_boxplot", default="logCPM_boxplots.png", type=str, help="Output path for logCPM boxplots") |
| 1049 | + # Accept optional value; if provided without a path, use the default filename |
| 1050 | + ap.add_argument("--viz_boxplot", nargs="?", const="logCPM_boxplots.png", default="logCPM_boxplots.png", type=str, help="Output path for logCPM boxplots (optional value)") |
1196 | 1051 | # Differentiable biology preservation loss & adaptive adversary based on cond_sil |
1197 | 1052 | ap.add_argument("--bio_weight", default=0.0, type=float, help="Weight for supervised center loss on latent (preserve biology)") |
1198 | 1053 | ap.add_argument("--bio_gamma", default=0.5, type=float, help="Relative weight for between-class separation in center loss") |
@@ -1339,38 +1194,12 @@ def main(): |
1339 | 1194 | dl_val = DataLoader(ds_val, batch_size=args.batch_size, shuffle=False, **loader_kwargs) |
1340 | 1195 |
|
1341 | 1196 | # Model |
1342 | | - if args.model_type == "vae_attention": |
1343 | | - if VaeAttentionBatchCorrector is None: |
1344 | | - raise RuntimeError("vae_attention model requested but module not available.") |
1345 | | - model = VaeAttentionBatchCorrector( |
1346 | | - num_genes=logcpm.shape[1], |
1347 | | - num_batches=len(batch_classes), |
1348 | | - latent_dim=args.latent_dim, |
1349 | | - hidden_dim=args.vae_hidden_dim, |
1350 | | - attention_dim=args.vae_attention_dim, |
1351 | | - n_heads=args.vae_attn_heads, |
1352 | | - dropout=args.dropout, |
1353 | | - dispersion=args.vae_dispersion, |
1354 | | - attn_max_tokens=args.attn_max_tokens, |
1355 | | - ) |
1356 | | - else: |
1357 | | - enc_hidden = tuple(int(x) for x in args.enc_hidden.split(",") if x.strip()) |
1358 | | - dec_hidden = tuple(int(x) for x in args.dec_hidden.split(",") if x.strip()) |
1359 | | - adv_hidden = tuple(int(x) for x in args.adv_hidden.split(",") if x.strip()) |
1360 | | - sup_hidden = tuple(int(x) for x in args.sup_hidden.split(",") if x.strip()) |
1361 | | - model = AEBatchCorrector( |
1362 | | - n_genes=logcpm.shape[1], |
1363 | | - latent_dim=args.latent_dim, |
1364 | | - enc_hidden=enc_hidden, |
1365 | | - dec_hidden=dec_hidden, |
1366 | | - adv_hidden=adv_hidden, |
1367 | | - sup_hidden=sup_hidden, |
1368 | | - n_batches=len(batch_classes), |
1369 | | - n_labels=(len(label_classes) if label_classes is not None else None), |
1370 | | - dropout=args.dropout, |
1371 | | - adv_lambda=args.adv_weight, |
1372 | | - use_residual=args.use_residual, |
1373 | | - ) |
| 1197 | + model = build_model_from_args( |
| 1198 | + args, |
| 1199 | + n_genes=logcpm.shape[1], |
| 1200 | + n_batches=len(batch_classes), |
| 1201 | + n_labels=(len(label_classes) if label_classes is not None else None), |
| 1202 | + ) |
1374 | 1203 |
|
1375 | 1204 | # Optional torch.compile (PyTorch 2+) |
1376 | 1205 | if args.compile: |
|
0 commit comments