fuse_proj: Initial operational package for 3 researchers (Pavlenko/Blizno/Moroz)
Multimodal fusion research on StripNet+GTA-UAV proxy: - 3 independent fusion tracks: condition-aware (A), token/bottleneck (B), role-aware (C) - Shared interfaces, protocol, dataset audit, baseline benchmarks - Canonical version-chain references to vault (SPEC, ANALYSIS, TRIAGE) - Personalized task plans and decision tables for each researcher - 3 generated DOCX task assignment files with milestones and DoD checklist - Full modality dropout diagnostics and missing-modality robustness requirements - Data contract, benchmark registry, experiment tracking infrastructure Operational documents: - docs/00_project/: MERIDIAN context, protocol, repository reuse guide, experiment specification - docs/01_tasks/: Master assignment + 3 individual researcher tracks + joint integration - docs/02_references/: Core literature, version-chain bases, code maps - docs/03_codebase_guides/: Existing code snapshots from vault - scripts/: gen_task_plans.js (DOCX generation), placeholder infrastructure - vendor_reference/: Snapshots of caption_test, depth_edges_annotate, existing SOFIA/SegModel code - reports/, results/, experiments/: Shared output structure for all 3 researchers 3 DOCX files generated from gen_task_plans.js (Times New Roman 14pt, GOST format): - План_заданий_Павленко_БВ.docx (Condition-Aware track, fusion API owner) - План_заданий_Близно_МВ.docx (Token/Bottleneck track, benchmark owner) - План_заданий_Мороз_ЕС.docx (Role-Aware track, data contract owner) Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
1
src/fuse_proj/models/__init__.py
Normal file
1
src/fuse_proj/models/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
"""Model components for multimodal fusion experiments."""
|
||||
6
src/fuse_proj/models/fusion/__init__.py
Normal file
6
src/fuse_proj/models/fusion/__init__.py
Normal file
@@ -0,0 +1,6 @@
|
||||
"""Fusion interfaces and registry shared by all architecture families."""
|
||||
|
||||
from fuse_proj.models.fusion.base import FusionModelBase
|
||||
from fuse_proj.models.fusion.registry import build_fusion_model, register_fusion_model
|
||||
|
||||
__all__ = ["FusionModelBase", "build_fusion_model", "register_fusion_model"]
|
||||
139
src/fuse_proj/models/fusion/base.py
Normal file
139
src/fuse_proj/models/fusion/base.py
Normal file
@@ -0,0 +1,139 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Common interface for independently encoded satellite and UAV fusion branches.
|
||||
|
||||
Architecture overview:
|
||||
satellite ViewBatch -> encode_view("satellite") -> normalized descriptor [B, D]
|
||||
UAV ViewBatch -> encode_view("uav") -> normalized descriptor [B, D]
|
||||
|
||||
The base class intentionally does not define a fusion operator. Condition-aware,
|
||||
token-bottleneck, and role-aware implementations must expose the same contract.
|
||||
|
||||
References:
|
||||
- Hou et al. "Strip R-CNN: Large Strip Convolution for Remote Sensing Object
|
||||
Detection," CVPR 2025. StripNet provides the shared RGB feature hierarchy.
|
||||
"""
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.nn.functional as F
|
||||
|
||||
from fuse_proj.data.types import FusionPairOutput, FusionViewOutput, ViewBatch, ViewName
|
||||
|
||||
|
||||
class FusionModelBase(nn.Module, ABC):
|
||||
"""Base class for multimodal cross-view retrieval encoders.
|
||||
|
||||
Architecture:
|
||||
satellite batch -- encode_view --> satellite descriptor [B, D]
|
||||
UAV batch -- encode_view --> UAV descriptor [B, D]
|
||||
|
|
||||
+--> retrieval objective outside model
|
||||
|
||||
Args:
|
||||
descriptor_dim: Retrieval descriptor width D. Project default is 1024.
|
||||
|
||||
Shape contract:
|
||||
Inputs: Two independent ``ViewBatch`` objects with RGB shape [B, 3, H, W].
|
||||
Outputs: Two ``FusionViewOutput`` dictionaries with descriptors [B, D].
|
||||
|
||||
Cost:
|
||||
This abstract wrapper adds no trainable parameters and negligible compute.
|
||||
|
||||
References:
|
||||
- Strip R-CNN (CVPR 2025) for the shared StripNet RGB backbone contract.
|
||||
"""
|
||||
|
||||
def __init__(self, descriptor_dim: int = 1024) -> None:
|
||||
super().__init__()
|
||||
if descriptor_dim <= 0:
|
||||
raise ValueError(f"descriptor_dim must be positive, got {descriptor_dim}")
|
||||
self.descriptor_dim = descriptor_dim
|
||||
|
||||
@abstractmethod
|
||||
def encode_view(self, batch: ViewBatch, view: ViewName) -> FusionViewOutput:
|
||||
"""Encode one view without access to features from the paired view.
|
||||
|
||||
Args:
|
||||
batch: Multimodal inputs for one view.
|
||||
view: ``"satellite"`` for CHM geometry or ``"uav"`` for depth geometry.
|
||||
|
||||
Returns:
|
||||
Fusion output with L2-normalized descriptor [B, descriptor_dim].
|
||||
"""
|
||||
|
||||
def forward(self, satellite: ViewBatch, uav: ViewBatch) -> FusionPairOutput:
|
||||
"""Encode satellite and UAV batches independently.
|
||||
|
||||
Args:
|
||||
satellite: Satellite batch with CHM geometry.
|
||||
uav: UAV batch with relative-depth geometry.
|
||||
|
||||
Returns:
|
||||
Independent satellite and UAV outputs. The retrieval loss is computed
|
||||
outside this module to prevent paired-view feature leakage.
|
||||
"""
|
||||
self.validate_view_batch(satellite, view="satellite")
|
||||
self.validate_view_batch(uav, view="uav")
|
||||
return {
|
||||
"satellite": self.encode_view(satellite, view="satellite"),
|
||||
"uav": self.encode_view(uav, view="uav"),
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def normalize_descriptor(descriptor: torch.Tensor) -> torch.Tensor:
|
||||
"""L2-normalize descriptors [B, D] along the feature dimension."""
|
||||
if descriptor.ndim != 2:
|
||||
raise ValueError(f"descriptor must have shape [B, D], got {tuple(descriptor.shape)}")
|
||||
if not torch.isfinite(descriptor).all():
|
||||
raise ValueError("descriptor contains NaN or Inf values")
|
||||
return F.normalize(descriptor, dim=-1)
|
||||
|
||||
@staticmethod
|
||||
def validate_view_batch(batch: ViewBatch, view: ViewName) -> None:
|
||||
"""Validate required keys, batch dimensions, and view-independent shapes.
|
||||
|
||||
Args:
|
||||
batch: Candidate multimodal batch.
|
||||
view: View name used only to improve validation error messages.
|
||||
|
||||
Raises:
|
||||
KeyError: If a required field is missing.
|
||||
ValueError: If tensor or list batch dimensions are inconsistent.
|
||||
"""
|
||||
required = {
|
||||
"rgb",
|
||||
"geometry",
|
||||
"segmentation",
|
||||
"geometry_valid",
|
||||
"segmentation_valid",
|
||||
"text_valid",
|
||||
"text",
|
||||
"sample_id",
|
||||
}
|
||||
missing = required.difference(batch.keys())
|
||||
if missing:
|
||||
raise KeyError(f"{view} batch is missing keys: {sorted(missing)}")
|
||||
|
||||
rgb = batch["rgb"]
|
||||
geometry = batch["geometry"]
|
||||
segmentation = batch["segmentation"]
|
||||
if rgb.ndim != 4 or rgb.shape[1] != 3:
|
||||
raise ValueError(f"{view} rgb must be [B, 3, H, W], got {tuple(rgb.shape)}")
|
||||
if geometry.ndim != 4 or geometry.shape[1] != 1:
|
||||
raise ValueError(f"{view} geometry must be [B, 1, H, W], got {tuple(geometry.shape)}")
|
||||
if segmentation.ndim != 4 or segmentation.shape[1] not in {1, 17}:
|
||||
raise ValueError(
|
||||
f"{view} segmentation must be [B, 1, H, W] or [B, 17, H, W], "
|
||||
f"got {tuple(segmentation.shape)}"
|
||||
)
|
||||
|
||||
batch_size = rgb.shape[0]
|
||||
tensor_fields = ("geometry", "segmentation", "geometry_valid", "segmentation_valid", "text_valid")
|
||||
for field in tensor_fields:
|
||||
if batch[field].shape[0] != batch_size:
|
||||
raise ValueError(f"{view} field {field} has inconsistent batch size")
|
||||
if len(batch["text"]) != batch_size or len(batch["sample_id"]) != batch_size:
|
||||
raise ValueError(f"{view} text/sample_id length must equal batch size {batch_size}")
|
||||
56
src/fuse_proj/models/fusion/registry.py
Normal file
56
src/fuse_proj/models/fusion/registry.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from __future__ import annotations
|
||||
|
||||
"""Registry for selecting a fusion implementation from a gin configuration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
|
||||
from fuse_proj.models.fusion.base import FusionModelBase
|
||||
|
||||
|
||||
FusionBuilder = Callable[..., FusionModelBase]
|
||||
_FUSION_REGISTRY: dict[str, FusionBuilder] = {}
|
||||
|
||||
|
||||
def register_fusion_model(name: str) -> Callable[[FusionBuilder], FusionBuilder]:
|
||||
"""Register a fusion model builder under a stable configuration name.
|
||||
|
||||
Args:
|
||||
name: Unique lowercase identifier used by experiment configuration.
|
||||
|
||||
Returns:
|
||||
Decorator that stores and returns the original builder.
|
||||
|
||||
Raises:
|
||||
ValueError: If the name is empty or already registered.
|
||||
"""
|
||||
normalized = name.strip().lower()
|
||||
if not normalized:
|
||||
raise ValueError("Fusion model name must not be empty")
|
||||
|
||||
def decorator(builder: FusionBuilder) -> FusionBuilder:
|
||||
if normalized in _FUSION_REGISTRY:
|
||||
raise ValueError(f"Fusion model already registered: {normalized}")
|
||||
_FUSION_REGISTRY[normalized] = builder
|
||||
return builder
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
def build_fusion_model(name: str, **kwargs: object) -> FusionModelBase:
|
||||
"""Build a registered fusion model.
|
||||
|
||||
Args:
|
||||
name: Registered fusion model identifier.
|
||||
**kwargs: Constructor arguments forwarded to the registered builder.
|
||||
|
||||
Returns:
|
||||
Constructed fusion model.
|
||||
|
||||
Raises:
|
||||
KeyError: If no model is registered under ``name``.
|
||||
"""
|
||||
normalized = name.strip().lower()
|
||||
if normalized not in _FUSION_REGISTRY:
|
||||
available = ", ".join(sorted(_FUSION_REGISTRY)) or "<none>"
|
||||
raise KeyError(f"Unknown fusion model '{normalized}'. Available: {available}")
|
||||
return _FUSION_REGISTRY[normalized](**kwargs)
|
||||
Reference in New Issue
Block a user