View on GitHub Needs the datasets and trained checkpoints, and a GPU (see the notebook's first cells).

Open In Colab

Leading eigenvectors of G(x)#

  • Author: laquitainesteeve@gmail.com

  • Goal: Plot an example image with the receptive fields of the V1 and V4 models, and the leading eigenvectors of G(x) and J(x) (paper Fig. 3D–F).

  • Requirements: TinyImageNet and the trained checkpoints (downloaded below), and a GPU.

  • Time: about 2 min.

1. Setup#

1.1. Install package#

On Google Colab, install multigeom and diffusers; elsewhere this does nothing.

time: <1 sec

[7]:
%%time

# on Google Colab install the package from PyPI, without its dependencies: Colab already
# has torch, numpy, scipy and matplotlib, and reinstalling torch is slow and can break
# the GPU runtime (nothing to do on your own machine)
import sys

if "google.colab" in sys.modules:
    %pip install -q --no-deps --pre multigeom
    %pip install -q "diffusers>=0.30,<0.38"     # the diffusion model of this notebook
CPU times: user 4 μs, sys: 0 ns, total: 4 μs
Wall time: 5.01 μs

2. Import packages and custom functions#

2.1. Imports and parameters#

Work from the repository root, import the packages, and choose the encoders and the example image.

time: <1 sec

[8]:
%%time

# import built-in packages
import os, sys, subprocess
from pathlib import Path
import torch
import copy, time
import matplotlib.pyplot as plt
from huggingface_hub import snapshot_download
from PIL import Image

# move to the repository root (the folder containing environment.yml): data/ and runs/
# are read from there
root = Path.cwd().resolve()
while not (root / "environment.yml").exists() and root != root.parent:
    root = root.parent

if (root / "environment.yml").exists():
    os.chdir(root)

sys.path.insert(0, str(root))  # the figures/ folder of this repository
print("repo root:", root)

# import custom package
from multigeom import compute_g, compute_fisher
from multigeom.config import load_config
from multigeom.utils import get_device, make_generator
from multigeom.datasets import (
    CANONICAL_TRANSFORM,
    TinyImageNetDataset,
    download_tinyimagenet,
)

from multigeom.encoders import build_encoder
from multigeom.diffusion.model import build_diffusion_model, build_scheduler
from figures.fig3 import panel_rf, panel_eigvecs
from figures.fig4 import panel_eigvecs as panel_eigvecs_fig4
from figures.style import panel_label

# choose an encoder (and area) to load in section 5, and the encoders to compare in
# section 6
ENC, AREA = "inception", "V1"
ENCODERS = ["inception", "resnet50"]  # onoff checkpoints are not available
AREAS = ["V1", "V4"]

# load the encoder, diffusion and estimator configs (packaged with multigeom)
enc_cfg = load_config(ENC)
diff_cfg = load_config("diffusion")
est_cfg = load_config("estimator")

# choose the image for which to estimate G(x) and J(x): the example image of paper Fig.
# 3D (first TinyImageNet *training* image of the class n01644900, a frog)
IMG_PATH = "data/tiny-imagenet-200/train/n01644900/images/n01644900_0.JPEG"
N_MC = 50  # (default: 200) number of monte carlo samples

# compute device
device = get_device(None)
print("device:", device)
repo root: /home/steeve/steeve/idv/code/multigeom-tutorials
device: cuda
CPU times: user 2.59 ms, sys: 34 μs, total: 2.62 ms
Wall time: 2.5 ms

2.2. Custom functions#

load_models loads the encoder and the diffusion model fitted to one encoder and area.

time: <1 sec

[9]:
%%time


def load_models(enc_name, area):
    """The checkpoint, the encoder and the diffusion model fitted to one area."""
    path = f"runs/encoders/{enc_name}/{area}/best_model.pt"
    ckpt = torch.load(path, map_location="cpu", weights_only=False)
    encoder = build_encoder(load_config(enc_name)).to(device)
    encoder.load_state_dict(ckpt["model_state_dict"])
    encoder.eval()

    path = f"runs/diffusion/{enc_name}/{area}/final_model.pt"
    unet = build_diffusion_model(diff_cfg).to(device)
    unet.load_state_dict(
        torch.load(path, map_location=device, weights_only=False)["model_state_dict"]
    )
    unet.eval()
    return ckpt, encoder, unet
CPU times: user 4 μs, sys: 0 ns, total: 4 μs
Wall time: 5.72 μs

3. Download trained encoders and diffusion models#

The checkpoints (the fitted readouts and the diffusion models) are on Hugging Face. The backbone weights (InceptionV3, ResNet-50) are downloaded by torchvision on first use.

time: 22 secs

[10]:
%%time

# download the checkpoints into runs/ (files that are already there are kept)
snapshot_download(repo_id="steevelaquitaine/multigeom", local_dir="runs")
CPU times: user 5.33 s, sys: 3.71 s, total: 9.04 s
Wall time: 22.8 s
[10]:
'/home/steeve/steeve/idv/code/multigeom-tutorials/runs'

4. Download datasets#

TinyImageNet gives the example image (240 MB the first time). TVSD and the THINGS images are only needed to refit the encoders, not here.

time: <1 sec

[11]:
%%time

# TinyImageNet: the example image and the test images (nothing if already there)
download_tinyimagenet()
CPU times: user 175 μs, sys: 73 μs, total: 248 μs
Wall time: 192 μs
[11]:
PosixPath('/home/steeve/steeve/idv/code/multigeom-tutorials/data/tiny-imagenet-200')

5. Load one model pair#

Encoder f_φ (frozen backbone + fitted readout) and the conditional diffusion model ε_θ, here for inception / V1. Change ENC and AREA to look at another configuration.

time: <1 secs

[12]:
%%time

# encoder
encoder = build_encoder(enc_cfg).to(device)
encoder.load_state_dict(
    torch.load(
        f"runs/encoders/{ENC}/{AREA}/best_model.pt",
        map_location=device,
        weights_only=False,
    )["model_state_dict"]
)

encoder.eval()

# diffusion model
unet = build_diffusion_model(diff_cfg).to(device)
diff_ckpt = torch.load(
    f"runs/diffusion/{ENC}/{AREA}/final_model.pt",
    map_location=device,
    weights_only=False,
)

unet.load_state_dict(diff_ckpt["model_state_dict"])
unet.eval()
scheduler = build_scheduler(diff_cfg)
alphas_cumprod = scheduler.alphas_cumprod.to(device)

# report info
print("diffusion epoch:", diff_ckpt.get("epoch"))
diffusion epoch: 120
CPU times: user 855 ms, sys: 195 ms, total: 1.05 s
Wall time: 665 ms

6. Estimate G(x) and J(x), and plot paper Fig. 3D-F and Fig. 4B-C#

We estimate G(x) and J(x) for the example image, for Inception and ResNet-50 fitted to V1 and V4, and plot their leading eigenvectors. N_MC is the number of Monte-Carlo samples (the paper uses 200, about 15 s per image, encoder and area); use fewer for a quick look. Results are not saved.

Details

  • Fig. 3D-F (Inception): the image with the V1 and V4 receptive fields (D), and the leading four eigenvectors of G (E) and of J (F), V1 on top and V4 below.

  • Fig. 4B-C (Inception and ResNet-50): the leading eigenvectors of G (B) and of J (C) for each architecture, V1 block above the V4 block. The paper also shows the ON-OFF model, for which we have no checkpoints (empty rows).

The plotting functions are in figures.fig3 and figures.fig4.

6.1. Compute#

time: 12 secs with N_MC = 50 (about 1 min with 200)

[13]:
%%time

# 1. estimator settings, and the example image
cfg = copy.deepcopy(est_cfg)
cfg.monte_carlo.n_samples = N_MC
sigma = float(cfg.monte_carlo.observation_noise_sigma)

# the example image, [1, 1, 64, 64] in [-1, 1]
x = CANONICAL_TRANSFORM(Image.open(IMG_PATH)).unsqueeze(0).to(device)

# 2. for every encoder and area: load the models, then estimate G(x) and J(x)
# both keyed by (encoder, area): the encoder checkpoint; the image and the eigenpairs
# of G and J
ckpts, records = {}, {}
for enc_name in ENCODERS:
    for area in AREAS:
        # encoder and diffusion model fitted to this area
        ckpts[(enc_name, area)], area_encoder, area_unet = load_models(enc_name, area)

        # G(x) (conditional-pair estimator) and J(x) (closed form)
        g = compute_g(
            image=x,
            encoder=area_encoder,
            unet=area_unet,
            alphas_cumprod=alphas_cumprod,
            cfg=cfg,
            device=device,
            generator=make_generator(int(cfg.seed), device),
        )
        j = compute_fisher(
            image=x,
            encoder=area_encoder,
            sigma=sigma,
            top_k=int(cfg.fisher_baseline.top_k),
            device=device,
        )
        records[(enc_name, area)] = {
            "image": x.cpu(),
            "G_eigenvalues": g.eigenvalues.cpu(),
            "G_eigenvectors": g.eigenvectors.cpu(),
            "J_eigenvalues": j.eigenvalues.cpu(),
            "J_eigenvectors": j.eigenvectors.cpu(),
        }
        print(
            enc_name,
            area,
            "G:",
            tuple(g.eigenvectors.shape),
            " J:",
            tuple(j.eigenvectors.shape),
        )
inception V1 G: (50, 4096)  J: (50, 4096)
inception V4 G: (50, 4096)  J: (50, 4096)
resnet50 V1 G: (50, 4096)  J: (50, 4096)
resnet50 V4 G: (50, 4096)  J: (50, 4096)
CPU times: user 56 s, sys: 3.1 s, total: 59.1 s
Wall time: 15.1 s

6.2. Plot leading eigenvectors for inception#

These panels correspond to the paper’s Fig. 3D-F.

time: <1 sec

Differences with the paper

  • The diffusion checkpoints report epoch 120, not the 200 used for the paper.

  • Monte-Carlo noise of the G(x) estimator: seed 42.

  • The encoder weight checkpoint may differ from the paper’s.

[14]:
%%time

# Fig. 3D-F: receptive fields, and leading eigenvectors of G(x) and J(x) for Inception
fig = plt.figure(figsize=(15, 3.9))
spec = fig.add_gridspec(1, 3, width_ratios=[1.0, 2.2, 2.2], wspace=0.15)
ax_d = fig.add_subplot(spec[0])

# D: image with V1 and V4 receptive fields
panel_rf(ax_d, records[(ENC, "V1")]["image"], {a: ckpts[(ENC, a)] for a in AREAS})

# E: leading eigenvectors of G(x); F: those of J(x)
ax_e = panel_eigvecs(fig, spec[1], {a: records[(ENC, a)] for a in AREAS}, "G", 4)
ax_f = panel_eigvecs(fig, spec[2], {a: records[(ENC, a)] for a in AREAS}, "J", 4)
for letter, ax in zip("DEF", (ax_d, ax_e, ax_f)):
    panel_label(fig, ax, letter)
CPU times: user 520 ms, sys: 2.87 ms, total: 523 ms
Wall time: 69.9 ms
../_images/demo_notebooks_3_leading_eigenvectors_of_G_21_1.png

6.3. Plot comparison inception vs resnet50#

  • cf. paper’s Fig. 4B-C

For ResNet-50, each displayed eigenvector is the one, among its first 30, that best matches (largest |cosine|) the Inception eigenvector of the same column, with its sign flipped to agree. This only changes the display.

time: <1 sec

[15]:
%%time

# Fig. 4B-C: leading eigenvectors of G(x) and J(x) for each encoder
fig = plt.figure(figsize=(11, 8.5))
spec = fig.add_gridspec(1, 2, wspace=0.22)

# B: leading eigenvectors of G(x), per encoder; C: those of J(x)
ax_b = panel_eigvecs_fig4(fig, spec[0], records, "G", 3, 30)
ax_c = panel_eigvecs_fig4(fig, spec[1], records, "J", 3, 30)
for letter, ax in zip("BC", (ax_b, ax_c)):
    panel_label(fig, ax, letter)
CPU times: user 855 ms, sys: 0 ns, total: 855 ms
Wall time: 252 ms
../_images/demo_notebooks_3_leading_eigenvectors_of_G_24_1.png