Эффективный TensorFlow Глава 12. Численная стабильность в TensorFlow

TensorFlow

Эта статья переведена с:«Численная стабильность в TensorFlow», Если есть какое-либо нарушение, пожалуйста, свяжитесь, чтобы удалить его, только для академических обменов, пожалуйста, не используйте его в коммерческих целях. Если есть какие-либо ошибки, пожалуйста, свяжитесь, чтобы указать.

При использовании любой библиотеки численных вычислений (такой как NumPy или TensorFlow) стоит отметить, что для вычисления правильного результата не обязательно писать правильный код математического расчета. Также необходимо убедиться, что весь процесс расчета стабилен.

Начнем с примера. В начальной школе мы знали, что для любого отличного от нуля числа 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 isfloat32Номер типа, значение, которое может быть представлено, слишком мало. Аналогичные проблемы возникают, когда 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 будет сохранен как инф.

print(np.nextafter(np.float32(0), np.float32(1)))  # prints 1.4013e-45
print(np.finfo(np.float32).max)  # print 3.40282e+38

Чтобы гарантировать стабильность расчета, вам нужно избегать использования очень маленьких или очень больших абсолютных значений. Это может показаться проблемой низкого уровня, но эти проблемы могут затруднить отладку программ, особенно при выполнении градиентного спуска в TensorFlow. Это связано с тем, что вам нужно не только убедиться, что все значения в прямом проходе находятся в пределах допустимого диапазона типа данных, но то же самое верно и для обратного прохода (во время операции градиента).

Давайте посмотрим на реальный пример. Мы хотим вычислить значение его softmax на векторе логитов. Слишком морской флот реализован следующим образом:

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. Для нашей наивной реализации 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]

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