|
# ClimateGAN |
|
- [ClimateGAN](#climategan) |
|
- [Setup](#setup) |
|
- [Coding conventions](#coding-conventions) |
|
- [updates](#updates) |
|
- [interfaces](#interfaces) |
|
- [Logging on comet](#logging-on-comet) |
|
- [Resources](#resources) |
|
- [Example](#example) |
|
- [Release process](#release-process) |
|
|
|
## Setup |
|
|
|
**`PyTorch >= 1.1.0`** otherwise optimizer.step() and scheduler.step() are in the wrong order ([docs](https://pytorch.org/docs/stable/optim.html#how-to-adjust-learning-rate)) |
|
|
|
**pytorch==1.6** to use pytorch-xla or automatic mixed precision (`amp` branch). |
|
|
|
Configuration files use the **YAML** syntax. If you don't know what `&` and `<<` mean, you'll have a hard time reading the files. Have a look at: |
|
|
|
* https://dev.to/paulasantamaria/introduction-to-yaml-125f |
|
* https://stackoverflow.com/questions/41063361/what-is-the-double-left-arrow-syntax-in-yaml-called-and-wheres-it-specced/41065222 |
|
|
|
**pip** |
|
|
|
``` |
|
$ pip install comet_ml scipy opencv-python torch torchvision omegaconf==1.4.1 hydra-core==0.11.3 scikit-image imageio addict tqdm torch_optimizer |
|
``` |
|
|
|
## Coding conventions |
|
|
|
* Tasks |
|
* `x` is an input image, in [-1, 1] |
|
* `s` is a segmentation target with `long` classes |
|
* `d` is a depth map target in R, may be actually `log(depth)` or `1/depth` |
|
* `m` is a binary mask with 1s where water is/should be |
|
* Domains |
|
* `r` is the *real* domain for the masker. Input images are real pictures of urban/suburban/rural areas |
|
* `s` is the *simulated* domain for the masker. Input images are taken from our Unity world |
|
* `rf` is the *real flooded* domain for the painter. Training images are pairs `(x, m)` of flooded scenes for which the water should be reconstructed, in the validation data input images are not flooded and we provide a manually labeled mask `m` |
|
* `kitti` is a special `s` domain to pre-train the masker on [Virtual Kitti 2](https://europe.naverlabs.com/research/computer-vision/proxy-virtual-worlds-vkitti-2/) |
|
* it alters the `trainer.loaders` dict to select relevant data sources from `trainer.all_loaders` in `trainer.switch_data()`. The rest of the code is identical. |
|
* Flow |
|
* This describes the call stack for the trainers standard training procedure |
|
* `train()` |
|
* `run_epoch()` |
|
* `update_G()` |
|
* `zero_grad(G)` |
|
* `get_G_loss()` |
|
* `get_masker_loss()` |
|
* `masker_m_loss()` -> masking loss |
|
* `masker_s_loss()` -> segmentation loss |
|
* `masker_d_loss()` -> depth estimation loss |
|
* `get_painter_loss()` -> painter's loss |
|
* `g_loss.backward()` |
|
* `g_opt_step()` |
|
* `update_D()` |
|
* `zero_grad(D)` |
|
* `get_D_loss()` |
|
* painter's disc losses |
|
* `masker_m_loss()` -> masking AdvEnt disc loss |
|
* `masker_s_loss()` -> segmentation AdvEnt disc loss |
|
* `d_loss.backward()` |
|
* `d_opt_step()` |
|
* `update_learning_rates()` -> update learning rates according to schedules defined in `opts.gen.opt` and `opts.dis.opt` |
|
* `run_validation()` |
|
* compute val losses |
|
* `eval_images()` -> compute metrics |
|
* `log_comet_images()` -> compute and upload inferences |
|
* `save()` |
|
|
|
### Resuming |
|
|
|
Set `train.resume` to `True` in `opts.yaml` and specify where to load the weights: |
|
|
|
Use a config's `load_path` namespace. It should have sub-keys `m`, `p` and `pm`: |
|
|
|
```yaml |
|
load_paths: |
|
p: none # Painter weights |
|
m: none # Masker weights |
|
pm: none # Painter + Masker weights (single ckpt for both) |
|
``` |
|
|
|
1. any path which leads to a dir will be loaded as `path / checkpoints / latest_ckpt.pth` |
|
2. if you want to specify a specific checkpoint (not the latest), it MUST be a `.pth` file |
|
3. resuming a `P` **OR** an `M` model, you may only specify 1 of `load_path.p` **OR** `load_path.m`. |
|
You may also leave **BOTH** at `none`, in which case `output_path / checkpoints / latest_ckpt.pth` |
|
will be used |
|
4. resuming a P+M model, you may specify (`p` AND `m`) **OR** `pm` **OR** leave all at `none`, |
|
in which case `output_path / checkpoints / latest_ckpt.pth` will be used to load from |
|
a single checkpoint |
|
|
|
### Generator |
|
|
|
* **Encoder**: |
|
|
|
`trainer.G.encoder` Deeplabv2 or v3-based encoder |
|
* Code borrowed from |
|
* https://github.com/valeoai/ADVENT/blob/master/advent/model/deeplabv2.py |
|
* https://github.com/CoinCheung/DeepLab-v3-plus-cityscapes |
|
|
|
* **Decoders**: |
|
* `trainer.G.decoders["s"]` -> *Segmentation* -> DLV3+ architecture (ASPP + Decoder) |
|
* `trainer.G.decoders["d"]` -> *Depth* -> ResBlocks + (Upsample + Conv) |
|
* `trainer.G.decoders["m"]` -> *Mask* -> ResBlocks + (Upsample + Conv) -> Binary mask: 1 = water should be there |
|
* `trainer.G.mask()` predicts a mask and optionally applies `sigmoid` from an `x` input or a `z` input |
|
|
|
* **Painter**: `trainer.G.painter` -> [GauGAN SPADE-based](https://github.com/NVlabs/SPADE) |
|
* input = masked image |
|
* `trainer.G.paint(m, x)` higher level function which takes care of masking |
|
* If `opts.gen.p.paste_original_content` the painter should only create water and not reconstruct outside the mask: the output of `paint()` is `painted * m + x * (1 - m)` |
|
|
|
High level methods of interest: |
|
|
|
* `trainer.infer_all()` creates a dictionary of events with keys `flood` `wildfire` and `smog`. Can take in a single image or a batch, of numpy arrays or torch tensors, on CPU/GPU/TPU. This method calls, amongst others: |
|
* `trainer.G.encode()` to compute the shared latent vector `z` |
|
* `trainer.G.mask(z=z)` to infer the mask |
|
* `trainer.compute_fire(x, segmentation)` to create a wildfire image from `x` and inferred segmentation |
|
* `trainer.compute_smog(x, depth)` to create a smog image from `x` and inferred depth |
|
* `trainer.compute_flood(x, mask)` to create a flood image from `x` and inferred mask using the painter (`trainer.G.paint(m, x)`) |
|
* `Trainer.resume_from_path()` static method to resume a trainer from a path |
|
|
|
### Discriminator |
|
|
|
## updates |
|
|
|
multi-batch: |
|
|
|
``` |
|
multi_domain_batch = {"rf: batch0, "r": batch1, "s": batch2} |
|
``` |
|
|
|
## interfaces |
|
|
|
### batches |
|
```python |
|
batch = Dict({ |
|
"data": { |
|
"d": depthmap,, |
|
"s": segmentation_map, |
|
"m": binary_mask |
|
"x": real_flooded_image, |
|
}, |
|
"paths":{ |
|
same_keys: path_to_file |
|
} |
|
"domain": list(rf | r | s), |
|
"mode": list(train | val) |
|
}) |
|
``` |
|
|
|
### data |
|
|
|
#### json files |
|
|
|
| name | domain | description | author | |
|
| :--------------------------------------------- | :----: | :------------------------------------------------------------------------- | :-------: | |
|
| **train_r_full.json, val_r_full.json** | r | MiDaS+ Segmentation pseudo-labels .pt (HRNet + Cityscapes) | Mélisande | |
|
| **train_s_full.json, val_s_full.json** | s | Simulated data from Unity11k urban + Unity suburban dataset | *** | |
|
| train_s_nofences.json, val_s_nofences.json | s | Simulated data from Unity11k urban + Unity suburban dataset without fences | Alexia | |
|
| train_r_full_pl.json, val_r_full_pl.json | r | MegaDepth + Segmentation pseudo-labels .pt (HRNet + Cityscapes) | Alexia | |
|
| train_r_full_midas.json, val_r_full_midas.json | r | MiDaS+ Segmentation (HRNet + Cityscapes) | Mélisande | |
|
| train_r_full_old.json, val_r_full_old.json | r | MegaDepth+ Segmentation (HRNet + Cityscapes) | *** | |
|
| train_r_nopeople.json, val_r_nopeople.json | r | Same training data as above with people removed | Sasha | |
|
| train_rf_with_sim.json | rf | Doubled train_rf's size with sim data (randomly chosen) | Victor | |
|
| train_rf.json | rf | UPDATE (12/12/20): added 50 ims & masks from ADE20K Outdoors | Victor | |
|
| train_allres.json, val_allres.json | rf | includes both lowres and highres from ORCA_water_seg | Tianyu | |
|
| train_highres_only.json, val_highres_only.json | rf | includes only highres from ORCA_water_seg | Tianyu | |
|
|
|
|
|
```yaml |
|
# data file ; one for each r|s |
|
- x: /path/to/image |
|
m: /path/to/mask |
|
s: /path/to/segmentation map |
|
- x: /path/to/another image |
|
d: /path/to/depth map |
|
m: /path/to/mask |
|
s: /path/to/segmentation map |
|
- x: ... |
|
``` |
|
|
|
or |
|
|
|
```json |
|
[ |
|
{ |
|
"x": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000005.jpg", |
|
"s": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000005.npy", |
|
"d": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000005_depth.jpg" |
|
}, |
|
{ |
|
"x": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000006.jpg", |
|
"s": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000006.npy", |
|
"d": "/Users/victor/Documents/ccai/github/climategan/example_data/gsv_000006_depth.jpg" |
|
} |
|
] |
|
``` |
|
|
|
The json files used are located at `/network/tmp1/ccai/data/climategan/`. In the basenames, `_s` denotes simulated domain data and `_r` real domain data. |
|
The `base` folder contains json files with paths to images (`"x"`key) and masks (taken as ground truth for the area that should be flooded, `"m"` key). |
|
The `seg` folder contains json files and keys `"x"`, `"m"` and `"s"` (segmentation) for each image. |
|
|
|
|
|
loaders |
|
|
|
``` |
|
loaders = Dict({ |
|
train: { r: loader, s: loader}, |
|
val: { r: loader, s: loader} |
|
}) |
|
``` |
|
|
|
### losses |
|
|
|
`trainer.losses` is a dictionary mapping to loss functions to optimize for the 3 main parts of the architecture: generator `G`, discriminators `D`: |
|
|
|
```python |
|
trainer.losses = { |
|
"G":{ # generator |
|
"gan": { # gan loss from the discriminators |
|
"a": GANLoss, # adaptation decoder |
|
"t": GANLoss # translation decoder |
|
}, |
|
"cycle": { # cycle-consistency loss |
|
"a": l1 | l2,, |
|
"t": l1 | l2, |
|
}, |
|
"auto": { # auto-encoding loss a.k.a. reconstruction loss |
|
"a": l1 | l2, |
|
"t": l1 | l2 |
|
}, |
|
"tasks": { # specific losses for each auxillary task |
|
"d": func, # depth estimation |
|
"h": func, # height estimation |
|
"s": cross_entropy_2d, # segmentation |
|
"w": func, # water generation |
|
}, |
|
"classifier": l1 | l2 | CE # loss from fooling the classifier |
|
}, |
|
"D": GANLoss, # discriminator losses from the generator and true data |
|
"C": l1 | l2 | CE # classifier should predict the right 1-h vector [rf, rn, sf, sn] |
|
} |
|
``` |
|
|
|
## Logging on comet |
|
|
|
Comet.ml will look for api keys in the following order: argument to the `Experiment(api_key=...)` call, `COMET_API_KEY` environment variable, `.comet.config` file in the current working directory, `.comet.config` in the current user's home directory. |
|
|
|
If your not managing several comet accounts at the same time, I recommend putting `.comet.config` in your home as such: |
|
|
|
``` |
|
[comet] |
|
api_key=<api_key> |
|
workspace=vict0rsch |
|
rest_api_key=<rest_api_key> |
|
``` |
|
|
|
### Tests |
|
|
|
Run tests by executing `python test_trainer.py`. You can add `--no_delete` not to delete the comet experiment at exit and inspect uploads. |
|
|
|
Write tests as scenarios by adding to the list `test_scenarios` in the file. A scenario is a dict of overrides over the base opts in `shared/trainer/defaults.yaml`. You can create special flags for the scenario by adding keys which start with `__`. For instance, `__doc` is a mandatory key in any scenario describing it succinctly. |
|
|
|
## Resources |
|
|
|
[Tricks and Tips for Training a GAN](https://chloes-dl.com/2019/11/19/tricks-and-tips-for-training-a-gan/) |
|
[GAN Hacks](https://github.com/soumith/ganhacks) |
|
[Keep Calm and train a GAN. Pitfalls and Tips on training Generative Adversarial Networks](https://medium.com/@utk.is.here/keep-calm-and-train-a-gan-pitfalls-and-tips-on-training-generative-adversarial-networks-edd529764aa9) |
|
|
|
## Example |
|
|
|
**Inference: computing floods** |
|
|
|
```python |
|
from pathlib import Path |
|
from skimage.io import imsave |
|
from tqdm import tqdm |
|
|
|
from climategan.trainer import Trainer |
|
from climategan.utils import find_images |
|
from climategan.tutils import tensor_ims_to_np_uint8s |
|
from climategan.transforms import PrepareInference |
|
|
|
|
|
model_path = "some/path/to/output/folder" # not .ckpt |
|
input_folder = "path/to/a/folder/with/images" |
|
output_path = "path/where/images/will/be/written" |
|
|
|
# resume trainer |
|
trainer = Trainer.resume_from_path(model_path, new_exp=None, inference=True) |
|
|
|
# find paths for all images in the input folder. There is a recursive option. |
|
im_paths = sorted(find_images(input_folder), key=lambda x: x.name) |
|
|
|
# Load images into tensors |
|
# * smaller side resized to 640 - keeping aspect ratio |
|
# * then longer side is cropped in the center |
|
# * result is a 1x3x640x640 float tensor in [-1; 1] |
|
xs = PrepareInference()(im_paths) |
|
|
|
# send to device |
|
xs = [x.to(trainer.device) for x in xs] |
|
|
|
# compute flood |
|
# * compute mask |
|
# * binarize mask if bin_value > 0 |
|
# * paint x using this mask |
|
ys = [trainer.compute_flood(x, bin_value=0.5) for x in tqdm(xs)] |
|
|
|
# convert 1x3x640x640 float tensors in [-1; 1] into 640x640x3 numpy arrays in [0, 255] |
|
np_ys = [tensor_ims_to_np_uint8s(y) for y in tqdm(ys)] |
|
|
|
# write images |
|
for i, n in tqdm(zip(im_paths, np_ys), total=len(im_paths)): |
|
imsave(Path(output_path) / i.name, n) |
|
``` |
|
|
|
## Release process |
|
|
|
In the `release/` folder |
|
* create a `model/` folder |
|
* create folders `model/masker/` and `model/painter/` |
|
* add the climategan code in `release/`: `git clone git@github.com:cc-ai/climategan.git` |
|
* move the code to `release/`: `cp climategan/* . && rm -rf climategan` |
|
* update `model/masker/opts/events` with `events:` from `shared/trainer/opts.yaml` |
|
* update `model/masker/opts/val.val_painter` to `"model/painter/checkpoints/latest_ckpt.pth"` |
|
* update `model/masker/opts/load_paths.m` to `"model/masker/checkpoints/latest_ckpt.pth"` |
|
|