Comprender las formas de distribución de TensorFlow

Ver en TensorFlow.org Ejecutar en Google Colab Ver fuente en GitHub Descargar cuaderno
import collections

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

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

Lo esencial

Hay tres conceptos importantes asociados con las formas de distribuciones de TensorFlow:

  • Forma Evento describe la forma de un único sorteo de la distribución; puede depender de las dimensiones. Para distribuciones escalares, la forma evento es [] . Para una MultivariateNormal 5-dimensional, la forma evento es [5] .
  • Forma de lotes describe independiente, no idénticamente distribuidas dibuja, también conocido como un "lote" de las distribuciones.
  • Forma de muestra describe independiente, idénticamente distribuidas extrae de los lotes de la familia de distribución.

La forma evento y la forma de lotes son propiedades de un Distribution objeto, mientras que la forma de la muestra está asociado con una llamada específica a la sample o log_prob .

El propósito de este cuaderno es ilustrar estos conceptos a través de ejemplos, así que si esto no es obvio de inmediato, ¡no se preocupe!

Por otra visión general conceptual de estos conceptos, consulte esta entrada del blog .

Una nota sobre TensorFlow Eager.

Todo este cuaderno está escrito utilizando TensorFlow Eager . Ninguno de los conceptos presentados depender de Eager, aunque con se evalúan Eager, la distribución de lotes de eventos y formas (y por lo tanto conocidos) cuando la Distribution se crea objeto en Python, mientras que en el gráfico (modo no Eager), es posible definir distribuciones cuyas formas de evento y lote están indeterminadas hasta que se ejecuta el gráfico.

Distribuciones escalares

Como señalamos anteriormente, una Distribution objeto ha definido formas de eventos y de lote. Comenzaremos con una utilidad para describir distribuciones:

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

En esta sección vamos a explorar las distribuciones escalares: distribuciones con una forma caso de [] . Un ejemplo típico es la distribución de Poisson, especificado por una 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)

La distribución de Poisson es una distribución escalar, por lo que su forma evento es siempre [] . Si especificamos más tasas, estas se muestran en forma de lote. El par final de ejemplos es interesante: solo hay una tasa única, pero debido a que esa tasa está incrustada en una matriz numpy con una forma no vacía, esa forma se convierte en la forma del lote.

La distribución normal estándar también es escalar. Es la forma evento es [] , al igual que para la de Poisson, pero vamos a jugar con él para ver nuestro primer ejemplo de la radiodifusión. La normal se especifica utilizando loc y 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)

El ejemplo anterior es interesante la Broadcasting Scale distribución. El loc parámetro tiene forma [4] , y la scale de parámetros tiene forma [2, 1] . El uso de reglas de radiodifusión NumPy , la forma de lote es [2, 4] . Una forma equivalente (pero menos elegante y no se recomienda-) para definir la "Broadcasting Scale" distribución sería la siguiente:

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 qué la notación de transmisión es útil, aunque también es una fuente de dolores de cabeza y errores.

Distribuciones escalares de muestreo

Hay dos cosas principales que podemos hacer con distribuciones: podemos sample de ellos y podemos calcular log_prob s. Exploremos primero el muestreo. La regla básica es que cuando nos muestra de una distribución, el tensor resultante tiene la forma [sample_shape, batch_shape, event_shape] , donde batch_shape y event_shape son proporcionados por la Distribution objeto, y sample_shape es proporcionado por la llamada a la sample . Para distribuciones escalares, event_shape = [] , por lo que el Tensor de regresar de muestra tendrá forma [sample_shape, batch_shape] . Vamos a intentarlo:

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)

Eso es todo lo que hay que decir acerca sample : tensores de muestra devueltos tienen forma [sample_shape, batch_shape, event_shape] .

Computar log_prob Para escalares Distribuciones

