Noções básicas sobre formas de distribuição do TensorFlow

Ver no TensorFlow.org Executar no Google Colab Ver fonte no GitHub Baixar caderno
import collections

import tensorflow as tf
tf.compat.v2.enable_v2_behavior()

import tensorflow_probability as tfp
tfd = tfp.distributions
tfb = tfp.bijectors

Fundamentos

Existem três conceitos importantes associados às formas das distribuições do TensorFlow:

  • Forma evento descreve a forma de um único desenho de distribuição; pode ser dependente entre as dimensões. Para distribuições escalares, a forma evento é [] . Para um MultivariateNormal 5-dimensional, a forma evento é [5] .
  • Forma de lote descreve independente, não identicamente distribuídos chama, também conhecido como um "lote" de distribuições.
  • Forma independente exemplo descreve, igualmente distribuída chama de lotes a partir da família de distribuição.

A forma evento e a forma de lote são as propriedades de um Distribution objecto, ao passo que a forma da amostra está associada com uma chamada específico para sample ou log_prob .

O propósito deste caderno é ilustrar esses conceitos através de exemplos, então se isso não for imediatamente óbvio, não se preocupe!

Por outro visão geral conceitual destes conceitos, veja este post .

Uma nota sobre o TensorFlow Eager.

Todo esse notebook é escrito usando TensorFlow Eager . Nenhum dos conceitos apresentados dependem ansioso, embora com ansiosos, em lotes de distribuição e de eventos formas são avaliados (e, portanto, conhecido) quando a Distribution objecto é criado em Python, ao passo que no gráfico (modo não ansiosos), é possível definir distribuições cujos formatos de evento e lote são indeterminados até que o gráfico seja executado.

Distribuições escalares

Como observamos acima, a Distribution objeto definiu formas de eventos e de lote. Começaremos com um utilitário para descrever distribuições:

def describe_distributions(distributions):
  print('\n'.join([str(d) for d in distributions]))

Nesta seção vamos explorar distribuições escalares: distribuições com uma forma caso de [] . Um exemplo típico é a distribuição de Poisson, especificado por uma rate :

poisson_distributions = [
    tfd.Poisson(rate=1., name='One Poisson Scalar Batch'),
    tfd.Poisson(rate=[1., 10., 100.], name='Three Poissons'),
    tfd.Poisson(rate=[[1., 10., 100.,], [2., 20., 200.]],
                name='Two-by-Three Poissons'),
    tfd.Poisson(rate=[1.], name='One Poisson Vector Batch'),
    tfd.Poisson(rate=[[1.]], name='One Poisson Expanded Batch')
]

describe_distributions(poisson_distributions)
tfp.distributions.Poisson("One_Poisson_Scalar_Batch", batch_shape=[], event_shape=[], dtype=float32)
tfp.distributions.Poisson("Three_Poissons", batch_shape=[3], event_shape=[], dtype=float32)
tfp.distributions.Poisson("Two_by_Three_Poissons", batch_shape=[2, 3], event_shape=[], dtype=float32)
tfp.distributions.Poisson("One_Poisson_Vector_Batch", batch_shape=[1], event_shape=[], dtype=float32)
tfp.distributions.Poisson("One_Poisson_Expanded_Batch", batch_shape=[1, 1], event_shape=[], dtype=float32)

A distribuição de Poisson é uma distribuição escalar, pelo que a sua forma evento é sempre [] . Se especificarmos mais taxas, elas aparecem na forma de lote. O par final de exemplos é interessante: há apenas uma única taxa, mas como essa taxa está incorporada em uma matriz numpy com forma não vazia, essa forma se torna a forma de lote.

A distribuição Normal padrão também é escalar. É forma evento é [] , assim como para a Poisson, mas vamos jogar com ele para ver o nosso primeiro exemplo de radiodifusão. O normal é especificado usando loc e scale parâmetros:

normal_distributions = [
    tfd.Normal(loc=0., scale=1., name='Standard'),
    tfd.Normal(loc=[0.], scale=1., name='Standard Vector Batch'),
    tfd.Normal(loc=[0., 1., 2., 3.], scale=1., name='Different Locs'),
    tfd.Normal(loc=[0., 1., 2., 3.], scale=[[1.], [5.]],
               name='Broadcasting Scale')
]

