Skip to content

Commit 04dec65

Browse files
committed
Formatting fixes using Black
Adds tests for overlapping context bonds, and a case with three modalities. context_bonds and dim_cond are now pytest.parametrize arguments instead of hardcoded class attributes.
1 parent a409f5b commit 04dec65

3 files changed

Lines changed: 141 additions & 81 deletions

File tree

cmonge/models/nn.py

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -324,7 +324,11 @@ class ConditionalPerturbationNetwork(BasePotential):
324324

325325
@nn.compact
326326
def __call__(
327-
self, x: jnp.ndarray, c: jnp.ndarray, num_contexts: int = 2, deterministic: bool = True
327+
self,
328+
x: jnp.ndarray,
329+
c: jnp.ndarray,
330+
num_contexts: int = 2,
331+
deterministic: bool = True,
328332
) -> jnp.ndarray: # noqa: D102
329333
"""
330334
Args:
@@ -387,25 +391,37 @@ def __call__(
387391
stacked_embeddings = jnp.stack(embeddings, axis=1) # (Batch, N, Dim)
388392

389393
# Input Dropout
390-
stacked_embeddings = nn.Dropout(rate=self.dropout_rate, deterministic=deterministic)(stacked_embeddings)
394+
stacked_embeddings = nn.Dropout(
395+
rate=self.dropout_rate, deterministic=deterministic
396+
)(stacked_embeddings)
391397

392398
# Multi-Head Attention Scores
393-
att_layer = nn.Dense(self.num_heads, use_bias=True, name="AttentionScores")
399+
att_layer = nn.Dense(
400+
self.num_heads, use_bias=True, name="AttentionScores"
401+
)
394402
scores = att_layer(stacked_embeddings) # (Batch, N, Heads)
395403
weights = jax.nn.softmax(scores, axis=1)
396404

397405
# Attention Weights Dropout
398-
weights = nn.Dropout(rate=self.dropout_rate, deterministic=deterministic)(weights)
406+
weights = nn.Dropout(
407+
rate=self.dropout_rate, deterministic=deterministic
408+
)(weights)
399409

400410
# Weighted Pooling: (B, N, D), (B, N, H) -> (B, H, D)
401-
weighted_sum = jnp.einsum('bnd,bnh->bhd', stacked_embeddings, weights)
411+
weighted_sum = jnp.einsum("bnd,bnh->bhd", stacked_embeddings, weights)
402412

403413
# Flatten and Project
404-
cond_embedding = weighted_sum.reshape(weighted_sum.shape[0], -1) # (B, H*D)
405-
cond_embedding = nn.Dense(dim_cond_map[0], use_bias=True, name="AttentionOutput")(cond_embedding)
414+
cond_embedding = weighted_sum.reshape(
415+
weighted_sum.shape[0], -1
416+
) # (B, H*D)
417+
cond_embedding = nn.Dense(
418+
dim_cond_map[0], use_bias=True, name="AttentionOutput"
419+
)(cond_embedding)
406420

407421
# Output Dropout
408-
cond_embedding = nn.Dropout(rate=self.dropout_rate, deterministic=deterministic)(cond_embedding)
422+
cond_embedding = nn.Dropout(
423+
rate=self.dropout_rate, deterministic=deterministic
424+
)(cond_embedding)
409425
else:
410426
# Average along stacked dimension (alternatives like summing are possible)
411427
cond_embedding = jnp.mean(jnp.stack(embeddings), axis=0)
@@ -434,7 +450,7 @@ def create_train_state(
434450

435451
# Split rng for dropout keys during init
436452
rng, rng_dropout = jax.random.split(rng)
437-
init_rngs = {'params': rng, 'dropout': rng_dropout}
453+
init_rngs = {"params": rng, "dropout": rng_dropout}
438454

439455
params = self.init(init_rngs, x=x, c=c)["params"]
440456
return PotentialTrainState.create(
Lines changed: 115 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -1,104 +1,148 @@
1+
import pytest
12
import jax
23
import jax.numpy as jnp
3-
import optax
44

55
from cmonge.models.nn import ConditionalPerturbationNetwork
66

7+
# (context_bonds, dim_cond, num_contexts)
8+
CONTEXT_BOND_CONFIGS = [
9+
pytest.param(
10+
((0, 10), (10, 20)),
11+
20,
12+
2,
13+
id="non_overlapping_2_modalities",
14+
),
15+
pytest.param(
16+
((0, 10), (0, 10)),
17+
10,
18+
2,
19+
id="overlapping_2_modalities",
20+
),
21+
pytest.param(
22+
((0, 10), (10, 20), (20, 30)),
23+
30,
24+
3,
25+
id="non_overlapping_3_modalities",
26+
),
27+
]
28+
29+
DIM_DATA = 16
30+
DIM_HIDDEN = [32, 32]
31+
DIM_COND_MAP = (8,)
32+
BATCH_SIZE = 4
33+
34+
35+
def _make_model(context_bonds, attention_pooling, dropout_rate=0.1):
36+
return ConditionalPerturbationNetwork(
37+
dim_hidden=DIM_HIDDEN,
38+
dim_data=DIM_DATA,
39+
dim_cond=max(stop for _, stop in context_bonds),
40+
dim_cond_map=DIM_COND_MAP,
41+
embed_cond_equal=True,
42+
attention_pooling=attention_pooling,
43+
num_heads=4,
44+
dropout_rate=dropout_rate,
45+
context_entity_bonds=context_bonds,
46+
)
47+
48+
49+
def _make_inputs(rng, dim_cond):
50+
rng_x, rng_c = jax.random.split(rng)
51+
x = jax.random.normal(rng_x, (BATCH_SIZE, DIM_DATA))
52+
c = jax.random.normal(rng_c, (BATCH_SIZE, dim_cond))
53+
return x, c
54+
755

856
class TestAttentionPooling:
957
"""Tests for attention pooling in ConditionalPerturbationNetwork."""
1058

11-
# Shared config for a model using embed_cond_equal (deep set path)
12-
DIM_DATA = 16
13-
DIM_COND = 20 # 2 contexts of size 10 each
14-
DIM_HIDDEN = [32, 32]
15-
DIM_COND_MAP = (8,)
16-
CONTEXT_BONDS = ((0, 10), (10, 20))
17-
BATCH_SIZE = 4
18-
NUM_CONTEXTS = 2
19-
20-
def _make_model(self, attention_pooling: bool, dropout_rate: float = 0.1):
21-
return ConditionalPerturbationNetwork(
22-
dim_hidden=self.DIM_HIDDEN,
23-
dim_data=self.DIM_DATA,
24-
dim_cond=self.DIM_COND,
25-
dim_cond_map=self.DIM_COND_MAP,
26-
embed_cond_equal=True,
27-
attention_pooling=attention_pooling,
28-
num_heads=4,
29-
dropout_rate=dropout_rate,
30-
context_entity_bonds=self.CONTEXT_BONDS,
31-
)
32-
33-
def _make_inputs(self, rng):
34-
rng_x, rng_c = jax.random.split(rng)
35-
x = jax.random.normal(rng_x, (self.BATCH_SIZE, self.DIM_DATA))
36-
c = jax.random.normal(rng_c, (self.BATCH_SIZE, self.DIM_COND))
37-
return x, c
38-
39-
def test_attention_pooling_forward_pass(self):
59+
@pytest.mark.parametrize(
60+
"context_bonds,dim_cond,num_contexts", CONTEXT_BOND_CONFIGS
61+
)
62+
def test_attention_pooling_forward_pass(
63+
self, context_bonds, dim_cond, num_contexts
64+
):
4065
"""Test that attention pooling produces correct output shape."""
41-
model = self._make_model(attention_pooling=True)
66+
model = _make_model(context_bonds, attention_pooling=True)
4267
rng = jax.random.PRNGKey(0)
43-
x, c = self._make_inputs(rng)
68+
x, c = _make_inputs(rng, dim_cond)
4469

4570
rng_params, rng_dropout = jax.random.split(rng)
46-
params = model.init(
47-
{"params": rng_params, "dropout": rng_dropout}, x=x, c=c
48-
)["params"]
71+
params = model.init({"params": rng_params, "dropout": rng_dropout}, x=x, c=c)[
72+
"params"
73+
]
4974

50-
out = model.apply({"params": params}, x, c, self.NUM_CONTEXTS)
51-
assert out.shape == (self.BATCH_SIZE, self.DIM_DATA)
52-
# Output should be a residual: x + f(x, c), so not all zeros
75+
out = model.apply({"params": params}, x, c, num_contexts)
76+
assert out.shape == (BATCH_SIZE, DIM_DATA)
5377
assert not jnp.allclose(out, 0.0)
5478

55-
def test_both_pooling_modes_same_output_shape(self):
56-
"""Test that mean pooling and attention pooling produce the same output shape."""
79+
@pytest.mark.parametrize(
80+
"context_bonds,dim_cond,num_contexts", CONTEXT_BOND_CONFIGS
81+
)
82+
def test_both_pooling_modes_same_output_shape(
83+
self, context_bonds, dim_cond, num_contexts
84+
):
85+
"""Test that mean and attention pooling produce the same output shape."""
5786
rng = jax.random.PRNGKey(42)
58-
x, c = self._make_inputs(rng)
87+
x, c = _make_inputs(rng, dim_cond)
5988

60-
# Mean pooling (default)
61-
model_mean = self._make_model(attention_pooling=False)
89+
model_mean = _make_model(context_bonds, attention_pooling=False)
6290
rng_p1, rng_d1, rng_p2, rng_d2 = jax.random.split(rng, 4)
63-
params_mean = model_mean.init(
64-
{"params": rng_p1, "dropout": rng_d1}, x=x, c=c
65-
)["params"]
66-
out_mean = model_mean.apply({"params": params_mean}, x, c, self.NUM_CONTEXTS)
67-
68-
# Attention pooling
69-
model_attn = self._make_model(attention_pooling=True)
70-
params_attn = model_attn.init(
71-
{"params": rng_p2, "dropout": rng_d2}, x=x, c=c
72-
)["params"]
73-
out_attn = model_attn.apply({"params": params_attn}, x, c, self.NUM_CONTEXTS)
74-
75-
assert out_mean.shape == out_attn.shape == (self.BATCH_SIZE, self.DIM_DATA)
76-
77-
def test_dropout_deterministic_vs_stochastic(self):
78-
"""Test that deterministic=False (training) produces different outputs across runs
79-
while deterministic=True (eval) is consistent."""
80-
model = self._make_model(attention_pooling=True, dropout_rate=0.5)
91+
params_mean = model_mean.init({"params": rng_p1, "dropout": rng_d1}, x=x, c=c)[
92+
"params"
93+
]
94+
out_mean = model_mean.apply({"params": params_mean}, x, c, num_contexts)
95+
96+
model_attn = _make_model(context_bonds, attention_pooling=True)
97+
params_attn = model_attn.init({"params": rng_p2, "dropout": rng_d2}, x=x, c=c)[
98+
"params"
99+
]
100+
out_attn = model_attn.apply({"params": params_attn}, x, c, num_contexts)
101+
102+
assert out_mean.shape == out_attn.shape == (BATCH_SIZE, DIM_DATA)
103+
104+
@pytest.mark.parametrize(
105+
"context_bonds,dim_cond,num_contexts", CONTEXT_BOND_CONFIGS
106+
)
107+
def test_dropout_deterministic_vs_stochastic(
108+
self, context_bonds, dim_cond, num_contexts
109+
):
110+
"""Test that deterministic=False produces different outputs across runs
111+
while deterministic=True is consistent."""
112+
model = _make_model(context_bonds, attention_pooling=True, dropout_rate=0.5)
81113
rng = jax.random.PRNGKey(7)
82-
x, c = self._make_inputs(rng)
114+
x, c = _make_inputs(rng, dim_cond)
83115

84116
rng_params, rng_dropout = jax.random.split(rng)
85-
params = model.init(
86-
{"params": rng_params, "dropout": rng_dropout}, x=x, c=c
87-
)["params"]
117+
params = model.init({"params": rng_params, "dropout": rng_dropout}, x=x, c=c)[
118+
"params"
119+
]
88120

89121
# Deterministic mode: two calls should be identical
90-
out_eval_1 = model.apply({"params": params}, x, c, self.NUM_CONTEXTS, deterministic=True)
91-
out_eval_2 = model.apply({"params": params}, x, c, self.NUM_CONTEXTS, deterministic=True)
122+
out_eval_1 = model.apply(
123+
{"params": params}, x, c, num_contexts, deterministic=True
124+
)
125+
out_eval_2 = model.apply(
126+
{"params": params}, x, c, num_contexts, deterministic=True
127+
)
92128
assert jnp.allclose(out_eval_1, out_eval_2)
93129

94130
# Stochastic mode: two calls with different dropout keys should differ
95131
key1, key2 = jax.random.split(jax.random.PRNGKey(99))
96132
out_train_1 = model.apply(
97-
{"params": params}, x, c, self.NUM_CONTEXTS,
98-
deterministic=False, rngs={"dropout": key1},
133+
{"params": params},
134+
x,
135+
c,
136+
num_contexts,
137+
deterministic=False,
138+
rngs={"dropout": key1},
99139
)
100140
out_train_2 = model.apply(
101-
{"params": params}, x, c, self.NUM_CONTEXTS,
102-
deterministic=False, rngs={"dropout": key2},
141+
{"params": params},
142+
x,
143+
c,
144+
num_contexts,
145+
deterministic=False,
146+
rngs={"dropout": key2},
103147
)
104148
assert not jnp.allclose(out_train_1, out_train_2)

cmonge/trainers/conditional_monge_trainer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,7 +192,7 @@ def loss_fn(
192192
batch["source"],
193193
batch["condition"],
194194
n_contexts,
195-
**kwargs
195+
**kwargs,
196196
)
197197

198198
# compute the loss

0 commit comments

Comments
 (0)