aicentreflip's picture
Publish the FLIP tutorial models
d4700c7 verified
Raw History Blame Contribute Delete
5.48 kB
# Copyright (c) 2026 Guy's and St Thomas' NHS Foundation Trust & King's College London
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# http://www.apache.org/licenses/LICENSE-2.0
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Autoencoder network for the FLIP autoencoder (VAE) tutorial.
The network pairs a KL-regularised autoencoder with a patch discriminator that adversarially
sharpens its reconstructions. Both are trained and aggregated together — unlike the latent
diffusion tutorial, nothing here is frozen.
The submodule holding the autoencoder is deliberately named ``autoencoder``, so its parameters
are keyed ``autoencoder.*`` in the state dict. That name is a **contract with the latent
diffusion tutorial**: a run of this tutorial produces the checkpoint that one loads as its frozen
encoder (via ``SERVER_CHECKPOINT``), and the keys line up only because both networks name the
submodule identically. See ``process_tools/extract_autoencoder.py`` there. Renaming this
attribute silently breaks that handoff — the load is ``strict=False``, so a mismatch is tolerated
rather than raised, and the diffusion model then trains against a randomly-initialised encoder.
"""
import json
from collections.abc import Mapping
from pathlib import Path
from typing import Any
from monai.networks.nets import AutoencoderKL, PatchDiscriminator
from torch import nn
def load_net_config():
"""Read the ``net_config`` block from the ``config.json`` next to this file."""
config_path = Path(__file__).parent / "config.json"
with open(config_path, "r") as f:
config = json.load(f)
net_config = config.get("net_config", {})
print(f"Loaded network config: {net_config}")
return net_config
def autoencoder_kwargs(net_config: Mapping[str, Any]) -> dict[str, Any]:
"""The exact keyword arguments this tutorial builds its ``AutoencoderKL`` with.
This is the sole definition of the autoencoder's architecture, and the reason it is a named
function rather than inline in ``__init__``: the latent diffusion tutorial's
``process_tools/extract_autoencoder.py`` calls it to record the architecture into the YAML that
travels with an extracted checkpoint, so the two tutorials cannot disagree about what a set of
weights means.
Every argument is passed explicitly, including the ones that happen to equal MONAI's defaults.
``norm_num_groups`` is why that matters: it changes how GroupNorm computes without changing a
single parameter *shape*, so a network built with one value loads a checkpoint trained with
another under ``strict=True`` and reports nothing. The only symptom is silently wrong latents.
Leaving it to the default meant ``config.json`` could name a value that steered nothing.
"""
stage_1 = net_config["stage_1"]
return {
"spatial_dims": net_config["spatial_dims"],
"in_channels": stage_1["in_channels"],
"out_channels": stage_1["out_channels"],
"num_res_blocks": stage_1["num_res_blocks"],
"channels": stage_1["channels"],
"attention_levels": stage_1["attention_levels"],
"latent_channels": stage_1["latent_channels"],
"norm_num_groups": stage_1["norm_num_groups"],
"norm_eps": stage_1["norm_eps"],
"with_encoder_nonlocal_attn": stage_1["with_encoder_nonlocal_attn"],
"with_decoder_nonlocal_attn": stage_1["with_decoder_nonlocal_attn"],
}
class AutoencoderNetwork(nn.Module):
"""Creates the autoencoder training network, containing a:
- Variational Autoencoder (compresses inputs into a latent space and reconstructs them)
- Discriminator (adversarially trains the autoencoder)
"""
def __init__(self):
super().__init__()
net_config = load_net_config()
self.autoencoder = AutoencoderKL(**autoencoder_kwargs(net_config))
self.discriminator = PatchDiscriminator(
spatial_dims=net_config["discriminator"]["spatial_dims"],
in_channels=net_config["discriminator"]["in_channels"],
channels=net_config["discriminator"]["channels"],
out_channels=net_config["discriminator"]["out_channels"],
num_layers_d=net_config["discriminator"]["num_layers_d"],
)
def forward_ae(self, x):
return self.autoencoder(x)
def discriminate(self, x):
return self.discriminator(x)
def load_autoencoder_dict(self, state_dict: Mapping[str, Any], strict: bool = True):
self.autoencoder.load_state_dict(state_dict, strict=strict)
def load_discriminator_dict(self, state_dict: Mapping[str, Any], strict: bool = True):
self.discriminator.load_state_dict(state_dict, strict=strict)
_net = AutoencoderNetwork()
def get_model() -> nn.Module:
"""
Returns the model defined in this file.
NOTE: This function needs to exist and cannot take any input arguments. If you would like to parameterize the
configuration of your model, for example loaded from a config file, do it when instantiating the model above.
"""
return _net