Wednesday, October 2, 2024

Posit AI Weblog: Entering into the movement: Bijectors in TensorFlow Chance

As of in the present day, deep studying’s biggest successes have taken place within the realm of supervised studying, requiring tons and many annotated coaching knowledge. Nonetheless, knowledge doesn’t (usually) include annotations or labels. Additionally, unsupervised studying is enticing due to the analogy to human cognition.

On this weblog up to now, we now have seen two main architectures for unsupervised studying: variational autoencoders and generative adversarial networks. Lesser recognized, however interesting for conceptual in addition to for efficiency causes are normalizing flows (Jimenez Rezende and Mohamed 2015). On this and the following publish, we’ll introduce flows, specializing in methods to implement them utilizing TensorFlow Chance (TFP).

In distinction to earlier posts involving TFP that accessed its performance utilizing low-level $-syntax, we now make use of tfprobability, an R wrapper within the fashion of keras, tensorflow and tfdatasets. A notice relating to this bundle: It’s nonetheless beneath heavy growth and the API might change. As of this writing, wrappers don’t but exist for all TFP modules, however all TFP performance is out there utilizing $-syntax if want be.

Density estimation and sampling

Again to unsupervised studying, and particularly considering of variational autoencoders, what are the principle issues they offer us? One factor that’s seldom lacking from papers on generative strategies are photos of super-real-looking faces (or mattress rooms, or animals …). So evidently sampling (or: technology) is a crucial half. If we are able to pattern from a mannequin and acquire real-seeming entities, this implies the mannequin has discovered one thing about how issues are distributed on this planet: it has discovered a distribution.
Within the case of variational autoencoders, there may be extra: The entities are imagined to be decided by a set of distinct, disentangled (hopefully!) latent elements. However this isn’t the idea within the case of normalizing flows, so we’re not going to elaborate on this right here.

As a recap, how will we pattern from a VAE? We draw from (z), the latent variable, and run the decoder community on it. The outcome ought to – we hope – appear like it comes from the empirical knowledge distribution. It mustn’t, nevertheless, look precisely like every of the objects used to coach the VAE, or else we now have not discovered something helpful.

The second factor we might get from a VAE is an evaluation of the plausibility of particular person knowledge, for use, for instance, in anomaly detection. Right here “plausibility” is obscure on goal: With VAE, we don’t have a method to compute an precise density beneath the posterior.

What if we would like, or want, each: technology of samples in addition to density estimation? That is the place normalizing flows are available in.

Normalizing flows

A movement is a sequence of differentiable, invertible mappings from knowledge to a “good” distribution, one thing we are able to simply pattern from and use to calculate a density. Let’s take as instance the canonical technique to generate samples from some distribution, the exponential, say.

We begin by asking our random quantity generator for some quantity between 0 and 1:

This quantity we deal with as coming from a cumulative chance distribution (CDF) – from an exponential CDF, to be exact. Now that we now have a worth from the CDF, all we have to do is map that “again” to a worth. That mapping CDF -> worth we’re in search of is simply the inverse of the CDF of an exponential distribution, the CDF being

[F(x) = 1 – e^{-lambda x}]

The inverse then is

[
F^{-1}(u) = -frac{1}{lambda} ln (1 – u)
]

which suggests we might get our exponential pattern doing

lambda <- 0.5 # decide some lambda
x <- -1/lambda * log(1-u)

We see the CDF is definitely a movement (or a constructing block thereof, if we image most flows as comprising a number of transformations), since

  • It maps knowledge to a uniform distribution between 0 and 1, permitting to evaluate knowledge probability.
  • Conversely, it maps a chance to an precise worth, thus permitting to generate samples.

From this instance, we see why a movement ought to be invertible, however we don’t but see why it ought to be differentiable. This may turn into clear shortly, however first let’s check out how flows can be found in tfprobability.

Bijectors

TFP comes with a treasure trove of transformations, known as bijectors, starting from easy computations like exponentiation to extra complicated ones just like the discrete cosine remodel.

To get began, let’s use tfprobability to generate samples from the traditional distribution.
There’s a bijector tfb_normal_cdf() that takes enter knowledge to the interval ([0,1]). Its inverse remodel then yields a random variable with the usual regular distribution:

Conversely, we are able to use this bijector to find out the (log) chance of a pattern from the traditional distribution. We’ll verify towards an easy use of tfd_normal within the distributions module:

x <- 2.01
d_n <- tfd_normal(loc = 0, scale = 1) 

d_n %>% tfd_log_prob(x) %>% as.numeric() # -2.938989

