Deconvolution Training and Reconstructions with ODP

This example demonstrates the training and application of the unrolled optimization with deep priors (ODP) with proximal map architecture described in [20] for a deconvolution (deblurring) problem.

The source images are foam phantoms generated with xdesign.

A class scico.flax.ODPNet implements the ODP architecture, which solves the optimization problem

\[\mathrm{argmin}_{\mathbf{x}} \; \| A \mathbf{x} - \mathbf{y} \|_2^2 + r(\mathbf{x}) \;,\]

where \(A\) is a circular convolution, \(\mathbf{y}\) is a set of blurred images, \(r\) is a regularizer and \(\mathbf{x}\) is the set of deblurred images. The ODP, proximal map architecture, abstracts the iterative solution by an unrolled network where each iteration corresponds to a different stage in the ODP network and updates the prediction by solving

\[\mathbf{x}^{k+1} = \mathrm{argmin}_{\mathbf{x}} \; \alpha_k \| A \mathbf{x} - \mathbf{y} \|_2^2 + \frac{1}{2} \| \mathbf{x} - \mathbf{x}^k - \mathbf{x}^{k+1/2} \|_2^2 \;,\]

which for the deconvolution problem corresponds to

\[\mathbf{x}^{k+1} = \mathcal{F}^{-1} \mathrm{diag} (\alpha_k | \mathcal{K}|^2 + 1 )^{-1} \mathcal{F} \, (\alpha_k K^T * \mathbf{y} + \mathbf{x}^k + \mathbf{x}^{k+1/2}) \;,\]

where \(k\) is the index of the stage (iteration), \(\mathbf{x}^k + \mathbf{x}^{k+1/2} = \mathrm{ResNet}(\mathbf{x}^{k})\) is the regularization (implemented as a residual convolutional neural network), \(\mathbf{x}^k\) is the output of the previous stage, \(\alpha_k > 0\) is a learned stage-wise parameter weighting the contribution of the fidelity term, \(\mathcal{F}\) is the DFT, \(K\) is the blur kernel, and \(\mathcal{K}\) is the DFT of \(K\). The output of the final stage is the set of deblurred images.

[1]:
# isort: off
import os
from functools import partial
from time import time

import numpy as np

import logging
import ray

ray.init(logging_level=logging.ERROR)  # need to call init before jax import: ray-project/ray#44087

# Set an arbitrary processor count (only applies if GPU is not available).
os.environ["XLA_FLAGS"] = "--xla_force_host_platform_device_count=8"

import jax

try:
    from jax.extend.backend import get_backend  # introduced in jax 0.4.33
except ImportError:
    from jax.lib.xla_bridge import get_backend

from mpl_toolkits.axes_grid1 import make_axes_locatable

import komplot as kplt
from scico import flax as sflax
from scico import metric
from scico.flax.examples import load_blur_data
from scico.flax.train.traversals import clip_positive, construct_traversal
from scico.linop import CircularConvolve
kplt.config_notebook_plotting()

platform = get_backend().platform
print("Platform: ", platform)
Platform:  gpu

Define blur operator.

[2]:
output_size = 256  # patch size

n = 3  # convolution kernel size
σ = 20.0 / 255  # noise level
psf = np.ones((n, n), dtype=np.float32) / (n * n)  # blur kernel

ishape = (output_size, output_size)
opBlur = CircularConvolve(h=psf, input_shape=ishape)
opBlur_vmap = jax.vmap(opBlur)  # for batch processing in data generation

Read data from cache or generate if not available.

[3]:
train_nimg = 416  # number of training images
test_nimg = 64  # number of testing images
nimg = train_nimg + test_nimg

train_ds, test_ds = load_blur_data(
    train_nimg,
    test_nimg,
    output_size,
    psf,
    σ,
    verbose=True,
)
Data read from path: ~/.cache/scico/examples/data
Set --training-- size: 416
Set --testing -- size: 64
Data range -- images --  Min:  0.00  Max:  1.00
Data range -- labels --  Min:  0.00  Max:  1.00

Define configuration dictionary for model and training loop.

Parameters have been selected for demonstration purposes and relatively short training. The model depth is akin to the number of unrolled iterations in the ODP model. The block depth controls the number of layers at each unrolled iteration. The number of filters is uniform throughout the iterations. Better performance may be obtained by increasing depth, block depth, number of filters or training epochs, but may require longer training times.

[4]:
# model configuration
model_conf = {
    "depth": 2,
    "num_filters": 64,
    "block_depth": 3,
}
# training configuration
train_conf: sflax.ConfigDict = {
    "seed": 0,
    "opt_type": "SGD",
    "momentum": 0.9,
    "batch_size": 16,
    "num_epochs": 50,
    "base_learning_rate": 1e-2,
    "warmup_epochs": 0,
    "log_every_steps": 100,
    "log": True,
    "checkpointing": True,
}

Construct ODPNet model.

[5]:
channels = train_ds["image"].shape[-1]
model = sflax.ODPNet(
    operator=opBlur,
    depth=model_conf["depth"],
    channels=channels,
    num_filters=model_conf["num_filters"],
    block_depth=model_conf["block_depth"],
    odp_block=sflax.inverse.ODPProxDcnvBlock,
)

