Posit AI Weblog: entrando no fluxo: bijetores em probabilidade de tensorflow


A partir de hoje, os maiores sucessos do Deep Studying ocorreram no campo da aprendizagem supervisionada, exigindo muitos dados de treinamento anotados. No entanto, os dados não são (normalmente) com anotações ou etiquetas. Também, aprendizado não supervisionado é atraente por causa da analogia à cognição humana.

Neste weblog até agora, vimos duas principais arquiteturas para o aprendizado não supervisionado: AutoEncoders variacionais e Redes adversárias generativas. Menos conhecido, mas atraente por razões conceituais e de desempenho são Fluxos normalizando (Jimenez Rezende e Mohamed 2015). Neste e no próximo publish, apresentaremos fluxos, concentrando -se em como implementá -los usando Probabilidade do tensorflow (TFP).

Em contraste com Postagens anteriores envolvendo TFP que acessou sua funcionalidade usando baixo nível $-Syntax, agora fazemos uso de TFProbabilityum invólucro no estilo de kerasAssim, tensorflow e tfdatasets. Uma nota sobre este pacote: ele ainda está sob desenvolvimento pesado e a API pode mudar. Até o momento em que este artigo foi escrito, os invólucros ainda não existem para todos os módulos TFP, mas toda a funcionalidade TFP está disponível usando $-Syntax Se necessário.

Estimativa de densidade e amostragem

De volta à aprendizagem não supervisionada e, especificamente, pensando em autoencoders variacionais, quais são as principais coisas que eles nos dão? Uma coisa que raramente está faltando nos artigos sobre métodos generativos são fotos de rostos de aparência tremendous actual (ou quartos ou animais …). Tão evidentemente amostragem (ou: geração) é uma parte importante. Se pudermos provar de um modelo e obter entidades reais, isso significa que o modelo aprendeu algo sobre como as coisas são distribuídas no mundo: ele aprendeu um distribuição. No caso de autoencoders variacionais, há mais: as entidades devem ser determinadas por um conjunto de fatores latentes distintos e distintos (esperançosamente!). Mas essa não é a suposição no caso de normalizar os fluxos, por isso não vamos elaborar isso aqui.

Como recapitulação, como amostramos de um VAE? Nós desenhamos de (z )a variável latente e execute a rede de decodificadores nela. O resultado deve – esperamos – parece que vem da distribuição empírica de dados. Não deve, no entanto, olhar exatamente Como qualquer um dos itens usados ​​para treinar o VAE, ou não aprendemos nada útil.

A segunda coisa que podemos obter de um VAE é uma avaliação da plausibilidade dos dados individuais, a ser usada, por exemplo, na detecção de anomalia. Aqui a “plausibilidade” é vaga de propósito: com VAE, não temos meios de calcular uma densidade actual sob o posterior.

E se quisermos ou precisarmos, ambos: geração de amostras e estimativa de densidade? É aqui que Fluxos normalizando entre.

Fluxos normalizando

UM fluxo é uma sequência de mapeamentos diferenciáveis ​​e invertíveis de dados para uma distribuição “agradável”, algo de que podemos facilmente amostrar e usar para calcular uma densidade. Vamos tomar como exemplo a maneira canônica de gerar amostras de alguma distribuição, digamos, o exponencial.

Começamos pedindo ao nosso gerador de números aleatórios um número entre 0 e 1:

Este número que tratamos como vindo de um Distribuição cumulativa de probabilidade (CDF) – de um exponencial CDF, para ser preciso. Agora que temos um valor do CDF, tudo o que precisamos fazer é mapear isso “de volta” para um valor. Esse mapeamento CDF -> worth Estamos procurando é apenas o inverso do CDF de uma distribuição exponencial, sendo o CDF

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

O inverso então é

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

o que significa que podemos fazer nossa amostra exponencial fazendo

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

Vemos o CDF é realmente um fluxo (ou um bloco de construção, se imaginarmos a maioria dos fluxos como compreendendo várias transformações), pois

  • Ele mapeia os dados para uma distribuição uniforme entre 0 e 1, permitindo avaliar a probabilidade de dados.
  • Por outro lado, ele mapeia uma probabilidade para um valor actual, permitindo gerar amostras.

A partir deste exemplo, vemos por que um fluxo deve ser invertível, mas ainda não vemos por que deveria ser diferenciável. Isso ficará claro em breve, mas primeiro vamos dar uma olhada em como os fluxos estão disponíveis em tfprobability.

Bijetores

TFP vem com um tesouro de transformações, chamado bijectorsvariando de cálculos simples como Expeneção para os mais complexos como o Transformação discreta de cosseno.

Para começar, vamos usar tfprobability Para gerar amostras a partir da distribuição regular. Há um bijetor tfb_normal_cdf() que leva dados de entrada para o intervalo ((0,1) ). Sua transformação inversa produz uma variável aleatória com a distribuição regular padrão:

Posit AI Weblog: entrando no fluxo: bijetores em probabilidade de tensorflow

Por outro lado, podemos usar esse bijetor para determinar a probabilidade (log) de uma amostra da distribuição regular. Vamos verificar contra um uso direto de tfd_normal no distributions módulo:

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

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

Para obter a mesma probabilidade de log do bijetor, adicionamos dois componentes:

  • Em primeiro lugar, executamos a amostra através do ahead Transformação e Calcule a probabilidade de log sob a distribuição uniforme.
  • Em segundo lugar, como estamos usando a distribuição uniforme para determinar a probabilidade de uma amostra regular, precisamos rastrear como a probabilidade muda sob essa transformação. Isso é feito ligando tfb_forward_log_det_jacobian (a ser elaborado mais abaixo).
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

Por que isso funciona? Vamos obter alguns antecedentes.

A massa de probabilidade é conservada