describe_distributions(normal_distributions)
tfp.distributions.Normal("Standard", batch_shape=[], event_shape=[], dtype=float32)
tfp.distributions.Normal("Standard_Vector_Batch", batch_shape=[1], event_shape=[], dtype=float32)
tfp.distributions.Normal("Different_Locs", batch_shape=[4], event_shape=[], dtype=float32)
tfp.distributions.Normal("Broadcasting_Scale", batch_shape=[2, 4], event_shape=[], dtype=float32)

O exemplo interessante acima é o Broadcasting Scale de distribuição. A loc parâmetro tem a forma [4] , e a scale parâmetro tem a forma [2, 1] . Utilizando as regras de radiodifusão Numpy , a forma de lote é [2, 4] . Uma maneira equivalente (mas menos elegante e não-recomendado) para definir o "Broadcasting Scale" distribuição seria:

describe_distributions(
    [tfd.Normal(loc=[[0., 1., 2., 3], [0., 1., 2., 3.]],
                scale=[[1., 1., 1., 1.], [5., 5., 5., 5.]])])
tfp.distributions.Normal("Normal", batch_shape=[2, 4], event_shape=[], dtype=float32)

Podemos ver por que a notação de transmissão é útil, embora também seja uma fonte de dores de cabeça e bugs.

Amostragem de Distribuições Escalares

Existem duas principais coisas que podemos fazer com as distribuições: podemos sample com eles e podemos calcular log_prob s. Vamos explorar a amostragem primeiro. A regra básica é que, quando se provar a partir de uma distribuição, o tensor resultante tem a forma [sample_shape, batch_shape, event_shape] , onde batch_shape e event_shape são fornecidos pelo Distribution objeto, e sample_shape é fornecido pela chamada para sample . Para distribuições escalares, event_shape = [] , de modo que o tensor devolvido a partir da amostra terá forma [sample_shape, batch_shape] . Vamos tentar:

def describe_sample_tensor_shape(sample_shape, distribution):
    print('Sample shape:', sample_shape)
    print('Returned sample tensor shape:',
          distribution.sample(sample_shape).shape)

def describe_sample_tensor_shapes(distributions, sample_shapes):
    started = False
    for distribution in distributions:
      print(distribution)
      for sample_shape in sample_shapes:
        describe_sample_tensor_shape(sample_shape, distribution)
      print()

sample_shapes = [1, 2, [1, 5], [3, 4, 5]]
describe_sample_tensor_shapes(poisson_distributions, sample_shapes)
tfp.distributions.Poisson("One_Poisson_Scalar_Batch", batch_shape=[], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1,)
Sample shape: 2
Returned sample tensor shape: (2,)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5)

tfp.distributions.Poisson("Three_Poissons", batch_shape=[3], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 3)
Sample shape: 2
Returned sample tensor shape: (2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 3)

tfp.distributions.Poisson("Two_by_Three_Poissons", batch_shape=[2, 3], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 2, 3)
Sample shape: 2
Returned sample tensor shape: (2, 2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 2, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 2, 3)

tfp.distributions.Poisson("One_Poisson_Vector_Batch", batch_shape=[1], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 1)
Sample shape: 2
Returned sample tensor shape: (2, 1)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 1)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 1)

tfp.distributions.Poisson("One_Poisson_Expanded_Batch", batch_shape=[1, 1], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 1, 1)
Sample shape: 2
Returned sample tensor shape: (2, 1, 1)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 1, 1)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 1, 1)
describe_sample_tensor_shapes(normal_distributions, sample_shapes)
tfp.distributions.Normal("Standard", batch_shape=[], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1,)
Sample shape: 2
Returned sample tensor shape: (2,)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5)

tfp.distributions.Normal("Standard_Vector_Batch", batch_shape=[1], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 1)
Sample shape: 2
Returned sample tensor shape: (2, 1)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 1)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 1)

tfp.distributions.Normal("Different_Locs", batch_shape=[4], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 4)
Sample shape: 2
Returned sample tensor shape: (2, 4)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 4)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 4)

tfp.distributions.Normal("Broadcasting_Scale", batch_shape=[2, 4], event_shape=[], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 2, 4)
Sample shape: 2
Returned sample tensor shape: (2, 2, 4)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 2, 4)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 2, 4)