Construct functionality for ensuring that the learned fidelity weight parameter is always positive.

[6]:
alphatrav = construct_traversal("alpha")  # select alpha parameters in model
alphapos = partial(
    clip_positive,  # apply this function
    traversal=alphatrav,  # to alpha parameters in model
    minval=1e-3,
)

Run training loop.

[7]:
print(f"\nJAX process: {jax.process_index()}{' / '}{jax.process_count()}")
print(f"JAX local devices: {jax.local_devices()}\n")

workdir = os.path.join(os.path.expanduser("~"), ".cache", "scico", "examples", "odp_dcnv_out")
train_conf["workdir"] = workdir
train_conf["post_lst"] = [alphapos]
# Construct training object
trainer = sflax.BasicFlaxTrainer(
    train_conf,
    model,
    train_ds,
    test_ds,
)
modvar, stats_object = trainer.train()

JAX process: 0 / 1
JAX local devices: [CudaDevice(id=0), CudaDevice(id=1), CudaDevice(id=2), CudaDevice(id=3), CudaDevice(id=4), CudaDevice(id=5), CudaDevice(id=6), CudaDevice(id=7)]

channels: 1   training signals: 416   testing signals: 64   signal size: 256

Network Structure:
+-----------------------------------------------------------+----------------+--------+----------+--------+
| Name                                                      | Shape          | Size   | Mean     | Std    |
+-----------------------------------------------------------+----------------+--------+----------+--------+
| ODPProxDcnvBlock_0/alpha                                  | (1,)           | 1      | 0.5      | 0.0    |
| ODPProxDcnvBlock_0/resnet/BatchNorm_0/bias                | (1,)           | 1      | 0.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/BatchNorm_0/scale               | (1,)           | 1      | 1.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_0/BatchNorm_0/bias  | (64,)          | 64     | 0.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_0/BatchNorm_0/scale | (64,)          | 64     | 1.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_0/Conv_0/kernel     | (3, 3, 1, 64)  | 576    | -0.00223 | 0.0572 |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_1/BatchNorm_0/bias  | (64,)          | 64     | 0.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_1/BatchNorm_0/scale | (64,)          | 64     | 1.0      | 0.0    |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_1/Conv_0/kernel     | (3, 3, 64, 64) | 36,864 | 4.27e-05 | 0.0418 |
| ODPProxDcnvBlock_0/resnet/Conv_0/kernel                   | (3, 3, 64, 1)  | 576    | 0.00189  | 0.0576 |
| ODPProxDcnvBlock_1/alpha                                  | (1,)           | 1      | 0.25     | 0.0    |
| ODPProxDcnvBlock_1/resnet/BatchNorm_0/bias                | (1,)           | 1      | 0.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/BatchNorm_0/scale               | (1,)           | 1      | 1.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_0/BatchNorm_0/bias  | (64,)          | 64     | 0.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_0/BatchNorm_0/scale | (64,)          | 64     | 1.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_0/Conv_0/kernel     | (3, 3, 1, 64)  | 576    | 0.000492 | 0.057  |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_1/BatchNorm_0/bias  | (64,)          | 64     | 0.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_1/BatchNorm_0/scale | (64,)          | 64     | 1.0      | 0.0    |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_1/Conv_0/kernel     | (3, 3, 64, 64) | 36,864 | 0.000375 | 0.0416 |
| ODPProxDcnvBlock_1/resnet/Conv_0/kernel                   | (3, 3, 64, 1)  | 576    | 0.00203  | 0.0587 |
+-----------------------------------------------------------+----------------+--------+----------+--------+
Total weights: 76,550

Batch Normalization:
+----------------------------------------------------------+-------+------+------+-----+
| Name                                                     | Shape | Size | Mean | Std |
+----------------------------------------------------------+-------+------+------+-----+
| ODPProxDcnvBlock_0/resnet/BatchNorm_0/mean               | (1,)  | 1    | 0.0  | 0.0 |
| ODPProxDcnvBlock_0/resnet/BatchNorm_0/var                | (1,)  | 1    | 1.0  | 0.0 |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_0/BatchNorm_0/mean | (64,) | 64   | 0.0  | 0.0 |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_0/BatchNorm_0/var  | (64,) | 64   | 1.0  | 0.0 |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_1/BatchNorm_0/mean | (64,) | 64   | 0.0  | 0.0 |
| ODPProxDcnvBlock_0/resnet/ConvBNBlock_1/BatchNorm_0/var  | (64,) | 64   | 1.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/BatchNorm_0/mean               | (1,)  | 1    | 0.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/BatchNorm_0/var                | (1,)  | 1    | 1.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_0/BatchNorm_0/mean | (64,) | 64   | 0.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_0/BatchNorm_0/var  | (64,) | 64   | 1.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_1/BatchNorm_0/mean | (64,) | 64   | 0.0  | 0.0 |
| ODPProxDcnvBlock_1/resnet/ConvBNBlock_1/BatchNorm_0/var  | (64,) | 64   | 1.0  | 0.0 |
+----------------------------------------------------------+-------+------+------+-----+
Total weights: 516