Ahora vamos a echar un vistazo a log_prob , que es algo más complicado. log_prob toma como entrada un tensor (no vacío) que representa la ubicación (s) en el que para calcular el log_prob para la distribución. En el caso más sencillo, este tensor tendrá una forma de la forma [sample_shape, batch_shape, event_shape] , donde batch_shape y event_shape partido las formas de proceso por lotes y eventos de la distribución. Recordemos una vez más que para las distribuciones escalares, event_shape = [] , por lo que el tensor de entrada tiene la forma [sample_shape, batch_shape] En este caso, tenemos que volver un tensor de la 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 cómo en el primer ejemplo, la entrada y salida tienen forma [2, 3] y en el segundo ejemplo que tienen forma [1, 1, 2, 3] .

Eso sería todo lo que habría que decir, si no fuera por la transmisión. Estas son las reglas una vez que tengamos en cuenta la transmisión. Lo describimos con total generalidad y notamos simplificaciones para distribuciones escalares:

  1. Definir n = len(batch_shape) + len(event_shape) . (Para distribuciones escalares, len(event_shape)=0 ).
  2. Si el tensor de entrada t tiene menos de n dimensiones, cojín su forma mediante la adición de dimensiones de tamaño 1 a la izquierda hasta que tiene exactamente n dimensiones. Llamar el tensor resultante t' .
  3. Broadcast las n dimensiones de la derecha de t' contra el [batch_shape, event_shape] de la distribución que está calculando una log_prob para. En más detalle, porque las dimensiones donde t' ya se corresponde con la distribución, no hacer nada, y por las dimensiones donde t' tiene un producto único, la réplica que Singleton el número apropiado de veces. Cualquier otra situación es un error. (Para distribuciones escalares, sólo transmitimos contra batch_shape , ya event_shape = [] ).
  4. Ahora estamos finalmente capaz de calcular el log_prob . El tensor resultante tendrá forma [sample_shape, batch_shape] , donde sample_shape se define como cualquier dimensión de t o t' a la izquierda de los n -rightmost dimensiones: sample_shape = shape(t)[:-n] .

Esto podría ser un desastre si no sabe lo que significa, así que trabajemos con algunos ejemplos:

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

El tensor [10.] (con forma de [1] ) se transmite a través de la batch_shape de 3, por lo que se evalúa la probabilidad de registro de los tres Poissons' en el valor 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)>

En el ejemplo anterior, el tensor de entrada tiene la forma [2, 2, 1] , mientras que el objeto distribuciones tiene una forma lote de 3. Así que para cada uno de los [2, 2] dimensiones de la muestra, el valor único previsto consigue broadcats a cada de los tres Poisson.

Una manera posiblemente útil pensar en él: porque three_poissons tiene batch_shape = [2, 3] , una llamada a log_prob debe tener un tensor cuya dimensión última es o bien 1 o 3; cualquier otra cosa es un error. (Las normas de radiodifusión numpy tratan el caso especial de un escalar como totalmente equivalente a un tensor de la forma de [1] ).

La prueba de dejar que nuestras chuletas jugando con la más compleja distribución de Poisson con 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)>

Los ejemplos anteriores involucraron la transmisión sobre el lote, pero la forma de la muestra estaba vacía. Supongamos que tenemos una colección de valores y queremos obtener la probabilidad logarítmica de cada valor en cada punto del lote. Podríamos hacerlo 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)>

O podríamos dejar que la radiodifusión maneje la última dimensión del 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)>

También podemos (quizás algo menos naturalmente) dejar que la transmisión maneje solo la primera dimensión del 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)>

O podríamos dejar que la difusión de manejar ambos lotes dimensiones:

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

Lo anterior funcionó bien cuando solo teníamos dos valores que queríamos, pero supongamos que tenemos una lista larga de valores que queremos evaluar en cada punto de lote. Para eso, la siguiente notación, que agrega dimensiones adicionales de tamaño 1 al lado derecho de la forma, es extremadamente ú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)>

Este es un ejemplo de notación de rebanada strided , que vale la pena conocer.

Volviendo a three_poissons esté completo, el mismo ejemplo se ve así:

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

