Распределения TensorFlow: краткое введение

Посмотреть на TensorFlow.org Запускаем в Google Colab Посмотреть исходный код на GitHub Скачать блокнот

В этой записной книжке мы рассмотрим дистрибутивы TensorFlow (сокращенно TFD). Цель этой записной книжки - помочь вам плавно подняться по кривой обучения, включая понимание того, как TFD обрабатывает тензорные формы. В этом блокноте мы пытаемся представить примеры, а не абстрактные концепции. Сначала мы представим канонические простые способы решения задач и сохраним наиболее общий абстрактный вид до конца. Если вы из тех, кто предпочитает более абстрактное руководство в справочном стиле, ознакомьтесь с разделом «Общие сведения о формах распределений TensorFlow» . Если у вас есть какие-либо вопросы по материалам, представленным здесь, не стесняйтесь обращаться (или присоединяться) к списку рассылки TensorFlow Probability . Мы рады помочь.

Прежде чем мы начнем, нам нужно импортировать соответствующие библиотеки. Наша общая библиотека - tensorflow_probability . По соглашению мы обычно tfd библиотеку дистрибутивов tfd .

Tensorflow Eager - это обязательная среда выполнения для TensorFlow. В TensorFlow eager каждая операция TF немедленно оценивается и дает результат. Это контрастирует со стандартным режимом «графа» TensorFlow, в котором операции TF добавляют узлы к графу, который позже выполняется. Вся эта записная книжка написана с использованием TF Eager, хотя ни одна из представленных здесь концепций не основывается на этом, и TFP можно использовать в графическом режиме.

import collections

import tensorflow as tf
import tensorflow_probability as tfp
tfd = tfp.distributions

try:
  tf.compat.v1.enable_eager_execution()
except ValueError:
  pass

import matplotlib.pyplot as plt

Основные одномерные распределения

Давайте углубимся и создадим нормальное распределение:

n = tfd.Normal(loc=0., scale=1.)
n
<tfp.distributions.Normal 'Normal' batch_shape=[] event_shape=[] dtype=float32>

Мы можем извлечь из него образец:

n.sample()
<tf.Tensor: shape=(), dtype=float32, numpy=0.25322816>

Мы можем нарисовать несколько образцов:

n.sample(3)
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-1.4658079, -0.5653636,  0.9314412], dtype=float32)>

Мы можем оценить ошибку журнала:

n.log_prob(0.)
<tf.Tensor: shape=(), dtype=float32, numpy=-0.9189385>

Мы можем оценить несколько вероятностей журнала:

n.log_prob([0., 2., 4.])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-0.9189385, -2.9189386, -8.918939 ], dtype=float32)>

У нас широкий выбор дистрибутивов. Попробуем Бернулли:

b = tfd.Bernoulli(probs=0.7)
b
<tfp.distributions.Bernoulli 'Bernoulli' batch_shape=[] event_shape=[] dtype=int32>
b.sample()
<tf.Tensor: shape=(), dtype=int32, numpy=1>
b.sample(8)
<tf.Tensor: shape=(8,), dtype=int32, numpy=array([1, 0, 0, 0, 1, 0, 1, 0], dtype=int32)>
b.log_prob(1)
<tf.Tensor: shape=(), dtype=float32, numpy=-0.35667497>
b.log_prob([1, 0, 1, 0])
<tf.Tensor: shape=(4,), dtype=float32, numpy=array([-0.35667497, -1.2039728 , -0.35667497, -1.2039728 ], dtype=float32)>

Многовариантные распределения

Мы создадим многомерную нормаль с диагональной ковариацией:

nd = tfd.MultivariateNormalDiag(loc=[0., 10.], scale_diag=[1., 4.])
nd
<tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[] event_shape=[2] dtype=float32>

Сравнивая это с одномерной нормалью, которую мы создали ранее, чем отличается?

tfd.Normal(loc=0., scale=1.)
<tfp.distributions.Normal 'Normal' batch_shape=[] event_shape=[] dtype=float32>

Мы видим, что одномерная нормаль имеет event_shape () , что указывает на скалярное распределение. Многомерная нормаль имеет event_shape 2 , что указывает на то, что основное [пространство событий] (https://en.wikipedia.org/wiki/Event_ (вероятность_теория)) этого распределения является двумерным.

Сэмплинг работает так же, как и раньше:

nd.sample()
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-1.2489667, 15.025171 ], dtype=float32)>
nd.sample(5)
<tf.Tensor: shape=(5, 2), dtype=float32, numpy=
array([[-1.5439653 ,  8.9968405 ],
       [-0.38730723, 12.448896  ],
       [-0.8697963 ,  9.330035  ],
       [-1.2541095 , 10.268944  ],
       [ 2.3475595 , 13.184147  ]], dtype=float32)>
nd.log_prob([0., 10])
<tf.Tensor: shape=(), dtype=float32, numpy=-3.2241714>

Многомерные нормали, как правило, не имеют диагональной ковариации. TFD предлагает несколько способов создания многомерных нормалей, включая спецификацию полной ковариации, которую мы здесь используем.

nd = tfd.MultivariateNormalFullCovariance(
    loc = [0., 5], covariance_matrix = [[1., .7], [.7, 1.]])
data = nd.sample(200)
plt.scatter(data[:, 0], data[:, 1], color='blue', alpha=0.4)
plt.axis([-5, 5, 0, 10])
plt.title("Data set")
plt.show()

PNG

Множественные распределения

Наша первая раздача Бернулли представляла собой подбрасывание единственной справедливой монеты. Мы также можем создать пакет независимых распределений Бернулли, каждое со своими параметрами, в одном объекте Distribution :

b3 = tfd.Bernoulli(probs=[.3, .5, .7])
b3
<tfp.distributions.Bernoulli 'Bernoulli' batch_shape=[3] event_shape=[] dtype=int32>

Важно понимать, что это означает. Вышеупомянутый вызов определяет три независимых распределения Бернулли, которые содержатся в одном и том же объекте Distribution Python. Этими тремя дистрибутивами нельзя управлять по отдельности. Обратите внимание на то, что batch_shape имеет значение (3,) , что указывает на пакет из трех распределений, а event_shape - () , указывая на то, что отдельные распределения имеют одномерное пространство событий.

Если мы вызовем sample , мы получим выборку из всех трех:

b3.sample()
<tf.Tensor: shape=(3,), dtype=int32, numpy=array([0, 1, 1], dtype=int32)>
b3.sample(6)
<tf.Tensor: shape=(6, 3), dtype=int32, numpy=
array([[1, 0, 1],
       [0, 1, 1],
       [0, 0, 1],
       [0, 0, 1],
       [0, 0, 1],
       [0, 1, 0]], dtype=int32)>

Если мы вызываем prob (он имеет ту же семантику формы, что и log_prob ; мы используем prob с этими небольшими примерами Бернулли для ясности, хотя log_prob обычно предпочитается в приложениях), мы можем передать ему вектор и оценить вероятность того, что каждая монета даст это значение :

b3.prob([1, 1, 0])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0.29999998, 0.5       , 0.29999998], dtype=float32)>

Почему API включает форму партии? Семантически можно выполнить те же вычисления, создав список распределений и перебирая их с помощью цикла for (по крайней мере, в режиме Eager, в режиме графа TF вам понадобится цикл tf.while ). Однако наличие (потенциально большого) набора идентично параметризованных распределений является чрезвычайно распространенным явлением, и использование векторизованных вычислений, когда это возможно, является ключевым компонентом возможности выполнять быстрые вычисления с использованием аппаратных ускорителей.

Использование независимых пакетов для агрегирования событий

В предыдущем разделе мы создали b3 , единственный объект Distribution , представляющий три подбрасывания монеты. Если мы вызвали b3.prob для вектора $ v $, то запись $ i $ была вероятностью того, что $ i $ -я монета примет значение $ v [i] $.

Предположим, вместо этого мы хотели бы указать «совместное» распределение по независимым случайным величинам из того же базового семейства. Это другой объект математически, в том , что для этого нового распределения prob на вектор $ V $ будут возвращать одно значение , представляющее собой вероятность того, что весь набор монет соответствует вектору $ v $.

Как мы этого добьемся? Мы используем распределение «более высокого порядка» под названием Independent , которое принимает распределение и дает новое распределение с формой пакета, перемещенной в форму события:

b3_joint = tfd.Independent(b3, reinterpreted_batch_ndims=1)
b3_joint
<tfp.distributions.Independent 'IndependentBernoulli' batch_shape=[] event_shape=[3] dtype=int32>

Сравните форму с оригинальным b3 :

b3
<tfp.distributions.Bernoulli 'Bernoulli' batch_shape=[3] event_shape=[] dtype=int32>

Как и было обещано, мы видим, что Independent переместил фигуру пакета в форму события: b3_joint - это единичное распределение ( batch_shape = () ) в трехмерном пространстве событий ( event_shape = (3,) ).

Проверим семантику:

b3_joint.prob([1, 1, 0])
<tf.Tensor: shape=(), dtype=float32, numpy=0.044999998>

Альтернативный способ получить тот же результат - вычислить вероятности с использованием b3 и выполнить сокращение вручную путем умножения (или, в более обычном случае, когда используются логарифмические вероятности, суммирования):

tf.reduce_prod(b3.prob([1, 1, 0]))
<tf.Tensor: shape=(), dtype=float32, numpy=0.044999994>

Indpendent позволяет пользователю более явно представить желаемую концепцию. Мы считаем это чрезвычайно полезным, хотя это не является строго необходимым.

Забавные факты:

  • b3.sample и b3_joint.sample имеют разные концептуальные реализации, но неразличимы на b3_joint.sample : разница между b3_joint.sample независимых распределений и одним распределением, созданным из пакета с использованием Independent проявляется при вычислении вероятностей, а не при выборке.
  • MultivariateNormalDiag можно тривиально реализовать с использованием скалярного Normal и Independent распределений (на самом деле это не реализовано таким образом, но может быть).

Партии многомерных распределений

Давайте создадим пакет из трех полноковариационных двумерных многомерных нормалей:

nd_batch = tfd.MultivariateNormalFullCovariance(
    loc = [[0., 0.], [1., 1.], [2., 2.]],
    covariance_matrix = [[[1., .1], [.1, 1.]], 
                         [[1., .3], [.3, 1.]],
                         [[1., .5], [.5, 1.]]])
nd_batch
<tfp.distributions.MultivariateNormalFullCovariance 'MultivariateNormalFullCovariance' batch_shape=[3] event_shape=[2] dtype=float32>

Мы видим batch_shape = (3,) , поэтому есть три независимых многомерных нормали, и event_shape = (2,) , поэтому каждая многомерная нормаль является двумерной. В этом примере отдельные распределения не имеют независимых элементов.

Пробоотборные работы:

nd_batch.sample(4)
<tf.Tensor: shape=(4, 3, 2), dtype=float32, numpy=
array([[[ 0.7367498 ,  2.730996  ],
        [-0.74080074, -0.36466932],
        [ 0.6516018 ,  0.9391426 ]],

       [[ 1.038303  ,  0.12231752],
        [-0.94788766, -1.204232  ],
        [ 4.059758  ,  3.035752  ]],

       [[ 0.56903946, -0.06875849],
        [-0.35127294,  0.5311631 ],
        [ 3.4635801 ,  4.565582  ]],

       [[-0.15989424, -0.25715637],
        [ 0.87479895,  0.97391707],
        [ 0.5211419 ,  2.32108   ]]], dtype=float32)>

Поскольку batch_shape = (3,) и event_shape = (2,) , мы передаем тензор формы (3, 2) в log_prob :

nd_batch.log_prob([[0., 0.], [1., 1.], [2., 2.]])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-1.8328519, -1.7907217, -1.694036 ], dtype=float32)>

Радиовещание, иначе почему это так сбивает с толку?

Если абстрагироваться от того, что мы сделали до сих пор, у каждого распределения есть форма пакета B и форма события E Пусть BE будет конкатенацией форм событий:

  • Для одномерных скалярных распределений n и b BE = (). .
  • Для двумерных многомерных нормалей nd . BE = (2).
  • И для b3 и для b3_joint BE = (3).
  • Для пакета многомерных нормалей ndb BE = (3, 2).

«Правила оценки», которые мы использовали до сих пор:

  • Выборка без аргументов возвращает тензор с формой BE ; выборка со скаляром n возвращает тензор «n на BE ».
  • prob и log_prob принять тензор формы BE и возвращает результат формы B .

Действительное «правила оценки» для prob и log_prob более сложным, таким образом , что обеспечивает потенциальные мощности и скорости , но также сложности и проблемы. Фактическое правило (по сути) состоит в том, что аргумент log_prob должен транслироваться против BE ; любые "лишние" измерения сохраняются в выводе.

Давайте изучим последствия. Для одномерного нормального n BE = () , поэтому log_prob ожидает скаляр. Если мы передадим log_prob тензор с непустой формой, они будут отображаться как размеры пакета на выходе:

n = tfd.Normal(loc=0., scale=1.)
n
<tfp.distributions.Normal 'Normal' batch_shape=[] event_shape=[] dtype=float32>
n.log_prob(0.)
<tf.Tensor: shape=(), dtype=float32, numpy=-0.9189385>
n.log_prob([0.])
<tf.Tensor: shape=(1,), dtype=float32, numpy=array([-0.9189385], dtype=float32)>
n.log_prob([[0., 1.], [-1., 2.]])
<tf.Tensor: shape=(2, 2), dtype=float32, numpy=
array([[-0.9189385, -1.4189385],
       [-1.4189385, -2.9189386]], dtype=float32)>

Обратимся к двумерной многомерной нормали nd (параметры изменены в иллюстративных целях):

nd = tfd.MultivariateNormalDiag(loc=[0., 1.], scale_diag=[1., 1.])
nd
<tfp.distributions.MultivariateNormalDiag 'MultivariateNormalDiag' batch_shape=[] event_shape=[2] dtype=float32>

log_prob "ожидает" аргумента с shape (2,) , но примет любой аргумент, который транслируется против этой формы:

nd.log_prob([0., 0.])
<tf.Tensor: shape=(), dtype=float32, numpy=-2.337877>

Но мы можем передать "больше" примеров и сразу оценить все их log_prob :

nd.log_prob([[0., 0.],
             [1., 1.],
             [2., 2.]])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.337877 , -2.337877 , -4.3378773], dtype=float32)>

Возможно, менее привлекательно то, что мы можем транслировать по размеру события:

nd.log_prob([0.])
<tf.Tensor: shape=(), dtype=float32, numpy=-2.337877>
nd.log_prob([[0.], [1.], [2.]])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([-2.337877 , -2.337877 , -4.3378773], dtype=float32)>

Такое вещание является следствием нашей концепции «разрешать вещание, когда это возможно»; это использование несколько спорно и потенциально могут быть удалены в версии будущего в TFP.

Теперь давайте снова посмотрим на пример с тремя монетами:

b3 = tfd.Bernoulli(probs=[.3, .5, .7])

Здесь использование широковещательной рассылки для представления вероятности выпадения орла на каждую монету довольно интуитивно понятно:

b3.prob([1])
<tf.Tensor: shape=(3,), dtype=float32, numpy=array([0.29999998, 0.5       , 0.7       ], dtype=float32)>

(Сравните это с b3.prob([1., 1., 1.]) , который мы использовали бы там, где был введен b3 .)

Теперь предположим, что мы хотим знать для каждой монеты вероятность выпадения решки и вероятность выпадения решки. Мы могли представить себе попытку:

b3.log_prob([0, 1])

К сожалению, это приводит к ошибке с длинной и не очень удобочитаемой трассировкой стека. b3 имеет BE = (3) , поэтому мы должны передать b3.prob что-то транслируемое против (3,) . [0, 1] имеет форму (2) , поэтому он не транслируется и создает ошибку. Вместо этого мы должны сказать:

b3.prob([[0], [1]])
<tf.Tensor: shape=(2, 3), dtype=float32, numpy=
array([[0.7, 0.5, 0.3],
       [0.3, 0.5, 0.7]], dtype=float32)>

Почему? [[0], [1]] имеет форму (2, 1) , поэтому он транслируется против формы (3) чтобы сделать форму трансляции (2, 3) .

Широковещание - это довольно мощный инструмент: в некоторых случаях он позволяет на порядок уменьшить объем используемой памяти и часто делает код пользователя короче. Однако программировать с ним может быть непросто. Если вы вызываете log_prob и получаете сообщение об ошибке, почти всегда проблема заключается в отказе от широковещательной передачи.

Дальше

В этом руководстве мы (надеюсь) предоставили простое введение. Несколько советов для дальнейшего развития:

  • event_shape , batch_shape и sample_shape могут иметь произвольный ранг (в этом руководстве они всегда либо скалярные, либо ранг 1). Это увеличивает мощность, но опять же может привести к проблемам с программированием, особенно когда речь идет о радиовещании. Для более глубокого погружения в манипуляции с фигурами см. Общие сведения о формах распределения TensorFlow .
  • TFP включает мощную абстракцию, известную как Bijectors , которая в сочетании с TransformedDistribution дает гибкий композиционный способ простого создания новых дистрибутивов, которые являются обратимыми преобразованиями существующих дистрибутивов. Вскоре мы постараемся написать руководство по этому вопросу, а пока ознакомьтесь с документацией.