Download src/models/fusion.py from minhquang47/temp: direct link, hf CLI and curl.
- Browser
- Download file 10.1 kB
-
https://huggingface.co/minhquang47/temp/resolve/main/src/models/fusion.py
- Command line
-
hf download hf://minhquang47/temp/src/models/fusion.py
-
curl -L -o fusion.py https://huggingface.co/minhquang47/temp/resolve/main/src/models/fusion.py
10.1 kB
| from dataclasses import dataclass | |
| from typing import Dict, Optional, Tuple | |
| import torch | |
| import torch.nn as nn | |
| class StructuralFusionConfig: | |
| """ | |
| Cấu hình cho StructuralVisualFusion. | |
| Args: | |
| d_model: Hidden dimension của visual tokens từ ViT/VLM. | |
| topo_feature_dim: Số chiều feature topology/morphology mỗi patch. | |
| hidden_dim: Hidden dimension của MLP chiếu topo_features -> d_model. | |
| prior_strength: Hệ số spatial modulation ban đầu cho lesion prior. | |
| topo_strength: Hệ số enrichment ban đầu cho topo embedding. | |
| dropout: Dropout trong topo MLP. | |
| use_layer_norm: Chuẩn hóa visual/topo branch để ổn định training. | |
| learnable_gates: Nếu True, alpha/beta là tham số học được. | |
| normalize_prior: Nếu True, normalize prior_mask về [0, 1] theo từng sample. | |
| residual_scale: Scale residual cuối để tránh làm lệch mạnh pretrained tokens. | |
| """ | |
| d_model: int | |
| topo_feature_dim: int | |
| hidden_dim: Optional[int] = None | |
| prior_strength: float = 0.30 | |
| topo_strength: float = 0.10 | |
| dropout: float = 0.10 | |
| use_layer_norm: bool = True | |
| learnable_gates: bool = True | |
| normalize_prior: bool = True | |
| residual_scale: float = 1.0 | |
| def __post_init__(self): | |
| if self.d_model <= 0: | |
| raise ValueError("d_model phải > 0") | |
| if self.topo_feature_dim <= 0: | |
| raise ValueError("topo_feature_dim phải > 0") | |
| if self.hidden_dim is None: | |
| self.hidden_dim = max(self.d_model // 2, self.topo_feature_dim * 4) | |
| if self.hidden_dim <= 0: | |
| raise ValueError("hidden_dim phải > 0") | |
| class StructuralVisualFusion(nn.Module): | |
| """ | |
| Fusion module nối ViT visual tokens với lesion prior và topology features. | |
| Forward logic: | |
| V_fused = V * (1 + alpha * P) + beta * MLP(F_topo) | |
| Trong đó: | |
| V: visual_tokens, shape [B, N, D] | |
| P: prior_mask, shape [B, N] hoặc [B, H, W] | |
| F_topo: topo_features, shape [B, N, F] hoặc [B, H, W, F] | |
| Output chính: | |
| fused_tokens, shape [B, N, D] | |
| """ | |
| def __init__(self, config: StructuralFusionConfig): | |
| super().__init__() | |
| self.config = config | |
| self.visual_norm = nn.LayerNorm(config.d_model) if config.use_layer_norm else nn.Identity() | |
| self.topo_norm = nn.LayerNorm(config.topo_feature_dim) if config.use_layer_norm else nn.Identity() | |
| self.topo_projector = nn.Sequential( | |
| nn.Linear(config.topo_feature_dim, config.hidden_dim), | |
| nn.GELU(), | |
| nn.Dropout(config.dropout), | |
| nn.Linear(config.hidden_dim, config.d_model), | |
| nn.Dropout(config.dropout), | |
| ) | |
| if config.learnable_gates: | |
| self.prior_gate = nn.Parameter(torch.tensor(float(config.prior_strength))) | |
| self.topo_gate = nn.Parameter(torch.tensor(float(config.topo_strength))) | |
| else: | |
| self.register_buffer("prior_gate", torch.tensor(float(config.prior_strength))) | |
| self.register_buffer("topo_gate", torch.tensor(float(config.topo_strength))) | |
| self.output_norm = nn.LayerNorm(config.d_model) if config.use_layer_norm else nn.Identity() | |
| def forward( | |
| self, | |
| visual_tokens: torch.Tensor, | |
| prior_mask: Optional[torch.Tensor] = None, | |
| topo_features: Optional[torch.Tensor] = None, | |
| return_diagnostics: bool = True, | |
| ): | |
| """ | |
| Args: | |
| visual_tokens: Tensor [B, N, D]. | |
| prior_mask: Optional tensor [B, N] hoặc [B, H, W]. | |
| topo_features: Optional tensor [B, N, F] hoặc [B, H, W, F]. | |
| return_diagnostics: Nếu True trả về dict debug; nếu False chỉ trả fused_tokens. | |
| Returns: | |
| dict hoặc Tensor: | |
| { | |
| "fused_tokens": [B, N, D], | |
| "prior_weights": [B, N, 1] | None, | |
| "topo_embedding": [B, N, D] | None, | |
| "prior_gate": scalar, | |
| "topo_gate": scalar, | |
| } | |
| """ | |
| self._validate_visual_tokens(visual_tokens) | |
| batch_size, num_tokens, d_model = visual_tokens.shape | |
| fused_tokens = self.visual_norm(visual_tokens) | |
| prior_weights = None | |
| topo_embedding = None | |
| if prior_mask is not None: | |
| prior_weights = self._prepare_prior_mask( | |
| prior_mask, | |
| batch_size=batch_size, | |
| num_tokens=num_tokens, | |
| device=visual_tokens.device, | |
| dtype=visual_tokens.dtype, | |
| ) | |
| fused_tokens = fused_tokens * (1.0 + self.prior_gate * prior_weights) | |
| if topo_features is not None: | |
| topo_features = self._prepare_topo_features( | |
| topo_features, | |
| batch_size=batch_size, | |
| num_tokens=num_tokens, | |
| device=visual_tokens.device, | |
| dtype=visual_tokens.dtype, | |
| ) | |
| topo_features = self.topo_norm(topo_features) | |
| topo_embedding = self.topo_projector(topo_features) | |
| fused_tokens = fused_tokens + self.topo_gate * topo_embedding | |
| if self.config.residual_scale != 1.0: | |
| fused_tokens = visual_tokens + self.config.residual_scale * (fused_tokens - visual_tokens) | |
| fused_tokens = self.output_norm(fused_tokens) | |
| if not return_diagnostics: | |
| return fused_tokens | |
| return { | |
| "fused_tokens": fused_tokens, | |
| "prior_weights": prior_weights, | |
| "topo_embedding": topo_embedding, | |
| "prior_gate": self.prior_gate.detach().clone(), | |
| "topo_gate": self.topo_gate.detach().clone(), | |
| } | |
| def _validate_visual_tokens(self, visual_tokens: torch.Tensor) -> None: | |
| if not torch.is_tensor(visual_tokens): | |
| raise TypeError("visual_tokens phải là torch.Tensor") | |
| if visual_tokens.ndim != 3: | |
| raise ValueError( | |
| f"visual_tokens phải có shape [B, N, D], nhận {tuple(visual_tokens.shape)}" | |
| ) | |
| if visual_tokens.shape[-1] != self.config.d_model: | |
| raise ValueError( | |
| f"visual_tokens D={visual_tokens.shape[-1]} không khớp d_model={self.config.d_model}" | |
| ) | |
| def _prepare_prior_mask( | |
| self, | |
| prior_mask: torch.Tensor, | |
| batch_size: int, | |
| num_tokens: int, | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| ) -> torch.Tensor: | |
| if not torch.is_tensor(prior_mask): | |
| prior_mask = torch.as_tensor(prior_mask) | |
| prior_mask = prior_mask.to(device=device, dtype=dtype) | |
| if prior_mask.ndim == 3: | |
| prior_mask = prior_mask.flatten(start_dim=1) | |
| elif prior_mask.ndim == 2: | |
| pass | |
| else: | |
| raise ValueError( | |
| f"prior_mask phải có shape [B, N] hoặc [B, H, W], nhận {tuple(prior_mask.shape)}" | |
| ) | |
| if prior_mask.shape[0] != batch_size: | |
| raise ValueError( | |
| f"prior_mask batch={prior_mask.shape[0]} không khớp visual batch={batch_size}" | |
| ) | |
| if prior_mask.shape[1] != num_tokens: | |
| raise ValueError( | |
| f"prior_mask N={prior_mask.shape[1]} không khớp visual tokens N={num_tokens}" | |
| ) | |
| if self.config.normalize_prior: | |
| prior_mask = self._minmax_normalize_per_sample(prior_mask) | |
| return prior_mask.unsqueeze(-1) | |
| def _prepare_topo_features( | |
| self, | |
| topo_features: torch.Tensor, | |
| batch_size: int, | |
| num_tokens: int, | |
| device: torch.device, | |
| dtype: torch.dtype, | |
| ) -> torch.Tensor: | |
| if not torch.is_tensor(topo_features): | |
| topo_features = torch.as_tensor(topo_features) | |
| topo_features = topo_features.to(device=device, dtype=dtype) | |
| if topo_features.ndim == 4: | |
| topo_features = topo_features.flatten(start_dim=1, end_dim=2) | |
| elif topo_features.ndim == 3: | |
| pass | |
| else: | |
| raise ValueError( | |
| "topo_features phải có shape [B, N, F] hoặc [B, H, W, F], " | |
| f"nhận {tuple(topo_features.shape)}" | |
| ) | |
| if topo_features.shape[0] != batch_size: | |
| raise ValueError( | |
| f"topo_features batch={topo_features.shape[0]} không khớp visual batch={batch_size}" | |
| ) | |
| if topo_features.shape[1] != num_tokens: | |
| raise ValueError( | |
| f"topo_features N={topo_features.shape[1]} không khớp visual tokens N={num_tokens}" | |
| ) | |
| if topo_features.shape[2] != self.config.topo_feature_dim: | |
| raise ValueError( | |
| f"topo_features F={topo_features.shape[2]} không khớp " | |
| f"topo_feature_dim={self.config.topo_feature_dim}" | |
| ) | |
| return topo_features | |
| def _minmax_normalize_per_sample(values: torch.Tensor, eps: float = 1e-8) -> torch.Tensor: | |
| min_values = values.amin(dim=1, keepdim=True) | |
| max_values = values.amax(dim=1, keepdim=True) | |
| norm = (values - min_values) / (max_values - min_values + eps) | |
| return torch.nan_to_num(norm, nan=0.0, posinf=0.0, neginf=0.0) | |
| def build_structural_visual_fusion( | |
| d_model: int, | |
| topo_feature_dim: int, | |
| **kwargs, | |
| ) -> StructuralVisualFusion: | |
| """Factory tiện dụng cho training scripts.""" | |
| config = StructuralFusionConfig( | |
| d_model=d_model, | |
| topo_feature_dim=topo_feature_dim, | |
| **kwargs, | |
| ) | |
| return StructuralVisualFusion(config) | |
| __all__ = [ | |
| "StructuralFusionConfig", | |
| "StructuralVisualFusion", | |
| "build_structural_visual_fusion", | |
| ] | |