Generative Adversarial Network¶
ProbFlow isn’t really built for the kind of flexibility you need to fit a GAN, so I wouldn’t recommend fitting GANs with ProbFlow. That said, you can technically do it.
So… Just for fun…
TODO: description… (Bayesian GAN, cite https://arxiv.org/pdf/1705.09558.pdf)
TODO: math
TODO: diagram
TODO: talk about overriding the Model.log_likelihood
method to compute the
log likelihood due to the batch of acual data and a batch of the same size
of data generated by the generator network.
First let’s build a generator:
import probflow as pf
import tensorflow as tf
class Generator(pf.Model):
def __init__(self, dims):
self.Dz = dims[0]
self.G = pf.DenseNetwork(dims)
self.D = None
def __call__(self, x):
z = tf.random.normal([x.shape[0], self.Dz])
return self.G(z)
def log_likelihood(self, _, x):
labels = tf.ones([x.shape[0], 1])
true_ll = self.D(self(x)).log_prob(labels)
return tf.reduce_sum(true_ll)
import probflow as pf
import torch
class Generator(pf.Model):
def __init__(self, dims):
self.Dz = dims[0]
self.G = pf.DenseNetwork(dims)
self.D = None
def __call__(self, x):
x = torch.tensor(x)
z = torch.randn([x.shape[0], self.Dz])
return self.G(z)
def log_likelihood(self, _, x):
labels = torch.ones([x.shape[0], 1])
true_ll = self.D(self(x)).log_prob(labels)
return torch.sum(true_ll)
Then a discriminator:
class Discriminator(pf.Model):
def __init__(self, dims):
self.G = None
self.D = pf.DenseNetwork(dims)
def __call__(self, x):
return pf.Bernoulli(self.D(x))
def log_likelihood(self, _, x):
labels = tf.ones([x.shape[0], 1])
true_ll = self(x).log_prob(labels)
fake_ll = self(self.G(x)).log_prob(0*labels)
return tf.reduce_sum(true_ll + fake_ll)
class Discriminator(pf.Model):
def __init__(self, dims):
self.G = None
self.D = pf.DenseNetwork(dims)
def __call__(self, x):
x = torch.tensor(x)
return pf.Bernoulli(self.D(x))
def log_likelihood(self, _, x):
labels = torch.ones([x.shape[0], 1])
true_ll = self(x).log_prob(labels)
fake_ll = self(self.G(x)).log_prob(0*labels)
return torch.sum(true_ll + fake_ll)
And a callback to train the generator for an epoch at the end of each epoch of the discriminator’s training:
class TrainGenerator(pf.Callback):
def __init__(self, G, x):
self.G = G
self.x = x
def on_epoch_end(self):
self.G.fit(self.x, epochs=1)
Then, we can instantiate the networks, the callback, and fit the network:
# x is a numpy array or pandas DataFrame of real data
Nf = 7 #number of features / input dimensionality (x.shape[1])
Nz = 3 #number of latent dimensions
# Create the networks
G = Generator([Nz, 256, 128, Nf])
D = Discriminator([Nf, 256, 128, 1])
# Let them know about each other <3
G.D = lambda x: D(x)
D.G = lambda x: G(x)
# Create the callback which trains the generator
train_g = TrainGenerator(G, x)
# Fit both models by fitting the discriminator w/ the callback
D.fit(x, callbacks=[train_g])
Note that we use lambda functions instead of simply assigning the opposite net
as an attribute of the each model instance. This is because ProbFlow
recursively searches a model instance’s attributes for Module
and
Parameter
objects, and optimizes all found parameters with respect
to the loss. However, we don’t want the discriminator’s parameters updated
with the the generator’s loss, or vice versa! ProbFlow even looks for
parameters in attributes which are lists and dictionaries, but it doesn’t look
in lambda functions, so we can “hide” the parameters of one model from the
other that way, while still allowing each model to __call__
the other to
compute its own loss.