Distribuciones multivariadas

Pasamos ahora a las distribuciones multivariadas, que tienen una forma de evento no vacía. Veamos las distribuciones multinomiales.

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)

Nota cómo en los últimos tres ejemplos, el batch_shape es siempre [2] , pero podemos utilizar la difusión de tener ya sea una responsabilidad compartida total_count o un compartidas probs (o ninguno), ya que bajo el capó que se emiten a tener la misma forma.

El muestreo es sencillo, dado lo que ya 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 logarítmicas es igualmente sencillo. Trabajemos un ejemplo con distribuciones normales multivariadas diagonales. (Los multinomios no son muy amigables con la transmisión, ya que las restricciones en los conteos y las probabilidades significan que la transmisión a menudo producirá valores inadmisibles). Usaremos un lote de 2 distribuciones tridimensionales con la misma media pero diferentes escalas (desviaciones estándar):

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>

(Tenga en cuenta que aunque utilizamos las distribuciones en las escalas eran múltiplos de la identidad, esto no es una restricción, Podríamos pasar scale en lugar de scale_identity_multiplier ).

Ahora evaluemos la probabilidad logarítmica de cada punto de lote en su media y en una media desplazada:

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

Exactamente lo que es equivalente, podemos utilizar https://www.tensorflow.org/api_docs/cc/class/tensorflow/ops/strided-slice para insertar una forma adicional = 1 dimensión en el medio de una 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 otro lado, si no lo hacemos insertar la dimensión extra, pasamos [1., 2., 3.] al primer punto de lote y [3., 4., 5.] a la segunda:

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 manipulación de formas

El biyector de remodelación

El Reshape bijector puede utilizarse para remodelar la event_shape de una distribución. Veamos un ejemplo:

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>

Hemos creado un polinomio con una forma caso de [6] . El Reshape Bijector nos permite tratar esta como una distribución con una forma caso de [2, 3] .

A Bijector representa un diferenciable, uno-a-una función en un abierto de \({\mathbb R}^n\). Bijectors se utilizan en conjunción con TransformedDistribution , que modela una distribución \(p(y)\) en términos de una distribución de base \(p(x)\) y una Bijector que representa \(Y = g(X)\). Veámoslo en acción:

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>

Esto es lo único que el Reshape bijector puede hacer: no se puede convertir dimensiones de eventos en dimensiones por lotes o viceversa.

La distribución independiente

El Independent de distribución se utiliza para tratar una colección de independiente, sin necesariamente idénticos (también conocido como un lote de) distribuciones como una sola distribución. De manera más concisa, Independent permite convertir dimensiones en batch_shape a dimensiones en event_shape . Lo ilustraremos con un ejemplo:

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 en esto como una matriz de monedas de dos por cinco con las probabilidades asociadas de caras. Evaluemos la probabilidad de un conjunto arbitrario particular de unos y ceros:

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 utilizar Independent de convertir esto en dos grupos de cinco "de Bernoulli" diferentes, lo cual es útil si queremos considerar una "fila" de los lanzamientos de la moneda viene en un patrón determinado como un solo 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>

Matemáticamente, estamos calculando la probabilidad logarítmica de cada "conjunto" de cinco sumando las probabilidades logarítmicas de los cinco lanzamientos de monedas "independientes" en el conjunto, que es donde la distribución recibe su nombre:

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

Podemos ir más allá y utilizar Independent para crear una distribución donde los eventos individuales son un conjunto de dos por cinco de Bernoulli:

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>

Vale la pena señalar que desde la perspectiva de sample , utilizando Independent no cambia 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 un ejercicio de separación para el lector, se sugiere considerar las diferencias y similitudes entre un lote vector de Normal distribuciones y una MultivariateNormalDiag distribución desde una perspectiva de probabilidad de muestreo y registro. ¿Cómo podemos utilizar Independent para construir una MultivariateNormalDiag de un lote de Normal s? (Tenga en cuenta que MultivariateNormalDiag no se llevan a la práctica esta manera).