To acquire that very same log chance from the bijector, we add two parts:

  • Firstly, we run the pattern via the ahead transformation and compute log chance beneath the uniform distribution.
  • Secondly, as we’re utilizing the uniform distribution to find out chance of a traditional pattern, we have to monitor how chance adjustments beneath this transformation. That is completed by calling tfb_forward_log_det_jacobian (to be additional elaborated on under).
b <- tfb_normal_cdf()
d_u <- tfd_uniform()

l <- d_u %>% tfd_log_prob(b %>% tfb_forward(x))
j <- b %>% tfb_forward_log_det_jacobian(x, event_ndims = 0)

(l + j) %>% as.numeric() # -2.938989

Why does this work? Let’s get some background.

Chance mass is conserved

Flows are based mostly on the precept that beneath transformation, chance mass is conserved. Say we now have a movement from (x) to (z):
[z = f(x)]

Suppose we pattern from (z) after which, compute the inverse remodel to acquire (x). We all know the chance of (z). What’s the chance that (x), the remodeled pattern, lies between (x_0) and (x_0 + dx)?

This chance is (p(x) dx), the density instances the size of the interval. This has to equal the chance that (z) lies between (f(x)) and (f(x + dx)). That new interval has size (f'(x) dx), so:

[p(x) dx = p(z) f'(x) dx]

Or equivalently

[p(x) = p(z) * dz/dx]

Thus, the pattern chance (p(x)) is set by the bottom chance (p(z)) of the remodeled distribution, multiplied by how a lot the movement stretches house.

The identical goes in greater dimensions: Once more, the movement is in regards to the change in chance quantity between the (z) and (y) areas:

[p(x) = p(z) frac{vol(dz)}{vol(dx)}]

In greater dimensions, the Jacobian replaces the by-product. Then, the change in quantity is captured by absolutely the worth of its determinant:

[p(mathbf{x}) = p(f(mathbf{x})) bigg|detfrac{partial f({mathbf{x})}}{partial{mathbf{x}}}bigg|]

In observe, we work with log possibilities, so

[log p(mathbf{x}) = log p(f(mathbf{x})) + log bigg|detfrac{partial f({mathbf{x})}}{partial{mathbf{x}}}bigg| ]

Let’s see this with one other bijector instance, tfb_affine_scalar. Under, we assemble a mini-flow that maps a couple of arbitrary chosen (x) values to double their worth (scale = 2):

x <- c(0, 0.5, 1)
b <- tfb_affine_scalar(shift = 0, scale = 2)

To match densities beneath the movement, we select the traditional distribution, and have a look at the log densities:

d_n <- tfd_normal(loc = 0, scale = 1)
d_n %>% tfd_log_prob(x) %>% as.numeric() # -0.9189385 -1.0439385 -1.4189385

Now apply the movement and compute the brand new log densities as a sum of the log densities of the corresponding (x) values and the log determinant of the Jacobian:

z <- b %>% tfb_forward(x)

(d_n  %>% tfd_log_prob(b %>% tfb_inverse(z))) +
  (b %>% tfb_inverse_log_det_jacobian(z, event_ndims = 0)) %>%
  as.numeric() # -1.6120857 -1.7370857 -2.1120858

We see that because the values get stretched in house (we multiply by 2), the person log densities go down.
We are able to confirm the cumulative chance stays the identical utilizing tfd_transformed_distribution():

d_t <- tfd_transformed_distribution(distribution = d_n, bijector = b)
d_n %>% tfd_cdf(x) %>% as.numeric()  # 0.5000000 0.6914625 0.8413447

d_t %>% tfd_cdf(y) %>% as.numeric()  # 0.5000000 0.6914625 0.8413447

To date, the flows we noticed had been static – how does this match into the framework of neural networks?

Coaching a movement

On condition that flows are bidirectional, there are two methods to consider them. Above, we now have principally careworn the inverse mapping: We wish a easy distribution we are able to pattern from, and which we are able to use to compute a density. In that line, flows are generally known as “mappings from knowledge to noise” – noise principally being an isotropic Gaussian. Nonetheless in observe, we don’t have that “noise” but, we simply have knowledge.
So in observe, we now have to be taught a movement that does such a mapping. We do that by utilizing bijectors with trainable parameters.
We’ll see a quite simple instance right here, and depart “actual world flows” to the following publish.

The instance relies on half 1 of Eric Jang’s introduction to normalizing flows. The primary distinction (aside from simplification to point out the essential sample) is that we’re utilizing keen execution.

We begin from a two-dimensional, isotropic Gaussian, and we wish to mannequin knowledge that’s additionally regular, however with a imply of 1 and a variance of two (in each dimensions).

library(tensorflow)
library(tfprobability)

tfe_enable_eager_execution(device_policy = "silent")

library(tfdatasets)

# the place we begin from
base_dist <- tfd_multivariate_normal_diag(loc = c(0, 0))

# the place we wish to go
target_dist <- tfd_multivariate_normal_diag(loc = c(1, 1), scale_identity_multiplier = 2)

# create coaching knowledge from the goal distribution
target_samples <- target_dist %>% tfd_sample(1000) %>% tf$forged(tf$float32)

batch_size <- 100
dataset <- tensor_slices_dataset(target_samples) %>%
  dataset_shuffle(buffer_size = dim(target_samples)[1]) %>%
  dataset_batch(batch_size)

Now we’ll construct a tiny neural community, consisting of an affine transformation and a nonlinearity.
For the previous, we are able to make use of tfb_affine, the multi-dimensional relative of tfb_affine_scalar.
As to nonlinearities, presently TFP comes with tfb_sigmoid and tfb_tanh, however we are able to construct our personal parameterized ReLU utilizing tfb_inline:

# alpha is a learnable parameter
bijector_leaky_relu <- perform(alpha) {
  
  tfb_inline(
    # ahead remodel leaves optimistic values untouched and scales damaging ones by alpha
    forward_fn = perform(x)
      tf$the place(tf$greater_equal(x, 0), x, alpha * x),
    # inverse remodel leaves optimistic values untouched and scales damaging ones by 1/alpha
    inverse_fn = perform(y)
      tf$the place(tf$greater_equal(y, 0), y, 1/alpha * y),
    # quantity change is 0 when optimistic and 1/alpha when damaging
    inverse_log_det_jacobian_fn = perform(y) {
      I <- tf$ones_like(y)
      J_inv <- tf$the place(tf$greater_equal(y, 0), I, 1/alpha * I)
      log_abs_det_J_inv <- tf$log(tf$abs(J_inv))
      tf$reduce_sum(log_abs_det_J_inv, axis = 1L)
    },
    forward_min_event_ndims = 1
  )
}

Outline the learnable variables for the affine and the PReLU layers:

d <- 2 # dimensionality
r <- 2 # rank of replace

# shift of affine bijector
shift <- tf$get_variable("shift", d)
# scale of affine bijector
L <- tf$get_variable('L', c(d * (d + 1) / 2))
# rank-r replace
V <- tf$get_variable("V", c(d, r))

# scaling issue of parameterized relu
alpha <- tf$abs(tf$get_variable('alpha', checklist())) + 0.01

With keen execution, the variables have for use contained in the loss perform, so that’s the place we outline the bijectors. Our little movement now’s a tfb_chain of bijectors, and we wrap it in a TransformedDistribution (tfd_transformed_distribution) that hyperlinks supply and goal distributions.

loss <- perform() {
  
 affine <- tfb_affine(
        scale_tril = tfb_fill_triangular() %>% tfb_forward(L),
        scale_perturb_factor = V,
        shift = shift
      )
 lrelu <- bijector_leaky_relu(alpha = alpha)  
 
 movement <- checklist(lrelu, affine) %>% tfb_chain()
 
 dist <- tfd_transformed_distribution(distribution = base_dist,
                          bijector = movement)
  
 l <- -tf$reduce_mean(dist$log_prob(batch))
 # preserve monitor of progress
 print(spherical(as.numeric(l), 2))
 l
}

Now we are able to truly run the coaching!

optimizer <- tf$practice$AdamOptimizer(1e-4)

n_epochs <- 100
for (i in 1:n_epochs) {
  iter <- make_iterator_one_shot(dataset)
  until_out_of_range({
    batch <- iterator_get_next(iter)
    optimizer$decrease(loss)
  })
}

Outcomes will differ relying on random initialization, however it is best to see a gentle (if gradual) progress. Utilizing bijectors, we now have truly educated and outlined slightly neural community.

Outlook

Undoubtedly, this movement is just too easy to mannequin complicated knowledge, nevertheless it’s instructive to have seen the essential ideas earlier than delving into extra complicated flows. Within the subsequent publish, we’ll take a look at autoregressive flows, once more utilizing TFP and tfprobability.

Jimenez Rezende, Danilo, and Shakir Mohamed. 2015. “Variational Inference with Normalizing Flows.” arXiv e-Prints, Might, arXiv:1505.05770. https://arxiv.org/abs/1505.05770.

Related Articles

LEAVE A REPLY

Please enter your comment!
Please enter your name here

Latest Articles