Isso é sobre tudo o que há a dizer sobre sample : tensores amostra devolvidos têm forma [sample_shape, batch_shape, event_shape] .

Computação log_prob Para escalares Distribuições

Agora vamos dar uma olhada log_prob , que é um pouco mais complicado. log_prob toma como entrada um tensor (não vazia), que representa o local (s) em que para calcular o log_prob para a distribuição. No caso mais simples, este tensor terá uma forma da forma [sample_shape, batch_shape, event_shape] , onde batch_shape e event_shape jogo, os lotes de eventos e formas de distribuição. Lembre-se mais uma vez que para distribuições escalares, event_shape = [] , de modo que o tensor de entrada tem forma [sample_shape, batch_shape] Neste caso, nós voltamos um tensor de forma [sample_shape, batch_shape] :

three_poissons = tfd.Poisson(rate=[1., 10., 100.], name='Three Poissons')
three_poissons
<tfp.distributions.Poisson 'Three_Poissons' batch_shape=[3] event_shape=[] dtype=float32>
three_poissons.log_prob([[1., 10., 100.], [100., 10., 1]])  # sample_shape is [2].
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -2.0785608,   -3.2223587],
       [-364.73938  ,   -2.0785608,  -95.39484  ]], dtype=float32)>
three_poissons.log_prob([[[[1., 10., 100.], [100., 10., 1.]]]])  # sample_shape is [1, 1, 2].
<tf.Tensor: shape=(1, 1, 2, 3), dtype=float32, numpy=
array([[[[  -1.       ,   -2.0785608,   -3.2223587],
         [-364.73938  ,   -2.0785608,  -95.39484  ]]]], dtype=float32)>

Nota como no primeiro exemplo, a entrada e saída têm forma [2, 3] e no segundo exemplo, eles têm a forma [1, 1, 2, 3] .

Isso seria tudo o que haveria a dizer, se não fosse pela transmissão. Aqui estão as regras, uma vez que levamos em consideração a transmissão. Nós o descrevemos em total generalidade e observamos simplificações para distribuições escalares:

  1. Definir n = len(batch_shape) + len(event_shape) . (Para as distribuições escalares, len(event_shape)=0 .)
  2. Se a entrada tensor t tem menos de n dimensões, almofada a sua forma através da adição de dimensões de tamanho 1 , à esquerda, até que tem exactamente n dimensões. Chame o tensor resultando t' .
  3. Transmissão das n dimensões mais à direita de t' contra o [batch_shape, event_shape] da distribuição você está computando uma log_prob para. Em mais pormenor: para as dimensões onde t' já corresponde à distribuição, não fazer nada, e por as dimensões onde t' tem um Singleton, replicado que Singleton o número adequado de vezes. Qualquer outra situação é um erro. (Para distribuições escalares, só transmitido contra batch_shape , desde event_shape = [] .)
  4. Agora estamos finalmente em condições de calcular o log_prob . O tensor resultante terá forma [sample_shape, batch_shape] , onde sample_shape é definido para ser quaisquer dimensões de t ou t' para a esquerda dos n -rightmost dimensões: sample_shape = shape(t)[:-n] .

Isso pode ser uma bagunça se você não sabe o que significa, então vamos trabalhar alguns exemplos:

three_poissons.log_prob([10.])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-16.104412 ,  -2.0785608, -69.05272  ], dtype=float32)>

O tensor [10.] (com a forma [1] ) é transmitido através do batch_shape de 3, de modo que avaliar a probabilidade log todos os três Poissons' no valor de 10.

three_poissons.log_prob([[[1.], [10.]], [[100.], [1000.]]])
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[-1.0000000e+00, -7.6974149e+00, -9.5394836e+01],
        [-1.6104412e+01, -2.0785608e+00, -6.9052719e+01]],

       [[-3.6473938e+02, -1.4348087e+02, -3.2223587e+00],
        [-5.9131279e+03, -3.6195427e+03, -1.4069575e+03]]], dtype=float32)>

No exemplo acima, o tensor de entrada tem a forma [2, 2, 1] , enquanto o objecto distribuições tem uma forma de lote 3. Assim, para cada um dos [2, 2] dimensões da amostra, o valor único fornecido a cada recebe broadcats dos três Poissons.

Uma maneira possivelmente útil para pensar sobre isso: porque three_poissons tem batch_shape = [2, 3] , uma chamada para log_prob deve tomar uma Tensor cuja última dimensão é 1 ou 3; qualquer outra coisa é um erro. (As regras de radiodifusão numpy tratar o caso especial de um escalar como sendo totalmente equivalente a um tensor de forma [1] ).

Vamos testar a nossa costeletas, jogando com a distribuição mais complexa Poisson com batch_shape = [2, 3] :

poisson_2_by_3 = tfd.Poisson(
    rate=[[1., 10., 100.,], [2., 20., 200.]],
    name='Two-by-Three Poissons')
poisson_2_by_3.log_prob(1.)
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [  -1.3068528,  -17.004269 , -194.70169  ]], dtype=float32)>
poisson_2_by_3.log_prob([1.])  # Exactly equivalent to above, demonstrating the scalar special case.
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [  -1.3068528,  -17.004269 , -194.70169  ]], dtype=float32)>
poisson_2_by_3.log_prob([[1., 1., 1.], [1., 1., 1.]])  # Another way to write the same thing. No broadcasting.
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [  -1.3068528,  -17.004269 , -194.70169  ]], dtype=float32)>
poisson_2_by_3.log_prob([[1., 10., 100.]])  # Input is [1, 3] broadcast to [2, 3].
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ -1.       ,  -2.0785608,  -3.2223587],
       [ -1.3068528,  -5.14709  , -33.90767  ]], dtype=float32)>
poisson_2_by_3.log_prob([[1., 10., 100.], [1., 10., 100.]])  # Equivalent to above. No broadcasting.
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[ -1.       ,  -2.0785608,  -3.2223587],
       [ -1.3068528,  -5.14709  , -33.90767  ]], dtype=float32)>
poisson_2_by_3.log_prob([[1., 1., 1.], [2., 2., 2.]])  # No broadcasting.
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [  -1.3068528,  -14.701683 , -190.09653  ]], dtype=float32)>
poisson_2_by_3.log_prob([[1.], [2.]])  # Equivalent to above. Input shape [2, 1] broadcast to [2, 3].
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [  -1.3068528,  -14.701683 , -190.09653  ]], dtype=float32)>

Os exemplos acima envolveram a transmissão no lote, mas a forma da amostra estava vazia. Suponha que temos uma coleção de valores e queremos obter o log de probabilidade de cada valor em cada ponto do lote. Poderíamos fazer isso manualmente:

poisson_2_by_3.log_prob([[[1., 1., 1.], [1., 1., 1.]], [[2., 2., 2.], [2., 2., 2.]]])  # Input shape [2, 2, 3].
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[  -1.       ,   -7.697415 ,  -95.39484  ],
        [  -1.3068528,  -17.004269 , -194.70169  ]],

       [[  -1.6931472,   -6.087977 ,  -91.48282  ],
        [  -1.3068528,  -14.701683 , -190.09653  ]]], dtype=float32)>

Ou podemos deixar a transmissão lidar com a última dimensão do lote:

poisson_2_by_3.log_prob([[[1.], [1.]], [[2.], [2.]]])  # Input shape [2, 2, 1].
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[  -1.       ,   -7.697415 ,  -95.39484  ],
        [  -1.3068528,  -17.004269 , -194.70169  ]],

       [[  -1.6931472,   -6.087977 ,  -91.48282  ],
        [  -1.3068528,  -14.701683 , -190.09653  ]]], dtype=float32)>

Também podemos (talvez um pouco menos naturalmente) deixar a transmissão lidar com apenas a primeira dimensão do lote:

poisson_2_by_3.log_prob([[[1., 1., 1.]], [[2., 2., 2.]]])  # Input shape [2, 1, 3].
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[  -1.       ,   -7.697415 ,  -95.39484  ],
        [  -1.3068528,  -17.004269 , -194.70169  ]],

       [[  -1.6931472,   -6.087977 ,  -91.48282  ],
        [  -1.3068528,  -14.701683 , -190.09653  ]]], dtype=float32)>

Ou poderíamos deixar transmitindo lidar com ambos os lotes dimensões:

poisson_2_by_3.log_prob([[[1.]], [[2.]]])  # Input shape [2, 1, 1].
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[  -1.       ,   -7.697415 ,  -95.39484  ],
        [  -1.3068528,  -17.004269 , -194.70169  ]],

       [[  -1.6931472,   -6.087977 ,  -91.48282  ],
        [  -1.3068528,  -14.701683 , -190.09653  ]]], dtype=float32)>

O acima funcionou bem quando tínhamos apenas dois valores que queríamos, mas suponha que tivéssemos uma longa lista de valores que queríamos avaliar em cada ponto de lote. Para isso, a seguinte notação, que adiciona dimensões extras de tamanho 1 ao lado direito da forma, é extremamente útil:

poisson_2_by_3.log_prob(tf.constant([1., 2.])[..., tf.newaxis, tf.newaxis])
<tf.Tensor: shape=(2, 2, 3), dtype=float32, numpy=
array([[[  -1.       ,   -7.697415 ,  -95.39484  ],
        [  -1.3068528,  -17.004269 , -194.70169  ]],

       [[  -1.6931472,   -6.087977 ,  -91.48282  ],
        [  -1.3068528,  -14.701683 , -190.09653  ]]], dtype=float32)>

Esta é uma instância de notação fatia strided , que vale a pena conhecer.

Voltando ao three_poissons Para completar, o mesmo exemplo se parece com:

three_poissons.log_prob([[1.], [10.], [50.], [100.]])
<tf.Tensor: shape=(4, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [ -16.104412 ,   -2.0785608,  -69.05272  ],
       [-149.47777  ,  -43.34851  ,  -18.219261 ],
       [-364.73938  , -143.48087  ,   -3.2223587]], dtype=float32)>
three_poissons.log_prob(tf.constant([1., 10., 50., 100.])[..., tf.newaxis])  # Equivalent to above.
<tf.Tensor: shape=(4, 3), dtype=float32, numpy=
array([[  -1.       ,   -7.697415 ,  -95.39484  ],
       [ -16.104412 ,   -2.0785608,  -69.05272  ],
       [-149.47777  ,  -43.34851  ,  -18.219261 ],
       [-364.73938  , -143.48087  ,   -3.2223587]], dtype=float32)>

Distribuições multivariadas

Agora nos voltamos para distribuições multivariadas, que têm formato de evento não vazio. Vejamos as distribuições multinomiais.

multinomial_distributions = [
    # Multinomial is a vector-valued distribution: if we have k classes,
    # an individual sample from the distribution has k values in it, so the
    # event_shape is `[k]`.
    tfd.Multinomial(total_count=100., probs=[.5, .4, .1],
                    name='One Multinomial'),
    tfd.Multinomial(total_count=[100., 1000.], probs=[.5, .4, .1],
                    name='Two Multinomials Same Probs'),
    tfd.Multinomial(total_count=100., probs=[[.5, .4, .1], [.1, .2, .7]],
                    name='Two Multinomials Same Counts'),
    tfd.Multinomial(total_count=[100., 1000.],
                    probs=[[.5, .4, .1], [.1, .2, .7]],
                    name='Two Multinomials Different Everything')

]

describe_distributions(multinomial_distributions)
tfp.distributions.Multinomial("One_Multinomial", batch_shape=[], event_shape=[3], dtype=float32)
tfp.distributions.Multinomial("Two_Multinomials_Same_Probs", batch_shape=[2], event_shape=[3], dtype=float32)
tfp.distributions.Multinomial("Two_Multinomials_Same_Counts", batch_shape=[2], event_shape=[3], dtype=float32)
tfp.distributions.Multinomial("Two_Multinomials_Different_Everything", batch_shape=[2], event_shape=[3], dtype=float32)

Note como nos últimos três exemplos, o batch_shape é sempre [2] , mas podemos usar a transmitir quer ter um compartilhada total_count ou um compartilhados probs (ou não), porque sob o capô que são transmitidos para ter a mesma forma.

A amostragem é simples, dado o que já sabemos:

describe_sample_tensor_shapes(multinomial_distributions, sample_shapes)
tfp.distributions.Multinomial("One_Multinomial", batch_shape=[], event_shape=[3], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 3)
Sample shape: 2
Returned sample tensor shape: (2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 3)

tfp.distributions.Multinomial("Two_Multinomials_Same_Probs", batch_shape=[2], event_shape=[3], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 2, 3)
Sample shape: 2
Returned sample tensor shape: (2, 2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 2, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 2, 3)

