# vision_encoder.py # VMamba Vision Encoder wrapper cho CLIMP-PAR # Dựa trên kiến trúc CLIMP (arXiv:2601.06891): # - VMamba làm vision backbone (thay thế ViT) # - Không dùng positional encoding (VMamba tự mã hóa vị trí qua SS2D scanning) # - Hỗ trợ đa phân giải tự nhiên (variable resolution) import sys import os import torch import torch.nn as nn # Thêm đường dẫn VMamba vào sys.path để import VSSM _VMAMBA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'VMamba') if _VMAMBA_DIR not in sys.path: sys.path.insert(0, _VMAMBA_DIR) from VMamba.vmamba import VSSM # ============================================================================ # Cấu hình VMamba theo từng variant (Tiny / Small / Base) # Tham khảo: vmamba.py factory functions # ============================================================================ VMAMBA_CONFIGS = { 'tiny': dict( depths=[2, 2, 9, 2], dims=96, drop_path_rate=0.2, patch_size=4, in_chans=3, num_classes=1000, ssm_d_state=16, ssm_ratio=2.0, ssm_dt_rank="auto", ssm_act_layer="silu", ssm_conv=3, ssm_conv_bias=True, ssm_drop_rate=0.0, ssm_init="v0", forward_type="v0", mlp_ratio=0.0, mlp_act_layer="gelu", mlp_drop_rate=0.0, gmlp=False, patch_norm=True, norm_layer="ln", downsample_version="v1", patchembed_version="v1", use_checkpoint=False, posembed=False, imgsize=224, ), 'small': dict( depths=[2, 2, 27, 2], dims=96, drop_path_rate=0.3, patch_size=4, in_chans=3, num_classes=1000, ssm_d_state=16, ssm_ratio=2.0, ssm_dt_rank="auto", ssm_act_layer="silu", ssm_conv=3, ssm_conv_bias=True, ssm_drop_rate=0.0, ssm_init="v0", forward_type="v0", mlp_ratio=0.0, mlp_act_layer="gelu", mlp_drop_rate=0.0, gmlp=False, patch_norm=True, norm_layer="ln", downsample_version="v1", patchembed_version="v1", use_checkpoint=False, posembed=False, imgsize=224, ), 'base': dict( depths=[2, 2, 27, 2], dims=128, drop_path_rate=0.6, patch_size=4, in_chans=3, num_classes=1000, ssm_d_state=16, ssm_ratio=2.0, ssm_dt_rank="auto", ssm_act_layer="silu", ssm_conv=3, ssm_conv_bias=True, ssm_drop_rate=0.0, ssm_init="v0", forward_type="v0", mlp_ratio=0.0, mlp_act_layer="gelu", mlp_drop_rate=0.0, gmlp=False, patch_norm=True, norm_layer="ln", downsample_version="v1", patchembed_version="v1", use_checkpoint=False, posembed=False, imgsize=224, ), } # Pretrained checkpoint URLs cho từng variant (tự động tải từ GitHub) VMAMBA_PRETRAINED_URLS = { 'tiny': "https://github.com/MzeroMiko/VMamba/releases/download/%23v0cls/vssmtiny_dp01_ckpt_epoch_292.pth", 'small': "https://github.com/MzeroMiko/VMamba/releases/download/%23v0cls/vssmsmall_dp03_ckpt_epoch_238.pth", 'base': "https://github.com/MzeroMiko/VMamba/releases/download/%23v0cls/vssmbase_dp06_ckpt_epoch_241.pth", } # Kích thước feature cuối cùng (num_features) của từng variant VMAMBA_FEATURE_DIMS = { 'tiny': 768, # dims=96, 4 stages → 96*8=768 'small': 768, # dims=96, 4 stages → 96*8=768 'base': 1024, # dims=128, 4 stages → 128*8=1024 } class VMambaVisionEncoder(nn.Module): """ Vision Encoder dựa trên VMamba cho CLIMP-PAR. Theo bài báo CLIMP (Section 3.2 - Vision Encoder): - Ảnh đầu vào I ∈ R^(H×W×3) được chia thành non-overlapping patches P×P - Đưa qua VSS blocks với SS2D cross-scan mechanism (4 hướng quét) - Hierarchical structure giảm dần kích thước feature map qua patch merging - Feature cuối cùng được chiếu sang shared embedding space qua projection W_v Ưu điểm chính: - Không cần positional encoding → hỗ trợ đa phân giải tự nhiên - Spatial inductive bias qua scanning patterns → robustness tốt hơn ViT - Sub-quadratic complexity → tiết kiệm bộ nhớ ở phân giải cao Args: variant (str): Phiên bản VMamba ('tiny', 'small', 'base'). Mặc định: 'tiny' embed_dim (int): Chiều của shared embedding space. Mặc định: 768 pretrained (bool): Có tải pretrained ImageNet-1K weights không. Mặc định: True pretrained_path (str, optional): Đường dẫn checkpoint local. Nếu None, tự động tải từ GitHub. """ def __init__(self, variant='tiny', embed_dim=768, pretrained=True, pretrained_path=None): super().__init__() assert variant in VMAMBA_CONFIGS, f"Variant '{variant}' không hợp lệ. Chọn: {list(VMAMBA_CONFIGS.keys())}" self.variant = variant self.embed_dim = embed_dim self.feature_dim = VMAMBA_FEATURE_DIMS[variant] # 1. Khởi tạo VMamba backbone cfg = VMAMBA_CONFIGS[variant].copy() self.backbone = VSSM(**cfg) # 2. Loại bỏ classification head ban đầu (classifier.head) # Giữ lại: norm, permute, avgpool # Bỏ: flatten, head (Linear → 1000 classes) # 3. Tạo projection head: feature_dim → embed_dim # Theo CLIMP: "final features mapped to the shared embedding space via a learned projection W_v" self.norm = nn.LayerNorm(self.feature_dim) self.avgpool = nn.AdaptiveAvgPool2d(1) self.projection = nn.Linear(self.feature_dim, embed_dim) # 4. Tải pretrained weights nếu yêu cầu if pretrained: self._load_pretrained(pretrained_path) def _load_pretrained(self, pretrained_path=None): """ Tải pretrained weights cho VMamba backbone. Nếu không có file local, tự động tải từ GitHub releases. """ if pretrained_path is not None and os.path.exists(pretrained_path): ckpt_path = pretrained_path else: # Tự động tải từ GitHub url = VMAMBA_PRETRAINED_URLS[self.variant] cache_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', 'checkpoints') os.makedirs(cache_dir, exist_ok=True) filename = f"vmamba_{self.variant}_pretrained.pth" ckpt_path = os.path.join(cache_dir, filename) if not os.path.exists(ckpt_path): print(f"[VMamba] Đang tải pretrained weights cho {self.variant} từ GitHub...") try: state_dict = torch.hub.load_state_dict_from_url( url, model_dir=cache_dir, map_location='cpu', check_hash=False, file_name=filename ) print(f"[VMamba] Tải thành công! Lưu tại: {ckpt_path}") # torch.hub.load_state_dict_from_url đã tải và lưu xong self._apply_pretrained(state_dict) return except Exception as e: print(f"[VMamba] Cảnh báo: Không thể tải pretrained weights: {e}") print(f"[VMamba] Tiếp tục với random initialization.") return # Tải từ file local try: ckpt = torch.load(ckpt_path, map_location='cpu') self._apply_pretrained(ckpt) except Exception as e: print(f"[VMamba] Lỗi khi tải checkpoint {ckpt_path}: {e}") print(f"[VMamba] Tiếp tục với random initialization.") def _apply_pretrained(self, ckpt): """Áp dụng pretrained state_dict vào backbone.""" if isinstance(ckpt, dict): # Checkpoint có thể chứa key 'model' hoặc là state_dict trực tiếp state_dict = ckpt.get('model', ckpt) else: state_dict = ckpt # Load với strict=False vì chúng ta đã bỏ classification head incompatible = self.backbone.load_state_dict(state_dict, strict=False) missing = [k for k in incompatible.missing_keys if 'classifier.head' not in k] if missing: print(f"[VMamba] Missing keys (ngoài classifier head): {missing[:5]}...") if incompatible.unexpected_keys: print(f"[VMamba] Unexpected keys: {incompatible.unexpected_keys[:5]}...") print(f"[VMamba] Đã tải pretrained weights cho {self.variant} thành công!") def forward(self, x): """ Forward pass: Ảnh → Image Embedding Args: x (Tensor): Ảnh đầu vào, shape (B, 3, H, W) Returns: image_features (Tensor): Image embedding, shape (B, embed_dim) """ # 1. Patch embedding + VSS blocks (hierarchical) x = self.backbone.patch_embed(x) # Thêm positional embedding nếu có (VMamba mặc định không dùng) if self.backbone.pos_embed is not None: channel_first = self.backbone.channel_first pos_embed = self.backbone.pos_embed.permute(0, 2, 3, 1) if not channel_first else self.backbone.pos_embed x = x + pos_embed # 2. Đi qua tất cả các VSS block stages for layer in self.backbone.layers: x = layer(x) # 3. x hiện tại có shape (B, H', W', C) hoặc (B, C, H', W') tùy channel_first # Chuẩn hóa về (B, C, H', W') cho AdaptiveAvgPool2d if not self.backbone.channel_first: x = x.permute(0, 3, 1, 2).contiguous() # (B, H', W', C) → (B, C, H', W') # 4. Nén bản đồ đặc trưng về kích thước 8x4 (32 vision tokens) # Bất kể đầu vào 448x448 (14x14 patches) hay gì, cũng sẽ mượt mà ép về 8x4. x = nn.functional.adaptive_avg_pool2d(x, (8, 4)) # 5. Flatten không gian: (B, C, 8, 4) -> (B, C, 32) B, C, H_prime, W_prime = x.shape x = x.view(B, C, -1) # 6. Chuyển sang (B, 32, C) x = x.transpose(1, 2) # 6. Layer Norm và Projection x = self.norm(x) image_features = self.projection(x) # (B, L, 768) return image_features def get_feature_dim(self): """Trả về chiều feature trước projection.""" return self.feature_dim def get_embed_dim(self): """Trả về chiều embedding sau projection.""" return self.embed_dim # ============================================================================ # Tiện ích: Tạo nhanh encoder theo variant # ============================================================================ def create_vision_encoder(variant='tiny', embed_dim=768, pretrained=True, **kwargs): """ Factory function tạo VMamba Vision Encoder. Args: variant: 'tiny', 'small', hoặc 'base' embed_dim: Chiều embedding đầu ra pretrained: Có tải pretrained weights không Returns: VMambaVisionEncoder instance """ return VMambaVisionEncoder( variant=variant, embed_dim=embed_dim, pretrained=pretrained, **kwargs )