Note
Go to the end to download the full example code.
Publishing and reproducing¶
LION.package.publish writes a trained model as a package: one folder
with everything needed to use it, test it and train it again, from the
records LION saved with it (Saving, loading and records). Someone with the
folder and the data then loads it, builds its pieces, or trains it again.
Previous: Evaluation and comparison. Next: Extending LION.
Needs a GPU and the processed LIDC-IDRI data in LION_DATA_PATH. Publishing
also needs LION’s code committed and pushed (it records the commit).
Note
The models trained in these tutorials are toys: a few epochs on a few slices, to show how LION is used, so their results are poor (often worse than FDK). Trained as their papers train them (many epochs, all the data), they do what the papers report.
A model to publish¶
Trained by a LION solver on a LION experiment, so that everything about it is recorded:
import tempfile
from pathlib import Path
import torch
from torch.utils.data import DataLoader, Subset
from LION.experiments import ct_experiments
from LION.models.post_processing.FBPConvNet import FBPConvNet
from LION.optimizers.SupervisedSolver import SupervisedSolver
experiment = ct_experiments.LowDoseCTRecon()
training = Subset(experiment.get_dataset("train"), range(40))
training_loader = DataLoader(training, batch_size=4, shuffle=True)
folder = Path(tempfile.mkdtemp())
model = FBPConvNet(experiment.operator_params)
solver = SupervisedSolver(
model, torch.optim.Adam(model.parameters()), torch.nn.MSELoss()
)
solver.set_training(training_loader)
solver.set_saving(folder, "FBPConvNet")
solver.train(2)
solver.save_final_results()
/mhome/damtp/r/ab2860/AItomotools/LION/data_loaders/LIDC_IDRI.py:487: UserWarning: LIDC-IDRI: left out 2 damaged or unusable files (re-run the pre-processing for these patients): ['LIDC-IDRI-0238 (not resampled)', 'LIDC-IDRI-0585 (not resampled)']
warnings.warn(
Publishing¶
publish(model_file, folder, evaluation=...): the evaluation (optional) is
run on the model and its test record included. The folder must be new (or
empty).
from LION import package
from LION.evaluation import Evaluation
evaluation = Evaluation(experiment, samples=10)
published = package.publish(
folder / "FBPConvNet", folder / "package", evaluation=evaluation
)
print(sorted(p.name for p in published.iterdir()))
['README.md', 'data', 'environment.txt', 'experiment.json', 'model.json', 'model.pt', 'package.json', 'solver.json', 'test.json']
The package holds the model file, the experiment, which samples it was
trained and validated on, the training record, the test record, the
environment, every citation, and a model card (README.md) with the code
to use and train it again:
print((published / "README.md").read_text())
# FBPConvNet trained on LIDC-IDRI: Low dose full angular sampling experiment
Published on 2026-11-10 with LION 0.2.0 (commit e1cf2a02f6). Generated from the package's records (`package.json` lists them); edit it freely, it isn't checksummed.
## The model
- `LION.models.post_processing.FBPConvNet.FBPConvNet`, 34,556,033 parameters.
- The weights: final, after epoch 2.
## The problem
- Experiment: `LION.experiments.ct_experiments.LowDoseCTRecon`, "Low dose full angular sampling experiment".
- Dataset: LIDC-IDRI (`LION.data_loaders.LIDC_IDRI.LIDC_IDRI`).
- Geometry: fan beam, 360 angles, detector 1x1024, image 1x512x512.
- Noise: I0 11000, sigma 5, sigma_blur 0.3015, model physical, detector energy_integrating, mass_attenuation 0.1837, min_counts 1.0 (seed 0).
## Training
- Solver: `LION.optimizers.SupervisedSolver.SupervisedSolver`, loss `MSELoss`; optimizer `Adam` (learning rate 0.001).
- Data: the train data, 40 samples from 8 groups; batch size 4, shuffled.
- 2 epochs of 2; at the end, training loss 0.03686.
- Seed 0; initial weights made again from the random state saved with the model.
- Trained with LION 0.2.0 (commit e1cf2a02f6), Python 3.12.15, torch 2.13.0, on Quadro RTX 6000, Quadro RTX 6000, Quadro RTX 6000, Quadro RTX 6000 (CUDA 12.9); deterministic: no, so runs on a GPU differ slightly.
## Test
On the test data (10 samples from 10 groups), metrics over the range [0.0, 2.89096999168396] (`test.json`):
| Method | PSNR | SSIM | HaarPSI |
|---|---|---|---|
| FBPConvNet | -5.6211 ± 6.3081 | 0.0432 ± 0.0320 | 0.0302 ± 0.0158 |
## Using it
```python
from LION.models.LIONmodel import LIONmodel
model, _, _ = LIONmodel.load("model")
```
## Training it again
In the package's folder, from its files alone:
```python
from LION.package import train_again
model = train_again(".", "retrained")
```
To change something first (the model, the experiment, the solver, the optimizer, the training):
```python
from LION.package import load, train_again
package = load(".")
package.training.epochs = 2 * package.training.epochs # for example
model = train_again(package, "retrained")
```
### Step by step
```python
import pathlib
import torch
from torch.utils.data import DataLoader, Subset
from LION.data_loaders.data_record import read_record
from LION.experiments.ct_experiments import Experiment
from LION.models.LIONmodel import LIONmodel
from LION.optimizers.LIONsolver import LIONsolver
from LION.optimizers.training_record import read_training_record
record = read_training_record("solver.json")
def data(file):
"""The data a record describes, made by its experiment."""
record = read_record(file)
made = Experiment.instantiate(file).get_dataset(record.split)
subset = getattr(record, "subset", None)
return made if subset is None else Subset(made, subset.indices)
model = LIONmodel.instantiate("model", initial_weights=True)
solver = LIONsolver.instantiate(
"solver.json", model, device=torch.device("cuda") # where it was trained
)
t = record.train
solver.set_training(
DataLoader(
data("data/training.json"),
batch_size=t.batch_size,
shuffle=t.shuffle,
drop_last=t.drop_last,
)
)
pathlib.Path("retrained").mkdir(exist_ok=True)
solver.set_saving("retrained", "model")
solver.train(record.epochs.done)
solver.save_final_results()
```
## Citations
Where the model and its components come from:
```bibtex
@article{jin2017deep,
title={Deep convolutional neural network for inverse problems in imaging},
author={Jin, Kyong Hwan and McCann, Michael T and Froustey, Emmanuel and Unser, Michael},
journal={IEEE transactions on image processing},
volume={26},
number={9},
pages={4509--4522},
year={2017},
publisher={IEEE}
}
@article{armato2011lidc,
title={The Lung Image Database Consortium (LIDC) and Image Database Resource Initiative (IDRI): a completed reference database of lung nodules on CT scans},
author={Armato III, Samuel G and McLennan, Geoffrey and Bidaut, Luc and others},
journal={Medical Physics},
volume={38},
number={2},
pages={915--931},
year={2011}
}
@misc{lidc2015data,
title={Data From LIDC-IDRI},
howpublished={The Cancer Imaging Archive},
doi={10.7937/K9/TCIA.2015.LO9QL9SX}
}
```
It refuses what couldn’t be reproduced from it, saying why: LION code with uncommitted or unpushed changes, a test on the training data, code outside LION it can’t name (e.g. a model class defined in a script), data that aren’t a LION dataset.
Loading a package¶
load reads the recorded arguments of every piece (the model, the
experiment, the data, the solver, the optimizer, the training), without
making anything, so they can be read or changed first:
p = package.load(published)
print(p.model)
print(p.optimizer)
print(p.training_data) # which split, and which samples of it
operator_params : CT Geometry
CT type/mode = fan
-----
Distance from source to detector (DSD) = 1050 mm
Distance from source to origin (DSO)= 575 mm
-----
Detector
Detector shape = [ 1 1024]
Detector size = [ 1 1024] mm
-----
Image
Image shape = [ 1 512 512]
Image size = [ 0.9765625 500. 500. ] mm
Number of angles = (360,)
model_parameters : model_input_type : ModelInputType.SINOGRAM
initial_reconstruction : <function fdk at 0x7f2b3c260220>
down_1_channels : [1, 64, 64, 64]
down_2_channels : [64, 128, 128]
down_3_channels : [128, 256, 256]
down_4_channels : [256, 512, 512]
latent_channels : [512, 1024, 1024]
up_1_channels : [1024, 512, 512]
up_2_channels : [512, 256, 256]
up_3_channels : [256, 128, 128]
up_4_channels : [128, 64, 64]
last_block : [64, 1, 1]
activation : ReLU
lr : 0.001
betas : (0.9, 0.999)
eps : 1e-08
weight_decay : 0
amsgrad : False
maximize : False
foreach : None
capturable : False
differentiable : False
fused : None
decoupled_weight_decay : False
split : train
subset : [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39]
build makes the pieces: the trained model, the experiment, the training
and validation data, the solver, the evaluation (load_and_build does
both):
built = package.build(p)
print(type(built.model).__name__, len(built.training_data))
/mhome/damtp/r/ab2860/AItomotools/LION/data_loaders/LIDC_IDRI.py:487: UserWarning: LIDC-IDRI: left out 2 damaged or unusable files (re-run the pre-processing for these patients): ['LIDC-IDRI-0238 (not resampled)', 'LIDC-IDRI-0585 (not resampled)']
warnings.warn(
FBPConvNet 40
The published model reconstructs like any other:
from LION.reconstructors.ForwardReconstructor import ForwardReconstructor
sinogram, image = built.experiment.get_dataset("test")[0]
reconstruction = ForwardReconstructor(
built.experiment.operator_params, built.model
).reconstruct(sinogram)
Training again¶
train_again trains the model again from the same start, on the same
data, with the same solver, seed and epochs: the published model again
(exactly on a CPU; on a GPU, to about 1e-3).
Changing a recorded argument first trains a variant: anything not changed stays as recorded. E.g. another activation (a new model, new random weights):
p.model.model_parameters.activation = "LeakyReLU"
variant = package.train_again(p, folder / "variant")
print(type(variant).__name__)
FBPConvNet
Total running time of the script: (2 minutes 56.729 seconds)