Os fluxos são baseados no princípio de que, sob transformação, a massa de probabilidade é conservada. Digamos que temos um fluxo de (x ) para (z ):
(z = f (x) )

Suponha que provemos de (z ) e então, calcule a transformação inversa para obter (x ). Nós sabemos a probabilidade de (z ). Qual é a probabilidade de que (x )a amostra transformada, está entre (x_0 ) e (x_0 + dx )?

Essa probabilidade é (p (x) dx )a densidade vezes o comprimento do intervalo. Isso tem que igualar a probabilidade de que (z ) mentiras entre (f (x) ) e (f (x + dx) ). Esse novo intervalo tem comprimento (f ‘(x) dx )então:

(p (x) dx = p (z) f ‘(x) dx )

Ou equivalentemente

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

Assim, a probabilidade de amostra (p (x) ) é determinado pela probabilidade base (p (z) ) da distribuição transformada, multiplicada por quanto o fluxo estende o espaço.

O mesmo acontece em dimensões mais altas: novamente, o fluxo é sobre a mudança no quantity de probabilidade entre o (z ) e (y ) espaços:

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

Em dimensões mais altas, o jacobiano substitui o derivado. Então, a mudança no quantity é capturada pelo valor absoluto de seu determinante:

(p ( mathbf {x}) = p (f ( mathbf {x})) bigg | det frac { parcial f ({ mathbf {x})}} { parcial { mathbf {x}}}}

Na prática, trabalhamos com probabilidades de log, então

(log p ( mathbf {x}) = log p (f ( mathbf {x})) + log bigg | det frac { parcial f ({ mathbf {x}}} { parcial { mathbf {x}}}}} { }

Vamos ver isso com outro bijector exemplo, tfb_affine_scalar. Abaixo, construímos um mini-fluxo que mapeia alguns escolhidos arbitrários (x ) valores para dobrar seu valor (scale = 2):

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

Para comparar densidades sob o fluxo, escolhemos a distribuição regular e examinamos as densidades de log:

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

Agora aplique o fluxo e calcule as novas densidades de log como uma soma das densidades de log do correspondente (x ) valores e o determinante do log do jacobiano:

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

Vemos que, à medida que os valores são esticados no espaço (multiplicamos por 2), as densidades individuais de log diminuem. Podemos verificar se a probabilidade cumulativa permanece a mesma usando 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

Até agora, os fluxos que vimos eram estáticos – como isso se encaixa na estrutura das redes neurais?

Treinando um fluxo

Dado que os fluxos são bidirecionais, há duas maneiras de pensar neles. Acima, enfatizamos principalmente o mapeamento inverso: queremos uma distribuição simples de que possamos provar e que podemos usar para calcular uma densidade. Nessa linha, os fluxos às vezes são chamados de “mapeamentos de dados para ruído” – barulho sendo principalmente um gaussiano isotrópico. No entanto, na prática, ainda não temos esse “ruído”, apenas temos dados. Então, na prática, temos que aprender um fluxo que faz esse mapeamento. Fazemos isso usando bijectors com parâmetros treináveis. Vamos ver um exemplo muito simples aqui e deixar o “mundo actual fluxos” para o próximo publish.

O exemplo é baseado na parte 1 de Introdução de Eric Jang aos fluxos normalizando. A principal diferença (além da simplificação para mostrar o padrão básico) é que estamos usando a execução ansiosa.

Começamos de um gaussiano bidimensional e isotrópico e queremos modelar dados que também são normais, mas com uma média de 1 e uma variação de 2 (em ambas as dimensões).

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 information 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)

Agora, construiremos uma pequena rede neural, composta por uma transformação afim e uma não linearidade. Para o primeiro, podemos usar tfb_affineo parente multidimensional de tfb_affine_scalar. Quanto às não linearidades, atualmente o TFP vem com tfb_sigmoid e tfb_tanhmas podemos construir nosso próprio relu parametrizado usando tfb_inline:

# alpha is a learnable parameter
bijector_leaky_relu <- operate(alpha) {
  
  tfb_inline(
    # ahead rework leaves optimistic values untouched and scales unfavourable ones by alpha
    forward_fn = operate(x)
      tf$the place(tf$greater_equal(x, 0), x, alpha * x),
    # inverse rework leaves optimistic values untouched and scales unfavourable ones by 1/alpha
    inverse_fn = operate(y)
      tf$the place(tf$greater_equal(y, 0), y, 1/alpha * y),
    # quantity change is 0 when optimistic and 1/alpha when unfavourable
    inverse_log_det_jacobian_fn = operate(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
  )
}

Defina as variáveis ​​aprendidas para o afine e as camadas pré -prelum:

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

Com a execução ansiosa, as variáveis ​​devem ser usadas dentro da função de perda, e é onde definimos os bijetores. Nosso pequeno fluxo agora é um tfb_chain de bijetores, e nós o envolvemos em um TransformedDistribution (tfd_transformed_distribution) que vincula distribuições de origem e destino.

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

Agora podemos realmente executar o treinamento!

optimizer <- tf$prepare$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)
  })
}

Os resultados diferirão dependendo da inicialização aleatória, mas você deve ver um progresso constante (se lento). Usando bijetores, na verdade treinamos e definimos um pouco de rede neural.

Panorama

Sem dúvida, esse fluxo é muito simples para modelar dados complexos, mas é instrutivo ter visto os princípios básicos antes de se aprofundar em fluxos mais complexos. No próximo publish, vamos checar Fluxos autoregressivosnovamente usando TFP e tfprobability.

Jimenez Rezende, Danilo e Shakir Mohamed. 2015. “Inferência variacional de normalizar fluxos”. ARXIV E-PRINTSMaio, arxiv: 1505.05770. https://arxiv.org/abs/1505.05770.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *