jcm/metrics.py (198 lines of code) (raw):
# Code modified from https://github.com/GaParmar/clean-fid/blob/main/cleanfid/fid.py
# Original license below:
# MIT License
#
# Copyright (c) 2021 Gaurav Parmar
#
# 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.
import torch
from cleanfid import fid
import torchvision
import numpy as np
import flax
import logging
from . import checkpoints
import tqdm
import time
import jax
import os
import io
import blobfile
import json
import uuid
import requests
import torch_fidelity
class ResizeDataset(torch.utils.data.Dataset):
"""
A placeholder Dataset that enables parallelizing the resize operation
using multiple CPU cores
files: list of all files in the folder
fn_resize: function that takes an np_array as input [0,255]
"""
def __init__(self, files, mode, size=(299, 299), fdir=None):
self.files = files
self.fdir = fdir
self.transforms = torchvision.transforms.ToTensor()
self.size = size
self.fn_resize = fid.build_resizer(mode)
self.custom_image_tranform = lambda x: x
def __len__(self):
return len(self.files)
def __getitem__(self, i):
img_np = self.files[i]
# apply a custom image transform before resizing the image to 299x299
img_np = self.custom_image_tranform(img_np)
# fn_resize expects a np array and returns a np array
img_resized = self.fn_resize(img_np)
# ToTensor() converts to [0,1] only if input in uint8
if img_resized.dtype == "uint8":
img_t = self.transforms(np.array(img_resized)) * 255
elif img_resized.dtype == "float32":
img_t = self.transforms(img_resized)
return img_t
class TorchDataset(torch.utils.data.Dataset):
"""
A placeholder Dataset that enables parallelizing the resize operation
using multiple CPU cores
files: list of all files in the folder
fn_resize: function that takes an np_array as input [0,255]
"""
def __init__(self, files, mode, size=(299, 299), fdir=None):
self.files = files
self.fdir = fdir
self.transforms = torchvision.transforms.ToTensor()
self.size = size
self.fn_resize = fid.build_resizer(mode)
self.custom_image_tranform = lambda x: x
def __len__(self):
return len(self.files)
def __getitem__(self, i):
img_np = self.files[i]
# apply a custom image transform before resizing the image to 299x299
img_np = self.custom_image_tranform(img_np)
# fn_resize expects a np array and returns a np array
img_resized = self.fn_resize(img_np)
# ToTensor() converts to [0,1] only if input in uint8
assert img_resized.dtype == "uint8"
img_t = (self.transforms(np.array(img_resized)) * 255).to(torch.uint8)
return img_t
def compute_fid(
samples,
feat_model,
dataset_name="cifar10",
dataset_res=32,
dataset_split="train",
batch_size=1024,
num_workers=12,
mode="legacy_tensorflow",
device=torch.device("cuda:0"),
):
dataset = ResizeDataset(samples, mode=mode)
dataloader = torch.utils.data.DataLoader(
dataset,
batch_size=batch_size,
shuffle=False,
drop_last=False,
num_workers=num_workers,
)
l_feats = []
for batch in tqdm.tqdm(dataloader):
l_feats.append(fid.get_batch_features(batch, feat_model, device))
np_feats = np.concatenate(l_feats)
mu = np.mean(np_feats, axis=0)
sigma = np.cov(np_feats, rowvar=False)
ref_mu, ref_sigma = fid.get_reference_statistics(
dataset_name, dataset_res, mode=mode, seed=0, split=dataset_split
)
score = fid.frechet_distance(mu, sigma, ref_mu, ref_sigma)
return score
def compute_all_metrics(
samples,
dataset_name="cifar10-train",
mode="legacy_tensorflow",
batch_size=1024,
num_workers=12,
):
dataset = TorchDataset(samples, mode=mode)
metrics_dict = torch_fidelity.calculate_metrics(
input1=dataset,
input2=dataset_name,
cuda=True,
isc=True,
fid=True,
verbose=False,
)
return metrics_dict
def get_samples_from_ckpt(folder, ckpt):
files = list(
blobfile.glob(os.path.join(folder, f"ckpt_{ckpt}_host_*", "samples_*.npz"))
)
all_samples = []
for file in files:
with blobfile.BlobFile(file, "rb") as fin:
all_samples.append(np.load(fin)["samples"])
if len(all_samples) >= 1:
all_samples = np.concatenate(all_samples)
else:
all_samples = np.zeros((0, 32, 32, 3), dtype=np.uint8)
return all_samples
def get_fids(folder, ckpt_range, mode, device):
ckpts = []
fids = []
feat_model = fid.build_feature_extractor(mode, device)
for ckpt in ckpt_range:
ckpts.append(ckpt)
print("Loading samples from ckpt", ckpt)
data = get_samples_from_ckpt(folder, ckpt)
print(f"data.shape: {data.shape}")
fids.append(
compute_fid(
data[:50000],
mode="legacy_tensorflow",
device=device,
feat_model=feat_model,
)
)
print("FID", fids[-1])
return ckpts, fids
def compute_metrics(
config,
workdir,
eval_folder,
mode="legacy_tensorflow",
device=torch.device("cuda:0"),
):
"""Compute the FID metrics from given samples.
Args:
config (dict): The config dict.
workdir (str): The working directory.
eval_folder (str): The folder to store the evaluation results.
"""
eval_dir = os.path.join(workdir, eval_folder)
blobfile.makedirs(eval_dir)
@flax.struct.dataclass
class MetricsMeta:
ckpt_id: int
metrics_meta = MetricsMeta(
ckpt_id=config.eval.begin_ckpt,
)
metrics_meta = checkpoints.restore_checkpoint(
eval_dir, metrics_meta, step=None, prefix="metrics_meta_"
)
feat_model = fid.build_feature_extractor(mode, device)
begin_ckpt = max(metrics_meta.ckpt_id, config.eval.begin_ckpt)
for ckpt in range(begin_ckpt, config.eval.end_ckpt + 1):
print(f"Start metric evaluation for ckpt {ckpt}")
all_samples = get_samples_from_ckpt(eval_dir, ckpt)
waiting_message_printed = False
while all_samples.shape[0] < config.eval.num_samples:
if not waiting_message_printed and jax.process_index() == 0:
logging.warning(f"Waiting for the arrival of samples for ckpt {ckpt}")
waiting_message_printed = True
time.sleep(100)
all_samples = get_samples_from_ckpt(eval_dir, ckpt)
fid_score = compute_fid(
all_samples[: config.eval.num_samples],
mode=mode,
device=device,
feat_model=feat_model,
)
with blobfile.BlobFile(
os.path.join(eval_dir, f"metrics_{ckpt}.npz"),
"wb",
) as fout:
io_buffer = io.BytesIO()
np.savez_compressed(io_buffer, fid=fid_score)
fout.write(io_buffer.getvalue())
metrics_meta = metrics_meta.replace(ckpt_id=ckpt + 1)
checkpoints.save_checkpoint(
eval_dir, metrics_meta, step=ckpt, keep=1, prefix="metrics_meta_"
)
meta_files = blobfile.glob(os.path.join(eval_dir, "metrics_meta_*.npz"))
for file in meta_files:
blobfile.remove(file)
def obtain_feature_extractor(mode="legacy_tensorflow", device=torch.device("cuda:0")):
return fid.build_feature_extractor(mode, device)
def compute_fid_jupyter(
all_samples,
feature_extractor,
mode="legacy_tensorflow",
device=torch.device("cuda:0"),
):
"""Compute the FID metrics from given samples.
Args:
config (dict): The config dict.
workdir (str): The working directory.
eval_folder (str): The folder to store the evaluation results.
"""
feat_model = feature_extractor
fid_score = compute_fid(
all_samples,
mode=mode,
device=device,
feat_model=feat_model,
)
return fid_score