chore: import upstream snapshot with attribution

This commit is contained in:
wehub-resource-sync
2026-07-13 12:19:01 +08:00
commit 3b90d1192f
2172 changed files with 594509 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
"""
---
title: Generative Adversarial Networks
summary: >
A set of PyTorch implementations/tutorials of GANs.
---
# Generative Adversarial Networks
* [Original GAN](original/index.html)
* [GAN with deep convolutional network](dcgan/index.html)
* [Cycle GAN](cycle_gan/index.html)
* [Wasserstein GAN](wasserstein/index.html)
* [Wasserstein GAN with Gradient Penalty](wasserstein/gradient_penalty/index.html)
* [StyleGAN 2](stylegan/index.html)
"""
+773
View File
@@ -0,0 +1,773 @@
"""
---
title: Cycle GAN
summary: >
A simple PyTorch implementation/tutorial of Cycle GAN introduced in paper
Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks.
---
# Cycle GAN
This is a [PyTorch](https://pytorch.org) implementation/tutorial of the paper
[Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks](https://arxiv.org/abs/1703.10593).
I've taken pieces of code from [eriklindernoren/PyTorch-GAN](https://github.com/eriklindernoren/PyTorch-GAN).
It is a very good resource if you want to checkout other GAN variations too.
Cycle GAN does image-to-image translation.
It trains a model to translate an image from given distribution to another, say, images of class A and B.
Images of a certain distribution could be things like images of a certain style, or nature.
The models do not need paired images between A and B.
Just a set of images of each class is enough.
This works very well on changing between image styles, lighting changes, pattern changes, etc.
For example, changing summer to winter, painting style to photos, and horses to zebras.
Cycle GAN trains two generator models and two discriminator models.
One generator translates images from A to B and the other from B to A.
The discriminators test whether the generated images look real.
This file contains the model code as well as the training code.
We also have a Google Colab notebook.
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/cycle_gan/experiment.ipynb)
"""
import itertools
import random
import zipfile
from typing import Tuple
import torch
import torch.nn as nn
import torchvision.transforms as transforms
from PIL import Image
from torch.utils.data import DataLoader, Dataset
from torchvision.transforms import InterpolationMode
from torchvision.utils import make_grid
from labml import lab, tracker, experiment, monit
from labml.configs import BaseConfigs
from labml.utils.download import download_file
from labml.utils.pytorch import get_modules
from labml_nn.helpers.device import DeviceConfigs
class GeneratorResNet(nn.Module):
"""
The generator is a residual network.
"""
def __init__(self, input_channels: int, n_residual_blocks: int):
super().__init__()
# This first block runs a $7\times7$ convolution and maps the image to
# a feature map.
# The output feature map has the same height and width because we have
# a padding of $3$.
# Reflection padding is used because it gives better image quality at edges.
#
# `inplace=True` in `ReLU` saves a little bit of memory.
out_features = 64
layers = [
nn.Conv2d(input_channels, out_features, kernel_size=7, padding=3, padding_mode='reflect'),
nn.InstanceNorm2d(out_features),
nn.ReLU(inplace=True),
]
in_features = out_features
# We down-sample with two $3 \times 3$ convolutions
# with stride of 2
for _ in range(2):
out_features *= 2
layers += [
nn.Conv2d(in_features, out_features, kernel_size=3, stride=2, padding=1),
nn.InstanceNorm2d(out_features),
nn.ReLU(inplace=True),
]
in_features = out_features
# We take this through `n_residual_blocks`.
# This module is defined below.
for _ in range(n_residual_blocks):
layers += [ResidualBlock(out_features)]
# Then the resulting feature map is up-sampled
# to match the original image height and width.
for _ in range(2):
out_features //= 2
layers += [
nn.Upsample(scale_factor=2),
nn.Conv2d(in_features, out_features, kernel_size=3, stride=1, padding=1),
nn.InstanceNorm2d(out_features),
nn.ReLU(inplace=True),
]
in_features = out_features
# Finally we map the feature map to an RGB image
layers += [nn.Conv2d(out_features, input_channels, 7, padding=3, padding_mode='reflect'), nn.Tanh()]
# Create a sequential module with the layers
self.layers = nn.Sequential(*layers)
# Initialize weights to $\mathcal{N}(0, 0.2)$
self.apply(weights_init_normal)
def forward(self, x):
return self.layers(x)
class ResidualBlock(nn.Module):
"""
This is the residual block, with two convolution layers.
"""
def __init__(self, in_features: int):
super().__init__()
self.block = nn.Sequential(
nn.Conv2d(in_features, in_features, kernel_size=3, padding=1, padding_mode='reflect'),
nn.InstanceNorm2d(in_features),
nn.ReLU(inplace=True),
nn.Conv2d(in_features, in_features, kernel_size=3, padding=1, padding_mode='reflect'),
nn.InstanceNorm2d(in_features),
nn.ReLU(inplace=True),
)
def forward(self, x: torch.Tensor):
return x + self.block(x)
class Discriminator(nn.Module):
"""
This is the discriminator.
"""
def __init__(self, input_shape: Tuple[int, int, int]):
super().__init__()
channels, height, width = input_shape
# Output of the discriminator is also a map of probabilities,
# whether each region of the image is real or generated
self.output_shape = (1, height // 2 ** 4, width // 2 ** 4)
self.layers = nn.Sequential(
# Each of these blocks will shrink the height and width by a factor of 2
DiscriminatorBlock(channels, 64, normalize=False),
DiscriminatorBlock(64, 128),
DiscriminatorBlock(128, 256),
DiscriminatorBlock(256, 512),
# Zero pad on top and left to keep the output height and width same
# with the $4 \times 4$ kernel
nn.ZeroPad2d((1, 0, 1, 0)),
nn.Conv2d(512, 1, kernel_size=4, padding=1)
)
# Initialize weights to $\mathcal{N}(0, 0.2)$
self.apply(weights_init_normal)
def forward(self, img):
return self.layers(img)
class DiscriminatorBlock(nn.Module):
"""
This is the discriminator block module.
It does a convolution, an optional normalization, and a leaky ReLU.
It shrinks the height and width of the input feature map by half.
"""
def __init__(self, in_filters: int, out_filters: int, normalize: bool = True):
super().__init__()
layers = [nn.Conv2d(in_filters, out_filters, kernel_size=4, stride=2, padding=1)]
if normalize:
layers.append(nn.InstanceNorm2d(out_filters))
layers.append(nn.LeakyReLU(0.2, inplace=True))
self.layers = nn.Sequential(*layers)
def forward(self, x: torch.Tensor):
return self.layers(x)
def weights_init_normal(m):
"""
Initialize convolution layer weights to $\mathcal{N}(0, 0.2)$
"""
classname = m.__class__.__name__
if classname.find("Conv") != -1:
torch.nn.init.normal_(m.weight.data, 0.0, 0.02)
def load_image(path: str):
"""
Load an image and change to RGB if in grey-scale.
"""
image = Image.open(path)
if image.mode != 'RGB':
image = Image.new("RGB", image.size).paste(image)
return image
class ImageDataset(Dataset):
"""
### Dataset to load images
"""
@staticmethod
def download(dataset_name: str):
"""
#### Download dataset and extract data
"""
# URL
url = f'https://people.eecs.berkeley.edu/~taesung_park/CycleGAN/datasets/{dataset_name}.zip'
# Download folder
root = lab.get_data_path() / 'cycle_gan'
if not root.exists():
root.mkdir(parents=True)
# Download destination
archive = root / f'{dataset_name}.zip'
# Download file (generally ~100MB)
download_file(url, archive)
# Extract the archive
with zipfile.ZipFile(archive, 'r') as f:
f.extractall(root)
def __init__(self, dataset_name: str, transforms_, mode: str):
"""
#### Initialize the dataset
* `dataset_name` is the name of the dataset
* `transforms_` is the set of image transforms
* `mode` is either `train` or `test`
"""
# Dataset path
root = lab.get_data_path() / 'cycle_gan' / dataset_name
# Download if missing
if not root.exists():
self.download(dataset_name)
# Image transforms
self.transform = transforms.Compose(transforms_)
# Get image paths
path_a = root / f'{mode}A'
path_b = root / f'{mode}B'
self.files_a = sorted(str(f) for f in path_a.iterdir())
self.files_b = sorted(str(f) for f in path_b.iterdir())
def __getitem__(self, index):
# Return a pair of images.
# These pairs get batched together, and they do not act like pairs in training.
# So it is kind of ok that we always keep giving the same pair.
return {"x": self.transform(load_image(self.files_a[index % len(self.files_a)])),
"y": self.transform(load_image(self.files_b[index % len(self.files_b)]))}
def __len__(self):
# Number of images in the dataset
return max(len(self.files_a), len(self.files_b))
class ReplayBuffer:
"""
### Replay Buffer
Replay buffer is used to train the discriminator.
Generated images are added to the replay buffer and sampled from it.
The replay buffer returns the newly added image with a probability of $0.5$.
Otherwise, it sends an older generated image and replaces the older image
with the newly generated image.
This is done to reduce model oscillation.
"""
def __init__(self, max_size: int = 50):
self.max_size = max_size
self.data = []
def push_and_pop(self, data: torch.Tensor):
"""Add/retrieve an image"""
data = data.detach()
res = []
for element in data:
if len(self.data) < self.max_size:
self.data.append(element)
res.append(element)
else:
if random.uniform(0, 1) > 0.5:
i = random.randint(0, self.max_size - 1)
res.append(self.data[i].clone())
self.data[i] = element
else:
res.append(element)
return torch.stack(res)
class Configs(BaseConfigs):
"""## Configurations"""
# `DeviceConfigs` will pick a GPU if available
device: torch.device = DeviceConfigs()
# Hyper-parameters
epochs: int = 200
dataset_name: str = 'monet2photo'
batch_size: int = 1
data_loader_workers = 8
learning_rate = 0.0002
adam_betas = (0.5, 0.999)
decay_start = 100
# The paper suggests using a least-squares loss instead of
# negative log-likelihood, at it is found to be more stable.
gan_loss = torch.nn.MSELoss()
# L1 loss is used for cycle loss and identity loss
cycle_loss = torch.nn.L1Loss()
identity_loss = torch.nn.L1Loss()
# Image dimensions
img_height = 256
img_width = 256
img_channels = 3
# Number of residual blocks in the generator
n_residual_blocks = 9
# Loss coefficients
cyclic_loss_coefficient = 10.0
identity_loss_coefficient = 5.
sample_interval = 500
# Models
generator_xy: GeneratorResNet
generator_yx: GeneratorResNet
discriminator_x: Discriminator
discriminator_y: Discriminator
# Optimizers
generator_optimizer: torch.optim.Adam
discriminator_optimizer: torch.optim.Adam
# Learning rate schedules
generator_lr_scheduler: torch.optim.lr_scheduler.LambdaLR
discriminator_lr_scheduler: torch.optim.lr_scheduler.LambdaLR
# Data loaders
dataloader: DataLoader
valid_dataloader: DataLoader
def sample_images(self, n: int):
"""Generate samples from test set and save them"""
batch = next(iter(self.valid_dataloader))
self.generator_xy.eval()
self.generator_yx.eval()
with torch.no_grad():
data_x, data_y = batch['x'].to(self.generator_xy.device), batch['y'].to(self.generator_yx.device)
gen_y = self.generator_xy(data_x)
gen_x = self.generator_yx(data_y)
# Arrange images along x-axis
data_x = make_grid(data_x, nrow=5, normalize=True)
data_y = make_grid(data_y, nrow=5, normalize=True)
gen_x = make_grid(gen_x, nrow=5, normalize=True)
gen_y = make_grid(gen_y, nrow=5, normalize=True)
# Arrange images along y-axis
image_grid = torch.cat((data_x, gen_y, data_y, gen_x), 1)
# Show samples
plot_image(image_grid)
def initialize(self):
"""
## Initialize models and data loaders
"""
input_shape = (self.img_channels, self.img_height, self.img_width)
# Create the models
self.generator_xy = GeneratorResNet(self.img_channels, self.n_residual_blocks).to(self.device)
self.generator_yx = GeneratorResNet(self.img_channels, self.n_residual_blocks).to(self.device)
self.discriminator_x = Discriminator(input_shape).to(self.device)
self.discriminator_y = Discriminator(input_shape).to(self.device)
# Create the optmizers
self.generator_optimizer = torch.optim.Adam(
itertools.chain(self.generator_xy.parameters(), self.generator_yx.parameters()),
lr=self.learning_rate, betas=self.adam_betas)
self.discriminator_optimizer = torch.optim.Adam(
itertools.chain(self.discriminator_x.parameters(), self.discriminator_y.parameters()),
lr=self.learning_rate, betas=self.adam_betas)
# Create the learning rate schedules.
# The learning rate stars flat until `decay_start` epochs,
# and then linearly reduce to $0$ at end of training.
decay_epochs = self.epochs - self.decay_start
self.generator_lr_scheduler = torch.optim.lr_scheduler.LambdaLR(
self.generator_optimizer, lr_lambda=lambda e: 1.0 - max(0, e - self.decay_start) / decay_epochs)
self.discriminator_lr_scheduler = torch.optim.lr_scheduler.LambdaLR(
self.discriminator_optimizer, lr_lambda=lambda e: 1.0 - max(0, e - self.decay_start) / decay_epochs)
# Image transformations
transforms_ = [
transforms.Resize(int(self.img_height * 1.12), InterpolationMode.BICUBIC),
transforms.RandomCrop((self.img_height, self.img_width)),
transforms.RandomHorizontalFlip(),
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
]
# Training data loader
self.dataloader = DataLoader(
ImageDataset(self.dataset_name, transforms_, 'train'),
batch_size=self.batch_size,
shuffle=True,
num_workers=self.data_loader_workers,
)
# Validation data loader
self.valid_dataloader = DataLoader(
ImageDataset(self.dataset_name, transforms_, "test"),
batch_size=5,
shuffle=True,
num_workers=self.data_loader_workers,
)
def run(self):
"""
## Training
We aim to solve:
$$G^{*}, F^{*} = \arg \min_{G,F} \max_{D_X, D_Y} \mathcal{L}(G, F, D_X, D_Y)$$
where,
$G$ translates images from $X \rightarrow Y$,
$F$ translates images from $Y \rightarrow X$,
$D_X$ tests if images are from $X$ space,
$D_Y$ tests if images are from $Y$ space, and
\begin{align}
\mathcal{L}(G, F, D_X, D_Y)
&= \mathcal{L}_{GAN}(G, D_Y, X, Y) \\
&+ \mathcal{L}_{GAN}(F, D_X, Y, X) \\
&+ \lambda_1 \mathcal{L}_{cyc}(G, F) \\
&+ \lambda_2 \mathcal{L}_{identity}(G, F) \\
\\
\mathcal{L}_{GAN}(G, F, D_Y, X, Y)
&= \mathbb{E}_{y \sim p_{data}(y)} \Big[log D_Y(y)\Big] \\
&+ \mathbb{E}_{x \sim p_{data}(x)} \bigg[log\Big(1 - D_Y(G(x))\Big)\bigg] \\
&+ \mathbb{E}_{x \sim p_{data}(x)} \Big[log D_X(x)\Big] \\
&+ \mathbb{E}_{y \sim p_{data}(y)} \bigg[log\Big(1 - D_X(F(y))\Big)\bigg] \\
\\
\mathcal{L}_{cyc}(G, F)
&= \mathbb{E}_{x \sim p_{data}(x)} \Big[\lVert F(G(x)) - x \lVert_1\Big] \\
&+ \mathbb{E}_{y \sim p_{data}(y)} \Big[\lVert G(F(y)) - y \rVert_1\Big] \\
\\
\mathcal{L}_{identity}(G, F)
&= \mathbb{E}_{x \sim p_{data}(x)} \Big[\lVert F(x) - x \lVert_1\Big] \\
&+ \mathbb{E}_{y \sim p_{data}(y)} \Big[\lVert G(y) - y \rVert_1\Big] \\
\end{align}
$\mathcal{L}_{GAN}$ is the generative adversarial loss from the original
GAN paper.
$\mathcal{L}_{cyc}$ is the cyclic loss, where we try to get $F(G(x))$ to be similar to $x$,
and $G(F(y))$ to be similar to $y$.
Basically if the two generators (transformations) are applied in series it should give back the
original image.
This is the main contribution of this paper.
It trains the generators to generate an image of the other distribution that is similar to
the original image.
Without this loss $G(x)$ could generate anything that's from the distribution of $Y$.
Now it needs to generate something from the distribution of $Y$ but still has properties of $x$,
so that $F(G(x)$ can re-generate something like $x$.
$\mathcal{L}_{cyc}$ is the identity loss.
This was used to encourage the mapping to preserve color composition between
the input and the output.
To solve $$G^*, F^*$$,
discriminators $D_X$ and $D_Y$ should **ascend** on the gradient,
\begin{align}
\nabla_{\theta_{D_X, D_Y}} \frac{1}{m} \sum_{i=1}^m
&\Bigg[
\log D_Y\Big(y^{(i)}\Big) \\
&+ \log \Big(1 - D_Y\Big(G\Big(x^{(i)}\Big)\Big)\Big) \\
&+ \log D_X\Big(x^{(i)}\Big) \\
& +\log\Big(1 - D_X\Big(F\Big(y^{(i)}\Big)\Big)\Big)
\Bigg]
\end{align}
That is descend on *negative* log-likelihood loss.
In order to stabilize the training the negative log- likelihood objective
was replaced by a least-squared loss -
the least-squared error of discriminator, labelling real images with 1,
and generated images with 0.
So we want to descend on the gradient,
\begin{align}
\nabla_{\theta_{D_X, D_Y}} \frac{1}{m} \sum_{i=1}^m
&\Bigg[
\bigg(D_Y\Big(y^{(i)}\Big) - 1\bigg)^2 \\
&+ D_Y\Big(G\Big(x^{(i)}\Big)\Big)^2 \\
&+ \bigg(D_X\Big(x^{(i)}\Big) - 1\bigg)^2 \\
&+ D_X\Big(F\Big(y^{(i)}\Big)\Big)^2
\Bigg]
\end{align}
We use least-squares for generators also.
The generators should *descend* on the gradient,
\begin{align}
\nabla_{\theta_{F, G}} \frac{1}{m} \sum_{i=1}^m
&\Bigg[
\bigg(D_Y\Big(G\Big(x^{(i)}\Big)\Big) - 1\bigg)^2 \\
&+ \bigg(D_X\Big(F\Big(y^{(i)}\Big)\Big) - 1\bigg)^2 \\
&+ \mathcal{L}_{cyc}(G, F)
+ \mathcal{L}_{identity}(G, F)
\Bigg]
\end{align}
We use `generator_xy` for $G$ and `generator_yx` for $F$.
We use `discriminator_x` for $D_X$ and `discriminator_y` for $D_Y$.
"""
# Replay buffers to keep generated samples
gen_x_buffer = ReplayBuffer()
gen_y_buffer = ReplayBuffer()
# Loop through epochs
for epoch in monit.loop(self.epochs):
# Loop through the dataset
for i, batch in monit.enum('Train', self.dataloader):
# Move images to the device
data_x, data_y = batch['x'].to(self.device), batch['y'].to(self.device)
# true labels equal to $1$
true_labels = torch.ones(data_x.size(0), *self.discriminator_x.output_shape,
device=self.device, requires_grad=False)
# false labels equal to $0$
false_labels = torch.zeros(data_x.size(0), *self.discriminator_x.output_shape,
device=self.device, requires_grad=False)
# Train the generators.
# This returns the generated images.
gen_x, gen_y = self.optimize_generators(data_x, data_y, true_labels)
# Train discriminators
self.optimize_discriminator(data_x, data_y,
gen_x_buffer.push_and_pop(gen_x), gen_y_buffer.push_and_pop(gen_y),
true_labels, false_labels)
# Save training statistics and increment the global step counter
tracker.save()
tracker.add_global_step(max(len(data_x), len(data_y)))
# Save images at intervals
batches_done = epoch * len(self.dataloader) + i
if batches_done % self.sample_interval == 0:
# Sample images
self.sample_images(batches_done)
# Update learning rates
self.generator_lr_scheduler.step()
self.discriminator_lr_scheduler.step()
# New line
tracker.new_line()
def optimize_generators(self, data_x: torch.Tensor, data_y: torch.Tensor, true_labels: torch.Tensor):
"""
### Optimize the generators with identity, gan and cycle losses.
"""
# Change to training mode
self.generator_xy.train()
self.generator_yx.train()
# Identity loss
# $$\lVert F(G(x^{(i)})) - x^{(i)} \lVert_1\
# \lVert G(F(y^{(i)})) - y^{(i)} \rVert_1$$
loss_identity = (self.identity_loss(self.generator_yx(data_x), data_x) +
self.identity_loss(self.generator_xy(data_y), data_y))
# Generate images $G(x)$ and $F(y)$
gen_y = self.generator_xy(data_x)
gen_x = self.generator_yx(data_y)
# GAN loss
# $$\bigg(D_Y\Big(G\Big(x^{(i)}\Big)\Big) - 1\bigg)^2
# + \bigg(D_X\Big(F\Big(y^{(i)}\Big)\Big) - 1\bigg)^2$$
loss_gan = (self.gan_loss(self.discriminator_y(gen_y), true_labels) +
self.gan_loss(self.discriminator_x(gen_x), true_labels))
# Cycle loss
# $$
# \lVert F(G(x^{(i)})) - x^{(i)} \lVert_1 +
# \lVert G(F(y^{(i)})) - y^{(i)} \rVert_1
# $$
loss_cycle = (self.cycle_loss(self.generator_yx(gen_y), data_x) +
self.cycle_loss(self.generator_xy(gen_x), data_y))
# Total loss
loss_generator = (loss_gan +
self.cyclic_loss_coefficient * loss_cycle +
self.identity_loss_coefficient * loss_identity)
# Take a step in the optimizer
self.generator_optimizer.zero_grad()
loss_generator.backward()
self.generator_optimizer.step()
# Log losses
tracker.add({'loss.generator': loss_generator,
'loss.generator.cycle': loss_cycle,
'loss.generator.gan': loss_gan,
'loss.generator.identity': loss_identity})
# Return generated images
return gen_x, gen_y
def optimize_discriminator(self, data_x: torch.Tensor, data_y: torch.Tensor,
gen_x: torch.Tensor, gen_y: torch.Tensor,
true_labels: torch.Tensor, false_labels: torch.Tensor):
"""
### Optimize the discriminators with gan loss.
"""
# GAN Loss
#
# \begin{align}
# \bigg(D_Y\Big(y ^ {(i)}\Big) - 1\bigg) ^ 2
# + D_Y\Big(G\Big(x ^ {(i)}\Big)\Big) ^ 2 + \\
# \bigg(D_X\Big(x ^ {(i)}\Big) - 1\bigg) ^ 2
# + D_X\Big(F\Big(y ^ {(i)}\Big)\Big) ^ 2
# \end{align}
loss_discriminator = (self.gan_loss(self.discriminator_x(data_x), true_labels) +
self.gan_loss(self.discriminator_x(gen_x), false_labels) +
self.gan_loss(self.discriminator_y(data_y), true_labels) +
self.gan_loss(self.discriminator_y(gen_y), false_labels))
# Take a step in the optimizer
self.discriminator_optimizer.zero_grad()
loss_discriminator.backward()
self.discriminator_optimizer.step()
# Log losses
tracker.add({'loss.discriminator': loss_discriminator})
def train():
"""
## Train Cycle GAN
"""
# Create configurations
conf = Configs()
# Create an experiment
experiment.create(name='cycle_gan')
# Calculate configurations.
# It will calculate `conf.run` and all other configs required by it.
experiment.configs(conf, {'dataset_name': 'summer2winter_yosemite'})
conf.initialize()
# Register models for saving and loading.
# `get_modules` gives a dictionary of `nn.Modules` in `conf`.
# You can also specify a custom dictionary of models.
experiment.add_pytorch_models(get_modules(conf))
# Start and watch the experiment
with experiment.start():
# Run the training
conf.run()
def plot_image(img: torch.Tensor):
"""
### Plot an image with matplotlib
"""
from matplotlib import pyplot as plt
# Move tensor to CPU
img = img.cpu()
# Get min and max values of the image for normalization
img_min, img_max = img.min(), img.max()
# Scale image values to be [0...1]
img = (img - img_min) / (img_max - img_min + 1e-5)
# We have to change the order of dimensions to HWC.
img = img.permute(1, 2, 0)
# Show Image
plt.imshow(img)
# We don't need axes
plt.axis('off')
# Display
plt.show()
def evaluate():
"""
## Evaluate trained Cycle GAN
"""
# Set the run UUID from the training run
trained_run_uuid = 'f73c1164184711eb9190b74249275441'
# Create configs object
conf = Configs()
# Create experiment
experiment.create(name='cycle_gan_inference')
# Load hyper parameters set for training
conf_dict = experiment.load_configs(trained_run_uuid)
# Calculate configurations. We specify the generators `'generator_xy', 'generator_yx'`
# so that it only loads those and their dependencies.
# Configs like `device` and `img_channels` will be calculated, since these are required by
# `generator_xy` and `generator_yx`.
#
# If you want other parameters like `dataset_name` you should specify them here.
# If you specify nothing, all the configurations will be calculated, including data loaders.
# Calculation of configurations and their dependencies will happen when you call `experiment.start`
experiment.configs(conf, conf_dict)
conf.initialize()
# Register models for saving and loading.
# `get_modules` gives a dictionary of `nn.Modules` in `conf`.
# You can also specify a custom dictionary of models.
experiment.add_pytorch_models(get_modules(conf))
# Specify which run to load from.
# Loading will actually happen when you call `experiment.start`
experiment.load(trained_run_uuid)
# Start the experiment
with experiment.start():
# Image transformations
transforms_ = [
transforms.ToTensor(),
transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),
]
# Load your own data. Here we try the test set.
# I was trying with Yosemite photos, they look awesome.
# You can use `conf.dataset_name`, if you specified `dataset_name` as something you wanted to be calculated
# in the call to `experiment.configs`
dataset = ImageDataset(conf.dataset_name, transforms_, 'train')
# Get an image from dataset
x_image = dataset[10]['x']
# Display the image
plot_image(x_image)
# Evaluation mode
conf.generator_xy.eval()
conf.generator_yx.eval()
# We don't need gradients
with torch.no_grad():
# Add batch dimension and move to the device we use
data = x_image.unsqueeze(0).to(conf.device)
generated_y = conf.generator_xy(data)
# Display the generated image.
plot_image(generated_y[0].cpu())
if __name__ == '__main__':
train()
# evaluate()
+226
View File
@@ -0,0 +1,226 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Cycle GAN",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"accelerator": "GPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "AYV_dMVDxyc2"
},
"source": [
"[![Github](https://img.shields.io/github/stars/labmlai/annotated_deep_learning_paper_implementations?style=social)](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/cycle_gan/experiment.ipynb)\n",
"\n",
"## Cycle GAN\n",
"\n",
"This is an experiment training Cycle GAN model."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "AahG_i2y5tY9"
},
"source": [
"Install the `labml-nn` package"
]
},
{
"cell_type": "code",
"metadata": {
"id": "ZCzmCrAIVg0L",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "2fe2685f-731c-4c47-854e-a4f00e485281"
},
"source": [
"!pip install labml-nn"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "SE2VUQ6L5zxI"
},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"metadata": {
"id": "0hJXx_g0wS2C"
},
"source": [
"from labml import experiment\n",
"from labml.utils.pytorch import get_modules\n",
"from labml_nn.gan.cycle_gan import Configs"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "Lpggo0wM6qb-"
},
"source": [
"Create an experiment"
]
},
{
"cell_type": "code",
"metadata": {
"id": "bFcr9k-l4cAg"
},
"source": [
"experiment.create(name=\"cycle_gan\")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "-OnHLi626tJt"
},
"source": [
"Initialize configurations"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Piz0c5f44hRo"
},
"source": [
"conf = Configs()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "wwMzCqpD6vkL"
},
"source": [
"Set experiment configurations and assign a configurations dictionary to override configurations"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 17
},
"id": "e6hmQhTw4nks",
"outputId": "4be767af-0ebd-4c35-8da0-0e532495e037"
},
"source": [
"experiment.configs(conf, {'dataset_name': 'summer2winter_yosemite'})"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "DHyNvXfnzeWQ"
},
"source": [
"Initialize"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 85
},
"id": "59ZeTv5SzcVe",
"outputId": "55f4af22-b6df-4335-e4fb-d6d675e69b4e"
},
"source": [
"conf.initialize()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "EvI7MtgJ61w5"
},
"source": [
"Set PyTorch models for loading and saving"
]
},
{
"cell_type": "code",
"metadata": {
"id": "GDlt7dp-5ALt"
},
"source": [
"experiment.add_pytorch_models(get_modules(conf))"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "KJZRf8527GxL"
},
"source": [
"Start the experiment and run the training loop."
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 649
},
"id": "aIAWo7Fw5DR8",
"outputId": "e3b02247-8ff9-47b5-8f52-49c9e3b8377f"
},
"source": [
"with experiment.start():\n",
" conf.run()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {
"id": "oBXXlP2b7XZO"
},
"source": [
""
],
"outputs": [],
"execution_count": null
}
]
}
+4
View File
@@ -0,0 +1,4 @@
# [Cycle GAN](https://nn.labml.ai/gan/cycle_gan/index.html)
This is a [PyTorch](https://pytorch.org) implementation/tutorial of the paper
[Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks](https://arxiv.org/abs/1703.10593).
+119
View File
@@ -0,0 +1,119 @@
"""
---
title: Deep Convolutional Generative Adversarial Networks (DCGAN)
summary: A simple PyTorch implementation/tutorial of Deep Convolutional Generative Adversarial Networks (DCGAN).
---
# Deep Convolutional Generative Adversarial Networks (DCGAN)
This is a [PyTorch](https://pytorch.org) implementation of paper
[Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks](https://arxiv.org/abs/1511.06434).
This implementation is based on the [PyTorch DCGAN Tutorial](https://pytorch.org/tutorials/beginner/dcgan_faces_tutorial.html).
"""
import torch.nn as nn
from labml import experiment
from labml.configs import calculate
from labml_nn.gan.original.experiment import Configs
class Generator(nn.Module):
"""
### Convolutional Generator Network
This is similar to the de-convolutional network used for CelebA faces,
but modified for MNIST images.
![DCGan Architecture](https://pytorch.org/tutorials/_images/dcgan_generator.png)
"""
def __init__(self):
super().__init__()
# The input is $1 \times 1$ with 100 channels
self.layers = nn.Sequential(
# This gives $3 \times 3$ output
nn.ConvTranspose2d(100, 1024, 3, 1, 0, bias=False),
nn.BatchNorm2d(1024),
nn.ReLU(True),
# This gives $7 \times 7$
nn.ConvTranspose2d(1024, 512, 3, 2, 0, bias=False),
nn.BatchNorm2d(512),
nn.ReLU(True),
# This gives $14 \times 14$
nn.ConvTranspose2d(512, 256, 4, 2, 1, bias=False),
nn.BatchNorm2d(256),
nn.ReLU(True),
# This gives $28 \times 28$
nn.ConvTranspose2d(256, 1, 4, 2, 1, bias=False),
nn.Tanh()
)
self.apply(_weights_init)
def forward(self, x):
# Change from shape `[batch_size, 100]` to `[batch_size, 100, 1, 1]`
x = x.unsqueeze(-1).unsqueeze(-1)
x = self.layers(x)
return x
class Discriminator(nn.Module):
"""
### Convolutional Discriminator Network
"""
def __init__(self):
super().__init__()
# The input is $28 \times 28$ with one channel
self.layers = nn.Sequential(
# This gives $14 \times 14$
nn.Conv2d(1, 256, 4, 2, 1, bias=False),
nn.LeakyReLU(0.2, inplace=True),
# This gives $7 \times 7$
nn.Conv2d(256, 512, 4, 2, 1, bias=False),
nn.BatchNorm2d(512),
nn.LeakyReLU(0.2, inplace=True),
# This gives $3 \times 3$
nn.Conv2d(512, 1024, 3, 2, 0, bias=False),
nn.BatchNorm2d(1024),
nn.LeakyReLU(0.2, inplace=True),
# This gives $1 \times 1$
nn.Conv2d(1024, 1, 3, 1, 0, bias=False),
)
self.apply(_weights_init)
def forward(self, x):
x = self.layers(x)
return x.view(x.shape[0], -1)
def _weights_init(m):
classname = m.__class__.__name__
if classname.find('Conv') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
# We import the [simple gan experiment](../original/experiment.html) and change the
# generator and discriminator networks
calculate(Configs.generator, 'cnn', lambda c: Generator().to(c.device))
calculate(Configs.discriminator, 'cnn', lambda c: Discriminator().to(c.device))
def main():
conf = Configs()
experiment.create(name='mnist_dcgan')
experiment.configs(conf,
{'discriminator': 'cnn',
'generator': 'cnn',
'label_smoothing': 0.01})
with experiment.start():
conf.run()
if __name__ == '__main__':
main()
+184
View File
@@ -0,0 +1,184 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Cycle GAN",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"language": "python",
"display_name": "Python 3"
},
"accelerator": "GPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "AYV_dMVDxyc2"
},
"source": [
"[![Github](https://img.shields.io/github/stars/labmlai/annotated_deep_learning_paper_implementations?style=social)](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/dcgan/experiment.ipynb)\n",
"\n",
"## DCGAN\n",
"\n",
"This is an experiment training DCGAN model."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "AahG_i2y5tY9"
},
"source": [
"Install the `labml-nn` package"
]
},
{
"cell_type": "code",
"metadata": {
"id": "ZCzmCrAIVg0L",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "2fe2685f-731c-4c47-854e-a4f00e485281"
},
"source": [
"!pip install labml-nn"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "SE2VUQ6L5zxI"
},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"metadata": {
"id": "0hJXx_g0wS2C"
},
"source": [
"from labml import experiment\n",
"from labml_nn.gan.dcgan import Configs"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "Lpggo0wM6qb-"
},
"source": [
"Create an experiment"
]
},
{
"cell_type": "code",
"metadata": {
"id": "bFcr9k-l4cAg"
},
"source": [
"experiment.create(name=\"mnist_dcgan\")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "-OnHLi626tJt"
},
"source": [
"Initialize configurations"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Piz0c5f44hRo"
},
"source": [
"conf = Configs()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "wwMzCqpD6vkL"
},
"source": [
"Set experiment configurations and assign a configurations dictionary to override configurations"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 17
},
"id": "e6hmQhTw4nks",
"outputId": "4be767af-0ebd-4c35-8da0-0e532495e037"
},
"source": [
"experiment.configs(conf,\n",
" {'discriminator': 'cnn',\n",
" 'generator': 'cnn',\n",
" 'label_smoothing': 0.01})"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "KJZRf8527GxL"
},
"source": [
"Start the experiment and run the training loop."
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 649
},
"id": "aIAWo7Fw5DR8",
"outputId": "e3b02247-8ff9-47b5-8f52-49c9e3b8377f"
},
"source": [
"with experiment.start():\n",
" conf.run()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {
"id": "oBXXlP2b7XZO"
},
"source": [
""
],
"outputs": [],
"execution_count": null
}
]
}
+4
View File
@@ -0,0 +1,4 @@
# [Deep Convolutional Generative Adversarial Networks - DCGAN](https://nn.labml.ai/gan/dcgan/index.html)
This is a [PyTorch](https://pytorch.org) implementation of paper
[Unsupervised Representation Learning with Deep Convolutional Generative Adversarial Networks](https://arxiv.org/abs/1511.06434).
+125
View File
@@ -0,0 +1,125 @@
"""
---
title: Generative Adversarial Networks (GAN)
summary: A simple PyTorch implementation/tutorial of Generative Adversarial Networks (GAN) loss functions.
---
# Generative Adversarial Networks (GAN)
This is an implementation of
[Generative Adversarial Networks](https://arxiv.org/abs/1406.2661).
The generator, $G(\pmb{z}; \theta_g)$ generates samples that match the
distribution of data, while the discriminator, $D(\pmb{x}; \theta_g)$
gives the probability that $\pmb{x}$ came from data rather than $G$.
We train $D$ and $G$ simultaneously on a two-player min-max game with value
function $V(G, D)$.
$$\min_G \max_D V(D, G) =
\mathop{\mathbb{E}}_{\pmb{x} \sim p_{data}(\pmb{x})}
\big[\log D(\pmb{x})\big] +
\mathop{\mathbb{E}}_{\pmb{z} \sim p_{\pmb{z}}(\pmb{z})}
\big[\log (1 - D(G(\pmb{z}))\big]
$$
$p_{data}(\pmb{x})$ is the probability distribution over data,
whilst $p_{\pmb{z}}(\pmb{z})$ probability distribution of $\pmb{z}$, which is set to
gaussian noise.
This file defines the loss functions. [Here](experiment.html) is an MNIST example
with two multilayer perceptron for the generator and discriminator.
"""
import torch
import torch.nn as nn
import torch.utils.data
import torch.utils.data
class DiscriminatorLogitsLoss(nn.Module):
"""
## Discriminator Loss
Discriminator should **ascend** on the gradient,
$$\nabla_{\theta_d} \frac{1}{m} \sum_{i=1}^m \Bigg[
\log D\Big(\pmb{x}^{(i)}\Big) +
\log \Big(1 - D\Big(G\Big(\pmb{z}^{(i)}\Big)\Big)\Big)
\Bigg]$$
$m$ is the mini-batch size and $(i)$ is used to index samples in the mini-batch.
$\pmb{x}$ are samples from $p_{data}$ and $\pmb{z}$ are samples from $p_z$.
"""
def __init__(self, smoothing: float = 0.2):
super().__init__()
# We use PyTorch Binary Cross Entropy Loss, which is
# $-\sum\Big[y \log(\hat{y}) + (1 - y) \log(1 - \hat{y})\Big]$,
# where $y$ are the labels and $\hat{y}$ are the predictions.
# *Note the negative sign*.
# We use labels equal to $1$ for $\pmb{x}$ from $p_{data}$
# and labels equal to $0$ for $\pmb{x}$ from $p_{G}.$
# Then descending on the sum of these is the same as ascending on
# the above gradient.
#
# `BCEWithLogitsLoss` combines softmax and binary cross entropy loss.
self.loss_true = nn.BCEWithLogitsLoss()
self.loss_false = nn.BCEWithLogitsLoss()
# We use label smoothing because it seems to work better in some cases
self.smoothing = smoothing
# Labels are registered as buffered and persistence is set to `False`.
self.register_buffer('labels_true', _create_labels(256, 1.0 - smoothing, 1.0), False)
self.register_buffer('labels_false', _create_labels(256, 0.0, smoothing), False)
def forward(self, logits_true: torch.Tensor, logits_false: torch.Tensor):
"""
`logits_true` are logits from $D(\pmb{x}^{(i)})$ and
`logits_false` are logits from $D(G(\pmb{z}^{(i)}))$
"""
if len(logits_true) > len(self.labels_true):
self.register_buffer("labels_true",
_create_labels(len(logits_true), 1.0 - self.smoothing, 1.0, logits_true.device), False)
if len(logits_false) > len(self.labels_false):
self.register_buffer("labels_false",
_create_labels(len(logits_false), 0.0, self.smoothing, logits_false.device), False)
return (self.loss_true(logits_true, self.labels_true[:len(logits_true)]),
self.loss_false(logits_false, self.labels_false[:len(logits_false)]))
class GeneratorLogitsLoss(nn.Module):
"""
## Generator Loss
Generator should **descend** on the gradient,
$$\nabla_{\theta_g} \frac{1}{m} \sum_{i=1}^m \Bigg[
\log \Big(1 - D\Big(G\Big(\pmb{z}^{(i)}\Big)\Big)\Big)
\Bigg]$$
"""
def __init__(self, smoothing: float = 0.2):
super().__init__()
self.loss_true = nn.BCEWithLogitsLoss()
self.smoothing = smoothing
# We use labels equal to $1$ for $\pmb{x}$ from $p_{G}.$
# Then descending on this loss is the same as descending on
# the above gradient.
self.register_buffer('fake_labels', _create_labels(256, 1.0 - smoothing, 1.0), False)
def forward(self, logits: torch.Tensor):
if len(logits) > len(self.fake_labels):
self.register_buffer("fake_labels",
_create_labels(len(logits), 1.0 - self.smoothing, 1.0, logits.device), False)
return self.loss_true(logits, self.fake_labels[:len(logits)])
def _create_labels(n: int, r1: float, r2: float, device: torch.device = None):
"""
Create smoothed labels
"""
return torch.empty(n, 1, requires_grad=False, device=device).uniform_(r1, r2)
+183
View File
@@ -0,0 +1,183 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Cycle GAN",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"language": "python",
"display_name": "Python 3"
},
"accelerator": "GPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "AYV_dMVDxyc2"
},
"source": [
"[![Github](https://img.shields.io/github/stars/labmlai/annotated_deep_learning_paper_implementations?style=social)](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/original/experiment.ipynb)\n",
"\n",
"## DCGAN\n",
"\n",
"This is an experiment training DCGAN model."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "AahG_i2y5tY9"
},
"source": [
"Install the `labml-nn` package"
]
},
{
"cell_type": "code",
"metadata": {
"id": "ZCzmCrAIVg0L",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "2fe2685f-731c-4c47-854e-a4f00e485281"
},
"source": [
"!pip install labml-nn"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "SE2VUQ6L5zxI"
},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"metadata": {
"id": "0hJXx_g0wS2C"
},
"source": [
"\n",
"from labml import experiment\n",
"from labml_nn.gan.original.experiment import Configs"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "Lpggo0wM6qb-"
},
"source": [
"Create an experiment"
]
},
{
"cell_type": "code",
"metadata": {
"id": "bFcr9k-l4cAg"
},
"source": [
"experiment.create(name=\"mnist_gan\")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "-OnHLi626tJt"
},
"source": [
"Initialize configurations"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Piz0c5f44hRo"
},
"source": [
"conf = Configs()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "wwMzCqpD6vkL"
},
"source": [
"Set experiment configurations and assign a configurations dictionary to override configurations"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 17
},
"id": "e6hmQhTw4nks",
"outputId": "4be767af-0ebd-4c35-8da0-0e532495e037"
},
"source": [
"experiment.configs(conf,\n",
" {'label_smoothing': 0.01})"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "KJZRf8527GxL"
},
"source": [
"Start the experiment and run the training loop."
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 649
},
"id": "aIAWo7Fw5DR8",
"outputId": "e3b02247-8ff9-47b5-8f52-49c9e3b8377f"
},
"source": [
"with experiment.start():\n",
" conf.run()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {
"id": "oBXXlP2b7XZO"
},
"source": [
""
],
"outputs": [],
"execution_count": null
}
]
}
+250
View File
@@ -0,0 +1,250 @@
"""
---
title: Generative Adversarial Networks experiment with MNIST
summary: This experiment generates MNIST images using multi-layer perceptron.
---
# Generative Adversarial Networks experiment with MNIST
"""
from typing import Any
from torchvision import transforms
import torch
import torch.nn as nn
import torch.utils.data
from labml import tracker, monit, experiment
from labml.configs import option, calculate
from labml_nn.gan.original import DiscriminatorLogitsLoss, GeneratorLogitsLoss
from labml_nn.helpers.datasets import MNISTConfigs
from labml_nn.helpers.device import DeviceConfigs
from labml_nn.helpers.optimizer import OptimizerConfigs
from labml_nn.helpers.trainer import TrainValidConfigs, BatchIndex
def weights_init(m):
classname = m.__class__.__name__
if classname.find('Linear') != -1:
nn.init.normal_(m.weight.data, 0.0, 0.02)
elif classname.find('BatchNorm') != -1:
nn.init.normal_(m.weight.data, 1.0, 0.02)
nn.init.constant_(m.bias.data, 0)
class Generator(nn.Module):
"""
### Simple MLP Generator
This has three linear layers of increasing size with `LeakyReLU` activations.
The final layer has a $tanh$ activation.
"""
def __init__(self):
super().__init__()
layer_sizes = [256, 512, 1024]
layers = []
d_prev = 100
for size in layer_sizes:
layers = layers + [nn.Linear(d_prev, size), nn.LeakyReLU(0.2)]
d_prev = size
self.layers = nn.Sequential(*layers, nn.Linear(d_prev, 28 * 28), nn.Tanh())
self.apply(weights_init)
def forward(self, x):
return self.layers(x).view(x.shape[0], 1, 28, 28)
class Discriminator(nn.Module):
"""
### Simple MLP Discriminator
This has three linear layers of decreasing size with `LeakyReLU` activations.
The final layer has a single output that gives the logit of whether input
is real or fake. You can get the probability by calculating the sigmoid of it.
"""
def __init__(self):
super().__init__()
layer_sizes = [1024, 512, 256]
layers = []
d_prev = 28 * 28
for size in layer_sizes:
layers = layers + [nn.Linear(d_prev, size), nn.LeakyReLU(0.2)]
d_prev = size
self.layers = nn.Sequential(*layers, nn.Linear(d_prev, 1))
self.apply(weights_init)
def forward(self, x):
return self.layers(x.view(x.shape[0], -1))
class Configs(MNISTConfigs, TrainValidConfigs):
"""
## Configurations
This extends MNIST configurations to get the data loaders and Training and validation loop
configurations to simplify our implementation.
"""
device: torch.device = DeviceConfigs()
dataset_transforms = 'mnist_gan_transforms'
epochs: int = 10
is_save_models = True
discriminator: nn.Module = 'mlp'
generator: nn.Module = 'mlp'
generator_optimizer: torch.optim.Adam
discriminator_optimizer: torch.optim.Adam
generator_loss: GeneratorLogitsLoss = 'original'
discriminator_loss: DiscriminatorLogitsLoss = 'original'
label_smoothing: float = 0.2
discriminator_k: int = 1
def init(self):
"""
Initializations
"""
self.state_modules = []
tracker.set_scalar("loss.generator.*", True)
tracker.set_scalar("loss.discriminator.*", True)
tracker.set_image("generated", True, 1 / 100)
def sample_z(self, batch_size: int):
"""
$$z \sim p(z)$$
"""
return torch.randn(batch_size, 100, device=self.device)
def step(self, batch: Any, batch_idx: BatchIndex):
"""
Take a training step
"""
# Set model states
self.generator.train(self.mode.is_train)
self.discriminator.train(self.mode.is_train)
# Get MNIST images
data = batch[0].to(self.device)
# Increment step in training mode
if self.mode.is_train:
tracker.add_global_step(len(data))
# Train the discriminator
with monit.section("discriminator"):
# Get discriminator loss
loss = self.calc_discriminator_loss(data)
# Train
if self.mode.is_train:
self.discriminator_optimizer.zero_grad()
loss.backward()
if batch_idx.is_last:
tracker.add('discriminator', self.discriminator)
self.discriminator_optimizer.step()
# Train the generator once in every `discriminator_k`
if batch_idx.is_interval(self.discriminator_k):
with monit.section("generator"):
loss = self.calc_generator_loss(data.shape[0])
# Train
if self.mode.is_train:
self.generator_optimizer.zero_grad()
loss.backward()
if batch_idx.is_last:
tracker.add('generator', self.generator)
self.generator_optimizer.step()
tracker.save()
def calc_discriminator_loss(self, data):
"""
Calculate discriminator loss
"""
latent = self.sample_z(data.shape[0])
logits_true = self.discriminator(data)
logits_false = self.discriminator(self.generator(latent).detach())
loss_true, loss_false = self.discriminator_loss(logits_true, logits_false)
loss = loss_true + loss_false
# Log stuff
tracker.add("loss.discriminator.true.", loss_true)
tracker.add("loss.discriminator.false.", loss_false)
tracker.add("loss.discriminator.", loss)
return loss
def calc_generator_loss(self, batch_size: int):
"""
Calculate generator loss
"""
latent = self.sample_z(batch_size)
generated_images = self.generator(latent)
logits = self.discriminator(generated_images)
loss = self.generator_loss(logits)
# Log stuff
tracker.add('generated', generated_images[0:6])
tracker.add("loss.generator.", loss)
return loss
@option(Configs.dataset_transforms)
def mnist_gan_transforms():
return transforms.Compose([
transforms.ToTensor(),
transforms.Normalize((0.5,), (0.5,))
])
@option(Configs.discriminator_optimizer)
def _discriminator_optimizer(c: Configs):
opt_conf = OptimizerConfigs()
opt_conf.optimizer = 'Adam'
opt_conf.parameters = c.discriminator.parameters()
opt_conf.learning_rate = 2.5e-4
# Setting exponent decay rate for first moment of gradient,
# $\beta_1$ to `0.5` is important.
# Default of `0.9` fails.
opt_conf.betas = (0.5, 0.999)
return opt_conf
@option(Configs.generator_optimizer)
def _generator_optimizer(c: Configs):
opt_conf = OptimizerConfigs()
opt_conf.optimizer = 'Adam'
opt_conf.parameters = c.generator.parameters()
opt_conf.learning_rate = 2.5e-4
# Setting exponent decay rate for first moment of gradient,
# $\beta_1$ to `0.5` is important.
# Default of `0.9` fails.
opt_conf.betas = (0.5, 0.999)
return opt_conf
calculate(Configs.generator, 'mlp', lambda c: Generator().to(c.device))
calculate(Configs.discriminator, 'mlp', lambda c: Discriminator().to(c.device))
calculate(Configs.generator_loss, 'original', lambda c: GeneratorLogitsLoss(c.label_smoothing).to(c.device))
calculate(Configs.discriminator_loss, 'original', lambda c: DiscriminatorLogitsLoss(c.label_smoothing).to(c.device))
def main():
conf = Configs()
experiment.create(name='mnist_gan', comment='test')
experiment.configs(conf,
{'label_smoothing': 0.01})
with experiment.start():
conf.run()
if __name__ == '__main__':
main()
+4
View File
@@ -0,0 +1,4 @@
# [Generative Adversarial Networks - GAN](https://nn.labml.ai/gan/original/index.html)
This is an annotated implementation of
[Generative Adversarial Networks](https://arxiv.org/abs/1406.2661).
+965
View File
@@ -0,0 +1,965 @@
"""
---
title: StyleGAN 2
summary: >
An annotated PyTorch implementation of StyleGAN2.
---
# StyleGAN 2
This is a [PyTorch](https://pytorch.org) implementation of the paper
[Analyzing and Improving the Image Quality of StyleGAN](https://arxiv.org/abs/1912.04958)
which introduces **StyleGAN 2**.
StyleGAN 2 is an improvement over **StyleGAN** from the paper
[A Style-Based Generator Architecture for Generative Adversarial Networks](https://arxiv.org/abs/1812.04948).
And StyleGAN is based on **Progressive GAN** from the paper
[Progressive Growing of GANs for Improved Quality, Stability, and Variation](https://arxiv.org/abs/1710.10196).
All three papers are from the same authors from [NVIDIA AI](https://twitter.com/NVIDIAAI).
*Our implementation is a minimalistic StyleGAN 2 model training code.
Only single GPU training is supported to keep the implementation simple.
We managed to shrink it to keep it at less than 500 lines of code, including the training loop.*
**🏃 Here's the training code: [`experiment.py`](experiment.html).**
![Generated Images](generated_64.png)
---*These are $64 \times 64$ images generated after training for about 80K steps.*---
We'll first introduce the three papers at a high level.
## Generative Adversarial Networks
Generative adversarial networks have two components; the generator and the discriminator.
The generator network takes a random latent vector ($z \in \mathcal{Z}$)
and tries to generate a realistic image.
The discriminator network tries to differentiate the real images from generated images.
When we train the two networks together the generator starts generating images indistinguishable from real images.
## Progressive GAN
Progressive GAN generates high-resolution images ($1080 \times 1080$) of size.
It does so by *progressively* increasing the image size.
First, it trains a network that produces a $4 \times 4$ image, then $8 \times 8$ ,
then an $16 \times 16$ image, and so on up to the desired image resolution.
At each resolution, the generator network produces an image in latent space which is converted into RGB,
with a $1 \times 1$ convolution.
When we progress from a lower resolution to a higher resolution
(say from $4 \times 4$ to $8 \times 8$ ) we scale the latent image by $2\times$
and add a new block (two $3 \times 3$ convolution layers)
and a new $1 \times 1$ layer to get RGB.
The transition is done smoothly by adding a residual connection to
the $2\times$ scaled $4 \times 4$ RGB image.
The weight of this residual connection is slowly reduced, to let the new block take over.
The discriminator is a mirror image of the generator network.
The progressive growth of the discriminator is done similarly.
![progressive_gan.svg](progressive_gan.svg)
---*$2\times$ and $0.5\times$ denote feature map resolution scaling and scaling.
$4\times4$, $8\times4$, ... denote feature map resolution at the generator or discriminator block.
Each discriminator and generator block consists of 2 convolution layers with leaky ReLU activations.*---
They use **minibatch standard deviation** to increase variation and
**equalized learning rate** which we discussed below in the implementation.
They also use **pixel-wise normalization** where at each pixel the feature vector is normalized.
They apply this to all the convolution layer outputs (except RGB).
## StyleGAN
StyleGAN improves the generator of Progressive GAN keeping the discriminator architecture the same.
#### Mapping Network
It maps the random latent vector ($z \in \mathcal{Z}$)
into a different latent space ($w \in \mathcal{W}$),
with an 8-layer neural network.
This gives an intermediate latent space $\mathcal{W}$
where the factors of variations are more linear (disentangled).
#### AdaIN
Then $w$ is transformed into two vectors (**styles**) per layer,
$i$, $y_i = (y_{s,i}, y_{b,i}) = f_{A_i}(w)$ and used for scaling and shifting (biasing)
in each layer with $\text{AdaIN}$ operator (normalize and scale):
$$\text{AdaIN}(x_i, y_i) = y_{s, i} \frac{x_i - \mu(x_i)}{\sigma(x_i)} + y_{b,i}$$
#### Style Mixing
To prevent the generator from assuming adjacent styles are correlated,
they randomly use different styles for different blocks.
That is, they sample two latent vectors $(z_1, z_2)$ and corresponding $(w_1, w_2)$ and
use $w_1$ based styles for some blocks and $w_2$ based styles for some blacks randomly.
#### Stochastic Variation
Noise is made available to each block which helps the generator create more realistic images.
Noise is scaled per channel by a learned weight.
#### Bilinear Up and Down Sampling
All the up and down-sampling operations are accompanied by bilinear smoothing.
![style_gan.svg](style_gan.svg)
---*$A$ denotes a linear layer.
$B$ denotes a broadcast and scaling operation (noise is a single channel).
StyleGAN also uses progressive growing like Progressive GAN.*---
## StyleGAN 2
StyleGAN 2 changes both the generator and the discriminator of StyleGAN.
#### Weight Modulation and Demodulation
They remove the $\text{AdaIN}$ operator and replace it with
the weight modulation and demodulation step.
This is supposed to improve what they call droplet artifacts that are present in generated images,
which are caused by the normalization in $\text{AdaIN}$ operator.
Style vector per layer is calculated from $w_i \in \mathcal{W}$ as $s_i = f_{A_i}(w_i)$.
Then the convolution weights $w$ are modulated as follows.
($w$ here on refers to weights not intermediate latent space,
we are sticking to the same notation as the paper.)
$$w'_{i, j, k} = s_i \cdot w_{i, j, k}$$
Then it's demodulated by normalizing,
$$w''_{i,j,k} = \frac{w'_{i,j,k}}{\sqrt{\sum_{i,k}{w'_{i, j, k}}^2 + \epsilon}}$$
where $i$ is the input channel, $j$ is the output channel, and $k$ is the kernel index.
#### Path Length Regularization
Path length regularization encourages a fixed-size step in $\mathcal{W}$ to result in a non-zero,
fixed-magnitude change in the generated image.
#### No Progressive Growing
StyleGAN2 uses residual connections (with down-sampling) in the discriminator and skip connections
in the generator with up-sampling
(the RGB outputs from each layer are added - no residual connections in feature maps).
They show that with experiments that the contribution of low-resolution layers is higher
at beginning of the training and then high-resolution layers take over.
"""
import math
from typing import Tuple, Optional, List
import numpy as np
import torch
import torch.nn.functional as F
import torch.utils.data
from torch import nn
class MappingNetwork(nn.Module):
"""
<a id="mapping_network"></a>
## Mapping Network
![Mapping Network](mapping_network.svg)
This is an MLP with 8 linear layers.
The mapping network maps the latent vector $z \in \mathcal{W}$
to an intermediate latent space $w \in \mathcal{W}$.
$\mathcal{W}$ space will be disentangled from the image space
where the factors of variation become more linear.
"""
def __init__(self, features: int, n_layers: int):
"""
* `features` is the number of features in $z$ and $w$
* `n_layers` is the number of layers in the mapping network.
"""
super().__init__()
# Create the MLP
layers = []
for i in range(n_layers):
# [Equalized learning-rate linear layers](#equalized_linear)
layers.append(EqualizedLinear(features, features))
# Leaky Relu
layers.append(nn.LeakyReLU(negative_slope=0.2, inplace=True))
self.net = nn.Sequential(*layers)
def forward(self, z: torch.Tensor):
# Normalize $z$
z = F.normalize(z, dim=1)
# Map $z$ to $w$
return self.net(z)
class Generator(nn.Module):
"""
<a id="generator"></a>
## StyleGAN2 Generator
![Generator](style_gan2.svg)
---*$A$ denotes a linear layer.
$B$ denotes a broadcast and scaling operation (noise is a single channel).
[`toRGB`](#to_rgb) also has a style modulation which is not shown in the diagram to keep it simple.*---
The generator starts with a learned constant.
Then it has a series of blocks. The feature map resolution is doubled at each block
Each block outputs an RGB image and they are scaled up and summed to get the final RGB image.
"""
def __init__(self, log_resolution: int, d_latent: int, n_features: int = 32, max_features: int = 512):
"""
* `log_resolution` is the $\log_2$ of image resolution
* `d_latent` is the dimensionality of $w$
* `n_features` number of features in the convolution layer at the highest resolution (final block)
* `max_features` maximum number of features in any generator block
"""
super().__init__()
# Calculate the number of features for each block
#
# Something like `[512, 512, 256, 128, 64, 32]`
features = [min(max_features, n_features * (2 ** i)) for i in range(log_resolution - 2, -1, -1)]
# Number of generator blocks
self.n_blocks = len(features)
# Trainable $4 \times 4$ constant
self.initial_constant = nn.Parameter(torch.randn((1, features[0], 4, 4)))
# First style block for $4 \times 4$ resolution and layer to get RGB
self.style_block = StyleBlock(d_latent, features[0], features[0])
self.to_rgb = ToRGB(d_latent, features[0])
# Generator blocks
blocks = [GeneratorBlock(d_latent, features[i - 1], features[i]) for i in range(1, self.n_blocks)]
self.blocks = nn.ModuleList(blocks)
# $2 \times$ up sampling layer. The feature space is up sampled
# at each block
self.up_sample = UpSample()
def forward(self, w: torch.Tensor, input_noise: List[Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]]):
"""
* `w` is $w$. In order to mix-styles (use different $w$ for different layers), we provide a separate
$w$ for each [generator block](#generator_block). It has shape `[n_blocks, batch_size, d_latent]`.
* `input_noise` is the noise for each block.
It's a list of pairs of noise sensors because each block (except the initial) has two noise inputs
after each convolution layer (see the diagram).
"""
# Get batch size
batch_size = w.shape[1]
# Expand the learned constant to match batch size
x = self.initial_constant.expand(batch_size, -1, -1, -1)
# The first style block
x = self.style_block(x, w[0], input_noise[0][1])
# Get first rgb image
rgb = self.to_rgb(x, w[0])
# Evaluate rest of the blocks
for i in range(1, self.n_blocks):
# Up sample the feature map
x = self.up_sample(x)
# Run it through the [generator block](#generator_block)
x, rgb_new = self.blocks[i - 1](x, w[i], input_noise[i])
# Up sample the RGB image and add to the rgb from the block
rgb = self.up_sample(rgb) + rgb_new
# Return the final RGB image
return rgb
class GeneratorBlock(nn.Module):
"""
<a id="generator_block"></a>
### Generator Block
![Generator block](generator_block.svg)
---*$A$ denotes a linear layer.
$B$ denotes a broadcast and scaling operation (noise is a single channel).
[`toRGB`](#to_rgb) also has a style modulation which is not shown in the diagram to keep it simple.*---
The generator block consists of two [style blocks](#style_block) ($3 \times 3$ convolutions with style modulation)
and an RGB output.
"""
def __init__(self, d_latent: int, in_features: int, out_features: int):
"""
* `d_latent` is the dimensionality of $w$
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
"""
super().__init__()
# First [style block](#style_block) changes the feature map size to `out_features`
self.style_block1 = StyleBlock(d_latent, in_features, out_features)
# Second [style block](#style_block)
self.style_block2 = StyleBlock(d_latent, out_features, out_features)
# *toRGB* layer
self.to_rgb = ToRGB(d_latent, out_features)
def forward(self, x: torch.Tensor, w: torch.Tensor, noise: Tuple[Optional[torch.Tensor], Optional[torch.Tensor]]):
"""
* `x` is the input feature map of shape `[batch_size, in_features, height, width]`
* `w` is $w$ with shape `[batch_size, d_latent]`
* `noise` is a tuple of two noise tensors of shape `[batch_size, 1, height, width]`
"""
# First style block with first noise tensor.
# The output is of shape `[batch_size, out_features, height, width]`
x = self.style_block1(x, w, noise[0])
# Second style block with second noise tensor.
# The output is of shape `[batch_size, out_features, height, width]`
x = self.style_block2(x, w, noise[1])
# Get RGB image
rgb = self.to_rgb(x, w)
# Return feature map and rgb image
return x, rgb
class StyleBlock(nn.Module):
"""
<a id="style_block"></a>
### Style Block
![Style block](style_block.svg)
---*$A$ denotes a linear layer.
$B$ denotes a broadcast and scaling operation (noise is single channel).*---
Style block has a weight modulation convolution layer.
"""
def __init__(self, d_latent: int, in_features: int, out_features: int):
"""
* `d_latent` is the dimensionality of $w$
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
"""
super().__init__()
# Get style vector from $w$ (denoted by $A$ in the diagram) with
# an [equalized learning-rate linear layer](#equalized_linear)
self.to_style = EqualizedLinear(d_latent, in_features, bias=1.0)
# Weight modulated convolution layer
self.conv = Conv2dWeightModulate(in_features, out_features, kernel_size=3)
# Noise scale
self.scale_noise = nn.Parameter(torch.zeros(1))
# Bias
self.bias = nn.Parameter(torch.zeros(out_features))
# Activation function
self.activation = nn.LeakyReLU(0.2, True)
def forward(self, x: torch.Tensor, w: torch.Tensor, noise: Optional[torch.Tensor]):
"""
* `x` is the input feature map of shape `[batch_size, in_features, height, width]`
* `w` is $w$ with shape `[batch_size, d_latent]`
* `noise` is a tensor of shape `[batch_size, 1, height, width]`
"""
# Get style vector $s$
s = self.to_style(w)
# Weight modulated convolution
x = self.conv(x, s)
# Scale and add noise
if noise is not None:
x = x + self.scale_noise[None, :, None, None] * noise
# Add bias and evaluate activation function
return self.activation(x + self.bias[None, :, None, None])
class ToRGB(nn.Module):
"""
<a id="to_rgb"></a>
### To RGB
![To RGB](to_rgb.svg)
---*$A$ denotes a linear layer.*---
Generates an RGB image from a feature map using $1 \times 1$ convolution.
"""
def __init__(self, d_latent: int, features: int):
"""
* `d_latent` is the dimensionality of $w$
* `features` is the number of features in the feature map
"""
super().__init__()
# Get style vector from $w$ (denoted by $A$ in the diagram) with
# an [equalized learning-rate linear layer](#equalized_linear)
self.to_style = EqualizedLinear(d_latent, features, bias=1.0)
# Weight modulated convolution layer without demodulation
self.conv = Conv2dWeightModulate(features, 3, kernel_size=1, demodulate=False)
# Bias
self.bias = nn.Parameter(torch.zeros(3))
# Activation function
self.activation = nn.LeakyReLU(0.2, True)
def forward(self, x: torch.Tensor, w: torch.Tensor):
"""
* `x` is the input feature map of shape `[batch_size, in_features, height, width]`
* `w` is $w$ with shape `[batch_size, d_latent]`
"""
# Get style vector $s$
style = self.to_style(w)
# Weight modulated convolution
x = self.conv(x, style)
# Add bias and evaluate activation function
return self.activation(x + self.bias[None, :, None, None])
class Conv2dWeightModulate(nn.Module):
"""
### Convolution with Weight Modulation and Demodulation
This layer scales the convolution weights by the style vector and demodulates by normalizing it.
"""
def __init__(self, in_features: int, out_features: int, kernel_size: int,
demodulate: float = True, eps: float = 1e-8):
"""
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
* `kernel_size` is the size of the convolution kernel
* `demodulate` is flag whether to normalize weights by its standard deviation
* `eps` is the $\epsilon$ for normalizing
"""
super().__init__()
# Number of output features
self.out_features = out_features
# Whether to normalize weights
self.demodulate = demodulate
# Padding size
self.padding = (kernel_size - 1) // 2
# [Weights parameter with equalized learning rate](#equalized_weight)
self.weight = EqualizedWeight([out_features, in_features, kernel_size, kernel_size])
# $\epsilon$
self.eps = eps
def forward(self, x: torch.Tensor, s: torch.Tensor):
"""
* `x` is the input feature map of shape `[batch_size, in_features, height, width]`
* `s` is style based scaling tensor of shape `[batch_size, in_features]`
"""
# Get batch size, height and width
b, _, h, w = x.shape
# Reshape the scales
s = s[:, None, :, None, None]
# Get [learning rate equalized weights](#equalized_weight)
weights = self.weight()[None, :, :, :, :]
# $$w`_{i,j,k} = s_i * w_{i,j,k}$$
# where $i$ is the input channel, $j$ is the output channel, and $k$ is the kernel index.
#
# The result has shape `[batch_size, out_features, in_features, kernel_size, kernel_size]`
weights = weights * s
# Demodulate
if self.demodulate:
# $$\sigma_j = \sqrt{\sum_{i,k} (w'_{i, j, k})^2 + \epsilon}$$
sigma_inv = torch.rsqrt((weights ** 2).sum(dim=(2, 3, 4), keepdim=True) + self.eps)
# $$w''_{i,j,k} = \frac{w'_{i,j,k}}{\sqrt{\sum_{i,k} (w'_{i, j, k})^2 + \epsilon}}$$
weights = weights * sigma_inv
# Reshape `x`
x = x.reshape(1, -1, h, w)
# Reshape weights
_, _, *ws = weights.shape
weights = weights.reshape(b * self.out_features, *ws)
# Use grouped convolution to efficiently calculate the convolution with sample wise kernel.
# i.e. we have a different kernel (weights) for each sample in the batch
x = F.conv2d(x, weights, padding=self.padding, groups=b)
# Reshape `x` to `[batch_size, out_features, height, width]` and return
return x.reshape(-1, self.out_features, h, w)
class Discriminator(nn.Module):
"""
<a id="discriminator"></a>
## StyleGAN 2 Discriminator
![Discriminator](style_gan2_disc.svg)
Discriminator first transforms the image to a feature map of the same resolution and then
runs it through a series of blocks with residual connections.
The resolution is down-sampled by $2 \times$ at each block while doubling the
number of features.
"""
def __init__(self, log_resolution: int, n_features: int = 64, max_features: int = 512):
"""
* `log_resolution` is the $\log_2$ of image resolution
* `n_features` number of features in the convolution layer at the highest resolution (first block)
* `max_features` maximum number of features in any generator block
"""
super().__init__()
# Layer to convert RGB image to a feature map with `n_features` number of features.
self.from_rgb = nn.Sequential(
EqualizedConv2d(3, n_features, 1),
nn.LeakyReLU(0.2, True),
)
# Calculate the number of features for each block.
#
# Something like `[64, 128, 256, 512, 512, 512]`.
features = [min(max_features, n_features * (2 ** i)) for i in range(log_resolution - 1)]
# Number of [discirminator blocks](#discriminator_block)
n_blocks = len(features) - 1
# Discriminator blocks
blocks = [DiscriminatorBlock(features[i], features[i + 1]) for i in range(n_blocks)]
self.blocks = nn.Sequential(*blocks)
# [Mini-batch Standard Deviation](#mini_batch_std_dev)
self.std_dev = MiniBatchStdDev()
# Number of features after adding the standard deviations map
final_features = features[-1] + 1
# Final $3 \times 3$ convolution layer
self.conv = EqualizedConv2d(final_features, final_features, 3)
# Final linear layer to get the classification
self.final = EqualizedLinear(2 * 2 * final_features, 1)
def forward(self, x: torch.Tensor):
"""
* `x` is the input image of shape `[batch_size, 3, height, width]`
"""
# Try to normalize the image (this is totally optional, but sped up the early training a little)
x = x - 0.5
# Convert from RGB
x = self.from_rgb(x)
# Run through the [discriminator blocks](#discriminator_block)
x = self.blocks(x)
# Calculate and append [mini-batch standard deviation](#mini_batch_std_dev)
x = self.std_dev(x)
# $3 \times 3$ convolution
x = self.conv(x)
# Flatten
x = x.reshape(x.shape[0], -1)
# Return the classification score
return self.final(x)
class DiscriminatorBlock(nn.Module):
"""
<a id="discriminator_black"></a>
### Discriminator Block
![Discriminator block](discriminator_block.svg)
Discriminator block consists of two $3 \times 3$ convolutions with a residual connection.
"""
def __init__(self, in_features, out_features):
"""
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
"""
super().__init__()
# Down-sampling and $1 \times 1$ convolution layer for the residual connection
self.residual = nn.Sequential(DownSample(),
EqualizedConv2d(in_features, out_features, kernel_size=1))
# Two $3 \times 3$ convolutions
self.block = nn.Sequential(
EqualizedConv2d(in_features, in_features, kernel_size=3, padding=1),
nn.LeakyReLU(0.2, True),
EqualizedConv2d(in_features, out_features, kernel_size=3, padding=1),
nn.LeakyReLU(0.2, True),
)
# Down-sampling layer
self.down_sample = DownSample()
# Scaling factor $\frac{1}{\sqrt 2}$ after adding the residual
self.scale = 1 / math.sqrt(2)
def forward(self, x):
# Get the residual connection
residual = self.residual(x)
# Convolutions
x = self.block(x)
# Down-sample
x = self.down_sample(x)
# Add the residual and scale
return (x + residual) * self.scale
class MiniBatchStdDev(nn.Module):
"""
<a id="mini_batch_std_dev"></a>
### Mini-batch Standard Deviation
Mini-batch standard deviation calculates the standard deviation
across a mini-batch (or a subgroups within the mini-batch)
for each feature in the feature map. Then it takes the mean of all
the standard deviations and appends it to the feature map as one extra feature.
"""
def __init__(self, group_size: int = 4):
"""
* `group_size` is the number of samples to calculate standard deviation across.
"""
super().__init__()
self.group_size = group_size
def forward(self, x: torch.Tensor):
"""
* `x` is the feature map
"""
# Check if the batch size is divisible by the group size
assert x.shape[0] % self.group_size == 0
# Split the samples into groups of `group_size`, we flatten the feature map to a single dimension
# since we want to calculate the standard deviation for each feature.
grouped = x.view(self.group_size, -1)
# Calculate the standard deviation for each feature among `group_size` samples
#
# \begin{align}
# \mu_{i} &= \frac{1}{N} \sum_g x_{g,i} \\
# \sigma_{i} &= \sqrt{\frac{1}{N} \sum_g (x_{g,i} - \mu_i)^2 + \epsilon}
# \end{align}
std = torch.sqrt(grouped.var(dim=0) + 1e-8)
# Get the mean standard deviation
std = std.mean().view(1, 1, 1, 1)
# Expand the standard deviation to append to the feature map
b, _, h, w = x.shape
std = std.expand(b, -1, h, w)
# Append (concatenate) the standard deviations to the feature map
return torch.cat([x, std], dim=1)
class DownSample(nn.Module):
"""
<a id="down_sample"></a>
### Down-sample
The down-sample operation [smoothens](#smooth) each feature channel and
scale $2 \times$ using bilinear interpolation.
This is based on the paper
[Making Convolutional Networks Shift-Invariant Again](https://arxiv.org/abs/1904.11486).
"""
def __init__(self):
super().__init__()
# Smoothing layer
self.smooth = Smooth()
def forward(self, x: torch.Tensor):
# Smoothing or blurring
x = self.smooth(x)
# Scaled down
return F.interpolate(x, (x.shape[2] // 2, x.shape[3] // 2), mode='bilinear', align_corners=False)
class UpSample(nn.Module):
"""
<a id="up_sample"></a>
### Up-sample
The up-sample operation scales the image up by $2 \times$ and [smoothens](#smooth) each feature channel.
This is based on the paper
[Making Convolutional Networks Shift-Invariant Again](https://arxiv.org/abs/1904.11486).
"""
def __init__(self):
super().__init__()
# Up-sampling layer
self.up_sample = nn.Upsample(scale_factor=2, mode='bilinear', align_corners=False)
# Smoothing layer
self.smooth = Smooth()
def forward(self, x: torch.Tensor):
# Up-sample and smoothen
return self.smooth(self.up_sample(x))
class Smooth(nn.Module):
"""
<a id="smooth"></a>
### Smoothing Layer
This layer blurs each channel
"""
def __init__(self):
super().__init__()
# Blurring kernel
kernel = [[1, 2, 1],
[2, 4, 2],
[1, 2, 1]]
# Convert the kernel to a PyTorch tensor
kernel = torch.tensor([[kernel]], dtype=torch.float)
# Normalize the kernel
kernel /= kernel.sum()
# Save kernel as a fixed parameter (no gradient updates)
self.kernel = nn.Parameter(kernel, requires_grad=False)
# Padding layer
self.pad = nn.ReplicationPad2d(1)
def forward(self, x: torch.Tensor):
# Get shape of the input feature map
b, c, h, w = x.shape
# Reshape for smoothening
x = x.view(-1, 1, h, w)
# Add padding
x = self.pad(x)
# Smoothen (blur) with the kernel
x = F.conv2d(x, self.kernel)
# Reshape and return
return x.view(b, c, h, w)
class EqualizedLinear(nn.Module):
"""
<a id="equalized_linear"></a>
## Learning-rate Equalized Linear Layer
This uses [learning-rate equalized weights](#equalized_weights) for a linear layer.
"""
def __init__(self, in_features: int, out_features: int, bias: float = 0.):
"""
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
* `bias` is the bias initialization constant
"""
super().__init__()
# [Learning-rate equalized weights](#equalized_weights)
self.weight = EqualizedWeight([out_features, in_features])
# Bias
self.bias = nn.Parameter(torch.ones(out_features) * bias)
def forward(self, x: torch.Tensor):
# Linear transformation
return F.linear(x, self.weight(), bias=self.bias)
class EqualizedConv2d(nn.Module):
"""
<a id="equalized_conv2d"></a>
## Learning-rate Equalized 2D Convolution Layer
This uses [learning-rate equalized weights](#equalized_weights) for a convolution layer.
"""
def __init__(self, in_features: int, out_features: int,
kernel_size: int, padding: int = 0):
"""
* `in_features` is the number of features in the input feature map
* `out_features` is the number of features in the output feature map
* `kernel_size` is the size of the convolution kernel
* `padding` is the padding to be added on both sides of each size dimension
"""
super().__init__()
# Padding size
self.padding = padding
# [Learning-rate equalized weights](#equalized_weights)
self.weight = EqualizedWeight([out_features, in_features, kernel_size, kernel_size])
# Bias
self.bias = nn.Parameter(torch.ones(out_features))
def forward(self, x: torch.Tensor):
# Convolution
return F.conv2d(x, self.weight(), bias=self.bias, padding=self.padding)
class EqualizedWeight(nn.Module):
"""
<a id="equalized_weight"></a>
## Learning-rate Equalized Weights Parameter
This is based on equalized learning rate introduced in the Progressive GAN paper.
Instead of initializing weights at $\mathcal{N}(0,c)$ they initialize weights
to $\mathcal{N}(0, 1)$ and then multiply them by $c$ when using it.
$$w_i = c \hat{w}_i$$
The gradients on stored parameters $\hat{w}$ get multiplied by $c$ but this doesn't have
an affect since optimizers such as Adam normalize them by a running mean of the squared gradients.
The optimizer updates on $\hat{w}$ are proportionate to the learning rate $\lambda$.
But the effective weights $w$ get updated proportionately to $c \lambda$.
Without equalized learning rate, the effective weights will get updated proportionately to just $\lambda$.
So we are effectively scaling the learning rate by $c$ for these weight parameters.
"""
def __init__(self, shape: List[int]):
"""
* `shape` is the shape of the weight parameter
"""
super().__init__()
# He initialization constant
self.c = 1 / math.sqrt(np.prod(shape[1:]))
# Initialize the weights with $\mathcal{N}(0, 1)$
self.weight = nn.Parameter(torch.randn(shape))
# Weight multiplication coefficient
def forward(self):
# Multiply the weights by $c$ and return
return self.weight * self.c
class GradientPenalty(nn.Module):
"""
<a id="gradient_penalty"></a>
## Gradient Penalty
This is the $R_1$ regularization penality from the paper
[Which Training Methods for GANs do actually Converge?](https://arxiv.org/abs/1801.04406).
$$R_1(\psi) = \frac{\gamma}{2} \mathbb{E}_{p_\mathcal{D}(x)}
\Big[\Vert \nabla_x D_\psi(x)^2 \Vert\Big]$$
That is we try to reduce the L2 norm of gradients of the discriminator with
respect to images, for real images ($P_\mathcal{D}$).
"""
def forward(self, x: torch.Tensor, d: torch.Tensor):
"""
* `x` is $x \sim \mathcal{D}$
* `d` is $D(x)$
"""
# Get batch size
batch_size = x.shape[0]
# Calculate gradients of $D(x)$ with respect to $x$.
# `grad_outputs` is set to $1$ since we want the gradients of $D(x)$,
# and we need to create and retain graph since we have to compute gradients
# with respect to weight on this loss.
gradients, *_ = torch.autograd.grad(outputs=d,
inputs=x,
grad_outputs=d.new_ones(d.shape),
create_graph=True)
# Reshape gradients to calculate the norm
gradients = gradients.reshape(batch_size, -1)
# Calculate the norm $\Vert \nabla_{x} D(x)^2 \Vert$
norm = gradients.norm(2, dim=-1)
# Return the loss $\Vert \nabla_x D_\psi(x)^2 \Vert$
return torch.mean(norm ** 2)
class PathLengthPenalty(nn.Module):
"""
<a id="path_length_penalty"></a>
## Path Length Penalty
This regularization encourages a fixed-size step in $w$ to result in a fixed-magnitude
change in the image.
$$\mathbb{E}_{w \sim f(z), y \sim \mathcal{N}(0, \mathbf{I})}
\Big(\Vert \mathbf{J}^\top_{w} y \Vert_2 - a \Big)^2$$
where $\mathbf{J}_w$ is the Jacobian
$\mathbf{J}_w = \frac{\partial g}{\partial w}$,
$w$ are sampled from $w \in \mathcal{W}$ from the mapping network, and
$y$ are images with noise $\mathcal{N}(0, \mathbf{I})$.
$a$ is the exponential moving average of $\Vert \mathbf{J}^\top_{w} y \Vert_2$
as the training progresses.
$\mathbf{J}^\top_{w} y$ is calculated without explicitly calculating the Jacobian using
$$\mathbf{J}^\top_{w} y = \nabla_w \big(g(w) \cdot y \big)$$
"""
def __init__(self, beta: float):
"""
* `beta` is the constant $\beta$ used to calculate the exponential moving average $a$
"""
super().__init__()
# $\beta$
self.beta = beta
# Number of steps calculated $N$
self.steps = nn.Parameter(torch.tensor(0.), requires_grad=False)
# Exponential sum of $\mathbf{J}^\top_{w} y$
# $$\sum^N_{i=1} \beta^{(N - i)}[\mathbf{J}^\top_{w} y]_i$$
# where $[\mathbf{J}^\top_{w} y]_i$ is the value of it at $i$-th step of training
self.exp_sum_a = nn.Parameter(torch.tensor(0.), requires_grad=False)
def forward(self, w: torch.Tensor, x: torch.Tensor):
"""
* `w` is the batch of $w$ of shape `[batch_size, d_latent]`
* `x` are the generated images of shape `[batch_size, 3, height, width]`
"""
# Get the device
device = x.device
# Get number of pixels
image_size = x.shape[2] * x.shape[3]
# Calculate $y \in \mathcal{N}(0, \mathbf{I})$
y = torch.randn(x.shape, device=device)
# Calculate $\big(g(w) \cdot y \big)$ and normalize by the square root of image size.
# This is scaling is not mentioned in the paper but was present in
# [their implementation](https://github.com/NVlabs/stylegan2/blob/master/training/loss.py#L167).
output = (x * y).sum() / math.sqrt(image_size)
# Calculate gradients to get $\mathbf{J}^\top_{w} y$
gradients, *_ = torch.autograd.grad(outputs=output,
inputs=w,
grad_outputs=torch.ones(output.shape, device=device),
create_graph=True)
# Calculate L2-norm of $\mathbf{J}^\top_{w} y$
norm = (gradients ** 2).sum(dim=2).mean(dim=1).sqrt()
# Regularize after first step
if self.steps > 0:
# Calculate $a$
# $$\frac{1}{1 - \beta^N} \sum^N_{i=1} \beta^{(N - i)}[\mathbf{J}^\top_{w} y]_i$$
a = self.exp_sum_a / (1 - self.beta ** self.steps)
# Calculate the penalty
# $$\mathbb{E}_{w \sim f(z), y \sim \mathcal{N}(0, \mathbf{I})}
# \Big(\Vert \mathbf{J}^\top_{w} y \Vert_2 - a \Big)^2$$
loss = torch.mean((norm - a) ** 2)
else:
# Return a dummy loss if we can't calculate $a$
loss = norm.new_tensor(0)
# Calculate the mean of $\Vert \mathbf{J}^\top_{w} y \Vert_2$
mean = norm.mean().detach()
# Update exponential sum
self.exp_sum_a.mul_(self.beta).add_(mean, alpha=1 - self.beta)
# Increment $N$
self.steps.add_(1.)
# Return the penalty
return loss
+459
View File
@@ -0,0 +1,459 @@
"""
---
title: StyleGAN 2 Model Training
summary: >
An annotated PyTorch implementation of StyleGAN2 model training code.
---
# [StyleGAN 2](index.html) Model Training
This is the training code for [StyleGAN 2](index.html) model.
![Generated Images](generated_64.png)
---*These are $64 \times 64$ images generated after training for about 80K steps.*---
*Our implementation is a minimalistic StyleGAN 2 model training code.
Only single GPU training is supported to keep the implementation simple.
We managed to shrink it to keep it at less than 500 lines of code, including the training loop.*
*Without DDP (distributed data parallel) and multi-gpu training it will not be possible to train the model
for large resolutions (128+).
If you want training code with fp16 and DDP take a look at
[lucidrains/stylegan2-pytorch](https://github.com/lucidrains/stylegan2-pytorch).*
We trained this on [CelebA-HQ dataset](https://github.com/tkarras/progressive_growing_of_gans).
You can find the download instruction in this
[discussion on fast.ai](https://forums.fast.ai/t/download-celeba-hq-dataset/45873/3).
Save the images inside [`data/stylegan` folder](#dataset_path).
"""
import math
from pathlib import Path
from typing import Iterator, Tuple
import torchvision
from PIL import Image
import torch
import torch.utils.data
from labml import tracker, lab, monit, experiment
from labml.configs import BaseConfigs
from labml_nn.gan.stylegan import Discriminator, Generator, MappingNetwork, GradientPenalty, PathLengthPenalty
from labml_nn.gan.wasserstein import DiscriminatorLoss, GeneratorLoss
from labml_nn.helpers.device import DeviceConfigs
from labml_nn.helpers.trainer import ModeState
from labml_nn.utils import cycle_dataloader
class Dataset(torch.utils.data.Dataset):
"""
## Dataset
This loads the training dataset and resize it to the give image size.
"""
def __init__(self, path: str, image_size: int):
"""
* `path` path to the folder containing the images
* `image_size` size of the image
"""
super().__init__()
# Get the paths of all `jpg` files
self.paths = [p for p in Path(path).glob(f'**/*.jpg')]
# Transformation
self.transform = torchvision.transforms.Compose([
# Resize the image
torchvision.transforms.Resize(image_size),
# Convert to PyTorch tensor
torchvision.transforms.ToTensor(),
])
def __len__(self):
"""Number of images"""
return len(self.paths)
def __getitem__(self, index):
"""Get the the `index`-th image"""
path = self.paths[index]
img = Image.open(path)
return self.transform(img)
class Configs(BaseConfigs):
"""
## Configurations
"""
# Device to train the model on.
# [`DeviceConfigs`](../../helpers/device.html)
# picks up an available CUDA device or defaults to CPU.
device: torch.device = DeviceConfigs()
# [StyleGAN2 Discriminator](index.html#discriminator)
discriminator: Discriminator
# [StyleGAN2 Generator](index.html#generator)
generator: Generator
# [Mapping network](index.html#mapping_network)
mapping_network: MappingNetwork
# Discriminator and generator loss functions.
# We use [Wasserstein loss](../wasserstein/index.html)
discriminator_loss: DiscriminatorLoss
generator_loss: GeneratorLoss
# Optimizers
generator_optimizer: torch.optim.Adam
discriminator_optimizer: torch.optim.Adam
mapping_network_optimizer: torch.optim.Adam
# [Gradient Penalty Regularization Loss](index.html#gradient_penalty)
gradient_penalty = GradientPenalty()
# Gradient penalty coefficient $\gamma$
gradient_penalty_coefficient: float = 10.
# [Path length penalty](index.html#path_length_penalty)
path_length_penalty: PathLengthPenalty
# Data loader
loader: Iterator
# Batch size
batch_size: int = 32
# Dimensionality of $z$ and $w$
d_latent: int = 512
# Height/width of the image
image_size: int = 32
# Number of layers in the mapping network
mapping_network_layers: int = 8
# Generator & Discriminator learning rate
learning_rate: float = 1e-3
# Mapping network learning rate ($100 \times$ lower than the others)
mapping_network_learning_rate: float = 1e-5
# Number of steps to accumulate gradients on. Use this to increase the effective batch size.
gradient_accumulate_steps: int = 1
# $\beta_1$ and $\beta_2$ for Adam optimizer
adam_betas: Tuple[float, float] = (0.0, 0.99)
# Probability of mixing styles
style_mixing_prob: float = 0.9
# Total number of training steps
training_steps: int = 150_000
# Number of blocks in the generator (calculated based on image resolution)
n_gen_blocks: int
# ### Lazy regularization
# Instead of calculating the regularization losses, the paper proposes lazy regularization
# where the regularization terms are calculated once in a while.
# This improves the training efficiency a lot.
# The interval at which to compute gradient penalty
lazy_gradient_penalty_interval: int = 4
# Path length penalty calculation interval
lazy_path_penalty_interval: int = 32
# Skip calculating path length penalty during the initial phase of training
lazy_path_penalty_after: int = 5_000
# How often to log generated images
log_generated_interval: int = 500
# How often to save model checkpoints
save_checkpoint_interval: int = 2_000
# Training mode state for logging activations
mode: ModeState
# <a id="dataset_path"></a>
# We trained this on [CelebA-HQ dataset](https://github.com/tkarras/progressive_growing_of_gans).
# You can find the download instruction in this
# [discussion on fast.ai](https://forums.fast.ai/t/download-celeba-hq-dataset/45873/3).
# Save the images inside `data/stylegan` folder.
dataset_path: str = str(lab.get_data_path() / 'stylegan2')
def init(self):
"""
### Initialize
"""
# Create dataset
dataset = Dataset(self.dataset_path, self.image_size)
# Create data loader
dataloader = torch.utils.data.DataLoader(dataset, batch_size=self.batch_size, num_workers=8,
shuffle=True, drop_last=True, pin_memory=True)
# Continuous [cyclic loader](../../utils.html#cycle_dataloader)
self.loader = cycle_dataloader(dataloader)
# $\log_2$ of image resolution
log_resolution = int(math.log2(self.image_size))
# Create discriminator and generator
self.discriminator = Discriminator(log_resolution).to(self.device)
self.generator = Generator(log_resolution, self.d_latent).to(self.device)
# Get number of generator blocks for creating style and noise inputs
self.n_gen_blocks = self.generator.n_blocks
# Create mapping network
self.mapping_network = MappingNetwork(self.d_latent, self.mapping_network_layers).to(self.device)
# Create path length penalty loss
self.path_length_penalty = PathLengthPenalty(0.99).to(self.device)
# Discriminator and generator losses
self.discriminator_loss = DiscriminatorLoss().to(self.device)
self.generator_loss = GeneratorLoss().to(self.device)
# Create optimizers
self.discriminator_optimizer = torch.optim.Adam(
self.discriminator.parameters(),
lr=self.learning_rate, betas=self.adam_betas
)
self.generator_optimizer = torch.optim.Adam(
self.generator.parameters(),
lr=self.learning_rate, betas=self.adam_betas
)
self.mapping_network_optimizer = torch.optim.Adam(
self.mapping_network.parameters(),
lr=self.mapping_network_learning_rate, betas=self.adam_betas
)
# Set tracker configurations
tracker.set_image("generated", True)
def get_w(self, batch_size: int):
"""
### Sample $w$
This samples $z$ randomly and get $w$ from the mapping network.
We also apply style mixing sometimes where we generate two latent variables
$z_1$ and $z_2$ and get corresponding $w_1$ and $w_2$.
Then we randomly sample a cross-over point and apply $w_1$ to
the generator blocks before the cross-over point and
$w_2$ to the blocks after.
"""
# Mix styles
if torch.rand(()).item() < self.style_mixing_prob:
# Random cross-over point
cross_over_point = int(torch.rand(()).item() * self.n_gen_blocks)
# Sample $z_1$ and $z_2$
z2 = torch.randn(batch_size, self.d_latent).to(self.device)
z1 = torch.randn(batch_size, self.d_latent).to(self.device)
# Get $w_1$ and $w_2$
w1 = self.mapping_network(z1)
w2 = self.mapping_network(z2)
# Expand $w_1$ and $w_2$ for the generator blocks and concatenate
w1 = w1[None, :, :].expand(cross_over_point, -1, -1)
w2 = w2[None, :, :].expand(self.n_gen_blocks - cross_over_point, -1, -1)
return torch.cat((w1, w2), dim=0)
# Without mixing
else:
# Sample $z$ and $z$
z = torch.randn(batch_size, self.d_latent).to(self.device)
# Get $w$ and $w$
w = self.mapping_network(z)
# Expand $w$ for the generator blocks
return w[None, :, :].expand(self.n_gen_blocks, -1, -1)
def get_noise(self, batch_size: int):
"""
### Generate noise
This generates noise for each [generator block](index.html#generator_block)
"""
# List to store noise
noise = []
# Noise resolution starts from $4$
resolution = 4
# Generate noise for each generator block
for i in range(self.n_gen_blocks):
# The first block has only one $3 \times 3$ convolution
if i == 0:
n1 = None
# Generate noise to add after the first convolution layer
else:
n1 = torch.randn(batch_size, 1, resolution, resolution, device=self.device)
# Generate noise to add after the second convolution layer
n2 = torch.randn(batch_size, 1, resolution, resolution, device=self.device)
# Add noise tensors to the list
noise.append((n1, n2))
# Next block has $2 \times$ resolution
resolution *= 2
# Return noise tensors
return noise
def generate_images(self, batch_size: int):
"""
### Generate images
This generate images using the generator
"""
# Get $w$
w = self.get_w(batch_size)
# Get noise
noise = self.get_noise(batch_size)
# Generate images
images = self.generator(w, noise)
# Return images and $w$
return images, w
def step(self, idx: int):
"""
### Training Step
"""
# Train the discriminator
with monit.section('Discriminator'):
# Reset gradients
self.discriminator_optimizer.zero_grad()
# Accumulate gradients for `gradient_accumulate_steps`
for i in range(self.gradient_accumulate_steps):
# Sample images from generator
generated_images, _ = self.generate_images(self.batch_size)
# Discriminator classification for generated images
fake_output = self.discriminator(generated_images.detach())
# Get real images from the data loader
real_images = next(self.loader).to(self.device)
# We need to calculate gradients w.r.t. real images for gradient penalty
if (idx + 1) % self.lazy_gradient_penalty_interval == 0:
real_images.requires_grad_()
# Discriminator classification for real images
real_output = self.discriminator(real_images)
# Get discriminator loss
real_loss, fake_loss = self.discriminator_loss(real_output, fake_output)
disc_loss = real_loss + fake_loss
# Add gradient penalty
if (idx + 1) % self.lazy_gradient_penalty_interval == 0:
# Calculate and log gradient penalty
gp = self.gradient_penalty(real_images, real_output)
tracker.add('loss.gp', gp)
# Multiply by coefficient and add gradient penalty
disc_loss = disc_loss + 0.5 * self.gradient_penalty_coefficient * gp * self.lazy_gradient_penalty_interval
# Compute gradients
disc_loss.backward()
# Log discriminator loss
tracker.add('loss.discriminator', disc_loss)
if (idx + 1) % self.log_generated_interval == 0:
# Log discriminator model parameters occasionally
tracker.add('discriminator', self.discriminator)
# Clip gradients for stabilization
torch.nn.utils.clip_grad_norm_(self.discriminator.parameters(), max_norm=1.0)
# Take optimizer step
self.discriminator_optimizer.step()
# Train the generator
with monit.section('Generator'):
# Reset gradients
self.generator_optimizer.zero_grad()
self.mapping_network_optimizer.zero_grad()
# Accumulate gradients for `gradient_accumulate_steps`
for i in range(self.gradient_accumulate_steps):
# Sample images from generator
generated_images, w = self.generate_images(self.batch_size)
# Discriminator classification for generated images
fake_output = self.discriminator(generated_images)
# Get generator loss
gen_loss = self.generator_loss(fake_output)
# Add path length penalty
if idx > self.lazy_path_penalty_after and (idx + 1) % self.lazy_path_penalty_interval == 0:
# Calculate path length penalty
plp = self.path_length_penalty(w, generated_images)
# Ignore if `nan`
if not torch.isnan(plp):
tracker.add('loss.plp', plp)
gen_loss = gen_loss + plp
# Calculate gradients
gen_loss.backward()
# Log generator loss
tracker.add('loss.generator', gen_loss)
if (idx + 1) % self.log_generated_interval == 0:
# Log discriminator model parameters occasionally
tracker.add('generator', self.generator)
tracker.add('mapping_network', self.mapping_network)
# Clip gradients for stabilization
torch.nn.utils.clip_grad_norm_(self.generator.parameters(), max_norm=1.0)
torch.nn.utils.clip_grad_norm_(self.mapping_network.parameters(), max_norm=1.0)
# Take optimizer step
self.generator_optimizer.step()
self.mapping_network_optimizer.step()
# Log generated images
if (idx + 1) % self.log_generated_interval == 0:
tracker.add('generated', torch.cat([generated_images[:6], real_images[:3]], dim=0))
# Save model checkpoints
if (idx + 1) % self.save_checkpoint_interval == 0:
# Save checkpoint
pass
# Flush tracker
tracker.save()
def train(self):
"""
## Train model
"""
# Loop for `training_steps`
for i in monit.loop(self.training_steps):
# Take a training step
self.step(i)
#
if (i + 1) % self.log_generated_interval == 0:
tracker.new_line()
def main():
"""
### Train StyleGAN2
"""
# Create an experiment
experiment.create(name='stylegan2')
# Create configurations object
configs = Configs()
# Set configurations and override some
experiment.configs(configs, {
'device.cuda_device': 0,
'image_size': 64,
'log_generated_interval': 200
})
# Initialize
configs.init()
# Set models for saving and loading
experiment.add_pytorch_models(mapping_network=configs.mapping_network,
generator=configs.generator,
discriminator=configs.discriminator)
# Start the experiment
with experiment.start():
# Run the training loop
configs.train()
#
if __name__ == '__main__':
main()
+10
View File
@@ -0,0 +1,10 @@
# [StyleGAN 2](https://nn.labml.ai/gan/stylegan/index.html)
This is a [PyTorch](https://pytorch.org) implementation of the paper
[Analyzing and Improving the Image Quality of StyleGAN](https://arxiv.org/abs/1912.04958)
which introduces **StyleGAN2**.
StyleGAN 2 is an improvement over **StyleGAN** from the paper
[A Style-Based Generator Architecture for Generative Adversarial Networks](https://arxiv.org/abs/1812.04948).
And StyleGAN is based on **Progressive GAN** from the paper
[Progressive Growing of GANs for Improved Quality, Stability, and Variation](https://arxiv.org/abs/1710.10196).
All three papers are from the same authors from [NVIDIA AI](https://twitter.com/NVIDIAAI).
+133
View File
@@ -0,0 +1,133 @@
r"""
---
title: Wasserstein GAN (WGAN)
summary: A simple PyTorch implementation/tutorial of Wasserstein Generative Adversarial Networks (WGAN) loss functions.
---
# Wasserstein GAN (WGAN)
This is an implementation of
[Wasserstein GAN](https://arxiv.org/abs/1701.07875).
The original GAN loss is based on Jensen-Shannon (JS) divergence
between the real distribution $\mathbb{P}_r$ and generated distribution $\mathbb{P}_g$.
The Wasserstein GAN is based on Earth Mover distance between these distributions.
$$
W(\mathbb{P}_r, \mathbb{P}_g) =
\underset{\gamma \in \Pi(\mathbb{P}_r, \mathbb{P}_g)} {\mathrm{inf}}
\mathbb{E}_{(x,y) \sim \gamma}
\Vert x - y \Vert
$$
$\Pi(\mathbb{P}_r, \mathbb{P}_g)$ is the set of all joint distributions, whose
marginal probabilities are $\gamma(x, y)$.
$\mathbb{E}_{(x,y) \sim \gamma} \Vert x - y \Vert$ is the earth mover distance for
a given joint distribution ($x$ and $y$ are probabilities).
So $W(\mathbb{P}_r, \mathbb{P}_g)$ is equal to the least earth mover distance for
any joint distribution between the real distribution $\mathbb{P}_r$ and generated distribution $\mathbb{P}_g$.
The paper shows that Jensen-Shannon (JS) divergence and other measures for the difference between two probability
distributions are not smooth. And therefore if we are doing gradient descent on one of the probability
distributions (parameterized) it will not converge.
Based on Kantorovich-Rubinstein duality,
$$
W(\mathbb{P}_r, \mathbb{P}_g) =
\underset{\Vert f \Vert_L \le 1} {\mathrm{sup}}
\mathbb{E}_{x \sim \mathbb{P}_r} [f(x)]- \mathbb{E}_{x \sim \mathbb{P}_g} [f(x)]
$$
where $\Vert f \Vert_L \le 1$ are all 1-Lipschitz functions.
That is, it is equal to the greatest difference
$$\mathbb{E}_{x \sim \mathbb{P}_r} [f(x)] - \mathbb{E}_{x \sim \mathbb{P}_g} [f(x)]$$
among all 1-Lipschitz functions.
For $K$-Lipschitz functions,
$$
W(\mathbb{P}_r, \mathbb{P}_g) =
\underset{\Vert f \Vert_L \le K} {\mathrm{sup}}
\mathbb{E}_{x \sim \mathbb{P}_r} \Bigg[\frac{1}{K} f(x) \Bigg]
- \mathbb{E}_{x \sim \mathbb{P}_g} \Bigg[\frac{1}{K} f(x) \Bigg]
$$
If all $K$-Lipschitz functions can be represented as $f_w$ where $f$ is parameterized by
$w \in \mathcal{W}$,
$$
K \cdot W(\mathbb{P}_r, \mathbb{P}_g) =
\max_{w \in \mathcal{W}}
\mathbb{E}_{x \sim \mathbb{P}_r} [f_w(x)]- \mathbb{E}_{x \sim \mathbb{P}_g} [f_w(x)]
$$
If $(\mathbb{P}_{g})$ is represented by a generator $$g_\theta (z)$$ and $z$ is from a known
distribution $z \sim p(z)$,
$$
K \cdot W(\mathbb{P}_r, \mathbb{P}_\theta) =
\max_{w \in \mathcal{W}}
\mathbb{E}_{x \sim \mathbb{P}_r} [f_w(x)]- \mathbb{E}_{z \sim p(z)} [f_w(g_\theta(z))]
$$
Now to converge $g_\theta$ with $\mathbb{P}_{r}$ we can gradient descent on $\theta$
to minimize above formula.
Similarly we can find $\max_{w \in \mathcal{W}}$ by ascending on $w$,
while keeping $K$ bounded. *One way to keep $K$ bounded is to clip all weights in the neural
network that defines $f$ clipped within a range.*
Here is the code to try this on a [simple MNIST generation experiment](experiment.html).
[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/wasserstein/experiment.ipynb)
"""
import torch.utils.data
from torch import nn
from torch.nn import functional as F
class DiscriminatorLoss(nn.Module):
"""
## Discriminator Loss
We want to find $w$ to maximize
$$\mathbb{E}_{x \sim \mathbb{P}_r} [f_w(x)]- \mathbb{E}_{z \sim p(z)} [f_w(g_\theta(z))]$$,
so we minimize,
$$-\frac{1}{m} \sum_{i=1}^m f_w \big(x^{(i)} \big) +
\frac{1}{m} \sum_{i=1}^m f_w \big( g_\theta(z^{(i)}) \big)$$
"""
def forward(self, f_real: torch.Tensor, f_fake: torch.Tensor):
"""
* `f_real` is $f_w(x)$
* `f_fake` is $f_w(g_\theta(z))$
This returns the a tuple with losses for $f_w(x)$ and $f_w(g_\theta(z))$,
which are later added.
They are kept separate for logging.
"""
# We use ReLUs to clip the loss to keep $f \in [-1, +1]$ range.
return F.relu(1 - f_real).mean(), F.relu(1 + f_fake).mean()
class GeneratorLoss(nn.Module):
"""
## Generator Loss
We want to find $\theta$ to minimize
$$\mathbb{E}_{x \sim \mathbb{P}_r} [f_w(x)]- \mathbb{E}_{z \sim p(z)} [f_w(g_\theta(z))]$$
The first component is independent of $\theta$,
so we minimize,
$$-\frac{1}{m} \sum_{i=1}^m f_w \big( g_\theta(z^{(i)}) \big)$$
"""
def forward(self, f_fake: torch.Tensor):
"""
* `f_fake` is $f_w(g_\theta(z))$
"""
return -f_fake.mean()
+189
View File
@@ -0,0 +1,189 @@
{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"name": "Cycle GAN",
"provenance": [],
"collapsed_sections": [],
"toc_visible": true
},
"kernelspec": {
"name": "python3",
"language": "python",
"display_name": "Python 3"
},
"accelerator": "GPU"
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "AYV_dMVDxyc2"
},
"source": [
"[![Github](https://img.shields.io/github/stars/labmlai/annotated_deep_learning_paper_implementations?style=social)](https://github.com/labmlai/annotated_deep_learning_paper_implementations)\n",
"[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/labmlai/annotated_deep_learning_paper_implementations/blob/master/labml_nn/gan/wasserstein/experiment.ipynb)\n",
"\n",
"## DCGAN\n",
"\n",
"This is an experiment training DCGAN model."
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "AahG_i2y5tY9"
},
"source": [
"Install the `labml-nn` package"
]
},
{
"cell_type": "code",
"metadata": {
"id": "ZCzmCrAIVg0L",
"colab": {
"base_uri": "https://localhost:8080/"
},
"outputId": "2fe2685f-731c-4c47-854e-a4f00e485281"
},
"source": [
"!pip install labml-nn"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "SE2VUQ6L5zxI"
},
"source": [
"Imports"
]
},
{
"cell_type": "code",
"metadata": {
"id": "0hJXx_g0wS2C"
},
"source": [
"\n",
"from labml import experiment\n",
"from labml_nn.gan.wasserstein.experiment import Configs"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "Lpggo0wM6qb-"
},
"source": [
"Create an experiment"
]
},
{
"cell_type": "code",
"metadata": {
"id": "bFcr9k-l4cAg"
},
"source": [
"experiment.create(name=\"mnist_wgan\")"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "-OnHLi626tJt"
},
"source": [
"Initialize configurations"
]
},
{
"cell_type": "code",
"metadata": {
"id": "Piz0c5f44hRo"
},
"source": [
"conf = Configs()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "wwMzCqpD6vkL"
},
"source": [
"Set experiment configurations and assign a configurations dictionary to override configurations"
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 17
},
"id": "e6hmQhTw4nks",
"outputId": "4be767af-0ebd-4c35-8da0-0e532495e037"
},
"source": [
"experiment.configs(conf,\n",
" {\n",
" 'discriminator': 'cnn',\n",
" 'generator': 'cnn',\n",
" 'label_smoothing': 0.01,\n",
" 'generator_loss': 'wasserstein',\n",
" 'discriminator_loss': 'wasserstein',\n",
" })"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "markdown",
"metadata": {
"id": "KJZRf8527GxL"
},
"source": [
"Start the experiment and run the training loop."
]
},
{
"cell_type": "code",
"metadata": {
"colab": {
"base_uri": "https://localhost:8080/",
"height": 649
},
"id": "aIAWo7Fw5DR8",
"outputId": "e3b02247-8ff9-47b5-8f52-49c9e3b8377f"
},
"source": [
"with experiment.start():\n",
" conf.run()"
],
"outputs": [],
"execution_count": null
},
{
"cell_type": "code",
"metadata": {
"id": "oBXXlP2b7XZO"
},
"source": [
""
],
"outputs": [],
"execution_count": null
}
]
}
+44
View File
@@ -0,0 +1,44 @@
"""
---
title: WGAN experiment with MNIST
summary: This experiment generates MNIST images using convolutional neural network.
---
# WGAN experiment with MNIST
"""
from labml import experiment
from labml.configs import calculate
# Import configurations from [DCGAN experiment](../dcgan/index.html)
from labml_nn.gan.dcgan import Configs
# Import [Wasserstein GAN losses](./index.html)
from labml_nn.gan.wasserstein import GeneratorLoss, DiscriminatorLoss
# Set configurations options for Wasserstein GAN losses
calculate(Configs.generator_loss, 'wasserstein', lambda c: GeneratorLoss())
calculate(Configs.discriminator_loss, 'wasserstein', lambda c: DiscriminatorLoss())
def main():
# Create configs object
conf = Configs()
# Create experiment
experiment.create(name='mnist_wassertein_dcgan', comment='test')
# Override configurations
experiment.configs(conf,
{
'discriminator': 'cnn',
'generator': 'cnn',
'label_smoothing': 0.01,
'generator_loss': 'wasserstein',
'discriminator_loss': 'wasserstein',
})
# Start the experiment and run training loop
with experiment.start():
conf.run()
if __name__ == '__main__':
main()
@@ -0,0 +1,83 @@
r"""
---
title: Gradient Penalty for Wasserstein GAN (WGAN-GP)
summary: >
An annotated PyTorch implementation/tutorial of
Improved Training of Wasserstein GANs.
---
# Gradient Penalty for Wasserstein GAN (WGAN-GP)
This is an implementation of
[Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028).
[WGAN](../index.html) suggests clipping weights to enforce Lipschitz constraint
on the discriminator network (critic).
This and other weight constraints like L2 norm clipping, weight normalization,
L1, L2 weight decay have problems:
1. Limiting the capacity of the discriminator
2. Exploding and vanishing gradients (without [Batch Normalization](../../../normalization/batch_norm/index.html)).
The paper [Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028)
proposal a better way to improve Lipschitz constraint, a gradient penalty.
$$\mathcal{L}_{GP} = \lambda \underset{\hat{x} \sim \mathbb{P}_{\hat{x}}}{\mathbb{E}}
\Big[ \big(\Vert \nabla_{\hat{x}} D(\hat{x}) \Vert_2 - 1\big)^2 \Big]
$$
where $\lambda$ is the penalty weight and
\begin{align}
x &\sim \mathbb{P}_r \\
z &\sim p(z) \\
\epsilon &\sim U[0,1] \\
\tilde{x} &\leftarrow G_\theta (z) \\
\hat{x} &\leftarrow \epsilon x + (1 - \epsilon) \tilde{x}
\end{align}
That is we try to keep the gradient norm $\Vert \nabla_{\hat{x}} D(\hat{x}) \Vert_2$ close to $1$.
In this implementation we set $\epsilon = 1$.
Here is the [code for an experiment](experiment.html) that uses gradient penalty.
"""
import torch
import torch.autograd
from torch import nn
class GradientPenalty(nn.Module):
"""
## Gradient Penalty
"""
def forward(self, x: torch.Tensor, f: torch.Tensor):
"""
* `x` is $x \sim \mathbb{P}_r$
* `f` is $D(x)$
$\hat{x} \leftarrow x$
since we set $\epsilon = 1$ for this implementation.
"""
# Get batch size
batch_size = x.shape[0]
# Calculate gradients of $D(x)$ with respect to $x$.
# `grad_outputs` is set to ones since we want the gradients of $D(x)$,
# and we need to create and retain graph since we have to compute gradients
# with respect to weight on this loss.
gradients, *_ = torch.autograd.grad(outputs=f,
inputs=x,
grad_outputs=f.new_ones(f.shape),
create_graph=True)
# Reshape gradients to calculate the norm
gradients = gradients.reshape(batch_size, -1)
# Calculate the norm $\Vert \nabla_{\hat{x}} D(\hat{x}) \Vert_2$
norm = gradients.norm(2, dim=-1)
# Return the loss $\big(\Vert \nabla_{\hat{x}} D(\hat{x}) \Vert_2 - 1\big)^2$
return torch.mean((norm - 1) ** 2)
@@ -0,0 +1,86 @@
"""
---
title: WGAN-GP experiment with MNIST
summary: This experiment generates MNIST images using convolutional neural network.
---
# WGAN-GP experiment with MNIST
"""
import torch
from labml import experiment, tracker
# Import configurations from [Wasserstein experiment](../experiment.html)
from labml_nn.gan.wasserstein.experiment import Configs as OriginalConfigs
#
from labml_nn.gan.wasserstein.gradient_penalty import GradientPenalty
class Configs(OriginalConfigs):
"""
## Configuration class
We extend [original GAN implementation](../../original/experiment.html) and override the discriminator (critic) loss
calculation to include gradient penalty.
"""
# Gradient penalty coefficient $\lambda$
gradient_penalty_coefficient: float = 10.0
#
gradient_penalty = GradientPenalty()
def calc_discriminator_loss(self, data: torch.Tensor):
"""
This overrides the original discriminator loss calculation and
includes gradient penalty.
"""
# Require gradients on $x$ to calculate gradient penalty
data.requires_grad_()
# Sample $z \sim p(z)$
latent = self.sample_z(data.shape[0])
# $D(x)$
f_real = self.discriminator(data)
# $D(G_\theta(z))$
f_fake = self.discriminator(self.generator(latent).detach())
# Get discriminator losses
loss_true, loss_false = self.discriminator_loss(f_real, f_fake)
# Calculate gradient penalties in training mode
if self.mode.is_train:
gradient_penalty = self.gradient_penalty(data, f_real)
tracker.add("loss.gp.", gradient_penalty)
loss = loss_true + loss_false + self.gradient_penalty_coefficient * gradient_penalty
# Skip gradient penalty otherwise
else:
loss = loss_true + loss_false
# Log stuff
tracker.add("loss.discriminator.true.", loss_true)
tracker.add("loss.discriminator.false.", loss_false)
tracker.add("loss.discriminator.", loss)
return loss
def main():
# Create configs object
conf = Configs()
# Create experiment
experiment.create(name='mnist_wassertein_gp_dcgan')
# Override configurations
experiment.configs(conf,
{
'discriminator': 'cnn',
'generator': 'cnn',
'label_smoothing': 0.01,
'generator_loss': 'wasserstein',
'discriminator_loss': 'wasserstein',
'discriminator_k': 5,
})
# Start the experiment and run training loop
with experiment.start():
conf.run()
if __name__ == '__main__':
main()
@@ -0,0 +1,16 @@
# [Gradient Penalty for Wasserstein GAN (WGAN-GP)](https://nn.labml.ai/gan/wasserstein/gradient_penalty/index.html)
This is an implementation of
[Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028).
[WGAN](https://nn.labml.ai/gan/wasserstein/index.html) suggests
clipping weights to enforce Lipschitz constraint
on the discriminator network (critic).
This and other weight constraints like L2 norm clipping, weight normalization,
L1, L2 weight decay have problems:
1. Limiting the capacity of the discriminator
2. Exploding and vanishing gradients (without [Batch Normalization](https://nn.labml.ai/normalization/batch_norm/index.html)).
The paper [Improved Training of Wasserstein GANs](https://arxiv.org/abs/1704.00028)
proposal a better way to improve Lipschitz constraint, a gradient penalty.
+4
View File
@@ -0,0 +1,4 @@
# [Wasserstein GAN - WGAN](https://nn.labml.ai/gan/wasserstein/index.html)
This is an implementation of
[Wasserstein GAN](https://arxiv.org/abs/1701.07875).