Skip to content

Commit a4a9917

Browse files
committed
fix(networks): return mu from VAE reparameterize at inference
VarAutoEncoder.reparameterize added the standard deviation to mu at eval time (`std.add_(mu)` with no noise term), so inference returned mu + std instead of the posterior mean. At inference the latent code should be mu; the random term belongs only to training (the reparameterization trick). Return mu directly when not training, and compute mu + eps * std out-of-place otherwise. VarFullyConnectedNet.reparameterize had the identical bug and is fixed the same way. Adds regression tests asserting eval is deterministic and equals mu while training stays stochastic. Fixes #8413. Signed-off-by: Lanre Shittu <136805224+Shizoqua@users.noreply.github.com>
1 parent b7d14c8 commit a4a9917

4 files changed

Lines changed: 55 additions & 10 deletions

File tree

monai/networks/nets/fullyconnectednet.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -172,12 +172,13 @@ def decode_forward(self, z: torch.Tensor, use_sigmoid: bool = True) -> torch.Ten
172172
return x
173173

174174
def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
175+
if not self.training:
176+
# At inference the latent code is the posterior mean; the random
177+
# term is only added during training (the reparameterization trick).
178+
return mu
175179
std = torch.exp(0.5 * logvar)
176-
177-
if self.training: # multiply random noise with std only during training
178-
std = torch.randn_like(std).mul(std)
179-
180-
return std.add_(mu)
180+
eps = torch.randn_like(std)
181+
return mu + eps * std
181182

182183
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
183184
mu, logvar = self.encode_forward(x)

monai/networks/nets/varautoencoder.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,13 @@ def decode_forward(self, z: torch.Tensor, use_sigmoid: bool = True) -> torch.Ten
142142
return x
143143

144144
def reparameterize(self, mu: torch.Tensor, logvar: torch.Tensor) -> torch.Tensor:
145+
if not self.training:
146+
# At inference the latent code is the posterior mean; the random
147+
# term is only added during training (the reparameterization trick).
148+
return mu
145149
std = torch.exp(0.5 * logvar)
146-
147-
if self.training: # multiply random noise with std only during training
148-
std = torch.randn_like(std).mul(std)
149-
150-
return std.add_(mu)
150+
eps = torch.randn_like(std)
151+
return mu + eps * std
151152

152153
def forward(self, x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
153154
mu, logvar = self.encode_forward(x)

tests/networks/nets/test_fullyconnectednet.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,27 @@ def test_vfc_shape(self, input_param, input_shape, expected_shape):
6464
result = net.forward(torch.randn(input_shape).to(device))[0]
6565
self.assertEqual(result.shape, expected_shape)
6666

67+
def test_vfc_reparameterize_eval_returns_mu(self):
68+
# At eval the latent code must equal mu (deterministic); at train it must
69+
# be stochastic. Same #8413 reparameterize bug as VarAutoEncoder.
70+
net = VarFullyConnectedNet(
71+
in_channels=10, out_channels=10, latent_size=30, encode_channels=(15, 20, 25), decode_channels=(15, 20, 25)
72+
).to(device)
73+
data = torch.randn(3, 10).to(device)
74+
75+
with eval_mode(net):
76+
_, mu1, _, z1 = net(data)
77+
_, _, _, z2 = net(data)
78+
self.assertTrue(torch.allclose(z1, mu1))
79+
self.assertTrue(torch.allclose(z1, z2))
80+
81+
net.train()
82+
with torch.no_grad():
83+
_, mu_t, _, zt1 = net(data)
84+
_, _, _, zt2 = net(data)
85+
self.assertFalse(torch.allclose(zt1, mu_t))
86+
self.assertFalse(torch.allclose(zt1, zt2))
87+
6788

6889
if __name__ == "__main__":
6990
unittest.main()

tests/networks/nets/test_varautoencoder.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,28 @@ def test_script(self):
122122
test_data = torch.randn(2, 1, 32, 32)
123123
test_script_save(net, test_data, rtol=1e-3, atol=1e-3)
124124

125+
def test_reparameterize_eval_returns_mu(self):
126+
# At eval the latent code must equal mu (deterministic, no noise added);
127+
# at train it must be stochastic. Regression test for #8413, where eval
128+
# returned mu + std.
129+
net = VarAutoEncoder(
130+
spatial_dims=2, in_shape=(1, 32, 32), out_channels=1, latent_size=4, channels=(4, 8), strides=(2, 2)
131+
).to(device)
132+
data = torch.randn(2, 1, 32, 32).to(device)
133+
134+
with eval_mode(net):
135+
_, mu1, _, z1 = net(data)
136+
_, _, _, z2 = net(data)
137+
self.assertTrue(torch.allclose(z1, mu1))
138+
self.assertTrue(torch.allclose(z1, z2))
139+
140+
net.train()
141+
with torch.no_grad():
142+
_, mu_t, _, zt1 = net(data)
143+
_, _, _, zt2 = net(data)
144+
self.assertFalse(torch.allclose(zt1, mu_t))
145+
self.assertFalse(torch.allclose(zt1, zt2))
146+
125147

126148
if __name__ == "__main__":
127149
unittest.main()

0 commit comments

Comments
 (0)