本文翻譯自: 《Numerical stability in TensorFlow》, 如有侵權請聯絡刪除,僅限於學術交流,請勿商用。如有謬誤,請聯絡指出。
當使用任何數值計算庫(如NumPy或TensorFlow)時,值得注意的是,編寫出正確的數學計算程式碼對於計算出正確結果並不是必須的。你同樣需要確保整個計算過程是穩定的。
讓我們從一個例子入手。小學的時候我們就知道,對於任意一個非0的數x,都有x*y/y=x
。但是讓我們在實踐中看看是否如此:
import numpy as np
x = np.float32(1)
y = np.float32(1e-50) # y would be stored as zero
z = x * y / y
print(z) # prints nan
複製程式碼
錯誤的原因是:y是float32
型別的數字,所能表示的數值太小。當y太大時會出現類似的問題:
y = np.float32(1e39) # y would be stored as inf
z = x * y / y
print(z) # prints 0
複製程式碼
float32型別可以表示的最小正值是1.4013e-45,任何低於該值的數都將儲存為零。此外,任何超過3.40282e + 38的數都將儲存為inf。
print(np.nextafter(np.float32(0), np.float32(1))) # prints 1.4013e-45
print(np.finfo(np.float32).max) # print 3.40282e+38
複製程式碼
為了保證計算的穩定性,你需要避免使用絕對值非常小或非常大的值。可能聽起來這種問題比較低階,但這些問題可能會讓程式變得難以除錯,尤其是在TensorFlow中進行梯度下降時。這是因為你不僅需要確保正向傳遞中的所有值都在資料型別的有效範圍內,而且反向傳播時同樣如此(在梯度運算期間)。
讓我們看一個真實的例子。我們想要在logits向量上計算其softmax的值。一個too navie的實現方式就像這樣:
import tensorflow as tf
def unstable_softmax(logits):
exp = tf.exp(logits)
return exp / tf.reduce_sum(exp)
tf.Session().run(unstable_softmax([1000., 0.])) # prints [ nan, 0.]
複製程式碼
注意一下,計算相對較小的數的對數,將會得到一個超出float32範圍的大數。對於我們的naive softmax實現來說,最大的有效對數是ln(3.40282e+38) = 88.7,如果超過這個值,就會導致nan結果。
但是我們怎樣才能使它更穩定呢?解決辦法相當簡單。很容易看到,exp (x - c) /∑exp (x - c) = exp (x) /∑exp (x)。因此,我們可以從邏輯中減去任何常數,結果還是一樣的。我們選擇這個常數作為邏輯的最大值。這樣,指數函式的定義域將被限制為[-inf, 0],因此其範圍將為[0.0,1.0],這是可取的:
import tensorflow as tf
def softmax(logits):
exp = tf.exp(logits - tf.reduce_max(logits))
return exp / tf.reduce_sum(exp)
tf.Session().run(softmax([1000., 0.])) # prints [ 1., 0.]
複製程式碼
讓我們來看一個複雜點案例。假設我們有一個分類問題,並且使用softmax函式從我們的邏輯中產生概率。然後我們定義一個真實值和預測值之間的交叉熵損失函式。回想一下,交叉熵的分類分佈可以簡單地定義為xe(p, q) = -∑ p_i log(q_i)
,所以一個簡單的交叉熵程式碼是這樣的:
def unstable_softmax_cross_entropy(labels, logits):
logits = tf.log(softmax(logits))
return -tf.reduce_sum(labels * logits)
labels = tf.constant([0.5, 0.5])
logits = tf.constant([1000., 0.])
xe = unstable_softmax_cross_entropy(labels, logits)
print(tf.Session().run(xe)) # prints inf
複製程式碼
請注意,在這個程式碼中,當softmax輸出接近0時,輸出將會接近無窮,這將導致我們的計算不穩定。我們可以通過擴充套件softmax函式並做一些簡化來重寫它:
def softmax_cross_entropy(labels, logits):
scaled_logits = logits - tf.reduce_max(logits)
normalized_logits = scaled_logits - tf.reduce_logsumexp(scaled_logits)
return -tf.reduce_sum(labels * normalized_logits)
labels = tf.constant([0.5, 0.5])
logits = tf.constant([1000., 0.])
xe = softmax_cross_entropy(labels, logits)
print(tf.Session().run(xe)) # prints 500.0
複製程式碼
我們也可以驗證梯度計算也是正確的:
g = tf.gradients(xe, logits)
print(tf.Session().run(g)) # prints [0.5, -0.5]
複製程式碼
再次提醒一下,在做梯度下降的時候務必格外小心,以確保函式以及每一層的梯度值都在一個有效的範圍內。指數函式和對數函式在使用時也要格外的注意,因為它們可以將小數字對映為大數字,反之亦然。