Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

IG warm-start init

Iterative-Gaussianization warm start

Notebooks 01 and 02 trained Gaussianization / coupling flows from random initialisation. That works, but spends the first hundreds of epochs pulling the flow from a near-identity map towards the data.

Iterative Gaussianization (RBIG — Laparra & Malo 2011) is a non-gradient, greedy procedure that warm-starts the flow by walking block-by-block through the architecture and fitting each block’s parameters directly:

  1. Rotation block — fit PCA on the current state YY; push the eigenvectors into the rotation layer (for Householder this uses a QR decomposition to extract reflector vectors).
  2. Marginal block — fit a per-dim mixture-of-Gaussians on the rotated data with scikit-learn’s GaussianMixture, and assign (π,μ,σ)(\pi, \mu, \sigma) directly into the MixtureCDFGaussianization layer weights.
  3. Coupling block — fit a per-b-dim GMM on the current YY; zero the conditioner’s final Dense kernel and set its bias to the flattened GMM params. At initialisation the conditioner output is constant-in-xax_a, so the coupling layer acts like a diagonal marginal with the fitted mixture. Gradient training then lets the conditioner learn to modulate on xax_a.

Between blocks the current YY is propagated forward through the newly-parameterised bijector, so the next block sees data that has already been partially Gaussianized. A classical result (Laparra & Malo 2011, Chen & Gopinath 2000) is that this drives YY towards N(0,I)\mathcal{N}(0, I) at a geometric rate — even with no gradient descent at all, the flow is already a decent density estimator after a handful of blocks.

The point of this notebook: a Keras flow initialised this way starts at a much lower NLL than random init and reaches its training optimum in a fraction of the epochs.

WARNING: All log messages before absl::InitializeLog() is called are written to STDERR
I0000 00:00:1776868596.826946  386554 cpu_feature_guard.cc:227] This TensorFlow binary is optimized to use available CPU instructions in performance-critical operations.
To enable the following instructions: AVX2 AVX512F FMA, in other operations, rebuild TensorFlow with the appropriate compiler flags.

1. Dataset

Two moons, standardised. Same data as notebooks 01/02 so the comparison is apples-to-apples.

2. Diagonal flow — random vs IG init

Build two identical diagonal flows, warm-start one of them from IG, and train both with the same optimiser and schedule.

E0000 00:00:1776868601.498397  386554 cuda_platform.cc:52] failed call to cuInit: INTERNAL: CUDA error: Failed call to cuInit: CUDA_ERROR_NO_DEVICE: no CUDA-capable device is detected
array([[ 0.21370773, 0.68232536], [-1.675768 , -0.09274741], [ 0.5233947 , 1.6713623 ], ..., [ 1.3039094 , -0.8307837 ], [ 0.17580122, -0.15649901], [ 0.17348166, -0.39425728]], shape=(5000, 2), dtype=float32)

Before any training: what did IG init already buy us?

RBIG on its own produces a density estimate. Below we compare the mean log-likelihood (per sample) of the IG-initialised flow against the random-init flow, both before a single gradient step.

mean log-likelihood before training:
  random init : -881.520 nats/sample
  IG init     : -1.652 nats/sample  (improvement: +879.868)

Pushforward at IG-init time

With no gradient training whatsoever, the pushforward of the IG-initialised flow should already be approximately N(0,I)\mathcal{N}(0, I). Jointplots make this immediately visible — the IG scatter should look isotropic and the marginals should line up with the black N(0,1)\mathcal{N}(0, 1) curves, while the random-init pushforward won’t.

/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>
/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>
IG-init pushforward (pre-training)
  mean:    [+0.000, -0.000]   target = [0, 0]
  cov[0,0]:1.003    cov[1,1]:1.006    target = 1, 1
  cov[0,1]:+0.004                          target = 0

Training comparison

Both flows, same optimiser, same 150 epochs. The IG curve starts at (and finishes near) the random curve’s final value — the zoomed panel on the right makes the small end-of-training gap legible.

<Figure size 1870x715 with 2 Axes>
final loss:  random init = +1.232   IG init = +1.149

Density and samples after training

<Figure size 1870x825 with 4 Axes>
/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>
/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>

3. Coupling flow — random vs IG init

Same experiment with a coupling flow. IG init for coupling layers is weaker in isolation (the conditioner is not exercised at init — it emits a constant bias), but it still lets training skip the “the conditioner is effectively random, so the coupling transform is random” phase by handing it a sensible starting point where the conditioner behaves as a well-fit diagonal marginal.

array([[ 0.33592415, -0.63961166], [-1.7532271 , -0.11788122], [ 0.52066326, -1.6266781 ], ..., [ 1.1722953 , 0.8738398 ], [ 0.15655102, 0.20274866], [ 0.06255252, 0.3414428 ]], shape=(5000, 2), dtype=float32)
<Figure size 1870x715 with 2 Axes>
final loss:  random init = +1.400   IG init = +1.092

Coupling density and samples after training

<Figure size 1870x825 with 4 Axes>
/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>
/home/azureuser/.cache/uv/archive-v0/aYSeLZUlluhRY4DCBNG7F/lib/python3.13/site-packages/seaborn/axisgrid.py:1766: UserWarning: The figure layout has changed to tight
  f.tight_layout()
<Figure size 825x825 with 3 Axes>

4. Verifying the zero-kernel contract

Immediately after initialize_flow_from_ig every MixtureCDFCoupling’s final Dense has a zero kernel (so the conditioner emits its bias for any input — the coupling layer behaves as a diagonal marginal). During gradient training those kernels become non-zero as the conditioner learns to modulate on xax_a.

Below: build a fresh coupling flow, IG-init it, check the kernels before any training, then train briefly and check again.

fresh flow (before IG init):      max|W| per coupling = ['0.000', '0.000', '0.000', '0.000', '0.000', '0.000', '0.000', '0.000']
right after IG init (no train):   max|W| per coupling = ['0.0e+00', '0.0e+00', '0.0e+00', '0.0e+00', '0.0e+00', '0.0e+00', '0.0e+00', '0.0e+00']
all zero after IG init : True
after 20 epochs of training:      max|W| per coupling = ['0.283', '0.394', '0.373', '0.376', '0.556', '0.181', '0.145', '0.226']
(non-zero is expected — the conditioner has started to modulate on x_a)

Recap

  • RBIG (Laparra & Malo 2011) is a non-gradient greedy warm-start that fits each block’s parameters in sequence (PCA for rotation, sklearn GaussianMixture per dim for marginal/coupling) and propagates the data forward.
  • For the diagonal flow the init is exact: the layer’s (π,μ,σ)(\pi, \mu, \sigma) parameters are exactly the GMM fit, and the pre-training pushforward is already near N(0,I)\mathcal{N}(0, I).
  • For the coupling flow the conditioner’s inner Dense weights stay random, but the final Dense is forced to kernel=0, bias = fitted mixture params. So at init the coupling layer behaves like a diagonal marginal layer with the IG-fitted mixture; gradient training then lets the conditioner become genuinely conditional.
  • End result in both cases: lower starting NLL, faster convergence, and fewer chances for the optimiser to get stuck in a bad local minimum.