tfp.distributions.Multinomial("Two_Multinomials_Same_Counts", batch_shape=[2], event_shape=[3], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 2, 3)
Sample shape: 2
Returned sample tensor shape: (2, 2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 2, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 2, 3)

tfp.distributions.Multinomial("Two_Multinomials_Different_Everything", batch_shape=[2], event_shape=[3], dtype=float32)
Sample shape: 1
Returned sample tensor shape: (1, 2, 3)
Sample shape: 2
Returned sample tensor shape: (2, 2, 3)
Sample shape: [1, 5]
Returned sample tensor shape: (1, 5, 2, 3)
Sample shape: [3, 4, 5]
Returned sample tensor shape: (3, 4, 5, 2, 3)

Calcular probabilidades de log é igualmente simples. Vamos trabalhar um exemplo com distribuições normais multivariadas diagonais. (Multinomiais não são muito amigáveis ​​à transmissão, pois as restrições nas contagens e probabilidades significam que a transmissão geralmente produzirá valores inadmissíveis.) Usaremos um lote de 2 distribuições tridimensionais com a mesma média, mas diferentes escalas (desvios padrão):

two_multivariate_normals = tfd.MultivariateNormalDiag(loc=[1., 2., 3.], scale_identity_multiplier=[1., 2.])
two_multivariate_normals
<tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[2] event_shape=[3] dtype=float32>

(Note-se que embora tenham sido utilizados distribuições onde as escalas foram múltiplos da identidade, esta não é uma restrição; poderíamos passar scale em vez de scale_identity_multiplier .)

Agora vamos avaliar a probabilidade logarítmica de cada ponto de lote em sua média e em uma média deslocada:

two_multivariate_normals.log_prob([[[1., 2., 3.]], [[3., 4., 5.]]])  # Input has shape [2,1,3].
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-2.7568154, -4.836257 ],
       [-8.756816 , -6.336257 ]], dtype=float32)>

Precisamente equivalentemente, podemos usar https://www.tensorflow.org/api_docs/cc/class/tensorflow/ops/strided-slice para inserir uma forma adicional = uma dimensão no meio de uma constante:

two_multivariate_normals.log_prob(
    tf.constant([[1., 2., 3.], [3., 4., 5.]])[:, tf.newaxis, :])  # Equivalent to above.
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-2.7568154, -4.836257 ],
       [-8.756816 , -6.336257 ]], dtype=float32)>

Por outro lado, se não o fizermos inserir a dimensão extra, passamos [1., 2., 3.] para o primeiro ponto de lote e [3., 4., 5.] para o segundo:

two_multivariate_normals.log_prob(tf.constant([[1., 2., 3.], [3., 4., 5.]]))
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-2.7568154, -6.336257 ], dtype=float32)>

Técnicas de Manipulação de Forma

O Remodelador Bijetor

O Reshape bijector pode ser usado para remodelar o event_shape de uma distribuição. Vejamos um exemplo:

six_way_multinomial = tfd.Multinomial(total_count=1000., probs=[.3, .25, .2, .15, .08, .02])
six_way_multinomial
<tfp.distributions.Multinomial 'Multinomial' batch_shape=[] event_shape=[6] dtype=float32>

Criamos um multinomial com uma forma caso de [6] . O Reshape Bijector nos permite tratar isso como uma distribuição com uma forma caso de [2, 3] .

Um Bijector representa um diferenciável, uma-a-uma função em um subconjunto aberto de \({\mathbb R}^n\). Bijectors são utilizados em conjunto com TransformedDistribution , que modela uma distribuição \(p(y)\) em termos de uma base de distribuição \(p(x)\) e um Bijector que representa \(Y = g(X)\). Vamos vê-lo em ação:

transformed_multinomial = tfd.TransformedDistribution(
    distribution=six_way_multinomial,
    bijector=tfb.Reshape(event_shape_out=[2, 3]))
transformed_multinomial
<tfp.distributions.TransformedDistribution 'reshapeMultinomial' batch_shape=[] event_shape=[2, 3] dtype=float32>
six_way_multinomial.log_prob([500., 100., 100., 150., 100., 50.])
<tf.Tensor: shape=(), dtype=float32, numpy=-178.22021>
transformed_multinomial.log_prob([[500., 100., 100.], [150., 100., 50.]])
<tf.Tensor: shape=(), dtype=float32, numpy=-178.22021>

