cuibinge commited on
Commit
e88d9ee
·
verified ·
1 Parent(s): 2364ad3

Sync Coast-style multi-scale fusion (part 2)

Browse files
Files changed (1) hide show
  1. marine_dual_dinov3_backbone.py +180 -30
marine_dual_dinov3_backbone.py CHANGED
@@ -55,6 +55,163 @@ def _set_module_trainable(module: nn.Module, trainable: bool) -> None:
55
  param.requires_grad = trainable
56
 
57
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  class DINOv3ViTConvNeXtBackbone(nn.Module):
59
  """Dual-branch DINOv3 backbone with explicit partial unfreezing.
60
 
@@ -88,30 +245,21 @@ class DINOv3ViTConvNeXtBackbone(nn.Module):
88
  )
89
  self.vit_channels = int(getattr(self.vit, "embed_dim"))
90
  self.convnext_channels = int(getattr(self.convnext, "embed_dim"))
 
91
  self.out_channels = out_channels
92
 
93
- self.vit_proj = nn.Sequential(
94
- nn.Conv2d(self.vit_channels, out_channels, kernel_size=1, bias=False),
95
- nn.BatchNorm2d(out_channels),
96
- nn.GELU(),
97
- )
98
- self.convnext_proj = nn.Sequential(
99
- nn.Conv2d(self.convnext_channels, out_channels, kernel_size=1, bias=False),
100
- nn.BatchNorm2d(out_channels),
101
- nn.GELU(),
102
- )
103
- self.fusion = nn.Sequential(
104
- nn.Conv2d(out_channels * 2, out_channels, kernel_size=3, padding=1, bias=False),
105
- nn.BatchNorm2d(out_channels),
106
- nn.GELU(),
107
  )
108
 
109
  self.apply_finetune_policy(vit_unfrozen_blocks, convnext_unfrozen_stages)
110
 
111
  def apply_finetune_policy(self, vit_unfrozen_blocks: int = 2, convnext_unfrozen_stages: int = 2) -> None:
112
  self.requires_grad_(False)
113
- self.vit_proj.requires_grad_(True)
114
- self.convnext_proj.requires_grad_(True)
115
  self.fusion.requires_grad_(True)
116
  self._unfreeze_vit_tail(vit_unfrozen_blocks)
117
  self._unfreeze_convnext_tail(convnext_unfrozen_stages)
@@ -148,15 +296,15 @@ class DINOv3ViTConvNeXtBackbone(nn.Module):
148
 
149
  def forward(self, x: Tensor) -> dict[str, Tensor]:
150
  vit_map = self._vit_feature_map(x)
151
- convnext_map = self._convnext_feature_map(x)
152
- convnext_map = F.interpolate(convnext_map, size=vit_map.shape[-2:], mode="bilinear", align_corners=False)
153
- vit_feature = self.vit_proj(vit_map)
154
- convnext_feature = self.convnext_proj(convnext_map)
155
- fused = self.fusion(torch.cat([vit_feature, convnext_feature], dim=1))
156
  return {
157
- "fused": fused,
158
- "vit": vit_feature,
159
- "convnext": convnext_feature,
 
 
 
160
  }
161
 
162
  def _vit_feature_map(self, x: Tensor) -> Tensor:
@@ -166,12 +314,14 @@ class DINOv3ViTConvNeXtBackbone(nn.Module):
166
  w = x.shape[-1] // int(getattr(self.vit, "patch_size"))
167
  return patch_tokens.transpose(1, 2).reshape(x.shape[0], self.vit_channels, h, w)
168
 
169
- def _convnext_feature_map(self, x: Tensor) -> Tensor:
170
- features = self.convnext.forward_features(x)
171
- patch_tokens = features["x_norm_patchtokens"]
172
- h = x.shape[-2] // 32
173
- w = x.shape[-1] // 32
174
- return patch_tokens.transpose(1, 2).reshape(x.shape[0], self.convnext_channels, h, w)
 
 
175
 
176
  def trainable_summary(self) -> TrainableBackboneSummary:
177
  trainable_names = [name for name, param in self.named_parameters() if param.requires_grad]
 
55
  param.requires_grad = trainable
56
 
57
 
58
+ class CrossScaleAttention(nn.Module):
59
+ """Use ViT tokens as queries to align one ConvNeXt feature scale."""
60
+
61
+ def __init__(self, dim: int, num_heads: int = 8, mlp_ratio: int = 4) -> None:
62
+ super().__init__()
63
+ self.q_norm = nn.LayerNorm(dim)
64
+ self.kv_norm = nn.LayerNorm(dim)
65
+ self.attn = nn.MultiheadAttention(embed_dim=dim, num_heads=num_heads, batch_first=True)
66
+ self.norm = nn.LayerNorm(dim)
67
+ self.ffn = nn.Sequential(
68
+ nn.Linear(dim, dim * mlp_ratio),
69
+ nn.GELU(),
70
+ nn.Linear(dim * mlp_ratio, dim),
71
+ )
72
+
73
+ def forward(self, vit_tokens: Tensor, conv_tokens: Tensor) -> Tensor:
74
+ aligned, _ = self.attn(
75
+ query=self.q_norm(vit_tokens),
76
+ key=self.kv_norm(conv_tokens),
77
+ value=self.kv_norm(conv_tokens),
78
+ need_weights=False,
79
+ )
80
+ x = vit_tokens + aligned
81
+ return x + self.ffn(self.norm(x))
82
+
83
+
84
+ class CoastStyleMultiScaleFusion(nn.Module):
85
+ """CoastGPT-inspired heterogeneous multi-scale fusion for dense heads."""
86
+
87
+ def __init__(
88
+ self,
89
+ vit_dim: int,
90
+ convnext_dims: tuple[int, int, int, int],
91
+ out_channels: int = 256,
92
+ semantic_grid_size: tuple[int, int] | None = None,
93
+ detail_grid_size: tuple[int, int] = (28, 28),
94
+ num_heads: int = 8,
95
+ ) -> None:
96
+ super().__init__()
97
+ self.vit_dim = int(vit_dim)
98
+ self.out_channels = int(out_channels)
99
+ self.semantic_grid_size = semantic_grid_size
100
+ self.detail_grid_size = detail_grid_size
101
+
102
+ c4_dim, c8_dim, c16_dim, c32_dim = [int(v) for v in convnext_dims]
103
+ self.c4_proj = nn.Sequential(nn.Linear(c4_dim, vit_dim), nn.LayerNorm(vit_dim))
104
+ self.c16_proj = nn.Sequential(nn.Linear(c16_dim, vit_dim), nn.LayerNorm(vit_dim))
105
+ self.c32_proj = nn.Sequential(nn.Linear(c32_dim, vit_dim), nn.LayerNorm(vit_dim))
106
+ self.c4_attn = CrossScaleAttention(vit_dim, num_heads=num_heads)
107
+ self.c16_attn = CrossScaleAttention(vit_dim, num_heads=num_heads)
108
+ self.c32_attn = CrossScaleAttention(vit_dim, num_heads=num_heads)
109
+ self.semantic_logits = nn.Parameter(torch.zeros(4))
110
+ self.semantic_compress = nn.Sequential(
111
+ nn.Linear(vit_dim * 4, vit_dim),
112
+ nn.LayerNorm(vit_dim),
113
+ nn.GELU(),
114
+ nn.Linear(vit_dim, out_channels),
115
+ nn.LayerNorm(out_channels),
116
+ )
117
+ self.detail_proj = nn.Sequential(
118
+ nn.Linear(c8_dim, vit_dim),
119
+ nn.LayerNorm(vit_dim),
120
+ nn.GELU(),
121
+ nn.Linear(vit_dim, out_channels),
122
+ nn.LayerNorm(out_channels),
123
+ )
124
+ self.detail_to_semantic = nn.Sequential(
125
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, groups=out_channels, bias=False),
126
+ nn.Conv2d(out_channels, out_channels, kernel_size=1, bias=False),
127
+ nn.GroupNorm(1, out_channels),
128
+ nn.GELU(),
129
+ )
130
+ self.fusion_gate = nn.Sequential(
131
+ nn.Conv2d(out_channels * 2, out_channels, kernel_size=1),
132
+ nn.GELU(),
133
+ nn.Conv2d(out_channels, out_channels, kernel_size=1),
134
+ nn.Sigmoid(),
135
+ )
136
+ self.output_smooth = nn.Sequential(
137
+ nn.Conv2d(out_channels, out_channels, kernel_size=3, padding=1, bias=False),
138
+ nn.GroupNorm(1, out_channels),
139
+ nn.GELU(),
140
+ )
141
+ self._last_fusion_weights: Tensor | None = None
142
+
143
+ @staticmethod
144
+ def _add_2d_sincos_pos_embed(tokens: Tensor, h: int, w: int) -> Tensor:
145
+ device, dtype = tokens.device, tokens.dtype
146
+ channels = tokens.shape[-1]
147
+ quarter = max(channels // 4, 1)
148
+ yy, xx = torch.meshgrid(
149
+ torch.arange(h, device=device),
150
+ torch.arange(w, device=device),
151
+ indexing="ij",
152
+ )
153
+ omega = torch.arange(quarter, device=device, dtype=dtype)
154
+ omega = 1.0 / (10000 ** (omega / quarter))
155
+ yy = yy.reshape(-1, 1).to(dtype) * omega
156
+ xx = xx.reshape(-1, 1).to(dtype) * omega
157
+ pos = torch.cat([torch.sin(yy), torch.cos(yy), torch.sin(xx), torch.cos(xx)], dim=1)
158
+ if pos.shape[1] < channels:
159
+ pad = torch.zeros(pos.shape[0], channels - pos.shape[1], device=device, dtype=dtype)
160
+ pos = torch.cat([pos, pad], dim=1)
161
+ return tokens + pos[:, :channels].unsqueeze(0)
162
+
163
+ @classmethod
164
+ def _feature_to_tokens(cls, feat: Tensor, target_size: tuple[int, int] | None = None) -> Tensor:
165
+ if target_size is not None and feat.shape[-2:] != target_size:
166
+ feat = F.interpolate(feat, size=target_size, mode="bilinear", align_corners=False)
167
+ _, _, h, w = feat.shape
168
+ tokens = feat.flatten(2).transpose(1, 2).contiguous()
169
+ return cls._add_2d_sincos_pos_embed(tokens, h, w)
170
+
171
+ @staticmethod
172
+ def _tokens_to_map(tokens: Tensor, h: int, w: int) -> Tensor:
173
+ return tokens.transpose(1, 2).reshape(tokens.shape[0], tokens.shape[-1], h, w).contiguous()
174
+
175
+ def forward(self, vit_map: Tensor, convnext_feats: tuple[Tensor, Tensor, Tensor, Tensor]) -> dict[str, Tensor]:
176
+ c4, c8, c16, c32 = convnext_feats
177
+ if self.semantic_grid_size is not None and vit_map.shape[-2:] != self.semantic_grid_size:
178
+ vit_map = F.interpolate(vit_map, size=self.semantic_grid_size, mode="bilinear", align_corners=False)
179
+ h, w = vit_map.shape[-2:]
180
+ vit_tokens = self._feature_to_tokens(vit_map)
181
+
182
+ f4 = self.c4_attn(vit_tokens, self.c4_proj(self._feature_to_tokens(c4)))
183
+ f16 = self.c16_attn(vit_tokens, self.c16_proj(self._feature_to_tokens(c16)))
184
+ f32 = self.c32_attn(vit_tokens, self.c32_proj(self._feature_to_tokens(c32)))
185
+
186
+ weights = torch.softmax(self.semantic_logits, dim=0)
187
+ semantic_tokens = torch.cat(
188
+ [
189
+ vit_tokens * weights[0],
190
+ f4 * weights[1],
191
+ f16 * weights[2],
192
+ f32 * weights[3],
193
+ ],
194
+ dim=-1,
195
+ )
196
+ semantic_tokens = self.semantic_compress(semantic_tokens)
197
+ semantic = self._tokens_to_map(semantic_tokens, h, w)
198
+
199
+ detail_tokens = self.detail_proj(self._feature_to_tokens(c8, target_size=self.detail_grid_size))
200
+ detail = self._tokens_to_map(detail_tokens, self.detail_grid_size[0], self.detail_grid_size[1])
201
+ detail_down = F.interpolate(detail, size=semantic.shape[-2:], mode="bilinear", align_corners=False)
202
+ detail_down = self.detail_to_semantic(detail_down)
203
+
204
+ gate = self.fusion_gate(torch.cat([semantic, detail_down], dim=1))
205
+ fused = self.output_smooth(semantic + gate * detail_down)
206
+ self._last_fusion_weights = weights.detach()
207
+ return {
208
+ "fused": fused,
209
+ "semantic": semantic,
210
+ "detail": detail,
211
+ "fusion_weights": weights,
212
+ }
213
+
214
+
215
  class DINOv3ViTConvNeXtBackbone(nn.Module):
216
  """Dual-branch DINOv3 backbone with explicit partial unfreezing.
