climateGAN / climategan /trainer.py
vict0rsch's picture
add stable diffusion painter for gradio app
cd31093
raw
history blame
71.5 kB
"""
Main component: the trainer handles everything:
* initializations
* training
* saving
"""
import inspect
import warnings
from copy import deepcopy
from pathlib import Path
from time import time
import numpy as np
from comet_ml import ExistingExperiment, Experiment
warnings.simplefilter("ignore", UserWarning)
import torch
import torch.nn as nn
from addict import Dict
from torch import autograd, sigmoid, softmax
from torch.cuda.amp import GradScaler, autocast
from tqdm import tqdm
from climategan.data import get_all_loaders
from climategan.discriminator import OmniDiscriminator, create_discriminator
from climategan.eval_metrics import accuracy, mIOU
from climategan.fid import compute_val_fid
from climategan.fire import add_fire
from climategan.generator import OmniGenerator, create_generator
from climategan.logger import Logger
from climategan.losses import get_losses
from climategan.optim import get_optimizer
from climategan.transforms import DiffTransforms
from climategan.tutils import (
divide_pred,
get_num_params,
get_WGAN_gradient,
lrgb2srgb,
normalize,
print_num_parameters,
shuffle_batch_tuple,
srgb2lrgb,
vgg_preprocess,
zero_grad,
)
from climategan.utils import (
comet_kwargs,
div_dict,
find_target_size,
flatten_opts,
get_display_indices,
get_existing_comet_id,
get_latest_opts,
merge,
resolve,
sum_dict,
Timer,
)
try:
import torch_xla.core.xla_model as xm # type: ignore
except ImportError:
pass
class Trainer:
"""Main trainer class"""
def __init__(self, opts, comet_exp=None, verbose=0, device=None):
"""Trainer class to gather various model training procedures
such as training evaluating saving and logging
init:
* creates an addict.Dict logger
* creates logger.exp as a comet_exp experiment if `comet` arg is True
* sets the device (1 GPU or CPU)
Args:
opts (addict.Dict): options to configure the trainer, the data, the models
comet (bool, optional): whether to log the trainer with comet.ml.
Defaults to False.
verbose (int, optional): printing level to debug. Defaults to 0.
"""
super().__init__()
self.opts = opts
self.verbose = verbose
self.logger = Logger(self)
self.losses = None
self.G = self.D = None
self.real_val_fid_stats = None
self.use_pl4m = False
self.is_setup = False
self.loaders = self.all_loaders = None
self.exp = None
self.current_mode = "train"
self.diff_transforms = None
self.kitti_pretrain = self.opts.train.kitti.pretrain
self.pseudo_training_tasks = set(self.opts.train.pseudo.tasks)
self.lr_names = {}
self.base_display_images = {}
self.kitty_display_images = {}
self.domain_labels = {"s": 0, "r": 1}
self.device = device or torch.device(
"cuda:0" if torch.cuda.is_available() else "cpu"
)
if isinstance(comet_exp, Experiment):
self.exp = comet_exp
if self.opts.train.amp:
optimizers = [
self.opts.gen.opt.optimizer.lower(),
self.opts.dis.opt.optimizer.lower(),
]
if "extraadam" in optimizers:
raise ValueError(
"AMP does not work with ExtraAdam ({})".format(optimizers)
)
self.grad_scaler_d = GradScaler()
self.grad_scaler_g = GradScaler()
# -------------------------------
# ----- Legacy Overwrites -----
# -------------------------------
if (
self.opts.gen.s.depth_feat_fusion is True
or self.opts.gen.s.depth_dada_fusion is True
):
self.opts.gen.s.use_dada = True
@torch.no_grad()
def paint_and_mask(self, image_batch, mask_batch=None, resolution="approx"):
"""
Paints a batch of images (or a single image with a batch dim of 1). If
masks are not provided, they are inferred from the masker.
Resolution can either be the train-time resolution or the closest
multiple of 2 ** spade_n_up
Operations performed without gradient
If resolution == "approx" then the output image has the shape:
(dim // 2 ** spade_n_up) * 2 ** spade_n_up, for dim in [height, width]
eg: (1000, 1300) => (896, 1280) for spade_n_up = 7
If resolution == "exact" then the output image has the same shape:
we first process in "approx" mode then upsample bilinear
If resolution == "basic" image output shape is the train-time's
(typically 640x640)
If resolution == "upsample" image is inferred as "basic" and
then upsampled to original size
Args:
image_batch (torch.Tensor): 4D batch of images to flood
mask_batch (torch.Tensor, optional): Masks for the images.
Defaults to None (infer with Masker).
resolution (str, optional): "approx", "exact" or False
Returns:
torch.Tensor: N x C x H x W where H and W depend on `resolution`
"""
assert resolution in {"approx", "exact", "basic", "upsample"}
previous_mode = self.current_mode
if previous_mode == "train":
self.eval_mode()
if mask_batch is None:
mask_batch = self.G.mask(x=image_batch)
else:
assert len(image_batch) == len(mask_batch)
assert image_batch.shape[-2:] == mask_batch.shape[-2:]
if resolution not in {"approx", "exact"}:
painted = self.G.paint(mask_batch, image_batch)
if resolution == "upsample":
painted = nn.functional.interpolate(
painted, size=image_batch.shape[-2:], mode="bilinear"
)
else:
# save latent shape
zh = self.G.painter.z_h
zw = self.G.painter.z_w
# adapt latent shape to approximately keep the resolution
self.G.painter.z_h = (
image_batch.shape[-2] // 2**self.opts.gen.p.spade_n_up
)
self.G.painter.z_w = (
image_batch.shape[-1] // 2**self.opts.gen.p.spade_n_up
)
painted = self.G.paint(mask_batch, image_batch)
self.G.painter.z_h = zh
self.G.painter.z_w = zw
if resolution == "exact":
painted = nn.functional.interpolate(
painted, size=image_batch.shape[-2:], mode="bilinear"
)
if previous_mode == "train":
self.train_mode()
return painted
def _p(self, *args, **kwargs):
"""
verbose-dependant print util
"""
if self.verbose > 0:
print(*args, **kwargs)
@torch.no_grad()
def infer_all(
self,
x,
numpy=True,
stores={},
bin_value=-1,
half=False,
xla=False,
cloudy=True,
auto_resize_640=False,
ignore_event=set(),
return_masks=False,
):
"""
Create a dictionnary of events from a numpy or tensor,
single or batch image data.
stores is a dictionnary of times for the Timer class.
bin_value is used to binarize (or not) flood masks
"""
assert self.is_setup
assert len(x.shape) in {3, 4}, f"Unknown Data shape {x.shape}"
# convert numpy to tensor
if not isinstance(x, torch.Tensor):
x = torch.tensor(x, device=self.device)
# add batch dimension
if len(x.shape) == 3:
x.unsqueeze_(0)
# permute channels as second dimension
if x.shape[1] != 3:
assert x.shape[-1] == 3, f"Unknown x shape to permute {x.shape}"
x = x.permute(0, 3, 1, 2)
# send to device
if x.device != self.device:
x = x.to(self.device)
# interpolate to standard input size
if auto_resize_640 and (x.shape[-1] != 640 or x.shape[-2] != 640):
x = torch.nn.functional.interpolate(x, (640, 640), mode="bilinear")
if half:
x = x.half()
# adjust painter's latent vector
self.G.painter.set_latent_shape(x.shape, True)
with Timer(store=stores.get("all events", [])):
# encode
with Timer(store=stores.get("encode", [])):
z = self.G.encode(x)
if xla:
xm.mark_step()
# predict from masker
with Timer(store=stores.get("depth", [])):
depth, z_depth = self.G.decoders["d"](z)
if xla:
xm.mark_step()
with Timer(store=stores.get("segmentation", [])):
segmentation = self.G.decoders["s"](z, z_depth)
if xla:
xm.mark_step()
with Timer(store=stores.get("mask", [])):
cond = self.G.make_m_cond(depth, segmentation, x)
mask = self.G.mask(z=z, cond=cond, z_depth=z_depth)
if xla:
xm.mark_step()
# apply events
if "wildfire" not in ignore_event:
with Timer(store=stores.get("wildfire", [])):
wildfire = self.compute_fire(x, seg_preds=segmentation)
if "smog" not in ignore_event:
with Timer(store=stores.get("smog", [])):
smog = self.compute_smog(x, d=depth, s=segmentation)
if "flood" not in ignore_event:
with Timer(store=stores.get("flood", [])):
flood = self.compute_flood(
x,
m=mask,
s=segmentation,
cloudy=cloudy,
bin_value=bin_value,
)
if xla:
xm.mark_step()
output_data = {}
if numpy:
with Timer(store=stores.get("numpy", [])):
if "flood" not in ignore_event:
# normalize to 0-1
flood = normalize(flood).cpu()
# convert to numpy
flood = flood.permute(0, 2, 3, 1).numpy()
# convert to 0-255 uint8
flood = (flood * 255).astype(np.uint8)
output_data["flood"] = flood
if "wildfire" not in ignore_event:
wildfire = normalize(wildfire).cpu()
wildfire = wildfire.permute(0, 2, 3, 1).numpy()
wildfire = (wildfire * 255).astype(np.uint8)
output_data["wildfire"] = wildfire
if "smog" not in ignore_event:
smog = normalize(smog).cpu()
smog = smog.permute(0, 2, 3, 1).numpy()
smog = (smog * 255).astype(np.uint8)
output_data["smog"] = smog
if return_masks:
output_data["mask"] = (
((mask > bin_value) * 255).cpu().numpy().astype(np.uint8)
)
return output_data
@classmethod
def resume_from_path(
cls,
path,
overrides={},
setup=True,
inference=False,
new_exp=False,
device=None,
verbose=1,
):
"""
Resume and optionally setup a trainer from a specific path,
using the latest opts and checkpoint. Requires path to contain opts.yaml
(or increased), url.txt (or increased) and checkpoints/
Args:
path (str | pathlib.Path): Trainer to resume
overrides (dict, optional): Override loaded opts with those. Defaults to {}.
setup (bool, optional): Wether or not to setup the trainer before
returning it. Defaults to True.
inference (bool, optional): Setup should be done in inference mode or not.
Defaults to False.
new_exp (bool, optional): Re-use existing comet exp in path or create
a new one? Defaults to False.
device (torch.device, optional): Device to use
Returns:
climategan.Trainer: Loaded and resumed trainer
"""
p = resolve(path)
assert p.exists()
c = p / "checkpoints"
assert c.exists() and c.is_dir()
opts = get_latest_opts(p)
opts = Dict(merge(overrides, opts))
opts.train.resume = True
if new_exp is None:
exp = None
elif new_exp is True:
exp = Experiment(project_name="climategan", **comet_kwargs)
exp.log_asset_folder(
str(resolve(Path(__file__)).parent),
recursive=True,
log_file_name=True,
)
exp.log_parameters(flatten_opts(opts))
else:
comet_id = get_existing_comet_id(p)
exp = ExistingExperiment(previous_experiment=comet_id, **comet_kwargs)
trainer = cls(opts, comet_exp=exp, device=device, verbose=verbose)
if setup:
trainer.setup(inference=inference)
return trainer
def save(self):
save_dir = Path(self.opts.output_path) / Path("checkpoints")
save_dir.mkdir(exist_ok=True)
save_path = save_dir / "latest_ckpt.pth"
# Construct relevant state dicts / optims:
# Save at least G
save_dict = {
"epoch": self.logger.epoch,
"G": self.G.state_dict(),
"g_opt": self.g_opt.state_dict(),
"step": self.logger.global_step,
}
if self.D is not None and get_num_params(self.D) > 0:
save_dict["D"] = self.D.state_dict()
save_dict["d_opt"] = self.d_opt.state_dict()
if (
self.logger.epoch >= self.opts.train.min_save_epoch
and self.logger.epoch % self.opts.train.save_n_epochs == 0
):
torch.save(save_dict, save_dir / f"epoch_{self.logger.epoch}_ckpt.pth")
torch.save(save_dict, save_path)
def resume(self, inference=False):
tpu = "xla" in str(self.device)
if tpu:
print("Resuming on TPU:", self.device)
m_path = Path(self.opts.load_paths.m)
p_path = Path(self.opts.load_paths.p)
pm_path = Path(self.opts.load_paths.pm)
output_path = Path(self.opts.output_path)
map_loc = self.device if not tpu else "cpu"
if "m" in self.opts.tasks and "p" in self.opts.tasks:
# ----------------------------------------
# ----- Masker and Painter Loading -----
# ----------------------------------------
# want to resume a pm model but no path was provided:
# resume a single pm model from output_path
if all([str(p) == "none" for p in [m_path, p_path, pm_path]]):
checkpoint_path = output_path / "checkpoints/latest_ckpt.pth"
print("Resuming P+M model from", str(checkpoint_path))
checkpoint = torch.load(checkpoint_path, map_location=map_loc)
# want to resume a pm model with a pm_path provided:
# resume a single pm model from load_paths.pm
# depending on whether a dir or a file is specified
elif str(pm_path) != "none":
assert pm_path.exists()
if pm_path.is_dir():
checkpoint_path = pm_path / "checkpoints/latest_ckpt.pth"
else:
assert pm_path.suffix == ".pth"
checkpoint_path = pm_path
print("Resuming P+M model from", str(checkpoint_path))
checkpoint = torch.load(checkpoint_path, map_location=map_loc)
# want to resume a pm model, pm_path not provided:
# m_path and p_path must be provided as dirs or pth files
elif m_path != p_path:
assert m_path.exists()
assert p_path.exists()
if m_path.is_dir():
m_path = m_path / "checkpoints/latest_ckpt.pth"
if p_path.is_dir():
p_path = p_path / "checkpoints/latest_ckpt.pth"
assert m_path.suffix == ".pth"
assert p_path.suffix == ".pth"
print(f"Resuming P+M model from \n -{p_path} \nand \n -{m_path}")
m_checkpoint = torch.load(m_path, map_location=map_loc)
p_checkpoint = torch.load(p_path, map_location=map_loc)
checkpoint = merge(m_checkpoint, p_checkpoint)
else:
raise ValueError(
"Cannot resume a P+M model with provided load_paths:\n{}".format(
self.opts.load_paths
)
)
else:
# ----------------------------------
# ----- Single Model Loading -----
# ----------------------------------
# cannot specify both paths
if str(m_path) != "none" and str(p_path) != "none":
raise ValueError(
"Opts tasks are {} but received 2 values for the load_paths".format(
self.opts.tasks
)
)
# specified m
elif str(m_path) != "none":
assert m_path.exists()
assert "m" in self.opts.tasks
model = "M"
if m_path.is_dir():
m_path = m_path / "checkpoints/latest_ckpt.pth"
checkpoint_path = m_path
# specified m
elif str(p_path) != "none":
assert p_path.exists()
assert "p" in self.opts.tasks
model = "P"
if p_path.is_dir():
p_path = p_path / "checkpoints/latest_ckpt.pth"
checkpoint_path = p_path
# specified neither p nor m: resume from output_path
else:
model = "P" if "p" in self.opts.tasks else "M"
checkpoint_path = output_path / "checkpoints/latest_ckpt.pth"
print(f"Resuming {model} model from {checkpoint_path}")
checkpoint = torch.load(checkpoint_path, map_location=map_loc)
# On TPUs must send the data to the xla device as it cannot be mapped
# there directly from torch.load
if tpu:
checkpoint = xm.send_cpu_data_to_device(checkpoint, self.device)
# -----------------------
# ----- Restore G -----
# -----------------------
if inference:
incompatible_keys = self.G.load_state_dict(checkpoint["G"], strict=False)
if incompatible_keys.missing_keys:
print("WARNING: Missing keys in self.G.load_state_dict, keeping inits")
print(incompatible_keys.missing_keys)
if incompatible_keys.unexpected_keys:
print("WARNING: Ignoring Unexpected keys in self.G.load_state_dict")
print(incompatible_keys.unexpected_keys)
else:
self.G.load_state_dict(checkpoint["G"])
if inference:
# only G is needed to infer
print("Done loading checkpoints.")
return
self.g_opt.load_state_dict(checkpoint["g_opt"])
# ------------------------------
# ----- Resume scheduler -----
# ------------------------------
# https://discuss.pytorch.org/t/a-problem-occured-when-resuming-an-optimizer/28822
for _ in range(self.logger.epoch + 1):
self.update_learning_rates()
# -----------------------
# ----- Restore D -----
# -----------------------
if self.D is not None and get_num_params(self.D) > 0:
self.D.load_state_dict(checkpoint["D"])
self.d_opt.load_state_dict(checkpoint["d_opt"])
# ---------------------------
# ----- Resore logger -----
# ---------------------------
self.logger.epoch = checkpoint["epoch"]
self.logger.global_step = checkpoint["step"]
self.exp.log_text(
"Resuming from epoch {} & step {}".format(
checkpoint["epoch"], checkpoint["step"]
)
)
# Round step to even number for extraGradient
if self.logger.global_step % 2 != 0:
self.logger.global_step += 1
def eval_mode(self):
"""
Set trainer's models in eval mode
"""
if self.G is not None:
self.G.eval()
if self.D is not None:
self.D.eval()
self.current_mode = "eval"
def train_mode(self):
"""
Set trainer's models in train mode
"""
if self.G is not None:
self.G.train()
if self.D is not None:
self.D.train()
self.current_mode = "train"
def assert_z_matches_x(self, x, z):
assert x.shape[0] == (
z.shape[0] if not isinstance(z, (list, tuple)) else z[0].shape[0]
), "x-> {}, z->{}".format(
x.shape, z.shape if not isinstance(z, (list, tuple)) else z[0].shape
)
def batch_to_device(self, b):
"""sends the data in b to self.device
Args:
b (dict): the batch dictionnay
Returns:
dict: the batch dictionnary with its "data" field sent to self.device
"""
for task, tensor in b["data"].items():
b["data"][task] = tensor.to(self.device)
return b
def sample_painter_z(self, batch_size):
return self.G.sample_painter_z(batch_size, self.device)
@property
def train_loaders(self):
"""Get a zip of all training loaders
Returns:
generator: zip generator yielding tuples:
(batch_rf, batch_rn, batch_sf, batch_sn)
"""
return zip(*list(self.loaders["train"].values()))
@property
def val_loaders(self):
"""Get a zip of all validation loaders
Returns:
generator: zip generator yielding tuples:
(batch_rf, batch_rn, batch_sf, batch_sn)
"""
return zip(*list(self.loaders["val"].values()))
def compute_latent_shape(self):
"""Compute the latent shape, i.e. the Encoder's output shape,
from a batch.
Raises:
ValueError: If no loader, the latent_shape cannot be inferred
Returns:
tuple: (c, h, w)
"""
x = None
for mode in self.all_loaders:
for domain in self.all_loaders.loaders[mode]:
x = (
self.all_loaders[mode][domain]
.dataset[0]["data"]["x"]
.to(self.device)
)
break
if x is not None:
break
if x is None:
raise ValueError("No batch found to compute_latent_shape")
x = x.unsqueeze(0)
z = self.G.encode(x)
return z.shape[1:] if not isinstance(z, (list, tuple)) else z[0].shape[1:]
def g_opt_step(self):
"""Run an optimizing step ; if using ExtraAdam, there needs to be an extrapolation
step every other step
"""
if "extra" in self.opts.gen.opt.optimizer.lower() and (
self.logger.global_step % 2 == 0
):
self.g_opt.extrapolation()
else:
self.g_opt.step()
def d_opt_step(self):
"""Run an optimizing step ; if using ExtraAdam, there needs to be an extrapolation
step every other step
"""
if "extra" in self.opts.dis.opt.optimizer.lower() and (
self.logger.global_step % 2 == 0
):
self.d_opt.extrapolation()
else:
self.d_opt.step()
def update_learning_rates(self):
if self.g_scheduler is not None:
self.g_scheduler.step()
if self.d_scheduler is not None:
self.d_scheduler.step()
def setup(self, inference=False):
"""Prepare the trainer before it can be used to train the models:
* initialize G and D
* creates 2 optimizers
"""
self.logger.global_step = 0
start_time = time()
self.logger.time.start_time = start_time
verbose = self.verbose
if not inference:
self.all_loaders = get_all_loaders(self.opts)
# -----------------------
# ----- Generator -----
# -----------------------
__t = time()
print("Creating generator...")
self.G: OmniGenerator = create_generator(
self.opts, device=self.device, no_init=inference, verbose=verbose
)
self.has_painter = get_num_params(self.G.painter) or self.G.load_val_painter()
if self.has_painter:
self.G.painter.set_latent_shape(find_target_size(self.opts, "x"), True)
print(f"Generator OK in {time() - __t:.1f}s.")
if inference: # Inference mode: no more than a Generator needed
print("Inference mode: no Discriminator, no optimizers")
print_num_parameters(self)
self.switch_data(to="base")
if self.opts.train.resume:
self.resume(True)
self.eval_mode()
print("Trainer is in evaluation mode.")
print("Setup done.")
self.is_setup = True
return
# ---------------------------
# ----- Discriminator -----
# ---------------------------
self.D: OmniDiscriminator = create_discriminator(
self.opts, self.device, verbose=verbose
)
print("Discriminator OK.")
print_num_parameters(self)
# --------------------------
# ----- Optimization -----
# --------------------------
# Get different optimizers for each task (different learning rates)
self.g_opt, self.g_scheduler, self.lr_names["G"] = get_optimizer(
self.G, self.opts.gen.opt, self.opts.tasks
)
if get_num_params(self.D) > 0:
self.d_opt, self.d_scheduler, self.lr_names["D"] = get_optimizer(
self.D, self.opts.dis.opt, self.opts.tasks, True
)
else:
self.d_opt, self.d_scheduler = None, None
self.losses = get_losses(self.opts, verbose, device=self.device)
if "p" in self.opts.tasks and self.opts.gen.p.diff_aug.use:
self.diff_transforms = DiffTransforms(self.opts.gen.p.diff_aug)
if verbose > 0:
for mode, mode_dict in self.all_loaders.items():
for domain, domain_loader in mode_dict.items():
print(
"Loader {} {} : {}".format(
mode, domain, len(domain_loader.dataset)
)
)
# ----------------------------
# ----- Display images -----
# ----------------------------
self.set_display_images()
# -------------------------------
# ----- Log Architectures -----
# -------------------------------
self.logger.log_architecture()
# -----------------------------
# ----- Set data source -----
# -----------------------------
if self.kitti_pretrain:
self.switch_data(to="kitti")
else:
self.switch_data(to="base")
# -------------------------
# ----- Setup Done. -----
# -------------------------
print(" " * 50, end="\r")
print("Done creating display images")
if self.opts.train.resume:
print("Resuming Model (inference: False)")
self.resume(False)
else:
print("Not resuming: starting a new model")
print("Setup done.")
self.is_setup = True
def switch_data(self, to="kitti"):
caller = inspect.stack()[1].function
print(f"[{caller}] Switching data source to", to)
self.data_source = to
if to == "kitti":
self.display_images = self.kitty_display_images
if self.all_loaders is not None:
self.loaders = {
mode: {"s": self.all_loaders[mode]["kitti"]}
for mode in self.all_loaders
}
else:
self.display_images = self.base_display_images
if self.all_loaders is not None:
self.loaders = {
mode: {
domain: self.all_loaders[mode][domain]
for domain in self.all_loaders[mode]
if domain != "kitti"
}
for mode in self.all_loaders
}
if (
self.logger.global_step % 2 != 0
and "extra" in self.opts.dis.opt.optimizer.lower()
):
print(
"Warning: artificially bumping step to run an extrapolation step first."
)
self.logger.global_step += 1
def set_display_images(self, use_all=False):
for mode, mode_dict in self.all_loaders.items():
if self.kitti_pretrain:
self.kitty_display_images[mode] = {}
self.base_display_images[mode] = {}
for domain in mode_dict:
if self.kitti_pretrain and domain == "kitti":
target_dict = self.kitty_display_images
else:
if domain == "kitti":
continue
target_dict = self.base_display_images
dataset = self.all_loaders[mode][domain].dataset
display_indices = (
get_display_indices(self.opts, domain, len(dataset))
if not use_all
else list(range(len(dataset)))
)
ldis = len(display_indices)
print(
f" Creating {ldis} {mode} {domain} display images...",
end="\r",
flush=True,
)
target_dict[mode][domain] = [
Dict(dataset[i])
for i in display_indices
if (print(f"({i})", end="\r") is None and i < len(dataset))
]
if self.exp is not None:
for im_id, d in enumerate(target_dict[mode][domain]):
self.exp.log_parameter(
"display_image_{}_{}_{}".format(mode, domain, im_id),
d["paths"],
)
def train(self):
"""For each epoch:
* train
* eval
* save
"""
assert self.is_setup
for self.logger.epoch in range(
self.logger.epoch, self.logger.epoch + self.opts.train.epochs
):
# backprop painter's disc loss to masker
if (
self.logger.epoch == self.opts.gen.p.pl4m_epoch
and get_num_params(self.G.painter) > 0
and "p" in self.opts.tasks
and self.opts.gen.m.use_pl4m
):
print(
"\n\n >>> Enabling pl4m at epoch {}\n\n".format(self.logger.epoch)
)
self.use_pl4m = True
self.run_epoch()
self.run_evaluation(verbose=1)
self.save()
# end vkitti2 pre-training
if self.logger.epoch == self.opts.train.kitti.epochs - 1:
self.switch_data(to="base")
self.kitti_pretrain = False
# end pseudo training
if self.logger.epoch == self.opts.train.pseudo.epochs - 1:
self.pseudo_training_tasks = set()
def run_epoch(self):
"""Runs an epoch:
* checks trainer is setup
* gets a tuple of batches per domain
* sends batches to device
* updates sequentially G, D
"""
assert self.is_setup
self.train_mode()
if self.exp is not None:
self.exp.log_parameter("epoch", self.logger.epoch)
epoch_len = min(len(loader) for loader in self.loaders["train"].values())
epoch_desc = "Epoch {}".format(self.logger.epoch)
self.logger.time.epoch_start = time()
for multi_batch_tuple in tqdm(
self.train_loaders,
desc=epoch_desc,
total=epoch_len,
mininterval=0.5,
unit="batch",
):
self.logger.time.step_start = time()
multi_batch_tuple = shuffle_batch_tuple(multi_batch_tuple)
# The `[0]` is because the domain is contained in a list
multi_domain_batch = {
batch["domain"][0]: self.batch_to_device(batch)
for batch in multi_batch_tuple
}
# ------------------------------
# ----- Update Generator -----
# ------------------------------
# freeze params of the discriminator
if self.d_opt is not None:
for param in self.D.parameters():
param.requires_grad = False
self.update_G(multi_domain_batch)
# ----------------------------------
# ----- Update Discriminator -----
# ----------------------------------
# unfreeze params of the discriminator
if self.d_opt is not None and not self.kitti_pretrain:
for param in self.D.parameters():
param.requires_grad = True
self.update_D(multi_domain_batch)
# -------------------------
# ----- Log Metrics -----
# -------------------------
self.logger.global_step += 1
self.logger.log_step_time(time())
if not self.kitti_pretrain:
self.update_learning_rates()
self.logger.log_learning_rates()
self.logger.log_epoch_time(time())
def update_G(self, multi_domain_batch, verbose=0):
"""Perform an update on g from multi_domain_batch which is a dictionary
domain => batch
* automatic mixed precision according to self.opts.train.amp
* compute loss for each task
* loss.backward()
* g_opt_step()
* g_opt.step() or .extrapolation() depending on self.logger.global_step
* logs losses on comet.ml with self.logger.log_losses(model_to_update="G")
Args:
multi_domain_batch (dict): dictionnary of domain batches
"""
zero_grad(self.G)
if self.opts.train.amp:
with autocast():
g_loss = self.get_G_loss(multi_domain_batch, verbose)
self.grad_scaler_g.scale(g_loss).backward()
self.grad_scaler_g.step(self.g_opt)
self.grad_scaler_g.update()
else:
g_loss = self.get_G_loss(multi_domain_batch, verbose)
g_loss.backward()
self.g_opt_step()
self.logger.log_losses(model_to_update="G", mode="train")
def update_D(self, multi_domain_batch, verbose=0):
zero_grad(self.D)
if self.opts.train.amp:
with autocast():
d_loss = self.get_D_loss(multi_domain_batch, verbose)
self.grad_scaler_d.scale(d_loss).backward()
self.grad_scaler_d.step(self.d_opt)
self.grad_scaler_d.update()
else:
d_loss = self.get_D_loss(multi_domain_batch, verbose)
d_loss.backward()
self.d_opt_step()
self.logger.losses.disc.total_loss = d_loss.item()
self.logger.log_losses(model_to_update="D", mode="train")
def get_D_loss(self, multi_domain_batch, verbose=0):
"""Compute the discriminators' losses:
* for each domain-specific batch:
* encode the image
* get the conditioning tensor if using spade
* source domain is the data's domain, sequentially r|s then f|n
* get the target domain accordingly
* compute the translated image from the data
* compute the source domain discriminator's loss on the data
* compute the target domain discriminator's loss on the translated image
# ? In this setting, each D[decoder][domain] is updated twice towards
# real or fake data
See readme's update d section for details
Args:
multi_domain_batch ([type]): [description]
Returns:
[type]: [description]
"""
disc_loss = {
"m": {"Advent": 0},
"s": {"Advent": 0},
}
if self.opts.dis.p.use_local_discriminator:
disc_loss["p"] = {"global": 0, "local": 0}
else:
disc_loss["p"] = {"gan": 0}
for domain, batch in multi_domain_batch.items():
x = batch["data"]["x"]
# ---------------------
# ----- Painter -----
# ---------------------
if domain == "rf" and self.has_painter:
m = batch["data"]["m"]
# sample vector
with torch.no_grad():
# see spade compute_discriminator_loss
fake = self.G.paint(m, x)
if self.opts.gen.p.diff_aug.use:
fake = self.diff_transforms(fake)
x = self.diff_transforms(x)
fake = fake.detach()
fake.requires_grad_()
if self.opts.dis.p.use_local_discriminator:
fake_d_global = self.D["p"]["global"](fake)
real_d_global = self.D["p"]["global"](x)
fake_d_local = self.D["p"]["local"](fake * m)
real_d_local = self.D["p"]["local"](x * m)
global_loss = self.losses["D"]["p"](fake_d_global, False, True)
global_loss += self.losses["D"]["p"](real_d_global, True, True)
local_loss = self.losses["D"]["p"](fake_d_local, False, True)
local_loss += self.losses["D"]["p"](real_d_local, True, True)
disc_loss["p"]["global"] += global_loss
disc_loss["p"]["local"] += local_loss
else:
real_cat = torch.cat([m, x], axis=1)
fake_cat = torch.cat([m, fake], axis=1)
real_fake_cat = torch.cat([real_cat, fake_cat], dim=0)
real_fake_d = self.D["p"](real_fake_cat)
real_d, fake_d = divide_pred(real_fake_d)
disc_loss["p"]["gan"] = self.losses["D"]["p"](fake_d, False, True)
disc_loss["p"]["gan"] += self.losses["D"]["p"](real_d, True, True)
# --------------------
# ----- Masker -----
# --------------------
else:
z = self.G.encode(x)
s_pred = d_pred = cond = z_depth = None
if "s" in batch["data"]:
if "d" in self.opts.tasks and self.opts.gen.s.use_dada:
d_pred, z_depth = self.G.decoders["d"](z)
step_loss, s_pred = self.masker_s_loss(
x, z, d_pred, z_depth, None, domain, for_="D"
)
step_loss *= self.opts.train.lambdas.advent.adv_main
disc_loss["s"]["Advent"] += step_loss
if "m" in batch["data"]:
if "d" in self.opts.tasks:
if self.opts.gen.m.use_spade:
if d_pred is None:
d_pred, z_depth = self.G.decoders["d"](z)
cond = self.G.make_m_cond(d_pred, s_pred, x)
elif self.opts.gen.m.use_dada:
if d_pred is None:
d_pred, z_depth = self.G.decoders["d"](z)
step_loss, _ = self.masker_m_loss(
x,
z,
None,
domain,
for_="D",
cond=cond,
z_depth=z_depth,
depth_preds=d_pred,
)
step_loss *= self.opts.train.lambdas.advent.adv_main
disc_loss["m"]["Advent"] += step_loss
self.logger.losses.disc.update(
{
dom: {
k: v.item() if isinstance(v, torch.Tensor) else v
for k, v in d.items()
}
for dom, d in disc_loss.items()
}
)
loss = sum(v for d in disc_loss.values() for k, v in d.items())
return loss
def get_G_loss(self, multi_domain_batch, verbose=0):
m_loss = p_loss = None
# For now, always compute "representation loss"
g_loss = 0
if any(t in self.opts.tasks for t in "msd"):
m_loss = self.get_masker_loss(multi_domain_batch)
self.logger.losses.gen.masker = m_loss.item()
g_loss += m_loss
if "p" in self.opts.tasks and not self.kitti_pretrain:
p_loss = self.get_painter_loss(multi_domain_batch)
self.logger.losses.gen.painter = p_loss.item()
g_loss += p_loss
assert g_loss != 0 and not isinstance(g_loss, int), "No update in get_G_loss!"
self.logger.losses.gen.total_loss = g_loss.item()
return g_loss
def get_masker_loss(self, multi_domain_batch): # TODO update docstrings
"""Only update the representation part of the model, meaning everything
but the translation part
* for each batch in available domains:
* compute task-specific losses
* compute the adaptation and translation decoders' auto-encoding losses
* compute the adaptation decoder's translation losses (GAN and Cycle)
Args:
multi_domain_batch (dict): dictionnary mapping domain names to batches from
the trainer's loaders
Returns:
torch.Tensor: scalar loss tensor, weighted according to opts.train.lambdas
"""
m_loss = 0
for domain, batch in multi_domain_batch.items():
# We don't care about the flooded domain here
if domain == "rf":
continue
x = batch["data"]["x"]
z = self.G.encode(x)
# --------------------------------------
# ----- task-specific losses (2) -----
# --------------------------------------
d_pred = s_pred = z_depth = None
for task in ["d", "s", "m"]:
if task not in batch["data"]:
continue
target = batch["data"][task]
if task == "d":
loss, d_pred, z_depth = self.masker_d_loss(
x, z, target, domain, "G"
)
m_loss += loss
self.logger.losses.gen.task["d"][domain] = loss.item()
elif task == "s":
loss, s_pred = self.masker_s_loss(
x, z, d_pred, z_depth, target, domain, "G"
)
m_loss += loss
self.logger.losses.gen.task["s"][domain] = loss.item()
elif task == "m":
cond = None
if self.opts.gen.m.use_spade:
if not self.opts.gen.m.detach:
d_pred = d_pred.clone()
s_pred = s_pred.clone()
cond = self.G.make_m_cond(d_pred, s_pred, x)
loss, _ = self.masker_m_loss(
x,
z,
target,
domain,
"G",
cond=cond,
z_depth=z_depth,
depth_preds=d_pred,
)
m_loss += loss
self.logger.losses.gen.task["m"][domain] = loss.item()
return m_loss
def get_painter_loss(self, multi_domain_batch):
"""Computes the translation loss when flooding/deflooding images
Args:
multi_domain_batch (dict): dictionnary mapping domain names to batches from
the trainer's loaders
Returns:
torch.Tensor: scalar loss tensor, weighted according to opts.train.lambdas
"""
step_loss = 0
# self.g_opt.zero_grad()
lambdas = self.opts.train.lambdas
batch_domain = "rf"
batch = multi_domain_batch[batch_domain]
x = batch["data"]["x"]
# ! different mask: hides water to be reconstructed
# ! 1 for water, 0 otherwise
m = batch["data"]["m"]
fake_flooded = self.G.paint(m, x)
# ----------------------
# ----- VGG Loss -----
# ----------------------
if lambdas.G.p.vgg != 0:
loss = self.losses["G"]["p"]["vgg"](
vgg_preprocess(fake_flooded * m), vgg_preprocess(x * m)
)
loss *= lambdas.G.p.vgg
self.logger.losses.gen.p.vgg = loss.item()
step_loss += loss
# ---------------------
# ----- TV Loss -----
# ---------------------
if lambdas.G.p.tv != 0:
loss = self.losses["G"]["p"]["tv"](fake_flooded * m)
loss *= lambdas.G.p.tv
self.logger.losses.gen.p.tv = loss.item()
step_loss += loss
# --------------------------
# ----- Context Loss -----
# --------------------------
if lambdas.G.p.context != 0:
loss = self.losses["G"]["p"]["context"](fake_flooded, x, m)
loss *= lambdas.G.p.context
self.logger.losses.gen.p.context = loss.item()
step_loss += loss
# ---------------------------------
# ----- Reconstruction Loss -----
# ---------------------------------
if lambdas.G.p.reconstruction != 0:
loss = self.losses["G"]["p"]["reconstruction"](fake_flooded, x, m)
loss *= lambdas.G.p.reconstruction
self.logger.losses.gen.p.reconstruction = loss.item()
step_loss += loss
# -------------------------------------
# ----- Local & Global GAN Loss -----
# -------------------------------------
if self.opts.gen.p.diff_aug.use:
fake_flooded = self.diff_transforms(fake_flooded)
x = self.diff_transforms(x)
if self.opts.dis.p.use_local_discriminator:
fake_d_global = self.D["p"]["global"](fake_flooded)
fake_d_local = self.D["p"]["local"](fake_flooded * m)
real_d_global = self.D["p"]["global"](x)
# Note: discriminator returns [out_1,...,out_num_D] outputs
# Each out_i is a list [feat1, feat2, ..., pred_i]
self.logger.losses.gen.p.gan = 0
loss = self.losses["G"]["p"]["gan"](fake_d_global, True, False)
loss += self.losses["G"]["p"]["gan"](fake_d_local, True, False)
loss *= lambdas.G["p"]["gan"]
self.logger.losses.gen.p.gan = loss.item()
step_loss += loss
# -----------------------------------
# ----- Feature Matching Loss -----
# -----------------------------------
# (only on global discriminator)
# Order must be real, fake
if self.opts.dis.p.get_intermediate_features:
loss = self.losses["G"]["p"]["featmatch"](real_d_global, fake_d_global)
loss *= lambdas.G["p"]["featmatch"]
if isinstance(loss, float):
self.logger.losses.gen.p.featmatch = loss
else:
self.logger.losses.gen.p.featmatch = loss.item()
step_loss += loss
# -------------------------------------------
# ----- Single Discriminator GAN Loss -----
# -------------------------------------------
else:
real_cat = torch.cat([m, x], axis=1)
fake_cat = torch.cat([m, fake_flooded], axis=1)
real_fake_cat = torch.cat([real_cat, fake_cat], dim=0)
real_fake_d = self.D["p"](real_fake_cat)
real_d, fake_d = divide_pred(real_fake_d)
loss = self.losses["G"]["p"]["gan"](fake_d, True, False)
self.logger.losses.gen.p.gan = loss.item()
step_loss += loss
# -----------------------------------
# ----- Feature Matching Loss -----
# -----------------------------------
if self.opts.dis.p.get_intermediate_features and lambdas.G.p.featmatch != 0:
loss = self.losses["G"]["p"]["featmatch"](real_d, fake_d)
loss *= lambdas.G.p.featmatch
if isinstance(loss, float):
self.logger.losses.gen.p.featmatch = loss
else:
self.logger.losses.gen.p.featmatch = loss.item()
step_loss += loss
return step_loss
def masker_d_loss(self, x, z, target, domain, for_="G"):
assert for_ in {"G", "D"}
self.assert_z_matches_x(x, z)
assert x.shape[0] == target.shape[0]
zero_loss = torch.tensor(0.0, device=self.device)
weight = self.opts.train.lambdas.G.d.main
prediction, z_depth = self.G.decoders["d"](z)
if self.opts.gen.d.classify.enable:
target.squeeze_(1)
full_loss = self.losses["G"]["tasks"]["d"](prediction, target)
full_loss *= weight
if weight == 0 or (domain == "r" and "d" not in self.pseudo_training_tasks):
return zero_loss, prediction, z_depth
return full_loss, prediction, z_depth
def masker_s_loss(self, x, z, depth_preds, z_depth, target, domain, for_="G"):
assert for_ in {"G", "D"}
assert domain in {"r", "s"}
self.assert_z_matches_x(x, z)
assert x.shape[0] == target.shape[0] if target is not None else True
full_loss = torch.tensor(0.0, device=self.device)
softmax_preds = None
# --------------------------
# ----- Segmentation -----
# --------------------------
pred = None
if for_ == "G" or self.opts.gen.s.use_advent:
pred = self.G.decoders["s"](z, z_depth)
# Supervised segmentation loss: crossent for sim domain,
# crossent_pseudo for real ; loss is crossent in any case
if for_ == "G":
if domain == "s" or "s" in self.pseudo_training_tasks:
if domain == "s":
logger = self.logger.losses.gen.task["s"]["crossent"]
weight = self.opts.train.lambdas.G["s"]["crossent"]
else:
logger = self.logger.losses.gen.task["s"]["crossent_pseudo"]
weight = self.opts.train.lambdas.G["s"]["crossent_pseudo"]
if weight != 0:
# Cross-Entropy loss
loss_func = self.losses["G"]["tasks"]["s"]["crossent"]
loss = loss_func(pred, target.squeeze(1))
loss *= weight
full_loss += loss
logger[domain] = loss.item()
if domain == "r":
weight = self.opts.train.lambdas.G["s"]["minent"]
if self.opts.gen.s.use_minent and weight != 0:
softmax_preds = softmax(pred, dim=1)
# Entropy minimization loss
loss = self.losses["G"]["tasks"]["s"]["minent"](softmax_preds)
loss *= weight
full_loss += loss
self.logger.losses.gen.task["s"]["minent"]["r"] = loss.item()
# Fool ADVENT discriminator
if self.opts.gen.s.use_advent:
if self.opts.gen.s.use_dada and depth_preds is not None:
depth_preds = depth_preds.detach()
else:
depth_preds = None
if for_ == "D":
domain_label = domain
logger = {}
loss_func = self.losses["D"]["advent"]
pred = pred.detach()
weight = self.opts.train.lambdas.advent.adv_main
else:
domain_label = "s"
logger = self.logger.losses.gen.task["s"]["advent"]
loss_func = self.losses["G"]["tasks"]["s"]["advent"]
weight = self.opts.train.lambdas.G["s"]["advent"]
if (for_ == "D" or domain == "r") and weight != 0:
if softmax_preds is None:
softmax_preds = softmax(pred, dim=1)
loss = loss_func(
softmax_preds,
self.domain_labels[domain_label],
self.D["s"]["Advent"],
depth_preds,
)
loss *= weight
full_loss += loss
logger[domain] = loss.item()
if for_ == "D":
# WGAN: clipping or GP
if self.opts.dis.s.gan_type == "GAN" or "WGAN_norm":
pass
elif self.opts.dis.s.gan_type == "WGAN":
for p in self.D["s"]["Advent"].parameters():
p.data.clamp_(
self.opts.dis.s.wgan_clamp_lower,
self.opts.dis.s.wgan_clamp_upper,
)
elif self.opts.dis.s.gan_type == "WGAN_gp":
prob_need_grad = autograd.Variable(pred, requires_grad=True)
d_out = self.D["s"]["Advent"](prob_need_grad)
gp = get_WGAN_gradient(prob_need_grad, d_out)
gp_loss = gp * self.opts.train.lambdas.advent.WGAN_gp
full_loss += gp_loss
else:
raise NotImplementedError
return full_loss, pred
def masker_m_loss(
self, x, z, target, domain, for_="G", cond=None, z_depth=None, depth_preds=None
):
assert for_ in {"G", "D"}
assert domain in {"r", "s"}
self.assert_z_matches_x(x, z)
assert x.shape[0] == target.shape[0] if target is not None else True
full_loss = torch.tensor(0.0, device=self.device)
pred_logits = self.G.decoders["m"](z, cond=cond, z_depth=z_depth)
pred_prob = sigmoid(pred_logits)
pred_prob_complementary = 1 - pred_prob
prob = torch.cat([pred_prob, pred_prob_complementary], dim=1)
if for_ == "G":
# TV loss
weight = self.opts.train.lambdas.G.m.tv
if weight != 0:
loss = self.losses["G"]["tasks"]["m"]["tv"](pred_prob)
loss *= weight
full_loss += loss
self.logger.losses.gen.task["m"]["tv"][domain] = loss.item()
weight = self.opts.train.lambdas.G.m.bce
if domain == "s" and weight != 0:
# CrossEnt Loss
loss = self.losses["G"]["tasks"]["m"]["bce"](pred_logits, target)
loss *= weight
full_loss += loss
self.logger.losses.gen.task["m"]["bce"]["s"] = loss.item()
if domain == "r":
weight = self.opts.train.lambdas.G["m"]["gi"]
if self.opts.gen.m.use_ground_intersection and weight != 0:
# GroundIntersection loss
loss = self.losses["G"]["tasks"]["m"]["gi"](pred_prob, target)
loss *= weight
full_loss += loss
self.logger.losses.gen.task["m"]["gi"]["r"] = loss.item()
weight = self.opts.train.lambdas.G.m.pl4m
if self.use_pl4m and weight != 0:
# Painter loss
pl4m_loss = self.painter_loss_for_masker(x, pred_prob)
pl4m_loss *= weight
full_loss += pl4m_loss
self.logger.losses.gen.task.m.pl4m.r = pl4m_loss.item()
weight = self.opts.train.lambdas.advent.ent_main
if self.opts.gen.m.use_minent and weight != 0:
# MinEnt loss
loss = self.losses["G"]["tasks"]["m"]["minent"](prob)
loss *= weight
full_loss += loss
self.logger.losses.gen.task["m"]["minent"]["r"] = loss.item()
if self.opts.gen.m.use_advent:
# AdvEnt loss
if self.opts.gen.m.use_dada and depth_preds is not None:
depth_preds = depth_preds.detach()
depth_preds = torch.nn.functional.interpolate(
depth_preds, size=x.shape[-2:], mode="nearest"
)
else:
depth_preds = None
if for_ == "D":
domain_label = domain
logger = {}
loss_func = self.losses["D"]["advent"]
prob = prob.detach()
weight = self.opts.train.lambdas.advent.adv_main
else:
domain_label = "s"
logger = self.logger.losses.gen.task["m"]["advent"]
loss_func = self.losses["G"]["tasks"]["m"]["advent"]
weight = self.opts.train.lambdas.advent.adv_main
if (for_ == "D" or domain == "r") and weight != 0:
loss = loss_func(
prob.to(self.device),
self.domain_labels[domain_label],
self.D["m"]["Advent"],
depth_preds,
)
loss *= weight
full_loss += loss
logger[domain] = loss.item()
if for_ == "D":
# WGAN: clipping or GP
if self.opts.dis.m.gan_type == "GAN" or "WGAN_norm":
pass
elif self.opts.dis.m.gan_type == "WGAN":
for p in self.D["s"]["Advent"].parameters():
p.data.clamp_(
self.opts.dis.m.wgan_clamp_lower,
self.opts.dis.m.wgan_clamp_upper,
)
elif self.opts.dis.m.gan_type == "WGAN_gp":
prob_need_grad = autograd.Variable(prob, requires_grad=True)
d_out = self.D["s"]["Advent"](prob_need_grad)
gp = get_WGAN_gradient(prob_need_grad, d_out)
gp_loss = self.opts.train.lambdas.advent.WGAN_gp * gp
full_loss += gp_loss
else:
raise NotImplementedError
return full_loss, prob
def painter_loss_for_masker(self, x, m):
# pl4m loss
# painter should not be updated
for param in self.G.painter.parameters():
param.requires_grad = False
# TODO for param in self.D.painter.parameters():
# param.requires_grad = False
fake_flooded = self.G.paint(m, x)
if self.opts.dis.p.use_local_discriminator:
fake_d_global = self.D["p"]["global"](fake_flooded)
fake_d_local = self.D["p"]["local"](fake_flooded * m)
# Note: discriminator returns [out_1,...,out_num_D] outputs
# Each out_i is a list [feat1, feat2, ..., pred_i]
pl4m_loss = self.losses["G"]["p"]["gan"](fake_d_global, True, False)
pl4m_loss += self.losses["G"]["p"]["gan"](fake_d_local, True, False)
else:
real_cat = torch.cat([m, x], axis=1)
fake_cat = torch.cat([m, fake_flooded], axis=1)
real_fake_cat = torch.cat([real_cat, fake_cat], dim=0)
real_fake_d = self.D["p"](real_fake_cat)
_, fake_d = divide_pred(real_fake_d)
pl4m_loss = self.losses["G"]["p"]["gan"](fake_d, True, False)
if "p" in self.opts.tasks:
for param in self.G.painter.parameters():
param.requires_grad = True
return pl4m_loss
@torch.no_grad()
def run_evaluation(self, verbose=0):
print("******************* Running Evaluation ***********************")
start_time = time()
self.eval_mode()
val_logger = None
nb_of_batches = None
for i, multi_batch_tuple in enumerate(self.val_loaders):
# create a dictionnary (domain => batch) from tuple
# (batch_domain_0, ..., batch_domain_i)
# and send it to self.device
nb_of_batches = i + 1
multi_domain_batch = {
batch["domain"][0]: self.batch_to_device(batch)
for batch in multi_batch_tuple
}
self.get_G_loss(multi_domain_batch, verbose)
if val_logger is None:
val_logger = deepcopy(self.logger.losses.generator)
else:
val_logger = sum_dict(val_logger, self.logger.losses.generator)
val_logger = div_dict(val_logger, nb_of_batches)
self.logger.losses.generator = val_logger
self.logger.log_losses(model_to_update="G", mode="val")
for d in self.opts.domains:
self.logger.log_comet_images("train", d)
self.logger.log_comet_images("val", d)
if "m" in self.opts.tasks and self.has_painter and not self.kitti_pretrain:
self.logger.log_comet_combined_images("train", "r")
self.logger.log_comet_combined_images("val", "r")
if self.exp is not None:
print()
if "m" in self.opts.tasks or "s" in self.opts.tasks:
self.eval_images("val", "r")
self.eval_images("val", "s")
if "p" in self.opts.tasks and not self.kitti_pretrain:
val_fid = compute_val_fid(self)
if self.exp is not None:
self.exp.log_metric("val_fid", val_fid, step=self.logger.global_step)
else:
print("Validation FID Score", val_fid)
self.train_mode()
timing = int(time() - start_time)
print("****************** Done in {}s *********************".format(timing))
def eval_images(self, mode, domain):
if domain == "s" and self.kitti_pretrain:
domain = "kitti"
if domain == "rf" or domain not in self.display_images[mode]:
return
metric_funcs = {"accuracy": accuracy, "mIOU": mIOU}
metric_avg_scores = {"m": {}}
if "s" in self.opts.tasks:
metric_avg_scores["s"] = {}
if "d" in self.opts.tasks and domain == "s" and self.opts.gen.d.classify.enable:
metric_avg_scores["d"] = {}
for key in metric_funcs:
for task in metric_avg_scores:
metric_avg_scores[task][key] = []
for im_set in self.display_images[mode][domain]:
x = im_set["data"]["x"].unsqueeze(0).to(self.device)
z = self.G.encode(x)
s_pred = d_pred = z_depth = None
if "d" in metric_avg_scores:
d_pred, z_depth = self.G.decoders["d"](z)
d_pred = d_pred.detach().cpu()
if domain == "s":
d = im_set["data"]["d"].unsqueeze(0).detach()
for metric in metric_funcs:
metric_score = metric_funcs[metric](d_pred, d)
metric_avg_scores["d"][metric].append(metric_score)
if "s" in metric_avg_scores:
if z_depth is None:
if self.opts.gen.s.use_dada and "d" in self.opts.tasks:
_, z_depth = self.G.decoders["d"](z)
s_pred = self.G.decoders["s"](z, z_depth).detach().cpu()
s = im_set["data"]["s"].unsqueeze(0).detach()
for metric in metric_funcs:
metric_score = metric_funcs[metric](s_pred, s)
metric_avg_scores["s"][metric].append(metric_score)
if "m" in self.opts:
cond = None
if s_pred is not None and d_pred is not None:
cond = self.G.make_m_cond(d_pred, s_pred, x)
if z_depth is None:
if self.opts.gen.m.use_dada and "d" in self.opts.tasks:
_, z_depth = self.G.decoders["d"](z)
pred_mask = (
(self.G.mask(z=z, cond=cond, z_depth=z_depth)).detach().cpu()
)
pred_mask = (pred_mask > 0.5).to(torch.float32)
pred_prob = torch.cat([1 - pred_mask, pred_mask], dim=1)
m = im_set["data"]["m"].unsqueeze(0).detach()
for metric in metric_funcs:
if metric != "mIOU":
metric_score = metric_funcs[metric](pred_mask, m)
else:
metric_score = metric_funcs[metric](pred_prob, m)
metric_avg_scores["m"][metric].append(metric_score)
metric_avg_scores = {
task: {
metric: np.mean(values) if values else float("nan")
for metric, values in met_dict.items()
}
for task, met_dict in metric_avg_scores.items()
}
metric_avg_scores = {
task: {
metric: value if not np.isnan(value) else -1
for metric, value in met_dict.items()
}
for task, met_dict in metric_avg_scores.items()
}
if self.exp is not None:
self.exp.log_metrics(
flatten_opts(metric_avg_scores),
prefix=f"metrics_{mode}_{domain}",
step=self.logger.global_step,
)
else:
print(f"metrics_{mode}_{domain}")
print(flatten_opts(metric_avg_scores))
return 0
def functional_test_mode(self):
import atexit
self.opts.output_path = (
Path("~").expanduser() / "climategan" / "functional_tests"
)
Path(self.opts.output_path).mkdir(parents=True, exist_ok=True)
with open(Path(self.opts.output_path) / "is_functional.test", "w") as f:
f.write("trainer functional test - delete this dir")
if self.exp is not None:
self.exp.log_parameter("is_functional_test", True)
atexit.register(self.del_output_path)
def del_output_path(self, force=False):
import shutil
if not Path(self.opts.output_path).exists():
return
if (Path(self.opts.output_path) / "is_functional.test").exists() or force:
shutil.rmtree(self.opts.output_path)
def compute_fire(self, x, seg_preds=None, z=None, z_depth=None):
"""
Transforms input tensor given wildfires event
Args:
x (torch.Tensor): Input tensor
seg_preds (torch.Tensor): Semantic segmentation
predictions for input tensor
z (torch.Tensor): Latent vector of encoded "x".
Can be None if seg_preds is given.
Returns:
torch.Tensor: Wildfire version of input tensor
"""
if seg_preds is None:
if z is None:
z = self.G.encode(x)
seg_preds = self.G.decoders["s"](z, z_depth)
return add_fire(x, seg_preds, self.opts.events.fire)
def compute_flood(
self, x, z=None, z_depth=None, m=None, s=None, cloudy=None, bin_value=-1
):
"""
Applies a flood (mask + paint) to an input image, with optionally
pre-computed masker z or mask
Args:
x (torch.Tensor): B x C x H x W -1:1 input image
z (torch.Tensor, optional): B x C x H x W Masker latent vector.
Defaults to None.
m (torch.Tensor, optional): B x 1 x H x W Mask. Defaults to None.
bin_value (float, optional): Mask binarization value.
Set to -1 to use smooth masks (no binarization)
Returns:
torch.Tensor: B x 3 x H x W -1:1 flooded image
"""
if m is None:
if z is None:
z = self.G.encode(x)
if "d" in self.opts.tasks and self.opts.gen.m.use_dada and z_depth is None:
_, z_depth = self.G.decoders["d"](z)
m = self.G.mask(x=x, z=z, z_depth=z_depth)
if bin_value >= 0:
m = (m > bin_value).to(m.dtype)
if cloudy:
assert s is not None
return self.G.paint_cloudy(m, x, s)
return self.G.paint(m, x)
def compute_smog(self, x, z=None, d=None, s=None, use_sky_seg=False):
# implementation from the paper:
# HazeRD: An outdoor scene dataset and benchmark for single image dehazing
sky_mask = None
if d is None or (use_sky_seg and s is None):
if z is None:
z = self.G.encode(x)
if d is None:
d, _ = self.G.decoders["d"](z)
if use_sky_seg and s is None:
if "s" not in self.opts.tasks:
raise ValueError(
"Cannot have "
+ "(use_sky_seg is True and s is None and 's' not in tasks)"
)
s = self.G.decoders["s"](z)
# TODO: s to sky mask
# TODO: interpolate to d's size
params = self.opts.events.smog
airlight = params.airlight * torch.ones(3)
airlight = airlight.view(1, -1, 1, 1).to(self.device)
irradiance = srgb2lrgb(x)
beta = torch.tensor([params.beta / params.vr] * 3)
beta = beta.view(1, -1, 1, 1).to(self.device)
d = normalize(d, mini=0.3, maxi=1.0)
d = 1.0 / d
d = normalize(d, mini=0.1, maxi=1)
if sky_mask is not None:
d[sky_mask] = 1
d = torch.nn.functional.interpolate(
d, size=x.shape[-2:], mode="bilinear", align_corners=True
)
d = d.repeat(1, 3, 1, 1)
transmission = torch.exp(d * -beta)
smogged = transmission * irradiance + (1 - transmission) * airlight
smogged = lrgb2srgb(smogged)
# add yellow filter
alpha = params.alpha / 255
yellow_mask = torch.Tensor([params.yellow_color]) / 255
yellow_filter = (
yellow_mask.unsqueeze(2)
.unsqueeze(2)
.repeat(1, 1, smogged.shape[-2], smogged.shape[-1])
.to(self.device)
)
smogged = smogged * (1 - alpha) + yellow_filter * alpha
return smogged