diff --git a/MindEye_Imagery/src/generative_models/data/DejaVuSans.ttf b/MindEye_Imagery/src/generative_models/data/DejaVuSans.ttf new file mode 100644 index 0000000000000000000000000000000000000000..e5f7eecce43be41ff0703ed99e1553029b849f14 Binary files /dev/null and b/MindEye_Imagery/src/generative_models/data/DejaVuSans.ttf differ diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/__init__.py b/MindEye_Imagery/src/generative_models/scripts/demo/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/detect.py b/MindEye_Imagery/src/generative_models/scripts/demo/detect.py new file mode 100644 index 0000000000000000000000000000000000000000..96e9f212b868e60602b0b1880a0f3d7c66e16703 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/detect.py @@ -0,0 +1,156 @@ +import argparse + +import cv2 +import numpy as np + +try: + from imwatermark import WatermarkDecoder +except ImportError as e: + try: + # Assume some of the other dependencies such as torch are not fulfilled + # import file without loading unnecessary libraries. + import importlib.util + import sys + + spec = importlib.util.find_spec("imwatermark.maxDct") + assert spec is not None + maxDct = importlib.util.module_from_spec(spec) + sys.modules["maxDct"] = maxDct + spec.loader.exec_module(maxDct) + + class WatermarkDecoder(object): + """A minimal version of + https://github.com/ShieldMnt/invisible-watermark/blob/main/imwatermark/watermark.py + to only reconstruct bits using dwtDct""" + + def __init__(self, wm_type="bytes", length=0): + assert wm_type == "bits", "Only bits defined in minimal import" + self._wmType = wm_type + self._wmLen = length + + def reconstruct(self, bits): + if len(bits) != self._wmLen: + raise RuntimeError("bits are not matched with watermark length") + + return bits + + def decode(self, cv2Image, method="dwtDct", **configs): + (r, c, channels) = cv2Image.shape + if r * c < 256 * 256: + raise RuntimeError("image too small, should be larger than 256x256") + + bits = [] + assert method == "dwtDct" + embed = maxDct.EmbedMaxDct(watermarks=[], wmLen=self._wmLen, **configs) + bits = embed.decode(cv2Image) + return self.reconstruct(bits) + + except: + raise e + + +# A fixed 48-bit message that was choosen at random +# WATERMARK_MESSAGE = 0xB3EC907BB19E +WATERMARK_MESSAGE = 0b101100111110110010010000011110111011000110011110 +# bin(x)[2:] gives bits of x as str, use int to convert them to 0/1 +WATERMARK_BITS = [int(bit) for bit in bin(WATERMARK_MESSAGE)[2:]] +MATCH_VALUES = [ + [27, "No watermark detected"], + [33, "Partial watermark match. Cannot determine with certainty."], + [ + 35, + ( + "Likely watermarked. In our test 0.02% of real images were " + 'falsely detected as "Likely watermarked"' + ), + ], + [ + 49, + ( + "Very likely watermarked. In our test no real images were " + 'falsely detected as "Very likely watermarked"' + ), + ], +] + + +class GetWatermarkMatch: + def __init__(self, watermark): + self.watermark = watermark + self.num_bits = len(self.watermark) + self.decoder = WatermarkDecoder("bits", self.num_bits) + + def __call__(self, x: np.ndarray) -> np.ndarray: + """ + Detects the number of matching bits the predefined watermark with one + or multiple images. Images should be in cv2 format, e.g. h x w x c BGR. + + Args: + x: ([B], h w, c) in range [0, 255] + + Returns: + number of matched bits ([B],) + """ + squeeze = len(x.shape) == 3 + if squeeze: + x = x[None, ...] + + bs = x.shape[0] + detected = np.empty((bs, self.num_bits), dtype=bool) + for k in range(bs): + detected[k] = self.decoder.decode(x[k], "dwtDct") + result = np.sum(detected == self.watermark, axis=-1) + if squeeze: + return result[0] + else: + return result + + +get_watermark_match = GetWatermarkMatch(WATERMARK_BITS) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument( + "filename", + nargs="+", + type=str, + help="Image files to check for watermarks", + ) + opts = parser.parse_args() + + print( + """ + This script tries to detect watermarked images. Please be aware of + the following: + - As the watermark is supposed to be invisible, there is the risk that + watermarked images may not be detected. + - To maximize the chance of detection make sure that the image has the same + dimensions as when the watermark was applied (most likely 1024x1024 + or 512x512). + - Specific image manipulation may drastically decrease the chance that + watermarks can be detected. + - There is also the chance that an image has the characteristics of the + watermark by chance. + - The watermark script is public, anybody may watermark any images, and + could therefore claim it to be generated. + - All numbers below are based on a test using 10,000 images without any + modifications after applying the watermark. + """ + ) + + for fn in opts.filename: + image = cv2.imread(fn) + if image is None: + print(f"Couldn't read {fn}. Skipping") + continue + + num_bits = get_watermark_match(image) + k = 0 + while num_bits > MATCH_VALUES[k][0]: + k += 1 + print( + f"{fn}: {MATCH_VALUES[k][1]}", + f"Bits that matched the watermark {num_bits} from {len(WATERMARK_BITS)}\n", + sep="\n\t", + ) diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/discretization.py b/MindEye_Imagery/src/generative_models/scripts/demo/discretization.py new file mode 100644 index 0000000000000000000000000000000000000000..b7030a229692e25ddf8d2f516aef2aba0e6149b9 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/discretization.py @@ -0,0 +1,59 @@ +import torch + +from sgm.modules.diffusionmodules.discretizer import Discretization + + +class Img2ImgDiscretizationWrapper: + """ + wraps a discretizer, and prunes the sigmas + params: + strength: float between 0.0 and 1.0. 1.0 means full sampling (all sigmas are returned) + """ + + def __init__(self, discretization: Discretization, strength: float = 1.0): + self.discretization = discretization + self.strength = strength + assert 0.0 <= self.strength <= 1.0 + + def __call__(self, *args, **kwargs): + # sigmas start large first, and decrease then + sigmas = self.discretization(*args, **kwargs) + print(f"sigmas after discretization, before pruning img2img: ", sigmas) + sigmas = torch.flip(sigmas, (0,)) + sigmas = sigmas[: max(int(self.strength * len(sigmas)), 1)] + print("prune index:", max(int(self.strength * len(sigmas)), 1)) + sigmas = torch.flip(sigmas, (0,)) + print(f"sigmas after pruning: ", sigmas) + return sigmas + + +class Txt2NoisyDiscretizationWrapper: + """ + wraps a discretizer, and prunes the sigmas + params: + strength: float between 0.0 and 1.0. 0.0 means full sampling (all sigmas are returned) + """ + + def __init__( + self, discretization: Discretization, strength: float = 0.0, original_steps=None + ): + self.discretization = discretization + self.strength = strength + self.original_steps = original_steps + assert 0.0 <= self.strength <= 1.0 + + def __call__(self, *args, **kwargs): + # sigmas start large first, and decrease then + sigmas = self.discretization(*args, **kwargs) + print(f"sigmas after discretization, before pruning img2img: ", sigmas) + sigmas = torch.flip(sigmas, (0,)) + if self.original_steps is None: + steps = len(sigmas) + else: + steps = self.original_steps + 1 + prune_index = max(min(int(self.strength * steps) - 1, steps - 1), 0) + sigmas = sigmas[prune_index:] + print("prune index:", prune_index) + sigmas = torch.flip(sigmas, (0,)) + print(f"sigmas after pruning: ", sigmas) + return sigmas diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/sampling.py b/MindEye_Imagery/src/generative_models/scripts/demo/sampling.py new file mode 100644 index 0000000000000000000000000000000000000000..46c20048176aeb0db96114f95d760091a46d49cd --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/sampling.py @@ -0,0 +1,364 @@ +from pytorch_lightning import seed_everything + +from scripts.demo.streamlit_helpers import * + +SAVE_PATH = "outputs/demo/txt2img/" + +SD_XL_BASE_RATIOS = { + "0.5": (704, 1408), + "0.52": (704, 1344), + "0.57": (768, 1344), + "0.6": (768, 1280), + "0.68": (832, 1216), + "0.72": (832, 1152), + "0.78": (896, 1152), + "0.82": (896, 1088), + "0.88": (960, 1088), + "0.94": (960, 1024), + "1.0": (1024, 1024), + "1.07": (1024, 960), + "1.13": (1088, 960), + "1.21": (1088, 896), + "1.29": (1152, 896), + "1.38": (1152, 832), + "1.46": (1216, 832), + "1.67": (1280, 768), + "1.75": (1344, 768), + "1.91": (1344, 704), + "2.0": (1408, 704), + "2.09": (1472, 704), + "2.4": (1536, 640), + "2.5": (1600, 640), + "2.89": (1664, 576), + "3.0": (1728, 576), +} + +VERSION2SPECS = { + "SDXL-base-1.0": { + "H": 1024, + "W": 1024, + "C": 4, + "f": 8, + "is_legacy": False, + "config": "configs/inference/sd_xl_base.yaml", + "ckpt": "checkpoints/sd_xl_base_1.0.safetensors", + }, + "SDXL-base-0.9": { + "H": 1024, + "W": 1024, + "C": 4, + "f": 8, + "is_legacy": False, + "config": "configs/inference/sd_xl_base.yaml", + "ckpt": "checkpoints/sd_xl_base_0.9.safetensors", + }, + "SD-2.1": { + "H": 512, + "W": 512, + "C": 4, + "f": 8, + "is_legacy": True, + "config": "configs/inference/sd_2_1.yaml", + "ckpt": "checkpoints/v2-1_512-ema-pruned.safetensors", + }, + "SD-2.1-768": { + "H": 768, + "W": 768, + "C": 4, + "f": 8, + "is_legacy": True, + "config": "configs/inference/sd_2_1_768.yaml", + "ckpt": "checkpoints/v2-1_768-ema-pruned.safetensors", + }, + "SDXL-refiner-0.9": { + "H": 1024, + "W": 1024, + "C": 4, + "f": 8, + "is_legacy": True, + "config": "configs/inference/sd_xl_refiner.yaml", + "ckpt": "checkpoints/sd_xl_refiner_0.9.safetensors", + }, + "SDXL-refiner-1.0": { + "H": 1024, + "W": 1024, + "C": 4, + "f": 8, + "is_legacy": True, + "config": "configs/inference/sd_xl_refiner.yaml", + "ckpt": "checkpoints/sd_xl_refiner_1.0.safetensors", + }, +} + + +def load_img(display=True, key=None, device="cuda"): + image = get_interactive_image(key=key) + if image is None: + return None + if display: + st.image(image) + w, h = image.size + print(f"loaded input image of size ({w}, {h})") + width, height = map( + lambda x: x - x % 64, (w, h) + ) # resize to integer multiple of 64 + image = image.resize((width, height)) + image = np.array(image.convert("RGB")) + image = image[None].transpose(0, 3, 1, 2) + image = torch.from_numpy(image).to(dtype=torch.float32) / 127.5 - 1.0 + return image.to(device) + + +def run_txt2img( + state, + version, + version_dict, + is_legacy=False, + return_latents=False, + filter=None, + stage2strength=None, +): + if version.startswith("SDXL-base"): + W, H = st.selectbox("Resolution:", list(SD_XL_BASE_RATIOS.values()), 10) + else: + H = st.number_input("H", value=version_dict["H"], min_value=64, max_value=2048) + W = st.number_input("W", value=version_dict["W"], min_value=64, max_value=2048) + C = version_dict["C"] + F = version_dict["f"] + + init_dict = { + "orig_width": W, + "orig_height": H, + "target_width": W, + "target_height": H, + } + value_dict = init_embedder_options( + get_unique_embedder_keys_from_conditioner(state["model"].conditioner), + init_dict, + prompt=prompt, + negative_prompt=negative_prompt, + ) + sampler, num_rows, num_cols = init_sampling(stage2strength=stage2strength) + num_samples = num_rows * num_cols + + if st.button("Sample"): + st.write(f"**Model I:** {version}") + out = do_sample( + state["model"], + sampler, + value_dict, + num_samples, + H, + W, + C, + F, + force_uc_zero_embeddings=["txt"] if not is_legacy else [], + return_latents=return_latents, + filter=filter, + ) + return out + + +def run_img2img( + state, + version_dict, + is_legacy=False, + return_latents=False, + filter=None, + stage2strength=None, +): + img = load_img() + if img is None: + return None + H, W = img.shape[2], img.shape[3] + + init_dict = { + "orig_width": W, + "orig_height": H, + "target_width": W, + "target_height": H, + } + value_dict = init_embedder_options( + get_unique_embedder_keys_from_conditioner(state["model"].conditioner), + init_dict, + prompt=prompt, + negative_prompt=negative_prompt, + ) + strength = st.number_input( + "**Img2Img Strength**", value=0.75, min_value=0.0, max_value=1.0 + ) + sampler, num_rows, num_cols = init_sampling( + img2img_strength=strength, + stage2strength=stage2strength, + ) + num_samples = num_rows * num_cols + + if st.button("Sample"): + out = do_img2img( + repeat(img, "1 ... -> n ...", n=num_samples), + state["model"], + sampler, + value_dict, + num_samples, + force_uc_zero_embeddings=["txt"] if not is_legacy else [], + return_latents=return_latents, + filter=filter, + ) + return out + + +def apply_refiner( + input, + state, + sampler, + num_samples, + prompt, + negative_prompt, + filter=None, + finish_denoising=False, +): + init_dict = { + "orig_width": input.shape[3] * 8, + "orig_height": input.shape[2] * 8, + "target_width": input.shape[3] * 8, + "target_height": input.shape[2] * 8, + } + + value_dict = init_dict + value_dict["prompt"] = prompt + value_dict["negative_prompt"] = negative_prompt + + value_dict["crop_coords_top"] = 0 + value_dict["crop_coords_left"] = 0 + + value_dict["aesthetic_score"] = 6.0 + value_dict["negative_aesthetic_score"] = 2.5 + + st.warning(f"refiner input shape: {input.shape}") + samples = do_img2img( + input, + state["model"], + sampler, + value_dict, + num_samples, + skip_encode=True, + filter=filter, + add_noise=not finish_denoising, + ) + + return samples + + +if __name__ == "__main__": + st.title("Stable Diffusion") + version = st.selectbox("Model Version", list(VERSION2SPECS.keys()), 0) + version_dict = VERSION2SPECS[version] + if st.checkbox("Load Model"): + mode = st.radio("Mode", ("txt2img", "img2img"), 0) + else: + mode = "skip" + st.write("__________________________") + + set_lowvram_mode(st.checkbox("Low vram mode", True)) + + if version.startswith("SDXL-base"): + add_pipeline = st.checkbox("Load SDXL-refiner?", False) + st.write("__________________________") + else: + add_pipeline = False + + seed = st.sidebar.number_input("seed", value=42, min_value=0, max_value=int(1e9)) + seed_everything(seed) + + save_locally, save_path = init_save_locally(os.path.join(SAVE_PATH, version)) + + if mode != "skip": + state = init_st(version_dict, load_filter=True) + if state["msg"]: + st.info(state["msg"]) + model = state["model"] + + is_legacy = version_dict["is_legacy"] + + prompt = st.text_input( + "prompt", + "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k", + ) + if is_legacy: + negative_prompt = st.text_input("negative prompt", "") + else: + negative_prompt = "" # which is unused + + stage2strength = None + finish_denoising = False + + if add_pipeline: + st.write("__________________________") + version2 = st.selectbox("Refiner:", ["SDXL-refiner-1.0", "SDXL-refiner-0.9"]) + st.warning( + f"Running with {version2} as the second stage model. Make sure to provide (V)RAM :) " + ) + st.write("**Refiner Options:**") + + version_dict2 = VERSION2SPECS[version2] + state2 = init_st(version_dict2, load_filter=False) + st.info(state2["msg"]) + + stage2strength = st.number_input( + "**Refinement strength**", value=0.15, min_value=0.0, max_value=1.0 + ) + + sampler2, *_ = init_sampling( + key=2, + img2img_strength=stage2strength, + specify_num_samples=False, + ) + st.write("__________________________") + finish_denoising = st.checkbox("Finish denoising with refiner.", True) + if not finish_denoising: + stage2strength = None + + if mode == "txt2img": + out = run_txt2img( + state, + version, + version_dict, + is_legacy=is_legacy, + return_latents=add_pipeline, + filter=state.get("filter"), + stage2strength=stage2strength, + ) + elif mode == "img2img": + out = run_img2img( + state, + version_dict, + is_legacy=is_legacy, + return_latents=add_pipeline, + filter=state.get("filter"), + stage2strength=stage2strength, + ) + elif mode == "skip": + out = None + else: + raise ValueError(f"unknown mode {mode}") + if isinstance(out, (tuple, list)): + samples, samples_z = out + else: + samples = out + samples_z = None + + if add_pipeline and samples_z is not None: + st.write("**Running Refinement Stage**") + samples = apply_refiner( + samples_z, + state2, + sampler2, + samples_z.shape[0], + prompt=prompt, + negative_prompt=negative_prompt if is_legacy else "", + filter=state.get("filter"), + finish_denoising=finish_denoising, + ) + + if save_locally and samples is not None: + perform_save_locally(save_path, samples) diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/streamlit_helpers.py b/MindEye_Imagery/src/generative_models/scripts/demo/streamlit_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..6c5760e26fcffefc95ddc5435870b2a804257e51 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/streamlit_helpers.py @@ -0,0 +1,887 @@ +import copy +import math +import os +from glob import glob +from typing import Dict, List, Optional, Tuple, Union + +import cv2 +import numpy as np +import streamlit as st +import torch +import torch.nn as nn +import torchvision.transforms as TT +from einops import rearrange, repeat +from imwatermark import WatermarkEncoder +from omegaconf import ListConfig, OmegaConf +from PIL import Image +from safetensors.torch import load_file as load_safetensors +from torch import autocast +from torchvision import transforms +from torchvision.utils import make_grid, save_image + +from scripts.demo.discretization import (Img2ImgDiscretizationWrapper, + Txt2NoisyDiscretizationWrapper) +from scripts.util.detection.nsfw_and_watermark_dectection import \ + DeepFloydDataFiltering +from sgm.inference.helpers import embed_watermark +from sgm.modules.diffusionmodules.guiders import (LinearPredictionGuider, + VanillaCFG) +from sgm.modules.diffusionmodules.sampling import (DPMPP2MSampler, + DPMPP2SAncestralSampler, + EulerAncestralSampler, + EulerEDMSampler, + HeunEDMSampler, + LinearMultistepSampler) +from sgm.util import append_dims, default, instantiate_from_config + + +@st.cache_resource() +def init_st(version_dict, load_ckpt=True, load_filter=True): + state = dict() + if not "model" in state: + config = version_dict["config"] + ckpt = version_dict["ckpt"] + + config = OmegaConf.load(config) + model, msg = load_model_from_config(config, ckpt if load_ckpt else None) + + state["msg"] = msg + state["model"] = model + state["ckpt"] = ckpt if load_ckpt else None + state["config"] = config + if load_filter: + state["filter"] = DeepFloydDataFiltering(verbose=False) + return state + + +def load_model(model): + model.cuda() + + +lowvram_mode = False + + +def set_lowvram_mode(mode): + global lowvram_mode + lowvram_mode = mode + + +def initial_model_load(model): + global lowvram_mode + if lowvram_mode: + model.model.half() + else: + model.cuda() + return model + + +def unload_model(model): + global lowvram_mode + if lowvram_mode: + model.cpu() + torch.cuda.empty_cache() + + +def load_model_from_config(config, ckpt=None, verbose=True): + model = instantiate_from_config(config.model) + + if ckpt is not None: + print(f"Loading model from {ckpt}") + if ckpt.endswith("ckpt"): + pl_sd = torch.load(ckpt, map_location="cpu") + if "global_step" in pl_sd: + global_step = pl_sd["global_step"] + st.info(f"loaded ckpt from global step {global_step}") + print(f"Global Step: {pl_sd['global_step']}") + sd = pl_sd["state_dict"] + elif ckpt.endswith("safetensors"): + sd = load_safetensors(ckpt) + else: + raise NotImplementedError + + msg = None + + m, u = model.load_state_dict(sd, strict=False) + + if len(m) > 0 and verbose: + print("missing keys:") + print(m) + if len(u) > 0 and verbose: + print("unexpected keys:") + print(u) + else: + msg = None + + model = initial_model_load(model) + model.eval() + return model, msg + + +def get_unique_embedder_keys_from_conditioner(conditioner): + return list(set([x.input_key for x in conditioner.embedders])) + + +def init_embedder_options(keys, init_dict, prompt=None, negative_prompt=None): + # Hardcoded demo settings; might undergo some changes in the future + + value_dict = {} + for key in keys: + if key == "txt": + if prompt is None: + prompt = "A professional photograph of an astronaut riding a pig" + if negative_prompt is None: + negative_prompt = "" + + prompt = st.text_input("Prompt", prompt) + negative_prompt = st.text_input("Negative prompt", negative_prompt) + + value_dict["prompt"] = prompt + value_dict["negative_prompt"] = negative_prompt + + if key == "original_size_as_tuple": + orig_width = st.number_input( + "orig_width", + value=init_dict["orig_width"], + min_value=16, + ) + orig_height = st.number_input( + "orig_height", + value=init_dict["orig_height"], + min_value=16, + ) + + value_dict["orig_width"] = orig_width + value_dict["orig_height"] = orig_height + + if key == "crop_coords_top_left": + crop_coord_top = st.number_input("crop_coords_top", value=0, min_value=0) + crop_coord_left = st.number_input("crop_coords_left", value=0, min_value=0) + + value_dict["crop_coords_top"] = crop_coord_top + value_dict["crop_coords_left"] = crop_coord_left + + if key == "aesthetic_score": + value_dict["aesthetic_score"] = 6.0 + value_dict["negative_aesthetic_score"] = 2.5 + + if key == "target_size_as_tuple": + value_dict["target_width"] = init_dict["target_width"] + value_dict["target_height"] = init_dict["target_height"] + + if key in ["fps_id", "fps"]: + fps = st.number_input("fps", value=6, min_value=1) + + value_dict["fps"] = fps + value_dict["fps_id"] = fps - 1 + + if key == "motion_bucket_id": + mb_id = st.number_input("motion bucket id", 0, 511, value=127) + value_dict["motion_bucket_id"] = mb_id + + if key == "pool_image": + st.text("Image for pool conditioning") + image = load_img( + key="pool_image_input", + size=224, + center_crop=True, + ) + if image is None: + st.info("Need an image here") + image = torch.zeros(1, 3, 224, 224) + value_dict["pool_image"] = image + + return value_dict + + +def perform_save_locally(save_path, samples): + os.makedirs(os.path.join(save_path), exist_ok=True) + base_count = len(os.listdir(os.path.join(save_path))) + samples = embed_watermark(samples) + for sample in samples: + sample = 255.0 * rearrange(sample.cpu().numpy(), "c h w -> h w c") + Image.fromarray(sample.astype(np.uint8)).save( + os.path.join(save_path, f"{base_count:09}.png") + ) + base_count += 1 + + +def init_save_locally(_dir, init_value: bool = False): + save_locally = st.sidebar.checkbox("Save images locally", value=init_value) + if save_locally: + save_path = st.text_input("Save path", value=os.path.join(_dir, "samples")) + else: + save_path = None + + return save_locally, save_path + + +def get_guider(options, key): + guider = st.sidebar.selectbox( + f"Discretization #{key}", + [ + "VanillaCFG", + "IdentityGuider", + "LinearPredictionGuider", + ], + options.get("guider", 0), + ) + + additional_guider_kwargs = options.pop("additional_guider_kwargs", {}) + + if guider == "IdentityGuider": + guider_config = { + "target": "sgm.modules.diffusionmodules.guiders.IdentityGuider" + } + elif guider == "VanillaCFG": + scale = st.number_input( + f"cfg-scale #{key}", + value=options.get("cfg", 5.0), + min_value=0.0, + ) + + guider_config = { + "target": "sgm.modules.diffusionmodules.guiders.VanillaCFG", + "params": { + "scale": scale, + **additional_guider_kwargs, + }, + } + elif guider == "LinearPredictionGuider": + max_scale = st.number_input( + f"max-cfg-scale #{key}", + value=options.get("cfg", 1.5), + min_value=1.0, + ) + min_scale = st.number_input( + f"min guidance scale", + value=options.get("min_cfg", 1.0), + min_value=1.0, + max_value=10.0, + ) + + guider_config = { + "target": "sgm.modules.diffusionmodules.guiders.LinearPredictionGuider", + "params": { + "max_scale": max_scale, + "min_scale": min_scale, + "num_frames": options["num_frames"], + **additional_guider_kwargs, + }, + } + else: + raise NotImplementedError + return guider_config + + +def init_sampling( + key=1, + img2img_strength: Optional[float] = None, + specify_num_samples: bool = True, + stage2strength: Optional[float] = None, + options: Optional[Dict[str, int]] = None, +): + options = {} if options is None else options + + num_rows, num_cols = 1, 1 + if specify_num_samples: + num_cols = st.number_input( + f"num cols #{key}", value=num_cols, min_value=1, max_value=10 + ) + + steps = st.sidebar.number_input( + f"steps #{key}", value=options.get("num_steps", 40), min_value=1, max_value=1000 + ) + sampler = st.sidebar.selectbox( + f"Sampler #{key}", + [ + "EulerEDMSampler", + "HeunEDMSampler", + "EulerAncestralSampler", + "DPMPP2SAncestralSampler", + "DPMPP2MSampler", + "LinearMultistepSampler", + ], + options.get("sampler", 0), + ) + discretization = st.sidebar.selectbox( + f"Discretization #{key}", + [ + "LegacyDDPMDiscretization", + "EDMDiscretization", + ], + options.get("discretization", 0), + ) + + discretization_config = get_discretization(discretization, options=options, key=key) + + guider_config = get_guider(options=options, key=key) + + sampler = get_sampler(sampler, steps, discretization_config, guider_config, key=key) + if img2img_strength is not None: + st.warning( + f"Wrapping {sampler.__class__.__name__} with Img2ImgDiscretizationWrapper" + ) + sampler.discretization = Img2ImgDiscretizationWrapper( + sampler.discretization, strength=img2img_strength + ) + if stage2strength is not None: + sampler.discretization = Txt2NoisyDiscretizationWrapper( + sampler.discretization, strength=stage2strength, original_steps=steps + ) + return sampler, num_rows, num_cols + + +def get_discretization(discretization, options, key=1): + if discretization == "LegacyDDPMDiscretization": + discretization_config = { + "target": "sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization", + } + elif discretization == "EDMDiscretization": + sigma_min = st.number_input( + f"sigma_min #{key}", value=options.get("sigma_min", 0.03) + ) # 0.0292 + sigma_max = st.number_input( + f"sigma_max #{key}", value=options.get("sigma_max", 14.61) + ) # 14.6146 + rho = st.number_input(f"rho #{key}", value=options.get("rho", 3.0)) + discretization_config = { + "target": "sgm.modules.diffusionmodules.discretizer.EDMDiscretization", + "params": { + "sigma_min": sigma_min, + "sigma_max": sigma_max, + "rho": rho, + }, + } + + return discretization_config + + +def get_sampler(sampler_name, steps, discretization_config, guider_config, key=1): + if sampler_name == "EulerEDMSampler" or sampler_name == "HeunEDMSampler": + s_churn = st.sidebar.number_input(f"s_churn #{key}", value=0.0, min_value=0.0) + s_tmin = st.sidebar.number_input(f"s_tmin #{key}", value=0.0, min_value=0.0) + s_tmax = st.sidebar.number_input(f"s_tmax #{key}", value=999.0, min_value=0.0) + s_noise = st.sidebar.number_input(f"s_noise #{key}", value=1.0, min_value=0.0) + + if sampler_name == "EulerEDMSampler": + sampler = EulerEDMSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + s_churn=s_churn, + s_tmin=s_tmin, + s_tmax=s_tmax, + s_noise=s_noise, + verbose=True, + ) + elif sampler_name == "HeunEDMSampler": + sampler = HeunEDMSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + s_churn=s_churn, + s_tmin=s_tmin, + s_tmax=s_tmax, + s_noise=s_noise, + verbose=True, + ) + elif ( + sampler_name == "EulerAncestralSampler" + or sampler_name == "DPMPP2SAncestralSampler" + ): + s_noise = st.sidebar.number_input("s_noise", value=1.0, min_value=0.0) + eta = st.sidebar.number_input("eta", value=1.0, min_value=0.0) + + if sampler_name == "EulerAncestralSampler": + sampler = EulerAncestralSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + eta=eta, + s_noise=s_noise, + verbose=True, + ) + elif sampler_name == "DPMPP2SAncestralSampler": + sampler = DPMPP2SAncestralSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + eta=eta, + s_noise=s_noise, + verbose=True, + ) + elif sampler_name == "DPMPP2MSampler": + sampler = DPMPP2MSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + verbose=True, + ) + elif sampler_name == "LinearMultistepSampler": + order = st.sidebar.number_input("order", value=4, min_value=1) + sampler = LinearMultistepSampler( + num_steps=steps, + discretization_config=discretization_config, + guider_config=guider_config, + order=order, + verbose=True, + ) + else: + raise ValueError(f"unknown sampler {sampler_name}!") + + return sampler + + +def get_interactive_image() -> Image.Image: + image = st.file_uploader("Input", type=["jpg", "JPEG", "png"]) + if image is not None: + image = Image.open(image) + if not image.mode == "RGB": + image = image.convert("RGB") + return image + + +def load_img( + display: bool = True, + size: Union[None, int, Tuple[int, int]] = None, + center_crop: bool = False, +): + image = get_interactive_image() + if image is None: + return None + if display: + st.image(image) + w, h = image.size + print(f"loaded input image of size ({w}, {h})") + + transform = [] + if size is not None: + transform.append(transforms.Resize(size)) + if center_crop: + transform.append(transforms.CenterCrop(size)) + transform.append(transforms.ToTensor()) + transform.append(transforms.Lambda(lambda x: 2.0 * x - 1.0)) + + transform = transforms.Compose(transform) + img = transform(image)[None, ...] + st.text(f"input min/max/mean: {img.min():.3f}/{img.max():.3f}/{img.mean():.3f}") + return img + + +def get_init_img(batch_size=1, key=None): + init_image = load_img(key=key).cuda() + init_image = repeat(init_image, "1 ... -> b ...", b=batch_size) + return init_image + + +def do_sample( + model, + sampler, + value_dict, + num_samples, + H, + W, + C, + F, + force_uc_zero_embeddings: Optional[List] = None, + force_cond_zero_embeddings: Optional[List] = None, + batch2model_input: List = None, + return_latents=False, + filter=None, + T=None, + additional_batch_uc_fields=None, + decoding_t=None, +): + force_uc_zero_embeddings = default(force_uc_zero_embeddings, []) + batch2model_input = default(batch2model_input, []) + additional_batch_uc_fields = default(additional_batch_uc_fields, []) + + st.text("Sampling") + + outputs = st.empty() + precision_scope = autocast + with torch.no_grad(): + with precision_scope("cuda"): + with model.ema_scope(): + if T is not None: + num_samples = [num_samples, T] + else: + num_samples = [num_samples] + + load_model(model.conditioner) + batch, batch_uc = get_batch( + get_unique_embedder_keys_from_conditioner(model.conditioner), + value_dict, + num_samples, + T=T, + additional_batch_uc_fields=additional_batch_uc_fields, + ) + + c, uc = model.conditioner.get_unconditional_conditioning( + batch, + batch_uc=batch_uc, + force_uc_zero_embeddings=force_uc_zero_embeddings, + force_cond_zero_embeddings=force_cond_zero_embeddings, + ) + unload_model(model.conditioner) + + for k in c: + if not k == "crossattn": + c[k], uc[k] = map( + lambda y: y[k][: math.prod(num_samples)].to("cuda"), (c, uc) + ) + if k in ["crossattn", "concat"] and T is not None: + uc[k] = repeat(uc[k], "b ... -> b t ...", t=T) + uc[k] = rearrange(uc[k], "b t ... -> (b t) ...", t=T) + c[k] = repeat(c[k], "b ... -> b t ...", t=T) + c[k] = rearrange(c[k], "b t ... -> (b t) ...", t=T) + + additional_model_inputs = {} + for k in batch2model_input: + if k == "image_only_indicator": + assert T is not None + + if isinstance( + sampler.guider, (VanillaCFG, LinearPredictionGuider) + ): + additional_model_inputs[k] = torch.zeros( + num_samples[0] * 2, num_samples[1] + ).to("cuda") + else: + additional_model_inputs[k] = torch.zeros(num_samples).to( + "cuda" + ) + else: + additional_model_inputs[k] = batch[k] + + shape = (math.prod(num_samples), C, H // F, W // F) + randn = torch.randn(shape).to("cuda") + + def denoiser(input, sigma, c): + return model.denoiser( + model.model, input, sigma, c, **additional_model_inputs + ) + + load_model(model.denoiser) + load_model(model.model) + samples_z = sampler(denoiser, randn, cond=c, uc=uc) + unload_model(model.model) + unload_model(model.denoiser) + + load_model(model.first_stage_model) + model.en_and_decode_n_samples_a_time = ( + decoding_t # Decode n frames at a time + ) + samples_x = model.decode_first_stage(samples_z) + samples = torch.clamp((samples_x + 1.0) / 2.0, min=0.0, max=1.0) + unload_model(model.first_stage_model) + + if filter is not None: + samples = filter(samples) + + if T is None: + grid = torch.stack([samples]) + grid = rearrange(grid, "n b c h w -> (n h) (b w) c") + outputs.image(grid.cpu().numpy()) + else: + as_vids = rearrange(samples, "(b t) c h w -> b t c h w", t=T) + for i, vid in enumerate(as_vids): + grid = rearrange(make_grid(vid, nrow=4), "c h w -> h w c") + st.image( + grid.cpu().numpy(), + f"Sample #{i} as image", + ) + + if return_latents: + return samples, samples_z + return samples + + +def get_batch( + keys, + value_dict: dict, + N: Union[List, ListConfig], + device: str = "cuda", + T: int = None, + additional_batch_uc_fields: List[str] = [], +): + # Hardcoded demo setups; might undergo some changes in the future + + batch = {} + batch_uc = {} + + for key in keys: + if key == "txt": + batch["txt"] = [value_dict["prompt"]] * math.prod(N) + + batch_uc["txt"] = [value_dict["negative_prompt"]] * math.prod(N) + + elif key == "original_size_as_tuple": + batch["original_size_as_tuple"] = ( + torch.tensor([value_dict["orig_height"], value_dict["orig_width"]]) + .to(device) + .repeat(math.prod(N), 1) + ) + elif key == "crop_coords_top_left": + batch["crop_coords_top_left"] = ( + torch.tensor( + [value_dict["crop_coords_top"], value_dict["crop_coords_left"]] + ) + .to(device) + .repeat(math.prod(N), 1) + ) + elif key == "aesthetic_score": + batch["aesthetic_score"] = ( + torch.tensor([value_dict["aesthetic_score"]]) + .to(device) + .repeat(math.prod(N), 1) + ) + batch_uc["aesthetic_score"] = ( + torch.tensor([value_dict["negative_aesthetic_score"]]) + .to(device) + .repeat(math.prod(N), 1) + ) + + elif key == "target_size_as_tuple": + batch["target_size_as_tuple"] = ( + torch.tensor([value_dict["target_height"], value_dict["target_width"]]) + .to(device) + .repeat(math.prod(N), 1) + ) + elif key == "fps": + batch[key] = ( + torch.tensor([value_dict["fps"]]).to(device).repeat(math.prod(N)) + ) + elif key == "fps_id": + batch[key] = ( + torch.tensor([value_dict["fps_id"]]).to(device).repeat(math.prod(N)) + ) + elif key == "motion_bucket_id": + batch[key] = ( + torch.tensor([value_dict["motion_bucket_id"]]) + .to(device) + .repeat(math.prod(N)) + ) + elif key == "pool_image": + batch[key] = repeat(value_dict[key], "1 ... -> b ...", b=math.prod(N)).to( + device, dtype=torch.half + ) + elif key == "cond_aug": + batch[key] = repeat( + torch.tensor([value_dict["cond_aug"]]).to("cuda"), + "1 -> b", + b=math.prod(N), + ) + elif key == "cond_frames": + batch[key] = repeat(value_dict["cond_frames"], "1 ... -> b ...", b=N[0]) + elif key == "cond_frames_without_noise": + batch[key] = repeat( + value_dict["cond_frames_without_noise"], "1 ... -> b ...", b=N[0] + ) + else: + batch[key] = value_dict[key] + + if T is not None: + batch["num_video_frames"] = T + + for key in batch.keys(): + if key not in batch_uc and isinstance(batch[key], torch.Tensor): + batch_uc[key] = torch.clone(batch[key]) + elif key in additional_batch_uc_fields and key not in batch_uc: + batch_uc[key] = copy.copy(batch[key]) + return batch, batch_uc + + +@torch.no_grad() +def do_img2img( + img, + model, + sampler, + value_dict, + num_samples, + force_uc_zero_embeddings: Optional[List] = None, + force_cond_zero_embeddings: Optional[List] = None, + additional_kwargs={}, + offset_noise_level: int = 0.0, + return_latents=False, + skip_encode=False, + filter=None, + add_noise=True, +): + st.text("Sampling") + + outputs = st.empty() + precision_scope = autocast + with torch.no_grad(): + with precision_scope("cuda"): + with model.ema_scope(): + load_model(model.conditioner) + batch, batch_uc = get_batch( + get_unique_embedder_keys_from_conditioner(model.conditioner), + value_dict, + [num_samples], + ) + c, uc = model.conditioner.get_unconditional_conditioning( + batch, + batch_uc=batch_uc, + force_uc_zero_embeddings=force_uc_zero_embeddings, + force_cond_zero_embeddings=force_cond_zero_embeddings, + ) + unload_model(model.conditioner) + for k in c: + c[k], uc[k] = map(lambda y: y[k][:num_samples].to("cuda"), (c, uc)) + + for k in additional_kwargs: + c[k] = uc[k] = additional_kwargs[k] + if skip_encode: + z = img + else: + load_model(model.first_stage_model) + z = model.encode_first_stage(img) + unload_model(model.first_stage_model) + + noise = torch.randn_like(z) + + sigmas = sampler.discretization(sampler.num_steps).cuda() + sigma = sigmas[0] + + st.info(f"all sigmas: {sigmas}") + st.info(f"noising sigma: {sigma}") + if offset_noise_level > 0.0: + noise = noise + offset_noise_level * append_dims( + torch.randn(z.shape[0], device=z.device), z.ndim + ) + if add_noise: + noised_z = z + noise * append_dims(sigma, z.ndim).cuda() + noised_z = noised_z / torch.sqrt( + 1.0 + sigmas[0] ** 2.0 + ) # Note: hardcoded to DDPM-like scaling. need to generalize later. + else: + noised_z = z / torch.sqrt(1.0 + sigmas[0] ** 2.0) + + def denoiser(x, sigma, c): + return model.denoiser(model.model, x, sigma, c) + + load_model(model.denoiser) + load_model(model.model) + samples_z = sampler(denoiser, noised_z, cond=c, uc=uc) + unload_model(model.model) + unload_model(model.denoiser) + + load_model(model.first_stage_model) + samples_x = model.decode_first_stage(samples_z) + unload_model(model.first_stage_model) + samples = torch.clamp((samples_x + 1.0) / 2.0, min=0.0, max=1.0) + + if filter is not None: + samples = filter(samples) + + grid = rearrange(grid, "n b c h w -> (n h) (b w) c") + outputs.image(grid.cpu().numpy()) + if return_latents: + return samples, samples_z + return samples + + +def get_resizing_factor( + desired_shape: Tuple[int, int], current_shape: Tuple[int, int] +) -> float: + r_bound = desired_shape[1] / desired_shape[0] + aspect_r = current_shape[1] / current_shape[0] + if r_bound >= 1.0: + if aspect_r >= r_bound: + factor = min(desired_shape) / min(current_shape) + else: + if aspect_r < 1.0: + factor = max(desired_shape) / min(current_shape) + else: + factor = max(desired_shape) / max(current_shape) + else: + if aspect_r <= r_bound: + factor = min(desired_shape) / min(current_shape) + else: + if aspect_r > 1: + factor = max(desired_shape) / min(current_shape) + else: + factor = max(desired_shape) / max(current_shape) + + return factor + + +def get_interactive_image(key=None) -> Image.Image: + image = st.file_uploader("Input", type=["jpg", "JPEG", "png"], key=key) + if image is not None: + image = Image.open(image) + if not image.mode == "RGB": + image = image.convert("RGB") + return image + + +def load_img_for_prediction( + W: int, H: int, display=True, key=None, device="cuda" +) -> torch.Tensor: + image = get_interactive_image(key=key) + if image is None: + return None + if display: + st.image(image) + w, h = image.size + + image = np.array(image).transpose(2, 0, 1) + image = torch.from_numpy(image).to(dtype=torch.float32) / 255.0 + image = image.unsqueeze(0) + + rfs = get_resizing_factor((H, W), (h, w)) + resize_size = [int(np.ceil(rfs * s)) for s in (h, w)] + top = (resize_size[0] - H) // 2 + left = (resize_size[1] - W) // 2 + + image = torch.nn.functional.interpolate( + image, resize_size, mode="area", antialias=False + ) + image = TT.functional.crop(image, top=top, left=left, height=H, width=W) + + if display: + numpy_img = np.transpose(image[0].numpy(), (1, 2, 0)) + pil_image = Image.fromarray((numpy_img * 255).astype(np.uint8)) + st.image(pil_image) + return image.to(device) * 2.0 - 1.0 + + +def save_video_as_grid_and_mp4( + video_batch: torch.Tensor, save_path: str, T: int, fps: int = 5 +): + os.makedirs(save_path, exist_ok=True) + base_count = len(glob(os.path.join(save_path, "*.mp4"))) + + video_batch = rearrange(video_batch, "(b t) c h w -> b t c h w", t=T) + video_batch = embed_watermark(video_batch) + for vid in video_batch: + save_image(vid, fp=os.path.join(save_path, f"{base_count:06d}.png"), nrow=4) + + video_path = os.path.join(save_path, f"{base_count:06d}.mp4") + + writer = cv2.VideoWriter( + video_path, + cv2.VideoWriter_fourcc(*"MP4V"), + fps, + (vid.shape[-1], vid.shape[-2]), + ) + + vid = ( + (rearrange(vid, "t c h w -> t h w c") * 255).cpu().numpy().astype(np.uint8) + ) + for frame in vid: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + writer.write(frame) + + writer.release() + + video_path_h264 = video_path[:-4] + "_h264.mp4" + os.system(f"ffmpeg -i {video_path} -c:v libx264 {video_path_h264}") + + with open(video_path_h264, "rb") as f: + video_bytes = f.read() + st.video(video_bytes) + + base_count += 1 diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/turbo.py b/MindEye_Imagery/src/generative_models/scripts/demo/turbo.py new file mode 100644 index 0000000000000000000000000000000000000000..7c641f81661bfaa7a10c5fd0514c6e9148a5e0c0 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/turbo.py @@ -0,0 +1,223 @@ +from streamlit_helpers import * +from st_keyup import st_keyup +from sgm.modules.diffusionmodules.sampling import EulerAncestralSampler + +VERSION2SPECS = { + "SDXL-Turbo": { + "H": 512, + "W": 512, + "C": 4, + "f": 8, + "is_legacy": False, + "config": "configs/inference/sd_xl_base.yaml", + "ckpt": "checkpoints/sd_xl_turbo_1.0.safetensors", + }, + "SD-Turbo": { + "H": 512, + "W": 512, + "C": 4, + "f": 8, + "is_legacy": False, + "config": "configs/inference/sd_2_1.yaml", + "ckpt": "checkpoints/sd_turbo.safetensors", + }, +} + + +class SubstepSampler(EulerAncestralSampler): + def __init__(self, n_sample_steps=1, *args, **kwargs): + super().__init__(*args, **kwargs) + self.n_sample_steps = n_sample_steps + self.steps_subset = [0, 100, 200, 300, 1000] + + def prepare_sampling_loop(self, x, cond, uc=None, num_steps=None): + sigmas = self.discretization( + self.num_steps if num_steps is None else num_steps, device=self.device + ) + sigmas = sigmas[ + self.steps_subset[: self.n_sample_steps] + self.steps_subset[-1:] + ] + uc = cond + x *= torch.sqrt(1.0 + sigmas[0] ** 2.0) + num_sigmas = len(sigmas) + s_in = x.new_ones([x.shape[0]]) + return x, s_in, sigmas, num_sigmas, cond, uc + + +def seeded_randn(shape, seed): + randn = np.random.RandomState(seed).randn(*shape) + randn = torch.from_numpy(randn).to(device="cuda", dtype=torch.float32) + return randn + + +class SeededNoise: + def __init__(self, seed): + self.seed = seed + + def __call__(self, x): + self.seed = self.seed + 1 + return seeded_randn(x.shape, self.seed) + + +def init_embedder_options(keys, init_dict, prompt=None, negative_prompt=None): + value_dict = {} + for key in keys: + if key == "txt": + value_dict["prompt"] = prompt + value_dict["negative_prompt"] = "" + + if key == "original_size_as_tuple": + orig_width = init_dict["orig_width"] + orig_height = init_dict["orig_height"] + + value_dict["orig_width"] = orig_width + value_dict["orig_height"] = orig_height + + if key == "crop_coords_top_left": + crop_coord_top = 0 + crop_coord_left = 0 + + value_dict["crop_coords_top"] = crop_coord_top + value_dict["crop_coords_left"] = crop_coord_left + + if key == "aesthetic_score": + value_dict["aesthetic_score"] = 6.0 + value_dict["negative_aesthetic_score"] = 2.5 + + if key == "target_size_as_tuple": + value_dict["target_width"] = init_dict["target_width"] + value_dict["target_height"] = init_dict["target_height"] + + return value_dict + + +def sample( + model, + sampler, + prompt="A lush garden with oversized flowers and vibrant colors, inhabited by miniature animals.", + H=1024, + W=1024, + seed=0, + filter=None, +): + F = 8 + C = 4 + shape = (1, C, H // F, W // F) + + value_dict = init_embedder_options( + keys=get_unique_embedder_keys_from_conditioner(model.conditioner), + init_dict={ + "orig_width": W, + "orig_height": H, + "target_width": W, + "target_height": H, + }, + prompt=prompt, + ) + + if seed is None: + seed = torch.seed() + precision_scope = autocast + with torch.no_grad(): + with precision_scope("cuda"): + batch, batch_uc = get_batch( + get_unique_embedder_keys_from_conditioner(model.conditioner), + value_dict, + [1], + ) + c = model.conditioner(batch) + uc = None + randn = seeded_randn(shape, seed) + + def denoiser(input, sigma, c): + return model.denoiser( + model.model, + input, + sigma, + c, + ) + + samples_z = sampler(denoiser, randn, cond=c, uc=uc) + samples_x = model.decode_first_stage(samples_z) + samples = torch.clamp((samples_x + 1.0) / 2.0, min=0.0, max=1.0) + if filter is not None: + samples = filter(samples) + samples = ( + (255 * samples) + .to(dtype=torch.uint8) + .permute(0, 2, 3, 1) + .detach() + .cpu() + .numpy() + ) + return samples + + +def v_spacer(height) -> None: + for _ in range(height): + st.write("\n") + + +if __name__ == "__main__": + st.title("Turbo") + + head_cols = st.columns([1, 1, 1]) + with head_cols[0]: + version = st.selectbox("Model Version", list(VERSION2SPECS.keys()), 0) + version_dict = VERSION2SPECS[version] + + with head_cols[1]: + v_spacer(2) + if st.checkbox("Load Model"): + mode = "txt2img" + else: + mode = "skip" + + if mode != "skip": + state = init_st(version_dict, load_filter=True) + if state["msg"]: + st.info(state["msg"]) + model = state["model"] + load_model(model) + + # seed + if "seed" not in st.session_state: + st.session_state.seed = 0 + + def increment_counter(): + st.session_state.seed += 1 + + def decrement_counter(): + if st.session_state.seed > 0: + st.session_state.seed -= 1 + + with head_cols[2]: + n_steps = st.number_input(label="number of steps", min_value=1, max_value=4) + + sampler = SubstepSampler( + n_sample_steps=1, + num_steps=1000, + eta=1.0, + discretization_config=dict( + target="sgm.modules.diffusionmodules.discretizer.LegacyDDPMDiscretization" + ), + ) + sampler.n_sample_steps = n_steps + default_prompt = "A cinematic shot of a baby racoon wearing an intricate italian priest robe." + prompt = st_keyup("Enter a value", value=default_prompt, debounce=300, key="interactive_text") + + cols = st.columns([1, 5, 1]) + if mode != "skip": + with cols[0]: + v_spacer(14) + st.button("↩", on_click=decrement_counter) + with cols[2]: + v_spacer(14) + st.button("↪", on_click=increment_counter) + + sampler.noise_sampler = SeededNoise(seed=st.session_state.seed) + out = sample( + model, sampler, H=512, W=512, seed=st.session_state.seed, prompt=prompt, filter=state.get("filter") + ) + with cols[1]: + st.image(out[0]) diff --git a/MindEye_Imagery/src/generative_models/scripts/demo/video_sampling.py b/MindEye_Imagery/src/generative_models/scripts/demo/video_sampling.py new file mode 100644 index 0000000000000000000000000000000000000000..95789020110f3abaf6e5a7755a7e5910b864c6df --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/demo/video_sampling.py @@ -0,0 +1,200 @@ +import os + +from pytorch_lightning import seed_everything + +from scripts.demo.streamlit_helpers import * + +SAVE_PATH = "outputs/demo/vid/" + +VERSION2SPECS = { + "svd": { + "T": 14, + "H": 576, + "W": 1024, + "C": 4, + "f": 8, + "config": "configs/inference/svd.yaml", + "ckpt": "checkpoints/svd.safetensors", + "options": { + "discretization": 1, + "cfg": 2.5, + "sigma_min": 0.002, + "sigma_max": 700.0, + "rho": 7.0, + "guider": 2, + "force_uc_zero_embeddings": ["cond_frames", "cond_frames_without_noise"], + "num_steps": 25, + }, + }, + "svd_image_decoder": { + "T": 14, + "H": 576, + "W": 1024, + "C": 4, + "f": 8, + "config": "configs/inference/svd_image_decoder.yaml", + "ckpt": "checkpoints/svd_image_decoder.safetensors", + "options": { + "discretization": 1, + "cfg": 2.5, + "sigma_min": 0.002, + "sigma_max": 700.0, + "rho": 7.0, + "guider": 2, + "force_uc_zero_embeddings": ["cond_frames", "cond_frames_without_noise"], + "num_steps": 25, + }, + }, + "svd_xt": { + "T": 25, + "H": 576, + "W": 1024, + "C": 4, + "f": 8, + "config": "configs/inference/svd.yaml", + "ckpt": "checkpoints/svd_xt.safetensors", + "options": { + "discretization": 1, + "cfg": 3.0, + "min_cfg": 1.5, + "sigma_min": 0.002, + "sigma_max": 700.0, + "rho": 7.0, + "guider": 2, + "force_uc_zero_embeddings": ["cond_frames", "cond_frames_without_noise"], + "num_steps": 30, + "decoding_t": 14, + }, + }, + "svd_xt_image_decoder": { + "T": 25, + "H": 576, + "W": 1024, + "C": 4, + "f": 8, + "config": "configs/inference/svd_image_decoder.yaml", + "ckpt": "checkpoints/svd_xt_image_decoder.safetensors", + "options": { + "discretization": 1, + "cfg": 3.0, + "min_cfg": 1.5, + "sigma_min": 0.002, + "sigma_max": 700.0, + "rho": 7.0, + "guider": 2, + "force_uc_zero_embeddings": ["cond_frames", "cond_frames_without_noise"], + "num_steps": 30, + "decoding_t": 14, + }, + }, +} + + +if __name__ == "__main__": + st.title("Stable Video Diffusion") + version = st.selectbox( + "Model Version", + [k for k in VERSION2SPECS.keys()], + 0, + ) + version_dict = VERSION2SPECS[version] + if st.checkbox("Load Model"): + mode = "img2vid" + else: + mode = "skip" + + H = st.sidebar.number_input( + "H", value=version_dict["H"], min_value=64, max_value=2048 + ) + W = st.sidebar.number_input( + "W", value=version_dict["W"], min_value=64, max_value=2048 + ) + T = st.sidebar.number_input( + "T", value=version_dict["T"], min_value=0, max_value=128 + ) + C = version_dict["C"] + F = version_dict["f"] + options = version_dict["options"] + + if mode != "skip": + state = init_st(version_dict, load_filter=True) + if state["msg"]: + st.info(state["msg"]) + model = state["model"] + + ukeys = set( + get_unique_embedder_keys_from_conditioner(state["model"].conditioner) + ) + + value_dict = init_embedder_options( + ukeys, + {}, + ) + + value_dict["image_only_indicator"] = 0 + + if mode == "img2vid": + img = load_img_for_prediction(W, H) + cond_aug = st.number_input( + "Conditioning augmentation:", value=0.02, min_value=0.0 + ) + value_dict["cond_frames_without_noise"] = img + value_dict["cond_frames"] = img + cond_aug * torch.randn_like(img) + value_dict["cond_aug"] = cond_aug + + seed = st.sidebar.number_input( + "seed", value=23, min_value=0, max_value=int(1e9) + ) + seed_everything(seed) + + save_locally, save_path = init_save_locally( + os.path.join(SAVE_PATH, version), init_value=True + ) + + options["num_frames"] = T + + sampler, num_rows, num_cols = init_sampling(options=options) + num_samples = num_rows * num_cols + + decoding_t = st.number_input( + "Decode t frames at a time (set small if you are low on VRAM)", + value=options.get("decoding_t", T), + min_value=1, + max_value=int(1e9), + ) + + if st.checkbox("Overwrite fps in mp4 generator", False): + saving_fps = st.number_input( + f"saving video at fps:", value=value_dict["fps"], min_value=1 + ) + else: + saving_fps = value_dict["fps"] + + if st.button("Sample"): + out = do_sample( + model, + sampler, + value_dict, + num_samples, + H, + W, + C, + F, + T=T, + batch2model_input=["num_video_frames", "image_only_indicator"], + force_uc_zero_embeddings=options.get("force_uc_zero_embeddings", None), + force_cond_zero_embeddings=options.get( + "force_cond_zero_embeddings", None + ), + return_latents=False, + decoding_t=decoding_t, + ) + + if isinstance(out, (tuple, list)): + samples, samples_z = out + else: + samples = out + samples_z = None + + if save_locally: + save_video_as_grid_and_mp4(samples, save_path, T, fps=saving_fps) diff --git a/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd.yaml b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd.yaml new file mode 100644 index 0000000000000000000000000000000000000000..ec8e80552ff47df39e59c5f2e3411677a51c09b1 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd.yaml @@ -0,0 +1,146 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.18215 + disable_first_stage_autocast: True + ckpt_path: checkpoints/svd.safetensors + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.Denoiser + params: + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.VScalingWithEDMcNoise + + network_config: + target: sgm.modules.diffusionmodules.video_model.VideoUNet + params: + adm_in_channels: 768 + num_classes: sequential + use_checkpoint: True + in_channels: 8 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2, 1] + num_res_blocks: 2 + channel_mult: [1, 2, 4, 4] + num_head_channels: 64 + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + spatial_transformer_attn_type: softmax-xformers + extra_ff_mix_layer: True + use_spatial_context: True + merge_strategy: learned_with_images + video_kernel_size: [3, 1, 1] + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + - is_trainable: False + input_key: cond_frames_without_noise + target: sgm.modules.encoders.modules.FrozenOpenCLIPImagePredictionEmbedder + params: + n_cond_frames: 1 + n_copies: 1 + open_clip_embedding_config: + target: sgm.modules.encoders.modules.FrozenOpenCLIPImageEmbedder + params: + freeze: True + + - input_key: fps_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: motion_bucket_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: cond_frames + is_trainable: False + target: sgm.modules.encoders.modules.VideoPredictionEmbedderWithEncoder + params: + disable_encoder_autocast: True + n_cond_frames: 1 + n_copies: 1 + is_ae: True + encoder_config: + target: sgm.models.autoencoder.AutoencoderKLModeOnly + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + - input_key: cond_aug + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + first_stage_config: + target: sgm.models.autoencoder.AutoencodingEngine + params: + loss_config: + target: torch.nn.Identity + regularizer_config: + target: sgm.modules.autoencoding.regularizers.DiagonalGaussianRegularizer + encoder_config: + target: sgm.modules.diffusionmodules.model.Encoder + params: + attn_type: vanilla + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + decoder_config: + target: sgm.modules.autoencoding.temporal_ae.VideoDecoder + params: + attn_type: vanilla + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + video_kernel_size: [3, 1, 1] + + sampler_config: + target: sgm.modules.diffusionmodules.sampling.EulerEDMSampler + params: + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.EDMDiscretization + params: + sigma_max: 700.0 + + guider_config: + target: sgm.modules.diffusionmodules.guiders.LinearPredictionGuider + params: + max_scale: 2.5 + min_scale: 1.0 \ No newline at end of file diff --git a/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_image_decoder.yaml b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_image_decoder.yaml new file mode 100644 index 0000000000000000000000000000000000000000..f731c5958f1935e4f3c1913c97fff4932520c12d --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_image_decoder.yaml @@ -0,0 +1,129 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.18215 + disable_first_stage_autocast: True + ckpt_path: checkpoints/svd_image_decoder.safetensors + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.Denoiser + params: + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.VScalingWithEDMcNoise + + network_config: + target: sgm.modules.diffusionmodules.video_model.VideoUNet + params: + adm_in_channels: 768 + num_classes: sequential + use_checkpoint: True + in_channels: 8 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2, 1] + num_res_blocks: 2 + channel_mult: [1, 2, 4, 4] + num_head_channels: 64 + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + spatial_transformer_attn_type: softmax-xformers + extra_ff_mix_layer: True + use_spatial_context: True + merge_strategy: learned_with_images + video_kernel_size: [3, 1, 1] + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + - is_trainable: False + input_key: cond_frames_without_noise + target: sgm.modules.encoders.modules.FrozenOpenCLIPImagePredictionEmbedder + params: + n_cond_frames: 1 + n_copies: 1 + open_clip_embedding_config: + target: sgm.modules.encoders.modules.FrozenOpenCLIPImageEmbedder + params: + freeze: True + + - input_key: fps_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: motion_bucket_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: cond_frames + is_trainable: False + target: sgm.modules.encoders.modules.VideoPredictionEmbedderWithEncoder + params: + disable_encoder_autocast: True + n_cond_frames: 1 + n_copies: 1 + is_ae: True + encoder_config: + target: sgm.models.autoencoder.AutoencoderKLModeOnly + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + - input_key: cond_aug + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + sampler_config: + target: sgm.modules.diffusionmodules.sampling.EulerEDMSampler + params: + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.EDMDiscretization + params: + sigma_max: 700.0 + + guider_config: + target: sgm.modules.diffusionmodules.guiders.LinearPredictionGuider + params: + max_scale: 2.5 + min_scale: 1.0 \ No newline at end of file diff --git a/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt.yaml b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt.yaml new file mode 100644 index 0000000000000000000000000000000000000000..7e98c466aced5a540e2c8b0d81ee178d572400dc --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt.yaml @@ -0,0 +1,146 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.18215 + disable_first_stage_autocast: True + ckpt_path: checkpoints/svd_xt.safetensors + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.Denoiser + params: + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.VScalingWithEDMcNoise + + network_config: + target: sgm.modules.diffusionmodules.video_model.VideoUNet + params: + adm_in_channels: 768 + num_classes: sequential + use_checkpoint: True + in_channels: 8 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2, 1] + num_res_blocks: 2 + channel_mult: [1, 2, 4, 4] + num_head_channels: 64 + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + spatial_transformer_attn_type: softmax-xformers + extra_ff_mix_layer: True + use_spatial_context: True + merge_strategy: learned_with_images + video_kernel_size: [3, 1, 1] + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + - is_trainable: False + input_key: cond_frames_without_noise + target: sgm.modules.encoders.modules.FrozenOpenCLIPImagePredictionEmbedder + params: + n_cond_frames: 1 + n_copies: 1 + open_clip_embedding_config: + target: sgm.modules.encoders.modules.FrozenOpenCLIPImageEmbedder + params: + freeze: True + + - input_key: fps_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: motion_bucket_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: cond_frames + is_trainable: False + target: sgm.modules.encoders.modules.VideoPredictionEmbedderWithEncoder + params: + disable_encoder_autocast: True + n_cond_frames: 1 + n_copies: 1 + is_ae: True + encoder_config: + target: sgm.models.autoencoder.AutoencoderKLModeOnly + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + - input_key: cond_aug + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + first_stage_config: + target: sgm.models.autoencoder.AutoencodingEngine + params: + loss_config: + target: torch.nn.Identity + regularizer_config: + target: sgm.modules.autoencoding.regularizers.DiagonalGaussianRegularizer + encoder_config: + target: sgm.modules.diffusionmodules.model.Encoder + params: + attn_type: vanilla + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + decoder_config: + target: sgm.modules.autoencoding.temporal_ae.VideoDecoder + params: + attn_type: vanilla + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + video_kernel_size: [3, 1, 1] + + sampler_config: + target: sgm.modules.diffusionmodules.sampling.EulerEDMSampler + params: + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.EDMDiscretization + params: + sigma_max: 700.0 + + guider_config: + target: sgm.modules.diffusionmodules.guiders.LinearPredictionGuider + params: + max_scale: 3.0 + min_scale: 1.5 \ No newline at end of file diff --git a/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt_image_decoder.yaml b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt_image_decoder.yaml new file mode 100644 index 0000000000000000000000000000000000000000..afb38387d2344b8f75371b3b51c281acca523272 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/sampling/configs/svd_xt_image_decoder.yaml @@ -0,0 +1,129 @@ +model: + target: sgm.models.diffusion.DiffusionEngine + params: + scale_factor: 0.18215 + disable_first_stage_autocast: True + ckpt_path: checkpoints/svd_xt_image_decoder.safetensors + + denoiser_config: + target: sgm.modules.diffusionmodules.denoiser.Denoiser + params: + scaling_config: + target: sgm.modules.diffusionmodules.denoiser_scaling.VScalingWithEDMcNoise + + network_config: + target: sgm.modules.diffusionmodules.video_model.VideoUNet + params: + adm_in_channels: 768 + num_classes: sequential + use_checkpoint: True + in_channels: 8 + out_channels: 4 + model_channels: 320 + attention_resolutions: [4, 2, 1] + num_res_blocks: 2 + channel_mult: [1, 2, 4, 4] + num_head_channels: 64 + use_linear_in_transformer: True + transformer_depth: 1 + context_dim: 1024 + spatial_transformer_attn_type: softmax-xformers + extra_ff_mix_layer: True + use_spatial_context: True + merge_strategy: learned_with_images + video_kernel_size: [3, 1, 1] + + conditioner_config: + target: sgm.modules.GeneralConditioner + params: + emb_models: + - is_trainable: False + input_key: cond_frames_without_noise + target: sgm.modules.encoders.modules.FrozenOpenCLIPImagePredictionEmbedder + params: + n_cond_frames: 1 + n_copies: 1 + open_clip_embedding_config: + target: sgm.modules.encoders.modules.FrozenOpenCLIPImageEmbedder + params: + freeze: True + + - input_key: fps_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: motion_bucket_id + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + - input_key: cond_frames + is_trainable: False + target: sgm.modules.encoders.modules.VideoPredictionEmbedderWithEncoder + params: + disable_encoder_autocast: True + n_cond_frames: 1 + n_copies: 1 + is_ae: True + encoder_config: + target: sgm.models.autoencoder.AutoencoderKLModeOnly + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + - input_key: cond_aug + is_trainable: False + target: sgm.modules.encoders.modules.ConcatTimestepEmbedderND + params: + outdim: 256 + + first_stage_config: + target: sgm.models.autoencoder.AutoencoderKL + params: + embed_dim: 4 + monitor: val/rec_loss + ddconfig: + attn_type: vanilla-xformers + double_z: True + z_channels: 4 + resolution: 256 + in_channels: 3 + out_ch: 3 + ch: 128 + ch_mult: [1, 2, 4, 4] + num_res_blocks: 2 + attn_resolutions: [] + dropout: 0.0 + lossconfig: + target: torch.nn.Identity + + sampler_config: + target: sgm.modules.diffusionmodules.sampling.EulerEDMSampler + params: + discretization_config: + target: sgm.modules.diffusionmodules.discretizer.EDMDiscretization + params: + sigma_max: 700.0 + + guider_config: + target: sgm.modules.diffusionmodules.guiders.LinearPredictionGuider + params: + max_scale: 3.0 + min_scale: 1.5 \ No newline at end of file diff --git a/MindEye_Imagery/src/generative_models/scripts/sampling/simple_video_sample.py b/MindEye_Imagery/src/generative_models/scripts/sampling/simple_video_sample.py new file mode 100644 index 0000000000000000000000000000000000000000..c3f4ad2a01ee62c8b36449a9d61f5909baafa702 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/sampling/simple_video_sample.py @@ -0,0 +1,278 @@ +import math +import os +from glob import glob +from pathlib import Path +from typing import Optional + +import cv2 +import numpy as np +import torch +from einops import rearrange, repeat +from fire import Fire +from omegaconf import OmegaConf +from PIL import Image +from torchvision.transforms import ToTensor + +from scripts.util.detection.nsfw_and_watermark_dectection import \ + DeepFloydDataFiltering +from sgm.inference.helpers import embed_watermark +from sgm.util import default, instantiate_from_config + + +def sample( + input_path: str = "assets/test_image.png", # Can either be image file or folder with image files + num_frames: Optional[int] = None, + num_steps: Optional[int] = None, + version: str = "svd", + fps_id: int = 6, + motion_bucket_id: int = 127, + cond_aug: float = 0.02, + seed: int = 23, + decoding_t: int = 14, # Number of frames decoded at a time! This eats most VRAM. Reduce if necessary. + device: str = "cuda", + output_folder: Optional[str] = None, +): + """ + Simple script to generate a single sample conditioned on an image `input_path` or multiple images, one for each + image file in folder `input_path`. If you run out of VRAM, try decreasing `decoding_t`. + """ + + if version == "svd": + num_frames = default(num_frames, 14) + num_steps = default(num_steps, 25) + output_folder = default(output_folder, "outputs/simple_video_sample/svd/") + model_config = "scripts/sampling/configs/svd.yaml" + elif version == "svd_xt": + num_frames = default(num_frames, 25) + num_steps = default(num_steps, 30) + output_folder = default(output_folder, "outputs/simple_video_sample/svd_xt/") + model_config = "scripts/sampling/configs/svd_xt.yaml" + elif version == "svd_image_decoder": + num_frames = default(num_frames, 14) + num_steps = default(num_steps, 25) + output_folder = default( + output_folder, "outputs/simple_video_sample/svd_image_decoder/" + ) + model_config = "scripts/sampling/configs/svd_image_decoder.yaml" + elif version == "svd_xt_image_decoder": + num_frames = default(num_frames, 25) + num_steps = default(num_steps, 30) + output_folder = default( + output_folder, "outputs/simple_video_sample/svd_xt_image_decoder/" + ) + model_config = "scripts/sampling/configs/svd_xt_image_decoder.yaml" + else: + raise ValueError(f"Version {version} does not exist.") + + model, filter = load_model( + model_config, + device, + num_frames, + num_steps, + ) + torch.manual_seed(seed) + + path = Path(input_path) + all_img_paths = [] + if path.is_file(): + if any([input_path.endswith(x) for x in ["jpg", "jpeg", "png"]]): + all_img_paths = [input_path] + else: + raise ValueError("Path is not valid image file.") + elif path.is_dir(): + all_img_paths = sorted( + [ + f + for f in path.iterdir() + if f.is_file() and f.suffix.lower() in [".jpg", ".jpeg", ".png"] + ] + ) + if len(all_img_paths) == 0: + raise ValueError("Folder does not contain any images.") + else: + raise ValueError + + for input_img_path in all_img_paths: + with Image.open(input_img_path) as image: + if image.mode == "RGBA": + image = image.convert("RGB") + w, h = image.size + + if h % 64 != 0 or w % 64 != 0: + width, height = map(lambda x: x - x % 64, (w, h)) + image = image.resize((width, height)) + print( + f"WARNING: Your image is of size {h}x{w} which is not divisible by 64. We are resizing to {height}x{width}!" + ) + + image = ToTensor()(image) + image = image * 2.0 - 1.0 + + image = image.unsqueeze(0).to(device) + H, W = image.shape[2:] + assert image.shape[1] == 3 + F = 8 + C = 4 + shape = (num_frames, C, H // F, W // F) + if (H, W) != (576, 1024): + print( + "WARNING: The conditioning frame you provided is not 576x1024. This leads to suboptimal performance as model was only trained on 576x1024. Consider increasing `cond_aug`." + ) + if motion_bucket_id > 255: + print( + "WARNING: High motion bucket! This may lead to suboptimal performance." + ) + + if fps_id < 5: + print("WARNING: Small fps value! This may lead to suboptimal performance.") + + if fps_id > 30: + print("WARNING: Large fps value! This may lead to suboptimal performance.") + + value_dict = {} + value_dict["motion_bucket_id"] = motion_bucket_id + value_dict["fps_id"] = fps_id + value_dict["cond_aug"] = cond_aug + value_dict["cond_frames_without_noise"] = image + value_dict["cond_frames"] = image + cond_aug * torch.randn_like(image) + value_dict["cond_aug"] = cond_aug + + with torch.no_grad(): + with torch.autocast(device): + batch, batch_uc = get_batch( + get_unique_embedder_keys_from_conditioner(model.conditioner), + value_dict, + [1, num_frames], + T=num_frames, + device=device, + ) + c, uc = model.conditioner.get_unconditional_conditioning( + batch, + batch_uc=batch_uc, + force_uc_zero_embeddings=[ + "cond_frames", + "cond_frames_without_noise", + ], + ) + + for k in ["crossattn", "concat"]: + uc[k] = repeat(uc[k], "b ... -> b t ...", t=num_frames) + uc[k] = rearrange(uc[k], "b t ... -> (b t) ...", t=num_frames) + c[k] = repeat(c[k], "b ... -> b t ...", t=num_frames) + c[k] = rearrange(c[k], "b t ... -> (b t) ...", t=num_frames) + + randn = torch.randn(shape, device=device) + + additional_model_inputs = {} + additional_model_inputs["image_only_indicator"] = torch.zeros( + 2, num_frames + ).to(device) + additional_model_inputs["num_video_frames"] = batch["num_video_frames"] + + def denoiser(input, sigma, c): + return model.denoiser( + model.model, input, sigma, c, **additional_model_inputs + ) + + samples_z = model.sampler(denoiser, randn, cond=c, uc=uc) + model.en_and_decode_n_samples_a_time = decoding_t + samples_x = model.decode_first_stage(samples_z) + samples = torch.clamp((samples_x + 1.0) / 2.0, min=0.0, max=1.0) + + os.makedirs(output_folder, exist_ok=True) + base_count = len(glob(os.path.join(output_folder, "*.mp4"))) + video_path = os.path.join(output_folder, f"{base_count:06d}.mp4") + writer = cv2.VideoWriter( + video_path, + cv2.VideoWriter_fourcc(*"MP4V"), + fps_id + 1, + (samples.shape[-1], samples.shape[-2]), + ) + + samples = embed_watermark(samples) + samples = filter(samples) + vid = ( + (rearrange(samples, "t c h w -> t h w c") * 255) + .cpu() + .numpy() + .astype(np.uint8) + ) + for frame in vid: + frame = cv2.cvtColor(frame, cv2.COLOR_RGB2BGR) + writer.write(frame) + writer.release() + + +def get_unique_embedder_keys_from_conditioner(conditioner): + return list(set([x.input_key for x in conditioner.embedders])) + + +def get_batch(keys, value_dict, N, T, device): + batch = {} + batch_uc = {} + + for key in keys: + if key == "fps_id": + batch[key] = ( + torch.tensor([value_dict["fps_id"]]) + .to(device) + .repeat(int(math.prod(N))) + ) + elif key == "motion_bucket_id": + batch[key] = ( + torch.tensor([value_dict["motion_bucket_id"]]) + .to(device) + .repeat(int(math.prod(N))) + ) + elif key == "cond_aug": + batch[key] = repeat( + torch.tensor([value_dict["cond_aug"]]).to(device), + "1 -> b", + b=math.prod(N), + ) + elif key == "cond_frames": + batch[key] = repeat(value_dict["cond_frames"], "1 ... -> b ...", b=N[0]) + elif key == "cond_frames_without_noise": + batch[key] = repeat( + value_dict["cond_frames_without_noise"], "1 ... -> b ...", b=N[0] + ) + else: + batch[key] = value_dict[key] + + if T is not None: + batch["num_video_frames"] = T + + for key in batch.keys(): + if key not in batch_uc and isinstance(batch[key], torch.Tensor): + batch_uc[key] = torch.clone(batch[key]) + return batch, batch_uc + + +def load_model( + config: str, + device: str, + num_frames: int, + num_steps: int, +): + config = OmegaConf.load(config) + if device == "cuda": + config.model.params.conditioner_config.params.emb_models[ + 0 + ].params.open_clip_embedding_config.params.init_device = device + + config.model.params.sampler_config.params.num_steps = num_steps + config.model.params.sampler_config.params.guider_config.params.num_frames = ( + num_frames + ) + if device == "cuda": + with torch.device(device): + model = instantiate_from_config(config.model).to(device).eval() + else: + model = instantiate_from_config(config.model).to(device).eval() + + filter = DeepFloydDataFiltering(verbose=False, device=device) + return model, filter + + +if __name__ == "__main__": + Fire(sample) diff --git a/MindEye_Imagery/src/generative_models/scripts/tests/attention.py b/MindEye_Imagery/src/generative_models/scripts/tests/attention.py new file mode 100644 index 0000000000000000000000000000000000000000..d7c3f7c8da27c577a7ce0ea3a01ab7f9e9c1baa2 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/tests/attention.py @@ -0,0 +1,319 @@ +import einops +import torch +import torch.nn.functional as F +import torch.utils.benchmark as benchmark +from torch.backends.cuda import SDPBackend + +from sgm.modules.attention import BasicTransformerBlock, SpatialTransformer + + +def benchmark_attn(): + # Lets define a helpful benchmarking function: + # https://pytorch.org/tutorials/intermediate/scaled_dot_product_attention_tutorial.html + device = "cuda" if torch.cuda.is_available() else "cpu" + + def benchmark_torch_function_in_microseconds(f, *args, **kwargs): + t0 = benchmark.Timer( + stmt="f(*args, **kwargs)", globals={"args": args, "kwargs": kwargs, "f": f} + ) + return t0.blocked_autorange().mean * 1e6 + + # Lets define the hyper-parameters of our input + batch_size = 32 + max_sequence_len = 1024 + num_heads = 32 + embed_dimension = 32 + + dtype = torch.float16 + + query = torch.rand( + batch_size, + num_heads, + max_sequence_len, + embed_dimension, + device=device, + dtype=dtype, + ) + key = torch.rand( + batch_size, + num_heads, + max_sequence_len, + embed_dimension, + device=device, + dtype=dtype, + ) + value = torch.rand( + batch_size, + num_heads, + max_sequence_len, + embed_dimension, + device=device, + dtype=dtype, + ) + + print(f"q/k/v shape:", query.shape, key.shape, value.shape) + + # Lets explore the speed of each of the 3 implementations + from torch.backends.cuda import SDPBackend, sdp_kernel + + # Helpful arguments mapper + backend_map = { + SDPBackend.MATH: { + "enable_math": True, + "enable_flash": False, + "enable_mem_efficient": False, + }, + SDPBackend.FLASH_ATTENTION: { + "enable_math": False, + "enable_flash": True, + "enable_mem_efficient": False, + }, + SDPBackend.EFFICIENT_ATTENTION: { + "enable_math": False, + "enable_flash": False, + "enable_mem_efficient": True, + }, + } + + from torch.profiler import ProfilerActivity, profile, record_function + + activities = [ProfilerActivity.CPU, ProfilerActivity.CUDA] + + print( + f"The default implementation runs in {benchmark_torch_function_in_microseconds(F.scaled_dot_product_attention, query, key, value):.3f} microseconds" + ) + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("Default detailed stats"): + for _ in range(25): + o = F.scaled_dot_product_attention(query, key, value) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + + print( + f"The math implementation runs in {benchmark_torch_function_in_microseconds(F.scaled_dot_product_attention, query, key, value):.3f} microseconds" + ) + with sdp_kernel(**backend_map[SDPBackend.MATH]): + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("Math implmentation stats"): + for _ in range(25): + o = F.scaled_dot_product_attention(query, key, value) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + + with sdp_kernel(**backend_map[SDPBackend.FLASH_ATTENTION]): + try: + print( + f"The flash attention implementation runs in {benchmark_torch_function_in_microseconds(F.scaled_dot_product_attention, query, key, value):.3f} microseconds" + ) + except RuntimeError: + print("FlashAttention is not supported. See warnings for reasons.") + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("FlashAttention stats"): + for _ in range(25): + o = F.scaled_dot_product_attention(query, key, value) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + + with sdp_kernel(**backend_map[SDPBackend.EFFICIENT_ATTENTION]): + try: + print( + f"The memory efficient implementation runs in {benchmark_torch_function_in_microseconds(F.scaled_dot_product_attention, query, key, value):.3f} microseconds" + ) + except RuntimeError: + print("EfficientAttention is not supported. See warnings for reasons.") + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("EfficientAttention stats"): + for _ in range(25): + o = F.scaled_dot_product_attention(query, key, value) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + + +def run_model(model, x, context): + return model(x, context) + + +def benchmark_transformer_blocks(): + device = "cuda" if torch.cuda.is_available() else "cpu" + import torch.utils.benchmark as benchmark + + def benchmark_torch_function_in_microseconds(f, *args, **kwargs): + t0 = benchmark.Timer( + stmt="f(*args, **kwargs)", globals={"args": args, "kwargs": kwargs, "f": f} + ) + return t0.blocked_autorange().mean * 1e6 + + checkpoint = True + compile = False + + batch_size = 32 + h, w = 64, 64 + context_len = 77 + embed_dimension = 1024 + context_dim = 1024 + d_head = 64 + + transformer_depth = 4 + + n_heads = embed_dimension // d_head + + dtype = torch.float16 + + model_native = SpatialTransformer( + embed_dimension, + n_heads, + d_head, + context_dim=context_dim, + use_linear=True, + use_checkpoint=checkpoint, + attn_type="softmax", + depth=transformer_depth, + sdp_backend=SDPBackend.FLASH_ATTENTION, + ).to(device) + model_efficient_attn = SpatialTransformer( + embed_dimension, + n_heads, + d_head, + context_dim=context_dim, + use_linear=True, + depth=transformer_depth, + use_checkpoint=checkpoint, + attn_type="softmax-xformers", + ).to(device) + if not checkpoint and compile: + print("compiling models") + model_native = torch.compile(model_native) + model_efficient_attn = torch.compile(model_efficient_attn) + + x = torch.rand(batch_size, embed_dimension, h, w, device=device, dtype=dtype) + c = torch.rand(batch_size, context_len, context_dim, device=device, dtype=dtype) + + from torch.profiler import ProfilerActivity, profile, record_function + + activities = [ProfilerActivity.CPU, ProfilerActivity.CUDA] + + with torch.autocast("cuda"): + print( + f"The native model runs in {benchmark_torch_function_in_microseconds(model_native.forward, x, c):.3f} microseconds" + ) + print( + f"The efficientattn model runs in {benchmark_torch_function_in_microseconds(model_efficient_attn.forward, x, c):.3f} microseconds" + ) + + print(75 * "+") + print("NATIVE") + print(75 * "+") + torch.cuda.reset_peak_memory_stats() + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("NativeAttention stats"): + for _ in range(25): + model_native(x, c) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + print(torch.cuda.max_memory_allocated() * 1e-9, "GB used by native block") + + print(75 * "+") + print("Xformers") + print(75 * "+") + torch.cuda.reset_peak_memory_stats() + with profile( + activities=activities, record_shapes=False, profile_memory=True + ) as prof: + with record_function("xformers stats"): + for _ in range(25): + model_efficient_attn(x, c) + print(prof.key_averages().table(sort_by="cuda_time_total", row_limit=10)) + print(torch.cuda.max_memory_allocated() * 1e-9, "GB used by xformers block") + + +def test01(): + # conv1x1 vs linear + from sgm.util import count_params + + conv = torch.nn.Conv2d(3, 32, kernel_size=1).cuda() + print(count_params(conv)) + linear = torch.nn.Linear(3, 32).cuda() + print(count_params(linear)) + + print(conv.weight.shape) + + # use same initialization + linear.weight = torch.nn.Parameter(conv.weight.squeeze(-1).squeeze(-1)) + linear.bias = torch.nn.Parameter(conv.bias) + + print(linear.weight.shape) + + x = torch.randn(11, 3, 64, 64).cuda() + + xr = einops.rearrange(x, "b c h w -> b (h w) c").contiguous() + print(xr.shape) + out_linear = linear(xr) + print(out_linear.mean(), out_linear.shape) + + out_conv = conv(x) + print(out_conv.mean(), out_conv.shape) + print("done with test01.\n") + + +def test02(): + # try cosine flash attention + import time + + torch.backends.cuda.matmul.allow_tf32 = True + torch.backends.cudnn.allow_tf32 = True + torch.backends.cudnn.benchmark = True + print("testing cosine flash attention...") + DIM = 1024 + SEQLEN = 4096 + BS = 16 + + print(" softmax (vanilla) first...") + model = BasicTransformerBlock( + dim=DIM, + n_heads=16, + d_head=64, + dropout=0.0, + context_dim=None, + attn_mode="softmax", + ).cuda() + try: + x = torch.randn(BS, SEQLEN, DIM).cuda() + tic = time.time() + y = model(x) + toc = time.time() + print(y.shape, toc - tic) + except RuntimeError as e: + # likely oom + print(str(e)) + + print("\n now flash-cosine...") + model = BasicTransformerBlock( + dim=DIM, + n_heads=16, + d_head=64, + dropout=0.0, + context_dim=None, + attn_mode="flash-cosine", + ).cuda() + x = torch.randn(BS, SEQLEN, DIM).cuda() + tic = time.time() + y = model(x) + toc = time.time() + print(y.shape, toc - tic) + print("done with test02.\n") + + +if __name__ == "__main__": + # test01() + # test02() + # test03() + + # benchmark_attn() + benchmark_transformer_blocks() + + print("done.") diff --git a/MindEye_Imagery/src/generative_models/scripts/util/detection/__init__.py b/MindEye_Imagery/src/generative_models/scripts/util/detection/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/MindEye_Imagery/src/generative_models/scripts/util/detection/nsfw_and_watermark_dectection.py b/MindEye_Imagery/src/generative_models/scripts/util/detection/nsfw_and_watermark_dectection.py new file mode 100644 index 0000000000000000000000000000000000000000..1096b8177d8e3dbcf8e913f924e98d5ce58cb120 --- /dev/null +++ b/MindEye_Imagery/src/generative_models/scripts/util/detection/nsfw_and_watermark_dectection.py @@ -0,0 +1,110 @@ +import os + +import clip +import numpy as np +import torch +import torchvision.transforms as T +from PIL import Image + +RESOURCES_ROOT = "scripts/util/detection/" + + +def predict_proba(X, weights, biases): + logits = X @ weights.T + biases + proba = np.where( + logits >= 0, 1 / (1 + np.exp(-logits)), np.exp(logits) / (1 + np.exp(logits)) + ) + return proba.T + + +def load_model_weights(path: str): + model_weights = np.load(path) + return model_weights["weights"], model_weights["biases"] + + +def clip_process_images(images: torch.Tensor) -> torch.Tensor: + min_size = min(images.shape[-2:]) + return T.Compose( + [ + T.CenterCrop(min_size), # TODO: this might affect the watermark, check this + T.Resize(224, interpolation=T.InterpolationMode.BICUBIC, antialias=True), + T.Normalize( + (0.48145466, 0.4578275, 0.40821073), + (0.26862954, 0.26130258, 0.27577711), + ), + ] + )(images) + + +class DeepFloydDataFiltering(object): + def __init__( + self, verbose: bool = False, device: torch.device = torch.device("cpu") + ): + super().__init__() + self.verbose = verbose + self._device = None + self.clip_model, _ = clip.load("ViT-L/14", device=device) + self.clip_model.eval() + + self.cpu_w_weights, self.cpu_w_biases = load_model_weights( + os.path.join(RESOURCES_ROOT, "w_head_v1.npz") + ) + self.cpu_p_weights, self.cpu_p_biases = load_model_weights( + os.path.join(RESOURCES_ROOT, "p_head_v1.npz") + ) + self.w_threshold, self.p_threshold = 0.5, 0.5 + + @torch.inference_mode() + def __call__(self, images: torch.Tensor) -> torch.Tensor: + imgs = clip_process_images(images) + if self._device is None: + self._device = next(p for p in self.clip_model.parameters()).device + image_features = self.clip_model.encode_image(imgs.to(self._device)) + image_features = image_features.detach().cpu().numpy().astype(np.float16) + p_pred = predict_proba(image_features, self.cpu_p_weights, self.cpu_p_biases) + w_pred = predict_proba(image_features, self.cpu_w_weights, self.cpu_w_biases) + print(f"p_pred = {p_pred}, w_pred = {w_pred}") if self.verbose else None + query = p_pred > self.p_threshold + if query.sum() > 0: + print(f"Hit for p_threshold: {p_pred}") if self.verbose else None + images[query] = T.GaussianBlur(99, sigma=(100.0, 100.0))(images[query]) + query = w_pred > self.w_threshold + if query.sum() > 0: + print(f"Hit for w_threshold: {w_pred}") if self.verbose else None + images[query] = T.GaussianBlur(99, sigma=(100.0, 100.0))(images[query]) + return images + + +def load_img(path: str) -> torch.Tensor: + image = Image.open(path) + if not image.mode == "RGB": + image = image.convert("RGB") + image_transforms = T.Compose( + [ + T.ToTensor(), + ] + ) + return image_transforms(image)[None, ...] + + +def test(root): + from einops import rearrange + + filter = DeepFloydDataFiltering(verbose=True) + for p in os.listdir((root)): + print(f"running on {p}...") + img = load_img(os.path.join(root, p)) + filtered_img = filter(img) + filtered_img = rearrange( + 255.0 * (filtered_img.numpy())[0], "c h w -> h w c" + ).astype(np.uint8) + Image.fromarray(filtered_img).save( + os.path.join(root, f"{os.path.splitext(p)[0]}-filtered.jpg") + ) + + +if __name__ == "__main__": + import fire + + fire.Fire(test) + print("done.") diff --git a/MindEye_Imagery/src/vdvae/.ipynb_checkpoints/vae-checkpoint.py b/MindEye_Imagery/src/vdvae/.ipynb_checkpoints/vae-checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..97b08874d1c436fc9fd7a7c3b32ae44cf4656330 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/.ipynb_checkpoints/vae-checkpoint.py @@ -0,0 +1,252 @@ +import torch +from torch import nn +from torch.nn import functional as F +from vae_helpers import HModule, get_1x1, get_3x3, DmolNet, draw_gaussian_diag_samples, gaussian_analytical_kl +from collections import defaultdict +import numpy as np +import itertools + + +class Block(nn.Module): + def __init__(self, in_width, middle_width, out_width, down_rate=None, residual=False, use_3x3=True, zero_last=False): + super().__init__() + self.down_rate = down_rate + self.residual = residual + self.c1 = get_1x1(in_width, middle_width) + self.c2 = get_3x3(middle_width, middle_width) if use_3x3 else get_1x1(middle_width, middle_width) + self.c3 = get_3x3(middle_width, middle_width) if use_3x3 else get_1x1(middle_width, middle_width) + self.c4 = get_1x1(middle_width, out_width, zero_weights=zero_last) + + def forward(self, x): + xhat = self.c1(F.gelu(x)) + xhat = self.c2(F.gelu(xhat)) + xhat = self.c3(F.gelu(xhat)) + xhat = self.c4(F.gelu(xhat)) + out = x + xhat if self.residual else xhat + if self.down_rate is not None: + out = F.avg_pool2d(out, kernel_size=self.down_rate, stride=self.down_rate) + return out + + +def parse_layer_string(s): + layers = [] + for ss in s.split(','): + if 'x' in ss: + res, num = ss.split('x') + count = int(num) + layers += [(int(res), None) for _ in range(count)] + elif 'm' in ss: + res, mixin = [int(a) for a in ss.split('m')] + layers.append((res, mixin)) + elif 'd' in ss: + res, down_rate = [int(a) for a in ss.split('d')] + layers.append((res, down_rate)) + else: + res = int(ss) + layers.append((res, None)) + return layers + + +def pad_channels(t, width): + d1, d2, d3, d4 = t.shape + empty = torch.zeros(d1, width, d3, d4, device=t.device) + empty[:, :d2, :, :] = t + return empty + + +def get_width_settings(width, s): + mapping = defaultdict(lambda: width) + if s: + s = s.split(',') + for ss in s: + k, v = ss.split(':') + mapping[int(k)] = int(v) + return mapping + + +class Encoder(HModule): + def build(self): + H = self.H + self.in_conv = get_3x3(H.image_channels, H.width) + self.widths = get_width_settings(H.width, H.custom_width_str) + enc_blocks = [] + blockstr = parse_layer_string(H.enc_blocks) + for res, down_rate in blockstr: + use_3x3 = res > 2 # Don't use 3x3s for 1x1, 2x2 patches + enc_blocks.append(Block(self.widths[res], int(self.widths[res] * H.bottleneck_multiple), self.widths[res], down_rate=down_rate, residual=True, use_3x3=use_3x3)) + n_blocks = len(blockstr) + for b in enc_blocks: + b.c4.weight.data *= np.sqrt(1 / n_blocks) + self.enc_blocks = nn.ModuleList(enc_blocks) + + def forward(self, x): + x = x.permute(0, 3, 1, 2).contiguous() + x = self.in_conv(x) + activations = {} + activations[x.shape[2]] = x + for block in self.enc_blocks: + x = block(x) + res = x.shape[2] + x = x if x.shape[1] == self.widths[res] else pad_channels(x, self.widths[res]) + activations[res] = x + return activations + + +class DecBlock(nn.Module): + def __init__(self, H, res, mixin, n_blocks): + super().__init__() + self.base = res + self.mixin = mixin + self.H = H + self.widths = get_width_settings(H.width, H.custom_width_str) + width = self.widths[res] + use_3x3 = res > 2 + cond_width = int(width * H.bottleneck_multiple) + self.zdim = H.zdim + self.enc = Block(width * 2, cond_width, H.zdim * 2, residual=False, use_3x3=use_3x3) + self.prior = Block(width, cond_width, H.zdim * 2 + width, residual=False, use_3x3=use_3x3, zero_last=True) + self.z_proj = get_1x1(H.zdim, width) + self.z_proj.weight.data *= np.sqrt(1 / n_blocks) + self.resnet = Block(width, cond_width, width, residual=True, use_3x3=use_3x3) + self.resnet.c4.weight.data *= np.sqrt(1 / n_blocks) + self.z_fn = lambda x: self.z_proj(x) + + def sample(self, x, acts): + qm, qv = self.enc(torch.cat([x, acts], dim=1)).chunk(2, dim=1) + feats = self.prior(x) + pm, pv, xpp = feats[:, :self.zdim, ...], feats[:, self.zdim:self.zdim * 2, ...], feats[:, self.zdim * 2:, ...] + x = x + xpp + z = draw_gaussian_diag_samples(qm, qv) + kl = gaussian_analytical_kl(qm, pm, qv, pv) + return z, x, kl + + def sample_uncond(self, x, t=None, lvs=None): + n, c, h, w = x.shape + feats = self.prior(x) + pm, pv, xpp = feats[:, :self.zdim, ...], feats[:, self.zdim:self.zdim * 2, ...], feats[:, self.zdim * 2:, ...] + x = x + xpp + if lvs is not None: + z = lvs + else: + if t is not None: + pv = pv + torch.ones_like(pv) * np.log(t) + z = draw_gaussian_diag_samples(pm, pv) + return z, x + + def get_inputs(self, xs, activations): + acts = activations[self.base] + try: + x = xs[self.base] + except KeyError: + x = torch.zeros_like(acts) + if acts.shape[0] != x.shape[0]: + x = x.repeat(acts.shape[0], 1, 1, 1) + return x, acts + + def forward(self, xs, activations, get_latents=False): + x, acts = self.get_inputs(xs, activations) + if self.mixin is not None: + x = x + F.interpolate(xs[self.mixin][:, :x.shape[1], ...], scale_factor=self.base // self.mixin) + z, x, kl = self.sample(x, acts) + x = x + self.z_fn(z) + x = self.resnet(x) + xs[self.base] = x + if get_latents: + return xs, dict(z=z.detach(), kl=kl) + return xs, dict(kl=kl) + + def forward_uncond(self, xs, t=None, lvs=None): + try: + x = xs[self.base] + except KeyError: + ref = xs[list(xs.keys())[0]] + x = torch.zeros(dtype=ref.dtype, size=(ref.shape[0], self.widths[self.base], self.base, self.base), device=ref.device) + if self.mixin is not None: + x = x + F.interpolate(xs[self.mixin][:, :x.shape[1], ...], scale_factor=self.base // self.mixin) + z, x = self.sample_uncond(x, t, lvs=lvs) + x = x + self.z_fn(z) + x = self.resnet(x) + xs[self.base] = x + return xs + + +class Decoder(HModule): + + def build(self): + H = self.H + resos = set() + dec_blocks = [] + self.widths = get_width_settings(H.width, H.custom_width_str) + blocks = parse_layer_string(H.dec_blocks) + for idx, (res, mixin) in enumerate(blocks): + dec_blocks.append(DecBlock(H, res, mixin, n_blocks=len(blocks))) + resos.add(res) + self.resolutions = sorted(resos) + self.dec_blocks = nn.ModuleList(dec_blocks) + self.bias_xs = nn.ParameterList([nn.Parameter(torch.zeros(1, self.widths[res], res, res)) for res in self.resolutions if res <= H.no_bias_above]) + self.out_net = DmolNet(H) + self.gain = nn.Parameter(torch.ones(1, H.width, 1, 1)) + self.bias = nn.Parameter(torch.zeros(1, H.width, 1, 1)) + self.final_fn = lambda x: x * self.gain + self.bias + + def forward(self, activations, get_latents=False): + stats = [] + xs = {a.shape[2]: a for a in self.bias_xs} + for block in self.dec_blocks: + xs, block_stats = block(xs, activations, get_latents=get_latents) + stats.append(block_stats) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size], stats + + def forward_uncond(self, n, t=None, y=None): + xs = {} + for bias in self.bias_xs: + xs[bias.shape[2]] = bias.repeat(n, 1, 1, 1) + for idx, block in enumerate(self.dec_blocks): + try: + temp = t[idx] + except TypeError: + temp = t + xs = block.forward_uncond(xs, temp) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size] + + def forward_manual_latents(self, n, latents, t=None): + xs = {} + for bias in self.bias_xs: + xs[bias.shape[2]] = bias.repeat(n, 1, 1, 1) + for block, lvs in itertools.zip_longest(self.dec_blocks, latents): + xs = block.forward_uncond(xs, t, lvs=lvs) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size] + + +class VAE(HModule): + def build(self): + self.encoder = Encoder(self.H) + self.decoder = Decoder(self.H) + + def forward(self, x, x_target): + activations = self.encoder.forward(x) + px_z, stats = self.decoder.forward(activations) + distortion_per_pixel = self.decoder.out_net.nll(px_z, x_target) + rate_per_pixel = torch.zeros_like(distortion_per_pixel) + ndims = np.prod(x.shape[1:]) + for statdict in stats: + rate_per_pixel += statdict['kl'].sum(dim=(1, 2, 3)) + rate_per_pixel /= ndims + elbo = (distortion_per_pixel + rate_per_pixel).mean() + return dict(elbo=elbo, distortion=distortion_per_pixel.mean(), rate=rate_per_pixel.mean()) + + def forward_get_latents(self, x): + activations = self.encoder.forward(x) + _, stats = self.decoder.forward(activations, get_latents=True) + return stats + + def forward_uncond_samples(self, n_batch, t=None): + px_z = self.decoder.forward_uncond(n_batch, t=t) + return self.decoder.out_net.sample(px_z) + + def forward_samples_set_latents(self, n_batch, latents, t=None): + px_z = self.decoder.forward_manual_latents(n_batch, latents, t=t) + return self.decoder.out_net.sample(px_z) diff --git a/MindEye_Imagery/src/vdvae/LICENSE.md b/MindEye_Imagery/src/vdvae/LICENSE.md new file mode 100644 index 0000000000000000000000000000000000000000..1e865209800c80f532b2231b542fea496a731e15 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/LICENSE.md @@ -0,0 +1,7 @@ +Copyright © 2020 OpenAI + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. \ No newline at end of file diff --git a/MindEye_Imagery/src/vdvae/README.md b/MindEye_Imagery/src/vdvae/README.md new file mode 100644 index 0000000000000000000000000000000000000000..bde734963a2485ee66c5afed116afdf6421c4d47 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/README.md @@ -0,0 +1,103 @@ +# Very Deep VAEs + +Repository for the paper "Very Deep VAEs Generalize Autoregressive Models and Can Outperform Them on Images" (https://arxiv.org/abs/2011.10650) + +Some model samples and a visualization of how it generates them: +![image](header-image.png) + +This repository is tested with PyTorch 1.6, CUDA 10.1, Numpy 1.16, Ubuntu 18.04, and V100 GPUs. + +# Setup +Several additional packages are required, including NVIDIA Apex: +``` +pip install imageio +pip install mpi4py +pip install sklearn +git clone https://github.com/NVIDIA/apex +cd apex +pip install -v --no-cache-dir --global-option="--cpp_ext" --global-option="--cuda_ext" ./ +cd .. +``` + +Also, you'll have to download the data, depending on which one you want to run: +``` +./setup_cifar10.sh +./setup_imagenet.sh imagenet32 +./setup_imagenet.sh imagenet64 +./setup_ffhq256.sh +./setup_ffhq1024.sh /path/to/images1024x1024 # this one depends on you first downloading the subfolder `images_1024x1024` from https://github.com/NVlabs/ffhq-dataset on your own +``` + +# Training models +Hyperparameters all reside in `hps.py`. We use 2 gpus for our CIFAR-10 runs, and 32 for the rest of the models. (Using a lower batch size is also possible and results in slower learning, and may also require a lower learning rate). + +The `mpiexec` arguments you use for runs with more than 1 node depend on the configuration of your system, so please adapt accordingly. + +```bash +mpiexec -n 2 python train.py --hps cifar10 +mpiexec -n 32 python train.py --hps imagenet32 +mpiexec -n 32 python train.py --hps imagenet64 +mpiexec -n 32 python train.py --hps ffhq256 +mpiexec -n 32 python train.py --hps ffhq1024 +``` + +# Restoring saved models +For convenience, we have included training checkpoints which can be restored in order to confirm performance, continue training, or generate samples. + +### ImageNet 32 +```bash +# 119M parameter model, trained for 1.7M iters (about 2.5 weeks on 32 V100) +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/imagenet32-iter-1700000-log.jsonl +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/imagenet32-iter-1700000-model.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/imagenet32-iter-1700000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/imagenet32-iter-1700000-opt.th +python train.py --hps imagenet32 --restore_path imagenet32-iter-1700000-model.th --restore_ema_path imagenet32-iter-1700000-model-ema.th --restore_log_path imagenet32-iter-1700000-log.jsonl --restore_optimizer_path imagenet32-iter-1700000-opt.th --test_eval +# should give 2.6364 nats per dim, which is 3.80 bpd +``` + +### ImageNet 64 +```bash +# 125M parameter model, trained for 1.6M iters (about 2.5 weeks on 32 V100) +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/imagenet64-iter-1600000-log.jsonl +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/imagenet64-iter-1600000-model.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/imagenet64-iter-1600000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/imagenet64-iter-1600000-opt.th +python train.py --hps imagenet64 --restore_path imagenet64-iter-1600000-model.th --restore_ema_path imagenet64-iter-1600000-model-ema.th --restore_log_path imagenet64-iter-1600000-log.jsonl --restore_optimizer_path imagenet64-iter-1600000-opt.th --test_eval +# should be 2.44 nats, or 3.52 bits per dim +``` + +### FFHQ-256 +```bash +# 115M parameters, trained for 1.7M iterations (or about 2.5 weeks) on 32 V100 +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq256-iter-1700000-log.jsonl +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq256-iter-1700000-model.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq256-iter-1700000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq256-iter-1700000-opt.th +python train.py --hps ffhq256 --restore_path ffhq256-iter-1700000-model.th --restore_ema_path ffhq256-iter-1700000-model-ema.th --restore_log_path ffhq256-iter-1700000-log.jsonl --restore_optimizer_path ffhq256-iter-1700000-opt.th --test_eval +# should be 0.4232 nats, or 0.61 bits per dim +``` + +### FFHQ-1024 +```bash +# 115M parameters, trained for 1.7M iterations (or about 2.5 weeks) on 32 V100 +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq1024-iter-1700000-log.jsonl +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq1024-iter-1700000-model.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq1024-iter-1700000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq1024-iter-1700000-opt.th +python train.py --hps ffhq1024 --restore_path ffhq1024-iter-1700000-model.th --restore_ema_path ffhq1024-iter-1700000-model-ema.th --restore_log_path ffhq1024-iter-1700000-log.jsonl --restore_optimizer_path ffhq1024-iter-1700000-opt.th --test_eval +# should be 1.678 nats, or 2.42 bits per dim +``` + +### CIFAR-10 +```bash +# 39M parameters, trained for ~1M iterations with early stopping (a little less than a week on 2 GPUs) +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/cifar10-seed0-iter-900000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/cifar10-seed1-iter-1050000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/cifar10-seed2-iter-650000-model-ema.th +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets-2/cifar10-seed3-iter-1050000-model-ema.th +python train.py --hps cifar10 --restore_ema_path cifar10-seed0-iter-900000-model-ema.th --test_eval +python train.py --hps cifar10 --restore_ema_path cifar10-seed1-iter-1050000-model-ema.th --test_eval +python train.py --hps cifar10 --restore_ema_path cifar10-seed2-iter-650000-model-ema.th --test_eval +python train.py --hps cifar10 --restore_ema_path cifar10-seed3-iter-1050000-model-ema.th --test_eval +# seeds 0, 1, 2, 3 should give 2.879, 2.842, 2.898, 2.864 bits per dim, for an average of 2.87 bits per dim. +``` diff --git a/MindEye_Imagery/src/vdvae/__pycache__/data.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/data.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..7c5be01993a052522053dc606dea9fe769534c81 Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/data.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/hps.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/hps.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..004f7a4c38615b21ccc5f12224bead0a58b15871 Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/hps.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/image_utils.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/image_utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..07020613bb87f683e4bd98a555dc1c94a079a1e8 Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/image_utils.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/model_utils.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/model_utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..66f5014e68f80799208223979dff4a0f0c69643d Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/model_utils.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/train_helpers.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/train_helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..199a4460d3921057c86b659fec7fe2ef3e7d8697 Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/train_helpers.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/vae.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/vae.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9017e0d97b0ab5d987f42bb07304cb71b2c25a68 Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/vae.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/vae_helpers.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/vae_helpers.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc9a12f6c5b1a4de32bc853f810e3500ebfdbe9a Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/vae_helpers.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/__pycache__/vdvae_utils.cpython-311.pyc b/MindEye_Imagery/src/vdvae/__pycache__/vdvae_utils.cpython-311.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4bb1e5dc1bd4ad88631a4dc7d087a624d738042f Binary files /dev/null and b/MindEye_Imagery/src/vdvae/__pycache__/vdvae_utils.cpython-311.pyc differ diff --git a/MindEye_Imagery/src/vdvae/data.py b/MindEye_Imagery/src/vdvae/data.py new file mode 100644 index 0000000000000000000000000000000000000000..8131b42e5730a147cd9ab08600c863a203e2efe8 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/data.py @@ -0,0 +1,163 @@ +import numpy as np +import pickle +import os +import torch +from torch.utils.data import TensorDataset +from torchvision.datasets import ImageFolder +import torchvision.transforms as transforms +from sklearn.model_selection import train_test_split + + +def set_up_data(H): + shift_loss = -127.5 + scale_loss = 1. / 127.5 + if H.dataset == 'imagenet32': + trX, vaX, teX = imagenet32(H.data_root) + H.image_size = 32 + H.image_channels = 3 + shift = -116.2373 + scale = 1. / 69.37404 + elif H.dataset == 'imagenet64': + trX, vaX, teX = imagenet64(H.data_root) + H.image_size = 64 + H.image_channels = 3 + shift = -115.92961967 + scale = 1. / 69.37404 + elif H.dataset == 'ffhq_256': + trX, vaX, teX = ffhq256(H.data_root) + H.image_size = 256 + H.image_channels = 3 + shift = -112.8666757481 + scale = 1. / 69.84780273 + elif H.dataset == 'ffhq_1024': + trX, vaX, teX = ffhq1024(H.data_root) + H.image_size = 1024 + H.image_channels = 3 + shift = -0.4387 + scale = 1.0 / 0.2743 + shift_loss = -0.5 + scale_loss = 2.0 + elif H.dataset == 'cifar10': + (trX, _), (vaX, _), (teX, _) = cifar10(H.data_root, one_hot=False) + H.image_size = 32 + H.image_channels = 3 + shift = -120.63838 + scale = 1. / 64.16736 + else: + raise ValueError('unknown dataset: ', H.dataset) + + do_low_bit = H.dataset in ['ffhq_256'] + + if H.test_eval: + print('DOING TEST') + eval_dataset = teX + else: + eval_dataset = vaX + + shift = torch.tensor([shift]).cuda().view(1, 1, 1, 1) + scale = torch.tensor([scale]).cuda().view(1, 1, 1, 1) + shift_loss = torch.tensor([shift_loss]).cuda().view(1, 1, 1, 1) + scale_loss = torch.tensor([scale_loss]).cuda().view(1, 1, 1, 1) + + if H.dataset == 'ffhq_1024': + train_data = ImageFolder(trX, transforms.ToTensor()) + valid_data = ImageFolder(eval_dataset, transforms.ToTensor()) + untranspose = True + else: + train_data = TensorDataset(torch.as_tensor(trX)) + valid_data = TensorDataset(torch.as_tensor(eval_dataset)) + untranspose = False + + def preprocess_func(x): + nonlocal shift + nonlocal scale + nonlocal shift_loss + nonlocal scale_loss + nonlocal do_low_bit + nonlocal untranspose + 'takes in a data example and returns the preprocessed input' + 'as well as the input processed for the loss' + if untranspose: + x[0] = x[0].permute(0, 2, 3, 1) + inp = x[0].cuda(non_blocking=True).float() + out = inp.clone() + inp.add_(shift).mul_(scale) + if do_low_bit: + # 5 bits of precision + out.mul_(1. / 8.).floor_().mul_(8.) + out.add_(shift_loss).mul_(scale_loss) + return inp, out + + return H, train_data, valid_data, preprocess_func + + +def mkdir_p(path): + os.makedirs(path, exist_ok=True) + + +def flatten(outer): + return [el for inner in outer for el in inner] + + +def unpickle_cifar10(file): + fo = open(file, 'rb') + data = pickle.load(fo, encoding='bytes') + fo.close() + data = dict(zip([k.decode() for k in data.keys()], data.values())) + return data + + +def imagenet32(data_root): + trX = np.load(os.path.join(data_root, 'imagenet32-train.npy'), mmap_mode='r') + np.random.seed(42) + tr_va_split_indices = np.random.permutation(trX.shape[0]) + train = trX[tr_va_split_indices[:-5000]] + valid = trX[tr_va_split_indices[-5000:]] + test = np.load(os.path.join(data_root, 'imagenet32-valid.npy'), mmap_mode='r') + return train, valid, test + + +def imagenet64(data_root): + trX = np.load(os.path.join(data_root, 'imagenet64-train.npy'), mmap_mode='r') + np.random.seed(42) + tr_va_split_indices = np.random.permutation(trX.shape[0]) + train = trX[tr_va_split_indices[:-5000]] + valid = trX[tr_va_split_indices[-5000:]] + test = np.load(os.path.join(data_root, 'imagenet64-valid.npy'), mmap_mode='r') # this is test. + return train, valid, test + + +def ffhq1024(data_root): + # we did not significantly tune hyperparameters on ffhq-1024, and so simply evaluate on the test set + return os.path.join(data_root, 'ffhq1024/train'), os.path.join(data_root, 'ffhq1024/valid'), os.path.join(data_root, 'ffhq1024/valid') + + +def ffhq256(data_root): + trX = np.load(os.path.join(data_root, 'ffhq-256.npy'), mmap_mode='r') + np.random.seed(5) + tr_va_split_indices = np.random.permutation(trX.shape[0]) + train = trX[tr_va_split_indices[:-7000]] + valid = trX[tr_va_split_indices[-7000:]] + # we did not significantly tune hyperparameters on ffhq-256, and so simply evaluate on the test set + return train, valid, valid + + +def cifar10(data_root, one_hot=True): + tr_data = [unpickle_cifar10(os.path.join(data_root, 'cifar-10-batches-py/', 'data_batch_%d' % i)) for i in range(1, 6)] + trX = np.vstack(data['data'] for data in tr_data) + trY = np.asarray(flatten([data['labels'] for data in tr_data])) + te_data = unpickle_cifar10(os.path.join(data_root, 'cifar-10-batches-py/', 'test_batch')) + teX = np.asarray(te_data['data']) + teY = np.asarray(te_data['labels']) + trX = trX.reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1) + teX = teX.reshape(-1, 3, 32, 32).transpose(0, 2, 3, 1) + trX, vaX, trY, vaY = train_test_split(trX, trY, test_size=5000, random_state=11172018) + if one_hot: + trY = np.eye(10, dtype=np.float32)[trY] + vaY = np.eye(10, dtype=np.float32)[vaY] + teY = np.eye(10, dtype=np.float32)[teY] + else: + trY = np.reshape(trY, [-1, 1]) + vaY = np.reshape(vaY, [-1, 1]) + teY = np.reshape(teY, [-1, 1]) + return (trX, trY), (vaX, vaY), (teX, teY) diff --git a/MindEye_Imagery/src/vdvae/files_to_npy.py b/MindEye_Imagery/src/vdvae/files_to_npy.py new file mode 100644 index 0000000000000000000000000000000000000000..e47318ca1b6812513d1885e6b932849dcaf83d84 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/files_to_npy.py @@ -0,0 +1,14 @@ +import sys +import numpy as np +import imageio +import glob +import os + +if __name__ == "__main__": + print("moving images in", sys.argv[1], "to", sys.argv[2]) + files = glob.glob(os.path.join(sys.argv[1], "*.png")) + shape = imageio.imread(files[0]).shape + data = np.zeros(shape=(len(files), *shape), dtype=np.uint8) + for idx, f in enumerate(files): + data[idx] = imageio.imread(f) + np.save(sys.argv[2], data) diff --git a/MindEye_Imagery/src/vdvae/hps.py b/MindEye_Imagery/src/vdvae/hps.py new file mode 100644 index 0000000000000000000000000000000000000000..ccecb937874160b0ece1ad92794a789392ad3f69 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/hps.py @@ -0,0 +1,159 @@ +HPARAMS_REGISTRY = {} + + +class Hyperparams(dict): + def __getattr__(self, attr): + try: + return self[attr] + except KeyError: + return None + + def __setattr__(self, attr, value): + self[attr] = value + + +cifar10 = Hyperparams() +cifar10.width = 384 +cifar10.lr = 0.0002 +cifar10.zdim = 16 +cifar10.wd = 0.01 +cifar10.dec_blocks = "1x1,4m1,4x2,8m4,8x5,16m8,16x10,32m16,32x21" +cifar10.enc_blocks = "32x11,32d2,16x6,16d2,8x6,8d2,4x3,4d4,1x3" +cifar10.warmup_iters = 100 +cifar10.dataset = 'cifar10' +cifar10.n_batch = 16 +cifar10.ema_rate = 0.9999 +HPARAMS_REGISTRY['cifar10'] = cifar10 + + +i32 = Hyperparams() +i32.update(cifar10) +i32.dataset = 'imagenet32' +i32.ema_rate = 0.999 +i32.dec_blocks = "1x2,4m1,4x4,8m4,8x9,16m8,16x19,32m16,32x40" +i32.enc_blocks = "32x15,32d2,16x9,16d2,8x8,8d2,4x6,4d4,1x6" +i32.width = 512 +i32.n_batch = 8 +i32.lr = 0.00015 +i32.grad_clip = 200. +i32.skip_threshold = 300. +i32.epochs_per_eval = 1 +i32.epochs_per_eval_save = 1 +HPARAMS_REGISTRY['imagenet32'] = i32 + +i64 = Hyperparams() +i64.update(i32) +i64.n_batch = 4 +i64.grad_clip = 220.0 +i64.skip_threshold = 380.0 +i64.dataset = 'imagenet64' +i64.dec_blocks = "1x2,4m1,4x3,8m4,8x7,16m8,16x15,32m16,32x31,64m32,64x12" +i64.enc_blocks = "64x11,64d2,32x20,32d2,16x9,16d2,8x8,8d2,4x7,4d4,1x5" +HPARAMS_REGISTRY['imagenet64'] = i64 + +ffhq_256 = Hyperparams() +ffhq_256.update(i64) +ffhq_256.n_batch = 1 +ffhq_256.lr = 0.00015 +ffhq_256.dataset = 'ffhq_256' +ffhq_256.epochs_per_eval = 1 +ffhq_256.epochs_per_eval_save = 1 +ffhq_256.num_images_visualize = 2 +ffhq_256.num_variables_visualize = 3 +ffhq_256.num_temperatures_visualize = 1 +ffhq_256.dec_blocks = "1x2,4m1,4x3,8m4,8x4,16m8,16x9,32m16,32x21,64m32,64x13,128m64,128x7,256m128" +ffhq_256.enc_blocks = "256x3,256d2,128x8,128d2,64x12,64d2,32x17,32d2,16x7,16d2,8x5,8d2,4x5,4d4,1x4" +ffhq_256.no_bias_above = 64 +ffhq_256.grad_clip = 130. +ffhq_256.skip_threshold = 180. +HPARAMS_REGISTRY['ffhq256'] = ffhq_256 + + +ffhq1024 = Hyperparams() +ffhq1024.update(ffhq_256) +ffhq1024.dataset = 'ffhq_1024' +ffhq1024.data_root = './ffhq_images1024x1024' +ffhq1024.epochs_per_eval = 1 +ffhq1024.epochs_per_eval_save = 1 +ffhq1024.num_images_visualize = 1 +ffhq1024.iters_per_images = 25000 +ffhq1024.num_variables_visualize = 0 +ffhq1024.num_temperatures_visualize = 4 +ffhq1024.grad_clip = 360. +ffhq1024.skip_threshold = 500. +ffhq1024.num_mixtures = 2 +ffhq1024.width = 16 +ffhq1024.lr = 0.00007 +ffhq1024.dec_blocks = "1x2,4m1,4x3,8m4,8x4,16m8,16x9,32m16,32x20,64m32,64x14,128m64,128x7,256m128,256x2,512m256,1024m512" +ffhq1024.enc_blocks = "1024x1,1024d2,512x3,512d2,256x5,256d2,128x7,128d2,64x10,64d2,32x14,32d2,16x7,16d2,8x5,8d2,4x5,4d4,1x4" +ffhq1024.custom_width_str = "512:32,256:64,128:512,64:512,32:512,16:512,8:512,4:512,1:512" +HPARAMS_REGISTRY['ffhq1024'] = ffhq1024 + + +def parse_args_and_update_hparams(H, parser, s=None): + args = parser.parse_args(s) + valid_args = set(args.__dict__.keys()) + hparam_sets = [x for x in args.hparam_sets.split(',') if x] + for hp_set in hparam_sets: + hps = HPARAMS_REGISTRY[hp_set] + for k in hps: + if k not in valid_args: + raise ValueError(f"{k} not in default args") + parser.set_defaults(**hps) + H.update(parser.parse_args(s).__dict__) + + +def add_vae_arguments(parser): + parser.add_argument('--seed', type=int, default=0) + parser.add_argument('--port', type=int, default=29500) + parser.add_argument('--save_dir', type=str, default='./saved_models') + parser.add_argument('--data_root', type=str, default='./') + + parser.add_argument('--desc', type=str, default='test') + parser.add_argument('--hparam_sets', '--hps', type=str) + parser.add_argument('--restore_path', type=str, default=None) + parser.add_argument('--restore_ema_path', type=str, default=None) + parser.add_argument('--restore_log_path', type=str, default=None) + parser.add_argument('--restore_optimizer_path', type=str, default=None) + parser.add_argument('--dataset', type=str, default='cifar10') + + parser.add_argument('--ema_rate', type=float, default=0.999) + + parser.add_argument('--enc_blocks', type=str, default=None) + parser.add_argument('--dec_blocks', type=str, default=None) + parser.add_argument('--zdim', type=int, default=16) + parser.add_argument('--width', type=int, default=512) + parser.add_argument('--custom_width_str', type=str, default='') + parser.add_argument('--bottleneck_multiple', type=float, default=0.25) + + parser.add_argument('--no_bias_above', type=int, default=64) + parser.add_argument('--scale_encblock', action="store_true") + + parser.add_argument('--test_eval', action="store_true") + parser.add_argument('--warmup_iters', type=float, default=0) + + parser.add_argument('--num_mixtures', type=int, default=10) + parser.add_argument('--grad_clip', type=float, default=200.0) + parser.add_argument('--skip_threshold', type=float, default=400.0) + parser.add_argument('--lr', type=float, default=0.00015) + parser.add_argument('--lr_prior', type=float, default=0.00015) + parser.add_argument('--wd', type=float, default=0.0) + parser.add_argument('--wd_prior', type=float, default=0.0) + parser.add_argument('--num_epochs', type=int, default=10000) + parser.add_argument('--n_batch', type=int, default=32) + parser.add_argument('--adam_beta1', type=float, default=0.9) + parser.add_argument('--adam_beta2', type=float, default=0.9) + + parser.add_argument('--temperature', type=float, default=1.0) + + parser.add_argument('--iters_per_ckpt', type=int, default=25000) + parser.add_argument('--iters_per_print', type=int, default=1000) + parser.add_argument('--iters_per_save', type=int, default=10000) + parser.add_argument('--iters_per_images', type=int, default=10000) + parser.add_argument('--epochs_per_eval', type=int, default=10) + parser.add_argument('--epochs_per_probe', type=int, default=None) + parser.add_argument('--epochs_per_eval_save', type=int, default=20) + parser.add_argument('--num_images_visualize', type=int, default=8) + parser.add_argument('--num_variables_visualize', type=int, default=6) + parser.add_argument('--num_temperatures_visualize', type=int, default=3) + return parser diff --git a/MindEye_Imagery/src/vdvae/image_utils.py b/MindEye_Imagery/src/vdvae/image_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..7f03dfb2cb7517a6762df0d61b6fbd27cbebe266 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/image_utils.py @@ -0,0 +1,68 @@ +import io +import IPython.display +import PIL.Image +import os +from pprint import pformat +import numpy as np + +def imgrid(imarray, cols=4, pad=1, padval=255, row_major=True): + """Lays out a [N, H, W, C] image array as a single image grid.""" + pad = int(pad) + if pad < 0: + raise ValueError('pad must be non-negative') + cols = int(cols) + assert cols >= 1 + N, H, W, C = imarray.shape + rows = N // cols + int(N % cols != 0) + batch_pad = rows * cols - N + assert batch_pad >= 0 + post_pad = [batch_pad, pad, pad, 0] + pad_arg = [[0, p] for p in post_pad] + imarray = np.pad(imarray, pad_arg, 'constant', constant_values=padval) + H += pad + W += pad + grid = (imarray + .reshape(rows, cols, H, W, C) + .transpose(0, 2, 1, 3, 4) + .reshape(rows*H, cols*W, C)) + if pad: + grid = grid[:-pad, :-pad] + return grid + +def interleave(*args): + """Interleaves input arrays of the same shape along the batch axis.""" + if not args: + raise ValueError('At least one argument is required.') + a0 = args[0] + if any(a.shape != a0.shape for a in args): + raise ValueError('All inputs must have the same shape.') + if not a0.shape: + raise ValueError('Inputs must have at least one axis.') + out = np.transpose(args, [1, 0] + list(range(2, len(a0.shape) + 1))) + out = out.reshape(-1, *a0.shape[1:]) + return out + +def imshow(a, format='png', jpeg_fallback=True): + """Displays an image in the given format.""" + a = a.astype(np.uint8) + data = io.BytesIO() + PIL.Image.fromarray(a).save(data, format) + im_data = data.getvalue() + try: + disp = IPython.display.display(IPython.display.Image(im_data)) + except IOError: + if jpeg_fallback and format != 'jpeg': + print ('Warning: image was too large to display in format "{}"; ' + 'trying jpeg instead.').format(format) + return imshow(a, format='jpeg') + else: + raise + return disp + +def image_to_uint8(x): + """Converts [-1, 1] float array to [0, 255] uint8.""" + x = np.asarray(x) + x = (256. / 2.) * (x + 1.) + x = np.clip(x, 0, 255) + x = x.astype(np.uint8) + return x diff --git a/MindEye_Imagery/src/vdvae/model_utils.py b/MindEye_Imagery/src/vdvae/model_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..d8ec8e20e344b34290817ef52f11ba0d5fceab3f --- /dev/null +++ b/MindEye_Imagery/src/vdvae/model_utils.py @@ -0,0 +1,102 @@ +import torch +import numpy as np +#from mpi4py import MPI +import socket +import argparse +import os +import json +import subprocess +from hps import Hyperparams, parse_args_and_update_hparams, add_vae_arguments +from vdvae_utils import (logger, + local_mpi_rank, + mpi_size, + maybe_download, + mpi_rank) +from data import mkdir_p +from contextlib import contextmanager +import torch.distributed as dist +#from apex.optimizers import FusedAdam as AdamW +from vae import VAE +from torch.nn.parallel.distributed import DistributedDataParallel +from train_helpers import restore_params + +def set_up_hyperparams(s=None): + H = Hyperparams() + parser = argparse.ArgumentParser() + parser = add_vae_arguments(parser) + parse_args_and_update_hparams(H, parser, s=s) + setup_mpi(H) + setup_save_dirs(H) + logprint = logger(H.logdir) + for i, k in enumerate(sorted(H)): + logprint(type='hparam', key=k, value=H[k]) + np.random.seed(H.seed) + torch.manual_seed(H.seed) + torch.cuda.manual_seed(H.seed) + logprint('training model', H.desc, 'on', H.dataset) + return H, logprint + +def set_up_data(H, device="cuda"): + shift_loss = -127.5 + scale_loss = 1. / 127.5 + + #trX, vaX, teX = imagenet64(H.data_root) + H.image_size = 64 + H.image_channels = 3 + shift = -115.92961967 + scale = 1. / 69.37404 + + #if H.test_eval: + # print('DOING TEST') + # eval_dataset = teX + #else: + # eval_dataset = vaX + + shift = torch.tensor([shift]).to(device).view(1, 1, 1, 1) + scale = torch.tensor([scale]).to(device).view(1, 1, 1, 1) + shift_loss = torch.tensor([shift_loss]).to(device).view(1, 1, 1, 1) + scale_loss = torch.tensor([scale_loss]).to(device).view(1, 1, 1, 1) + + #train_data = TensorDataset(torch.as_tensor(trX)) + #valid_data = TensorDataset(torch.as_tensor(eval_dataset)) + #untranspose = False + + def preprocess_func(x): + nonlocal shift + nonlocal scale + nonlocal shift_loss + nonlocal scale_loss + 'takes in a data example and returns the preprocessed input' + 'as well as the input processed for the loss' + #untranspose = False + #if untranspose: + # x[0] = x[0].permute(0, 2, 3, 1) + inp = x.to(device, non_blocking=True).float() + out = inp.clone() + inp.add_(shift).mul_(scale) + out.add_(shift_loss).mul_(scale_loss) + return inp, out + + return H, preprocess_func + +def load_vaes(H, logprint=None, device="cuda"): + + ema_vae = VAE(H) + if H.restore_ema_path: + print(f'Restoring ema vae from {H.restore_ema_path}') + restore_params(ema_vae, H.restore_ema_path, map_cpu=True, local_rank=H.local_rank, mpi_size=H.mpi_size) + else: + ema_vae.load_state_dict(vae.state_dict()) + ema_vae.requires_grad_(False) + ema_vae = ema_vae.to(device) + + #vae = DistributedDataParallel(vae, device_ids=[H.local_rank], output_device=H.local_rank) + + #if len(list(vae.named_parameters())) != len(list(vae.parameters())): + # raise ValueError('Some params are not named. Please name all params.') + #total_params = 0 + #for name, p in vae.named_parameters(): + # total_params += np.prod(p.shape) + + #print(total_params=total_params, readable=f'{total_params:,}') + return ema_vae \ No newline at end of file diff --git a/MindEye_Imagery/src/vdvae/setup_cifar10.sh b/MindEye_Imagery/src/vdvae/setup_cifar10.sh new file mode 100644 index 0000000000000000000000000000000000000000..42b6c54229d327a033355d57d2476033ab15d860 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/setup_cifar10.sh @@ -0,0 +1,2 @@ +wget https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz +tar -xf cifar-10-python.tar.gz diff --git a/MindEye_Imagery/src/vdvae/setup_ffhq1024.sh b/MindEye_Imagery/src/vdvae/setup_ffhq1024.sh new file mode 100644 index 0000000000000000000000000000000000000000..511066400a6b89180c008c65e88b86868265ac48 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/setup_ffhq1024.sh @@ -0,0 +1,13 @@ +# the first argument to this script should be the path to the ffhq_images1024x1024 folder +# the same path should be provided as the `data_root` argument to train.py +cd $1 +mkdir train +mkdir train/0 +mkdir valid +mkdir valid/0 +for i in $(seq -f "%05g" 0 64999); do + mv $i.png train/0 +done +for i in $(seq -f "%05g" 65000 69999); do + mv $i.png valid/0 +done diff --git a/MindEye_Imagery/src/vdvae/setup_ffhq256.sh b/MindEye_Imagery/src/vdvae/setup_ffhq256.sh new file mode 100644 index 0000000000000000000000000000000000000000..454a81f669207cda28be3905dba50be54c65fafa --- /dev/null +++ b/MindEye_Imagery/src/vdvae/setup_ffhq256.sh @@ -0,0 +1,12 @@ +# we provide a copy of ffhq-256 for convenience, downsampled using the same function as NVAE (https://github.com/NVlabs/NVAE) (personal communication with author) + +# Resizing function is this one, with the default second argument and size=256 +# https://pytorch.org/docs/stable/torchvision/transforms.html#torchvision.transforms.Resize + +# 5-bit precision is calculated using the following lines, with num_bits=5 and for an x in [0, 1] +# x = torch.floor(x * 255 / 2 ** (8 - num_bits)) +# x /= (2 ** num_bits - 1) + +# the DMOL loss should also be adjusted to have 32 buckets instead of 256 (this code, or NVAE, can be used as reference) + +wget https://openaipublic.blob.core.windows.net/very-deep-vaes-assets/vdvae-assets/ffhq-256.npy \ No newline at end of file diff --git a/MindEye_Imagery/src/vdvae/setup_imagenet.sh b/MindEye_Imagery/src/vdvae/setup_imagenet.sh new file mode 100644 index 0000000000000000000000000000000000000000..fec4223129fb3cd80100e08d8542ac5baea95a02 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/setup_imagenet.sh @@ -0,0 +1,28 @@ +if [ "$1" == "imagenet32" ]; then + +echo "downloading imagenet32" +wget http://www.image-net.org/small/train_32x32.tar +wget http://www.image-net.org/small/valid_32x32.tar +tar -xvf train_32x32.tar +tar -xvf valid_32x32.tar +python files_to_npy.py train_32x32/ imagenet32-train.npy +python files_to_npy.py valid_32x32/ imagenet32-valid.npy + +elif [ "$1" == "imagenet64" ]; then + +echo "downloading imagenet64" +wget http://www.image-net.org/small/train_64x64.tar +wget http://www.image-net.org/small/valid_64x64.tar +tar -xvf train_64x64.tar +tar -xvf valid_64x64.tar +python files_to_npy.py train_64x64/ imagenet64-train.npy +python files_to_npy.py valid_64x64/ imagenet64-valid.npy + +else + +echo "please pass the string imagenet32 or imagenet64 as an argument" + +fi + + + diff --git a/MindEye_Imagery/src/vdvae/train.py b/MindEye_Imagery/src/vdvae/train.py new file mode 100644 index 0000000000000000000000000000000000000000..ab588cb50678275fd9a7c5f82bb4a850ebb5a759 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/train.py @@ -0,0 +1,136 @@ +import numpy as np +import imageio +import os +import time +import torch +from torch.utils.data import DataLoader +from torch.utils.data.distributed import DistributedSampler +from data import set_up_data +from vdvae_utils import get_cpu_stats_over_ranks +from train_helpers import set_up_hyperparams, load_vaes, load_opt, accumulate_stats, save_model, update_ema + + +def training_step(H, data_input, target, vae, ema_vae, optimizer, iterate): + t0 = time.time() + vae.zero_grad() + stats = vae.forward(data_input, target) + stats['elbo'].backward() + grad_norm = torch.nn.utils.clip_grad_norm_(vae.parameters(), H.grad_clip).item() + distortion_nans = torch.isnan(stats['distortion']).sum() + rate_nans = torch.isnan(stats['rate']).sum() + stats.update(dict(rate_nans=0 if rate_nans == 0 else 1, distortion_nans=0 if distortion_nans == 0 else 1)) + stats = get_cpu_stats_over_ranks(stats) + + skipped_updates = 1 + # only update if no rank has a nan and if the grad norm is below a specific threshold + if stats['distortion_nans'] == 0 and stats['rate_nans'] == 0 and (H.skip_threshold == -1 or grad_norm < H.skip_threshold): + optimizer.step() + skipped_updates = 0 + update_ema(vae, ema_vae, H.ema_rate) + + t1 = time.time() + stats.update(skipped_updates=skipped_updates, iter_time=t1 - t0, grad_norm=grad_norm) + return stats + + +def eval_step(data_input, target, ema_vae): + with torch.no_grad(): + stats = ema_vae.forward(data_input, target) + stats = get_cpu_stats_over_ranks(stats) + return stats + + +def get_sample_for_visualization(data, preprocess_fn, num, dataset): + for x in DataLoader(data, batch_size=num): + break + orig_image = (x[0] * 255.0).to(torch.uint8).permute(0, 2, 3, 1) if dataset == 'ffhq_1024' else x[0] + preprocessed = preprocess_fn(x)[0] + return orig_image, preprocessed + + +def train_loop(H, data_train, data_valid, preprocess_fn, vae, ema_vae, logprint): + optimizer, scheduler, cur_eval_loss, iterate, starting_epoch = load_opt(H, vae, logprint) + train_sampler = DistributedSampler(data_train, num_replicas=H.mpi_size, rank=H.rank) + viz_batch_original, viz_batch_processed = get_sample_for_visualization(data_valid, preprocess_fn, H.num_images_visualize, H.dataset) + early_evals = set([1] + [2 ** exp for exp in range(3, 14)]) + stats = [] + iters_since_starting = 0 + H.ema_rate = torch.as_tensor(H.ema_rate).cuda() + for epoch in range(starting_epoch, H.num_epochs): + train_sampler.set_epoch(epoch) + for x in DataLoader(data_train, batch_size=H.n_batch, drop_last=True, pin_memory=True, sampler=train_sampler): + data_input, target = preprocess_fn(x) + training_stats = training_step(H, data_input, target, vae, ema_vae, optimizer, iterate) + stats.append(training_stats) + scheduler.step() + if iterate % H.iters_per_print == 0 or iters_since_starting in early_evals: + logprint(model=H.desc, type='train_loss', lr=scheduler.get_last_lr()[0], epoch=epoch, step=iterate, **accumulate_stats(stats, H.iters_per_print)) + + if iterate % H.iters_per_images == 0 or (iters_since_starting in early_evals and H.dataset != 'ffhq_1024') and H.rank == 0: + write_images(H, ema_vae, viz_batch_original, viz_batch_processed, f'{H.save_dir}/samples-{iterate}.png', logprint) + + iterate += 1 + iters_since_starting += 1 + if iterate % H.iters_per_save == 0 and H.rank == 0: + if np.isfinite(stats[-1]['elbo']): + logprint(model=H.desc, type='train_loss', epoch=epoch, step=iterate, **accumulate_stats(stats, H.iters_per_print)) + fp = os.path.join(H.save_dir, 'latest') + logprint(f'Saving model@ {iterate} to {fp}') + save_model(fp, vae, ema_vae, optimizer, H) + + if iterate % H.iters_per_ckpt == 0 and H.rank == 0: + save_model(os.path.join(H.save_dir, f'iter-{iterate}'), vae, ema_vae, optimizer, H) + + if epoch % H.epochs_per_eval == 0: + valid_stats = evaluate(H, ema_vae, data_valid, preprocess_fn) + logprint(model=H.desc, type='eval_loss', epoch=epoch, step=iterate, **valid_stats) + + +def evaluate(H, ema_vae, data_valid, preprocess_fn): + stats_valid = [] + valid_sampler = DistributedSampler(data_valid, num_replicas=H.mpi_size, rank=H.rank) + for x in DataLoader(data_valid, batch_size=H.n_batch, drop_last=True, pin_memory=True, sampler=valid_sampler): + data_input, target = preprocess_fn(x) + stats_valid.append(eval_step(data_input, target, ema_vae)) + vals = [a['elbo'] for a in stats_valid] + finites = np.array(vals)[np.isfinite(vals)] + stats = dict(n_batches=len(vals), filtered_elbo=np.mean(finites), **{k: np.mean([a[k] for a in stats_valid]) for k in stats_valid[-1]}) + return stats + + +def write_images(H, ema_vae, viz_batch_original, viz_batch_processed, fname, logprint): + zs = [s['z'].cuda() for s in ema_vae.forward_get_latents(viz_batch_processed)] + batches = [viz_batch_original.numpy()] + mb = viz_batch_processed.shape[0] + lv_points = np.floor(np.linspace(0, 1, H.num_variables_visualize + 2) * len(zs)).astype(int)[1:-1] + for i in lv_points: + batches.append(ema_vae.forward_samples_set_latents(mb, zs[:i], t=0.1)) + for t in [1.0, 0.9, 0.8, 0.7][:H.num_temperatures_visualize]: + batches.append(ema_vae.forward_uncond_samples(mb, t=t)) + n_rows = len(batches) + im = np.concatenate(batches, axis=0).reshape((n_rows, mb, *viz_batch_processed.shape[1:])).transpose([0, 2, 1, 3, 4]).reshape([n_rows * viz_batch_processed.shape[1], mb * viz_batch_processed.shape[2], 3]) + logprint(f'printing samples to {fname}') + imageio.imwrite(fname, im) + + +def run_test_eval(H, ema_vae, data_test, preprocess_fn, logprint): + print('evaluating') + stats = evaluate(H, ema_vae, data_test, preprocess_fn) + print('test results') + for k in stats: + print(k, stats[k]) + logprint(type='test_loss', **stats) + + +def main(): + H, logprint = set_up_hyperparams() + H, data_train, data_valid_or_test, preprocess_fn = set_up_data(H) + vae, ema_vae = load_vaes(H, logprint) + if H.test_eval: + run_test_eval(H, ema_vae, data_valid_or_test, preprocess_fn, logprint) + else: + train_loop(H, data_train, data_valid_or_test, preprocess_fn, vae, ema_vae, logprint) + + +if __name__ == "__main__": + main() diff --git a/MindEye_Imagery/src/vdvae/train_helpers.py b/MindEye_Imagery/src/vdvae/train_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..14b31d3ecaa77372c1d1cd4515cb378529abd3a2 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/train_helpers.py @@ -0,0 +1,190 @@ +import torch +import numpy as np +#from mpi4py import MPI +import socket +import argparse +import os +import json +import subprocess +from hps import Hyperparams, parse_args_and_update_hparams, add_vae_arguments +from vdvae_utils import (logger, + local_mpi_rank, + mpi_size, + maybe_download, + mpi_rank) +from data import mkdir_p +from contextlib import contextmanager +import torch.distributed as dist +#from apex.optimizers import FusedAdam as AdamW +from vae import VAE +from torch.nn.parallel.distributed import DistributedDataParallel + + +def update_ema(vae, ema_vae, ema_rate): + for p1, p2 in zip(vae.parameters(), ema_vae.parameters()): + p2.data.mul_(ema_rate) + p2.data.add_(p1.data * (1 - ema_rate)) + + +def save_model(path, vae, ema_vae, optimizer, H): + torch.save(vae.state_dict(), f'{path}-model.th') + torch.save(ema_vae.state_dict(), f'{path}-model-ema.th') + torch.save(optimizer.state_dict(), f'{path}-opt.th') + from_log = os.path.join(H.save_dir, 'log.jsonl') + to_log = f'{os.path.dirname(path)}/{os.path.basename(path)}-log.jsonl' + subprocess.check_output(['cp', from_log, to_log]) + + +def accumulate_stats(stats, frequency): + z = {} + for k in stats[-1]: + if k in ['distortion_nans', 'rate_nans', 'skipped_updates', 'gcskip']: + z[k] = np.sum([a[k] for a in stats[-frequency:]]) + elif k == 'grad_norm': + vals = [a[k] for a in stats[-frequency:]] + finites = np.array(vals)[np.isfinite(vals)] + if len(finites) == 0: + z[k] = 0.0 + else: + z[k] = np.max(finites) + elif k == 'elbo': + vals = [a[k] for a in stats[-frequency:]] + finites = np.array(vals)[np.isfinite(vals)] + z['elbo'] = np.mean(vals) + z['elbo_filtered'] = np.mean(finites) + elif k == 'iter_time': + z[k] = stats[-1][k] if len(stats) < frequency else np.mean([a[k] for a in stats[-frequency:]]) + else: + z[k] = np.mean([a[k] for a in stats[-frequency:]]) + return z + + +def linear_warmup(warmup_iters): + def f(iteration): + return 1.0 if iteration > warmup_iters else iteration / warmup_iters + return f + + +def setup_mpi(H): + H.mpi_size = mpi_size() + H.local_rank = local_mpi_rank() + H.rank = mpi_rank() + os.environ["RANK"] = str(H.rank) + os.environ["WORLD_SIZE"] = str(H.mpi_size) + os.environ["MASTER_PORT"] = str(H.port) + # os.environ["NCCL_LL_THRESHOLD"] = "0" + os.environ["MASTER_ADDR"] = MPI.COMM_WORLD.bcast(socket.gethostname(), root=0) + torch.cuda.set_device(H.local_rank) + dist.init_process_group(backend='nccl', init_method=f"env://") + + +def distributed_maybe_download(path, local_rank, mpi_size): + if not path.startswith('gs://'): + return path + filename = path[5:].replace('/', '-') + with first_rank_first(local_rank, mpi_size): + fp = maybe_download(path, filename) + return fp + + +@contextmanager +def first_rank_first(local_rank, mpi_size): + if mpi_size > 1 and local_rank > 0: + dist.barrier() + + try: + yield + finally: + if mpi_size > 1 and local_rank == 0: + dist.barrier() + + +def setup_save_dirs(H): + H.save_dir = os.path.join(H.save_dir, H.desc) + mkdir_p(H.save_dir) + H.logdir = os.path.join(H.save_dir, 'log') + + +def set_up_hyperparams(s=None): + H = Hyperparams() + parser = argparse.ArgumentParser() + parser = add_vae_arguments(parser) + parse_args_and_update_hparams(H, parser, s=s) + setup_mpi(H) + setup_save_dirs(H) + logprint = logger(H.logdir) + for i, k in enumerate(sorted(H)): + logprint(type='hparam', key=k, value=H[k]) + np.random.seed(H.seed) + torch.manual_seed(H.seed) + torch.cuda.manual_seed(H.seed) + logprint('training model', H.desc, 'on', H.dataset) + return H, logprint + + +def restore_params(model, path, local_rank, mpi_size, map_ddp=True, map_cpu=False): + state_dict = torch.load(distributed_maybe_download(path, local_rank, mpi_size), map_location='cpu' if map_cpu else None) + if map_ddp: + new_state_dict = {} + l = len('module.') + for k in state_dict: + if k.startswith('module.'): + new_state_dict[k[l:]] = state_dict[k] + else: + new_state_dict[k] = state_dict[k] + state_dict = new_state_dict + model.load_state_dict(state_dict) + + +def restore_log(path, local_rank, mpi_size): + loaded = [json.loads(l) for l in open(distributed_maybe_download(path, local_rank, mpi_size))] + try: + cur_eval_loss = min([z['elbo'] for z in loaded if 'type' in z and z['type'] == 'eval_loss']) + except ValueError: + cur_eval_loss = float('inf') + starting_epoch = max([z['epoch'] for z in loaded if 'type' in z and z['type'] == 'train_loss']) + iterate = max([z['step'] for z in loaded if 'type' in z and z['type'] == 'train_loss']) + return cur_eval_loss, iterate, starting_epoch + + +def load_vaes(H, logprint): + vae = VAE(H) + if H.restore_path: + logprint(f'Restoring vae from {H.restore_path}') + restore_params(vae, H.restore_path, map_cpu=True, local_rank=H.local_rank, mpi_size=H.mpi_size) + + ema_vae = VAE(H) + if H.restore_ema_path: + logprint(f'Restoring ema vae from {H.restore_ema_path}') + restore_params(ema_vae, H.restore_ema_path, map_cpu=True, local_rank=H.local_rank, mpi_size=H.mpi_size) + else: + ema_vae.load_state_dict(vae.state_dict()) + ema_vae.requires_grad_(False) + + vae = vae.cuda(H.local_rank) + ema_vae = ema_vae.cuda(H.local_rank) + + vae = DistributedDataParallel(vae, device_ids=[H.local_rank], output_device=H.local_rank) + + if len(list(vae.named_parameters())) != len(list(vae.parameters())): + raise ValueError('Some params are not named. Please name all params.') + total_params = 0 + for name, p in vae.named_parameters(): + total_params += np.prod(p.shape) + logprint(total_params=total_params, readable=f'{total_params:,}') + return vae, ema_vae + + +def load_opt(H, vae, logprint): + optimizer = AdamW(vae.parameters(), weight_decay=H.wd, lr=H.lr, betas=(H.adam_beta1, H.adam_beta2)) + scheduler = torch.optim.lr_scheduler.LambdaLR(optimizer, lr_lambda=linear_warmup(H.warmup_iters)) + + if H.restore_optimizer_path: + optimizer.load_state_dict( + torch.load(distributed_maybe_download(H.restore_optimizer_path, H.local_rank, H.mpi_size), map_location='cpu')) + if H.restore_log_path: + cur_eval_loss, iterate, starting_epoch = restore_log(H.restore_log_path, H.local_rank, H.mpi_size) + else: + cur_eval_loss, iterate, starting_epoch = float('inf'), 0, 0 + logprint('starting at epoch', starting_epoch, 'iterate', iterate, 'eval loss', cur_eval_loss) + return optimizer, scheduler, cur_eval_loss, iterate, starting_epoch diff --git a/MindEye_Imagery/src/vdvae/vae.py b/MindEye_Imagery/src/vdvae/vae.py new file mode 100644 index 0000000000000000000000000000000000000000..97b08874d1c436fc9fd7a7c3b32ae44cf4656330 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/vae.py @@ -0,0 +1,252 @@ +import torch +from torch import nn +from torch.nn import functional as F +from vae_helpers import HModule, get_1x1, get_3x3, DmolNet, draw_gaussian_diag_samples, gaussian_analytical_kl +from collections import defaultdict +import numpy as np +import itertools + + +class Block(nn.Module): + def __init__(self, in_width, middle_width, out_width, down_rate=None, residual=False, use_3x3=True, zero_last=False): + super().__init__() + self.down_rate = down_rate + self.residual = residual + self.c1 = get_1x1(in_width, middle_width) + self.c2 = get_3x3(middle_width, middle_width) if use_3x3 else get_1x1(middle_width, middle_width) + self.c3 = get_3x3(middle_width, middle_width) if use_3x3 else get_1x1(middle_width, middle_width) + self.c4 = get_1x1(middle_width, out_width, zero_weights=zero_last) + + def forward(self, x): + xhat = self.c1(F.gelu(x)) + xhat = self.c2(F.gelu(xhat)) + xhat = self.c3(F.gelu(xhat)) + xhat = self.c4(F.gelu(xhat)) + out = x + xhat if self.residual else xhat + if self.down_rate is not None: + out = F.avg_pool2d(out, kernel_size=self.down_rate, stride=self.down_rate) + return out + + +def parse_layer_string(s): + layers = [] + for ss in s.split(','): + if 'x' in ss: + res, num = ss.split('x') + count = int(num) + layers += [(int(res), None) for _ in range(count)] + elif 'm' in ss: + res, mixin = [int(a) for a in ss.split('m')] + layers.append((res, mixin)) + elif 'd' in ss: + res, down_rate = [int(a) for a in ss.split('d')] + layers.append((res, down_rate)) + else: + res = int(ss) + layers.append((res, None)) + return layers + + +def pad_channels(t, width): + d1, d2, d3, d4 = t.shape + empty = torch.zeros(d1, width, d3, d4, device=t.device) + empty[:, :d2, :, :] = t + return empty + + +def get_width_settings(width, s): + mapping = defaultdict(lambda: width) + if s: + s = s.split(',') + for ss in s: + k, v = ss.split(':') + mapping[int(k)] = int(v) + return mapping + + +class Encoder(HModule): + def build(self): + H = self.H + self.in_conv = get_3x3(H.image_channels, H.width) + self.widths = get_width_settings(H.width, H.custom_width_str) + enc_blocks = [] + blockstr = parse_layer_string(H.enc_blocks) + for res, down_rate in blockstr: + use_3x3 = res > 2 # Don't use 3x3s for 1x1, 2x2 patches + enc_blocks.append(Block(self.widths[res], int(self.widths[res] * H.bottleneck_multiple), self.widths[res], down_rate=down_rate, residual=True, use_3x3=use_3x3)) + n_blocks = len(blockstr) + for b in enc_blocks: + b.c4.weight.data *= np.sqrt(1 / n_blocks) + self.enc_blocks = nn.ModuleList(enc_blocks) + + def forward(self, x): + x = x.permute(0, 3, 1, 2).contiguous() + x = self.in_conv(x) + activations = {} + activations[x.shape[2]] = x + for block in self.enc_blocks: + x = block(x) + res = x.shape[2] + x = x if x.shape[1] == self.widths[res] else pad_channels(x, self.widths[res]) + activations[res] = x + return activations + + +class DecBlock(nn.Module): + def __init__(self, H, res, mixin, n_blocks): + super().__init__() + self.base = res + self.mixin = mixin + self.H = H + self.widths = get_width_settings(H.width, H.custom_width_str) + width = self.widths[res] + use_3x3 = res > 2 + cond_width = int(width * H.bottleneck_multiple) + self.zdim = H.zdim + self.enc = Block(width * 2, cond_width, H.zdim * 2, residual=False, use_3x3=use_3x3) + self.prior = Block(width, cond_width, H.zdim * 2 + width, residual=False, use_3x3=use_3x3, zero_last=True) + self.z_proj = get_1x1(H.zdim, width) + self.z_proj.weight.data *= np.sqrt(1 / n_blocks) + self.resnet = Block(width, cond_width, width, residual=True, use_3x3=use_3x3) + self.resnet.c4.weight.data *= np.sqrt(1 / n_blocks) + self.z_fn = lambda x: self.z_proj(x) + + def sample(self, x, acts): + qm, qv = self.enc(torch.cat([x, acts], dim=1)).chunk(2, dim=1) + feats = self.prior(x) + pm, pv, xpp = feats[:, :self.zdim, ...], feats[:, self.zdim:self.zdim * 2, ...], feats[:, self.zdim * 2:, ...] + x = x + xpp + z = draw_gaussian_diag_samples(qm, qv) + kl = gaussian_analytical_kl(qm, pm, qv, pv) + return z, x, kl + + def sample_uncond(self, x, t=None, lvs=None): + n, c, h, w = x.shape + feats = self.prior(x) + pm, pv, xpp = feats[:, :self.zdim, ...], feats[:, self.zdim:self.zdim * 2, ...], feats[:, self.zdim * 2:, ...] + x = x + xpp + if lvs is not None: + z = lvs + else: + if t is not None: + pv = pv + torch.ones_like(pv) * np.log(t) + z = draw_gaussian_diag_samples(pm, pv) + return z, x + + def get_inputs(self, xs, activations): + acts = activations[self.base] + try: + x = xs[self.base] + except KeyError: + x = torch.zeros_like(acts) + if acts.shape[0] != x.shape[0]: + x = x.repeat(acts.shape[0], 1, 1, 1) + return x, acts + + def forward(self, xs, activations, get_latents=False): + x, acts = self.get_inputs(xs, activations) + if self.mixin is not None: + x = x + F.interpolate(xs[self.mixin][:, :x.shape[1], ...], scale_factor=self.base // self.mixin) + z, x, kl = self.sample(x, acts) + x = x + self.z_fn(z) + x = self.resnet(x) + xs[self.base] = x + if get_latents: + return xs, dict(z=z.detach(), kl=kl) + return xs, dict(kl=kl) + + def forward_uncond(self, xs, t=None, lvs=None): + try: + x = xs[self.base] + except KeyError: + ref = xs[list(xs.keys())[0]] + x = torch.zeros(dtype=ref.dtype, size=(ref.shape[0], self.widths[self.base], self.base, self.base), device=ref.device) + if self.mixin is not None: + x = x + F.interpolate(xs[self.mixin][:, :x.shape[1], ...], scale_factor=self.base // self.mixin) + z, x = self.sample_uncond(x, t, lvs=lvs) + x = x + self.z_fn(z) + x = self.resnet(x) + xs[self.base] = x + return xs + + +class Decoder(HModule): + + def build(self): + H = self.H + resos = set() + dec_blocks = [] + self.widths = get_width_settings(H.width, H.custom_width_str) + blocks = parse_layer_string(H.dec_blocks) + for idx, (res, mixin) in enumerate(blocks): + dec_blocks.append(DecBlock(H, res, mixin, n_blocks=len(blocks))) + resos.add(res) + self.resolutions = sorted(resos) + self.dec_blocks = nn.ModuleList(dec_blocks) + self.bias_xs = nn.ParameterList([nn.Parameter(torch.zeros(1, self.widths[res], res, res)) for res in self.resolutions if res <= H.no_bias_above]) + self.out_net = DmolNet(H) + self.gain = nn.Parameter(torch.ones(1, H.width, 1, 1)) + self.bias = nn.Parameter(torch.zeros(1, H.width, 1, 1)) + self.final_fn = lambda x: x * self.gain + self.bias + + def forward(self, activations, get_latents=False): + stats = [] + xs = {a.shape[2]: a for a in self.bias_xs} + for block in self.dec_blocks: + xs, block_stats = block(xs, activations, get_latents=get_latents) + stats.append(block_stats) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size], stats + + def forward_uncond(self, n, t=None, y=None): + xs = {} + for bias in self.bias_xs: + xs[bias.shape[2]] = bias.repeat(n, 1, 1, 1) + for idx, block in enumerate(self.dec_blocks): + try: + temp = t[idx] + except TypeError: + temp = t + xs = block.forward_uncond(xs, temp) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size] + + def forward_manual_latents(self, n, latents, t=None): + xs = {} + for bias in self.bias_xs: + xs[bias.shape[2]] = bias.repeat(n, 1, 1, 1) + for block, lvs in itertools.zip_longest(self.dec_blocks, latents): + xs = block.forward_uncond(xs, t, lvs=lvs) + xs[self.H.image_size] = self.final_fn(xs[self.H.image_size]) + return xs[self.H.image_size] + + +class VAE(HModule): + def build(self): + self.encoder = Encoder(self.H) + self.decoder = Decoder(self.H) + + def forward(self, x, x_target): + activations = self.encoder.forward(x) + px_z, stats = self.decoder.forward(activations) + distortion_per_pixel = self.decoder.out_net.nll(px_z, x_target) + rate_per_pixel = torch.zeros_like(distortion_per_pixel) + ndims = np.prod(x.shape[1:]) + for statdict in stats: + rate_per_pixel += statdict['kl'].sum(dim=(1, 2, 3)) + rate_per_pixel /= ndims + elbo = (distortion_per_pixel + rate_per_pixel).mean() + return dict(elbo=elbo, distortion=distortion_per_pixel.mean(), rate=rate_per_pixel.mean()) + + def forward_get_latents(self, x): + activations = self.encoder.forward(x) + _, stats = self.decoder.forward(activations, get_latents=True) + return stats + + def forward_uncond_samples(self, n_batch, t=None): + px_z = self.decoder.forward_uncond(n_batch, t=t) + return self.decoder.out_net.sample(px_z) + + def forward_samples_set_latents(self, n_batch, latents, t=None): + px_z = self.decoder.forward_manual_latents(n_batch, latents, t=t) + return self.decoder.out_net.sample(px_z) diff --git a/MindEye_Imagery/src/vdvae/vae_helpers.py b/MindEye_Imagery/src/vdvae/vae_helpers.py new file mode 100644 index 0000000000000000000000000000000000000000..3010be5ad0ef84a74441da3b0fe3c1eb46eeddbc --- /dev/null +++ b/MindEye_Imagery/src/vdvae/vae_helpers.py @@ -0,0 +1,165 @@ +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F + + +@torch.jit.script +def gaussian_analytical_kl(mu1, mu2, logsigma1, logsigma2): + return -0.5 + logsigma2 - logsigma1 + 0.5 * (logsigma1.exp() ** 2 + (mu1 - mu2) ** 2) / (logsigma2.exp() ** 2) + + +@torch.jit.script +def draw_gaussian_diag_samples(mu, logsigma): + eps = torch.empty_like(mu).normal_(0., 1.) + return torch.exp(logsigma) * eps + mu + + +def get_conv(in_dim, out_dim, kernel_size, stride, padding, zero_bias=True, zero_weights=False, groups=1, scaled=False): + c = nn.Conv2d(in_dim, out_dim, kernel_size, stride, padding, groups=groups) + if zero_bias: + c.bias.data *= 0.0 + if zero_weights: + c.weight.data *= 0.0 + return c + + +def get_3x3(in_dim, out_dim, zero_bias=True, zero_weights=False, groups=1, scaled=False): + return get_conv(in_dim, out_dim, 3, 1, 1, zero_bias, zero_weights, groups=groups, scaled=scaled) + + +def get_1x1(in_dim, out_dim, zero_bias=True, zero_weights=False, groups=1, scaled=False): + return get_conv(in_dim, out_dim, 1, 1, 0, zero_bias, zero_weights, groups=groups, scaled=scaled) + + +def log_prob_from_logits(x): + """ numerically stable log_softmax implementation that prevents overflow """ + axis = len(x.shape) - 1 + m = x.max(dim=axis, keepdim=True)[0] + return x - m - torch.log(torch.exp(x - m).sum(dim=axis, keepdim=True)) + + +def const_max(t, constant): + other = torch.ones_like(t) * constant + return torch.max(t, other) + + +def const_min(t, constant): + other = torch.ones_like(t) * constant + return torch.min(t, other) + + +def discretized_mix_logistic_loss(x, l, low_bit=False): + """ log-likelihood for mixture of discretized logistics, assumes the data has been rescaled to [-1,1] interval """ + # Adapted from https://github.com/openai/pixel-cnn/blob/master/pixel_cnn_pp/nn.py + xs = [s for s in x.shape] # true image (i.e. labels) to regress to, e.g. (B,32,32,3) + ls = [s for s in l.shape] # predicted distribution, e.g. (B,32,32,100) + nr_mix = int(ls[-1] / 10) # here and below: unpacking the params of the mixture of logistics + logit_probs = l[:, :, :, :nr_mix] + l = torch.reshape(l[:, :, :, nr_mix:], xs + [nr_mix * 3]) + means = l[:, :, :, :, :nr_mix] + log_scales = const_max(l[:, :, :, :, nr_mix:2 * nr_mix], -7.) + coeffs = torch.tanh(l[:, :, :, :, 2 * nr_mix:3 * nr_mix]) + x = torch.reshape(x, xs + [1]) + torch.zeros(xs + [nr_mix]).to(x.device) # here and below: getting the means and adjusting them based on preceding sub-pixels + m2 = torch.reshape(means[:, :, :, 1, :] + coeffs[:, :, :, 0, :] * x[:, :, :, 0, :], [xs[0], xs[1], xs[2], 1, nr_mix]) + m3 = torch.reshape(means[:, :, :, 2, :] + coeffs[:, :, :, 1, :] * x[:, :, :, 0, :] + coeffs[:, :, :, 2, :] * x[:, :, :, 1, :], [xs[0], xs[1], xs[2], 1, nr_mix]) + means = torch.cat([torch.reshape(means[:, :, :, 0, :], [xs[0], xs[1], xs[2], 1, nr_mix]), m2, m3], dim=3) + centered_x = x - means + inv_stdv = torch.exp(-log_scales) + if low_bit: + plus_in = inv_stdv * (centered_x + 1. / 31.) + cdf_plus = torch.sigmoid(plus_in) + min_in = inv_stdv * (centered_x - 1. / 31.) + else: + plus_in = inv_stdv * (centered_x + 1. / 255.) + cdf_plus = torch.sigmoid(plus_in) + min_in = inv_stdv * (centered_x - 1. / 255.) + cdf_min = torch.sigmoid(min_in) + log_cdf_plus = plus_in - F.softplus(plus_in) # log probability for edge case of 0 (before scaling) + log_one_minus_cdf_min = -F.softplus(min_in) # log probability for edge case of 255 (before scaling) + cdf_delta = cdf_plus - cdf_min # probability for all other cases + mid_in = inv_stdv * centered_x + log_pdf_mid = mid_in - log_scales - 2. * F.softplus(mid_in) # log probability in the center of the bin, to be used in extreme cases (not actually used in our code) + + # now select the right output: left edge case, right edge case, normal case, extremely low prob case (doesn't actually happen for us) + + # this is what we are really doing, but using the robust version below for extreme cases in other applications and to avoid NaN issue with tf.select() + # log_probs = tf.select(x < -0.999, log_cdf_plus, tf.select(x > 0.999, log_one_minus_cdf_min, tf.log(cdf_delta))) + + # robust version, that still works if probabilities are below 1e-5 (which never happens in our code) + # tensorflow backpropagates through tf.select() by multiplying with zero instead of selecting: this requires use to use some ugly tricks to avoid potential NaNs + # the 1e-12 in tf.maximum(cdf_delta, 1e-12) is never actually used as output, it's purely there to get around the tf.select() gradient issue + # if the probability on a sub-pixel is below 1e-5, we use an approximation based on the assumption that the log-density is constant in the bin of the observed sub-pixel value + if low_bit: + log_probs = torch.where(x < -0.999, + log_cdf_plus, + torch.where(x > 0.999, + log_one_minus_cdf_min, + torch.where(cdf_delta > 1e-5, + torch.log(const_max(cdf_delta, 1e-12)), + log_pdf_mid - np.log(15.5)))) + else: + log_probs = torch.where(x < -0.999, + log_cdf_plus, + torch.where(x > 0.999, + log_one_minus_cdf_min, + torch.where(cdf_delta > 1e-5, + torch.log(const_max(cdf_delta, 1e-12)), + log_pdf_mid - np.log(127.5)))) + log_probs = log_probs.sum(dim=3) + log_prob_from_logits(logit_probs) + mixture_probs = torch.logsumexp(log_probs, -1) + return -1. * mixture_probs.sum(dim=[1, 2]) / np.prod(xs[1:]) + + +def sample_from_discretized_mix_logistic(l, nr_mix): + ls = [s for s in l.shape] + xs = ls[:-1] + [3] + # unpack parameters + logit_probs = l[:, :, :, :nr_mix] + l = torch.reshape(l[:, :, :, nr_mix:], xs + [nr_mix * 3]) + # sample mixture indicator from softmax + eps = torch.empty(logit_probs.shape, device=l.device).uniform_(1e-5, 1. - 1e-5) + amax = torch.argmax(logit_probs - torch.log(-torch.log(eps)), dim=3) + sel = F.one_hot(amax, num_classes=nr_mix).float() + sel = torch.reshape(sel, xs[:-1] + [1, nr_mix]) + # select logistic parameters + means = (l[:, :, :, :, :nr_mix] * sel).sum(dim=4) + log_scales = const_max((l[:, :, :, :, nr_mix:nr_mix * 2] * sel).sum(dim=4), -7.) + coeffs = (torch.tanh(l[:, :, :, :, nr_mix * 2:nr_mix * 3]) * sel).sum(dim=4) + # sample from logistic & clip to interval + # we don't actually round to the nearest 8bit value when sampling + u = torch.empty(means.shape, device=means.device).uniform_(1e-5, 1. - 1e-5) + x = means + torch.exp(log_scales) * (torch.log(u) - torch.log(1. - u)) + x0 = const_min(const_max(x[:, :, :, 0], -1.), 1.) + x1 = const_min(const_max(x[:, :, :, 1] + coeffs[:, :, :, 0] * x0, -1.), 1.) + x2 = const_min(const_max(x[:, :, :, 2] + coeffs[:, :, :, 1] * x0 + coeffs[:, :, :, 2] * x1, -1.), 1.) + return torch.cat([torch.reshape(x0, xs[:-1] + [1]), torch.reshape(x1, xs[:-1] + [1]), torch.reshape(x2, xs[:-1] + [1])], dim=3) + + +class HModule(nn.Module): + def __init__(self, H): + super().__init__() + self.H = H + self.build() + + +class DmolNet(nn.Module): + def __init__(self, H): + super().__init__() + self.H = H + self.width = H.width + self.out_conv = get_conv(H.width, H.num_mixtures * 10, kernel_size=1, stride=1, padding=0) + + def nll(self, px_z, x): + return discretized_mix_logistic_loss(x=x, l=self.forward(px_z), low_bit=self.H.dataset in ['ffhq_256']) + + def forward(self, px_z): + xhat = self.out_conv(px_z) + return xhat.permute(0, 2, 3, 1) + + def sample(self, px_z): + im = sample_from_discretized_mix_logistic(self.forward(px_z), self.H.num_mixtures) + xhat = (im + 1.0) * 127.5 + xhat = xhat.detach().cpu().numpy() + xhat = np.minimum(np.maximum(0.0, xhat), 255.0).astype(np.uint8) + return xhat \ No newline at end of file diff --git a/MindEye_Imagery/src/vdvae/vdvae_utils.py b/MindEye_Imagery/src/vdvae/vdvae_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..ae1544b8c817ccade236f082305065bdae538e62 --- /dev/null +++ b/MindEye_Imagery/src/vdvae/vdvae_utils.py @@ -0,0 +1,140 @@ +#from mpi4py import MPI +import os +import json +import tempfile +import numpy as np +import torch +import time +import subprocess +import torch.distributed as dist + + +def allreduce(x, average): + if mpi_size() > 1: + dist.all_reduce(x, dist.ReduceOp.SUM) + return x / mpi_size() if average else x + + +def get_cpu_stats_over_ranks(stat_dict): + keys = sorted(stat_dict.keys()) + allreduced = allreduce(torch.stack([torch.as_tensor(stat_dict[k]).detach().cuda().float() for k in keys]), average=True).cpu() + return {k: allreduced[i].item() for (i, k) in enumerate(keys)} + + +class Hyperparams(dict): + def __getattr__(self, attr): + try: + return self[attr] + except KeyError: + return None + + def __setattr__(self, attr, value): + self[attr] = value + + +def logger(log_prefix): + 'Prints the arguments out to stdout, .txt, and .jsonl files' + + jsonl_path = f'{log_prefix}.jsonl' + txt_path = f'{log_prefix}.txt' + + def log(*args, pprint=False, **kwargs): + if mpi_rank() != 0: + return + t = time.ctime() + argdict = {'time': t} + if len(args) > 0: + argdict['message'] = ' '.join([str(x) for x in args]) + argdict.update(kwargs) + + txt_str = [] + args_iter = sorted(argdict) if pprint else argdict + for k in args_iter: + val = argdict[k] + if isinstance(val, np.ndarray): + val = val.tolist() + elif isinstance(val, np.integer): + val = int(val) + elif isinstance(val, np.floating): + val = float(val) + argdict[k] = val + if isinstance(val, float): + val = f'{val:.5f}' + txt_str.append(f'{k}: {val}') + txt_str = ', '.join(txt_str) + + if pprint: + json_str = json.dumps(argdict, sort_keys=True) + txt_str = json.dumps(argdict, sort_keys=True, indent=4) + else: + json_str = json.dumps(argdict) + + print(txt_str, flush=True) + + with open(txt_path, "a+") as f: + print(txt_str, file=f, flush=True) + with open(jsonl_path, "a+") as f: + print(json_str, file=f, flush=True) + + return log + + +def maybe_download(path, filename=None): + '''If a path is a gsutil path, download it and return the local link, + otherwise return link''' + if not path.startswith('gs://'): + return path + if filename: + local_dest = f'/tmp/' + out_path = f'/tmp/{filename}' + if os.path.isfile(out_path): + return out_path + subprocess.check_output(['gsutil', '-m', 'cp', '-R', path, out_path]) + return out_path + else: + local_dest = tempfile.mkstemp()[1] + subprocess.check_output(['gsutil', '-m', 'cp', path, local_dest]) + return local_dest + + +def tile_images(images, d1=4, d2=4, border=1): + id1, id2, c = images[0].shape + out = np.ones([d1 * id1 + border * (d1 + 1), + d2 * id2 + border * (d2 + 1), + c], dtype=np.uint8) + out *= 255 + if len(images) != d1 * d2: + raise ValueError('Wrong num of images') + for imgnum, im in enumerate(images): + num_d1 = imgnum // d2 + num_d2 = imgnum % d2 + start_d1 = num_d1 * id1 + border * (num_d1 + 1) + start_d2 = num_d2 * id2 + border * (num_d2 + 1) + out[start_d1:start_d1 + id1, start_d2:start_d2 + id2, :] = im + return out + + +def mpi_size(): + return MPI.COMM_WORLD.Get_size() + + +def mpi_rank(): + return MPI.COMM_WORLD.Get_rank() + + +def num_nodes(): + nn = mpi_size() + if nn % 8 == 0: + return nn // 8 + return nn // 8 + 1 + + +def gpus_per_node(): + size = mpi_size() + if size > 1: + return max(size // num_nodes(), 1) + return 1 + + +def local_mpi_rank(): + return mpi_rank() % gpus_per_node() diff --git a/spurious_reconstruction/.gitignore b/spurious_reconstruction/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1cb45c5dbe98ab97abb2e9d777792c13b238c7c0 --- /dev/null +++ b/spurious_reconstruction/.gitignore @@ -0,0 +1,13 @@ +# python generated files +__pycache__/ +*.py[oc] +build/ +dist/ +wheels/ +*.egg-info + +# venv +.venv +./results +./analysis/my_playground +./data diff --git a/spurious_reconstruction/README.md b/spurious_reconstruction/README.md new file mode 100644 index 0000000000000000000000000000000000000000..8605a6c4e307d4800a4cd6df8d2d537c85c63769 --- /dev/null +++ b/spurious_reconstruction/README.md @@ -0,0 +1,58 @@ +# Spurious-reconstruction + +This repository contains demo code for the paper: +Ken Shirakawa, Yoshihiro Nagano, Misato Tanaka, Shuntaro C. Aoki, Yusuke Muraki, Kei Majima, and Yukiyasu Kamitani, "Spurious reconstruction from brain activity" ([arXiv](https://arxiv.org/abs/2405.10078)). + +## Getting Started + +### Installation +To clone this repository on your local machine, use the following `git clone` command with the project URL: + + +``` +git clone https://github.com/KamitaniLab/spurious_reconstruction.git +``` +### Environment Setup +The environment for this project was created using [Rye](https://rye.astral.sh/). To set it up, follow the official instructions to install Rye. After installation, you can synchronize the environment for this repository using: + +``` +cd spurious_reconstruction +rye sync +``` + +## Usage + +--- + +We are currently preparing to share the preprocessed data and files. These files will be made available on Figshare at [this URL](https://figshare.com/articles/dataset/Spurious_reconstruction_from_brain_activity/27013342). + +Reproducing the full analysis requires a large amount of data (approximately 3TB) and some analyses take considerable time to complete (up to ~2 weeks). Therefore, we recommend downloading only the minimum necessary files to reproduce specific analysis, such as image reconstruction, UMAP results or simulation analysis. + +For example, if you want to reproduce the UMAP results (related to Figure 4), you can download the required data by running the following command: + +``` +python ./download.py "UMAP visualization analysis" +``` + +Alternatively, you can directly download the results of UMAP analysis with: +``` +python ./download.py "UMAP visualization results" +``` +Note: At the moment, only the download of **result data** is supported. Please wait a little longer for the availability of **analysis data**. + +Each analysis directory contains additional information on how to reproduce the corresponding results. Please check the README files within each directory for specific instructions. + + +### Original Dataset +We used publicly available datasets for this project. The key datasets are: + +- **NSD (Natural Scenes Dataset)** + - Raw fMRI data (including visual images and text annotations): Available upon request via [Spurious Reconstruction@figshare](https://forms.gle/eT4jHxaWwYUDEf2i9). + +- **Deep Image Reconstruction** + - Raw fMRI data: Available from [OpenNeuro](https://openneuro.org/datasets/ds001506). + - Preprocessed fMRI data, DNN features extracted from images, and decoded DNN features: Available via [Deep Image Reconstruction@figshare](https://github.com/KamitaniLab/DeepImageReconstruction?tab=readme-ov-file#:~:text=Preprocessed%20fMRI%20data,Image%20Reconstruction%40figshare). + - Visual images: Available upon request via [this form](https://forms.gle/ujvA34948Xg49jdn9). + - Text annotations: Available from [GOD Stimuli Annotations](https://github.com/KamitaniLab/GOD_stimuli_annotations). + +If you want to fully reproduce the analysis, you can use these brain data, image stimuli and corresponding text annotations. diff --git a/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/extract_features-checkpoint.ipynb b/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/extract_features-checkpoint.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..0df467caff9a2bc38b8825268e9c8440d1363f98 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/extract_features-checkpoint.ipynb @@ -0,0 +1,437 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5082cdc1-b052-433d-b018-29ba6fa3d313", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 1]\n", + "from glob import glob\n", + "from tqdm.auto import tqdm\n", + "import os\n", + "from PIL import Image\n", + "import numpy as np\n", + "import torch\n", + "from scipy.io import savemat\n", + "from bdpy.dl.torch import FeatureExtractor\n", + "from torchvision import models, transforms\n", + "import yaml\n", + "import h5py" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a31be173-61ff-4adf-a558-63a3efe74cff", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 2]\n", + "# Custom transform class to convert RGB to BGR\n", + "class ConvertRGBtoBGR:\n", + " def __call__(self, image):\n", + " return image[[2, 1, 0], :, :] \n", + " \n", + "# Custom transform class to convert Image format to tensor while keeping pixel ranges\n", + "class ToTensorWithoutScaling:\n", + " def __call__(self, image):\n", + " image = np.array(image).astype(np.float32) # Convert PIL Image to NumPy array\n", + " tensor = torch.from_numpy(image).permute(2, 0, 1).float() # Convert NumPy array to tensor and rearrange dimensions\n", + " return tensor\n", + " \n", + "class Convert32to16:\n", + " def __call__(self, image):\n", + " return image.type(torch.float16)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "id": "969abfe4-e4b6-4fa2-bd09-17f77ddf2292", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 3]\n", + "def load_model(network):\n", + " \n", + " if network == \"VGG19_ILSVRC_19_layers\":\n", + " from bdpy.dl.torch.models import layer_map, model_factory\n", + " model = model_factory('vgg19')\n", + " encoder_param_file = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt'\n", + " model.load_state_dict(torch.load(encoder_param_file))\n", + " model.eval()\n", + " layer_mapping = layer_map('vgg19')\n", + " mean_image = [104., 117., 123.] # BGR\n", + " preprocess = transforms.Compose([\n", + " transforms.Resize((224, 224), interpolation=Image.BICUBIC),\n", + " # ToTensorWithoutScaling(), # Custom transform added\n", + " # ConvertRGBtoBGR(), # Custom transform added\n", + " transforms.Normalize(mean=mean_image, std=[1.,1.,1.]),\n", + " ])\n", + " def preprocess_and_check(img):\n", + " print(\"Original shape:\", img.shape)\n", + " img = transforms.Resize((224, 224), interpolation=Image.BICUBIC)(img)\n", + " print(\"After ToTensor:\", img.shape)\n", + " print(\"After Resize:\", img.shape)\n", + " img = transforms.Normalize(mean=mean_image, std=[1.,1.,1.])(img)\n", + " return img\n", + " \n", + " elif network == \"vgg19_torchvision\":\n", + " model = models.vgg19(pretrained=True)\n", + " model.eval()\n", + " \n", + " preprocess = transforms.Compose([\n", + " transforms.Resize((224, 224)), \n", + " transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n", + " ]) \n", + " def preprocess_and_check(img):\n", + " print(\"Original shape:\", img.shape)\n", + " img = transforms.ToTensor()(img)\n", + " print(\"After ToTensor:\", img.shape)\n", + " img = transforms.Resize(224)(img)\n", + " print(\"After Resize:\", img.shape)\n", + " img = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])(img)\n", + " return img\n", + " else:\n", + " raise ValueError(\"Network not supported. Please choose from: VGG19_ILSVRC_19_layers\"\n", + " +\"or define by yourself. The output should be model and preprocessing functions from RGB image to model input.\")\n", + " \n", + " return model, preprocess\n" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "id": "8020d232-72cd-42ed-bb8e-183c8c0ee9b6", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 4]\n", + "def load_config(config_path):\n", + " '''Load configuration file.'''\n", + " with open(config_path, 'r') as file:\n", + " config = yaml.safe_load(file)\n", + " return config\n" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "a766c128-7e43-419c-9d7e-6f317e44753e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/weka/proj-medarc/shared/mindeyev2_dataset//wds/subj01/train/{0..39}.tar\n", + "/weka/proj-medarc/shared/mindeyev2_dataset//wds/subj01/new_test/0.tar\n", + "Loaded test dl for subj1!\n", + "\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65cb44139d1e4cf788e5a8956d9c1eb1", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "981c8b8963d84fbf9994421e1e8ce02b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import webdataset as wds\n", + "import random\n", + "\n", + "def my_split_by_node(urls): return urls\n", + "data_path = '/weka/proj-medarc/shared/mindeyev2_dataset/'\n", + "subj_list = [1]\n", + "num_sessions = 40\n", + "multi_subject = False\n", + "batch_size = 1\n", + "data_type = torch.float16\n", + "subj = 1\n", + "\n", + "train_data = {}\n", + "train_dl = {}\n", + "num_voxels = {}\n", + "voxels = {}\n", + "\n", + "train_url = f\"{data_path}/wds/subj0{subj}/train/\" + \"{0..\" + f\"{num_sessions-1}\" + \"}.tar\"\n", + "print(train_url)\n", + "\n", + "train_data = wds.WebDataset(train_url,resampled=False,nodesplitter=my_split_by_node)\\\n", + " .decode(\"torch\")\\\n", + " .rename(behav=\"behav.npy\", past_behav=\"past_behav.npy\", future_behav=\"future_behav.npy\", olds_behav=\"olds_behav.npy\")\\\n", + " .to_tuple(*[\"behav\", \"past_behav\", \"future_behav\", \"olds_behav\"])\n", + "train_dl = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=False, drop_last=False, pin_memory=True)\n", + "\n", + "# f = h5py.File(f'{data_path}/betas_all_subj0{s}_fp32_renorm.hdf5', 'r')\n", + "# betas = f['betas'][:]\n", + "# betas = torch.Tensor(betas).to(\"cpu\").to(data_type)\n", + "# num_voxels_list.append(betas[0].shape[-1])\n", + "# num_voxels[f'subj0{s}'] = betas[0].shape[-1]\n", + "# voxels[f'subj0{s}'] = betas\n", + "# print(f\"num_voxels for subj0{s}: {num_voxels[f'subj0{s}']}\")\n", + "\n", + "\n", + "num_test=3000\n", + "test_url = f\"{data_path}/wds/subj0{subj}/new_test/\" + \"0.tar\"\n", + "print(test_url)\n", + "test_data = wds.WebDataset(test_url,resampled=False,nodesplitter=my_split_by_node)\\\n", + " .decode(\"torch\")\\\n", + " .rename(behav=\"behav.npy\", past_behav=\"past_behav.npy\", future_behav=\"future_behav.npy\", olds_behav=\"olds_behav.npy\")\\\n", + " .to_tuple(*[\"behav\", \"past_behav\", \"future_behav\", \"olds_behav\"])\n", + "test_dl = torch.utils.data.DataLoader(test_data, batch_size=1, shuffle=False, drop_last=True, pin_memory=True)\n", + "print(f\"Loaded test dl for subj{subj}!\\n\")\n", + "\n", + "all_indexes_train = []\n", + "\n", + "for behav0, past_behav0, future_behav0, old_behav0 in tqdm(train_dl):\n", + " img_idx = behav0[:,0,0].cpu().long().numpy()[0]\n", + " all_indexes_train.append(img_idx)\n", + "\n", + "all_indexes_test = []\n", + "\n", + "for behav0, past_behav0, future_behav0, old_behav0 in tqdm(test_dl):\n", + " img_idx = behav0[:,0,0].cpu().long().numpy()[0]\n", + " all_indexes_test.append(img_idx)" + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "17a4377a-67dc-49fc-b89c-7d641ef218ce", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_features(config, device='cuda'):\n", + " '''Extract features based on the configuration.'''\n", + " print(\"Extracting features using network:\", config['network'])\n", + " \n", + " f = h5py.File(f'{config[\"image path\"]}/coco_images_224_float16.hdf5', 'r')\n", + " images = f['images']\n", + " print(images.shape)\n", + " model, preprocess = load_model(config['network'])\n", + " layers = config[\"features\"]\n", + " feature_extractor = FeatureExtractor(model, layers, device=device, detach=True)\n", + " \n", + " output_dir = os.path.join(config[\"output base dir\"], \"pytorch\", config['network'])\n", + "\n", + " all_indexes_to_compute = set(all_indexes_train + list(set(all_indexes_test)))\n", + " \n", + " # First, ensure all layer directories exist\n", + " # for layer in layers:\n", + " # layer_output_dir = os.path.join(output_dir, layer)\n", + " # os.makedirs(layer_output_dir, exist_ok=True)\n", + " # img = torch.Tensor(images[0])\n", + "\n", + " # x = preprocess(img).unsqueeze(0).to(device)\n", + " # # Extract features\n", + " # features = feature_extractor.run(x)\n", + " \n", + " # # Get the feature for the current layer\n", + " # f = features.get(layer)\n", + " # print(f.shape)\n", + " \n", + "\n", + " \n", + " for layer in tqdm(layers, desc=\"Processing layers\"): # Iterate over each layer first\n", + " layer_output_dir = os.path.join(output_dir, layer)\n", + "\n", + " with h5py.File(f'{layer_output_dir}/{subj}.h5', 'a') as hdf5_file:\n", + "\n", + " # initial_shape = (0, 100) # Start with 0 rows and 100 columns (or your array shape)\n", + " # max_shape = (None, 100) # Allow unlimited rows\n", + " \n", + " # if 'dataset' not in hdf5_file:\n", + " # dataset = hdf5_file.create_dataset(\n", + " # 'dataset', \n", + " # shape=initial_shape, \n", + " # maxshape=max_shape, \n", + " # dtype='float64'\n", + " # )\n", + " # else:\n", + " # dataset = hdf5_file['dataset']\n", + " for i, image_index in tqdm(enumerate(all_indexes_to_compute), total = len(all_indexes_to_compute)):\n", + " output_file = os.path.join(layer_output_dir, f\"{image_index}.npy\")\n", + " \n", + " if os.path.exists(output_file):\n", + " continue # Skip if the feature for this image and layer already exists\n", + " \n", + " # Load and preprocess the image\n", + " img = torch.Tensor(images[image_index])\n", + " x = preprocess(img).unsqueeze(0).to(device)\n", + " \n", + " # Extract features\n", + " features = feature_extractor.run(x)\n", + " \n", + " # Get the feature for the current layer\n", + " f = features.get(layer)\n", + " if f is None:\n", + " print(f\"Warning: Layer '{layer}' not found in the extracted features.\")\n", + " continue\n", + "\n", + " if i == 0:\n", + " initial_shape = f.shape\n", + " max_shape = (None,) + initial_shape[1:]\n", + "\n", + " if 'dataset' not in hdf5_file:\n", + " dataset = hdf5_file.create_dataset(\n", + " 'dataset', \n", + " shape=initial_shape, \n", + " maxshape=max_shape, \n", + " dtype='float16'\n", + " )\n", + " else:\n", + " dataset = hdf5_file['dataset']\n", + "\n", + " dataset.resize(dataset.shape[0] + f.shape[0], axis=0)\n", + " \n", + " # Append the data\n", + " dataset[-f.shape[0]:] = f.astype(np.float16)\n", + " \n", + " # # Save the feature\n", + " # np.save(output_file, f.astype(np.float16))\n", + " \n", + " # Optional: Break early for testing\n", + " # if layer == some_condition:\n", + " # break\n", + "\n", + " print('All done')" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "5dc66f30-9406-4e7a-adca-81161df3611a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Extracting features using network: VGG19_ILSVRC_19_layers\n", + "(73000, 3, 224, 224)\n", + "(1, 64, 224, 224)\n", + "(1, 64, 224, 224)\n", + "(1, 128, 112, 112)\n", + "(1, 128, 112, 112)\n", + "(1, 256, 56, 56)\n", + "(1, 256, 56, 56)\n", + "(1, 256, 56, 56)\n", + "(1, 256, 56, 56)\n", + "(1, 512, 28, 28)\n", + "(1, 512, 28, 28)\n", + "(1, 512, 28, 28)\n", + "(1, 512, 28, 28)\n", + "(1, 512, 14, 14)\n", + "(1, 512, 14, 14)\n", + "(1, 512, 14, 14)\n", + "(1, 512, 14, 14)\n", + "(1, 4096)\n", + "(1, 4096)\n", + "(1, 1000)\n" + ] + }, + { + "ename": "NameError", + "evalue": "name 'ththth' is not defined", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mNameError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[38], line 8\u001b[0m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;66;03m# %%\u001b[39;00m\n\u001b[1;32m 6\u001b[0m \u001b[38;5;66;03m# Load config and extract features\u001b[39;00m\n\u001b[1;32m 7\u001b[0m config \u001b[38;5;241m=\u001b[39m load_config(config_path)\n\u001b[0;32m----> 8\u001b[0m \u001b[43mextract_features\u001b[49m\u001b[43m(\u001b[49m\u001b[43mconfig\u001b[49m\u001b[43m)\u001b[49m\n", + "Cell \u001b[0;32mIn[37], line 31\u001b[0m, in \u001b[0;36mextract_features\u001b[0;34m(config, device)\u001b[0m\n\u001b[1;32m 27\u001b[0m f \u001b[38;5;241m=\u001b[39m features\u001b[38;5;241m.\u001b[39mget(layer)\n\u001b[1;32m 28\u001b[0m \u001b[38;5;28mprint\u001b[39m(f\u001b[38;5;241m.\u001b[39mshape)\n\u001b[0;32m---> 31\u001b[0m \u001b[43mththth\u001b[49m\n\u001b[1;32m 32\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m layer \u001b[38;5;129;01min\u001b[39;00m tqdm(layers, desc\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mProcessing layers\u001b[39m\u001b[38;5;124m\"\u001b[39m): \u001b[38;5;66;03m# Iterate over each layer first\u001b[39;00m\n\u001b[1;32m 33\u001b[0m layer_output_dir \u001b[38;5;241m=\u001b[39m os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mjoin(output_dir, layer)\n", + "\u001b[0;31mNameError\u001b[0m: name 'ththth' is not defined" + ] + } + ], + "source": [ + "# %% [Block 6]\n", + "# Instead of using argparse, manually provide the config file path for a notebook\n", + "config_path = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/config/NSD/NSD.yaml' # Replace with your config file path\n", + "\n", + "# %%\n", + "# Load config and extract features\n", + "config = load_config(config_path)\n", + "extract_features(config)\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35e4f307-1652-4af7-ad82-3ee4c3f142fd", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "2d6c4809-0aab-4257-8994-4e892e23de9f", + "metadata": {}, + "outputs": [], + "source": [ + "# with h5py.File(f'/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/vgg19_torchvision/features[0]/1.h5', 'a') as hdf5_file:\n", + "# dataset = hdf5_file['dataset']\n", + "# print(len(dataset))\n", + "# print(dataset[10].shape, dataset[10].dtype)\n", + "# print(dataset.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "fcf6d112-e24c-4fd5-ac59-f21e3611ebbc", + "metadata": {}, + "outputs": [], + "source": [ + "# dataset" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mindeye", + "language": "python", + "name": "mindeye" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/iCNN_image_vgg19_feature_extraction-checkpoint.py b/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/iCNN_image_vgg19_feature_extraction-checkpoint.py new file mode 100644 index 0000000000000000000000000000000000000000..fb30b01de1b18c158f0bd351e297ea9fb250d00d --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/.ipynb_checkpoints/iCNN_image_vgg19_feature_extraction-checkpoint.py @@ -0,0 +1,130 @@ +'''DNN Feature extraction script''' + +# %%% +from glob import glob +from tqdm import tqdm +import os +import argparse +from PIL import Image +import numpy as np +import torch +from scipy.io import savemat +from bdpy.dl.torch import FeatureExtractor +from torchvision import models, transforms +import yaml +# %%% +# Custum transform class to convert RGB to BGR +class ConvertRGBtoBGR: + def __call__(self, image): + return image[[2, 1, 0], :, :] + +# Custum transform class to convert Image format to tensor while keeping pixel ranges +class ToTensorWithoutScaling: + def __call__(self, image): + image = np.array(image).astype(np.float32) # PIL ImageをNumPy配列に変換 + tensor = torch.from_numpy(image).permute(2, 0, 1).float() # NumPy配列をテンソルに変換し、次元を並べ替える + return tensor + +class Convert32to16: + def __call__(self, image): + return image.type(torch.float16) + +# %%% +def load_model(network): + + if network == "VGG19_ILSVRC_19_layers": + from bdpy.dl.torch.models import layer_map, model_factory + model = model_factory('vgg19') + encoder_param_file = './data/models_shared/pytorch/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt' + model.load_state_dict(torch.load(encoder_param_file)) + model.eval() + layer_mapping = layer_map('vgg19') + # change RGB-> BGR and subtract mean via lambda function + mean_image = [104., 117., 123.] #BGR! + #mean_image = [103.939, 116.779, 123.68] #BGR! + preprocess = transforms.Compose([ + transforms.Resize(224, interpolation=Image.BICUBIC), + #transforms.CenterCrop(224), + #transforms.ToTensor(), + ToTensorWithoutScaling(), # カスタム変換を追加 + ConvertRGBtoBGR(), # カスタム変換を追加 + transforms.Normalize(mean=mean_image, std=[1.,1.,1.]), + ]) + + elif network == "vgg19_torchvision": + model = models.vgg19(pretrained=True) + model.eval() + + # 画像の前処理 + preprocess = transforms.Compose([ + transforms.Resize(224),#transforms.Resize(256), + #transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ]) + else: + raise ValueError("Network not supported. Please choose from: VGG19_ILSVRC_19_layers" + +"or define by yourself. The output should be model and preprocessing functions from RGB image to model input." + ) + + return model, preprocess + +def load_config(config_path): + '''Load configuration file.''' + with open(config_path, 'r') as file: + config = yaml.safe_load(file) + return config + +def extract_features(config, device='cuda'): + '''Extract features based on the configuration.''' + print("Extracting features using network:", config['network']) + + image_dir = config['image path'] + image_ext = config["image_ext"] + image_files = glob(os.path.join(image_dir, '*.' + image_ext)) + + model, preprocess = load_model(config['network']) + layers = config["features"] + feature_extractor = FeatureExtractor(model, layers, device=device, detach=True) + + output_dir = os.path.join(config["output base dir"], "pytorch",config['network']) + + for image_file in tqdm(image_files): + img = Image.open(image_file).convert('RGB') + + x = preprocess(img).unsqueeze(0).to(device) + + # Extract features + features = feature_extractor.run(x) + + # Save features + for layer in features.keys(): + f = features[layer] + + output_file = os.path.join( + output_dir, + layer, + os.path.splitext(os.path.basename(image_file))[0] + '.mat' + ) + + if os.path.exists(output_file): + continue + + os.makedirs(os.path.join(output_dir, layer), exist_ok=True) + + savemat(output_file, {'feat': f}) + #print('Saved {}'.format(output_file)) + + print('All done') + + + +# %%% +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='DNN Feature extraction') + parser.add_argument('config', help='Configuration file') + args = parser.parse_args() + + config = load_config(args.config) + extract_features(config) +# %% \ No newline at end of file diff --git a/spurious_reconstruction/analysis/0_preprocessing/BD_extract_CLIP_vision_features.py b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_CLIP_vision_features.py new file mode 100644 index 0000000000000000000000000000000000000000..857355f4c564a9ffcdeddaf23e675f4f178b9735 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_CLIP_vision_features.py @@ -0,0 +1,115 @@ +## Code is based on https://github.com/ozcelikfu/brain-diffuser/blob/main/scripts/clipvision_extract_features.py + +# %% +import sys +# %% +sys.path.append('brain-diffuser/versatile_diffusion') +import os +import PIL +from PIL import Image +import numpy as np + +import torch +from lib.cfg_helper import model_cfg_bank +from lib.model_zoo import get_model +#from lib.experiments.sd_default import color_adjust, auto_merge_imlist +from torch.utils.data import DataLoader, Dataset + +from lib.model_zoo.vd import VD +from lib.cfg_holder import cfg_unique_holder as cfguh +from lib.cfg_helper import get_command_line_args, cfg_initiates, load_cfg_yaml +import torchvision.transforms as T +import yaml +from easydict import EasyDict as edict +import argparse +import os.path as osp + +from scipy.io import savemat +# %% +# %% +class batch_generator_external_image_files(Dataset): + + def __init__(self, data_path): + self.data_path = data_path + self.im = sorted(os.listdir(self.data_path)) + + + def __getitem__(self,idx): + img = Image.open(os.path.join(self.data_path,self.im[idx])).convert('RGB') + img = T.functional.resize(img,(512,512)) + img = torch.tensor(np.array(img)).float() + #img = img/255 + img = img*2 - 1 + return img.permute(2,0, 1), self.im[idx] + + def __len__(self): + return len(self.im) + +def load_config(config_path): + '''Load configuration file.''' + with open(config_path, 'r') as file: + config = yaml.safe_load(file) + return config + +def extract_CLIP_vision_features(config): + batch_size=1 + # %% + cfgm_name = 'vd_noema' + cfgm = model_cfg_bank()(cfgm_name) + net = get_model()(cfgm) + # %% + pth = 'versatile_diffusion/pretrained/vd-four-flow-v1-0-fp16-deprecated.pth' + sd = torch.load(pth, map_location='cpu') + net.load_state_dict(sd, strict=False) + # %% + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + net.clip = net.clip.to(device) + # %% + + data_path = config['image path'] + # %% + image_data = batch_generator_external_image_files(data_path) + data_loader = DataLoader(image_data,batch_size,shuffle=False) + # %% + num_embed, num_features, num_image = 257, 768, image_data.__len__() + + clip_feat = np.zeros((num_image,num_embed,num_features)) + # %% + output_dir = os.path.join(config["output base dir"], "pytorch", 'brain_diffuser_versatile_diffusion') + # %% + layer = 'vision_encoder' + # %% + with torch.no_grad(): + # %% + for i,(imgs_torch_tensor, image_files) in enumerate(data_loader): + + # %% + c_v_feature = net.clip_encode_vision(imgs_torch_tensor) + + for j, image_file in enumerate(image_files): + f = c_v_feature[j][np.newaxis].cpu().numpy() + + output_file = os.path.join( + output_dir, + layer, + os.path.splitext(os.path.basename(image_file))[0] + '.mat' + ) + + if os.path.exists(output_file): + continue + + os.makedirs(os.path.join(output_dir, layer), exist_ok=True) + + savemat(output_file, {'feat': f}) + + +# %% + +# %%% +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='DNN Feature extraction') + parser.add_argument('config', help='Configuration file') + args = parser.parse_args() + + config = load_config(args.config) + extract_CLIP_vision_features(config) \ No newline at end of file diff --git a/spurious_reconstruction/analysis/0_preprocessing/BD_extract_Deeprecon_CLIP_text_features.py b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_Deeprecon_CLIP_text_features.py new file mode 100644 index 0000000000000000000000000000000000000000..daf2e28307b59c128c91da9e5eba7bcec2db21fd --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_Deeprecon_CLIP_text_features.py @@ -0,0 +1,101 @@ +# %% +import sys +sys.path.append('versatile_diffusion') +import os +import numpy as np + +import torch +from lib.cfg_helper import model_cfg_bank +from lib.model_zoo import get_model +from torch.utils.data import DataLoader, Dataset + +from lib.model_zoo.vd import VD +from lib.cfg_holder import cfg_unique_holder as cfguh +from lib.cfg_helper import get_command_line_args, cfg_initiates, load_cfg_yaml +import matplotlib.pyplot as plt +import torchvision.transforms as T +from scipy.io import savemat +from glob import glob +import pandas as pd +# %% + +#def extract_NSD_CLIP_text_features(): +def extract_Deeprecon_CLIP_text_features(): + cfgm_name = 'vd_noema' + pth = 'versatile_diffusion/pretrained/vd-four-flow-v1-0-fp16-deprecated.pth' + cfgm = model_cfg_bank()(cfgm_name) + net = get_model()(cfgm) + sd = torch.load(pth, map_location='cpu') + net.load_state_dict(sd, strict=False) + # %% + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + net.clip = net.clip.to(device) + + # %% + num_embed, num_features = 77, 768 + layer = 'text_encoder' + # Deeprecon test setting + image_list, df, save_base_path = Deeprecon_test_setting() + + output_dir = os.path.join(save_base_path, "pytorch", 'brain_diffuser_versatile_diffusion') + print('Extract text features from Deeprecon test images') + extract_CLIP_text_features(image_list, df, output_dir, layer, net) + print('Done') + # %% + + # Deeprecon traininig setting + training_image_list, df, save_base_path_training = Deeprecon_training_setting() + output_dir_training = os.path.join(save_base_path_training, "pytorch", 'brain_diffuser_versatile_diffusion') + print('Extract text features from Deeprecon training images') + extract_CLIP_text_features(training_image_list, df, output_dir_training, layer, net) + print('Done') + + +def Deeprecon_test_setting(): + original_DeepreconTest_dir = './data/ImageNetTest' + image_list = glob(os.path.join(original_DeepreconTest_dir, 'source', '*.JPEG')) + # we didn't share the test annotation file due to the possibility to traing the future AI models. + df = pd.read_csv(os.path.join(original_DeepreconTest_dir, 'derivatives/captions/amt_20181204/amt_20181204.csv')) + save_base_path = "./data/ImageNetTest/derivatives/features" + return image_list, df, save_base_path + +def Deeprecon_training_setting(): + original_DeepreconTraining_dir = './data/contents_shared/ImageNetTraining' + image_list = glob(os.path.join(original_DeepreconTraining_dir, 'source', '*.JPEG')) + df = pd.read_csv(os.path.join(original_DeepreconTraining_dir, 'derivatives/captions/amt_20181204/amt_20181204.csv')) + save_base_path = "./data/ImageNetTraining/derivatives/features" + return image_list, df, save_base_path + +def extract_CLIP_text_features(image_list, df, output_dir, layer, net): + for image in image_list: + file_name = os.path.basename(image).split('.')[0] + caption_texts = df[df['content_id'] == file_name]['caption'].values + #print(img) + out_text_list =[] + # %% + for cap in caption_texts: + c_t_feature = net.clip_encode_text(cap) + out_text_list.append(c_t_feature.cpu().detach().numpy()) + out_text_list = np.array(out_text_list).squeeze() #[5, 77, 768] + + # %% + output_file = os.path.join( + output_dir, + layer, + file_name + '.mat' + ) + if os.path.exists(output_file): + continue + + os.makedirs(os.path.join(output_dir, layer), exist_ok=True) + + savemat(output_file, dict([('feat', np.mean(out_text_list, 0, keepdims=True)), + ('raw_feat', out_text_list)]) ) + + +# %% + +# %%% +if __name__ == '__main__': + + extract_Deeprecon_CLIP_text_features() diff --git a/spurious_reconstruction/analysis/0_preprocessing/BD_extract_NSD_CLIP_text_features.py b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_NSD_CLIP_text_features.py new file mode 100644 index 0000000000000000000000000000000000000000..0faec3179218e70ff57bfec05067f5bb977c56c4 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/BD_extract_NSD_CLIP_text_features.py @@ -0,0 +1,78 @@ +# %% +import sys +sys.path.append('versatile_diffusion') +import os +import numpy as np + +import torch +from lib.cfg_helper import model_cfg_bank +from lib.model_zoo import get_model +from torch.utils.data import DataLoader, Dataset + +from lib.model_zoo.vd import VD +from lib.cfg_holder import cfg_unique_holder as cfguh +from lib.cfg_helper import get_command_line_args, cfg_initiates, load_cfg_yaml +import matplotlib.pyplot as plt +import torchvision.transforms as T +from nsd_access.nsd_access.nsda import NSDAccess +from scipy.io import savemat +# %% + +def extract_NSD_CLIP_text_features(): + + cfgm_name = 'vd_noema' + pth = 'versatile_diffusion/pretrained/vd-four-flow-v1-0-fp16-deprecated.pth' + cfgm = model_cfg_bank()(cfgm_name) + net = get_model()(cfgm) + sd = torch.load(pth, map_location='cpu') + net.load_state_dict(sd, strict=False) + # %% + device = torch.device('cuda' if torch.cuda.is_available() else 'cpu') + net.clip = net.clip.to(device) + + # %% + num_embed, num_features = 77, 768 + + origina_nsd_dir = '[PATH_TO_NSD_DATASET_DIR]' #'/home/nu/data/fmri_shared/public/NSD/latest/' + nsda = NSDAccess(origina_nsd_dir) + + output_dir = os.path.join("./data/NSD-stimuli/derivatives/features/", "pytorch", 'brain_diffuser_versatile_diffusion') + layer = 'text_encoder' + # %% + total_num = 73000 + for img in range(total_num): + file_name = f'nsd{img+1:05}'+ '.mat' + ci = nsda.read_image_coco_info([img], info_type='captions', show_annot=True) + #print(img) + out_text_list =[] + # %% + + for annotator in ci: + cap = annotator['caption'] + + c_v_feature = net.clip_encode_text(cap) + out_text_list.append(c_v_feature.cpu().detach().numpy()) + out_text_list = np.array(out_text_list).squeeze() #[5, 77, 768] + + # %% + output_file = os.path.join( + output_dir, + layer, + file_name + ) + if os.path.exists(output_file): + continue + + + os.makedirs(os.path.join(output_dir, layer), exist_ok=True) + + savemat(output_file, dict([('feat', np.mean(out_text_list, 0, keepdims=True)), + ('raw_feat', out_text_list)]) ) + + +# %% + +# %%% +if __name__ == '__main__': + + extract_NSD_CLIP_text_features() diff --git a/spurious_reconstruction/analysis/0_preprocessing/README.md b/spurious_reconstruction/analysis/0_preprocessing/README.md new file mode 100644 index 0000000000000000000000000000000000000000..e000751bb534d54cc08d18424bf4a5f35d2c32e6 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/README.md @@ -0,0 +1,15 @@ +Scripts for extracting deep neural networks (DNNs) features from images (or text annotation), located at `./data/[dataset]/source` folders. You should put the stimuli files appropriately beforehand or run the download script. + +# Usage (using rye environment) +If you want to extract VGG19 (for iCNN) in the Deeprecon dataset, running this command: +```rye run python ./analysis/0_preprocessing/iCNN_image_vgg19_feature_extraction.py ./analysis/0_preprocessing/config/Deeprecon/ImageNetTest.yaml``` + +If you want to extract CLIP text (for Brain-Diffuser) in the Deeprecon dataset, running this command: +```rye run python ./analysis/0_preprocessing/BD_extract_NSD_CLIP_tex_feature.py ./analysis/0_preprocessing/config/NSD/NSD.yaml``` + +If you want to extract CLIP vision (for Brain-Diffuser) in the NSD dataset, running this command: +```rye run python ./analysis/0_preprocessing/BD_extract_CLIP_vision_feature.py ./analysis/0_preprocessing/config/NSD/NSD.yaml``` + +Note: +If you want to extract CLIP features used in the Brain-Diffuser models, you need additonaly prepare[versatile_diffusion](https://github.com/ozcelikfu/brain-diffuser/tree/main/versatile_diffusion) at the top of this directory. + diff --git a/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ArtificialShapes.yaml b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ArtificialShapes.yaml new file mode 100644 index 0000000000000000000000000000000000000000..be01f2aced3b9977b45c397372e434eecb2e29e3 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ArtificialShapes.yaml @@ -0,0 +1,34 @@ +image path: + ./data/ArtificialShapes/source + +image_ext: + tiff + +output base dir: + ./data/ArtificialShapes/derivatives/features/ + + +# This is only related to the script "iCNN_image_vgg19_feature_extraction.py" +network: + VGG19_ILSVRC_19_layers + +features: + - features[0] # conv1_1 + - features[2] # conv1_2 + - features[5] # conv2_1 + - features[7] # conv2_2 + - features[10] # conv3_1 + - features[12] # conv3_2 + - features[14] # conv3_3 + - features[16] # conv3_4 + - features[19] # conv4_1 + - features[21] # conv4_2 + - features[23] # conv4_3 + - features[25] # conv4_4 + - features[28] # conv5_1 + - features[30] # conv5_2 + - features[32] # conv5_3 + - features[34] # conv5_4 + - classifier[0] # fc6 + - classifier[3] # fc7 + - classifier[6] # fc8 \ No newline at end of file diff --git a/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTest.yaml b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTest.yaml new file mode 100644 index 0000000000000000000000000000000000000000..e64a95bf37f994f0cbda68bc0eb54e31e5e97820 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTest.yaml @@ -0,0 +1,33 @@ +image path: + ./data/ImageNetTest/source + +image_ext: + JPEG + +output base dir: + ./data/ImageNetTest/derivatives/features/ + +# This is only related to the script "iCNN_image_vgg19_feature_extraction.py" +network: + VGG19_ILSVRC_19_layers + +features: + - features[0] # conv1_1 + - features[2] # conv1_2 + - features[5] # conv2_1 + - features[7] # conv2_2 + - features[10] # conv3_1 + - features[12] # conv3_2 + - features[14] # conv3_3 + - features[16] # conv3_4 + - features[19] # conv4_1 + - features[21] # conv4_2 + - features[23] # conv4_3 + - features[25] # conv4_4 + - features[28] # conv5_1 + - features[30] # conv5_2 + - features[32] # conv5_3 + - features[34] # conv5_4 + - classifier[0] # fc6 + - classifier[3] # fc7 + - classifier[6] # fc8 diff --git a/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTraining.yaml b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTraining.yaml new file mode 100644 index 0000000000000000000000000000000000000000..c33c2e1a35b3920c7617363b81e5b73d273ed813 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/config/Deeprecon/ImageNetTraining.yaml @@ -0,0 +1,34 @@ +image path: + ./data/ImageNetTraining/source + +image_ext: + JPEG + +output base dir: + ./data/ImageNetTraining/derivatives/features/ + + +# This is only related to the script "iCNN_image_vgg19_feature_extraction.py" +network: + VGG19_ILSVRC_19_layers + +features: + - features[0] # conv1_1 + - features[2] # conv1_2 + - features[5] # conv2_1 + - features[7] # conv2_2 + - features[10] # conv3_1 + - features[12] # conv3_2 + - features[14] # conv3_3 + - features[16] # conv3_4 + - features[19] # conv4_1 + - features[21] # conv4_2 + - features[23] # conv4_3 + - features[25] # conv4_4 + - features[28] # conv5_1 + - features[30] # conv5_2 + - features[32] # conv5_3 + - features[34] # conv5_4 + - classifier[0] # fc6 + - classifier[3] # fc7 + - classifier[6] # fc8 \ No newline at end of file diff --git a/spurious_reconstruction/analysis/0_preprocessing/config/NSD/.ipynb_checkpoints/NSD-checkpoint.yaml b/spurious_reconstruction/analysis/0_preprocessing/config/NSD/.ipynb_checkpoints/NSD-checkpoint.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9ab9a2168eb45944162a9b33622c956fa2771fb5 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/config/NSD/.ipynb_checkpoints/NSD-checkpoint.yaml @@ -0,0 +1,34 @@ +image path: + /weka/proj-medarc/shared/mindeyev2_dataset/ + +image_ext: + png + +output base dir: + ./features/ + +# This is only related to the script "iCNN_image_vgg19_feature_extraction.py" +network: + # vgg19_torchvision + VGG19_ILSVRC_19_layers + +features: + - features[0] # conv1_1 + - features[2] # conv1_2 + # - features[5] # conv2_1 + # - features[7] # conv2_2 + # - features[10] # conv3_1 + # - features[12] # conv3_2 + # - features[14] # conv3_3 + # - features[16] # conv3_4 + # - features[19] # conv4_1 + # - features[21] # conv4_2 + # - features[23] # conv4_3 + # - features[25] # conv4_4 + # - features[28] # conv5_1 + # - features[30] # conv5_2 + # - features[32] # conv5_3 + # - features[34] # conv5_4 + # - classifier[0] # fc6 + # - classifier[3] # fc7 + # - classifier[6] # fc8 diff --git a/spurious_reconstruction/analysis/0_preprocessing/config/NSD/NSD.yaml b/spurious_reconstruction/analysis/0_preprocessing/config/NSD/NSD.yaml new file mode 100644 index 0000000000000000000000000000000000000000..9ab9a2168eb45944162a9b33622c956fa2771fb5 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/config/NSD/NSD.yaml @@ -0,0 +1,34 @@ +image path: + /weka/proj-medarc/shared/mindeyev2_dataset/ + +image_ext: + png + +output base dir: + ./features/ + +# This is only related to the script "iCNN_image_vgg19_feature_extraction.py" +network: + # vgg19_torchvision + VGG19_ILSVRC_19_layers + +features: + - features[0] # conv1_1 + - features[2] # conv1_2 + # - features[5] # conv2_1 + # - features[7] # conv2_2 + # - features[10] # conv3_1 + # - features[12] # conv3_2 + # - features[14] # conv3_3 + # - features[16] # conv3_4 + # - features[19] # conv4_1 + # - features[21] # conv4_2 + # - features[23] # conv4_3 + # - features[25] # conv4_4 + # - features[28] # conv5_1 + # - features[30] # conv5_2 + # - features[32] # conv5_3 + # - features[34] # conv5_4 + # - classifier[0] # fc6 + # - classifier[3] # fc7 + # - classifier[6] # fc8 diff --git a/spurious_reconstruction/analysis/0_preprocessing/extract_features.ipynb b/spurious_reconstruction/analysis/0_preprocessing/extract_features.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..b1c76fdf874a23c24e7c3fa16a887a8f879b4e59 --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/extract_features.ipynb @@ -0,0 +1,716 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "5082cdc1-b052-433d-b018-29ba6fa3d313", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 1]\n", + "from glob import glob\n", + "from tqdm.auto import tqdm\n", + "import os\n", + "from PIL import Image\n", + "import numpy as np\n", + "import torch\n", + "from scipy.io import savemat\n", + "from bdpy.dl.torch import FeatureExtractor\n", + "from torchvision import models, transforms\n", + "import yaml\n", + "import h5py" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "a31be173-61ff-4adf-a558-63a3efe74cff", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 2]\n", + "# Custom transform class to convert RGB to BGR\n", + "class ConvertRGBtoBGR:\n", + " def __call__(self, image):\n", + " return image[[2, 1, 0], :, :] \n", + " \n", + "# Custom transform class to convert Image format to tensor while keeping pixel ranges\n", + "class ToTensorWithoutScaling:\n", + " def __call__(self, image):\n", + " image = np.array(image).astype(np.float32) # Convert PIL Image to NumPy array\n", + " tensor = torch.from_numpy(image).permute(2, 0, 1).float() # Convert NumPy array to tensor and rearrange dimensions\n", + " return tensor\n", + " \n", + "class Convert32to16:\n", + " def __call__(self, image):\n", + " return image.type(torch.float16)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "969abfe4-e4b6-4fa2-bd09-17f77ddf2292", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 3]\n", + "def load_model(network):\n", + " \n", + " if network == \"VGG19_ILSVRC_19_layers\":\n", + " from bdpy.dl.torch.models import layer_map, model_factory\n", + " model = model_factory('vgg19')\n", + " encoder_param_file = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt'\n", + " model.load_state_dict(torch.load(encoder_param_file))\n", + " model.eval()\n", + " layer_mapping = layer_map('vgg19')\n", + " # mean_image = [104., 117., 123.] # BGR\n", + " preprocess = transforms.Compose([\n", + " transforms.Resize((256, 256), interpolation=Image.BICUBIC),\n", + " # ToTensorWithoutScaling(), # Custom transform added\n", + " # ConvertRGBtoBGR(), # Custom transform added\n", + " # transforms.Normalize(mean=mean_image, std=[1.,1.,1.]),\n", + " ])\n", + " def preprocess_and_check(img):\n", + " print(\"Original shape:\", img.shape)\n", + " img = transforms.Resize((224, 224), interpolation=Image.BICUBIC)(img)\n", + " print(\"After ToTensor:\", img.shape)\n", + " print(\"After Resize:\", img.shape)\n", + " img = transforms.Normalize(mean=mean_image, std=[1.,1.,1.])(img)\n", + " return img\n", + " \n", + " elif network == \"vgg19_torchvision\":\n", + " model = models.vgg19(pretrained=True)\n", + " model.eval()\n", + " \n", + " preprocess = transforms.Compose([\n", + " transforms.Resize((224, 224)), \n", + " transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),\n", + " ]) \n", + " def preprocess_and_check(img):\n", + " print(\"Original shape:\", img.shape)\n", + " img = transforms.ToTensor()(img)\n", + " print(\"After ToTensor:\", img.shape)\n", + " img = transforms.Resize(224)(img)\n", + " print(\"After Resize:\", img.shape)\n", + " img = transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])(img)\n", + " return img\n", + " else:\n", + " raise ValueError(\"Network not supported. Please choose from: VGG19_ILSVRC_19_layers\"\n", + " +\"or define by yourself. The output should be model and preprocessing functions from RGB image to model input.\")\n", + " \n", + " return model, preprocess\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "8020d232-72cd-42ed-bb8e-183c8c0ee9b6", + "metadata": {}, + "outputs": [], + "source": [ + "# %% [Block 4]\n", + "def load_config(config_path):\n", + " '''Load configuration file.'''\n", + " with open(config_path, 'r') as file:\n", + " config = yaml.safe_load(file)\n", + " return config\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "a766c128-7e43-419c-9d7e-6f317e44753e", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "/weka/proj-medarc/shared/mindeyev2_dataset//wds/subj01/train/{0..39}.tar\n", + "/weka/proj-medarc/shared/mindeyev2_dataset//wds/subj01/new_test/0.tar\n", + "Loaded test dl for subj1!\n", + "\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f28e9699465f4df1af719bcc78a898e2", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "542a05f56b9a40ab82d55751c552b7bb", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "0it [00:00, ?it/s]" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import webdataset as wds\n", + "import random\n", + "\n", + "def my_split_by_node(urls): return urls\n", + "data_path = '/weka/proj-medarc/shared/mindeyev2_dataset/'\n", + "subj_list = [1]\n", + "num_sessions = 40\n", + "multi_subject = False\n", + "batch_size = 1\n", + "data_type = torch.float16\n", + "subj = 1\n", + "\n", + "train_data = {}\n", + "train_dl = {}\n", + "num_voxels = {}\n", + "voxels = {}\n", + "\n", + "train_url = f\"{data_path}/wds/subj0{subj}/train/\" + \"{0..\" + f\"{num_sessions-1}\" + \"}.tar\"\n", + "print(train_url)\n", + "\n", + "train_data = wds.WebDataset(train_url,resampled=False,nodesplitter=my_split_by_node)\\\n", + " .decode(\"torch\")\\\n", + " .rename(behav=\"behav.npy\", past_behav=\"past_behav.npy\", future_behav=\"future_behav.npy\", olds_behav=\"olds_behav.npy\")\\\n", + " .to_tuple(*[\"behav\", \"past_behav\", \"future_behav\", \"olds_behav\"])\n", + "train_dl = torch.utils.data.DataLoader(train_data, batch_size=batch_size, shuffle=False, drop_last=False, pin_memory=True)\n", + "\n", + "# f = h5py.File(f'{data_path}/betas_all_subj0{s}_fp32_renorm.hdf5', 'r')\n", + "# betas = f['betas'][:]\n", + "# betas = torch.Tensor(betas).to(\"cpu\").to(data_type)\n", + "# num_voxels_list.append(betas[0].shape[-1])\n", + "# num_voxels[f'subj0{s}'] = betas[0].shape[-1]\n", + "# voxels[f'subj0{s}'] = betas\n", + "# print(f\"num_voxels for subj0{s}: {num_voxels[f'subj0{s}']}\")\n", + "\n", + "\n", + "num_test=3000\n", + "test_url = f\"{data_path}/wds/subj0{subj}/new_test/\" + \"0.tar\"\n", + "print(test_url)\n", + "test_data = wds.WebDataset(test_url,resampled=False,nodesplitter=my_split_by_node)\\\n", + " .decode(\"torch\")\\\n", + " .rename(behav=\"behav.npy\", past_behav=\"past_behav.npy\", future_behav=\"future_behav.npy\", olds_behav=\"olds_behav.npy\")\\\n", + " .to_tuple(*[\"behav\", \"past_behav\", \"future_behav\", \"olds_behav\"])\n", + "test_dl = torch.utils.data.DataLoader(test_data, batch_size=1, shuffle=False, drop_last=True, pin_memory=True)\n", + "print(f\"Loaded test dl for subj{subj}!\\n\")\n", + "\n", + "all_indexes_train = []\n", + "\n", + "for behav0, past_behav0, future_behav0, old_behav0 in tqdm(train_dl):\n", + " img_idx = behav0[:,0,0].cpu().long().numpy()[0]\n", + " all_indexes_train.append(img_idx)\n", + "\n", + "all_indexes_test = []\n", + "\n", + "for behav0, past_behav0, future_behav0, old_behav0 in tqdm(test_dl):\n", + " img_idx = behav0[:,0,0].cpu().long().numpy()[0]\n", + " all_indexes_test.append(img_idx)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "17a4377a-67dc-49fc-b89c-7d641ef218ce", + "metadata": {}, + "outputs": [], + "source": [ + "def extract_features(config, device='cuda'):\n", + " '''Extract features based on the configuration.'''\n", + " print(\"Extracting features using network:\", config['network'])\n", + " \n", + " f = h5py.File(f'{config[\"image path\"]}/coco_images_224_float16.hdf5', 'r')\n", + " images = f['images']\n", + " print(images.shape)\n", + " model, preprocess = load_model(config['network'])\n", + " layers = config[\"features\"]\n", + " feature_extractor = FeatureExtractor(model, layers, device=device, detach=True)\n", + " \n", + " output_dir = os.path.join(config[\"output base dir\"], \"pytorch\", config['network'])\n", + "\n", + " all_indexes_to_compute = set(all_indexes_train + list(set(all_indexes_test)))\n", + " \n", + " # First, ensure all layer directories exist\n", + " # for layer in layers:\n", + " # layer_output_dir = os.path.join(output_dir, layer)\n", + " # os.makedirs(layer_output_dir, exist_ok=True)\n", + " # img = torch.Tensor(images[0])\n", + "\n", + " # x = preprocess(img).unsqueeze(0).to(device)\n", + " # # Extract features\n", + " # features = feature_extractor.run(x)\n", + " \n", + " # # Get the feature for the current layer\n", + " # f = features.get(layer)\n", + " # print(f.shape)\n", + " \n", + "\n", + " \n", + " for layer in tqdm(layers, desc=\"Processing layers\"): # Iterate over each layer first\n", + " layer_output_dir = os.path.join(output_dir, layer)\n", + "\n", + " with h5py.File(f'{layer_output_dir}/{subj}.h5', 'a') as hdf5_file:\n", + "\n", + " # initial_shape = (0, 100) # Start with 0 rows and 100 columns (or your array shape)\n", + " # max_shape = (None, 100) # Allow unlimited rows\n", + " \n", + " # if 'dataset' not in hdf5_file:\n", + " # dataset = hdf5_file.create_dataset(\n", + " # 'dataset', \n", + " # shape=initial_shape, \n", + " # maxshape=max_shape, \n", + " # dtype='float64'\n", + " # )\n", + " # else:\n", + " # dataset = hdf5_file['dataset']\n", + " for i, image_index in tqdm(enumerate(all_indexes_to_compute), total = len(all_indexes_to_compute)):\n", + " output_file = os.path.join(layer_output_dir, f\"{image_index}.npy\")\n", + " \n", + " if os.path.exists(output_file):\n", + " continue # Skip if the feature for this image and layer already exists\n", + " \n", + " # Load and preprocess the image\n", + " img = torch.Tensor(images[image_index])\n", + " x = preprocess(img).unsqueeze(0).to(device)\n", + " \n", + " # Extract features\n", + " features = feature_extractor.run(x)\n", + " # Get the feature for the current layer\n", + " f = features.get(layer)\n", + " if f is None:\n", + " print(f\"Warning: Layer '{layer}' not found in the extracted features.\")\n", + " continue\n", + "\n", + " if i == 0:\n", + " initial_shape = f.shape\n", + " max_shape = (None,) + initial_shape[1:]\n", + "\n", + " if 'dataset' not in hdf5_file:\n", + " dataset = hdf5_file.create_dataset(\n", + " 'dataset', \n", + " shape=initial_shape, \n", + " maxshape=max_shape, \n", + " dtype='float16'\n", + " )\n", + " else:\n", + " dataset = hdf5_file['dataset']\n", + "\n", + " dataset.resize(dataset.shape[0] + f.shape[0], axis=0)\n", + " \n", + " # Append the data\n", + " dataset[-f.shape[0]:] = f.astype(np.float16)\n", + " \n", + " # # Save the feature\n", + " # np.save(output_file, f.astype(np.float16))\n", + " \n", + " # Optional: Break early for testing\n", + " # if layer == some_condition:\n", + " # break\n", + "\n", + " print('All done')" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5dc66f30-9406-4e7a-adca-81161df3611a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Extracting features using network: VGG19_ILSVRC_19_layers\n", + "(73000, 3, 224, 224)\n" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8d90c3331b2149948797a4b66d9c9e32", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "Processing layers: 0%| | 0/2 [00:00 1\u001b[0m \u001b[43muuuu\u001b[49m\n", + "\u001b[0;31mNameError\u001b[0m: name 'uuuu' is not defined" + ] + } + ], + "source": [ + "uuuu" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35e4f307-1652-4af7-ad82-3ee4c3f142fd", + "metadata": {}, + "outputs": [], + "source": [ + "device = 'cuda'\n", + "'''Extract features based on the configuration.'''\n", + "print(\"Extracting features using network:\", config['network'])\n", + "\n", + "f = h5py.File(f'{config[\"image path\"]}/coco_images_224_float16.hdf5', 'r')\n", + "images = f['images']\n", + "print(images.shape)\n", + "model, preprocess = load_model(config['network'])\n", + "layers = config[\"features\"]\n", + "feature_extractor = FeatureExtractor(model, layers, device=device, detach=True)\n", + "\n", + "output_dir = os.path.join(config[\"output base dir\"], \"pytorch\", config['network'])\n", + "\n", + "all_indexes_to_compute = set(all_indexes_train + list(set(all_indexes_test)))\n", + "\n", + "# First, ensure all layer directories exist\n", + "# for layer in layers:\n", + "# layer_output_dir = os.path.join(output_dir, layer)\n", + "# os.makedirs(layer_output_dir, exist_ok=True)\n", + "# img = torch.Tensor(images[0])\n", + "\n", + "# x = preprocess(img).unsqueeze(0).to(device)\n", + "# # Extract features\n", + "# features = feature_extractor.run(x)\n", + " \n", + "# # Get the feature for the current layer\n", + "# f = features.get(layer)\n", + "# print(f.shape)\n", + " \n", + "\n", + "\n", + "for layer in tqdm(layers, desc=\"Processing layers\"): # Iterate over each layer first\n", + " layer_output_dir = os.path.join(output_dir, layer)\n", + "\n", + " # with h5py.File(f'{layer_output_dir}/{subj}.h5', 'a') as hdf5_file:\n", + "\n", + " # initial_shape = (0, 100) # Start with 0 rows and 100 columns (or your array shape)\n", + " # max_shape = (None, 100) # Allow unlimited rows\n", + " \n", + " # if 'dataset' not in hdf5_file:\n", + " # dataset = hdf5_file.create_dataset(\n", + " # 'dataset', \n", + " # shape=initial_shape, \n", + " # maxshape=max_shape, \n", + " # dtype='float64'\n", + " # )\n", + " # else:\n", + " # dataset = hdf5_file['dataset']\n", + " for i, image_index in tqdm(enumerate(all_indexes_to_compute), total = len(all_indexes_to_compute)):\n", + " output_file = os.path.join(layer_output_dir, f\"{image_index}.npy\")\n", + " \n", + " if os.path.exists(output_file):\n", + " continue # Skip if the feature for this image and layer already exists\n", + " \n", + " # Load and preprocess the image\n", + " img = torch.Tensor(images[image_index])\n", + " x = preprocess(img).unsqueeze(0).to(device)\n", + " \n", + " # Extract features\n", + " features = feature_extractor.run(x)\n", + " ththth\n", + " # Get the feature for the current layer\n", + " f = features.get(layer)\n", + " if f is None:\n", + " print(f\"Warning: Layer '{layer}' not found in the extracted features.\")\n", + " continue\n", + "\n", + " if i == 0:\n", + " initial_shape = f.shape\n", + " max_shape = (None,) + initial_shape[1:]\n", + "\n", + " if 'dataset' not in hdf5_file:\n", + " dataset = hdf5_file.create_dataset(\n", + " 'dataset', \n", + " shape=initial_shape, \n", + " maxshape=max_shape, \n", + " dtype='float16'\n", + " )\n", + " else:\n", + " dataset = hdf5_file['dataset']\n", + "\n", + " dataset.resize(dataset.shape[0] + f.shape[0], axis=0)\n", + "\n", + " # Append the data\n", + " dataset[-f.shape[0]:] = f.astype(np.float16)\n", + " \n", + " # # Save the feature\n", + " # np.save(output_file, f.astype(np.float16))\n", + "\n", + "# Optional: Break early for testing\n", + "# if layer == some_condition:\n", + "# break\n", + "\n", + "print('All done')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "638ad922-e988-4331-9e56-4287648abe4e", + "metadata": {}, + "outputs": [], + "source": [ + "model" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "05a07c1f-9047-49e4-8a82-9133a55f469f", + "metadata": {}, + "outputs": [], + "source": [ + "from torchvision.transforms import ToPILImage\n", + "class EncoderFeatureExtractor(torch.nn.Module):\n", + " def __init__(self, encoder, target_layer=0):\n", + " super(EncoderFeatureExtractor, self).__init__()\n", + " self.encoder = encoder\n", + " self.target_layer = target_layer\n", + " self.features = torch.nn.Sequential(*list(self.encoder.features.children())[:self.target_layer + 1])\n", + "\n", + " def forward(self, x):\n", + " return self.features(x)\n", + "\n", + "feature_extractor = EncoderFeatureExtractor(model, target_layer=0).to(device)\n", + "\n", + "\n", + "from PIL import Image\n", + "img\n", + "\n", + "# Preprocess the image\n", + "preprocess = transforms.Compose([\n", + " transforms.Resize((256, 256)),\n", + " # transforms.ToTensor(),\n", + " # transforms.Normalize(mean=[0.485, 0.456, 0.406], # Example normalization (ImageNet)\n", + " # std=[0.229, 0.224, 0.225]),\n", + "])\n", + "\n", + "# Preprocess the image\n", + "image_tensor = preprocess(img).unsqueeze(0).to(device)\n", + "\n", + "# show processed image\n", + "to_pil = ToPILImage()\n", + "pil_image = to_pil(image_tensor.squeeze())\n", + "pil_image.show()\n", + "\n", + "# Generate features from the image\n", + "with torch.no_grad():\n", + " image_features = feature_extractor(image_tensor)\n", + "\n", + "# Define target features\n", + "target_features = image_features.to(device).float()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53d2ed5f-f166-4b56-ac5d-f995db7ffbd7", + "metadata": {}, + "outputs": [], + "source": [ + "target_features[0,0,0,10:30].cpu()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63d92bf0-79fe-4584-8f76-53e3d6fa216d", + "metadata": {}, + "outputs": [], + "source": [ + "features['features[0]'].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ebb6206b-084b-4204-b5d0-c8f920dea6c0", + "metadata": {}, + "outputs": [], + "source": [ + "features['features[0]'][0,0,0,10:30]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "276faec3-0d46-4218-88fa-5c3fa103c81e", + "metadata": {}, + "outputs": [], + "source": [ + "torch.save(features['features[0]'], './ff.pt')" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e2049a1-0ccc-43d3-8fcb-4ff1531b1e9e", + "metadata": {}, + "outputs": [], + "source": [ + "# from PIL import Image\n", + "# import numpy as np\n", + "\n", + "# # Create a random image tensor with dimensions (3, 224, 224) \n", + "# # and values between 0 and 255 to simulate an RGB image\n", + "\n", + "# image = Image.fromarray((img.numpy().transpose(1, 2, 0) * 255).astype(np.uint8))\n", + "\n", + "# # Display the image\n", + "# image.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a189b39e-8493-4dff-806d-ec7768acada6", + "metadata": {}, + "outputs": [], + "source": [ + "# features['features[0]'].min()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dd3fc063-68bd-4c31-b794-4baaf30d629b", + "metadata": {}, + "outputs": [], + "source": [ + "# features['features[0]']" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2d6c4809-0aab-4257-8994-4e892e23de9f", + "metadata": {}, + "outputs": [], + "source": [ + "# with h5py.File(f'/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/vgg19_torchvision/features[0]/1.h5', 'a') as hdf5_file:\n", + "# dataset = hdf5_file['dataset']\n", + "# print(len(dataset))\n", + "# print(dataset[10].shape, dataset[10].dtype)\n", + "# print(dataset.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fcf6d112-e24c-4fd5-ac59-f21e3611ebbc", + "metadata": {}, + "outputs": [], + "source": [ + "# dataset" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mindeye", + "language": "python", + "name": "mindeye" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/spurious_reconstruction/analysis/0_preprocessing/iCNN_image_vgg19_feature_extraction.py b/spurious_reconstruction/analysis/0_preprocessing/iCNN_image_vgg19_feature_extraction.py new file mode 100644 index 0000000000000000000000000000000000000000..fb30b01de1b18c158f0bd351e297ea9fb250d00d --- /dev/null +++ b/spurious_reconstruction/analysis/0_preprocessing/iCNN_image_vgg19_feature_extraction.py @@ -0,0 +1,130 @@ +'''DNN Feature extraction script''' + +# %%% +from glob import glob +from tqdm import tqdm +import os +import argparse +from PIL import Image +import numpy as np +import torch +from scipy.io import savemat +from bdpy.dl.torch import FeatureExtractor +from torchvision import models, transforms +import yaml +# %%% +# Custum transform class to convert RGB to BGR +class ConvertRGBtoBGR: + def __call__(self, image): + return image[[2, 1, 0], :, :] + +# Custum transform class to convert Image format to tensor while keeping pixel ranges +class ToTensorWithoutScaling: + def __call__(self, image): + image = np.array(image).astype(np.float32) # PIL ImageをNumPy配列に変換 + tensor = torch.from_numpy(image).permute(2, 0, 1).float() # NumPy配列をテンソルに変換し、次元を並べ替える + return tensor + +class Convert32to16: + def __call__(self, image): + return image.type(torch.float16) + +# %%% +def load_model(network): + + if network == "VGG19_ILSVRC_19_layers": + from bdpy.dl.torch.models import layer_map, model_factory + model = model_factory('vgg19') + encoder_param_file = './data/models_shared/pytorch/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt' + model.load_state_dict(torch.load(encoder_param_file)) + model.eval() + layer_mapping = layer_map('vgg19') + # change RGB-> BGR and subtract mean via lambda function + mean_image = [104., 117., 123.] #BGR! + #mean_image = [103.939, 116.779, 123.68] #BGR! + preprocess = transforms.Compose([ + transforms.Resize(224, interpolation=Image.BICUBIC), + #transforms.CenterCrop(224), + #transforms.ToTensor(), + ToTensorWithoutScaling(), # カスタム変換を追加 + ConvertRGBtoBGR(), # カスタム変換を追加 + transforms.Normalize(mean=mean_image, std=[1.,1.,1.]), + ]) + + elif network == "vgg19_torchvision": + model = models.vgg19(pretrained=True) + model.eval() + + # 画像の前処理 + preprocess = transforms.Compose([ + transforms.Resize(224),#transforms.Resize(256), + #transforms.CenterCrop(224), + transforms.ToTensor(), + transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]), + ]) + else: + raise ValueError("Network not supported. Please choose from: VGG19_ILSVRC_19_layers" + +"or define by yourself. The output should be model and preprocessing functions from RGB image to model input." + ) + + return model, preprocess + +def load_config(config_path): + '''Load configuration file.''' + with open(config_path, 'r') as file: + config = yaml.safe_load(file) + return config + +def extract_features(config, device='cuda'): + '''Extract features based on the configuration.''' + print("Extracting features using network:", config['network']) + + image_dir = config['image path'] + image_ext = config["image_ext"] + image_files = glob(os.path.join(image_dir, '*.' + image_ext)) + + model, preprocess = load_model(config['network']) + layers = config["features"] + feature_extractor = FeatureExtractor(model, layers, device=device, detach=True) + + output_dir = os.path.join(config["output base dir"], "pytorch",config['network']) + + for image_file in tqdm(image_files): + img = Image.open(image_file).convert('RGB') + + x = preprocess(img).unsqueeze(0).to(device) + + # Extract features + features = feature_extractor.run(x) + + # Save features + for layer in features.keys(): + f = features[layer] + + output_file = os.path.join( + output_dir, + layer, + os.path.splitext(os.path.basename(image_file))[0] + '.mat' + ) + + if os.path.exists(output_file): + continue + + os.makedirs(os.path.join(output_dir, layer), exist_ok=True) + + savemat(output_file, {'feat': f}) + #print('Saved {}'.format(output_file)) + + print('All done') + + + +# %%% +if __name__ == '__main__': + parser = argparse.ArgumentParser(description='DNN Feature extraction') + parser.add_argument('config', help='Configuration file') + args = parser.parse_args() + + config = load_config(args.config) + extract_features(config) +# %% \ No newline at end of file diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_reconstruction.py b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_reconstruction.py new file mode 100644 index 0000000000000000000000000000000000000000..0a9d0d34fef28a9e526a5ab105ce8b28041a9126 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_reconstruction.py @@ -0,0 +1,352 @@ +# %% +import argparse +import os +from glob import glob +import shutil +import bdpy +from bdpy.util import makedir_ifnot +from PIL import Image + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import yaml + +import PIL +from PIL import Image, ImageDraw, ImageFont +def draw_group_image_set(condition_list, background_color = (255, 255, 255), + image_size = (160, 160), image_margin = (1, 1, 0, 0), group_margin = (20, 0, 20, 0), max_column_size = 13, + title_fontsize = 20, title_top_padding = 70, title_left_padding = 15, + id_show = False, id_fontcolor = "black", id_fontsize = 18, image_id_list = [], maintain_aspect_ratio=False, + image_padding_color=(0, 0, 0)): + """ + condition_list : list + Each condition is a dictionary-type object that contains the following information: + ``` + condition = { + "title" : string, # Title name + "title_fontcolor" : string or list, # HTML color name or RGB value list + "image_list": list, # The list of image filepath, ndarray or PIL.Image object. + } + ``` + You can also use "image_filepath_list" instead of "image_list". + background_color : list or tuple + RGB value list like [Red, Green, Blue]. + image_size: list or tuple + The image size like [Height, Width]. + image_margin: list or tuple + The margin of an image like [Top, Right, Bottom, Left]. + group_margin : list or tuple + The margin of the multiple row images as [Top, Right, Bottom, Left]. + max_column_size : int + Maximum number of images arranged horizontally. + title_fontsize : int + The font size of titles. + title_top_padding : + Top margin of the title letter. + title_left_padding : + Left margin of the title letter. + id_show : bool + Specifying whether to display id name. + id_fontcolor : list or tuple + Font color of id name. + id_fontsize : int + Font size of id name. + image_id_list : list + List of id names. + This list is required when `id_show` is True. + """ + + #------------------------------------ + # Setting + #------------------------------------ + + for condition in condition_list: + if not condition.get("image_filepath_list") and not condition.get("image_list"): + raise RuntimeError("The element of `condition_list` needs `image_filepath_list` or `image_list`.") + return; + elif condition.get("image_filepath_list") and not condition.get("image_list"): + condition["image_list"] = condition["image_filepath_list"] + + total_image_size = len(condition_list[0]["image_list"]) + column_size = np.min([max_column_size, total_image_size]) + + # create canvas + turn_num = int(np.ceil(total_image_size / float(column_size))) + nImg_row = len(condition_list) * turn_num + nImg_col = 1 + column_size # 1 means title column + size_x = (image_size[0] + image_margin[0] + image_margin[2]) * nImg_row + (group_margin[0] + group_margin[2]) * turn_num + size_y = (image_size[1] + image_margin[1] + image_margin[3]) * nImg_col + (group_margin[1] + group_margin[3]) + image = np.ones([size_x, size_y, 3]) + for bi, bc in enumerate(background_color): + image[:, :, bi] = bc + + #------------------------------------ + # Draw image + #------------------------------------ + for cind, condition in enumerate(condition_list): + title = condition['title'] + image_list = condition['image_list'] + + for tind in range(total_image_size): + # Load image + an_image = image_list[tind] + if an_image is None: # skip + continue; + elif isinstance(an_image, str): # str: filepath + image_obj = Image.open(an_image) + elif isinstance(an_image, np.ndarray): # np.ndarray: array + image_obj = Image.fromarray(an_image) + elif hasattr(an_image, "im"): # im attribute: PIL.Image + image_obj = an_image + else: + raise RuntimeError("What can be treated as an element of `image_list` is only str, ndarray or PIL.Image type.") + return + + image_obj = image_obj.convert("RGB") + if maintain_aspect_ratio: + image_obj = expand2square(image_obj, image_padding_color) + image_obj = image_obj.resize((image_size[0], image_size[1]), Image.LANCZOS) + + # Calc image position + row_index = cind + (tind // column_size) * len(condition_list) + column_index = 1 + tind % column_size + turn_index = tind // column_size + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + y = image_margin[3] + group_margin[3] + column_index * (image_size[1] + image_margin[1] + image_margin[3]) + image[ x:(x+image_size[0]), y:(y+image_size[1]), : ] = np.array(image_obj)[:,:,:] + + #------------------------------------ + # Prepare for drawing text + #------------------------------------ + # cast to unsigned int8 + image = image.astype('uint8') + + # convert ndarray to image object + image_obj = Image.fromarray(image) + draw = ImageDraw.Draw(image_obj) + + #------------------------------------ + # Draw title name + #------------------------------------ + # Use default font instead of loading from a path + draw.font = ImageFont.load_default() + for cind, condition in enumerate(condition_list): + for turn_index in range(turn_num): + # Calc text position + row_index = cind + turn_index * len(condition_list) + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + x += title_top_padding + y = title_left_padding + + # textの座標指定はxとyが逆転するので注意 + if "title_fontcolor" not in condition.keys(): + title_fontcolor = "black" + else: + title_fontcolor = condition["title_fontcolor"] + draw.text([y, x], condition["title"], title_fontcolor) + + #------------------------------------ + # Draw image id name + # * image_id_list variables is necessary + #------------------------------------ + + if id_show: + draw.font = ImageFont.load_default() + for tind in range(total_image_size): + # Calc text position + row_index = (tind // column_size) * len(condition_list) + column_index = 1 + tind % column_size + turn_index = tind // column_size + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + x -= id_fontsize + y = image_margin[3] + group_margin[3] + column_index * (image_size[1] + image_margin[1] + image_margin[3]) + + draw.text([y, x], image_id_list[tind], id_fontcolor) + + return image_obj + + +# %% +save_dir = './results_dl/assets/fig02' +os.makedirs(save_dir, exist_ok=True) + + +# NSD +print("NSD") +true_image_dir = './data/NSD-stimuli/source/' +true_image_ext = 'png' +recon_image_ext = 'tiff' + +true_image_all = list(np.sort((glob(os.path.join(true_image_dir, '*.' + true_image_ext))))) + +selected_images = np.sort(['nsd03050', 'nsd03435', 'nsd05302', 'nsd07008', 'nsd10065', 'nsd22264', + 'nsd37802' , 'nsd19201', 'nsd26436', 'nsd10007', 'nsd21509', 'nsd67803' + ]) +true_images = [true_image for true_image in true_image_all if os.path.basename(true_image).split('.')[0] in selected_images] +print(len(true_images)) + +recon_path_dict = { + + #"StableDiffusionReconstruction": "./results/reconstruction/NSD-stimuli/derivatives/reconstruction/TN_SD_v1_4/img2img/decoded/gen_5_image2imagensd-betasfithrfGLMdenoiseRR_trainnoave_testave_fastl2lir_a1_nsd-betasfithrfGLMdenoiseRR_trainnoave_testave_fastl2lir_a1/nsd-01/streams_early-streams_ventralgen_0/", + + #"BrainDiffuser": "./results/reconstruction/NSD-stimuli/derivatives/reconstruction/... + + + "iCNN": "./results_dl/reconstruction/NSD-stimuli/derivatives/reconstruction/icnn/recon_bdpy_icnn_image_gd_dist_vgg19_relu7generator_gd_scaling_feature_std_train_mean_center_1000iter/decoded/NSD-stimuli/decoded_features" + } + + +image_set = [ + {'title': '', 'image_filepath_list': true_images}, + ] + +image_path_list = [] +for i, (cond_name, recon_path) in enumerate(recon_path_dict.items()): + print(cond_name) + + recon_images = glob(f'{recon_path}/*.tiff') + recon_images = [recon_image for recon_image in recon_images if os.path.basename(recon_image).split('.')[0].split('/')[-1] in selected_images] + recon_images =list(np.sort(recon_images)) + print(len(recon_images)) + image_set.append( + {'title': "",#f'{cond_name}', + 'image_filepath_list': recon_images}, + ) + #assert 1== 0 + +# %% +img = draw_group_image_set( + image_set, + max_column_size=12, + title_left_padding = 0, + title_fontsize = 18, + + ) + +img.save(f'{save_dir}/reconstruction_NSD_selected.pdf') + + +# %% +# Deeprecon +# reported image selection +print("Deeprecon") +# %% +true_image_dir = './data/ImageNetTest/source/' +true_image_ext = 'JPEG' +recon_image_ext = 'tiff' + +true_image_all = list(np.sort((glob(os.path.join(true_image_dir, '*.' + true_image_ext))))) + + +selected_images = [ "n01443537_22563", "n01858441_11077","n02139199_10398", "n02690373_7713", "n03710193_22225", "n04252077_10859"] + +true_images = [true_image for true_image in true_image_all if os.path.basename(true_image).split('.')[0] in selected_images ] +print(len(true_images)) + + +recon_path_dict = { + + #"StableDiffusionReconstruction": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/TN_SD_v1_4/img2img/decoded/gen_5_image2imagedeeprecon-fmriprep_trainnoave_testave_fastl2lir_no_vox_select_a1_deeprecon-fmriprep_trainnoave_testave_fastl2lir_no_vox_select_a1/dr-01/VC-VCgen_0/", + + #"Brain-Diffuser": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/versatile_diffusion/vd_doublecond_scaled_pytorch/decoded/deeprecon_testImageNet_trainnoave_testave_fastl2lir_alpha_100000/dr-01/VC/", + + "iCNN": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/icnn/recon_bdpy_icnn_image_AdamW_dist_vgg19_relu7generator_gd_scaling_feature_std_train_mean_center_800iter/decoded/ImageNetTest/decoded_features" + + } +# %% +# save all images + +image_set = [ + {'title': '', 'image_filepath_list': true_images}, + ] + +image_path_list = [] +for i, (cond_name, recon_path) in enumerate(recon_path_dict.items()): + print(cond_name) + + recon_images = glob(f'{recon_path}/*.tiff') + recon_images = [recon_image for recon_image in recon_images if os.path.basename(recon_image).split('.')[0].split('/')[-1] in selected_images] + recon_images =list(np.sort(recon_images)) + print(len(recon_images)) + image_set.append( + {'title': "",#f'{cond_name}', + 'image_filepath_list': recon_images}, + ) + + + + +# 画像をプロットする +# %% +img = draw_group_image_set( + image_set, + max_column_size=6, + title_left_padding = 0, + title_fontsize = 18, + + ) + +img.save(f'{save_dir}/reconstruction_Deeprecon_selected.pdf') + + +# ArtificialShapes +print("ArtificialShapes") +true_image_dir = './data/ArtificialShapes/source/' +true_image_ext = 'tiff' +recon_image_ext = 'tiff' + +true_image_all = list(np.sort((glob(os.path.join(true_image_dir, '*.' + true_image_ext))))) + +selected_images = np.sort(["colorExpStim01_red_square", + "colorExpStim03_red_largering", + "colorExpStim04_red_+", + "colorExpStim09_green_+", + "colorExpStim31_white_square","colorExpStim40_black_X"]) + +true_images = [true_image for true_image in true_image_all if os.path.basename(true_image).split('.')[0] in selected_images] +print(len(true_images)) + +recon_path_dict = { + + #"StableDiffusionReconstruction": "./results/reconstruction/NSD-stimuli/derivatives/reconstruction/TN_SD_v1_4/img2img/decoded/gen_5_image2imagensd-betasfithrfGLMdenoiseRR_trainnoave_testave_fastl2lir_a1_nsd-betasfithrfGLMdenoiseRR_trainnoave_testave_fastl2lir_a1/nsd-01/streams_early-streams_ventralgen_0/", + + #"BrainDiffuser": "./results/reconstruction/NSD-stimuli/derivatives/reconstruction/... + + + "iCNN": "./results/reconstruction/ArtificialShapes/derivatives/reconstruction/icnn/recon_bdpy_icnn_image_gd_dist_vgg19_relu7generator_gd_scaling_feature_std_train_mean_center_1000iter/decoded/ArtificialShapes/decoded_features" + } + + +image_set = [ + {'title': '', 'image_filepath_list': true_images}, + ] + +image_path_list = [] +for i, (cond_name, recon_path) in enumerate(recon_path_dict.items()): + print(cond_name) + + recon_images = glob(f'{recon_path}/*.tiff') + recon_images = [recon_image for recon_image in recon_images if os.path.basename(recon_image).split('.')[0].split('/')[-1] in selected_images] + recon_images =list(np.sort(recon_images)) + print(len(recon_images)) + image_set.append( + {'title': "",#f'{cond_name}', + 'image_filepath_list': recon_images}, + ) + +# %% +img = draw_group_image_set( + image_set, + max_column_size=6, + title_left_padding = 0, + title_fontsize = 18, + + ) + +img.save(f'{save_dir}/reconstruction_ArtificialShapes_selected.pdf') + +# %% diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_recovery_check.py b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_recovery_check.py new file mode 100644 index 0000000000000000000000000000000000000000..9d4e21928ebfbe4bdc86ce1381d38c319871a0ec --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/Figure_image_recovery_check.py @@ -0,0 +1,242 @@ +# %% +import argparse +import os +from glob import glob +import shutil +import bdpy +from bdpy.util import makedir_ifnot +from PIL import Image + +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +import yaml + +import PIL +from PIL import Image, ImageDraw, ImageFont +def draw_group_image_set(condition_list, background_color = (255, 255, 255), + image_size = (160, 160), image_margin = (1, 1, 0, 0), group_margin = (20, 0, 20, 0), max_column_size = 13, + title_fontsize = 20, title_top_padding = 70, title_left_padding = 15, + id_show = False, id_fontcolor = "black", id_fontsize = 18, image_id_list = [], maintain_aspect_ratio=False, + image_padding_color=(0, 0, 0)): + """ + condition_list : list + Each condition is a dictionary-type object that contains the following information: + ``` + condition = { + "title" : string, # Title name + "title_fontcolor" : string or list, # HTML color name or RGB value list + "image_list": list, # The list of image filepath, ndarray or PIL.Image object. + } + ``` + You can also use "image_filepath_list" instead of "image_list". + background_color : list or tuple + RGB value list like [Red, Green, Blue]. + image_size: list or tuple + The image size like [Height, Width]. + image_margin: list or tuple + The margin of an image like [Top, Right, Bottom, Left]. + group_margin : list or tuple + The margin of the multiple row images as [Top, Right, Bottom, Left]. + max_column_size : int + Maximum number of images arranged horizontally. + title_fontsize : int + The font size of titles. + title_top_padding : + Top margin of the title letter. + title_left_padding : + Left margin of the title letter. + id_show : bool + Specifying whether to display id name. + id_fontcolor : list or tuple + Font color of id name. + id_fontsize : int + Font size of id name. + image_id_list : list + List of id names. + This list is required when `id_show` is True. + """ + + #------------------------------------ + # Setting + #------------------------------------ + + for condition in condition_list: + if not condition.get("image_filepath_list") and not condition.get("image_list"): + raise RuntimeError("The element of `condition_list` needs `image_filepath_list` or `image_list`.") + return; + elif condition.get("image_filepath_list") and not condition.get("image_list"): + condition["image_list"] = condition["image_filepath_list"] + + total_image_size = len(condition_list[0]["image_list"]) + column_size = np.min([max_column_size, total_image_size]) + + # create canvas + turn_num = int(np.ceil(total_image_size / float(column_size))) + nImg_row = len(condition_list) * turn_num + nImg_col = 1 + column_size # 1 means title column + size_x = (image_size[0] + image_margin[0] + image_margin[2]) * nImg_row + (group_margin[0] + group_margin[2]) * turn_num + size_y = (image_size[1] + image_margin[1] + image_margin[3]) * nImg_col + (group_margin[1] + group_margin[3]) + image = np.ones([size_x, size_y, 3]) + for bi, bc in enumerate(background_color): + image[:, :, bi] = bc + + #------------------------------------ + # Draw image + #------------------------------------ + for cind, condition in enumerate(condition_list): + title = condition['title'] + image_list = condition['image_list'] + + for tind in range(total_image_size): + # Load image + an_image = image_list[tind] + if an_image is None: # skip + continue; + elif isinstance(an_image, str): # str: filepath + image_obj = Image.open(an_image) + elif isinstance(an_image, np.ndarray): # np.ndarray: array + image_obj = Image.fromarray(an_image) + elif hasattr(an_image, "im"): # im attribute: PIL.Image + image_obj = an_image + else: + raise RuntimeError("What can be treated as an element of `image_list` is only str, ndarray or PIL.Image type.") + return + + image_obj = image_obj.convert("RGB") + if maintain_aspect_ratio: + image_obj = expand2square(image_obj, image_padding_color) + image_obj = image_obj.resize((image_size[0], image_size[1]), Image.LANCZOS) + + # Calc image position + row_index = cind + (tind // column_size) * len(condition_list) + column_index = 1 + tind % column_size + turn_index = tind // column_size + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + y = image_margin[3] + group_margin[3] + column_index * (image_size[1] + image_margin[1] + image_margin[3]) + image[ x:(x+image_size[0]), y:(y+image_size[1]), : ] = np.array(image_obj)[:,:,:] + + #------------------------------------ + # Prepare for drawing text + #------------------------------------ + # cast to unsigned int8 + image = image.astype('uint8') + + # convert ndarray to image object + image_obj = Image.fromarray(image) + draw = ImageDraw.Draw(image_obj) + + #------------------------------------ + # Draw title name + #------------------------------------ + # Use default font instead of loading from a path + draw.font = ImageFont.load_default() + for cind, condition in enumerate(condition_list): + for turn_index in range(turn_num): + # Calc text position + row_index = cind + turn_index * len(condition_list) + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + x += title_top_padding + y = title_left_padding + + # textの座標指定はxとyが逆転するので注意 + if "title_fontcolor" not in condition.keys(): + title_fontcolor = "black" + else: + title_fontcolor = condition["title_fontcolor"] + draw.text([y, x], condition["title"], title_fontcolor) + + #------------------------------------ + # Draw image id name + # * image_id_list variables is necessary + #------------------------------------ + + if id_show: + draw.font = ImageFont.load_default() + for tind in range(total_image_size): + # Calc text position + row_index = (tind // column_size) * len(condition_list) + column_index = 1 + tind % column_size + turn_index = tind // column_size + x = image_margin[0] + group_margin[0] + row_index * (image_size[0] + image_margin[0] + image_margin[2]) + x += turn_index * (group_margin[0] + group_margin[2]) + x -= id_fontsize + y = image_margin[3] + group_margin[3] + column_index * (image_size[1] + image_margin[1] + image_margin[3]) + + draw.text([y, x], image_id_list[tind], id_fontcolor) + + return image_obj + + +# %% +save_dir = './results/assets/fig07' +os.makedirs(save_dir, exist_ok=True) + + +# %% +# Deeprecon +# reported image selection +print("Deeprecon") +# %% +true_image_dir = './data/ImageNetTest/source/' +true_image_ext = 'JPEG' +recon_image_ext = 'tiff' + +true_image_all = list(np.sort((glob(os.path.join(true_image_dir, '*.' + true_image_ext))))) + + +selected_images = [ "n01443537_22563", "n01858441_11077","n02139199_10398", "n02690373_7713", "n03710193_22225", "n04252077_10859"] + +true_images = [true_image for true_image in true_image_all if os.path.basename(true_image).split('.')[0] in selected_images ] +print(len(true_images)) + + +recon_path_dict = { + + #"StableDiffusionReconstruction": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/TN_SD_v1_4/img2img/decoded/gen_5_image2imagedeeprecon-fmriprep_trainnoave_testave_fastl2lir_no_vox_select_a1_deeprecon-fmriprep_trainnoave_testave_fastl2lir_no_vox_select_a1/dr-01/VC-VCgen_0/", + + #"Brain-Diffuser": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/versatile_diffusion/vd_doublecond_scaled_pytorch/decoded/deeprecon_testImageNet_trainnoave_testave_fastl2lir_alpha_100000/dr-01/VC/", + + "iCNN": "./results/reconstruction/ImageNetTest/derivatives/reconstruction/icnn/recovery_check_bdpy_icnn_image_AdamW_dist_vgg19_relu7generator_gd_scaling_feature_std_train_mean_center_800iter/decoded/ImageNetTest/true_features" + + } +# %% +# save all images + +image_set = [ + {'title': '', 'image_filepath_list': true_images}, + ] + +image_path_list = [] +for i, (cond_name, recon_path) in enumerate(recon_path_dict.items()): + print(cond_name) + + recon_images = glob(f'{recon_path}/*.tiff') + recon_images = [recon_image for recon_image in recon_images if os.path.basename(recon_image).split('.')[0].split('/')[-1] in selected_images] + recon_images =list(np.sort(recon_images)) + print(len(recon_images)) + image_set.append( + {'title': "",#f'{cond_name}', + 'image_filepath_list': recon_images}, + ) + + + + +# 画像をプロットする +# %% +img = draw_group_image_set( + image_set, + max_column_size=6, + title_left_padding = 0, + title_fontsize = 18, + + ) + +img.save(f'{save_dir}/recovery_check_Deeprecon_selected.pdf') + + + +# %% diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/README.md b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/README.md new file mode 100644 index 0000000000000000000000000000000000000000..3ff436768b2e0a746ec307cc4257a8d5b7d2d28f --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/README.md @@ -0,0 +1,48 @@ +# Image Reconstruction Analysis + +This directory contains scripts for performing image reconstruction, corresponding to Figures 2 and 7 in the main text. The repository currently supports only iCNN reconstruction methods (Shen et al., 2019). If you want to use other reconstruction methods, please refer to the authors' official repositories: + +- [StableDiffusionReconstruction (Takagi and Nishimoto, 2023)](https://github.com/yu-takagi/StableDiffusionReconstruction) +- [Brain-Diffuser (Ozcelik and VanRullen, 2023)](https://github.com/ozcelikfu/brain-diffuser) + +## Prerequisites + +To run the reconstruction scripts, you need to prepare sesveral files, including DNN features and corresponding decoded (translated) features, and model parameters beforehand. You can download those files by running download scripts. +``` +python ./download.py "image reconstruction analysis" +``` +Optionaly, you can prepraed decoded feature, conducting feature decoding analysis at `feature-decoding directory`. + +### Usage + +If you want to reconstruct Deeprecon data from their decoded features, running this command: + +``` + python ./analysis/1_case_study/image-reconstruction/iCNN/recon_bdpy_icnn_image_AdamW_dist.py ./analysis/1_case_study/image-reconstruction/iCNN/configs/recon_icnn_ImageNetTest_vgg19_relu7generator_AdamW_1000iter_decoded.yaml +``` +If you want to reconstruct NSD test data from their decoded features, running this command: + +``` +python ./analysis/1_case_study/image-reconstruction/iCNN/recon_bdpy_icnn_image_AdamW_dist.py ./analysis/1_case_study/image-reconstruction/iCNN/config/recon_icnn_nsd_vgg19_relu7generator_AdamW_1000iterl_decoded.yaml +``` +The similar figure related to Figure 2 in the paper can be produced by: +``` +python ./analysis/1_case_study/image-reconstruction/Figure_image_reconstruction.py + +``` + +The similar figure related to Figure 7 in the paper can be obtrained by: +Get recovered images from true features. For example, if you want to reconstruct ArtificialShapes data from their true features, running this command: + +```python ./analysis/1_case_study/image-reconstruction/iCNN/recovery_check_bdpy_icnn_image_AdamW_dist.py ./analysis/1_case_study/image-reconstruction/iCNN/config/recon_icnn_ArtificialShapes_vgg19_relu7generator_AdamW_1000iter_decoded.yaml +``` + +Then running: + +``` +python ./analysis/1_case_study/image-reconstruction/Figure_image_recovery_check.py + +``` +--- + +Ensure that all required data files and environment configurations are in place before running the commands. \ No newline at end of file diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/10_nsd.jpg b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/10_nsd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..2dfa5bca0dc61f115485d21117161cd64531c000 Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/10_nsd.jpg differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/44_nsd.jpg b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/44_nsd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7a374f011fb00fbce406cfbb181a842088d91bc6 Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/44_nsd.jpg differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/488_nsd.jpg b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/488_nsd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..ff9ab8fb9aa6e41738035f1fd9deedb5ccffd03a Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/488_nsd.jpg differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/66_nsd.jpg b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/66_nsd.jpg new file mode 100644 index 0000000000000000000000000000000000000000..138872994f16360b2dbfb87bad757245009f145c Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/66_nsd.jpg differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/accel.slurm b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/accel.slurm new file mode 100644 index 0000000000000000000000000000000000000000..97bb4bbf963b5a76cc382bff06d2da937d4f6f98 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/accel.slurm @@ -0,0 +1,40 @@ +#!/bin/bash +#SBATCH --account=fmri +#SBATCH --partition=p5 +#SBATCH --job-name=rc_icn0.5 +#SBATCH --ntasks-per-node=1 +#SBATCH --nodes=1 +#SBATCH --gres=gpu:1 +#SBATCH --gpus-per-task=1 # Set to equal gres=gpu:#! +#SBATCH --time=48:00:00 # total run time limit (HH:MM:SS) +#SBATCH -e slurms/%j.err # first create a "slurms" folder in current directory to store logs +#SBATCH -o slurms/%j.out +#SBATCH --comment=medarc +#SBATCH --no-requeue +#SBATCH --qos=normal + +export NUM_GPUS=1 # Set to equal gres=gpu:#! +echo NUM_GPUS=$NUM_GPUS + +source ~/.bashrc +source /admin/home-ckadirt/mindeye/bin/activate +cd /weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/ + +jupyter nbconvert reconstruct_sklearn.ipynb --to python +if [ $? -ne 0 ]; then + echo "Error: Conversion of ipynb to Python failed. Exiting." + exit 1 +fi + +# Make sure another job doesnt use same port, here using random number +export MASTER_PORT=$((RANDOM % (19000 - 11000 + 1) + 11000)) +export HOSTNAMES=$(scontrol show hostnames "$SLURM_JOB_NODELIST") +export MASTER_ADDR=$(scontrol show hostnames "$SLURM_JOB_NODELIST" | head -n 1) +echo MASTER_ADDR=${MASTER_ADDR} +echo MASTER_PORT=${MASTER_PORT} +echo WORLD_SIZE=$((${SLURM_NNODES} * ${NUM_GPUS})) + +export MODULEPATH=/opt/amazon/modules/modulefiles:$MODULEPATH + +python reconstruct_sklearn.py subj1_0.5 + diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/dist_loss.py b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/dist_loss.py new file mode 100644 index 0000000000000000000000000000000000000000..91cb788fc4d36f76c1c76b7006f9ad4ec82fe3c3 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/dist_loss.py @@ -0,0 +1,87 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod + +import torch +import torch.nn as nn +from typing import Dict, Iterable +from bdpy.task.callback import CallbackHandler, BaseCallback +from bdpy.recon.torch.modules.critic import BaseCritic, LayerWiseAverageCritic + +class DistsLoss(LayerWiseAverageCritic): + def compare_layer( + self, feature: torch.Tensor, target_feature: torch.Tensor, layer_name: str, + eps: float = 1e-6, alpha: float = 1.0, beta: float = 1.0 + ) -> torch.Tensor: + if feature.ndim == 2: + # feature.shape = (batch_size, feature_dim) + # target_feature.shape = (batch_size, feature_dim) + feature_mean = feature.mean(dim=1, keepdim=True) # (batch_size, 1) + target_feature_mean = target_feature.mean(dim=1, keepdim=True) # (batch_size, 1) + feature_var = ((feature - feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1) + target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1) + cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=1, keepdim=True) # (batch_size, 1) + else: + # feature.shape = (batch_size, channel, height, width) + # target_feature.shape = (batch_size, channel, height, width) + feature_mean = feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + target_feature_mean = target_feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + feature_var = ((feature - feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + #cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + cov = (feature * target_feature).mean(dim=[2,3], keepdim=True) - feature_mean * target_feature_mean + s1 = (2 * feature_mean * target_feature_mean + eps) / (feature_mean ** 2 + target_feature_mean ** 2 + eps) # (batch_size, ...) + s2 = (2 * cov + eps) / (feature_var + target_feature_var + eps) # (batch_size, ...) + + return - (alpha * s1 + beta * s2).mean(dim=tuple(range(1, s1.ndim))) / (alpha + beta) + +class LayerWiseDistsLoss(LayerWiseAverageCritic): + def __init__(self, callbacks: BaseCallback | Iterable[BaseCallback] | None =None, alpha_dict: Dict = {}, beta_dict: Dict= {}) -> None: + super(LayerWiseDistsLoss, self).__init__(callbacks) + self.alpha_dict = alpha_dict + self.beta_dict = beta_dict + def compare_layer( + self, feature: torch.Tensor, target_feature: torch.Tensor, layer_name: str,eps: float = 1e-6, + + ) -> torch.Tensor: + if feature.ndim == 2: + # feature.shape = (batch_size, feature_dim) + # target_feature.shape = (batch_size, feature_dim) + feature_mean = feature.mean(dim=1, keepdim=True) # (batch_size, 1) + target_feature_mean = target_feature.mean(dim=1, keepdim=True) # (batch_size, 1) + feature_var = ((feature - feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1) + target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1) + cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=1, keepdim=True) # (batch_size, 1) + else: + # feature.shape = (batch_size, channel, height, width) + # target_feature.shape = (batch_size, channel, height, width) + feature_mean = feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + target_feature_mean = target_feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + feature_var = ((feature - feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + #cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1) + cov = (feature * target_feature).mean(dim=[2,3], keepdim=True) - feature_mean * target_feature_mean + s1 = (2 * feature_mean * target_feature_mean + eps) / (feature_mean ** 2 + target_feature_mean ** 2 + eps) # (batch_size, ...) + s2 = (2 * cov + eps) / (feature_var + target_feature_var + eps) # (batch_size, ...) + + return - (self.alpha_dict[layer_name] * s1 + self.beta_dict[layer_name] * s2).mean(dim=tuple(range(1, s1.ndim))) / (self.alpha_dict[layer_name] + self.beta_dict[layer_name]) + + +class CombinationLoss(LayerWiseAverageCritic): + """Combination of other Critics""" + + def __init__(self, critics: list[BaseCritic], weights: list[float]) -> None: + super().__init__() + self.critics = critics + self.weights = weights + + def compare_layer( + self, feature: torch.Tensor, target_feature: torch.Tensor, layer_name: str + ) -> torch.Tensor: + loss = 0.0 + for critic, weight in zip(self.critics, self.weights): + loss += weight * critic.compare_layer(feature, target_feature, layer_name) + return loss + + + \ No newline at end of file diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/generated_image.jpg b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/generated_image.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f7f96de01ddd0f38b7551b833ca53af5c94a26e3 Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/generated_image.jpg differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/im2.png b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/im2.png new file mode 100644 index 0000000000000000000000000000000000000000..c0bf1bd6f6110475e85d4a0a8b208eac47dbde77 Binary files /dev/null and b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/im2.png differ diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/recon_bdpy_icnn_image_AdamW_dist.py b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/recon_bdpy_icnn_image_AdamW_dist.py new file mode 100644 index 0000000000000000000000000000000000000000..d9241c7a3696d257bd19f13b5ca57a53df4391a6 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/recon_bdpy_icnn_image_AdamW_dist.py @@ -0,0 +1,352 @@ +# %% +from __future__ import annotations +from pathlib import Path +from typing import Callable, Iterator +import os + +import hdf5storage +import numpy as np +import scipy.io as sio +from itertools import product +from PIL import Image +from hydra.utils import to_absolute_path + +import torch +import torch.optim as optim +import torch.nn as nn +from torch.utils.data import Dataset +from torch.utils.data import DataLoader + +from itertools import chain +from functools import partial + +from bdpy.recon.utils import normalize_image, clip_extreme +from bdpy.dl.torch.models import VGG19, layer_map, model_factory +from bdpy.pipeline.config import init_hydra_cfg +from bdpy.dl.torch.dataset import ImageDataset, FeaturesDataset, DecodedFeaturesDataset, RenameFeatureKeys +from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain + +from bdpy.recon.torch.task import FeatureInversionTask +from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE +from bdpy.recon.torch.modules.latent import ArbitraryLatent +from bdpy.recon.torch.modules.generator import BaseGenerator, FrozenGenerator + +import sys +sys.path.append('./analysis/1_case_study/image-reconstruction/iCNN') +from dist_loss import DistsLoss, CombinationLoss, LayerWiseDistsLoss + +# %% +def load_target_statistics( + center_path_template: str, scale_path: str, layer_path_names: list[str] +) -> tuple[dict[str, np.ndarray], dict[str, float]]: + target_center = {} + for layer_path_name in layer_path_names: + center_path = center_path_template.format(layer_path_name=layer_path_name) + target_center[layer_path_name] = hdf5storage.loadmat(center_path)["y_mean"] + target_scale = sio.loadmat(scale_path) + return target_center, target_scale + +class BoundedLatent(ArbitraryLatent): + def __init__(self, shape: tuple[int, ...], init_fn: Callable[[torch.Tensor], None], + upperbound: torch.Tensor, lowerbound: torch.Tensor) -> None: + + super().__init__(shape, init_fn) + self.upperbound = upperbound + self.lowerbound = lowerbound + + def generate(self) -> torch.Tensor: + # clip upper bound + if self.upperbound is not None: + for latent in self._latent: + latent.data = torch.clamp(latent, max=self.upperbound) + # clip lower bound + if self.lowerbound is not None: + for latent in self._latent: + latent.data = torch.clamp(latent, min=self.lowerbound) + + return latent + +# %% + +class ScaleFeatures: + def __init__( + self, + target_center: dict[str, np.ndarray], + target_scale: dict[str, float], + ddof: int = 1, + ) -> None: + self.target_center = target_center + self.target_scale = target_scale + self.ddof = ddof + + def __call__(self, features: dict[str, np.ndarray]) -> dict[str, np.ndarray]: + scaled_features = {} + for layer_name, feature in features.items(): + axes_to_reduce = tuple(range(1, feature.ndim)) if feature.ndim > 1 else (0,) + feature_scale = feature.std( + axis=axes_to_reduce, keepdims=True, ddof=self.ddof + ) + # NOTE: mean across channels + feature_scale = feature_scale.mean(axis=0, keepdims=True) + + scaled_feature = ( + feature - self.target_center[layer_name] + ) / feature_scale * self.target_scale[layer_name] + self.target_center[ + layer_name + ] + scaled_features[layer_name] = scaled_feature[0] + return scaled_features + +class SimpleStackDataset(Dataset): + def __init__(self, *datasets): + self.datasets = datasets + self._length = min(len(d) for d in datasets) # 最小の長さを使用して、すべてのデータセットを同じ長さに保つ + + def __getitem__(self, index): + return tuple(dataset[index] for dataset in self.datasets) + + def __len__(self): + return self._length +# %% + +# Setup results directory ------------------------------------------------ + +def run_icnn_reconstruction(config): + #assets_root_path = Path(f"./results/playground") + assets_root_path = Path(cfg.output.path) + os.makedirs(assets_root_path,exist_ok=True) + + + # %% + alpha = 4.0 + beta = 1.0 + dists_alpha = { + 'classifier[6]': 0.06661523244761258, + 'classifier[3]': 0.03871265134313895, + 'classifier[0]': 0.000629031742843134, + 'features[34]': 0.5310432634795051, + 'features[32]': 0.01975314483213067, + 'features[30]': 0.714010791024532, + 'features[28]': 0.08536182104218124, + 'features[25]': 0.030798346318926202, + 'features[23]': 0.004025735147052829, + 'features[21]': 0.0021716504774059618, + 'features[19]': 0.02880295296139471, + 'features[16]': 0.014169279688225732, + 'features[14]': 0.00019573287900056505, + 'features[12]': 0.0004887923569668929, + 'features[10]': 0.006857140440209977, + 'features[7]': 0.08084213863904581, + 'features[5]': 0.00024056214287883663, + 'features[2]': 0.003886371646003732, + 'features[0]': 0.009952859626673973, + } + dists_beta = { + 'classifier[6]': 0.008304840607742099, + 'classifier[3]': 0.044481711593671994, + 'classifier[0]': 0.038457933646483915, + 'features[34]': 0.0012780195483159135, + 'features[32]': 0.0018775814111698145, + 'features[30]': 0.5074163077203029, + 'features[28]': 0.002337825161420017, + 'features[25]': 0.7100372437615771, + 'features[23]': 0.5166895849277143, + 'features[21]': 0.03998274022264576, + 'features[19]': 0.04328555659354602, + 'features[16]': 0.024733951474856346, + 'features[14]': 0.0004859871528150426, + 'features[12]': 0.039778524165843814, + 'features[10]': 0.0002639605292406699, + 'features[7]': 0.02472305546171304, + 'features[5]': 0.12888847991806807, + 'features[2]': 0.008627502425502372, + 'features[0]': 0.000865427897168344 + } + batch_size = 1 + # %% + + encoder_name = cfg.encoder.name + device = torch.device('cuda:0') + dtype= torch.float32 + + feature_network = VGG19() + + feature_network.load_state_dict(torch.load(cfg.encoder.parameters_file)) + feature_network.to(device) + # %% + to_layer_name = layer_map(encoder_name) + # exclude the key starting from "key" + to_layer_name = {k: v for k, v in to_layer_name.items() if "relu" not in k} + to_path_name = { + layer_name: layer_path_name + for layer_name, layer_path_name in zip( + to_layer_name.values(), to_layer_name.keys() + ) + } + layer_names = list(to_layer_name.values()) + # Remove relu layer + layer_names = [layer_name for layer_name in layer_names if "relu" not in to_path_name[layer_name]] + + # %% + encoder = build_encoder(feature_network, layer_names, + domain= ComposedDomain([image_domain.BdPyVGGDomain(device=device,dtype=dtype), + image_domain.FixedResolutionDomain((224, 224))]), + + ) + # %% + #generator = build_generator(...) + + generator_network = model_factory(cfg.generator.name) + generator_network.load_state_dict(torch.load(cfg.generator.parameters_file)) + generator_network.to(device) + + + # %% + + latent_upperbound = np.loadtxt(cfg.generator.latent_upper_bound_file, delimiter=" ") + latent_feat_num = cfg.generator.latent_shape + latent_upperbound = torch.tensor(latent_upperbound).to(device, dtype=dtype) + # %% + generator = build_generator(generator_network, image_domain.BdPyVGGDomain(device=device, dtype=dtype) ) + # %% + + latent = BoundedLatent((batch_size, latent_feat_num[0]), partial(torch.nn.init.normal_, mean=0.0, std=0.01), + latent_upperbound, 0) + latent.to(device) + latent.reset_states() + # %% + critic = CombinationLoss([LayerWiseDistsLoss(alpha_dict=dists_alpha, beta_dict=dists_beta), TargetNormalizedMSE()], [alpha, beta]) + # critic_image = TVloss(alpha=1e-07) + + optimizer = optim.AdamW(latent.parameters(), lr=0.1) + scheduler = None + # %% + pipeline = FeatureInversionTask( + encoder=encoder, + generator=generator, + latent = latent, + critic=critic, + # critic_image=critic_image, + optimizer=optimizer, + scheduler=scheduler, + num_iterations=800, + + ) + # %% + + # %% + data_root_path = Path("./data") / cfg.decoded_features.name + + feature_root_path = ( + Path("./data")/ cfg.decoded_features.name / "derivatives" / "features" / cfg.decoded_features.model_name + ) + decoded_feature_root_path = ( + + Path("./data") / cfg.decoded_features.name + / "derivatives" + / "decoded_features" + / cfg.decoded_features.decoder_setting + / "decoded_features" + / cfg.decoded_features.model_name + ) + + # %% + # Load target statistics + print("Load target statistics") + target_path_template = ( + f"./data/{cfg.decoded_features.training_dataset}/derivatives" + f"/feature_decoders/{cfg.decoded_features.decoder_setting}/{cfg.decoded_features.model_name}" + "/{layer_path_name}" + f"/{cfg.decoded_features.subjects[0]}/{cfg.decoded_features.rois[0]}" + "/model/y_mean.mat" + ) + scale_path = ( + "./data/models_shared/caffe/VGG_ILSVRC_19_layers" + "/estimated_feat_std" + "/estimated_cnn_feat_std_VGG_ILSVRC_19_layers_ImgSize_224x224_chwise_dof1.mat" + ) + target_center, target_scale = load_target_statistics( + target_path_template, scale_path, layer_path_names=list(to_layer_name.keys()) + ) + + # %% + # Load features + print("Load features") + def compose(*transforms): + def _compose(x): + for transform in transforms: + x = transform(x) + return x + + return _compose + + #images_dataset = ImageDataset( + # root_path=data_root_path / "source", + # extension=cfg.evaluation.true_image.ext, + # stimulus_names=cfg.target_images, + + # ) + target_images = np.array(cfg.target_images) + for subject, roi in product(cfg.decoded_features.subjects, cfg.decoded_features.rois): + + + decoded_features_dataset = DecodedFeaturesDataset( + root_path=decoded_feature_root_path, + layer_path_names=list(to_layer_name.keys()), + subject_id=subject, + roi=roi, + #timulus_names=images_dataset._stimulus_names, + stimulus_names=target_images, + transform=compose( + ScaleFeatures(target_center, target_scale), + RenameFeatureKeys(to_layer_name), + ), + ) + # %% + #stacked_dataset = SimpleStackDataset( + # images_dataset, decoded_features_dataset + # ) + + + data_loader = DataLoader( + decoded_features_dataset, + batch_size=batch_size, + num_workers=1, + ) + # %% + for idx, decoded_features in enumerate( + data_loader + ): + stimulus_names = cfg.target_images[idx] + if type(stimulus_names) is str: + stimulus_names = stimulus_names, + print(stimulus_names) + pipeline.reset_states() + target_features = { + k: v.to(device=device, dtype=dtype) for k, v in decoded_features.items() + } + generated_images = image_domain.PILDomainWithExplicitCrop().receive(pipeline(target_features)) + for i, stimulus_name in enumerate(stimulus_names): + image = generated_images[i].detach().cpu().numpy() + pp_image = normalize_image(clip_extreme(image, pct=4)) + # crop the image to 224x224 + h, w, _ = image.shape + h_start = (h - 224) // 2 + w_start = (w - 224) // 2 + pp_image = pp_image[h_start : h_start + 224, w_start : w_start + 224] + + image = Image.fromarray(pp_image) + savedir = assets_root_path / "decoded_features" + savedir.mkdir(parents=True, exist_ok=True) + image.save(savedir / f"{stimulus_name}.tiff") + +# %% + +# Entry point ################################################################ + +if __name__ == "__main__": + + cfg = init_hydra_cfg() + run_icnn_reconstruction(cfg) + + \ No newline at end of file diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct.ipynb b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..272ac6672fa241fab67d4f39e36d4d8c0afe7ab0 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct.ipynb @@ -0,0 +1,1148 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 3, + "id": "ba7e174c-c3ec-4170-a863-c0014d479da0", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "from pathlib import Path\n", + "from typing import Callable, Iterator\n", + "import os \n", + "\n", + "import hdf5storage\n", + "import numpy as np\n", + "import scipy.io as sio\n", + "from itertools import product\n", + "from PIL import Image\n", + "from hydra.utils import to_absolute_path\n", + "\n", + "import torch\n", + "import torch.optim as optim\n", + "import torch.nn as nn\n", + "from torch.utils.data import Dataset\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from itertools import chain\n", + "from functools import partial\n", + "\n", + "from bdpy.recon.utils import normalize_image, clip_extreme\n", + "from bdpy.dl.torch.models import VGG19, layer_map, model_factory\n", + "from bdpy.pipeline.config import init_hydra_cfg\n", + "from bdpy.dl.torch.dataset import ImageDataset, FeaturesDataset, DecodedFeaturesDataset, RenameFeatureKeys\n", + "from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain\n", + "\n", + "from bdpy.recon.torch.task import FeatureInversionTask\n", + "from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE\n", + "from bdpy.recon.torch.modules.latent import ArbitraryLatent\n", + "from bdpy.recon.torch.modules.generator import BaseGenerator, FrozenGenerator\n", + "\n", + "import sys\n", + "sys.path.append('./analysis/1_case_study/image-reconstruction/iCNN')\n", + "from dist_loss import DistsLoss, CombinationLoss, LayerWiseDistsLoss\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1e9a7d65-3a09-4ee4-9338-205ac4c3c931", + "metadata": {}, + "outputs": [], + "source": [ + "def load_target_statistics(\n", + " center_path_template: str, scale_path: str, layer_path_names: list[str]\n", + ") -> tuple[dict[str, np.ndarray], dict[str, float]]:\n", + " target_center = {}\n", + " for layer_path_name in layer_path_names:\n", + " center_path = center_path_template.format(layer_path_name=layer_path_name)\n", + " target_center[layer_path_name] = hdf5storage.loadmat(center_path)[\"y_mean\"]\n", + " target_scale = sio.loadmat(scale_path)\n", + " return target_center, target_scale\n", + "\n", + "\n", + "class BoundedLatent(ArbitraryLatent):\n", + " def __init__(self, shape: tuple[int, ...], init_fn: Callable[[torch.Tensor], None],\n", + " upperbound: torch.Tensor, lowerbound: torch.Tensor) -> None:\n", + " \n", + " super().__init__(shape, init_fn)\n", + " self.upperbound = upperbound\n", + " self.lowerbound = lowerbound\n", + "\n", + " def generate(self) -> torch.Tensor:\n", + " # Clip upper bound\n", + " if self.upperbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, max=self.upperbound)\n", + " # Clip lower bound \n", + " if self.lowerbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, min=self.lowerbound)\n", + " \n", + " return latent\n", + "\n", + "\n", + "class ScaleFeatures:\n", + " def __init__(\n", + " self,\n", + " target_center: dict[str, np.ndarray],\n", + " target_scale: dict[str, float],\n", + " ddof: int = 1,\n", + " ) -> None:\n", + " self.target_center = target_center\n", + " self.target_scale = target_scale\n", + " self.ddof = ddof\n", + "\n", + " def __call__(self, features: dict[str, np.ndarray]) -> dict[str, np.ndarray]:\n", + " scaled_features = {}\n", + " for layer_name, feature in features.items():\n", + " axes_to_reduce = tuple(range(1, feature.ndim)) if feature.ndim > 1 else (0,)\n", + " feature_scale = feature.std(\n", + " axis=axes_to_reduce, keepdims=True, ddof=self.ddof\n", + " )\n", + " # NOTE: mean across channels\n", + " feature_scale = feature_scale.mean(axis=0, keepdims=True)\n", + "\n", + " scaled_feature = (\n", + " feature - self.target_center[layer_name]\n", + " ) / feature_scale * self.target_scale[layer_name] + self.target_center[\n", + " layer_name\n", + " ]\n", + " scaled_features[layer_name] = scaled_feature[0]\n", + " return scaled_features\n", + "\n", + "\n", + "class SimpleStackDataset(Dataset):\n", + " def __init__(self, *datasets):\n", + " self.datasets = datasets\n", + " self._length = min(len(d) for d in datasets) # Use the smallest length to keep all datasets the same length\n", + "\n", + " def __getitem__(self, index):\n", + " return tuple(dataset[index] for dataset in self.datasets)\n", + "\n", + " def __len__(self):\n", + " return self._length\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "cdf223be-d4f9-49ab-8a04-acb24714ae9c", + "metadata": {}, + "outputs": [], + "source": [ + "import h5py\n", + "def run_icnn_reconstruction(cfg):\n", + " # Setup results directory\n", + " # assets_root_path = Path(f\"./results/playground\")\n", + " assets_root_path = Path(cfg.output.path)\n", + " os.makedirs(assets_root_path, exist_ok=True)\n", + "\n", + " alpha = 4.0\n", + " beta = 1.0\n", + " dists_alpha = {\n", + " 'classifier[6]': 0.06661523244761258,\n", + " 'classifier[3]': 0.03871265134313895,\n", + " 'classifier[0]': 0.000629031742843134,\n", + " 'features[34]': 0.5310432634795051,\n", + " 'features[32]': 0.01975314483213067,\n", + " 'features[30]': 0.714010791024532,\n", + " 'features[28]': 0.08536182104218124,\n", + " 'features[25]': 0.030798346318926202,\n", + " 'features[23]': 0.004025735147052829,\n", + " 'features[21]': 0.0021716504774059618,\n", + " 'features[19]': 0.02880295296139471,\n", + " 'features[16]': 0.014169279688225732,\n", + " 'features[14]': 0.00019573287900056505,\n", + " 'features[12]': 0.0004887923569668929,\n", + " 'features[10]': 0.006857140440209977,\n", + " 'features[7]': 0.08084213863904581,\n", + " 'features[5]': 0.00024056214287883663,\n", + " 'features[2]': 0.3886371646003732,\n", + " 'features[0]': 0.9952859626673973,\n", + " }\n", + " dists_beta = {\n", + " 'classifier[6]': 0.008304840607742099,\n", + " 'classifier[3]': 0.044481711593671994, \n", + " 'classifier[0]': 0.038457933646483915, \n", + " 'features[34]': 0.0012780195483159135, \n", + " 'features[32]': 0.0018775814111698145,\n", + " 'features[30]': 0.5074163077203029, \n", + " 'features[28]': 0.002337825161420017, \n", + " 'features[25]': 0.7100372437615771,\n", + " 'features[23]': 0.5166895849277143,\n", + " 'features[21]': 0.03998274022264576,\n", + " 'features[19]': 0.04328555659354602,\n", + " 'features[16]': 0.024733951474856346,\n", + " 'features[14]': 0.0004859871528150426,\n", + " 'features[12]': 0.039778524165843814,\n", + " 'features[10]': 0.0002639605292406699,\n", + " 'features[7]': 0.02472305546171304,\n", + " 'features[5]': 0.12888847991806807,\n", + " 'features[2]': 0.8627502425502372,\n", + " 'features[0]': 0.0865427897168344\n", + " }\n", + " batch_size = 1\n", + "\n", + " encoder_name = cfg.encoder.name\n", + " device = torch.device('cuda:0')\n", + " dtype= torch.float32\n", + "\n", + " feature_network = VGG19()\n", + "\n", + " feature_network.load_state_dict(torch.load(cfg.encoder.parameters_file))\n", + " feature_network.to(device)\n", + " \n", + " to_layer_name = layer_map(encoder_name)\n", + " \n", + " # Exclude the key starting from \"relu\"\n", + " to_layer_name = {k: v for k, v in to_layer_name.items() if \"relu\" not in k}\n", + " to_path_name = {\n", + " layer_name: layer_path_name\n", + " for layer_name, layer_path_name in zip(\n", + " to_layer_name.values(), to_layer_name.keys()\n", + " )\n", + " }\n", + " layer_names = list(to_layer_name.values())\n", + " # Remove relu layer\n", + " layer_names = [layer_name for layer_name in layer_names if \"relu\" not in to_path_name[layer_name]]\n", + " print(layer_names)\n", + " layer_names = ['features[0]', 'features[2]']\n", + " \n", + " encoder = build_encoder(\n", + " feature_network, \n", + " layer_names, \n", + " domain=ComposedDomain([\n", + " image_domain.BdPyVGGDomain(device=device, dtype=dtype), \n", + " image_domain.FixedResolutionDomain((224, 224))\n", + " ]),\n", + " )\n", + "\n", + " # Generator setup\n", + " generator_network = model_factory(cfg.generator.name)\n", + " generator_network.load_state_dict(torch.load(cfg.generator.parameters_file))\n", + " generator_network.to(device)\n", + "\n", + " latent_upperbound = np.loadtxt(cfg.generator.latent_upper_bound_file, delimiter=\" \")\n", + " latent_feat_num = cfg.generator.latent_shape\n", + " latent_upperbound = torch.tensor(latent_upperbound).to(device, dtype=dtype)\n", + "\n", + " generator = build_generator(\n", + " generator_network, \n", + " image_domain.BdPyVGGDomain(device=device, dtype=dtype)\n", + " )\n", + "\n", + " latent = BoundedLatent(\n", + " (batch_size, latent_feat_num[0]), \n", + " partial(torch.nn.init.normal_, mean=0.0, std=0.01), \n", + " latent_upperbound, \n", + " 0\n", + " )\n", + " latent.to(device)\n", + " latent.reset_states()\n", + "\n", + " critic = CombinationLoss(\n", + " [\n", + " LayerWiseDistsLoss(alpha_dict=dists_alpha, beta_dict=dists_beta), \n", + " TargetNormalizedMSE()\n", + " ], \n", + " [alpha, beta]\n", + " )\n", + " # critic_image = TVloss(alpha=1e-07)\n", + "\n", + " optimizer = optim.AdamW(latent.parameters(), lr=0.1)\n", + " scheduler = None\n", + "\n", + " pipeline = FeatureInversionTask(\n", + " encoder=encoder,\n", + " generator=generator,\n", + " latent=latent,\n", + " critic=critic,\n", + " # critic_image=critic_image,\n", + " optimizer=optimizer,\n", + " scheduler=scheduler,\n", + " num_iterations=100,\n", + " )\n", + "\n", + " data_root_path = Path(\"./data\") / cfg.decoded_features.name\n", + " \n", + " feature_root_path = (\n", + " Path(\"./data\") / cfg.decoded_features.name / \"derivatives\" / \"features\" / cfg.decoded_features.model_name\n", + " )\n", + " decoded_feature_root_path = (\n", + " Path(\"./data\") / cfg.decoded_features.name\n", + " / \"derivatives\"\n", + " / \"decoded_features\"\n", + " / cfg.decoded_features.decoder_setting\n", + " / \"decoded_features\"\n", + " / cfg.decoded_features.model_name\n", + " )\n", + "\n", + " # Load target statistics\n", + " print(\"Load target statistics\")\n", + " target_path_template = (\n", + " f\"./data/{cfg.decoded_features.training_dataset}/derivatives\"\n", + " f\"/feature_decoders/{cfg.decoded_features.decoder_setting}/{cfg.decoded_features.model_name}\"\n", + " \"/{layer_path_name}\"\n", + " f\"/{cfg.decoded_features.subjects[0]}/{cfg.decoded_features.rois[0]}\"\n", + " \"/model/y_mean.mat\"\n", + " )\n", + " scale_path = (\n", + " \"./data/models_shared/caffe/VGG_ILSVRC_19_layers\"\n", + " \"/estimated_feat_std\"\n", + " \"/estimated_cnn_feat_std_VGG_ILSVRC_19_layers_ImgSize_224x224_chwise_dof1.mat\"\n", + " )\n", + " target_center, target_scale = 0.45, 1\n", + " # load_target_statistics(\n", + " # target_path_template, scale_path, layer_path_names=list(to_layer_name.keys())\n", + " # )\n", + "\n", + " # Load features\n", + " print(\"Load features\")\n", + " def compose(*transforms):\n", + " def _compose(x):\n", + " for transform in transforms:\n", + " x = transform(x)\n", + " return x\n", + " return _compose\n", + "\n", + " # images_dataset = ImageDataset(\n", + " # root_path=data_root_path / \"source\",\n", + " # extension=cfg.evaluation.true_image.ext,\n", + " # stimulus_names=cfg.target_images,\n", + " # )\n", + "\n", + " target_images = np.array(cfg.target_images)\n", + " for subject, roi in product(cfg.decoded_features.subjects, cfg.decoded_features.rois):\n", + " \n", + " # decoded_features_dataset = DecodedFeaturesDataset(\n", + " # root_path=decoded_feature_root_path,\n", + " # layer_path_names=list(to_layer_name.keys()),\n", + " # subject_id=subject,\n", + " # roi=roi,\n", + " # # stimulus_names=images_dataset._stimulus_names,\n", + " # stimulus_names=target_images,\n", + " # transform=compose(\n", + " # ScaleFeatures(target_center, target_scale),\n", + " # RenameFeatureKeys(to_layer_name),\n", + " # ),\n", + " # )\n", + "\n", + " # stacked_dataset = SimpleStackDataset(\n", + " # images_dataset, decoded_features_dataset\n", + " # )\n", + " \n", + " \n", + " features_dir=os.path.join(\n", + " '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/VGG19_ILSVRC_19_layers/'\n", + " # '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/vgg19_torchvision/'\n", + " )\n", + " yf = h5py.File(os.path.join(features_dir, 'features[0]', f'{1}.h5'), 'r')\n", + " features = yf['dataset']\n", + " decoded_features_dataset = features\n", + " data_loader = DataLoader(\n", + " decoded_features_dataset,\n", + " batch_size=batch_size,\n", + " num_workers=1,\n", + " )\n", + "\n", + " yf2 = h5py.File(os.path.join(features_dir, 'features[2]', f'{1}.h5'), 'r')\n", + " features2 = yf2['dataset']\n", + " decoded_features_dataset2 = features2\n", + " data_loader2 = DataLoader(\n", + " decoded_features_dataset2,\n", + " batch_size=batch_size,\n", + " num_workers=1,\n", + " )\n", + " \n", + " for idx, decoded_features in enumerate(data_loader):\n", + " print(decoded_features)\n", + " \n", + " # Calculate mean and standard deviation\n", + " mean_val = decoded_features.mean()\n", + " std_val = decoded_features.std()\n", + " \n", + " # Normalize using mean and standard deviation\n", + " decoded_features = (decoded_features - mean_val) / std_val\n", + " \n", + " stimulus_names = cfg.target_images[idx]\n", + " if isinstance(stimulus_names, str):\n", + " stimulus_names = (stimulus_names,)\n", + " print(stimulus_names)\n", + " print(decoded_features_dataset2[idx].shape)\n", + " pipeline.reset_states()\n", + " target_features = {\n", + " 'features[0]': decoded_features.to(device=device, dtype=dtype), 'features[2]': torch.Tensor(decoded_features_dataset2[idx]).unsqueeze(0).to(device=device, dtype=dtype)\n", + " }\n", + " generated_images = image_domain.PILDomainWithExplicitCrop().receive(pipeline(target_features))\n", + " # if idx == 2: return generated_images\n", + " # for i, stimulus_name in enumerate(stimulus_names):\n", + " image = generated_images[0].detach().cpu().numpy()\n", + " pp_image = normalize_image(clip_extreme(image, pct=4))\n", + " # Crop the image to 224x224\n", + " h, w, _ = image.shape\n", + " h_start = (h - 224) // 2\n", + " w_start = (w - 224) // 2\n", + " pp_image = pp_image[h_start : h_start + 224, w_start : w_start + 224]\n", + "\n", + " image = Image.fromarray(pp_image)\n", + " savedir = \"./recons\"\n", + " os.makedirs(savedir, exist_ok=True)\n", + " image.save(os.path.join(savedir, f\"{idx}.png\"))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "08a1c331-9ddb-4bd7-afe0-d4869f1cffa5", + "metadata": {}, + "outputs": [], + "source": [ + "import yaml\n", + "\n", + "config_path = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/config/recon_icnn_nsd_vgg19_relu7generator_AdamW_1000iterl_decoded.yaml' # Replace with your config file path\n", + "# %% [Block 4]\n", + "def load_config(config_path):\n", + " '''Load configuration file.'''\n", + " with open(config_path, 'r') as file:\n", + " config = yaml.safe_load(file)\n", + " return config\n", + "\n", + "# %%\n", + "# Load config and extract features\n", + "config = load_config(config_path)\n", + "\n", + "from types import SimpleNamespace\n", + "\n", + "# Recursive function to convert a dictionary to a SimpleNamespace\n", + "def dict_to_namespace(d):\n", + " for key, value in d.items():\n", + " if isinstance(value, dict):\n", + " d[key] = dict_to_namespace(value)\n", + " return SimpleNamespace(**d)\n", + "\n", + "# Now convert your config dictionary\n", + "config = dict_to_namespace(config)\n", + "\n", + "enc_conf = load_config('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/config/encoder/vgg_ilsvrc_19_layers_pytorch.yaml')\n", + "gen_conf = load_config('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/config/generator/relu7generator.yaml')\n", + "config.encoder = dict_to_namespace(enc_conf)\n", + "config.generator = dict_to_namespace(gen_conf)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "97420b87-e456-4546-8a2b-403cafb53b04", + "metadata": {}, + "outputs": [], + "source": [ + "# # Entry point ################################################################\n", + "\n", + "\n", + "# gi = run_icnn_reconstruction(config)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "656e44ba-b395-4044-b58b-7078688bcc98", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['features[0]', 'features[2]', 'features[5]', 'features[7]', 'features[10]', 'features[12]', 'features[14]', 'features[16]', 'features[19]', 'features[21]', 'features[23]', 'features[25]', 'features[28]', 'features[30]', 'features[32]', 'features[34]', 'classifier[0]', 'classifier[3]', 'classifier[6]']\n" + ] + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/admin/home-ckadirt/mindeye/lib/python3.11/site-packages/bdpy/dl/torch/domain/core.py:90: RuntimeWarning: FixedResolutionDomain is an irreversible domain. It does not guarantee the reversibility of `send` and `receive` methods. Please use the combination of `send` and `receive` methods with caution.\n", + " warnings.warn(\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Load target statistics\n", + "Load features\n" + ] + }, + { + "ename": "KeyError", + "evalue": "\"Unable to synchronously open object (object 'dataset' doesn't exist)\"", + "output_type": "error", + "traceback": [ + "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", + "\u001b[0;31mKeyError\u001b[0m Traceback (most recent call last)", + "Cell \u001b[0;32mIn[6], line 205\u001b[0m\n\u001b[1;32m 200\u001b[0m features_dir\u001b[38;5;241m=\u001b[39mos\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mjoin(\n\u001b[1;32m 201\u001b[0m \u001b[38;5;124m'\u001b[39m\u001b[38;5;124m/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/VGG19_ILSVRC_19_layers/\u001b[39m\u001b[38;5;124m'\u001b[39m\n\u001b[1;32m 202\u001b[0m \u001b[38;5;66;03m# '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/vgg19_torchvision/'\u001b[39;00m\n\u001b[1;32m 203\u001b[0m )\n\u001b[1;32m 204\u001b[0m yf \u001b[38;5;241m=\u001b[39m h5py\u001b[38;5;241m.\u001b[39mFile(os\u001b[38;5;241m.\u001b[39mpath\u001b[38;5;241m.\u001b[39mjoin(features_dir, \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mfeatures[0]\u001b[39m\u001b[38;5;124m'\u001b[39m, \u001b[38;5;124mf\u001b[39m\u001b[38;5;124m'\u001b[39m\u001b[38;5;132;01m{\u001b[39;00m\u001b[38;5;241m1\u001b[39m\u001b[38;5;132;01m}\u001b[39;00m\u001b[38;5;124m.h5\u001b[39m\u001b[38;5;124m'\u001b[39m), \u001b[38;5;124m'\u001b[39m\u001b[38;5;124mr\u001b[39m\u001b[38;5;124m'\u001b[39m)\n\u001b[0;32m--> 205\u001b[0m features \u001b[38;5;241m=\u001b[39m \u001b[43myf\u001b[49m\u001b[43m[\u001b[49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[38;5;124;43mdataset\u001b[39;49m\u001b[38;5;124;43m'\u001b[39;49m\u001b[43m]\u001b[49m\n\u001b[1;32m 206\u001b[0m decoded_features_dataset \u001b[38;5;241m=\u001b[39m features\n\u001b[1;32m 207\u001b[0m data_loader \u001b[38;5;241m=\u001b[39m DataLoader(\n\u001b[1;32m 208\u001b[0m decoded_features_dataset,\n\u001b[1;32m 209\u001b[0m batch_size\u001b[38;5;241m=\u001b[39mbatch_size,\n\u001b[1;32m 210\u001b[0m num_workers\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m,\n\u001b[1;32m 211\u001b[0m )\n", + "File \u001b[0;32mh5py/_objects.pyx:54\u001b[0m, in \u001b[0;36mh5py._objects.with_phil.wrapper\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mh5py/_objects.pyx:55\u001b[0m, in \u001b[0;36mh5py._objects.with_phil.wrapper\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/h5py/_hl/group.py:357\u001b[0m, in \u001b[0;36mGroup.__getitem__\u001b[0;34m(self, name)\u001b[0m\n\u001b[1;32m 355\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mValueError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mInvalid HDF5 object reference\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 356\u001b[0m \u001b[38;5;28;01melif\u001b[39;00m \u001b[38;5;28misinstance\u001b[39m(name, (\u001b[38;5;28mbytes\u001b[39m, \u001b[38;5;28mstr\u001b[39m)):\n\u001b[0;32m--> 357\u001b[0m oid \u001b[38;5;241m=\u001b[39m \u001b[43mh5o\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mopen\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mid\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_e\u001b[49m\u001b[43m(\u001b[49m\u001b[43mname\u001b[49m\u001b[43m)\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlapl\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_lapl\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 358\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[1;32m 359\u001b[0m \u001b[38;5;28;01mraise\u001b[39;00m \u001b[38;5;167;01mTypeError\u001b[39;00m(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mAccessing a group is done with bytes or str, \u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 360\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mnot \u001b[39m\u001b[38;5;132;01m{}\u001b[39;00m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;241m.\u001b[39mformat(\u001b[38;5;28mtype\u001b[39m(name)))\n", + "File \u001b[0;32mh5py/_objects.pyx:54\u001b[0m, in \u001b[0;36mh5py._objects.with_phil.wrapper\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mh5py/_objects.pyx:55\u001b[0m, in \u001b[0;36mh5py._objects.with_phil.wrapper\u001b[0;34m()\u001b[0m\n", + "File \u001b[0;32mh5py/h5o.pyx:189\u001b[0m, in \u001b[0;36mh5py.h5o.open\u001b[0;34m()\u001b[0m\n", + "\u001b[0;31mKeyError\u001b[0m: \"Unable to synchronously open object (object 'dataset' doesn't exist)\"" + ] + } + ], + "source": [ + "cfg = config\n", + "assets_root_path = Path(cfg.output.path)\n", + "os.makedirs(assets_root_path, exist_ok=True)\n", + "\n", + "alpha = 4.0\n", + "beta = 1.0\n", + "dists_alpha = {\n", + " # 'classifier[6]': 0.06661523244761258,\n", + " # 'classifier[3]': 0.03871265134313895,\n", + " # 'classifier[0]': 0.000629031742843134,\n", + " # 'features[34]': 0.5310432634795051,\n", + " # 'features[32]': 0.01975314483213067,\n", + " # 'features[30]': 0.714010791024532,\n", + " # 'features[28]': 0.08536182104218124,\n", + " # 'features[25]': 0.030798346318926202,\n", + " # 'features[23]': 0.004025735147052829,\n", + " # 'features[21]': 0.0021716504774059618,\n", + " # 'features[19]': 0.02880295296139471,\n", + " # 'features[16]': 0.014169279688225732,\n", + " # 'features[14]': 0.00019573287900056505,\n", + " # 'features[12]': 0.0004887923569668929,\n", + " # 'features[10]': 0.006857140440209977,\n", + " # 'features[7]': 0.08084213863904581,\n", + " # 'features[5]': 0.00024056214287883663,\n", + " 'features[2]': 0.3886371646003732,\n", + " 'features[0]': 0.9952859626673973,\n", + "}\n", + "dists_beta = {\n", + " # 'classifier[6]': 0.008304840607742099,\n", + " # 'classifier[3]': 0.044481711593671994, \n", + " # 'classifier[0]': 0.038457933646483915, \n", + " # 'features[34]': 0.0012780195483159135, \n", + " # 'features[32]': 0.0018775814111698145,\n", + " # 'features[30]': 0.5074163077203029, \n", + " # 'features[28]': 0.002337825161420017, \n", + " # 'features[25]': 0.7100372437615771,\n", + " # 'features[23]': 0.5166895849277143,\n", + " # 'features[21]': 0.03998274022264576,\n", + " # 'features[19]': 0.04328555659354602,\n", + " # 'features[16]': 0.024733951474856346,\n", + " # 'features[14]': 0.0004859871528150426,\n", + " # 'features[12]': 0.039778524165843814,\n", + " # 'features[10]': 0.0002639605292406699,\n", + " # 'features[7]': 0.02472305546171304,\n", + " # 'features[5]': 0.12888847991806807,\n", + " 'features[2]': 0.8627502425502372,\n", + " 'features[0]': 0.0865427897168344\n", + "}\n", + "batch_size = 1\n", + "\n", + "encoder_name = cfg.encoder.name\n", + "device = torch.device('cuda:0')\n", + "dtype= torch.float32\n", + "\n", + "feature_network = VGG19()\n", + "\n", + "feature_network.load_state_dict(torch.load(cfg.encoder.parameters_file))\n", + "feature_network.to(device)\n", + "\n", + "to_layer_name = layer_map(encoder_name)\n", + "\n", + "# Exclude the key starting from \"relu\"\n", + "to_layer_name = {k: v for k, v in to_layer_name.items() if \"relu\" not in k}\n", + "to_path_name = {\n", + " layer_name: layer_path_name\n", + " for layer_name, layer_path_name in zip(\n", + " to_layer_name.values(), to_layer_name.keys()\n", + " )\n", + "}\n", + "layer_names = list(to_layer_name.values())\n", + "# Remove relu layer\n", + "layer_names = [layer_name for layer_name in layer_names if \"relu\" not in to_path_name[layer_name]]\n", + "print(layer_names)\n", + "layer_names = ['features[0]']\n", + "\n", + "encoder = build_encoder(\n", + " feature_network, \n", + " layer_names, \n", + " domain=ComposedDomain([\n", + " image_domain.BdPyVGGDomain(device=device, dtype=dtype), \n", + " image_domain.FixedResolutionDomain((224, 224))\n", + " ]),\n", + ")\n", + "\n", + "# Generator setup\n", + "generator_network = model_factory(cfg.generator.name)\n", + "generator_network.load_state_dict(torch.load(cfg.generator.parameters_file))\n", + "generator_network.to(device)\n", + "\n", + "latent_upperbound = np.loadtxt(cfg.generator.latent_upper_bound_file, delimiter=\" \")\n", + "latent_feat_num = cfg.generator.latent_shape\n", + "latent_upperbound = torch.tensor(latent_upperbound).to(device, dtype=dtype)\n", + "\n", + "generator = build_generator(\n", + " generator_network, \n", + " image_domain.BdPyVGGDomain(device=device, dtype=dtype)\n", + ")\n", + "\n", + "latent = BoundedLatent(\n", + " (batch_size, latent_feat_num[0]), \n", + " partial(torch.nn.init.normal_, mean=0.0, std=0.01), \n", + " latent_upperbound, \n", + " 0\n", + ")\n", + "latent.to(device)\n", + "latent.reset_states()\n", + "\n", + "critic = CombinationLoss(\n", + " [\n", + " # LayerWiseDistsLoss(alpha_dict=dists_alpha, beta_dict=dists_beta), \n", + " TargetNormalizedMSE()\n", + " ], \n", + " [alpha, beta]\n", + ")\n", + "# critic_image = TVloss(alpha=1e-07)\n", + "\n", + "optimizer = optim.AdamW(latent.parameters(), lr=0.1)\n", + "scheduler = None\n", + "\n", + "pipeline = FeatureInversionTask(\n", + " encoder=encoder,\n", + " generator=generator,\n", + " latent=latent,\n", + " critic=critic,\n", + " # critic_image=critic_image,\n", + " optimizer=optimizer,\n", + " scheduler=scheduler,\n", + " num_iterations=100,\n", + ")\n", + "\n", + "data_root_path = Path(\"./data\") / cfg.decoded_features.name\n", + "\n", + "feature_root_path = (\n", + " Path(\"./data\") / cfg.decoded_features.name / \"derivatives\" / \"features\" / cfg.decoded_features.model_name\n", + ")\n", + "decoded_feature_root_path = (\n", + " Path(\"./data\") / cfg.decoded_features.name\n", + " / \"derivatives\"\n", + " / \"decoded_features\"\n", + " / cfg.decoded_features.decoder_setting\n", + " / \"decoded_features\"\n", + " / cfg.decoded_features.model_name\n", + ")\n", + "\n", + "# Load target statistics\n", + "print(\"Load target statistics\")\n", + "target_path_template = (\n", + " f\"./data/{cfg.decoded_features.training_dataset}/derivatives\"\n", + " f\"/feature_decoders/{cfg.decoded_features.decoder_setting}/{cfg.decoded_features.model_name}\"\n", + " \"/{layer_path_name}\"\n", + " f\"/{cfg.decoded_features.subjects[0]}/{cfg.decoded_features.rois[0]}\"\n", + " \"/model/y_mean.mat\"\n", + ")\n", + "scale_path = (\n", + " \"./data/models_shared/caffe/VGG_ILSVRC_19_layers\"\n", + " \"/estimated_feat_std\"\n", + " \"/estimated_cnn_feat_std_VGG_ILSVRC_19_layers_ImgSize_224x224_chwise_dof1.mat\"\n", + ")\n", + "target_center, target_scale = 0.45, 1\n", + "# load_target_statistics(\n", + "# target_path_template, scale_path, layer_path_names=list(to_layer_name.keys())\n", + "# )\n", + "\n", + "# Load features\n", + "print(\"Load features\")\n", + "def compose(*transforms):\n", + " def _compose(x):\n", + " for transform in transforms:\n", + " x = transform(x)\n", + " return x\n", + " return _compose\n", + "\n", + "# images_dataset = ImageDataset(\n", + "# root_path=data_root_path / \"source\",\n", + "# extension=cfg.evaluation.true_image.ext,\n", + "# stimulus_names=cfg.target_images,\n", + "# )\n", + "\n", + "target_images = np.array(cfg.target_images)\n", + "for subject, roi in product(cfg.decoded_features.subjects, cfg.decoded_features.rois):\n", + " \n", + " # decoded_features_dataset = DecodedFeaturesDataset(\n", + " # root_path=decoded_feature_root_path,\n", + " # layer_path_names=list(to_layer_name.keys()),\n", + " # subject_id=subject,\n", + " # roi=roi,\n", + " # # stimulus_names=images_dataset._stimulus_names,\n", + " # stimulus_names=target_images,\n", + " # transform=compose(\n", + " # ScaleFeatures(target_center, target_scale),\n", + " # RenameFeatureKeys(to_layer_name),\n", + " # ),\n", + " # )\n", + "\n", + " # stacked_dataset = SimpleStackDataset(\n", + " # images_dataset, decoded_features_dataset\n", + " # )\n", + " \n", + " \n", + " features_dir=os.path.join(\n", + " '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/VGG19_ILSVRC_19_layers/'\n", + " # '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/features/pytorch/vgg19_torchvision/'\n", + " )\n", + " yf = h5py.File(os.path.join(features_dir, 'features[0]', f'{1}.h5'), 'r')\n", + " features = yf['dataset']\n", + " decoded_features_dataset = features\n", + " data_loader = DataLoader(\n", + " decoded_features_dataset,\n", + " batch_size=batch_size,\n", + " num_workers=1,\n", + " )\n", + "\n", + " yf2 = h5py.File(os.path.join(features_dir, 'features[2]', f'{1}.h5'), 'r')\n", + " features2 = yf2['dataset']\n", + " decoded_features_dataset2 = features2\n", + " data_loader2 = DataLoader(\n", + " decoded_features_dataset2,\n", + " batch_size=batch_size,\n", + " num_workers=1,\n", + " )\n", + " \n", + " for idx, decoded_features in enumerate(data_loader):\n", + " \n", + " # # Calculate mean and standard deviation\n", + " mean_val = decoded_features.mean()\n", + " std_val = decoded_features.std()\n", + " \n", + " # Normalize using mean and standard deviation\n", + " decoded_features = (decoded_features - mean_val) / std_val\n", + " \n", + " stimulus_names = cfg.target_images[idx]\n", + " if isinstance(stimulus_names, str):\n", + " stimulus_names = (stimulus_names,)\n", + " print(stimulus_names)\n", + " print(decoded_features_dataset2[idx].shape)\n", + " pipeline.reset_states()\n", + " target_features = {\n", + " 'features[0]': target_featuresf1[:,:,:224,:224].to(device=device, dtype=dtype)#, 'features[2]': torch.Tensor(decoded_features_dataset2[idx]).unsqueeze(0).to(device=device, dtype=dtype)\n", + " }\n", + " generated_images = image_domain.PILDomainWithExplicitCrop().receive(pipeline(target_features))\n", + " # if idx == 2: return generated_images\n", + " # for i, stimulus_name in enumerate(stimulus_names):\n", + " image = generated_images[0].detach().cpu().numpy()\n", + " pp_image = normalize_image(clip_extreme(image, pct=4))\n", + " # Crop the image to 224x224\n", + " h, w, _ = image.shape\n", + " h_start = (h - 224) // 2\n", + " w_start = (w - 224) // 2\n", + " pp_image = pp_image[h_start : h_start + 224, w_start : w_start + 224]\n", + "\n", + " image = Image.fromarray(pp_image)\n", + " savedir = \"./recons\"\n", + " os.makedirs(savedir, exist_ok=True)\n", + " image.save(os.path.join(savedir, f\"{idx}.png\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "id": "7e303257-7ca0-40d1-b924-54a9a06f2298", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "from PIL import Image\n", + "image_path = \"./im.jpg\"\n", + "image = Image.open(image_path)\n", + "\n", + "# Preprocess the image\n", + "preprocess = transforms.Compose([\n", + " transforms.Resize((224, 224)),\n", + " transforms.ToTensor(),\n", + " # transforms.Normalize(mean=[0.485, 0.456, 0.406], # Example normalization (ImageNet)\n", + " # std=[0.229, 0.224, 0.225]),\n", + "])\n", + "\n", + "# Preprocess the image\n", + "image_tensor = preprocess(image).unsqueeze(0).to(device)\n", + "\n", + "# show processed image\n", + "to_pil = ToPILImage()\n", + "pil_image = to_pil(image_tensor.squeeze())\n", + "pil_image.show()\n", + "\n", + "\n", + "# Generate features from the image\n", + "with torch.no_grad():\n", + " image_features = feature_extractor(image_tensor)\n", + "\n", + "# Define target features\n", + "target_featuresf1 = image_features.to(device).float()" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "a60653ba-3405-4d1f-8cf8-26ca000c1072", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "torch.Size([1, 64, 224, 224])\n" + ] + } + ], + "source": [ + "print(target_featuresf1.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "3613a7de-d093-4451-b5f3-e563c0d74354", + "metadata": {}, + "outputs": [ + { + "data": { + "image/jpeg": "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", + "image/png": "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", + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f4cd956e68b84251b84b809f3991836d", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/10000 [00:00 149\u001b[0m \u001b[43mloss\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbackward\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 151\u001b[0m \u001b[38;5;66;03m# Gradient clipping (optional but recommended)\u001b[39;00m\n\u001b[1;32m 152\u001b[0m torch\u001b[38;5;241m.\u001b[39mnn\u001b[38;5;241m.\u001b[39mutils\u001b[38;5;241m.\u001b[39mclip_grad_norm_([z], max_norm\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1.0\u001b[39m)\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/torch/_tensor.py:492\u001b[0m, in \u001b[0;36mTensor.backward\u001b[0;34m(self, gradient, retain_graph, create_graph, inputs)\u001b[0m\n\u001b[1;32m 482\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m has_torch_function_unary(\u001b[38;5;28mself\u001b[39m):\n\u001b[1;32m 483\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m handle_torch_function(\n\u001b[1;32m 484\u001b[0m Tensor\u001b[38;5;241m.\u001b[39mbackward,\n\u001b[1;32m 485\u001b[0m (\u001b[38;5;28mself\u001b[39m,),\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 490\u001b[0m inputs\u001b[38;5;241m=\u001b[39minputs,\n\u001b[1;32m 491\u001b[0m )\n\u001b[0;32m--> 492\u001b[0m \u001b[43mtorch\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mautograd\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mbackward\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 493\u001b[0m \u001b[43m \u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mgradient\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mretain_graph\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mcreate_graph\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43minputs\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43minputs\u001b[49m\n\u001b[1;32m 494\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/torch/autograd/__init__.py:251\u001b[0m, in \u001b[0;36mbackward\u001b[0;34m(tensors, grad_tensors, retain_graph, create_graph, grad_variables, inputs)\u001b[0m\n\u001b[1;32m 246\u001b[0m retain_graph \u001b[38;5;241m=\u001b[39m create_graph\n\u001b[1;32m 248\u001b[0m \u001b[38;5;66;03m# The reason we repeat the same comment below is that\u001b[39;00m\n\u001b[1;32m 249\u001b[0m \u001b[38;5;66;03m# some Python versions print out the first line of a multi-line function\u001b[39;00m\n\u001b[1;32m 250\u001b[0m \u001b[38;5;66;03m# calls in the traceback and some print out the last line\u001b[39;00m\n\u001b[0;32m--> 251\u001b[0m \u001b[43mVariable\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_execution_engine\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun_backward\u001b[49m\u001b[43m(\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;66;43;03m# Calls into the C++ engine to run the backward pass\u001b[39;49;00m\n\u001b[1;32m 252\u001b[0m \u001b[43m \u001b[49m\u001b[43mtensors\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 253\u001b[0m \u001b[43m \u001b[49m\u001b[43mgrad_tensors_\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 254\u001b[0m \u001b[43m \u001b[49m\u001b[43mretain_graph\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 255\u001b[0m \u001b[43m \u001b[49m\u001b[43mcreate_graph\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 256\u001b[0m \u001b[43m \u001b[49m\u001b[43minputs\u001b[49m\u001b[43m,\u001b[49m\n\u001b[1;32m 257\u001b[0m \u001b[43m \u001b[49m\u001b[43mallow_unreachable\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 258\u001b[0m \u001b[43m \u001b[49m\u001b[43maccumulate_grad\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43;01mTrue\u001b[39;49;00m\u001b[43m,\u001b[49m\n\u001b[1;32m 259\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m\n", + "\u001b[0;31mOutOfMemoryError\u001b[0m: CUDA out of memory. Tried to allocate 64.00 MiB. GPU 0 has a total capacty of 79.11 GiB of which 51.38 MiB is free. Process 927161 has 3.18 GiB memory in use. Including non-PyTorch memory, this process has 75.87 GiB memory in use. Of the allocated memory 74.94 GiB is allocated by PyTorch, and 71.92 MiB is reserved by PyTorch but unallocated. If reserved but unallocated memory is large try setting max_split_size_mb to avoid fragmentation. See documentation for Memory Management and PYTORCH_CUDA_ALLOC_CONF" + ] + } + ], + "source": [ + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "from torchvision import transforms\n", + "from torchvision.transforms import ToPILImage\n", + "from tqdm.auto import tqdm\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "from PIL import Image\n", + "\n", + "# Define the center_crop_tensor function\n", + "def center_crop_tensor(tensor, target_height, target_width):\n", + " \"\"\"\n", + " Center crops a tensor to the specified height and width.\n", + "\n", + " Args:\n", + " tensor (torch.Tensor): Input tensor of shape [B, C, H, W].\n", + " target_height (int): Desired height after cropping.\n", + " target_width (int): Desired width after cropping.\n", + "\n", + " Returns:\n", + " torch.Tensor: Center-cropped tensor of shape [B, C, target_height, target_width].\n", + " \"\"\"\n", + " _, _, h, w = tensor.shape\n", + " if h < target_height or w < target_width:\n", + " raise ValueError(f\"Input tensor is smaller than the target size: ({h}, {w}) vs ({target_height}, {target_width})\")\n", + " \n", + " start_h = (h - target_height) // 2\n", + " start_w = (w - target_width) // 2\n", + " return tensor[:, :, start_h:start_h + target_height, start_w:start_w + target_width]\n", + "\n", + "# Assuming generator, encoder, and decoded_features are predefined\n", + "\n", + "# Create a directory to save intermediate images (optional)\n", + "output_dir = \"generated_images\"\n", + "os.makedirs(output_dir, exist_ok=True)\n", + "\n", + "# Move models to device\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "generator = generator.to(device)\n", + "encoder = encoder.to(device)\n", + "\n", + "# Set models to evaluation mode and freeze parameters\n", + "generator.eval()\n", + "encoder.eval()\n", + "for param in generator.parameters():\n", + " param.requires_grad = True\n", + "for param in encoder.parameters():\n", + " param.requires_grad = True\n", + "\n", + "# Define feature extractor\n", + "class EncoderFeatureExtractor(nn.Module):\n", + " def __init__(self, encoder, target_layer=0):\n", + " super(EncoderFeatureExtractor, self).__init__()\n", + " self.encoder = encoder\n", + " self.target_layer = target_layer\n", + " self.features = nn.Sequential(*list(self.encoder._feature_network.features.children())[:self.target_layer + 1])\n", + "\n", + " def forward(self, x):\n", + " return self.features(x)\n", + "\n", + "feature_extractor = EncoderFeatureExtractor(encoder, target_layer=0).to(device)\n", + "\n", + "\n", + "from PIL import Image\n", + "image_path = \"./im.jpg\"\n", + "image = Image.open(image_path)\n", + "\n", + "# Preprocess the image\n", + "preprocess = transforms.Compose([\n", + " transforms.Resize((256, 256)),\n", + " transforms.ToTensor(),\n", + " # transforms.Normalize(mean=[0.485, 0.456, 0.406], # Example normalization (ImageNet)\n", + " # std=[0.229, 0.224, 0.225]),\n", + "])\n", + "\n", + "# Preprocess the image\n", + "image_tensor = preprocess(image).unsqueeze(0).to(device)\n", + "\n", + "# show processed image\n", + "to_pil = ToPILImage()\n", + "pil_image = to_pil(image_tensor.squeeze())\n", + "pil_image.show()\n", + "\n", + "\n", + "# Generate features from the image\n", + "with torch.no_grad():\n", + " image_features = feature_extractor(image_tensor)\n", + "\n", + "# Define target features\n", + "target_features = torch.Tensor(torch.load('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/0_preprocessing/ff.pt')).to(device).float()\n", + "image_features.to(device).float()\n", + "\n", + "# Initialize latent vector z\n", + "z_dim = 4096 # Adjust based on your generator\n", + "# Initialize z with requires_grad=True to allow optimization\n", + "z = torch.randn(1, z_dim, device=device, requires_grad=True)\n", + "\n", + "# Set up optimizer\n", + "optimizer = optim.Adam([z], lr=0.01) # Reduced learning rate from 1 to 0.01\n", + "\n", + "# Set up learning rate scheduler\n", + "scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5000, gamma=0.5) # Adjusted gamma to 0.5\n", + "\n", + "# Define loss function\n", + "loss_fn = nn.MSELoss()\n", + "\n", + "# For saving images\n", + "to_pil = ToPILImage()\n", + "\n", + "# Optimization loop parameters\n", + "num_steps = 10000\n", + "log_interval = 1000 # How often to log and save images\n", + "save_image_interval = 100 # Save image every 100 steps\n", + "\n", + "# Lists to store loss values and steps\n", + "loss_history = []\n", + "steps_list = []\n", + "\n", + "# Regularization strength\n", + "lambda_reg = 0 # Increased from 1e-14 to 1e-5\n", + "\n", + "# Define latent upper bound\n", + "latent_upperbound = 30.0 # Upper bound for z\n", + "\n", + "# Start optimization\n", + "for step in tqdm(range(1, num_steps + 1)):\n", + " optimizer.zero_grad()\n", + " \n", + " # Generate image from z\n", + " generated_image = generator(z)\n", + " \n", + " # Center crop the generated image to match encoder's input resolution\n", + " try:\n", + " generated_image_cropped = generated_image #= center_crop_tensor(generated_image, 224, 224)\n", + " except ValueError as e:\n", + " print(f\"Step [{step}]: {e}\")\n", + " # Optionally, handle smaller images by padding or skipping this step\n", + " # For simplicity, we'll skip the optimization step in this case\n", + " continue\n", + " \n", + " # Extract features\n", + " generated_features = feature_extractor(generated_image_cropped)\n", + " \n", + " # Compute loss with regularization\n", + " loss = loss_fn(generated_features, target_features) + lambda_reg * torch.norm(z)\n", + " \n", + " # Backpropagate\n", + " loss.backward()\n", + " \n", + " # Gradient clipping (optional but recommended)\n", + " torch.nn.utils.clip_grad_norm_([z], max_norm=1.0)\n", + " \n", + " # Update z\n", + " optimizer.step()\n", + " \n", + " # Step the scheduler\n", + " scheduler.step()\n", + "\n", + " generated_image = generated_image.detach()\n", + " # Clamp z to enforce the latent upper bound\n", + " with torch.no_grad():\n", + " z.clamp_(-latent_upperbound, latent_upperbound)\n", + " \n", + " # Logging and storing loss\n", + " if step % log_interval == 0 or step == num_steps or step == 1:\n", + " current_loss = loss.item()\n", + " loss_history.append(current_loss)\n", + " steps_list.append(step)\n", + " # print(f\"Step [{step}/{num_steps}], Loss: {current_loss:.6f}\")\n", + " \n", + " # Optional: Save intermediate images\n", + " if step % save_image_interval == 0 or step == num_steps or step == 1:\n", + " with torch.no_grad():\n", + " final_image = generator(z).detach().cpu()\n", + " # final_image = (final_image + 1) / 2 # If generator outputs in [-1, 1]\n", + " # final_image = torch.clamp(final_image, 0, 1) # Ensure pixel values are in [0,1]\n", + " pil_image = to_pil(final_image.squeeze())\n", + " image_path = os.path.join(output_dir, f\"generated_step_{step}.png\")\n", + " pil_image.save(image_path)\n", + " # print(f\"Saved generated image at step {step} to {image_path}\")\n", + "\n", + "# Generate the final image\n", + "with torch.no_grad():\n", + " final_image = generator(z).detach().cpu()\n", + " # final_image = (final_image + 1) / 2 # If normalized\n", + " # final_image = torch.clamp(final_image, 0, 1) # Ensure pixel values are in [0,1]\n", + " pil_image = to_pil(final_image.squeeze())\n", + " pil_image.show()\n", + "\n", + "# Plot the loss curve\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(steps_list, loss_history, label='MSE Loss')\n", + "plt.xlabel('Optimization Steps')\n", + "plt.ylabel('Loss')\n", + "plt.title('Loss Curve During Optimization')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "546cfe63-4a15-4a39-b850-02052d1fff2c", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([1, 64, 256, 256])" + ] + }, + "execution_count": 11, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "target_features['features[0]'].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f8509196-a9a8-4104-839c-5ec786135db2", + "metadata": {}, + "outputs": [], + "source": [ + "generated_features" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8634ca76-dd4c-47de-af04-edda1973a0b8", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "from torchvision.transforms import ToPILImage\n", + "\n", + "# Assuming generator and z are already defined and moved to the correct device\n", + "\n", + "# Generate image with initial z\n", + "with torch.no_grad():\n", + " initial_generated_image = generator(torch.randn(1,4096).to(device)).cpu()\n", + " print(initial_generated_image)\n", + "\n", + "# Check the range of the generated image\n", + "print(f\"Initial Generated Image - Min: {initial_generated_image.min().item()}, \"\n", + " f\"Max: {initial_generated_image.max().item()}, \"\n", + " f\"Mean: {initial_generated_image.mean().item()}\")\n", + "\n", + "# Visualize the initial image\n", + "to_pil = ToPILImage()\n", + "initial_image = (initial_generated_image + 1) / 2 # Adjust based on generator's output range\n", + "initial_image = torch.clamp(initial_image, 0, 1)\n", + "pil_image = to_pil(initial_image.squeeze())\n", + "pil_image.show()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mindeye", + "language": "python", + "name": "mindeye" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_multi.ipynb b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_multi.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..03518f6732d78a3be7e84e115d4ef8799498b327 --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_multi.ipynb @@ -0,0 +1,689 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "id": "372b680c-c632-4f89-987a-82c866432c12", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "from pathlib import Path\n", + "from typing import Callable, Iterator\n", + "import os \n", + "\n", + "import hdf5storage\n", + "import numpy as np\n", + "import scipy.io as sio\n", + "from itertools import product\n", + "from PIL import Image\n", + "from hydra.utils import to_absolute_path\n", + "\n", + "import torch\n", + "import torch.optim as optim\n", + "import torch.nn as nn\n", + "from torch.utils.data import Dataset\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from itertools import chain\n", + "from functools import partial\n", + "\n", + "from bdpy.recon.utils import normalize_image, clip_extreme\n", + "from bdpy.dl.torch.models import VGG19, layer_map, model_factory\n", + "from bdpy.pipeline.config import init_hydra_cfg\n", + "from bdpy.dl.torch.dataset import ImageDataset, FeaturesDataset, DecodedFeaturesDataset, RenameFeatureKeys\n", + "from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain\n", + "\n", + "from bdpy.recon.torch.task import FeatureInversionTask\n", + "from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE\n", + "from bdpy.recon.torch.modules.latent import ArbitraryLatent\n", + "from bdpy.recon.torch.modules.generator import BaseGenerator, FrozenGenerator\n", + "\n", + "import sys\n", + "sys.path.append('./analysis/1_case_study/image-reconstruction/iCNN')\n", + "from dist_loss import DistsLoss, CombinationLoss, LayerWiseDistsLoss\n", + "import utils\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "from torchvision import transforms\n", + "from torchvision.transforms import ToPILImage\n", + "from tqdm.auto import tqdm\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "from PIL import Image\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "6b974a4e-5f25-4107-9d4e-a5ffd58e35da", + "metadata": {}, + "outputs": [], + "source": [ + "data_type = torch.float32\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "features = 'all'\n", + "all_features = ['features[0]', 'features[2]', 'features[5]', 'features[7]', 'features[10]', 'features[12]', 'features[14]', 'features[16]', 'features[19]', 'features[21]', 'features[23]', 'features[25]', 'features[28]', 'features[30]', 'features[32]', 'features[34]', 'classifier[0]', 'classifier[3]', 'classifier[6]']\n", + "\n", + "decoded_features_base_path = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/feature-decoding/decoding_of/'" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "5f77064b-c276-40b7-8095-9e3fb5ccd841", + "metadata": {}, + "outputs": [], + "source": [ + "from bdpy.dl.torch.models import VGG19, layer_map, model_factory\n", + "from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE\n", + "from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain\n", + "\n", + "generator_network = model_factory('relu7generator')\n", + "generator_network.load_state_dict(torch.load('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/bvlc_reference_caffenet_generator_ILSVRC2012_Training/generator_relu7.pt'))\n", + "generator_network = generator_network.to(device)\n", + "\n", + "generator = build_generator(\n", + " generator_network, \n", + " image_domain.BdPyVGGDomain(device=device, dtype=data_type)\n", + " )\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "4eeeaa0a-de30-4bc5-bc0e-58f35d1996c0", + "metadata": {}, + "outputs": [], + "source": [ + "feature_network = VGG19()\n", + "\n", + "feature_network.load_state_dict(torch.load('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt'))\n", + "encoder = feature_network.to(device)\n", + "\n", + "encoder.eval()\n", + "for param in encoder.parameters():\n", + " param.requires_grad = True\n", + "\n", + "# Define feature extractor\n", + "class EncoderFeatureExtractor(nn.Module):\n", + " def __init__(self, encoder, target_layers):\n", + " super(EncoderFeatureExtractor, self).__init__()\n", + " self.encoder = encoder\n", + " self.target_layers = target_layers\n", + " self.features_layers = list(self.encoder.features.children())\n", + " self.classifier_layers = list(self.encoder.classifier.children())\n", + "\n", + " def forward(self, x):\n", + " outputs = {}\n", + " \n", + " for idx, layer in enumerate(self.features_layers):\n", + " x = layer(x)\n", + " layer_name = f'features[{idx}]'\n", + " if layer_name in self.target_layers:\n", + " outputs[layer_name] = x\n", + "\n", + " if 'avgpool' in self.target_layers:\n", + " x = self.encoder.avgpool(x)\n", + " outputs['avgpool'] = x\n", + " else:\n", + " x = self.encoder.avgpool(x)\n", + "\n", + " x = torch.flatten(x, 1)\n", + "\n", + " for idx, layer in enumerate(self.classifier_layers):\n", + " x = layer(x)\n", + " layer_name = f'classifier[{idx}]'\n", + " if layer_name in self.target_layers:\n", + " outputs[layer_name] = x\n", + "\n", + " return outputs\n", + "\n", + "if features == 'all':\n", + " feature_extractor = EncoderFeatureExtractor(encoder, target_layers=all_features)\n", + "else:\n", + " feature_extractor = EncoderFeatureExtractor(encoder, target_layers=features)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "66bb8988-f1d2-416d-8c81-b4ddf43ac32c", + "metadata": {}, + "outputs": [], + "source": [ + "input_tensor = torch.randn(1, 3, 224, 224).to(device)\n", + "\n", + "# Extract features\n", + "with torch.no_grad():\n", + " extracted_features = feature_extractor(input_tensor)" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "id": "9d0de8c6-ffae-4695-aa54-2be6fed7ee7a", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Elapsed time: 0.00236845016479492188 s\n" + ] + } + ], + "source": [ + "input_tensor = torch.randn(128, 3, 224, 224).to(device)\n", + "# benchmark time\n", + "\n", + "import time\n", + "start = time.time()\n", + "\n", + "# Extract features\n", + "with torch.no_grad():\n", + " extracted_features = feature_extractor(input_tensor)\n", + "\n", + "end = time.time()\n", + "print(f'Elapsed time: {end - start:.20f} s')" + ] + }, + { + "cell_type": "code", + "execution_count": 25, + "id": "3c0894e6-5c83-4bb2-bc5a-ed0e39306596", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "features[0]: torch.Size([128, 64, 224, 224])\n", + "features[2]: torch.Size([128, 64, 224, 224])\n", + "features[5]: torch.Size([128, 128, 112, 112])\n", + "features[7]: torch.Size([128, 128, 112, 112])\n", + "features[10]: torch.Size([128, 256, 56, 56])\n", + "features[12]: torch.Size([128, 256, 56, 56])\n", + "features[14]: torch.Size([128, 256, 56, 56])\n", + "features[16]: torch.Size([128, 256, 56, 56])\n", + "features[19]: torch.Size([128, 512, 28, 28])\n", + "features[21]: torch.Size([128, 512, 28, 28])\n", + "features[23]: torch.Size([128, 512, 28, 28])\n", + "features[25]: torch.Size([128, 512, 28, 28])\n", + "features[28]: torch.Size([128, 512, 14, 14])\n", + "features[30]: torch.Size([128, 512, 14, 14])\n", + "features[32]: torch.Size([128, 512, 14, 14])\n", + "features[34]: torch.Size([128, 512, 14, 14])\n", + "classifier[0]: torch.Size([128, 4096])\n", + "classifier[3]: torch.Size([128, 4096])\n", + "classifier[6]: torch.Size([128, 1000])\n" + ] + } + ], + "source": [ + "# print the shapes of the extracted features\n", + "for key, value in extracted_features.items():\n", + " print(f'{key}: {value.shape}')\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "2c617029-fefc-44f2-9035-b2cfc942d308", + "metadata": {}, + "outputs": [], + "source": [ + "class BoundedLatent(ArbitraryLatent):\n", + " def __init__(self, shape: tuple[int, ...], init_fn: Callable[[torch.Tensor], None],\n", + " upperbound: torch.Tensor, lowerbound: torch.Tensor) -> None:\n", + " \n", + " super().__init__(shape, init_fn)\n", + " self.upperbound = upperbound\n", + " self.lowerbound = lowerbound\n", + "\n", + " def generate(self) -> torch.Tensor:\n", + " # clip upper bound\n", + " if self.upperbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, max=self.upperbound)\n", + " # clip lower bound \n", + " if self.lowerbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, min=self.lowerbound)\n", + " \n", + " return latent\n" + ] + }, + { + "cell_type": "code", + "execution_count": 46, + "id": "de3a6dc7-9061-4bb3-8420-1c0d8ccc8352", + "metadata": {}, + "outputs": [], + "source": [ + "latent_upperbound = np.loadtxt(\"/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/bvlc_reference_caffenet_generator_ILSVRC2012_Training/act_range/3x/relu7.txt\", delimiter=\" \")" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "8b886fc6-56e0-46f2-a002-379e58449252", + "metadata": {}, + "outputs": [], + "source": [ + "latent = BoundedLatent((1, 4096), partial(torch.nn.init.normal_, mean=0.0, std=0.01), \n", + " latent_upperbound, 0)\n", + "latent.to(device)\n", + "latent.reset_states()" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "047cef7d-c5fc-4c6e-83cd-056cde20dc46", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "torch.Size([4096])" + ] + }, + "execution_count": 69, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "latent.generate().shape" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "cbf2048c-d0dc-4e47-82f5-539a32b9b4f4", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([0.0143, 0.0000, 0.0089, ..., 0.0021, 0.0028, 0.0009], device='cuda:0',\n", + " grad_fn=)" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "latent()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c87921a9-e8bd-4c1f-8d8e-1f1c80a0969d", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "111b4ff1734445b196081e7cf275f7c3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1000 [00:00 None:\n", + "# super(LayerWiseDistsLoss, self).__init__(callbacks)\n", + "# self.alpha_dict = alpha_dict\n", + "# self.beta_dict = beta_dict\n", + "# def compare_layer(\n", + "# self, feature: torch.Tensor, target_feature: torch.Tensor, layer_name: str,eps: float = 1e-6,\n", + " \n", + "# ) -> torch.Tensor:\n", + "# if feature.ndim == 2:\n", + "# # feature.shape = (batch_size, feature_dim)\n", + "# # target_feature.shape = (batch_size, feature_dim)\n", + "# feature_mean = feature.mean(dim=1, keepdim=True) # (batch_size, 1)\n", + "# target_feature_mean = target_feature.mean(dim=1, keepdim=True) # (batch_size, 1)\n", + "# feature_var = ((feature - feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1)\n", + "# target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=1, keepdim=True) # (batch_size, 1)\n", + "# cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=1, keepdim=True) # (batch_size, 1)\n", + "# else:\n", + "# # feature.shape = (batch_size, channel, height, width)\n", + "# # target_feature.shape = (batch_size, channel, height, width)\n", + "# feature_mean = feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1)\n", + "# target_feature_mean = target_feature.mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1)\n", + "# feature_var = ((feature - feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1)\n", + "# target_feature_var = ((target_feature - target_feature_mean) ** 2).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1)\n", + "# #cov = ((feature - feature_mean) * (target_feature - target_feature_mean)).mean(dim=[2,3], keepdim=True) # (batch_size, channel, 1, 1)\n", + "# cov = (feature * target_feature).mean(dim=[2,3], keepdim=True) - feature_mean * target_feature_mean\n", + "# s1 = (2 * feature_mean * target_feature_mean + eps) / (feature_mean ** 2 + target_feature_mean ** 2 + eps) # (batch_size, ...)\n", + "# s2 = (2 * cov + eps) / (feature_var + target_feature_var + eps) # (batch_size, ...)\n", + "\n", + "# return - (self.alpha_dict[layer_name] * s1 + self.beta_dict[layer_name] * s2).mean(dim=tuple(range(1, s1.ndim))) / (self.alpha_dict[layer_name] + self.beta_dict[layer_name])\n", + "\n", + "# class TargetNormalizedMSE(LayerWiseAverageCritic):\n", + "# \"\"\"MSE loss divided by the squared norm of the target feature.\"\"\"\n", + "\n", + "# def compare_layer(\n", + "# self, feature: torch.Tensor, target_feature: torch.Tensor, layer_name: str\n", + "# ) -> torch.Tensor:\n", + "# \"\"\"Loss function per layer.\n", + "\n", + "# Parameters\n", + "# ----------\n", + "# feature : torch.Tensor\n", + "# Feature tensor of the layer specified by `layer_name`.\n", + "# target_feature : torch.Tensor\n", + "# Target feature tensor of the layer specified by `layer_name`.\n", + "# layer_name : str\n", + "# Layer name.\n", + "\n", + "# Returns\n", + "# -------\n", + "# torch.Tensor\n", + "# Loss value of the layer specified by `layer_name`.\n", + "# \"\"\"\n", + "# squared_norm = (target_feature ** 2).sum(dim=tuple(range(1, target_feature.ndim)))\n", + "# return ((feature - target_feature) ** 2).sum(\n", + "# dim=tuple(range(1, feature.ndim))\n", + "# ) / squared_norm\n", + "\n", + "layer_wise_dists_loss = LayerWiseDistsLoss(alpha_dict=dists_alpha, beta_dict=dists_beta)\n", + "mse_loss = TargetNormalizedMSE()\n", + "\n", + "def calculate_loss(features, target_features, alpha, beta):\n", + " loss_wise = 0\n", + " for key, value in features.items():\n", + " loss_wise += layer_wise_dists_loss.compare_layer(value, target_features[key], key)\n", + "\n", + " return alpha * loss_wise + beta * mse_loss.compare(features, target_features)\n", + "\n", + "\n", + "critic = CombinationLoss([LayerWiseDistsLoss(alpha_dict=dists_alpha, beta_dict=dists_beta), TargetNormalizedMSE()], [alpha, beta])\n", + "\n", + "# Initialize latent vector z\n", + "z_dim = 4096 # Adjust based on your generator\n", + "# Initialize z with requires_grad=True to allow optimization\n", + "# z = torch.randn(1, z_dim, device=device, requires_grad=True)\n", + "z = latent\n", + "\n", + "# Set up optimizer\n", + "optimizer = optim.AdamW(z.parameters(), lr=0.1) # Reduced learning rate from 1 to 0.01\n", + "\n", + "# Set up learning rate scheduler\n", + "scheduler = optim.lr_scheduler.StepLR(optimizer, step_size=5000, gamma=0.5) # Adjusted gamma to 0.5\n", + "\n", + "# Define loss function\n", + "# loss_fn = DistsLoss()\n", + "\n", + "# For saving images\n", + "to_pil = ToPILImage()\n", + "\n", + "# Optimization loop parameters\n", + "num_steps = 1000\n", + "log_interval = 100 # How often to log and save images\n", + "save_image_interval = 100 # Save image every 100 steps\n", + "\n", + "# Lists to store loss values and steps\n", + "loss_history = []\n", + "steps_list = []\n", + "\n", + "# Regularization strength\n", + "lambda_reg = 0 # Increased from 1e-14 to 1e-5\n", + "\n", + "\n", + "# Define latent upper bound\n", + "latent_upperbound = 20.0 # Upper bound for z\n", + "\n", + "# Start optimization\n", + "for step in tqdm(range(1, num_steps + 1)):\n", + " optimizer.zero_grad()\n", + " \n", + " # Generate image from z\n", + " generated_image = generator(z())\n", + " \n", + " \n", + " # Extract features\n", + " generated_features = feature_extractor(generated_image)\n", + " \n", + " # Compute loss with regularization\n", + " # loss = calculate_loss_mse(generated_features, extracted_features, alpha, beta) \n", + " loss = critic(generated_features, extracted_features)\n", + " # Backpropagate\n", + " loss.backward()\n", + " \n", + " # Gradient clipping (optional but recommended)\n", + " # torch.nn.utils.clip_grad_norm_([z], max_norm=1.0)\n", + " \n", + " # Update z\n", + " optimizer.step()\n", + " \n", + " # Step the scheduler\n", + " scheduler.step()\n", + "\n", + " generated_image = generated_image.detach()\n", + " # Clamp z to enforce the latent upper bound\n", + " # with torch.no_grad():\n", + " # z.clamp_(0, latent_upperbound)\n", + " \n", + " # Logging and storing loss\n", + " if step % log_interval == 0 or step == num_steps or step == 1:\n", + " current_loss = loss.item()\n", + " loss_history.append(current_loss)\n", + " steps_list.append(step)\n", + " # print(f\"Step [{step}/{num_steps}], Loss: {current_loss:.6f}\")\n", + " \n", + " # Optional: Save intermediate images\n", + " if step % save_image_interval == 0 or step == num_steps or step == 1:\n", + " with torch.no_grad():\n", + " final_image = generator(z()).detach().cpu()\n", + " # final_image = (final_image + 1) / 2 # If generator outputs in [-1, 1]\n", + " # final_image = torch.clamp(final_image, 0, 1) # Ensure pixel values are in [0,1]\n", + " pil_image = to_pil(final_image.squeeze())\n", + " image_path = os.path.join(output_dir, f\"generated_imagery_{'test'}_step_{step}.png\")\n", + " pil_image.save(image_path)\n", + " # print(f\"Saved generated image at step {step} to {image_path}\")\n", + "\n", + "# Generate the final image\n", + "with torch.no_grad():\n", + " final_image = generator(z()).detach().cpu()\n", + " # final_image = (final_image + 1) / 2 # If normalized\n", + " # final_image = torch.clamp(final_image, 0, 1) # Ensure pixel values are in [0,1]\n", + " pil_image = to_pil(final_image.squeeze())\n", + " pil_image.show()\n", + "\n", + "# Plot the loss curve\n", + "plt.figure(figsize=(10, 6))\n", + "plt.plot(steps_list, loss_history, label='MSE Loss')\n", + "plt.xlabel('Optimization Steps')\n", + "plt.ylabel('Loss')\n", + "plt.title('Loss Curve During Optimization')\n", + "plt.legend()\n", + "plt.grid(True)\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 42, + "id": "066d6453-ffb4-4a9e-a175-3d8f0f34a93a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([-2.8959], device='cuda:0', grad_fn=)" + ] + }, + "execution_count": 42, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "critic(generated_features, extracted_features)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "15ea78e2-9faf-41e0-b7af-0bfe37e6a244", + "metadata": {}, + "outputs": [], + "source": [ + "import torch\n", + "\n", + "aaa = torch.load('/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstructions/subj1_40/shared_1000/all_recon_shared_1000.pt')" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0388f3d0-624b-470e-9341-8cef20d3ade9", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "tensor([[[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]],\n", + "\n", + " [[0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " ...,\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.],\n", + " [0., 0., 0., ..., 0., 0., 0.]]])" + ] + }, + "execution_count": 12, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "aaa[4,1]" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mindeye", + "language": "python", + "name": "mindeye" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_sklearn.ipynb b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_sklearn.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..e9b58b6e5498bc90fccadb9296ffebed3b1a91ce --- /dev/null +++ b/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/reconstruct_sklearn.ipynb @@ -0,0 +1,975 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3c1ee8b6-7ad8-4035-b05d-6470ddd97b72", + "metadata": {}, + "outputs": [], + "source": [ + "from __future__ import annotations\n", + "from pathlib import Path\n", + "from typing import Callable, Iterator\n", + "import os \n", + "\n", + "import hdf5storage\n", + "import numpy as np\n", + "import scipy.io as sio\n", + "from itertools import product\n", + "from PIL import Image\n", + "from hydra.utils import to_absolute_path\n", + "\n", + "import torch\n", + "import torch.optim as optim\n", + "import torch.nn as nn\n", + "from torch.utils.data import Dataset\n", + "from torch.utils.data import DataLoader\n", + "\n", + "from itertools import chain\n", + "from functools import partial\n", + "\n", + "from bdpy.recon.utils import normalize_image, clip_extreme\n", + "from bdpy.dl.torch.models import VGG19, layer_map, model_factory\n", + "from bdpy.pipeline.config import init_hydra_cfg\n", + "from bdpy.dl.torch.dataset import ImageDataset, FeaturesDataset, DecodedFeaturesDataset, RenameFeatureKeys\n", + "from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain\n", + "\n", + "from bdpy.recon.torch.task import FeatureInversionTask\n", + "from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE\n", + "from bdpy.recon.torch.modules.latent import ArbitraryLatent\n", + "from bdpy.recon.torch.modules.generator import BaseGenerator, FrozenGenerator\n", + "\n", + "import sys\n", + "sys.path.append('./analysis/1_case_study/image-reconstruction/iCNN')\n", + "from dist_loss import DistsLoss, CombinationLoss, LayerWiseDistsLoss\n", + "import utils\n", + "import torch\n", + "import torch.nn as nn\n", + "import torch.optim as optim\n", + "from torchvision import transforms\n", + "from torchvision.transforms import ToPILImage\n", + "from tqdm.auto import tqdm\n", + "import matplotlib.pyplot as plt\n", + "import os\n", + "from PIL import Image\n" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "ab9fd98b-436b-417a-a978-4d2587ba686b", + "metadata": {}, + "outputs": [], + "source": [ + "# Define for the RR subdivision\n", + "splits_per_layer = {\n", + " 'features[0]': 32,\n", + " 'features[2]': 32,\n", + " 'features[5]': 16,\n", + " 'features[7]': 16,\n", + " 'features[10]': 8,\n", + " 'features[12]': 8,\n", + " 'features[14]': 8,\n", + " 'features[16]': 8,\n", + " 'features[19]': 4,\n", + " 'features[21]': 4,\n", + " 'features[23]': 4,\n", + " 'features[25]': 4,\n", + " 'features[28]': 2,\n", + " 'features[30]': 2,\n", + " 'features[32]': 2,\n", + " 'features[34]': 2,\n", + " 'classifier[0]': 1,\n", + " 'classifier[3]': 1,\n", + " 'classifier[6]': 1,\n", + "}\n", + "\n", + "layer_sizes = {\n", + " 'features[0]': 64,\n", + " 'features[2]': 64,\n", + " 'features[5]': 128,\n", + " 'features[7]': 128,\n", + " 'features[10]': 256,\n", + " 'features[12]': 256,\n", + " 'features[14]': 256,\n", + " 'features[16]': 256,\n", + " 'features[19]': 512,\n", + " 'features[21]': 512,\n", + " 'features[23]': 512,\n", + " 'features[25]': 512,\n", + " 'features[28]': 512,\n", + " 'features[30]': 512,\n", + " 'features[32]': 512,\n", + " 'features[34]': 512,\n", + " 'classifier[0]': 4096,\n", + " 'classifier[3]': 4096,\n", + " 'classifier[6]': 1000,\n", + "}\n", + "\n", + "features_shapes = {\n", + " 'features[0]': (64, 224, 224),\n", + " 'features[2]': (64, 224, 224),\n", + " 'features[5]': (128, 112, 112),\n", + " 'features[7]': (128, 112, 112),\n", + " 'features[10]': (256, 56, 56),\n", + " 'features[12]': (256, 56, 56),\n", + " 'features[14]': (256, 56, 56),\n", + " 'features[16]': (256, 56, 56),\n", + " 'features[19]': (512, 28, 28),\n", + " 'features[21]': (512, 28, 28),\n", + " 'features[23]': (512, 28, 28),\n", + " 'features[25]': (512, 28, 28),\n", + " 'features[28]': (512, 14, 14),\n", + " 'features[30]': (512, 14, 14),\n", + " 'features[32]': (512, 14, 14),\n", + " 'features[34]': (512, 14, 14),\n", + " 'classifier[0]': (4096,),\n", + " 'classifier[3]': (4096,),\n", + " 'classifier[6]': (1000,),\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "439e48f9-63db-4ca7-8b82-9818d6a03c60", + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def load_y_mean(decoded_features_path: str) -> np.ndarray:\n", + " # example /weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/feature-decoding/decoding_sklearn/features[0]/ridge_y_mean_features[0]_1.npy\n", + " num_splits_per_layer = splits_per_layer[decoded_features_path.split('/')[-1]]\n", + " feature_layer = decoded_features_path.split('/')[-1]\n", + " y_mean = []\n", + " for split in range(1, num_splits_per_layer + 1):\n", + " split_path = os.path.join(decoded_features_path, f'ridge_y_mean_{feature_layer}_{split}.npy')\n", + " y_mean.append(np.load(split_path))\n", + " y_mean = np.concatenate(y_mean, axis=0)\n", + " # expand the dim to match the shape of the decoded features for features[0] it should be (64, 1, 1) but for classifier[6] it should be (1000,) and so on\n", + " if not( feature_layer in ['classifier[0]', 'classifier[3]', 'classifier[6]']):\n", + " y_mean = np.expand_dims(y_mean, axis=1)\n", + " y_mean = np.expand_dims(y_mean, axis=1)\n", + "\n", + " return y_mean\n", + "\n", + "\n", + "\n", + "def load_target_statistics(\n", + " center_path_template: str, scale_path: str, layer_path_names: list[str], features: None\n", + ") -> tuple[dict[str, np.ndarray], dict[str, float]]:\n", + " if center_path_template is not None:\n", + " # target_center = {}\n", + " # for layer_path_name in layer_path_names:\n", + " # center_path = center_path_template.format(layer_path_name=layer_path_name)\n", + " # target_center[layer_path_name] = hdf5storage.loadmat(center_path)[\"y_mean\"]\n", + " # target_scale = sio.loadmat(scale_path)\n", + " # return target_center, target_scale\n", + "\n", + " target_center = {}\n", + " for layer_path_name in layer_path_names:\n", + " center_path = os.path.join(center_path_template, layer_path_name)\n", + " target_center[layer_path_name] = load_y_mean(center_path).tolist()\n", + " target_scale = sio.loadmat(scale_path)\n", + " return target_center, target_scale\n", + " else:\n", + " target_center = {}\n", + " for layer_path_name in layer_path_names:\n", + " center_current_layer = features[layer_path_name][0].mean(dim=tuple(range(1, features[layer_path_name][0].dim())), keepdims=True).to('cpu')\n", + " # convert to list\n", + " target_center[layer_path_name] = center_current_layer.numpy().tolist()\n", + " target_scale = sio.loadmat(scale_path)\n", + " return target_center, target_scale\n", + " \n", + "\n", + "class BoundedLatent(ArbitraryLatent):\n", + " def __init__(self, shape: tuple[int, ...], init_fn: Callable[[torch.Tensor], None],\n", + " upperbound: torch.Tensor, lowerbound: torch.Tensor) -> None:\n", + " \n", + " super().__init__(shape, init_fn)\n", + " self.upperbound = upperbound\n", + " self.lowerbound = lowerbound\n", + "\n", + " def generate(self) -> torch.Tensor:\n", + " # clip upper bound\n", + " if self.upperbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, max=self.upperbound)\n", + " # clip lower bound \n", + " if self.lowerbound is not None:\n", + " for latent in self._latent:\n", + " latent.data = torch.clamp(latent, min=self.lowerbound)\n", + " \n", + " return latent\n", + "\n", + "# %%\n", + "\n", + "class ScaleFeatures:\n", + " def __init__(\n", + " self,\n", + " target_center: dict[str, np.ndarray],\n", + " target_scale: dict[str, float],\n", + " ddof: int = 1,\n", + " ) -> None:\n", + " self.target_center = target_center\n", + " self.target_scale = target_scale\n", + " self.ddof = ddof\n", + "\n", + " def __call__(self, features: dict[str, np.ndarray]) -> dict[str, np.ndarray]:\n", + " scaled_features = {}\n", + " for layer_name, feature in features.items():\n", + " # if is tensor convert to numpy\n", + " if isinstance(feature, torch.Tensor):\n", + " feature = feature.cpu().detach().numpy()\n", + " axes_to_reduce = tuple(range(1, feature.ndim)) if feature.ndim > 1 else (0,)\n", + " feature_scale = feature.std(\n", + " axis=axes_to_reduce, keepdims=True, ddof=self.ddof\n", + " )\n", + " # NOTE: mean across channels\n", + " feature_scale = feature_scale.mean(axis=0, keepdims=True)\n", + "\n", + " scaled_feature = (\n", + " feature - self.target_center[layer_name]\n", + " ) / feature_scale * self.target_scale[layer_name] + self.target_center[\n", + " layer_name\n", + " ]\n", + " scaled_features[layer_name] = scaled_feature[0]\n", + " return scaled_features\n", + " \n", + "class SimpleStackDataset(Dataset):\n", + " def __init__(self, *datasets):\n", + " self.datasets = datasets\n", + " self._length = min(len(d) for d in datasets) # 最小の長さを使用して、すべてのデータセットを同じ長さに保つ\n", + "\n", + " def __getitem__(self, index):\n", + " return tuple(dataset[index] for dataset in self.datasets)\n", + "\n", + " def __len__(self):\n", + " return self._length\n", + "# %%" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4e1ad397-2a6c-4ace-ae59-22f3aa1d393a", + "metadata": {}, + "outputs": [], + "source": [ + "data_type = torch.float32\n", + "device = torch.device('cuda' if torch.cuda.is_available() else 'cpu')\n", + "\n", + "features = 'all'\n", + "all_features = ['features[0]', 'features[2]', 'features[5]', 'features[7]', 'features[10]', 'features[12]', 'features[14]', 'features[16]', 'features[19]', 'features[21]', 'features[23]', 'features[25]', 'features[28]', 'features[30]', 'features[32]', 'features[34]', 'classifier[0]', 'classifier[3]', 'classifier[6]']\n", + "\n", + "if utils.is_interactive():\n", + " run_name = 'subj1_1'\n", + "else:\n", + " if len(sys.argv) > 1:\n", + " run_name = sys.argv[1]\n", + " else:\n", + " raise ValueError(\"Debe proporcionar un argumento para 'run_name' en modo no interactivo\")\n", + "\n", + "decoded_features_base_path = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/feature-decoding/decoded_features/'\n", + "\n", + "decoded_features_path = os.path.join(decoded_features_base_path, run_name)\n", + "\n", + "print(\"Reconstructing for these features: \", decoded_features_path)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "6f1d78f8-9d97-46ca-bc54-819b32355807", + "metadata": {}, + "outputs": [], + "source": [ + "# # this is the number of ridge regression splits to perform bcz of memory constraints\n", + "# num_split = 64\n", + "# data_path = '/weka/proj-medarc/shared/mindeyev2_dataset/'\n", + "# num_sessions = 40\n", + "# multi_subject = False\n", + "# subj = 1" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ad3c0a9c-6e93-45d5-ac62-36f06d67fd68", + "metadata": {}, + "outputs": [], + "source": [ + "# import h5py\n", + "# f_betas = h5py.File(f'{data_path}/betas_all_subj0{subj}_fp32_renorm.hdf5', 'r')\n", + "# print(\"loading_betas\")\n", + "# betas = f_betas['betas'][:]\n", + "# betas = torch.from_numpy(betas).to(\"cpu\")\n", + "# print(\"betas_ loaded\")\n", + "# x_train, valid_nsd_ids_train, x_test, test_nsd_ids = utils.load_nsd(subject=subj, betas=betas, data_path=data_path)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "5ec2d95d-a0d6-4b15-978c-4cc86ef66f5b", + "metadata": {}, + "outputs": [], + "source": [ + "# selected_nsd_ids = [10,44,66,256,342,488]\n", + "# # find the indices of the selected_nsd_ids in the test_nsd_ids\n", + "# selected_nsd_indices = [i for i, x in enumerate(test_nsd_ids) if x in selected_nsd_ids]" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "id": "2f814ba8-fead-4cf1-ad8e-c7134965f5f0", + "metadata": {}, + "outputs": [], + "source": [ + "if features == 'all':\n", + " features = all_features\n", + "\n", + "\n", + "decoded_features_imagery = {}\n", + "# Load the precomputed decoded features\n", + "for feature in features:\n", + " num_splits_per_layer = splits_per_layer[feature]\n", + " decoded_features_path_ = os.path.join(decoded_features_path, feature)\n", + " current_decoded_features = []\n", + " for split in range(1, num_splits_per_layer + 1):\n", + " split_path = os.path.join(\n", + " decoded_features_path_, f'ridge_imagery_averaged_preds_{feature}_{split}.npy')\n", + " current_decoded_features.append(torch.tensor(\n", + " np.load(split_path), dtype=data_type, device='cpu'))\n", + " current_decoded_features = torch.cat(current_decoded_features, dim=1)\n", + " decoded_features_imagery[feature] = current_decoded_features\n", + "\n", + "decoded_features_vision = {}\n", + "# Load the precomputed decoded features\n", + "for feature in features:\n", + " num_splits_per_layer = splits_per_layer[feature]\n", + " decoded_features_path_ = os.path.join(decoded_features_path, feature)\n", + " current_decoded_features = []\n", + " for split in range(1, num_splits_per_layer + 1):\n", + " split_path = os.path.join(\n", + " decoded_features_path_, f'ridge_vision_averaged_preds_{feature}_{split}.npy')\n", + " current_decoded_features.append(torch.tensor(\n", + " np.load(split_path), dtype=data_type, device='cpu'))\n", + " current_decoded_features = torch.cat(current_decoded_features, dim=1)\n", + " decoded_features_vision[feature] = current_decoded_features\n", + "\n", + "decoded_features_shared_1000 = {}\n", + "# Load the precomputed decoded features\n", + "for feature in features:\n", + " num_splits_per_layer = splits_per_layer[feature]\n", + " decoded_features_path_ = os.path.join(decoded_features_path, feature)\n", + " current_decoded_features = []\n", + " for split in range(1, num_splits_per_layer + 1):\n", + " split_path = os.path.join(\n", + " decoded_features_path_, f'ridge_test_predictions_{feature}_{split}.npy')\n", + " current_decoded_features.append(torch.tensor(\n", + " np.load(split_path), dtype=data_type, device='cpu'))\n", + " current_decoded_features = torch.cat(current_decoded_features, dim=1)\n", + " decoded_features_shared_1000[feature] = current_decoded_features\n", + " " + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "id": "0b807a35-1603-4b0a-9b42-5238ead2b56f", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/admin/home-ckadirt/mindeye/lib/python3.11/site-packages/bdpy/dl/torch/domain/core.py:90: RuntimeWarning: FixedResolutionDomain is an irreversible domain. It does not guarantee the reversibility of `send` and `receive` methods. Please use the combination of `send` and `receive` methods with caution.\n", + " warnings.warn(\n" + ] + } + ], + "source": [ + "from bdpy.dl.torch.models import VGG19, layer_map, model_factory\n", + "from bdpy.recon.torch.modules import build_encoder, build_generator, TargetNormalizedMSE\n", + "from bdpy.dl.torch.domain import Domain, image_domain, ComposedDomain\n", + "\n", + "generator_network = model_factory('relu7generator')\n", + "generator_network.load_state_dict(torch.load(\n", + " '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/bvlc_reference_caffenet_generator_ILSVRC2012_Training/generator_relu7.pt'))\n", + "generator_network = generator_network.to(device)\n", + "\n", + "generator = build_generator(\n", + " generator_network,\n", + " image_domain.BdPyVGGDomain(device=device, dtype=data_type)\n", + ")\n", + "\n", + "feature_network = VGG19()\n", + "\n", + "feature_network.load_state_dict(torch.load(\n", + " '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/VGG_ILSVRC_19_layers/VGG_ILSVRC_19_layers.pt'))\n", + "encoder = feature_network.to(device)\n", + "\n", + "\n", + "if features == 'all':\n", + " layer_names = all_features\n", + "else:\n", + " layer_names = features\n", + "\n", + "\n", + "encoder = build_encoder(feature_network, layer_names,\n", + " domain=ComposedDomain([image_domain.BdPyVGGDomain(device=device, dtype=data_type),\n", + " image_domain.FixedResolutionDomain((224, 224))]),\n", + " )" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "1ab429cd-a0a1-46e9-ba9a-023cf2d8c932", + "metadata": {}, + "outputs": [], + "source": [ + "latent_upperbound = np.loadtxt(\n", + " \"/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/bvlc_reference_caffenet_generator_ILSVRC2012_Training/act_range/3x/relu7.txt\", delimiter=\" \")\n", + "latent_feat_num = 4096\n", + "latent_upperbound = torch.tensor(latent_upperbound).to(device, dtype=data_type)\n", + "\n", + "\n", + "\n", + "scale_path = '/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/VGG_ILSVRC_19_layers/estimated_cnn_feat_std_VGG_ILSVRC_19_layers_ImgSize_224x224_chwise_dof1.mat'\n", + "target_path_template = None" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "id": "c9f906ad-92aa-4b7b-a49a-bdb6756abdab", + "metadata": {}, + "outputs": [], + "source": [ + "target_center, target_scale = load_target_statistics(\n", + " decoded_features_path, \n", + " scale_path, \n", + " layer_names, \n", + " None\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "2df31bd9-fed0-4e4c-8ac7-9893c0616545", + "metadata": {}, + "outputs": [], + "source": [ + "to_layer_name = layer_map('vgg19')\n", + "# exclude the key starting from \"key\"\n", + "to_layer_name = {k: v for k, v in to_layer_name.items() if \"relu\" not in k}\n", + "to_path_name = {\n", + " layer_name: layer_path_name\n", + " for layer_name, layer_path_name in zip(\n", + " to_layer_name.values(), to_layer_name.keys()\n", + " )\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "id": "93669751-c247-433e-9b75-d3898dc4512f", + "metadata": {}, + "outputs": [], + "source": [ + "alpha = 4.0\n", + "beta = 1.0\n", + "dists_alpha = {\n", + " 'classifier[6]': 0.06661523244761258,\n", + " 'classifier[3]': 0.03871265134313895,\n", + " 'classifier[0]': 0.000629031742843134,\n", + " 'features[34]': 0.5310432634795051,\n", + " 'features[32]': 0.01975314483213067,\n", + " 'features[30]': 0.714010791024532,\n", + " 'features[28]': 0.08536182104218124,\n", + " 'features[25]': 0.030798346318926202,\n", + " 'features[23]': 0.004025735147052829,\n", + " 'features[21]': 0.0021716504774059618,\n", + " 'features[19]': 0.02880295296139471,\n", + " 'features[16]': 0.014169279688225732,\n", + " 'features[14]': 0.00019573287900056505,\n", + " 'features[12]': 0.0004887923569668929,\n", + " 'features[10]': 0.006857140440209977,\n", + " 'features[7]': 0.08084213863904581,\n", + " 'features[5]': 0.00024056214287883663,\n", + " 'features[2]': 0.003886371646003732,\n", + " 'features[0]': 0.009952859626673973,\n", + "}\n", + "dists_beta = {\n", + " 'classifier[6]': 0.008304840607742099,\n", + " 'classifier[3]': 0.044481711593671994,\n", + " 'classifier[0]': 0.038457933646483915,\n", + " 'features[34]': 0.0012780195483159135,\n", + " 'features[32]': 0.0018775814111698145,\n", + " 'features[30]': 0.5074163077203029,\n", + " 'features[28]': 0.002337825161420017,\n", + " 'features[25]': 0.7100372437615771,\n", + " 'features[23]': 0.5166895849277143,\n", + " 'features[21]': 0.03998274022264576,\n", + " 'features[19]': 0.04328555659354602,\n", + " 'features[16]': 0.024733951474856346,\n", + " 'features[14]': 0.0004859871528150426,\n", + " 'features[12]': 0.039778524165843814,\n", + " 'features[10]': 0.0002639605292406699,\n", + " 'features[7]': 0.02472305546171304,\n", + " 'features[5]': 0.12888847991806807,\n", + " 'features[2]': 0.008627502425502372,\n", + " 'features[0]': 0.000865427897168344\n", + "}" + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "id": "8060dfef-8c73-420f-91b6-5c8d118d547b", + "metadata": {}, + "outputs": [], + "source": [ + "def compose(*transforms):\n", + " def _compose(x):\n", + " for transform in transforms:\n", + " x = transform(x)\n", + " return x\n", + "\n", + " return _compose" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "03b4c824-4fc1-4324-8813-172433ac9beb", + "metadata": {}, + "outputs": [], + "source": [ + "transform = compose(\n", + " ScaleFeatures(target_center, RenameFeatureKeys(to_layer_name)(target_scale)),\n", + " RenameFeatureKeys(to_layer_name),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "803ccd84-9ee6-4329-b125-04aa7256e545", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8ead6a0f0f50486faefd6b02227e6b58", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/1000 [00:00 190\u001b[0m \u001b[43mpipeline\u001b[49m\u001b[43m(\u001b[49m\u001b[43mtarget_features\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 191\u001b[0m )\n\u001b[1;32m 193\u001b[0m image \u001b[38;5;241m=\u001b[39m generated_images[\u001b[38;5;241m0\u001b[39m]\u001b[38;5;241m.\u001b[39mdetach()\u001b[38;5;241m.\u001b[39mcpu()\u001b[38;5;241m.\u001b[39mnumpy()\n\u001b[1;32m 194\u001b[0m pp_image \u001b[38;5;241m=\u001b[39m normalize_image(clip_extreme(image, pct\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m4\u001b[39m))\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/task/core.py:45\u001b[0m, in \u001b[0;36mBaseTask.__call__\u001b[0;34m(self, *inputs, **parameters)\u001b[0m\n\u001b[1;32m 43\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39minputs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mparameters) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m Any:\n\u001b[1;32m 44\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Run the task.\"\"\"\u001b[39;00m\n\u001b[0;32m---> 45\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mrun\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43minputs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mparameters\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/recon/torch/task/inversion.py:199\u001b[0m, in \u001b[0;36mFeatureInversionTask.run\u001b[0;34m(self, target_features)\u001b[0m\n\u001b[1;32m 193\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_callback_handler\u001b[38;5;241m.\u001b[39mfire(\n\u001b[1;32m 194\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mon_image_generated\u001b[39m\u001b[38;5;124m\"\u001b[39m, step\u001b[38;5;241m=\u001b[39mstep, image\u001b[38;5;241m=\u001b[39mgenerated_image\u001b[38;5;241m.\u001b[39mclone()\u001b[38;5;241m.\u001b[39mdetach()\n\u001b[1;32m 195\u001b[0m )\n\u001b[1;32m 197\u001b[0m features \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_encoder(generated_image)\n\u001b[0;32m--> 199\u001b[0m loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_critic\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfeatures\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget_features\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 200\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_callback_handler\u001b[38;5;241m.\u001b[39mfire(\n\u001b[1;32m 201\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mon_loss_calculated\u001b[39m\u001b[38;5;124m\"\u001b[39m, step\u001b[38;5;241m=\u001b[39mstep, loss\u001b[38;5;241m=\u001b[39mloss\u001b[38;5;241m.\u001b[39mclone()\u001b[38;5;241m.\u001b[39mdetach()\n\u001b[1;32m 202\u001b[0m )\n\u001b[1;32m 203\u001b[0m loss\u001b[38;5;241m.\u001b[39mbackward()\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/recon/torch/modules/critic.py:68\u001b[0m, in \u001b[0;36mNNModuleCritic.__call__\u001b[0;34m(self, features, target_features)\u001b[0m\n\u001b[1;32m 67\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__call__\u001b[39m(\u001b[38;5;28mself\u001b[39m, features: _FeatureType, target_features: _FeatureType) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m torch\u001b[38;5;241m.\u001b[39mTensor:\n\u001b[0;32m---> 68\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mnn\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mModule\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[38;5;21;43m__call__\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfeatures\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget_features\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/torch/nn/modules/module.py:1518\u001b[0m, in \u001b[0;36mModule._wrapped_call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1516\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_compiled_call_impl(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs) \u001b[38;5;66;03m# type: ignore[misc]\u001b[39;00m\n\u001b[1;32m 1517\u001b[0m \u001b[38;5;28;01melse\u001b[39;00m:\n\u001b[0;32m-> 1518\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43m_call_impl\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/torch/nn/modules/module.py:1527\u001b[0m, in \u001b[0;36mModule._call_impl\u001b[0;34m(self, *args, **kwargs)\u001b[0m\n\u001b[1;32m 1522\u001b[0m \u001b[38;5;66;03m# If we don't have any hooks, we want to skip the rest of the logic in\u001b[39;00m\n\u001b[1;32m 1523\u001b[0m \u001b[38;5;66;03m# this function, and just call forward.\u001b[39;00m\n\u001b[1;32m 1524\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m (\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_forward_pre_hooks\n\u001b[1;32m 1525\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_backward_pre_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_backward_hooks\n\u001b[1;32m 1526\u001b[0m \u001b[38;5;129;01mor\u001b[39;00m _global_forward_hooks \u001b[38;5;129;01mor\u001b[39;00m _global_forward_pre_hooks):\n\u001b[0;32m-> 1527\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43mforward_call\u001b[49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43margs\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43mkwargs\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 1529\u001b[0m \u001b[38;5;28;01mtry\u001b[39;00m:\n\u001b[1;32m 1530\u001b[0m result \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/recon/torch/modules/critic.py:71\u001b[0m, in \u001b[0;36mNNModuleCritic.forward\u001b[0;34m(self, features, target_features)\u001b[0m\n\u001b[1;32m 70\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mforward\u001b[39m(\u001b[38;5;28mself\u001b[39m, features: _FeatureType, target_features: _FeatureType) \u001b[38;5;241m-\u001b[39m\u001b[38;5;241m>\u001b[39m torch\u001b[38;5;241m.\u001b[39mTensor:\n\u001b[0;32m---> 71\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mevaluate\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfeatures\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget_features\u001b[49m\u001b[43m)\u001b[49m\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/recon/torch/modules/critic.py:100\u001b[0m, in \u001b[0;36mLayerWiseAverageCritic.evaluate\u001b[0;34m(self, features, target_features)\u001b[0m\n\u001b[1;32m 98\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m layer_name, feature \u001b[38;5;129;01min\u001b[39;00m features\u001b[38;5;241m.\u001b[39mitems():\n\u001b[1;32m 99\u001b[0m target_feature \u001b[38;5;241m=\u001b[39m target_features[layer_name]\n\u001b[0;32m--> 100\u001b[0m layer_wise_loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;43mself\u001b[39;49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcompare_layer\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 101\u001b[0m \u001b[43m \u001b[49m\u001b[43mfeature\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget_feature\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlayer_name\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[43mlayer_name\u001b[49m\n\u001b[1;32m 102\u001b[0m \u001b[43m \u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 103\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_callback_handler\u001b[38;5;241m.\u001b[39mfire(\n\u001b[1;32m 104\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mon_layerwise_loss_calculated\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 105\u001b[0m layer_name\u001b[38;5;241m=\u001b[39mlayer_name,\n\u001b[1;32m 106\u001b[0m layer_loss\u001b[38;5;241m=\u001b[39mlayer_wise_loss,\n\u001b[1;32m 107\u001b[0m )\n\u001b[1;32m 108\u001b[0m loss \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m layer_wise_loss\n", + "File \u001b[0;32m/weka/proj-fmri/ckadirt/spurious_reconstruction/analysis/1_case_study/image-reconstruction/iCNN/dist_loss.py:83\u001b[0m, in \u001b[0;36mCombinationLoss.compare_layer\u001b[0;34m(self, feature, target_feature, layer_name)\u001b[0m\n\u001b[1;32m 81\u001b[0m loss \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m0.0\u001b[39m\n\u001b[1;32m 82\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m critic, weight \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(\u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mcritics, \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mweights):\n\u001b[0;32m---> 83\u001b[0m loss \u001b[38;5;241m+\u001b[39m\u001b[38;5;241m=\u001b[39m weight \u001b[38;5;241m*\u001b[39m \u001b[43mcritic\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mcompare_layer\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfeature\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mtarget_feature\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mlayer_name\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 84\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m loss\n", + "File \u001b[0;32m~/mindeye/lib/python3.11/site-packages/bdpy/recon/torch/modules/critic.py:185\u001b[0m, in \u001b[0;36mTargetNormalizedMSE.compare_layer\u001b[0;34m(self, feature, target_feature, layer_name)\u001b[0m\n\u001b[1;32m 168\u001b[0m \u001b[38;5;250m\u001b[39m\u001b[38;5;124;03m\"\"\"Loss function per layer.\u001b[39;00m\n\u001b[1;32m 169\u001b[0m \n\u001b[1;32m 170\u001b[0m \u001b[38;5;124;03mParameters\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m \u001b[38;5;124;03m Loss value of the layer specified by `layer_name`.\u001b[39;00m\n\u001b[1;32m 183\u001b[0m \u001b[38;5;124;03m\"\"\"\u001b[39;00m\n\u001b[1;32m 184\u001b[0m squared_norm \u001b[38;5;241m=\u001b[39m (target_feature \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39m \u001b[38;5;241m2\u001b[39m)\u001b[38;5;241m.\u001b[39msum(dim\u001b[38;5;241m=\u001b[39m\u001b[38;5;28mtuple\u001b[39m(\u001b[38;5;28mrange\u001b[39m(\u001b[38;5;241m1\u001b[39m, target_feature\u001b[38;5;241m.\u001b[39mndim)))\n\u001b[0;32m--> 185\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[43m(\u001b[49m\u001b[43m(\u001b[49m\u001b[43mfeature\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m-\u001b[39;49m\u001b[43m \u001b[49m\u001b[43mtarget_feature\u001b[49m\u001b[43m)\u001b[49m\u001b[43m \u001b[49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[38;5;241;43m*\u001b[39;49m\u001b[43m \u001b[49m\u001b[38;5;241;43m2\u001b[39;49m\u001b[43m)\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43msum\u001b[49m\u001b[43m(\u001b[49m\n\u001b[1;32m 186\u001b[0m \u001b[43m \u001b[49m\u001b[43mdim\u001b[49m\u001b[38;5;241;43m=\u001b[39;49m\u001b[38;5;28;43mtuple\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;28;43mrange\u001b[39;49m\u001b[43m(\u001b[49m\u001b[38;5;241;43m1\u001b[39;49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mfeature\u001b[49m\u001b[38;5;241;43m.\u001b[39;49m\u001b[43mndim\u001b[49m\u001b[43m)\u001b[49m\u001b[43m)\u001b[49m\n\u001b[1;32m 187\u001b[0m \u001b[43m\u001b[49m\u001b[43m)\u001b[49m \u001b[38;5;241m/\u001b[39m squared_norm\n", + "\u001b[0;31mKeyboardInterrupt\u001b[0m: " + ] + } + ], + "source": [ + "gen_rep = 10\n", + "gen_rep_shared_1000 = 5\n", + "\n", + "img_size = 3, 224, 224\n", + "# [10,44,66,265,342,488]\n", + "nsd_idx = 488\n", + "\n", + "# define output dirs\n", + "outdir_path_vision = f'./reconstructions/{run_name}/vision'\n", + "os.makedirs(outdir_path_vision, exist_ok=True)\n", + "\n", + "outdir_path_imagery = f'./reconstructions/{run_name}/imagery'\n", + "os.makedirs(outdir_path_imagery, exist_ok=True)\n", + "\n", + "outdir_path_shared_1000 = f'./reconstructions/{run_name}/shared_1000'\n", + "os.makedirs(outdir_path_shared_1000, exist_ok=True)\n", + "\n", + "\n", + "# # start with vision\n", + "\n", + "all_recon_vision = torch.zeros((gen_rep, decoded_features_vision['features[0]'].shape[0], 3, 224, 224))\n", + "\n", + "for rep in tqdm(range(gen_rep)):\n", + " rep_recon_vision = torch.zeros((decoded_features_vision['features[0]'].shape[0], 3, 224, 224))\n", + "\n", + " for im_index in tqdm(range(decoded_features_vision['features[0]'].shape[0])):\n", + "\n", + " target_features = {k:v[im_index].unsqueeze(0) for k,v in decoded_features_vision.items()}\n", + "\n", + " latent = BoundedLatent((1, latent_feat_num), partial(torch.nn.init.normal_, mean=0.0, std=0.01),\n", + " latent_upperbound, 0)\n", + "\n", + " latent.to(device)\n", + " latent.reset_states()\n", + " # %%\n", + " critic = CombinationLoss([LayerWiseDistsLoss(\n", + " alpha_dict=dists_alpha, beta_dict=dists_beta), TargetNormalizedMSE()], [alpha, beta])\n", + " # critic_image = TVloss(alpha=1e-07)\n", + "\n", + " optimizer = optim.AdamW(latent.parameters(), lr=0.1)\n", + " scheduler = None\n", + " # %%\n", + " pipeline = FeatureInversionTask(\n", + " encoder=encoder,\n", + " generator=generator,\n", + " latent=latent,\n", + " critic=critic,\n", + " # critic_image=critic_image,\n", + " optimizer=optimizer,\n", + " scheduler=scheduler,\n", + " num_iterations=800,\n", + " )\n", + "\n", + " pipeline.reset_states()\n", + " target_features_transformed = transform(target_features)\n", + " target_features = {\n", + " k: torch.Tensor(v).unsqueeze(0).to(device=device, dtype=data_type) for k, v in target_features_transformed.items()\n", + " }\n", + "\n", + " generated_images = image_domain.PILDomainWithExplicitCrop().receive(\n", + " pipeline(target_features)\n", + " )\n", + "\n", + " image = generated_images[0].detach().cpu().numpy()\n", + " pp_image = normalize_image(clip_extreme(image, pct=4))\n", + " # crop the image to 224x224\n", + " h, w, _ = image.shape\n", + " h_start = (h - 224) // 2\n", + " w_start = (w - 224) // 2\n", + " pp_image = pp_image[h_start: h_start + 224, w_start: w_start + 224]\n", + "\n", + " rep_recon_vision[im_index] = torch.Tensor(pp_image).permute(2, 0, 1).to('cpu')\n", + "\n", + " image = Image.fromarray(pp_image)\n", + " image.save(f'{outdir_path_vision}/idx_{im_index}_rep_{rep}.png')\n", + "\n", + " all_recon_vision[rep] = rep_recon_vision\n", + "\n", + "# save all_recon_vision\n", + "torch.save(all_recon_vision, f'{outdir_path_vision}/all_recon_vision.pt')\n", + "\n", + "# start with imagery\n", + "\n", + "all_recon_imagery = torch.zeros((gen_rep, decoded_features_imagery['features[0]'].shape[0], 3, 224, 224))\n", + "\n", + "for rep in tqdm(range(gen_rep)):\n", + " rep_recon_imagery = torch.zeros((decoded_features_imagery['features[0]'].shape[0], 3, 224, 224))\n", + "\n", + " for im_index in tqdm(range(decoded_features_imagery['features[0]'].shape[0])):\n", + "\n", + " target_features = {k:v[im_index].unsqueeze(0) for k,v in decoded_features_imagery.items()}\n", + "\n", + " latent = BoundedLatent((1, latent_feat_num), partial(torch.nn.init.normal_, mean=0.0, std=0.01),\n", + " latent_upperbound, 0)\n", + "\n", + " latent.to(device)\n", + " latent.reset_states()\n", + " # %%\n", + " critic = CombinationLoss([LayerWiseDistsLoss(\n", + " alpha_dict=dists_alpha, beta_dict=dists_beta), TargetNormalizedMSE()], [alpha, beta])\n", + " # critic_image = TVloss(alpha=1e-07)\n", + "\n", + " optimizer = optim.AdamW(latent.parameters(), lr=0.1)\n", + " scheduler = None\n", + " # %%\n", + " pipeline = FeatureInversionTask(\n", + " encoder=encoder,\n", + " generator=generator,\n", + " latent=latent,\n", + " critic=critic,\n", + " # critic_image=critic_image,\n", + " optimizer=optimizer,\n", + " scheduler=scheduler,\n", + " num_iterations=800,\n", + " )\n", + "\n", + " pipeline.reset_states()\n", + " target_features_transformed = transform(target_features)\n", + " target_features = {\n", + " k: torch.Tensor(v).unsqueeze(0).to(device=device, dtype=data_type) for k, v in target_features_transformed.items()\n", + " }\n", + "\n", + " generated_images = image_domain.PILDomainWithExplicitCrop().receive(\n", + " pipeline(target_features)\n", + " )\n", + "\n", + " image = generated_images[0].detach().cpu().numpy()\n", + " pp_image = normalize_image(clip_extreme(image, pct=4))\n", + " # crop the image to 224x224\n", + " h, w, _ = image.shape\n", + " h_start = (h - 224) // 2\n", + " w_start = (w - 224) // 2\n", + " pp_image = pp_image[h_start: h_start + 224, w_start: w_start + 224]\n", + "\n", + " rep_recon_imagery[im_index] = torch.Tensor(pp_image).permute(2, 0, 1).to('cpu')\n", + "\n", + " image = Image.fromarray(pp_image)\n", + " image.save(f'{outdir_path_imagery}/idx_{im_index}_rep_{rep}.png')\n", + "\n", + " all_recon_imagery[rep] = rep_recon_imagery\n", + "\n", + "# save all_recon_imagery\n", + "torch.save(all_recon_imagery, f'{outdir_path_imagery}/all_recon_imagery.pt')\n", + "\n", + "\n", + "# start with shared_1000\n", + "\n", + "# check if the tensor exist = torch.save(all_recon_shared_1000, f'{outdir_path_shared_1000}/all_recon_shared_1000.pt')\n", + "\n", + "if os.path.exists(f'{outdir_path_shared_1000}/all_recon_shared_1000.pt'):\n", + " all_recon_shared_1000 = torch.load(f'{outdir_path_shared_1000}/all_recon_shared_1000.pt')\n", + " # check the last index that is not zeros\n", + " for i in range(all_recon_shared_1000.shape[1]):\n", + " if torch.sum(all_recon_shared_1000[:,i]) == 0:\n", + " break\n", + " starting_nsd_index = i - 2\n", + "else:\n", + " all_recon_shared_1000 = torch.zeros((gen_rep_shared_1000, decoded_features_shared_1000['features[0]'].shape[0], 3, 224, 224))\n", + " starting_nsd_index = 0\n", + "\n", + "print(f\"Starting from index {starting_nsd_index}\")\n", + "\n", + "for im_index in tqdm(range(starting_nsd_index, decoded_features_shared_1000['features[0]'].shape[0])):\n", + "\n", + " image_recon_shared_1000 = torch.zeros((gen_rep_shared_1000, 3, 224, 224))\n", + "\n", + " for rep in tqdm(range(gen_rep_shared_1000)):\n", + "\n", + " target_features = {k:v[im_index].unsqueeze(0) for k,v in decoded_features_shared_1000.items()}\n", + "\n", + " latent = BoundedLatent((1, latent_feat_num), partial(torch.nn.init.normal_, mean=0.0, std=0.01),\n", + " latent_upperbound, 0)\n", + "\n", + " latent.to(device)\n", + " latent.reset_states()\n", + " # %%\n", + " critic = CombinationLoss([LayerWiseDistsLoss(\n", + " alpha_dict=dists_alpha, beta_dict=dists_beta), TargetNormalizedMSE()], [alpha, beta])\n", + " # critic_image = TVloss(alpha=1e-07)\n", + "\n", + " optimizer = optim.AdamW(latent.parameters(), lr=0.1)\n", + " scheduler = None\n", + " # %%\n", + " pipeline = FeatureInversionTask(\n", + " encoder=encoder,\n", + " generator=generator,\n", + " latent=latent,\n", + " critic=critic,\n", + " # critic_image=critic_image,\n", + " optimizer=optimizer,\n", + " scheduler=scheduler,\n", + " num_iterations=800,\n", + " )\n", + "\n", + " pipeline.reset_states()\n", + " target_features_transformed = transform(target_features)\n", + " target_features = {\n", + " k: torch.Tensor(v).unsqueeze(0).to(device=device, dtype=data_type) for k, v in target_features_transformed.items()\n", + " }\n", + "\n", + " generated_images = image_domain.PILDomainWithExplicitCrop().receive(\n", + " pipeline(target_features)\n", + " )\n", + "\n", + " image = generated_images[0].detach().cpu().numpy()\n", + " pp_image = normalize_image(clip_extreme(image, pct=4))\n", + " # crop the image to 224x224\n", + " h, w, _ = image.shape\n", + " h_start = (h - 224) // 2\n", + " w_start = (w - 224) // 2\n", + " pp_image = pp_image[h_start: h_start + 224, w_start: w_start + 224]\n", + "\n", + " image_recon_shared_1000[rep] = torch.Tensor(pp_image).permute(2, 0, 1).to('cpu')\n", + "\n", + " # image = Image.fromarray(pp_image)\n", + " # image.save(f'{outdir_path_shared_1000}/idx_{im_index}_rep_{rep}.png')\n", + "\n", + " all_recon_shared_1000[:, im_index, :, :, :] = image_recon_shared_1000\n", + "\n", + " if im_index % 50 == 0:\n", + " torch.save(all_recon_shared_1000, f'{outdir_path_shared_1000}/all_recon_shared_1000.pt')\n", + "\n", + "# save all_recon_shared_1000\n", + "torch.save(all_recon_shared_1000, f'{outdir_path_shared_1000}/all_recon_shared_1000.pt')" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "903b7786-7b4d-4607-881d-cc8550c769ce", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "18" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# decoded_features_vision['features[0]'].shape[0]" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "f9f25772-65e8-445d-9417-035626ec47c6", + "metadata": {}, + "outputs": [], + "source": [ + "# t = torch.zeros(5,100,224,224)\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "mindeye", + "language": "python", + "name": "mindeye" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/spurious_reconstruction/download.py b/spurious_reconstruction/download.py new file mode 100644 index 0000000000000000000000000000000000000000..69705abd006f2cb7c8867b0b2d3c2c8c92fb5645 --- /dev/null +++ b/spurious_reconstruction/download.py @@ -0,0 +1,66 @@ +import os +from glob import glob +import shutil +import argparse +import json +import urllib.request +import hashlib +from typing import Union + +from bdpy.dataset.utils import download_file +from tqdm import tqdm + + +def main(cfg): + with open(cfg.filelist, 'r') as f: + filelist = json.load(f) + + target = filelist[cfg.target] + + for fl in target['files']: + output = os.path.join(target['save_in'], fl['name']) + os.makedirs(target['save_in'], exist_ok=True) + + # Downloading + if not os.path.exists(output): + if isinstance(fl['url'], str): + print(f'Downloading {output} from {fl["url"]}') + download_file(fl['url'], output, progress_bar=True, md5sum=fl['md5sum']) + else: + # fl['url'] and fl['md5sum'] are lists + for i, (url, md5) in enumerate(zip(fl['url'], fl['md5sum'])): + output_chunk = output + f'.{i:04d}' + if os.path.exists(output_chunk): + continue + print(f'Downloading {output_chunk} from {url}') + download_file(url, output_chunk, progress_bar=True, md5sum=md5) + + # Postprocessing + if 'postproc' in fl: + for pp in fl['postproc']: + if pp['name'] == 'merge': + if os.path.exists(output): + continue + print(f'Merging {output}') + cat_files = sorted(glob(output + '.*')) + with open(output, 'wb') as f: + for cf in cat_files: + with open(cf, 'rb') as cf_f: + shutil.copyfileobj(cf_f, f) + elif pp['name'] == 'unzip': + print(f'Unzipping {output}') + if 'destination' in pp: + dest = pp['destination'] + else: + dest = './' + shutil.unpack_archive(output, extract_dir=dest) + + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--filelist', default='download_files.json') + parser.add_argument('target') + + cfg = parser.parse_args() + + main(cfg) \ No newline at end of file diff --git a/spurious_reconstruction/download_files.json b/spurious_reconstruction/download_files.json new file mode 100644 index 0000000000000000000000000000000000000000..1f1da6c997e5bb627a36fcc517c318858dc0e5af --- /dev/null +++ b/spurious_reconstruction/download_files.json @@ -0,0 +1,152 @@ +{ + + "image reconstruction results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "41ac287e5e51d52233d87704961907e2", + "name": "results-reconstruction.zip", + "url": "https://figshare.com/ndownloader/files/49414516", + "postproc": [{"name": "unzip", "destination": "./results"}]}]}, + + "UMAP visualization results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "8c7e5b1be5226299df69eb0c0e99d150", + "name": "results-res_umap.zip.01", + "url": "https://figshare.com/ndownloader/files/49498512", + "postproc": [{"name": "unzip", "destination": "./results_dl"}]}, + + {"md5sum": "6ba8edd0a375c47cabe33aab81452cf1", + "name": "results-res_umap.zip.02", + "url": "https://figshare.com/ndownloader/files/49501998", + "postproc": [{"name": "unzip", "destination": "./results"}]} + + ] + + + + }, + + "zero-shot identification results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "c740b087a0736c131172acec9e2510d4", + "name": "results-zero-shot_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414528", + "postproc": [{"name": "unzip", "destination": "./results"}]}]}, + + "hold-out results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "28e5b03e423bcce3a4166945f9d716c9", + "name": "results-cluster_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414489", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "f65d41cee676b748106d07a9f2b71c2f", + "name": "results-pairwise_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414513", + "postproc": [{"name": "unzip", "destination": "./results"}]} + + ]}, + "recovery_check results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "01d3a83f883e209433de40f868fdc5bb", + "name": "results-recovery_check.zip", + "url": "https://figshare.com/ndownloader/files/49414519"}], + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + "simulation results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "1c3efbf7a799e572527556ac2942f487", + "name": "results-simulation_cluster_identification_dim.zip", + "url": "https://figshare.com/ndownloader/files/49414522", + "postproc": [{"name": "unzip", "destination": "./results_dl"}]}, + + {"md5sum": "2b132281081e63ea47425eaf7c2e79af", + "name": "results-simulation_cluster_identification_ratio.zip", + "url": "https://figshare.com/ndownloader/files/49414525", + "postproc": [{"name": "unzip", "destination": "./results"}]} + + ]}, + + + "single layer recovery check results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "01d3a83f883e209433de40f868fdc5bb", + "name": "results-recovery_check.zip", + "url": "https://figshare.com/ndownloader/files/49414519", + "postproc": [{"name": "unzip", "destination": "./results"}]}]}, + + + + "all results": { + "zip": false, + "save_in": "./", + "files": [ + {"md5sum": "41ac287e5e51d52233d87704961907e2", + "name": "results-reconstruction.zip", + "url": "https://figshare.com/ndownloader/files/49414516", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"name": "results-res_umap.zip", + "md5sum": ["8c7e5b1be5226299df69eb0c0e99d150","6ba8edd0a375c47cabe33aab81452cf1"], + "url": ["https://figshare.com/ndownloader/files/49498512","https://figshare.com/ndownloader/files/49501998"], + "postproc": [{"name": "merge"}, + {"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "c740b087a0736c131172acec9e2510d4", + "name": "results-zero-shot_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414528", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "28e5b03e423bcce3a4166945f9d716c9", + "name": "results-cluster_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414489", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "f65d41cee676b748106d07a9f2b71c2f", + "name": "results-pairwise_identification.zip", + "url": "https://figshare.com/ndownloader/files/49414513", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "01d3a83f883e209433de40f868fdc5bb", + "name": "results-recovery_check.zip", + "url": "https://figshare.com/ndownloader/files/49414519", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "1c3efbf7a799e572527556ac2942f487", + "name": "results-simulation_cluster_identification_dim.zip", + "url": "https://figshare.com/ndownloader/files/49414522", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "2b132281081e63ea47425eaf7c2e79af", + "name": "results-simulation_cluster_identification_ratio.zip", + "url": "https://figshare.com/ndownloader/files/49414525", + "postproc": [{"name": "unzip", "destination": "./results"}]}, + + {"md5sum": "01d3a83f883e209433de40f868fdc5bb", + "name": "results-recovery_check.zip", + "url": "https://figshare.com/ndownloader/files/49414519", + "postproc": [{"name": "unzip", "destination": "./results"}]} + + ] + } + + + + + + + +} \ No newline at end of file diff --git a/spurious_reconstruction/pyproject.toml b/spurious_reconstruction/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..ffbfd5084ece2ef15336b907d6d3dbe6941b6d1b --- /dev/null +++ b/spurious_reconstruction/pyproject.toml @@ -0,0 +1,47 @@ +[project] +name = "spurious-reconstruction" +version = "0.1.0" +description = "Add your description here" +dependencies = [ + "torch==2.3.1+cu121", + "diffusers==0.14.0", + "bdpy==0.24.1", + "umap-learn>=0.5.6", + "pandas>=2.0.3", + "ipykernel>=6.29.4", + "matplotlib>=3.7.5", + "dash>=2.17.1", + "jupyter-dash>=0.4.2", + "nbformat>=5.10.4", + "seaborn>=0.13.2", + "natsort>=8.4.0", + "nibabel>=5.2.1", + "pycocotools>=2.0.7", + "transformers==4.42.4", + "torchvision==0.18.1+cu121", + "easydict>=1.13", + "einops>=0.8.0", + "omegaconf>=2.3.0", + "hydra-core>=1.3.2", + "fastl2lir>=0.10", +] +readme = "README.md" +requires-python = ">= 3.8" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.rye] +managed = true +dev-dependencies = [] + +[tool.hatch.metadata] +allow-direct-references = true + +[tool.hatch.build.targets.wheel] +packages = ["src/spurious_reconstruction"] + +[[tool.rye.sources]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cu121" diff --git a/spurious_reconstruction/requirements.lock b/spurious_reconstruction/requirements.lock new file mode 100644 index 0000000000000000000000000000000000000000..8dac841c2313e0bf9101eff36928fbaffc8af102 --- /dev/null +++ b/spurious_reconstruction/requirements.lock @@ -0,0 +1,362 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: false +# with-sources: false +# generate-hashes: false + +-e file:. +ansi2html==1.9.1 + # via jupyter-dash +antlr4-python3-runtime==4.9.3 + # via hydra-core + # via omegaconf +asttokens==2.4.1 + # via stack-data +attrs==23.2.0 + # via jsonschema + # via referencing +backcall==0.2.0 + # via ipython +bdpy==0.24.1 + # via spurious-reconstruction +blinker==1.8.2 + # via flask +certifi==2022.12.7 + # via requests +charset-normalizer==2.1.1 + # via requests +click==8.1.7 + # via flask +comm==0.2.2 + # via ipykernel +contourpy==1.1.1 + # via matplotlib +cycler==0.12.1 + # via matplotlib +dash==2.17.1 + # via jupyter-dash + # via spurious-reconstruction +dash-core-components==2.0.0 + # via dash +dash-html-components==2.0.0 + # via dash +dash-table==5.0.0 + # via dash +debugpy==1.8.1 + # via ipykernel +decorator==5.1.1 + # via ipython +diffusers==0.14.0 + # via spurious-reconstruction +easydict==1.13 + # via spurious-reconstruction +einops==0.8.0 + # via spurious-reconstruction +executing==2.0.1 + # via stack-data +fastjsonschema==2.20.0 + # via nbformat +fastl2lir==0.10 + # via spurious-reconstruction +filelock==3.13.1 + # via diffusers + # via huggingface-hub + # via torch + # via transformers + # via triton +flask==3.0.3 + # via dash + # via jupyter-dash +fonttools==4.53.0 + # via matplotlib +fsspec==2024.2.0 + # via huggingface-hub + # via torch +h5py==3.11.0 + # via bdpy + # via hdf5storage +hdf5storage==0.1.19 + # via bdpy +huggingface-hub==0.23.3 + # via diffusers + # via tokenizers + # via transformers +hydra-core==1.3.2 + # via spurious-reconstruction +idna==3.4 + # via requests +importlib-metadata==7.1.0 + # via dash + # via diffusers + # via flask + # via jupyter-client + # via numba +importlib-resources==6.4.0 + # via hydra-core + # via jsonschema + # via jsonschema-specifications + # via matplotlib + # via nibabel +ipykernel==6.29.4 + # via jupyter-dash + # via spurious-reconstruction +ipython==8.12.3 + # via ipykernel + # via jupyter-dash +itsdangerous==2.2.0 + # via flask +jedi==0.19.1 + # via ipython +jinja2==3.1.3 + # via flask + # via torch +joblib==1.4.2 + # via pynndescent + # via scikit-learn +jsonschema==4.22.0 + # via nbformat +jsonschema-specifications==2023.12.1 + # via jsonschema +jupyter-client==8.6.2 + # via ipykernel +jupyter-core==5.7.2 + # via ipykernel + # via jupyter-client + # via nbformat +jupyter-dash==0.4.2 + # via spurious-reconstruction +kiwisolver==1.4.5 + # via matplotlib +llvmlite==0.41.1 + # via numba + # via pynndescent +markupsafe==2.1.5 + # via jinja2 + # via werkzeug +matplotlib==3.7.5 + # via pycocotools + # via seaborn + # via spurious-reconstruction +matplotlib-inline==0.1.7 + # via ipykernel + # via ipython +mpmath==1.3.0 + # via sympy +natsort==8.4.0 + # via spurious-reconstruction +nbformat==5.10.4 + # via spurious-reconstruction +nest-asyncio==1.6.0 + # via dash + # via ipykernel + # via jupyter-dash +networkx==3.2.1 + # via torch +nibabel==5.2.1 + # via spurious-reconstruction +numba==0.58.1 + # via pynndescent + # via umap-learn +numpy==1.24.1 + # via bdpy + # via contourpy + # via diffusers + # via fastl2lir + # via h5py + # via hdf5storage + # via matplotlib + # via nibabel + # via numba + # via pandas + # via pycocotools + # via scikit-learn + # via scipy + # via seaborn + # via torchvision + # via transformers + # via umap-learn +nvidia-cublas-cu12==12.1.3.1 + # via nvidia-cudnn-cu12 + # via nvidia-cusolver-cu12 + # via torch +nvidia-cuda-cupti-cu12==12.1.105 + # via torch +nvidia-cuda-nvrtc-cu12==12.1.105 + # via torch +nvidia-cuda-runtime-cu12==12.1.105 + # via torch +nvidia-cudnn-cu12==8.9.2.26 + # via torch +nvidia-cufft-cu12==11.0.2.54 + # via torch +nvidia-curand-cu12==10.3.2.106 + # via torch +nvidia-cusolver-cu12==11.4.5.107 + # via torch +nvidia-cusparse-cu12==12.1.0.106 + # via nvidia-cusolver-cu12 + # via torch +nvidia-nccl-cu12==2.20.5 + # via torch +nvidia-nvjitlink-cu12==12.1.105 + # via nvidia-cusolver-cu12 + # via nvidia-cusparse-cu12 +nvidia-nvtx-cu12==12.1.105 + # via torch +omegaconf==2.3.0 + # via hydra-core + # via spurious-reconstruction +packaging==22.0 + # via huggingface-hub + # via hydra-core + # via ipykernel + # via matplotlib + # via nibabel + # via plotly + # via transformers +pandas==2.0.3 + # via bdpy + # via seaborn + # via spurious-reconstruction +parso==0.8.4 + # via jedi +pexpect==4.9.0 + # via ipython +pickleshare==0.7.5 + # via ipython +pillow==10.2.0 + # via diffusers + # via matplotlib + # via torchvision +pkgutil-resolve-name==1.3.10 + # via jsonschema +platformdirs==4.2.2 + # via jupyter-core +plotly==5.22.0 + # via dash +prompt-toolkit==3.0.47 + # via ipython +psutil==5.9.8 + # via ipykernel +ptyprocess==0.7.0 + # via pexpect +pure-eval==0.2.2 + # via stack-data +pycocotools==2.0.7 + # via spurious-reconstruction +pygments==2.18.0 + # via ipython +pynndescent==0.5.12 + # via umap-learn +pyparsing==3.1.2 + # via matplotlib +python-dateutil==2.9.0.post0 + # via jupyter-client + # via matplotlib + # via pandas +pytz==2024.1 + # via pandas +pyyaml==6.0.1 + # via bdpy + # via huggingface-hub + # via omegaconf + # via transformers +pyzmq==26.0.3 + # via ipykernel + # via jupyter-client +referencing==0.35.1 + # via jsonschema + # via jsonschema-specifications +regex==2024.5.15 + # via diffusers + # via transformers +requests==2.28.1 + # via dash + # via diffusers + # via huggingface-hub + # via jupyter-dash + # via transformers +retrying==1.3.4 + # via dash + # via jupyter-dash +rpds-py==0.18.1 + # via jsonschema + # via referencing +safetensors==0.4.4 + # via transformers +scikit-learn==1.3.2 + # via bdpy + # via pynndescent + # via umap-learn +scipy==1.10.1 + # via bdpy + # via pynndescent + # via scikit-learn + # via umap-learn +seaborn==0.13.2 + # via spurious-reconstruction +setuptools==70.0.0 + # via dash +six==1.16.0 + # via asttokens + # via python-dateutil + # via retrying +stack-data==0.6.3 + # via ipython +sympy==1.12 + # via torch +tenacity==8.4.1 + # via plotly +threadpoolctl==3.5.0 + # via scikit-learn +tokenizers==0.19.1 + # via transformers +torch==2.3.1+cu121 + # via spurious-reconstruction + # via torchvision +torchvision==0.18.1+cu121 + # via spurious-reconstruction +tornado==6.4.1 + # via ipykernel + # via jupyter-client +tqdm==4.64.1 + # via bdpy + # via fastl2lir + # via huggingface-hub + # via transformers + # via umap-learn +traitlets==5.14.3 + # via comm + # via ipykernel + # via ipython + # via jupyter-client + # via jupyter-core + # via matplotlib-inline + # via nbformat +transformers==4.42.4 + # via spurious-reconstruction +triton==2.3.1 + # via torch +typing-extensions==4.9.0 + # via dash + # via huggingface-hub + # via ipython + # via torch +tzdata==2024.1 + # via pandas +umap-learn==0.5.6 + # via spurious-reconstruction +urllib3==1.26.13 + # via requests +wcwidth==0.2.13 + # via prompt-toolkit +werkzeug==3.0.3 + # via dash + # via flask +zipp==3.19.2 + # via importlib-metadata + # via importlib-resources