Skip to content

andreaferretti/ganzo

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

60 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Ganzo

Ganzo is a framework to implement, train and run different types of GANs, based on PyTorch.

It aims to unify different types of GAN architectures, loss functions and generator/discriminator game strategies, as well as offer a collection of building blocks to reproduce popular GAN papers.

The guiding principles are:

  • be fully runnable and configurable from the command line or from a JSON file
  • be usable as a library
  • allow for reproducible experiments

Experiments

Some examples of configuration are available in the experiments folder, together with their output. For instance, a WGAN with gradient penalty running on the bedrooms dataset produces

wgan-gp-bedrooms

Installing

The only hard dependencies for Ganzo are PyTorch and TorchVision. For instance, in Conda you can create a dedicated environment with:

conda create -n ganzo python=3.7 pytorch torchvision -c pytorch

If available, Ganzo supports TensorBoardX. This is detected at runtime, so Ganzo can run with or without it. TensorBoardX can be installed with Pip:

pip install tensorboardX

If you want to use LMDB datasets such as LSUN, you will also need that dependency:

conda install python-lmdb

To download the LSUN image dataset, use the instructions in the linked repository.

Running

Ganzo can be used either from the command line or as a library. Each component in Ganzo can be imported and used separately, but everything is written in a way that allows for full customization on the command line or through a JSON file.

To see all available options, run python src/ganzo.py --help. Most options are relative to a component of Ganzo (data loading, generator models, discriminator models, loss functions, logging and so on) and are explained in detail together with the relative component.

Some options are global in nature:

  • experiment this is the name of the experiment. Models, outputs and so on are saved using this name. You can choose a custom experiment name, or let Ganzo use a hash generated based on the options passed.
  • device this is the device name (for instance cpu or cuda). If left unspecified, Ganzo will autodetect the presence of a GPU and use it if available.
  • epochs number of epochs for training.
  • model-dir path to the directory where models are saved. Models are further namespaced according to the experiment name.
  • restore if this flag is set, and an experiment with this name has already been run, Ganzo will reload existing models and keep running from there.
  • delete if this flag is set, and an experiment with this name has already been run, Ganzo will delete everything there and start from scratch. Note that by default Ganzo will ask on the command line what to do, unless at least one flag among delete and restore is active (delete takes precedence over restore).
  • seed this is the seed for PyTorch random number generator. This is used in order to reproduce results.
  • from-json: load configuration from this JSON file (see below)
  • start-epoch: the epoch to start with. By default it is 1, but it can make sense to override this if you are restoring from a previous session of training, so that statistics and snapshots will be assigned the correct epoch.
  • parallel: if this flag is active, the computation will be distributed across all available GPUs. You can limit the visible GPUs by the environment variable CUDA_VISIBLE_DEVICES

Running from a JSON configuration file

If options become too many to handle comfortably, you can run Ganzo with a JSON configuration file. There are two way to do this.

If you have already run an experiment, and you try to run it again, Ganzo suggests you to keep going from where it was left (this can even be forced by using the --restore flag).

Otherwise, if it is the first time that you run an experiment, you can create a JSON file containing some of the command line options, and ask Ganzo to load the configuration from this file using the --from-json flag. Command line and JSON options can also be mixed freely, with JSON options taking precedence.

Assuming you have an option file called options.json, you can load it with

python src/ganzo.py --from-json options.json

If you need a reference file, you can run any experiment, look at the generated options file, and tweak that.

Inference

If you already have a trained model, you can use Ganzo to perform inference. For this, you just need to pass a minimal set of arguments to Ganzo, namely --model-dir and --experiment. You can optionally specify the number of samples to generate with the option --num-samples. The script to perform inference is called deh.py, so you can invoke it like this:

python src/deh.py --model-dir $MODELS --experiment $EXPERIMENT --num-samples 10

The other options will be read from the file options.json that is saved next to the models, although you can override specific options on the command line. Not all training options make sense at inference time, those that are not relevant are just ignored.

Some options are just flags that may have been set a training time, for instance --sample-from-fixed-noise. If you need to override it, just prepend a no to the flag name, for instance

python src/deh.py --model-dir $MODELS --experiment $EXPERIMENT --num-samples 10 --no-sample-from-fixed-noise

