class SimpleGan():
def __init__(self):
self.n_epochs = 20000
self.batch_size = 512
# Submodels
self._generator = Generator(self.batch_size)
self._discriminator = Discriminator(self.batch_size)
def train(self):
for self._i in range(self.n_epochs):
### Train discriminator ###
### Train generator ###
During the training of the Discriminator through the loss V(D, G), to optimize the network parameters,
the Generator's parameters are not changed. In other words are not influenced through the backwards propagation.
In the context of neural networks, this is often times called freezing and in the context of PyTorch
the tensor elements gradient are shut off. Those are accessible through the parameters() method.
Correspondingly, during the Generator training, the
Discriminator is seen as predicting perfectly. Actually it is trained beforehand and at least in the beginning
the differentiation between almost randomness and reality might be trainable. The discriminator is basically
used to estimate the reality, as the true distribution is not available.
In fact, throught the interative training it will be approximated. As the loss is calcualted through the
Discriminator and the loss is then used to backpropagate and optimize, the Discriminator has to be frozen
at that point, to adapt the Generator network to improve.
class SimpleGan():
def __init__(self):
self.n_epochs = 20000
self.batch_size = 512
# Submodels
self._generator = Generator(self.batch_size)
self._discriminator = Discriminator(self.batch_size)
def _freeze_generator(self):
# Freeze generator, unfreeze discriminator
for p in self._generator.parameters():
p.requires_grad = False
for p in self._discriminator.parameters():
p.requires_grad = True
def _freeze_discriminator(self):
# Unfreeze generator, Freeze discriminator
for p in self._generator.parameters():
p.requires_grad = True
for p in self._discriminator.parameters():
p.requires_grad = False
def train(self):
for self._i in range(self.n_epochs):
### Train discriminator ###
self._freeze_generator()
### Train generator ###
self._freeze_discriminator()
To monitor the training process, the losses of both networks after each epoch should be returned and
saved in an array. Later on, this gives the possibility to visualize. To access to scalar values
from the PyTorch tensor object, here referenced through d_loss and g_loss, the item() method has to be called.
The same accounts for the accuracy estimation during training through the self.accs property.
A batch of samples drawn from the uniform distribution U(-1, 1) is handed to the generator, which
by definition responds with a batch of synthetic samples. Finally, to train the discriminator, those samples,
together with the real ones, are used to train it. The training logic of the discriminator model is
abstracted to the D class. To train it with batch_size number of samples, for batch_size half number of
elements are selected as synthetic and reality each. With regards to the Generator training, batch_size
number of samples are created throught the generator first and then forwarded to the Discriminator
to predict its reality likeness. Those predicted values are then used to train the Generator through
self._generator.train(synt_preds). As for the Discriminator, the training method is abstracted into
the Generator class. The accuracy after every epoch is estimated through a prediction from the
Discriminator on real and synthetic samples.
class SimpleGan():
def __init__(self):
self.n_epochs = 20000
self.batch_size = 512
# Losses of D and G network during training
self.d_losses = []
self.g_losses = []
self.accs = []
# Submodels
self._generator = Generator(self.batch_size)
self._discriminator = Discriminator(self.batch_size)
def _freeze_generator(self):
# Freeze generator, unfreeze discriminator
for p in self._generator.parameters():
p.requires_grad = False
for p in self._discriminator.parameters():
p.requires_grad = True
def _freeze_discriminator(self):
# Unfreeze generator, Freeze discriminator
for p in self._generator.parameters():
p.requires_grad = True
for p in self._discriminator.parameters():
p.requires_grad = False
def train(self):
for self._i in range(self.n_epochs):
### Train discriminator ###
self._freeze_generator()
# Uniformly distributed U(-1, 1) random noise
uni_samples = torch.rand(self.batch_size//2, 1)*2-1
# Synthetic data from generator
synt_samples = self._generator(uni_samples)
real_samples = torch.randn(self.batch_size//2, 1)
d_loss = self._discriminator.train(synt_samples,
real_samples)
self.d_losses.append(d_loss.item())
### Train generator ###
self._freeze_discriminator()
# Uniformly distributed U(-1, 1) random noise
uni_samples = torch.rand(self.batch_size, 1)*2-1
# Synthetic data from generator
synt_samples = self._generator(uni_samples)
synt_preds = self._discriminator(synt_samples)
g_loss = self._generator.train(synt_preds)
self.g_losses.append(g_loss.item())
# Estimate accuracy
real_preds = self._discriminator(real_samples)
real_preds_gt = torch.ones(self.batch_size//2, 1)
synt_preds_gt = torch.zeros(self.batch_size, 1)
accs_dis = real_preds - real_preds_gt
accs_dis_mean = torch.mean(torch.abs(accs_dis))
accs_gen = synt_preds - synt_preds_gt
accs_gen_mean = torch.mean(torch.abs(accs_gen))
acc = (accs_dis_mean + accs_gen_mean)/2
self.accs.append(1-acc.item())
The Generator is trained with a single type of data - synthetic samples. Therefore, the only
input parameter for the train() method is synt_preds - the Discriminator predictions on
the synthetical samples. Before each optimization step via the actualization of the gradients
for each model parameter, those gradients have to be nulled. In PyTorch this is handeled through
the zero_grad() method. To be able to use standard BCE as criteria/loss function, the
labels for the Generator training have to be inverted. In ohter words, instead of using 0 labels
for the fake/synthetic samples, 1 labels are applied. After calculating the loss, the backward
step has to be calculated, and finally applied on the tensor parameters with a step on the
optimizer algorithm used.
class Generator(nn.Module):
""" Maps from Uniform distribution
to Gaussian distribution.
Latent space of 1D vector input
to 1D vector output.
"""
def __init__(self, batch_size: int):
self.batch_size = batch_size
super(Generator, self).__init__()
self.lr = 0.0005
self.input_size = 1
self.output_size = 1
self.fc = nn.Linear(self.input_size, 8)
self.fc2 = nn.Linear(8, 12)
self.fc4 = nn.Linear(12, self.output_size)
self.lrelu = nn.LeakyReLU(negative_slope=0.1)
self.criterion = torch.nn.BCELoss()
self.optimizer = torch.optim.Adam(self.parameters(),
lr = self.lr)
def forward(self, x: Tensor) -> Tensor:
output = self.fc(x)
output = self.lrelu(output)
output = self.fc2(output)
output = self.lrelu(output)
output = self.fc4(output)
output = self.lrelu(output)
return output
def train(self, synt_preds: Tensor) -> Tensor:
self.zero_grad()
# Use ones, as instead of maximize with 0s,
# minimize with 1s
synt_labels = torch.ones(self.batch_size, 1)
loss = self.criterion(synt_preds, synt_labels)
# Optimize generator model
loss.backward()
self.optimizer.step()
return loss
Finally for the Discriminator train() method, two input parameters are handed over:
synth_samples and real_samples. Those parameters are tensors with batch_size/2 number of samples.
Correspondingly, labels are listed with 1s for real input data and 0s for fake/synthetic input
data. To determine the current quality of the prediction through the Discriminator, those samples
have first to be forwarded through the network, before the calculation the loss through
the criteria. As BCE is used as criteria, the loss is calculated with the ground truth.
After applying backwards propagation through the Discriminator network, the calculated gradients
are used to update the parameters with the optimizer.
class Discriminator(nn.Module):
""" Decides if a sample is drawn from
Gaussian distribution.
"""
def __init__(self, batch_size: int):
self.batch_size = batch_size
super(Discriminator, self).__init__()
self.lr = 0.0005
self.input_size = 1
self.output_size = 1
self.k = 1
self.fc = nn.Linear(self.input_size, 8)
self.fc2 = nn.Linear(8, self.output_size)
self.lrelu = nn.LeakyReLU(negative_slope=0.1)
self.sigmoid = nn.Sigmoid()
self.optimizer = torch.optim.Adam(self.parameters(),
lr = self.lr)
self.criterion = torch.nn.BCELoss()
def forward(self, x: Tensor):
output = self.fc(x)
output = self.lrelu(output)
output = self.fc2(output)
output = self.sigmoid(output)
return output
def train(self, synth_samples: Tensor,
real_samples: Tensor):
# Real data for discriminator training
for j in range(self.k):
self.zero_grad()
# Labels for real data
real_gt = torch.ones(self.batch_size//2, 1)
# Synthetic labels for generator data
synt_gt = torch.zeros(self.batch_size//2, 1)
# Predictions from discriminator
real_preds = self.forward(real_samples)
synt_preds = self.forward(synth_samples)
# Loss between ground truth and predictions
real_loss = self.criterion(real_preds,
real_gt)
synt_loss = self.criterion(synt_preds,
synt_gt)
loss = real_loss + synt_loss
# Optimize discriminator model
loss.backward()
self.optimizer.step()
return loss
[1] Generative Adversarial Networks - The original paper