Esta é a única coisa que o Reshape bijector pode fazer: ele não pode virar as dimensões do evento em dimensões de lote ou vice-versa.

A Distribuição Independente

O Independent distribuição é usado para tratar uma colecção de independente, não-necessariamente idênticos (também conhecido como um lote de) distribuições como uma única distribuição. De forma mais concisa, Independent permite converter dimensões em batch_shape para dimensões em event_shape . Ilustraremos por exemplo:

two_by_five_bernoulli = tfd.Bernoulli(
    probs=[[.05, .1, .15, .2, .25], [.3, .35, .4, .45, .5]],
    name="Two By Five Bernoulli")
two_by_five_bernoulli
<tfp.distributions.Bernoulli 'Two_By_Five_Bernoulli' batch_shape=[2, 5] event_shape=[] dtype=int32>

Podemos pensar nisso como um conjunto de moedas de dois por cinco com as probabilidades associadas de caras. Vamos avaliar a probabilidade de um determinado conjunto arbitrário de uns e zeros:

pattern = [[1., 0., 0., 1., 0.], [0., 0., 1., 1., 1.]]
two_by_five_bernoulli.log_prob(pattern)
<tf.Tensor: shape=(2, 5), dtype=float32, numpy=
array([[-2.9957323 , -0.10536052, -0.16251892, -1.609438  , -0.2876821 ],
       [-0.35667497, -0.4307829 , -0.9162907 , -0.7985077 , -0.6931472 ]],
      dtype=float32)>

Podemos usar Independent de transformar isso em dois "sets de cinco Bernoulli de" diferentes, o que é útil se quisermos considerar uma "linha" de coin flips chegando em um determinado padrão como um único resultado:

two_sets_of_five = tfd.Independent(
    distribution=two_by_five_bernoulli,
    reinterpreted_batch_ndims=1,
    name="Two Sets Of Five")
two_sets_of_five
<tfp.distributions.Independent 'Two_Sets_Of_Five' batch_shape=[2] event_shape=[5] dtype=int32>

Matematicamente, estamos calculando a probabilidade logarítmica de cada "conjunto" de cinco somando as probabilidades logarítmicas dos cinco lançamentos de moedas "independentes" no conjunto, que é onde a distribuição recebe seu nome:

two_sets_of_five.log_prob(pattern)
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-5.160732 , -3.1954036], dtype=float32)>

Podemos ir ainda mais longe e usar Independent para criar uma distribuição onde os eventos individuais são um conjunto de dois-por-cinco Bernoulli de:

one_set_of_two_by_five = tfd.Independent(
    distribution=two_by_five_bernoulli, reinterpreted_batch_ndims=2,
    name="One Set Of Two By Five")
one_set_of_two_by_five.log_prob(pattern)
<tf.Tensor: shape=(), dtype=float32, numpy=-8.356134>

É importante notar que a partir da perspectiva da sample , usando Independent não muda nada:

describe_sample_tensor_shapes(
    [two_by_five_bernoulli,
     two_sets_of_five,
     one_set_of_two_by_five],
    [[3, 5]])
tfp.distributions.Bernoulli("Two_By_Five_Bernoulli", batch_shape=[2, 5], event_shape=[], dtype=int32)
Sample shape: [3, 5]
Returned sample tensor shape: (3, 5, 2, 5)

tfp.distributions.Independent("Two_Sets_Of_Five", batch_shape=[2], event_shape=[5], dtype=int32)
Sample shape: [3, 5]
Returned sample tensor shape: (3, 5, 2, 5)

tfp.distributions.Independent("One_Set_Of_Two_By_Five", batch_shape=[], event_shape=[2, 5], dtype=int32)
Sample shape: [3, 5]
Returned sample tensor shape: (3, 5, 2, 5)

Como um exercício de despedida para o leitor, sugerimos considerar as diferenças e semelhanças entre um lote vetor de Normal distribuições e uma MultivariateNormalDiag distribuição de uma perspectiva probabilidade de amostragem e log. Como podemos usar Independent para construir um MultivariateNormalDiag de um lote de Normal s? (Note que MultivariateNormalDiag não é realmente implementado desta forma.)