Architecture

Ganzo is structured into modules that handles different concerns: data loading, generators, discriminators, loss functions and so on. Each of these modules defines some classes that can be exported and used on their own, or can be used fully through configuration, for instance from a JSON file.

To do so, each module defines a main class (for instance, loss.Loss, or data.Data), that has two static methods:

  • from_options(options, *args) initializes a class from an object representing the options. This object is a argparse.Namespace object that is obtained by parsing command line options or JSON configuration files. Most classes only require the options object in order to be instantiated, but in some cases, other arguments are passed in as well -- for instance the loss function requires the discriminator in order to compute generator loss.
  • add_options(parser) takes as input an argparse.ArgumentParser object, and adds a set of arguments that are relevant to the specific module. This is typically done by adding an argument group. Of course, options are not constrained to be used by the module that introduces them: for instance, data.Data adds the argument batch-size, that is used by many other modules.

Having each module defined by configuration allows Ganzo to wire them without knowing much of the specifics. The main loop run at training time looks like this:

data = Data.from_options(options)
generator = Generator.from_options(options)
discriminator = Discriminator.from_options(options)
loss = Loss.from_options(options, discriminator)
noise = Noise.from_options(options)
statistics = Statistics.from_options(options)
snapshot = Snapshot.from_options(options)
evaluation = Evaluation.from_options(options)
game = Game.from_options(options, generator, discriminator, loss)

for _ in range(options.epochs):
    losses = game.run_epoch(data, noise)
    statistics.log(losses)
    snapshot.save(data, noise, generator)
    if evaluation.has_improved(losses):
        # save models

You can write your own training script by adapting this basic structure. It should be easy: the source of ganzo.py is less than 100 lines, most of which deal with handling the case of restoring a training session.

Components

The following goes in detail about the various components defined by Ganzo: their role, the classes that they export, and the options that they provide for configuration.

Data

This module handles the loading of the image datasets.

Datasets can come in various formats: single image datasets (with or without a labeling) and datasets of input/output pairs. A single image dataset can be used to train a standard GAN, while having the labels can be used for conditioned GANs. Datasets of pairs can be used for tasks of image to image translation, such as super resolution or colorization.

Also, datasets can be stored in different ways. The simplest one is a folder containing images, possibly split into subfolders representing categories. But some datasets are stored in a custom way - for instance MNIST or LSUN.

The module data defines the following classes:

  • SingleImageData: Loads datasets of single images, possibly matched with a corresponding label.

    Takes care of resizing/cropping images, batching, shuffling and distributing the work of data loading across a number of workers. Each batch has shape

    B x C x W x H

    where

    • B is the batch size
    • C is the number of channels (1 for B/W images, 3 for colors)
    • W is the image width
    • H is the image height

The module data exports the following options:

  • data-format: the format of the dataset, such as single-image
  • data-dir: the (input) directory where the images are stored
  • dataset: the type of dataset, such as folder, mnist or lsun
  • image-class: this can be used to filter the dataset, by restricting it to the images having this label
  • image-size: if present, images are resized to this size
  • image-colors: can be 1 or 3 for B/W or color images
  • batch-size: how many images to consider in each minibatch
  • loader-workers: how many workers to use to load data in background

Generator

This module defines the various generator architectures.

The module generator defines the following classes:

  • FCGenerator TODO describe it
  • ConvGenerator TODO describe it
  • GoodGenerator TODO describe it

The module generator exports the following options:

  • generator: the type of generator (fc or conv)
  • generator-dropout: the amount of dropout to use between generator layers - leave unspecified to avoid using dropout
  • generator-layers: how many layers to use in the generator

Discriminator

This module defines the various generator architectures.

The module discriminator defines the following classes:

  • FCDiscriminator TODO describe it
  • ConvDiscriminator TODO describe it
  • GoodDiscriminator TODO describe it

The module discriminator exports the following options:

  • discriminator: the type of discriminator (fc or conv)
  • discriminator-dropout: the amount of dropout to use between discriminator layers - leave unspecified to avoid using dropout
  • discriminator-layers: how many layers to use in the discriminator

Loss

This module defines classes that compute the loss functions both for the generator and the discriminator. The reason why they are coupled is that the loss function for the generator needs access to the discriminator anyway.

The module loss defines the following classes:

  • GANLoss: the standard GAN loss from [1]
  • WGANLoss: the Wasserstein GAN loss from [2]
  • WGANGPLoss like WGAN, but uses gradient penalty instead of weight clipping [3]
  • Pix2PixLoss: the loss for Pix2Pix from [4]

[1] https://arxiv.org/abs/1406.2661 [2] https://arxiv.org/abs/1701.07875 [3] https://arxiv.org/abs/1704.00028 [4] https://arxiv.org/abs/1611.07004

The module loss exports the following options:

  • --loss: the type of loss (gan, wgan, wgan-gp or pix2pix)
  • --gradient-penalty-factor: the gradient penalty factor (λ) in WGAN-GP
  • --soft-labels: if true, use soft labels in the GAN loss (randomly fudge the labels by at most 0.1)
  • --noisy-labels': if true, use noisy labels in the GAN loss (sometimes invert labels for the discriminator)
  • --noisy-labels-frequency: how often to invert labels for the discriminator
  • --l1-weight: weight of the L¹ distance contribution to the GAN loss

Noise

This modules defines classese that generate random noise. Most GAN generators can be seen as implementing a map {latent space} -> {images}, where the latent space is some fixed Euclidean space. The noise generators implement sampling in latent space, so generating an image consists of sampling a random noise and applying the generator.

The module noise defines the following classes:

  • GaussianNoise: A generator of Gaussian noise

The module noise exports the following options:

  • state-size: the dimension of the latent space

Statistics

This module defines classes that handle logging stastistics such as time spent during training and the various losses. At this moment, logging can happen either on the console or via TensorBoard.

The module statistics defines the following classes:

  • NoStatistics: a class that just drops the logging information
  • ConsoleStatistics: a class that displays logging information on the console
  • FileStatistics: a class that writes logging information into a file
  • TensorBoardStatistics: a class that logs information via TensorBoard (requires TensorBoardX)

The module statistics defines the following options:

  • log: either none, console, file or tensorboard
  • log-file: when using --log file this determines the file where logs are written. If missing, it defaults to $OUTPUT_DIR/$EXPERIMENT/statistics.log.

Snapshot

This module define classes that periodically take example snapshot images and save them.

The module snapshot defines the following classes:

  • FolderSnaphot: a class that saves images on disk in a predefined folder
  • TensorBoardSnaphot: a class that saves images via TensorBoard (requires TensorBoardX)

The module snapshot defines the following options:

  • save-images-as: either folder or tensorboard
  • output-dir: directory where to store the generated images
  • snapshot-size: how many images to generate for each sample (must be <= batch-size)
  • sample-every: how often to sample images (in epochs)
  • sample-from-fixed-noise: if this flag is on, always use the same input noise when sampling, otherwise generate new random images each time

Evaluation

This module defines criteria that can be used to evaluate the quality of the produced images. Ganzo will save the model whenever these have improved.

The module evaluation defines the following classes:

  • Latest: always returns true, thus letting Ganzo always save the latest models
  • GeneratorLoss: defines that evaluation has improved when the loss for the generator decreases

The module evaluation defines the following options:

  • evaluation-criterion: either latest or generator-loss

Game

This module defines classes that implement the actual GAN logic, which has a few variants.

The module game defines the following classes:

  • StandardGame: the usual GAN game that opposes a generator, taking random noise as input, and a discriminator to learn classify real and fake samples
  • TranslateGame: a game that uses the generator to perform an image translation task. This is different from StandardGame, since the generator receives as input real images from a given domain, and needs to produce as output images in a different domain. The discriminator learns to classify real and fake samples, but both are overlaid to the original input, in order to evaluate the quality of the translation.

The module game defines the following options:

  • evaluation-criterion: either standard or translate
  • generator-iterations: number of iterations on each turn for the generator
  • discriminator-iterations: number of iterations on each turn for the discriminator
  • generator-lr: learning rate for the generator
  • discriminator-lr: learning rate for the discriminator
  • beta1: first beta
  • beta2: second beta
  • max-batches-per-epoch: maximum number of minibatches per epoch

