TensorFlow 發佈:簡易入門

在 TensorFlow.org 上檢視 在 Google Colab 中執行 在 GitHub 上檢視原始碼 下載筆記本

在本筆記本中,我們將探索 TensorFlow 發佈 (簡稱 TFD)。本筆記本的目標是讓您輕鬆上手,包括瞭解 TFD 對張量形狀的處理方式。本筆記本嘗試先呈現範例,再呈現抽象概念。我們會先介紹執行操作的標準簡易方法,並將最通用的抽象檢視保留到最後。如果您是偏好更抽象和參考樣式教學課程的類型,請查看瞭解 TensorFlow 發佈形狀。如果您對此處的資料有任何疑問,請隨時聯絡 (或加入)TensorFlow Probability 郵寄清單。我們很樂意提供協助。

在開始之前,我們需要匯入適當的程式庫。我們的整體程式庫是 tensorflow_probability。依照慣例,我們通常將發佈程式庫稱為 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_shape2,表示此分佈的基本事件空間是二維的。

取樣的運作方式與之前相同

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 提供多種建立多變量常態分佈的方法,包括完整共變異數規格 (由共變異數矩陣的 Cholesky 因子參數化),我們在此處使用此方法。

covariance_matrix = [[1., .7], [.7, 1.]]
nd = tfd.MultivariateNormalTriL(
    loc = [0., 5], scale_tril = tf.linalg.cholesky(covariance_matrix))
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>

務必清楚瞭解這表示什麼。上述呼叫定義了三個獨立的白努利分佈,它們恰好包含在同一個 Python Distribution 物件中。這三個分佈無法個別操作。請注意 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 迴圈) 逐一迭代來執行相同的計算。但是,擁有一組 (可能很大的) 相同參數化的分佈非常常見,而且盡可能使用向量化計算是能夠使用硬體加速器執行快速計算的關鍵要素。

使用 Independent 將批次聚合到事件

在上一節中,我們建立了 b3,這是一個代表三次擲硬幣的單一 Distribution 物件。如果我們對向量 \(v\) 呼叫 b3.prob,則第 \(i\) 個項目是第 \(i\) 個硬幣取值 \(v[i]\) 的機率。

假設我們想要指定相同基礎族系的獨立隨機變數的「聯合」分佈。這是一個不同的數學物件,因為對於這個新分佈,向量 \(v\) 上的 prob 將傳回一個單一值,表示整組硬幣與向量 \(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.sampleb3_joint.sample 具有不同的概念實作,但輸出無法區分:一批獨立分佈與使用 Independent 從批次建立的單一分佈之間的差異在計算機率時顯示,而不是在取樣時顯示。
  • MultivariateNormalDiag 可以使用純量 NormalIndependent 分佈輕鬆實作 (它實際上並非以這種方式實作,但可以這樣實作)。

多變量分佈批次

讓我們建立一批三個完整共變異數二維多變量常態分佈

covariance_matrix = [[[1., .1], [.1, 1.]], 
                      [[1., .3], [.3, 1.]],
                      [[1., .5], [.5, 1.]]]
nd_batch = tfd.MultivariateNormalTriL(
    loc = [[0., 0.], [1., 1.], [2., 2.]],
    scale_tril = tf.linalg.cholesky(covariance_matrix))
nd_batch
<tfp.distributions.MultivariateNormalTriL 'MultivariateNormalTriL' 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 成為事件形狀的串連

  • 對於單變量純量分佈 nbBE = ().
  • 對於二維多變量常態分佈 ndBE = (2).
  • 對於 b3b3_jointBE = (3).
  • 對於多變量常態分佈批次 ndbBE = (3, 2).

我們目前為止使用的「評估規則」是

  • 不使用引數取樣會傳回形狀為 BE 的張量;使用純量 n 取樣會傳回「n 乘 BE」張量。
  • problog_prob 採用形狀為 BE 的張量,並傳回形狀為 B 的結果。

problog_prob 的實際「評估規則」更複雜,它以提供潛在能力和速度的方式,但也帶來複雜性和挑戰。實際規則 (基本上) 是 log_prob 的引數必須廣播BE;任何「額外」維度都會保留在輸出中。

讓我們探索其含義。對於單變量常態分佈 nBE = (),因此 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「預期」形狀為 (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])

不幸的是,這會產生錯誤,並顯示冗長且難以閱讀的堆疊追蹤。b3BE = (3),因此我們必須將可廣播至 (3,) 的內容傳遞給 b3.prob[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_shapebatch_shapesample_shape 可以是任意等級 (在本教學課程中,它們始終是純量或等級 1)。這增加了功能,但再次可能導致程式設計挑戰,尤其是在涉及廣播時。如需深入瞭解形狀操作,請參閱瞭解 TensorFlow 發佈形狀
  • TFP 包含稱為 Bijectors 的強大抽象概念,它與 TransformedDistribution 結合使用,產生一種彈性、組合式的方式,可以輕鬆建立新的分佈,這些分佈是現有分佈的可逆轉換。我們將嘗試儘快撰寫關於此主題的教學課程,但在此期間,請查看文件