mb-cdar 0.3.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mb_cdar/__init__.py +3 -0
- mb_cdar/__main__.py +6 -0
- mb_cdar/_archive_v2/backbone/mb_cdar_backbone.py +379 -0
- mb_cdar/_archive_v2/data/augmentation.py +326 -0
- mb_cdar/_archive_v2/diffusion/schedules.py +43 -0
- mb_cdar/_archive_v2/loss/beta_diversity.py +96 -0
- mb_cdar/_archive_v2/loss/mmd_loss.py +60 -0
- mb_cdar/backbone/__init__.py +11 -0
- mb_cdar/backbone/discriminator.py +120 -0
- mb_cdar/backbone/unet.py +288 -0
- mb_cdar/backbone/vae.py +124 -0
- mb_cdar/cli.py +396 -0
- mb_cdar/cond/__init__.py +10 -0
- mb_cdar/data/__init__.py +44 -0
- mb_cdar/data/composition.py +433 -0
- mb_cdar/data/dataset.py +247 -0
- mb_cdar/data/image_coding.py +68 -0
- mb_cdar/data/normalizer.py +165 -0
- mb_cdar/data/phylogeny.py +241 -0
- mb_cdar/diffusion/__init__.py +11 -0
- mb_cdar/diffusion/ddpm_scheduler.py +67 -0
- mb_cdar/loss/__init__.py +8 -0
- mb_cdar/loss/ecology_loss.py +104 -0
- mb_cdar/reg/__init__.py +8 -0
- mb_cdar/train.py +195 -0
- mb_cdar/training/__init__.py +7 -0
- mb_cdar/training/sampler.py +404 -0
- mb_cdar/training/tail_calib.py +110 -0
- mb_cdar/training/trainer.py +796 -0
- mb_cdar-0.3.1.dist-info/METADATA +350 -0
- mb_cdar-0.3.1.dist-info/RECORD +35 -0
- mb_cdar-0.3.1.dist-info/WHEEL +5 -0
- mb_cdar-0.3.1.dist-info/entry_points.txt +2 -0
- mb_cdar-0.3.1.dist-info/licenses/LICENSE +21 -0
- mb_cdar-0.3.1.dist-info/top_level.txt +1 -0
mb_cdar/__init__.py
ADDED
mb_cdar/__main__.py
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
"""Continuous-only DiT backbone for MB-CDAR.
|
|
2
|
+
|
|
3
|
+
A Diffusion Transformer (DiT) that operates in ILR (Isometric Log-Ratio)
|
|
4
|
+
space for continuous compositional diffusion. Features:
|
|
5
|
+
|
|
6
|
+
1. 2-layer MLP numerical tokenisation for the continuous ILR channel.
|
|
7
|
+
2. Hand-written Multi-Head Attention with fused SDPA (memory-efficient at
|
|
8
|
+
D=1033 — avoids materialising the (B, nhead, D, D) score tensor).
|
|
9
|
+
3. AdaLN-Zero conditioning from the time embedding (and an optional external
|
|
10
|
+
condition embedding, e.g. simple categorical metadata).
|
|
11
|
+
4. Classifier-Free Guidance (CFG) condition dropout during training (10%
|
|
12
|
+
default) — the backbone learns both conditional and unconditional
|
|
13
|
+
trajectories.
|
|
14
|
+
5. Absolute per-taxon identity injection via a learnable ``taxon_id_embed``
|
|
15
|
+
that breaks permutation-equivariance over taxa (the precondition for
|
|
16
|
+
non-trivial Spearman R² on species co-occurrence).
|
|
17
|
+
|
|
18
|
+
The previous V1 architecture (PhyloBias attention bias, phylogenetic spectral
|
|
19
|
+
identity, CtrTab Laplace noise injection, CADD cross-attention, discrete
|
|
20
|
+
RADD track) has been removed. The phylogenetic tree is now used only for
|
|
21
|
+
UniFrac evaluation, not for model conditioning.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import math
|
|
27
|
+
|
|
28
|
+
import torch
|
|
29
|
+
import torch.nn as nn
|
|
30
|
+
import torch.nn.functional as F
|
|
31
|
+
from torch import Tensor
|
|
32
|
+
|
|
33
|
+
from mb_cdar.data.composition import helmert_contrast_matrix
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SinusoidalEmbedding(nn.Module):
|
|
37
|
+
"""Sinusoidal time embedding module."""
|
|
38
|
+
|
|
39
|
+
def __init__(self, d_model: int):
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.d_model = d_model
|
|
42
|
+
|
|
43
|
+
def forward(self, t: Tensor) -> Tensor:
|
|
44
|
+
device = t.device
|
|
45
|
+
half_dim = self.d_model // 2
|
|
46
|
+
emb = math.log(10000) / (half_dim - 1)
|
|
47
|
+
emb = torch.exp(torch.arange(half_dim, device=device, dtype=t.dtype) * -emb)
|
|
48
|
+
emb = t.unsqueeze(-1) * emb.unsqueeze(0)
|
|
49
|
+
emb = torch.cat((emb.sin(), emb.cos()), dim=-1)
|
|
50
|
+
return emb
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MBCDARBackbone(nn.Module):
|
|
54
|
+
"""Continuous-only DiT architecture operating in ILR space.
|
|
55
|
+
|
|
56
|
+
Parameters
|
|
57
|
+
----------
|
|
58
|
+
D : int
|
|
59
|
+
Number of OTUs (taxa).
|
|
60
|
+
d_model : int
|
|
61
|
+
DiT hidden dimension. Default 256.
|
|
62
|
+
nhead : int
|
|
63
|
+
Number of attention heads. Default 4.
|
|
64
|
+
num_layers : int
|
|
65
|
+
Number of DiT blocks. Default 8.
|
|
66
|
+
dropout : float
|
|
67
|
+
Dropout rate. Default 0.1.
|
|
68
|
+
id_dim : int
|
|
69
|
+
Dimensionality of the learnable per-taxon identity residual. Default
|
|
70
|
+
24. Set to 0 to disable (not recommended — permutation-equivariance
|
|
71
|
+
returns and Spearman R² collapses to ~0).
|
|
72
|
+
cond_dim : Optional[int]
|
|
73
|
+
Dimensionality of an external condition embedding (e.g. simple
|
|
74
|
+
categorical metadata projected to a fixed vector). When provided,
|
|
75
|
+
``cond_proj`` maps it to ``d_model`` and adds it to the time
|
|
76
|
+
condition. Default None (time-only conditioning).
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
def __init__(
|
|
80
|
+
self,
|
|
81
|
+
D: int = 1033,
|
|
82
|
+
d_model: int = 256,
|
|
83
|
+
nhead: int = 4,
|
|
84
|
+
num_layers: int = 8,
|
|
85
|
+
dropout: float = 0.1,
|
|
86
|
+
id_dim: int = 24,
|
|
87
|
+
cond_dim: int | None = None,
|
|
88
|
+
):
|
|
89
|
+
super().__init__()
|
|
90
|
+
self.D = D
|
|
91
|
+
self.K = D - 1 # ILR dimension
|
|
92
|
+
self.d_model = d_model
|
|
93
|
+
self.id_dim = id_dim
|
|
94
|
+
self.cond_dim = cond_dim
|
|
95
|
+
|
|
96
|
+
# Token embedding for Taxon space (shared across taxa — symmetric)
|
|
97
|
+
self.otu_embed = nn.Sequential(
|
|
98
|
+
nn.Linear(1, d_model),
|
|
99
|
+
nn.SiLU(),
|
|
100
|
+
nn.Linear(d_model, d_model),
|
|
101
|
+
nn.LayerNorm(d_model),
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# === Absolute per-taxon identity (learnable) =========================
|
|
105
|
+
# The shared ``otu_embed`` above is permutation-equivariant over taxa.
|
|
106
|
+
# We break that symmetry by adding a *unique* learnable identity vector
|
|
107
|
+
# per taxon, projected to d_model and added to the input embedding
|
|
108
|
+
# (broadcast over the batch). This is the necessary precondition for
|
|
109
|
+
# learning species-specific co-occurrence structure (Spearman R² > 0).
|
|
110
|
+
#
|
|
111
|
+
# The previous V1 architecture combined this with a *fixed*
|
|
112
|
+
# phylogenetic spectral prior; that has been removed (the tree is now
|
|
113
|
+
# used only for evaluation, not model conditioning). Identity comes
|
|
114
|
+
# entirely from the learnable code below.
|
|
115
|
+
if id_dim > 0:
|
|
116
|
+
self.taxon_id_embed = nn.Parameter(torch.randn(D, id_dim) * 0.02)
|
|
117
|
+
self.taxon_id_proj = nn.Linear(id_dim, d_model)
|
|
118
|
+
# ======================================================================
|
|
119
|
+
|
|
120
|
+
# Helmert contrast matrix cache for projections
|
|
121
|
+
self._V_np = helmert_contrast_matrix(D)
|
|
122
|
+
self._V_cache: dict[tuple[torch.device, torch.dtype], Tensor] = {}
|
|
123
|
+
|
|
124
|
+
# Time embedding
|
|
125
|
+
self.t_embed = SinusoidalEmbedding(d_model)
|
|
126
|
+
self.t_mlp = nn.Sequential(
|
|
127
|
+
nn.Linear(d_model, d_model),
|
|
128
|
+
nn.SiLU(),
|
|
129
|
+
nn.Linear(d_model, d_model),
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# External condition projection (e.g. simple categorical metadata).
|
|
133
|
+
# Projects an external condition embedding (cond_dim) to d_model so it
|
|
134
|
+
# can be added to the time-conditioning vector c. Zero-init the output
|
|
135
|
+
# layer so the condition starts as a no-op (DiT-style gradual onset).
|
|
136
|
+
if cond_dim is not None:
|
|
137
|
+
self.cond_proj = nn.Sequential(
|
|
138
|
+
nn.Linear(cond_dim, d_model),
|
|
139
|
+
nn.SiLU(),
|
|
140
|
+
nn.Linear(d_model, d_model),
|
|
141
|
+
)
|
|
142
|
+
nn.init.zeros_(self.cond_proj[-1].weight)
|
|
143
|
+
nn.init.zeros_(self.cond_proj[-1].bias)
|
|
144
|
+
|
|
145
|
+
# DiT blocks
|
|
146
|
+
self.blocks = nn.ModuleList([
|
|
147
|
+
DiTBlock(d_model, nhead, dropout=dropout)
|
|
148
|
+
for _ in range(num_layers)
|
|
149
|
+
])
|
|
150
|
+
|
|
151
|
+
# Output — final AdaLN-Zero modulation applied just before the output
|
|
152
|
+
# LayerNorm. SiLU + Linear(d → 2d) producing (shift, scale); zero-init
|
|
153
|
+
# so the network starts as identity → final_norm only.
|
|
154
|
+
self.final_adaln = nn.Sequential(
|
|
155
|
+
nn.SiLU(),
|
|
156
|
+
nn.Linear(d_model, 2 * d_model),
|
|
157
|
+
)
|
|
158
|
+
nn.init.zeros_(self.final_adaln[1].weight)
|
|
159
|
+
nn.init.zeros_(self.final_adaln[1].bias)
|
|
160
|
+
|
|
161
|
+
self.final_norm = nn.LayerNorm(d_model)
|
|
162
|
+
self.out_proj = nn.Sequential(
|
|
163
|
+
nn.Linear(d_model, d_model),
|
|
164
|
+
nn.SiLU(),
|
|
165
|
+
nn.Linear(d_model, 1),
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
def _get_V(self, ref: Tensor) -> Tensor:
|
|
169
|
+
key = (ref.device, ref.dtype)
|
|
170
|
+
if key not in self._V_cache:
|
|
171
|
+
self._V_cache[key] = torch.from_numpy(self._V_np).to(
|
|
172
|
+
device=ref.device, dtype=ref.dtype
|
|
173
|
+
)
|
|
174
|
+
return self._V_cache[key]
|
|
175
|
+
|
|
176
|
+
def _taxon_identity(self) -> Tensor | None:
|
|
177
|
+
"""Absolute per-taxon identity embedding, shape (D, d_model) or None."""
|
|
178
|
+
if self.id_dim > 0:
|
|
179
|
+
return self.taxon_id_proj(self.taxon_id_embed) # (D, d_model)
|
|
180
|
+
return None
|
|
181
|
+
|
|
182
|
+
def forward(
|
|
183
|
+
self,
|
|
184
|
+
x_ilr: Tensor,
|
|
185
|
+
t: Tensor,
|
|
186
|
+
cond_emb: Tensor | None = None,
|
|
187
|
+
cond_dropout_prob: float = 0.1,
|
|
188
|
+
) -> Tensor:
|
|
189
|
+
"""Forward pass.
|
|
190
|
+
|
|
191
|
+
Parameters
|
|
192
|
+
----------
|
|
193
|
+
x_ilr : (B, D-1) Tensor
|
|
194
|
+
ILR-transformed compositions.
|
|
195
|
+
t : (B,) Tensor
|
|
196
|
+
Diffusion time step.
|
|
197
|
+
cond_emb : Optional[Tensor]
|
|
198
|
+
External condition embedding, shape (B, cond_dim). When provided
|
|
199
|
+
it is projected to ``d_model`` and added to the time-conditioning
|
|
200
|
+
vector ``c``. Default None (time-only conditioning).
|
|
201
|
+
cond_dropout_prob : float
|
|
202
|
+
Probability of dropping the condition (CFG training). Set to 0
|
|
203
|
+
to disable (e.g. at inference). Default 0.1.
|
|
204
|
+
|
|
205
|
+
Returns
|
|
206
|
+
-------
|
|
207
|
+
noise_pred : (B, D-1) Tensor
|
|
208
|
+
"""
|
|
209
|
+
# 1. Project ILR to taxon space
|
|
210
|
+
V = self._get_V(x_ilr)
|
|
211
|
+
x_taxon = torch.matmul(x_ilr, V.t()) # (B, D)
|
|
212
|
+
|
|
213
|
+
# 2. Embed taxon coordinates + inject absolute per-taxon identity.
|
|
214
|
+
# The identity (D, d_model) breaks permutation-equivariance so the
|
|
215
|
+
# network can learn species-specific structure (Spearman R² fix).
|
|
216
|
+
x = self.otu_embed(x_taxon.unsqueeze(-1)) # (B, D, d_model)
|
|
217
|
+
ident = self._taxon_identity()
|
|
218
|
+
if ident is not None:
|
|
219
|
+
x = x + ident.unsqueeze(0) # broadcast (1, D, d_model) → (B, D, d_model)
|
|
220
|
+
|
|
221
|
+
# 3. Time embedding (the condition c)
|
|
222
|
+
c = self.t_mlp(self.t_embed(t)) # (B, d_model)
|
|
223
|
+
|
|
224
|
+
# External condition (e.g. simple categorical metadata) fused into c.
|
|
225
|
+
# cond_emb is projected to d_model and added to the time condition.
|
|
226
|
+
cond_signal = None
|
|
227
|
+
if cond_emb is not None and hasattr(self, 'cond_proj'):
|
|
228
|
+
cond_signal = self.cond_proj(cond_emb)
|
|
229
|
+
|
|
230
|
+
# CFG condition dropout: zero out the condition projection (NOT the
|
|
231
|
+
# time embedding!) on a random subset of the batch during training so
|
|
232
|
+
# the network learns both conditional and unconditional trajectories.
|
|
233
|
+
# The unconditional trajectory uses c = t_emb only (no condition
|
|
234
|
+
# signal), matching what is passed at inference time when
|
|
235
|
+
# cond_emb=None.
|
|
236
|
+
if self.training and cond_dropout_prob > 0 and cond_signal is not None:
|
|
237
|
+
drop_mask = torch.rand(c.shape[0], device=c.device) < cond_dropout_prob
|
|
238
|
+
cond_signal = torch.where(
|
|
239
|
+
drop_mask.unsqueeze(-1),
|
|
240
|
+
torch.zeros_like(cond_signal),
|
|
241
|
+
cond_signal,
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
if cond_signal is not None:
|
|
245
|
+
c = c + cond_signal
|
|
246
|
+
|
|
247
|
+
# 4. DiT blocks (no phylo attention bias — removed in V1 cleanup)
|
|
248
|
+
for block in self.blocks:
|
|
249
|
+
x = block(x, c) # (B, D, d_model)
|
|
250
|
+
|
|
251
|
+
# 5. Final AdaLN-Zero modulation + output projection back to ILR
|
|
252
|
+
# noise/score. c is (B, d_model) and x is (B, D, d_model), so the
|
|
253
|
+
# shift/scale are unsqueezed to (B, 1, d_model) for broadcasting.
|
|
254
|
+
final_shift, final_scale = self.final_adaln(c).chunk(2, dim=-1)
|
|
255
|
+
x = self.final_norm(x) * (1 + final_scale.unsqueeze(1)) + final_shift.unsqueeze(1)
|
|
256
|
+
score_taxon = self.out_proj(x).squeeze(-1) # (B, D)
|
|
257
|
+
noise_pred = torch.matmul(score_taxon, V) # (B, K)
|
|
258
|
+
|
|
259
|
+
return noise_pred
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
# =====================================================================
|
|
263
|
+
# DiTBlock — DiT block with adaLN modulation
|
|
264
|
+
# =====================================================================
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
class DiTBlock(nn.Module):
|
|
268
|
+
"""DiT block with adaLN modulation.
|
|
269
|
+
|
|
270
|
+
Components:
|
|
271
|
+
norm1, MultiheadAttention(d_model, nhead, dropout)
|
|
272
|
+
norm2, MLP(d → 4d → d) with GELU + Dropout
|
|
273
|
+
adaln: SiLU + Linear(d → 6d) 生成 6 个调制参数
|
|
274
|
+
|
|
275
|
+
Parameters
|
|
276
|
+
----------
|
|
277
|
+
d_model : int
|
|
278
|
+
Model hidden dimension.
|
|
279
|
+
nhead : int
|
|
280
|
+
Number of attention heads.
|
|
281
|
+
dropout : float
|
|
282
|
+
Dropout rate.
|
|
283
|
+
"""
|
|
284
|
+
|
|
285
|
+
def __init__(
|
|
286
|
+
self,
|
|
287
|
+
d_model: int = 256,
|
|
288
|
+
nhead: int = 4,
|
|
289
|
+
dropout: float = 0.1,
|
|
290
|
+
):
|
|
291
|
+
super().__init__()
|
|
292
|
+
self.d_model = d_model
|
|
293
|
+
self.nhead = nhead
|
|
294
|
+
self.head_dim = d_model // nhead
|
|
295
|
+
assert d_model % nhead == 0
|
|
296
|
+
|
|
297
|
+
# Self-attention QKV (attention itself uses fused SDPA in forward)
|
|
298
|
+
self.W_qkv = nn.Linear(d_model, 3 * d_model)
|
|
299
|
+
self.out_proj = nn.Linear(d_model, d_model)
|
|
300
|
+
self.attn_dropout_p = dropout # applied inside SDPA during training
|
|
301
|
+
self.resid_dropout = nn.Dropout(dropout)
|
|
302
|
+
|
|
303
|
+
self.norm1 = nn.LayerNorm(d_model)
|
|
304
|
+
|
|
305
|
+
# Feedforward MLP
|
|
306
|
+
self.mlp = nn.Sequential(
|
|
307
|
+
nn.Linear(d_model, 4 * d_model),
|
|
308
|
+
nn.GELU(),
|
|
309
|
+
nn.Dropout(dropout),
|
|
310
|
+
nn.Linear(4 * d_model, d_model),
|
|
311
|
+
nn.Dropout(dropout),
|
|
312
|
+
)
|
|
313
|
+
self.norm2 = nn.LayerNorm(d_model)
|
|
314
|
+
|
|
315
|
+
# adaLN: SiLU + Linear(d → 6d) 生成 6 个调制参数 (DiT-style).
|
|
316
|
+
# SiLU gate before the projection matches the DiT architecture; the
|
|
317
|
+
# Linear is zero-initialised so the network starts as identity (all
|
|
318
|
+
# shifts/scales/gates = 0 → modulation is a no-op at init).
|
|
319
|
+
self.adaln = nn.Sequential(nn.SiLU(), nn.Linear(d_model, 6 * d_model))
|
|
320
|
+
nn.init.zeros_(self.adaln[1].weight)
|
|
321
|
+
nn.init.zeros_(self.adaln[1].bias)
|
|
322
|
+
|
|
323
|
+
def forward(self, x: Tensor, c: Tensor) -> Tensor:
|
|
324
|
+
"""Forward pass.
|
|
325
|
+
|
|
326
|
+
Parameters
|
|
327
|
+
----------
|
|
328
|
+
x : (B, K, d_model) Tensor
|
|
329
|
+
Input sequence.
|
|
330
|
+
c : (B, d_model) Tensor
|
|
331
|
+
Condition vector (time embedding, optionally with cond signal).
|
|
332
|
+
|
|
333
|
+
Returns
|
|
334
|
+
-------
|
|
335
|
+
(B, K, d_model) Tensor
|
|
336
|
+
"""
|
|
337
|
+
B, K, C = x.shape
|
|
338
|
+
|
|
339
|
+
# --- adaLN modulation parameters ---
|
|
340
|
+
# 6 params: shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp
|
|
341
|
+
params = self.adaln(c) # (B, 6*d_model)
|
|
342
|
+
params = params.unsqueeze(1) # (B, 1, 6*d_model)
|
|
343
|
+
shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = \
|
|
344
|
+
params.chunk(6, dim=-1) # each (B, 1, d_model)
|
|
345
|
+
|
|
346
|
+
# --- Self-Attention ---
|
|
347
|
+
# Modulate input
|
|
348
|
+
h = self.norm1(x)
|
|
349
|
+
h = h * (1 + scale_msa) + shift_msa
|
|
350
|
+
|
|
351
|
+
# QKV projection
|
|
352
|
+
qkv = self.W_qkv(h) # (B, K, 3*C)
|
|
353
|
+
qkv = qkv.reshape(B, K, 3, self.nhead, self.head_dim)
|
|
354
|
+
qkv = qkv.permute(2, 0, 3, 1, 4) # (3, B, nhead, K, head_dim)
|
|
355
|
+
q, k, v = qkv.unbind(0)
|
|
356
|
+
|
|
357
|
+
# Fused scaled dot-product attention. SDPA computes
|
|
358
|
+
# softmax(QKᵀ/√d)·V without ever materialising the (B, nhead, K, K)
|
|
359
|
+
# score tensor — essential at K=1033 (the hand-written path allocated
|
|
360
|
+
# ~0.5 GB/layer here). SDPA's default scale is 1/√head_dim.
|
|
361
|
+
attn_out = F.scaled_dot_product_attention(
|
|
362
|
+
q, k, v,
|
|
363
|
+
dropout_p=self.attn_dropout_p if self.training else 0.0,
|
|
364
|
+
) # (B, nhead, K, head_dim)
|
|
365
|
+
attn_out = attn_out.transpose(1, 2).reshape(B, K, C)
|
|
366
|
+
attn_out = self.resid_dropout(self.out_proj(attn_out))
|
|
367
|
+
|
|
368
|
+
# Gate + residual
|
|
369
|
+
x = x + gate_msa * attn_out
|
|
370
|
+
|
|
371
|
+
# --- MLP ---
|
|
372
|
+
h = self.norm2(x)
|
|
373
|
+
h = h * (1 + scale_mlp) + shift_mlp
|
|
374
|
+
mlp_out = self.mlp(h)
|
|
375
|
+
|
|
376
|
+
# Gate + residual
|
|
377
|
+
x = x + gate_mlp * mlp_out
|
|
378
|
+
|
|
379
|
+
return x
|