Extending Ganzo

Ganzo can be extended by defining your custom modules. To do this, you do not need to write your training script (although this is certainly doable). If you want to define custom components and let Ganzo take advantage of them, you need to follow four steps:

  • write your custom component (this can a be a data loader, a generator, a loss function...). You will need to make sure that it can be initialized via an option object and that it exposes the same public methods as the other classes (for instance, a loss function exposes two public methods def for_generator(self, fake_data, labels=None) and def for_discriminator(self, real_data, fake_data, labels=None))
  • let Ganzo be aware of your component by registering it
  • add an enviroment variable to make Ganzo find your module
  • optionally, add your custom options to the argument parser.

To make this possible, Ganzo uses a registry, that can be found under registry.py. This exports the Registry singleton and the register decorator function (also, the RegistryException class, which you should not need).

Custom components and the registry

When you write your custom component, you need to register it in the correct namespace. This can be done with the Registry.add function, or more simply with the register decorator. In both cases you will need to provide the namespace (e.g. loss for a loss function) and the name of your component (this is your choice, just make sure not to collide with existing names). For instance, registering a CustomLoss class can be done like this:

from registry import register

@register('loss', 'custom-loss')
class CustomLoss:
    # make sure that your __init__ method has the same signature
    # as the existing components
    def __init__(self, options, discriminator):
        # initialization logic here

    # more class logic here, for instance the public API
    def for_generator(self, fake_data, labels=None):
        pass

    def for_discriminator(self, real_data, fake_data, labels=None):
        pass

This can also be done more explicitly by adding your class to the registry:

from registry import Registry

class CustomLoss:
    # make sure that your __init__ method has the same signature
    # as the existing components
    def __init__(self, options, discriminator):
        # initialization logic here

    # more class logic here, for instance the public API
    def for_generator(self, fake_data, labels=None):
        pass

    def for_discriminator(self, real_data, fake_data, labels=None):
        pass

Registry.add('loss', 'custom-loss', CustomLoss)

You will then be able to select your custom loss function by passing the command line argument --loss custom-loss. Both register and Registry.add take a flag default which means that the registered component will be selected as default when the corresponding command line option is missing. This can be used only once, though, and it is already taken by the default components of Ganzo, so you should not pass default=True while registering your component.

Finding your module

Of course, at this point Ganzo is not aware that your module exists, or that it defines and register new components. You need to make sure that Ganzo actually imports your module, so that your custom logic is run. This cannot be done with a flag on the command line: the reason is that you are able to add custom options to the argument parser, so your modules must be found before reading the command line options.

To do this, we use an environment variable GANZO_LOAD_MODULES, which should contain a comma-separated list of python modules that you want to import before Ganzo starts. These modules should be on the Python path, so that they can be imported by name. For instance, if you have defined your loss inside custom.py, you can call Ganzo like this:

GANZO_LOAD_MODULES=custom python src/ganzo.py # more options here

Customizing options

Probably, your custom component will need some degrees of freedom that are not applicable in general, and this means that you need to be able to extend the command line parser. To do this, import the decorator with_option_parser from the registry module and define a function that takes a parser argument and extends it. This function is also passed the train argument, which you can use to only activate certain options during training (or only during inference).

It is advised to namespace your options into their own argument group, in order to make the help message more understandable. An example would be

from registry import with_option_parser

@with_option_parser
def add_my_custom_options(parser, train):
    group = parser.add_argument_group('custom')
    group.add_argument('foos', type=int, default=3, help='the number of foos')
    if train:
        group.add_argument('bars', type=int, default=5, help='the number of bars')

Using Ganzo as a library

All components in Ganzo are designed to be used together by configuration, but this is not a requirement by any means. If you want to write your custom training and inference scripts, and only need access to some of Ganzo's generators, discriminators, loss function and so on, this is easily doable.

All classes need in the constructor an options parameter , which is an instance of argparse.Namespace. Other than that, you can just import and use the classes as needed. For example

from argparse import Namespace
from ganzo.generator import UGenerator

options = Namespace()
options.generator_layers = 5
options.generator_channels = 3
generator = UGenerator(options)