本文翻译自: 《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]
再次提醒一下, 在做梯度下降的时候务必格外小心, 以确保函数以及每一层的梯度值都在一个有效的范围内。 指数函数和对数函数在使用时也要格外的注意, 因为它们可以将小数字映射为大数字, 反之亦然。