Sync DINOv3 ViT ConvNeXt dual backbone (part 2)
Browse files
marine_dual_dinov3_backbone.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""DINOv3 ViT + ConvNeXt dual-branch backbone for marine feature models."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import sys
|
| 6 |
+
from dataclasses import dataclass
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
from typing import Callable
|
| 9 |
+
|
| 10 |
+
import torch
|
| 11 |
+
from torch import Tensor, nn
|
| 12 |
+
import torch.nn.functional as F
|
| 13 |
+
|
| 14 |
+
DINOV3_ROOT = Path(__file__).resolve().parent / "dinov3-main"
|
| 15 |
+
if str(DINOV3_ROOT) not in sys.path:
|
| 16 |
+
sys.path.append(str(DINOV3_ROOT))
|
| 17 |
+
|
| 18 |
+
from dinov3.hub import backbones as dinov3_backbones # noqa: E402
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
BatchNormTypes = (nn.BatchNorm1d, nn.BatchNorm2d, nn.BatchNorm3d, nn.SyncBatchNorm)
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
@dataclass(frozen=True)
|
| 25 |
+
class TrainableBackboneSummary:
|
| 26 |
+
total_tensors: int
|
| 27 |
+
trainable_tensors: int
|
| 28 |
+
trainable_parameters: int
|
| 29 |
+
trainable_prefixes: tuple[str, ...]
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
def _builder(name: str) -> Callable[..., nn.Module]:
|
| 33 |
+
try:
|
| 34 |
+
return getattr(dinov3_backbones, name)
|
| 35 |
+
except AttributeError as exc:
|
| 36 |
+
raise ValueError(f"Unsupported DINOv3 backbone '{name}'") from exc
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def _set_module_trainable(module: nn.Module, trainable: bool) -> None:
|
| 40 |
+
for param in module.parameters():
|
| 41 |
+
param.requires_grad = trainable
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
class DINOv3ViTConvNeXtBackbone(nn.Module):
|
| 45 |
+
"""Dual-branch DINOv3 backbone with explicit partial unfreezing.
|
| 46 |
+
|
| 47 |
+
The fine-tuning policy is intentionally narrow:
|
| 48 |
+
- freeze both pretrained branches by default;
|
| 49 |
+
- unfreeze the final ``vit_unfrozen_blocks`` ViT transformer blocks;
|
| 50 |
+
- unfreeze the final ``convnext_unfrozen_stages`` ConvNeXt stages and their
|
| 51 |
+
matching downsample layers;
|
| 52 |
+
- unfreeze every BatchNorm module in the complete model.
|
| 53 |
+
"""
|
| 54 |
+
|
| 55 |
+
def __init__(
|
| 56 |
+
self,
|
| 57 |
+
vit_name: str = "dinov3_vitl16",
|
| 58 |
+
convnext_name: str = "dinov3_convnext_base",
|
| 59 |
+
vit_weights: str | None = None,
|
| 60 |
+
convnext_weights: str | None = None,
|
| 61 |
+
pretrained: bool = True,
|
| 62 |
+
out_channels: int = 256,
|
| 63 |
+
vit_unfrozen_blocks: int = 2,
|
| 64 |
+
convnext_unfrozen_stages: int = 2,
|
| 65 |
+
) -> None:
|
| 66 |
+
super().__init__()
|
| 67 |
+
self.vit_name = vit_name
|
| 68 |
+
self.convnext_name = convnext_name
|
| 69 |
+
self.vit = _builder(vit_name)(pretrained=pretrained, weights=vit_weights) if vit_weights else _builder(vit_name)(pretrained=pretrained)
|
| 70 |
+
self.convnext = (
|
| 71 |
+
_builder(convnext_name)(pretrained=pretrained, weights=convnext_weights)
|
| 72 |
+
if convnext_weights
|
| 73 |
+
else _builder(convnext_name)(pretrained=pretrained)
|
| 74 |
+
)
|
| 75 |
+
self.vit_channels = int(getattr(self.vit, "embed_dim"))
|
| 76 |
+
self.convnext_channels = int(getattr(self.convnext, "embed_dim"))
|
| 77 |
+
self.out_channels = out_channels
|
| 78 |
+
|
| 79 |
+
self.vit_proj = nn.Sequential(
|
| 80 |
+
nn.Conv2d(self.vit_channels, out_channels, kernel_size=1, bias=False),
|
| 81 |
+
nn.BatchNorm2d(out_channels),
|
| 82 |
+
nn.GELU(),
|
| 83 |
+
)
|
| 84 |
+
self.convnext_proj = nn.Sequential(
|
| 85 |
+
nn.Conv2d(self.convnext_channels, out_channels, kernel_size=1, bias=False),
|
| 86 |
+
nn.BatchNorm2d(out_channels),
|
| 87 |
+
nn.GELU(),
|
| 88 |
+
)
|
| 89 |
+
self.fusion = nn.Sequential(
|
| 90 |
+
nn.Conv2d(out_channels * 2, out_channels, kernel_size=3, padding=1, bias=False),
|
| 91 |
+
nn.BatchNorm2d(out_channels),
|
| 92 |
+
nn.GELU(),
|
| 93 |
+
)
|
| 94 |
+
|
| 95 |
+
self.apply_finetune_policy(vit_unfrozen_blocks, convnext_unfrozen_stages)
|
| 96 |
+
|
| 97 |
+
def apply_finetune_policy(self, vit_unfrozen_blocks: int = 2, convnext_unfrozen_stages: int = 2) -> None:
|
| 98 |
+
self.requires_grad_(False)
|
| 99 |
+
self.vit_proj.requires_grad_(True)
|
| 100 |
+
self.convnext_proj.requires_grad_(True)
|
| 101 |
+
self.fusion.requires_grad_(True)
|
| 102 |
+
self._unfreeze_vit_tail(vit_unfrozen_blocks)
|
| 103 |
+
self._unfreeze_convnext_tail(convnext_unfrozen_stages)
|
| 104 |
+
self._unfreeze_all_batchnorm()
|
| 105 |
+
|
| 106 |
+
def _unfreeze_vit_tail(self, blocks_to_unfreeze: int) -> None:
|
| 107 |
+
blocks = getattr(self.vit, "blocks", None)
|
| 108 |
+
if blocks is None:
|
| 109 |
+
raise RuntimeError(f"{self.vit_name} does not expose a 'blocks' ModuleList")
|
| 110 |
+
for block in blocks[-blocks_to_unfreeze:]:
|
| 111 |
+
_set_module_trainable(block, True)
|
| 112 |
+
|
| 113 |
+
def _unfreeze_convnext_tail(self, stages_to_unfreeze: int) -> None:
|
| 114 |
+
stages = getattr(self.convnext, "stages", None)
|
| 115 |
+
downsample_layers = getattr(self.convnext, "downsample_layers", None)
|
| 116 |
+
if stages is None or downsample_layers is None:
|
| 117 |
+
raise RuntimeError(f"{self.convnext_name} does not expose ConvNeXt stages/downsample_layers")
|
| 118 |
+
start = max(0, len(stages) - stages_to_unfreeze)
|
| 119 |
+
for idx in range(start, len(stages)):
|
| 120 |
+
_set_module_trainable(stages[idx], True)
|
| 121 |
+
_set_module_trainable(downsample_layers[idx], True)
|
| 122 |
+
|
| 123 |
+
def _unfreeze_all_batchnorm(self) -> None:
|
| 124 |
+
for module in self.modules():
|
| 125 |
+
if isinstance(module, BatchNormTypes):
|
| 126 |
+
module.train()
|
| 127 |
+
_set_module_trainable(module, True)
|
| 128 |
+
|
| 129 |
+
def train(self, mode: bool = True) -> "DINOv3ViTConvNeXtBackbone":
|
| 130 |
+
super().train(mode)
|
| 131 |
+
if mode:
|
| 132 |
+
self._unfreeze_all_batchnorm()
|
| 133 |
+
return self
|
| 134 |
+
|
| 135 |
+
def forward(self, x: Tensor) -> dict[str, Tensor]:
|
| 136 |
+
vit_map = self._vit_feature_map(x)
|
| 137 |
+
convnext_map = self._convnext_feature_map(x)
|
| 138 |
+
convnext_map = F.interpolate(convnext_map, size=vit_map.shape[-2:], mode="bilinear", align_corners=False)
|
| 139 |
+
vit_feature = self.vit_proj(vit_map)
|
| 140 |
+
convnext_feature = self.convnext_proj(convnext_map)
|
| 141 |
+
fused = self.fusion(torch.cat([vit_feature, convnext_feature], dim=1))
|
| 142 |
+
return {
|
| 143 |
+
"fused": fused,
|
| 144 |
+
"vit": vit_feature,
|
| 145 |
+
"convnext": convnext_feature,
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
def _vit_feature_map(self, x: Tensor) -> Tensor:
|
| 149 |
+
features = self.vit.forward_features(x)
|
| 150 |
+
patch_tokens = features["x_norm_patchtokens"]
|
| 151 |
+
h = x.shape[-2] // int(getattr(self.vit, "patch_size"))
|
| 152 |
+
w = x.shape[-1] // int(getattr(self.vit, "patch_size"))
|
| 153 |
+
return patch_tokens.transpose(1, 2).reshape(x.shape[0], self.vit_channels, h, w)
|
| 154 |
+
|
| 155 |
+
def _convnext_feature_map(self, x: Tensor) -> Tensor:
|
| 156 |
+
features = self.convnext.forward_features(x)
|
| 157 |
+
patch_tokens = features["x_norm_patchtokens"]
|
| 158 |
+
h = x.shape[-2] // 32
|
| 159 |
+
w = x.shape[-1] // 32
|
| 160 |
+
return patch_tokens.transpose(1, 2).reshape(x.shape[0], self.convnext_channels, h, w)
|
| 161 |
+
|
| 162 |
+
def trainable_summary(self) -> TrainableBackboneSummary:
|
| 163 |
+
trainable_names = [name for name, param in self.named_parameters() if param.requires_grad]
|
| 164 |
+
prefixes = tuple(sorted({".".join(name.split(".")[:3]) for name in trainable_names}))
|
| 165 |
+
return TrainableBackboneSummary(
|
| 166 |
+
total_tensors=sum(1 for _ in self.parameters()),
|
| 167 |
+
trainable_tensors=len(trainable_names),
|
| 168 |
+
trainable_parameters=sum(param.numel() for param in self.parameters() if param.requires_grad),
|
| 169 |
+
trainable_prefixes=prefixes,
|
| 170 |
+
)
|
scripts/check_dual_backbone_policy.py
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Check trainable parameters for the DINOv3 ViT + ConvNeXt dual backbone."""
|
| 2 |
+
|
| 3 |
+
from __future__ import annotations
|
| 4 |
+
|
| 5 |
+
import argparse
|
| 6 |
+
import json
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import sys
|
| 9 |
+
|
| 10 |
+
ROOT = Path(__file__).resolve().parents[1]
|
| 11 |
+
if str(ROOT) not in sys.path:
|
| 12 |
+
sys.path.append(str(ROOT))
|
| 13 |
+
|
| 14 |
+
from marine_dual_dinov3_backbone import DINOv3ViTConvNeXtBackbone # noqa: E402
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def parse_args() -> argparse.Namespace:
|
| 18 |
+
parser = argparse.ArgumentParser(description=__doc__)
|
| 19 |
+
parser.add_argument("--vit-name", default="dinov3_vitl16")
|
| 20 |
+
parser.add_argument("--convnext-name", default="dinov3_convnext_base")
|
| 21 |
+
parser.add_argument("--vit-weights", default=None)
|
| 22 |
+
parser.add_argument("--convnext-weights", default=None)
|
| 23 |
+
parser.add_argument("--pretrained", action=argparse.BooleanOptionalAction, default=False)
|
| 24 |
+
parser.add_argument("--output", default=None)
|
| 25 |
+
return parser.parse_args()
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
def main() -> None:
|
| 29 |
+
args = parse_args()
|
| 30 |
+
model = DINOv3ViTConvNeXtBackbone(
|
| 31 |
+
vit_name=args.vit_name,
|
| 32 |
+
convnext_name=args.convnext_name,
|
| 33 |
+
vit_weights=args.vit_weights,
|
| 34 |
+
convnext_weights=args.convnext_weights,
|
| 35 |
+
pretrained=args.pretrained,
|
| 36 |
+
)
|
| 37 |
+
trainable = [name for name, param in model.named_parameters() if param.requires_grad]
|
| 38 |
+
frozen = [name for name, param in model.named_parameters() if not param.requires_grad]
|
| 39 |
+
summary = model.trainable_summary()
|
| 40 |
+
report = {
|
| 41 |
+
"vit_name": args.vit_name,
|
| 42 |
+
"convnext_name": args.convnext_name,
|
| 43 |
+
"pretrained": args.pretrained,
|
| 44 |
+
"summary": summary.__dict__,
|
| 45 |
+
"trainable_examples": trainable[:80],
|
| 46 |
+
"frozen_examples": frozen[:40],
|
| 47 |
+
"checks": {
|
| 48 |
+
"vit_last_two_blocks_trainable": any(name.startswith("vit.blocks.22.") for name in trainable)
|
| 49 |
+
and any(name.startswith("vit.blocks.23.") for name in trainable),
|
| 50 |
+
"vit_earlier_blocks_frozen": not any(name.startswith("vit.blocks.21.") for name in trainable),
|
| 51 |
+
"convnext_last_two_stages_trainable": any(name.startswith("convnext.stages.2.") for name in trainable)
|
| 52 |
+
and any(name.startswith("convnext.stages.3.") for name in trainable),
|
| 53 |
+
"convnext_early_stages_frozen": not any(name.startswith("convnext.stages.0.") for name in trainable)
|
| 54 |
+
and not any(name.startswith("convnext.stages.1.") for name in trainable),
|
| 55 |
+
"fusion_trainable": any(name.startswith("fusion.") for name in trainable),
|
| 56 |
+
},
|
| 57 |
+
}
|
| 58 |
+
text = json.dumps(report, indent=2, ensure_ascii=False)
|
| 59 |
+
if args.output:
|
| 60 |
+
Path(args.output).write_text(text, encoding="utf-8")
|
| 61 |
+
print(text)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
if __name__ == "__main__":
|
| 65 |
+
main()
|