cuibinge commited on
Commit
794caa0
·
verified ·
1 Parent(s): 75d8411

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()