thenukegun10x commited on
Commit
1f9a329
·
verified ·
1 Parent(s): 12db9dc

Update src\models\plant_vit_v2.py

Browse files
Files changed (1) hide show
  1. src/models/plant_vit_v2.py +295 -0
src/models/plant_vit_v2.py ADDED
@@ -0,0 +1,295 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ PlantViT v2 Architecture (PlantDetect v2.0):
3
+ 1. DINOv3 ViT-Base Backbone @ 448x448 High-Res (784 patch tokens).
4
+ 2. Multi-Layer Feature Pyramid Fusion (Block 8 Micro-Texture + Block 11 Macro-Semantics) — CLS excluded.
5
+ 3. Pure SwiGLU Mixture-of-Experts (MoESwiGLUv2 with Gating Residuals & Context Projection).
6
+ - v1 heterogeneous: 16 FFN + 1 zero + 1 copy + 2 const = 20 total (moe_vit.py)
7
+ - v2 pure: 16 SwiGLU experts (d_ff=512) configurable via --num-ffn 8/16/20, stacked 1-2 layers
8
+ 4. Sub-Center ArcFace Metric Head (K=3 Phenology Centers per Class) with diversity regularizer.
9
+ 5. Hierarchical Multi-Task Taxonomic Classification Heads (Species + Genus + Family) — Unknown_Family fallback.
10
+ """
11
+ import torch
12
+ import torch.nn as nn
13
+ import torch.nn.functional as F
14
+ import timm
15
+
16
+ from .moe_swiglu_v2 import MoESwiGLUv2
17
+ from .subcenter_arcface import SubCenterArcFace
18
+
19
+ class MultiLayerFeatureFusion(nn.Module):
20
+ def __init__(self, dim: int = 768, out_dim: int = 512, use_attn_pool: bool = True):
21
+ super().__init__()
22
+ self.dim = dim
23
+ self.out_dim = out_dim
24
+ self.use_attn_pool = use_attn_pool
25
+
26
+ self.norm_mid = nn.LayerNorm(dim)
27
+ self.norm_deep = nn.LayerNorm(dim)
28
+
29
+ # Attention pooling (learned query) retains spatial signal vs pure mean — P1 fix
30
+ if use_attn_pool:
31
+ self.pool_q_mid = nn.Parameter(torch.randn(dim) * (dim ** -0.5))
32
+ self.pool_q_deep = nn.Parameter(torch.randn(dim) * (dim ** -0.5))
33
+ self.scale = dim ** -0.5
34
+ else:
35
+ self.pool_q_mid = None
36
+ self.pool_q_deep = None
37
+
38
+ self.fusion_proj = nn.Sequential(
39
+ nn.Linear(dim * 2, out_dim),
40
+ nn.GELU(),
41
+ nn.LayerNorm(out_dim),
42
+ nn.Linear(out_dim, out_dim),
43
+ nn.LayerNorm(out_dim)
44
+ )
45
+
46
+ def _pool(self, tokens: torch.Tensor, q: torch.Tensor | None) -> torch.Tensor:
47
+ # tokens [B,T,D], q [D]
48
+ if q is not None and self.use_attn_pool:
49
+ # Attentive pooling: w = softmax(tokens @ q / sqrt(D))
50
+ attn = (tokens @ q) * self.scale # [B,T]
51
+ w = torch.softmax(attn, dim=1) # [B,T]
52
+ pooled = (tokens * w.unsqueeze(-1)).sum(dim=1) # [B,D]
53
+ return pooled
54
+ return tokens.mean(dim=1)
55
+
56
+ def forward(self, mid_tokens: torch.Tensor, deep_tokens: torch.Tensor) -> torch.Tensor:
57
+ # mid_tokens / deep_tokens are PATCH tokens only (CLS excluded by caller).
58
+ mid_pooled = self.norm_mid(self._pool(mid_tokens, self.pool_q_mid))
59
+ deep_pooled = self.norm_deep(self._pool(deep_tokens, self.pool_q_deep))
60
+
61
+ fused = torch.cat([mid_pooled, deep_pooled], dim=-1)
62
+ emb = self.fusion_proj(fused)
63
+ return F.normalize(emb, dim=-1)
64
+
65
+ class PlantViTv2(nn.Module):
66
+ def __init__(
67
+ self,
68
+ stem_name: str = "vit_base_patch16_dinov3.lvd1689m",
69
+ img_size: int = 448,
70
+ n_species: int = 1000,
71
+ n_genera: int = 358,
72
+ n_families: int = 80,
73
+ embed_dim: int = 512,
74
+ use_moe: bool = True,
75
+ num_ffn: int = 16,
76
+ d_ff: int = 512,
77
+ top_k: int = 2,
78
+ k_subcenters: int = 3,
79
+ pretrained: bool = True,
80
+ freeze_stem: bool = False,
81
+ num_moe_layers: int = 1,
82
+ enable_grad_checkpoint: bool = False,
83
+ shared_experts: int = 0,
84
+ granularity_factor: int = 1,
85
+ ):
86
+ super().__init__()
87
+ self.stem_name = stem_name
88
+ self.img_size = img_size
89
+ self.n_species = n_species
90
+ self.n_genera = n_genera
91
+ self.n_families = n_families
92
+ self.embed_dim = embed_dim
93
+ self.use_moe = use_moe
94
+
95
+ # 1. DINOv3 Backbone
96
+ stem_model_name = stem_name if stem_name.startswith("vit_") else f"{stem_name}.lvd1689m"
97
+ self.stem = timm.create_model(
98
+ stem_model_name,
99
+ pretrained=pretrained,
100
+ img_size=img_size,
101
+ num_classes=0,
102
+ dynamic_img_size=True
103
+ )
104
+ self.dim = self.stem.embed_dim
105
+ # Number of prefix tokens (CLS + registers) to exclude from patch averaging.
106
+ # v1 correctly excluded via n_prefix; v2 previously averaged CLS into fusion.
107
+ self.n_prefix = int(getattr(self.stem, "num_prefix_tokens", 1))
108
+
109
+ if freeze_stem:
110
+ for p in self.stem.parameters():
111
+ p.requires_grad = False
112
+
113
+ # Real grad checkpointing for 448px (784+5 tok vs 441 at 336). timm DINOv3 has no set_grad_checkpointing,
114
+ # so we checkpoint forward_intermediates via torch.utils.checkpoint when training.
115
+ self.enable_grad_checkpoint = bool(enable_grad_checkpoint)
116
+ # Try timm native if available (harmless), but our manual checkpoint below is the real saving.
117
+ if self.enable_grad_checkpoint and hasattr(self.stem, "set_grad_checkpointing"):
118
+ try:
119
+ self.stem.set_grad_checkpointing(True)
120
+ except Exception:
121
+ pass
122
+
123
+ # Granularity: DeepSeekMoE-style fine split — keep FLOPs ~const (num*d_ff)
124
+ # factor=2: 16×512 → 32×256, factor=4: 64×128 — cheap sweep for 3k species
125
+ self.shared_experts = int(shared_experts)
126
+ self.granularity_factor = int(granularity_factor) if granularity_factor and granularity_factor >= 1 else 1
127
+ eff_num_ffn = num_ffn * self.granularity_factor
128
+ eff_d_ff = max(64, d_ff // self.granularity_factor) # clamp to keep matrix sane
129
+ if self.granularity_factor > 1:
130
+ print(
131
+ f"[PlantViTv2] granularity x{self.granularity_factor}: {num_ffn}x{d_ff} -> {eff_num_ffn}x{eff_d_ff} (FLOPs ~const)",
132
+ flush=True,
133
+ )
134
+
135
+ # 2. Pure SwiGLU MoE Layer(s) — configurable stacking restores depth lost from v1's 2 blocks
136
+ self.num_moe_layers = int(num_moe_layers) if use_moe else 0
137
+ self.eff_num_ffn = eff_num_ffn
138
+ self.eff_d_ff = eff_d_ff
139
+ if self.use_moe and self.num_moe_layers > 0:
140
+ self.moe_layers = nn.ModuleList(
141
+ [
142
+ MoESwiGLUv2(
143
+ d_model=self.dim,
144
+ d_ff=eff_d_ff,
145
+ num_ffn=eff_num_ffn,
146
+ top_k=top_k,
147
+ gating_residual=True,
148
+ gate_ctx=True,
149
+ shared_experts=self.shared_experts,
150
+ )
151
+ for _ in range(self.num_moe_layers)
152
+ ]
153
+ )
154
+ self.moe_norms = nn.ModuleList([nn.LayerNorm(self.dim) for _ in range(self.num_moe_layers)])
155
+ # Backward compat: single-layer alias used by older checkpoints
156
+ self.moe_layer = self.moe_layers[0] if self.num_moe_layers == 1 else None
157
+ self.moe_norm = self.moe_norms[0] if self.num_moe_layers == 1 else None
158
+ else:
159
+ self.moe_layers = None
160
+ self.moe_norms = None
161
+ self.moe_layer = None
162
+ self.moe_norm = None
163
+
164
+ # 3. Multi-Layer Feature Fusion (Block 8 + Block 11)
165
+ self.fusion = MultiLayerFeatureFusion(dim=self.dim, out_dim=embed_dim)
166
+
167
+ # 4. Multi-Task Taxonomic Heads
168
+ self.head_species = nn.Linear(embed_dim, n_species, bias=False)
169
+ self.head_genus = nn.Linear(embed_dim, n_genera, bias=False)
170
+ self.head_family = nn.Linear(embed_dim, n_families, bias=False)
171
+
172
+ # 5. Sub-Center ArcFace Metric Head (K=3 Sub-Centers)
173
+ self.subcenter_arcface = SubCenterArcFace(
174
+ embed_dim=embed_dim,
175
+ n_classes=n_species,
176
+ k_subcenters=k_subcenters,
177
+ m=0.5,
178
+ s=30.0
179
+ )
180
+
181
+ def _to_tokens(self, feat: torch.Tensor) -> torch.Tensor:
182
+ """Converts [B, C, H, W] or [B, T, C] to standard [B, T, C]."""
183
+ if feat.dim() == 4:
184
+ return feat.flatten(2).transpose(1, 2)
185
+ return feat
186
+
187
+ def forward_features(self, x: torch.Tensor):
188
+ # Manual checkpointing for DINOv3 (timm has no set_grad_checkpointing)
189
+ if self.enable_grad_checkpoint and self.training:
190
+ # checkpoint expects pure function; wrap forward_intermediates
191
+ def _run_stem(inp):
192
+ return self.stem.forward_intermediates(inp, indices=[7, 11])
193
+
194
+ intermediates, final_feat = torch.utils.checkpoint.checkpoint(
195
+ _run_stem, x, use_reentrant=False
196
+ )
197
+ else:
198
+ intermediates, final_feat = self.stem.forward_intermediates(x, indices=[7, 11])
199
+
200
+ if isinstance(intermediates, (list, tuple)):
201
+ mid_raw = intermediates[0]
202
+ deep_raw = intermediates[1] if len(intermediates) > 1 else final_feat
203
+ else:
204
+ mid_raw = intermediates
205
+ deep_raw = final_feat
206
+
207
+ if isinstance(mid_raw, (list, tuple)): mid_raw = mid_raw[0]
208
+ if isinstance(deep_raw, (list, tuple)): deep_raw = deep_raw[0]
209
+
210
+ mid_tokens = self._to_tokens(mid_raw)
211
+ deep_tokens = self._to_tokens(deep_raw)
212
+
213
+ aux_loss = torch.tensor(0.0, device=x.device)
214
+ intra_loss = torch.tensor(0.0, device=x.device)
215
+ cross_loss = torch.tensor(0.0, device=x.device)
216
+ gate_logits = None
217
+ gate_weights_list: list[torch.Tensor] = []
218
+ if self.use_moe and self.moe_layers is not None and len(self.moe_layers) > 0:
219
+ prev_gate = None
220
+ total_aux = 0.0
221
+ total_intra = 0.0
222
+ for norm, moe in zip(self.moe_norms, self.moe_layers):
223
+ ctx = deep_tokens[:, 0] # CLS as global context
224
+ ret = moe(norm(deep_tokens), prev_gate=prev_gate, ctx=ctx)
225
+ # Backward compat: MoESwiGLUv2 now returns 5 (with intra + gate_weights), old 3
226
+ if len(ret) == 5:
227
+ moe_out, aux, gate_logits, intra, gate_weights = ret
228
+ elif len(ret) == 4:
229
+ moe_out, aux, gate_logits, intra = ret
230
+ gate_weights = torch.softmax(gate_logits, dim=-1)
231
+ else:
232
+ moe_out, aux, gate_logits = ret
233
+ intra = torch.tensor(0.0, device=x.device)
234
+ gate_weights = torch.softmax(gate_logits, dim=-1)
235
+ deep_tokens = deep_tokens + moe_out
236
+ total_aux = total_aux + aux
237
+ total_intra = total_intra + intra
238
+ gate_weights_list.append(gate_weights)
239
+ prev_gate = gate_logits
240
+ aux_loss = total_aux / max(1, len(self.moe_layers))
241
+ intra_loss = total_intra / max(1, len(self.moe_layers))
242
+ # Cross-layer joint routing coherence (Feb 2026): maximize joint Top-k confidence across depth
243
+ # For tokens, joint = max_prob(L) * max_prob(L+1); loss = -log(joint) encourages coherent pathways
244
+ if len(gate_weights_list) >= 2:
245
+ cross_terms = []
246
+ for a, b in zip(gate_weights_list[:-1], gate_weights_list[1:]):
247
+ # a,b [B,T,E] — per-token max prob
248
+ max_a = a.max(dim=-1).values.clamp(min=1e-6) # [B,T]
249
+ max_b = b.max(dim=-1).values.clamp(min=1e-6)
250
+ joint = (max_a * max_b).clamp(min=1e-6)
251
+ # Negative log likelihood of joint
252
+ cross_terms.append(-torch.log(joint).mean())
253
+ cross_loss = torch.stack(cross_terms).mean() if cross_terms else torch.tensor(0.0, device=x.device)
254
+ # Backward compat alias
255
+ if self.num_moe_layers == 1:
256
+ self.moe_layer = self.moe_layers[0]
257
+ self.moe_norm = self.moe_norms[0]
258
+
259
+ # Return intra + cross for trainer (add to aux_dict)
260
+ return mid_tokens, deep_tokens, aux_loss, gate_logits, intra_loss, cross_loss
261
+
262
+ def forward(self, x: torch.Tensor, y_species: torch.Tensor = None):
263
+ mid_tokens, deep_tokens, moe_aux, _, intra_loss, cross_loss = self.forward_features(x)
264
+
265
+ # 1. Multi-Layer Pyramid Fusion (Block 8 Texture + Block 11 Macro)
266
+ # Exclude CLS/registers from spatial averaging (fixes v2 bug where CLS diluted patch stats).
267
+ mid_patch = mid_tokens[:, self.n_prefix :] if mid_tokens.size(1) > self.n_prefix else mid_tokens
268
+ deep_patch = deep_tokens[:, self.n_prefix :] if deep_tokens.size(1) > self.n_prefix else deep_tokens
269
+ emb = self.fusion(mid_patch, deep_patch)
270
+
271
+ # 2. Multi-Task Linear Logits
272
+ logits_species = self.head_species(emb)
273
+ logits_genus = self.head_genus(emb)
274
+ logits_family = self.head_family(emb)
275
+
276
+ # 3. Sub-Center ArcFace Margin Logits — BF16 autocast (s=30) overflows in bf16, force FP32
277
+ # Keep BF16 for backbone/fusion (fast), drop to FP32 only here.
278
+ if emb.is_cuda:
279
+ with torch.amp.autocast("cuda", enabled=False):
280
+ arcface_logits = self.subcenter_arcface(emb.float(), y_species)
281
+ div_loss = self.subcenter_arcface.diversity_loss()
282
+ else:
283
+ arcface_logits = self.subcenter_arcface(emb.float(), y_species)
284
+ div_loss = self.subcenter_arcface.diversity_loss()
285
+
286
+ aux_dict = {"moe_aux": moe_aux, "div_loss": div_loss, "intra_loss": intra_loss, "cross_loss": cross_loss}
287
+
288
+ return (
289
+ logits_species,
290
+ logits_genus,
291
+ logits_family,
292
+ arcface_logits,
293
+ emb,
294
+ aux_dict
295
+ )