217
 
 
245
  )
246
  self.vit_channels = int(getattr(self.vit, "embed_dim"))
247
  self.convnext_channels = int(getattr(self.convnext, "embed_dim"))
248
+ self.convnext_dims = tuple(int(v) for v in getattr(self.convnext, "embed_dims"))
249
  self.out_channels = out_channels
250
 
251
+ self.fusion = CoastStyleMultiScaleFusion(
252
+ vit_dim=self.vit_channels,
253
+ convnext_dims=self.convnext_dims,
254
+ out_channels=out_channels,
255
+ semantic_grid_size=None,
256
+ detail_grid_size=(28, 28),
 
 
 
 
 
 
 
 
257
  )
258
 
259
  self.apply_finetune_policy(vit_unfrozen_blocks, convnext_unfrozen_stages)
260
 
261
  def apply_finetune_policy(self, vit_unfrozen_blocks: int = 2, convnext_unfrozen_stages: int = 2) -> None:
262
  self.requires_grad_(False)
 
 
263
  self.fusion.requires_grad_(True)
264
  self._unfreeze_vit_tail(vit_unfrozen_blocks)
265
  self._unfreeze_convnext_tail(convnext_unfrozen_stages)
 
296
 
297
  def forward(self, x: Tensor) -> dict[str, Tensor]:
298
  vit_map = self._vit_feature_map(x)
299
+ convnext_feats = self._convnext_pyramid(x)
300
+ fused_outputs = self.fusion(vit_map, convnext_feats)
 
 
 
301
  return {
302
+ "fused": fused_outputs["fused"],
303
+ "semantic": fused_outputs["semantic"],
304
+ "detail": fused_outputs["detail"],
305
+ "vit": vit_map,
306
+ "convnext_pyramid": convnext_feats,
307
+ "fusion_weights": fused_outputs["fusion_weights"],
308
  }
309
 
310
  def _vit_feature_map(self, x: Tensor) -> Tensor:
 
314
  w = x.shape[-1] // int(getattr(self.vit, "patch_size"))
315
  return patch_tokens.transpose(1, 2).reshape(x.shape[0], self.vit_channels, h, w)
316
 
317
+ def _convnext_pyramid(self, x: Tensor) -> tuple[Tensor, Tensor, Tensor, Tensor]:
318
+ feats = []
319
+ y = x
320
+ for downsample, stage in zip(self.convnext.downsample_layers, self.convnext.stages):
321
+ y = downsample(y)
322
+ y = stage(y)
323
+ feats.append(y)
324
+ return tuple(feats[:4])
325
 
326
  def trainable_summary(self) -> TrainableBackboneSummary:
327
  trainable_names = [name for name, param in self.named_parameters() if param.requires_grad]