Initial compilation, which might take some time ...
Initial compilation completed.

Epoch  Time      Train_LR  Train_Loss  Train_SNR  Eval_Loss  Eval_SNR
---------------------------------------------------------------------
    3  1.60e+01  0.010000    0.032192       8.98   0.051923      2.74
    7  2.62e+01  0.010000    0.005989      12.13   0.052100      2.73
   11  3.15e+01  0.010000    0.004935      12.96   0.094205      0.16
   15  3.63e+01  0.010000    0.004362      13.49   0.078199      0.96
   19  4.11e+01  0.010000    0.003985      13.88   0.043687      3.49
   23  4.61e+01  0.010000    0.003709      14.20   0.060562      2.07
   26  5.08e+01  0.010000    0.003493      14.46   0.019156      7.07
   30  5.59e+01  0.010000    0.003318      14.68   0.007038     11.42
   34  6.07e+01  0.010000    0.003168      14.88   0.004310     13.55
   38  6.55e+01  0.010000    0.003046      15.05   0.003383     14.60
   42  7.07e+01  0.010000    0.002939      15.21   0.002982     15.15
   46  7.57e+01  0.010000    0.002845      15.35   0.002829     15.38
   49  8.06e+01  0.010000    0.002766      15.47   0.002746     15.51

Evaluate on testing data.

[8]:
del train_ds["image"]
del train_ds["label"]

fmap = sflax.FlaxMap(model, modvar)
del model, modvar

maxn = test_nimg // 4
start_time = time()
output = fmap(test_ds["image"][:maxn])
time_eval = time() - start_time
output = np.clip(output, a_min=0, a_max=1.0)

Evaluate trained model in terms of reconstruction time and data fidelity.

[9]:
snr_eval = metric.snr(test_ds["label"][:maxn], output)
psnr_eval = metric.psnr(test_ds["label"][:maxn], output)
print(
    f"{'ODPNet training':18s}{'epochs:':2s}{train_conf['num_epochs']:>5d}"
    f"{'':21s}{'time[s]:':10s}{trainer.train_time:>7.2f}"
)
print(
    f"{'ODPNet testing':18s}{'SNR:':5s}{snr_eval:>5.2f}{' dB'}{'':3s}"
    f"{'PSNR:':6s}{psnr_eval:>5.2f}{' dB'}{'':3s}{'time[s]:':10s}{time_eval:>7.2f}"
)
ODPNet training   epochs:   50                     time[s]:    81.81
ODPNet testing    SNR: 15.80 dB   PSNR: 22.87 dB   time[s]:     7.53

Plot comparison.

[10]:
np.random.seed(123)
indx = np.random.randint(0, high=maxn)

fig, ax = kplt.subplots(nrows=1, ncols=3, figsize=(15, 5))
kplt.imview(test_ds["label"][indx, ..., 0], title="Ground truth", show_cbar=None, ax=ax[0])
kplt.imview(
    test_ds["image"][indx, ..., 0],
    title="Blurred: \nSNR: %.2f (dB), PSNR: %.2f"
    % (
        metric.snr(test_ds["label"][indx, ..., 0], test_ds["image"][indx, ..., 0]),
        metric.psnr(test_ds["label"][indx, ..., 0], test_ds["image"][indx, ..., 0]),
    ),
    show_cbar=None,
    ax=ax[1],
)
kplt.imview(
    output[indx, ..., 0],
    title="ODPNet Reconstruction\nSNR: %.2f (dB), PSNR: %.2f"
    % (
        metric.snr(test_ds["label"][indx, ..., 0], output[indx, ..., 0]),
        metric.psnr(test_ds["label"][indx, ..., 0], output[indx, ..., 0]),
    ),
    ax=ax[2],
)
divider = make_axes_locatable(ax[2])
cax = divider.append_axes("right", size="5%", pad=0.2)
fig.colorbar(ax[2].get_images()[0], cax=cax, label="arbitrary units")
fig.show()
../_images/examples_deconv_odp_train_foam1_19_0.png

Plot convergence statistics. Statistics are generated only if a training cycle was done (i.e. if not reading final epoch results from checkpoint).

[11]:
if stats_object is not None and len(stats_object.iterations) > 0:
    hist = stats_object.history(transpose=True)
    fig, ax = kplt.subplots(nrows=1, ncols=2, figsize=(12, 5))
    kplt.plot(
        hist.Epoch,
        np.array((hist.Train_Loss, hist.Eval_Loss)).T,
        ylog=True,
        title="Loss function",
        xlabel="Epoch",
        ylabel="Loss value",
        legend=("Train", "Test"),
        ax=ax[0],
    )
    kplt.plot(
        hist.Epoch,
        np.array((hist.Train_SNR, hist.Eval_SNR)).T,
        title="Metric",
        xlabel="Epoch",
        ylabel="SNR (dB)",
        legend=("Train", "Test"),
        ax=ax[1],
    )
    fig.show()
../_images/examples_deconv_odp_train_foam1_21_0.png