Я немного удивлен, что никто не упомянул основную (и единственную) причину данного предупреждения! Как представляется, этот код должен реализовывать обобщенный вариант функции Bump; однако, просто взгляните на функции, реализованные снова:
def f_True(x):
# Compute Bump Function
bump_value = 1-tf.math.pow(x,2)
bump_value = -tf.math.pow(bump_value,-1)
bump_value = tf.math.exp(bump_value)
return(bump_value)
def f_False(x):
# Compute Bump Function
x_out = 0*x
return(x_out)
Ошибка очевидна: в этих функциях не используется обучаемый вес слоя! Так что есть не удивительно, что вы получаете сообщение о том, что для этого не существует градиента: вы вообще его не используете, поэтому нет градиента для его обновления! Скорее, это именно оригинальная функция Bump (то есть без тренируемого веса).
Но вы можете сказать, что: «по крайней мере, я использовал тренируемый вес при условии tf.cond
, поэтому необходимо быть какие-то градиенты ?! "; однако, это не так, и позвольте мне прояснить путаницу:
Прежде всего, как вы заметили, нас интересует поэлементное обусловливание. Таким образом, вместо tf.cond
вам нужно использовать tf.where
.
Другое неправильное представление состоит в том, чтобы утверждать, что, поскольку tf.less
используется в качестве условия и поскольку оно не дифференцируемо, т.е. он не имеет градиента по отношению к своим входам (что верно: для функции с логическим выводом по сравнению с ее действительными значениями нет определенного градиента), то это приводит к выдаче предупреждения!
- Это просто неправильно! Производная здесь будет взята из вывода слоя по отношению к обучаемому весу, а условие выбора НЕ присутствует в выходных данных. Скорее, это просто логический тензор, который определяет выходную ветвь, которая будет выбрана. Это оно! Производная условия не берется и никогда не понадобится. Так что это не причина для данного предупреждения; причина только в том, что я упомянул выше: никакой вклад обучаемого веса в вывод слоя. (Примечание: если пункт об условии вас немного удивляет, подумайте о простом примере: функция ReLU, которая определяется как
relu(x) = 0 if x < 0 else x
. Если производная условия, т.е. x < 0
, считается / необходима, который не существует, то мы не сможем использовать ReLU в наших моделях и обучать их, используя методы градиентной оптимизации вообще!)
(Примечание: начиная с этого момента, я бы обозначил и обозначил пороговое значение как сигма , как в уравнении).
Хорошо! Мы нашли причину ошибки в реализации. Можем ли мы это исправить? Конечно! Вот обновленная рабочая реализация:
import tensorflow as tf
from tensorflow.keras.initializers import RandomUniform
from tensorflow.keras.constraints import NonNeg
class BumpLayer(tf.keras.layers.Layer):
def __init__(self, *args, **kwargs):
super(BumpLayer, self).__init__(*args, **kwargs)
def build(self, input_shape):
self.sigma = self.add_weight(
name='sigma',
shape=[1],
initializer=RandomUniform(minval=0.0, maxval=0.1),
trainable=True,
constraint=tf.keras.constraints.NonNeg()
)
super().build(input_shape)
def bump_function(self, x):
return tf.math.exp(-self.sigma / (self.sigma - tf.math.pow(x, 2)))
def call(self, inputs):
greater = tf.math.greater(inputs, -self.sigma)
less = tf.math.less(inputs, self.sigma)
condition = tf.logical_and(greater, less)
output = tf.where(
condition,
self.bump_function(inputs),
0.0
)
return output
Несколько замечаний относительно этой реализации:
Мы заменили tf.cond
на tf.where
, чтобы сделать элемент условное преобразование.
Далее, как вы можете видеть, в отличие от вашей реализации, которая проверяла только одну сторону неравенства, мы используем tf.math.less
, tf.math.greater
, а также tf.logical_and
чтобы выяснить, имеют ли входные значения величины меньше sigma
(альтернативно, мы могли бы сделать это, используя только tf.math.abs
и tf.math.less
; без разницы!). И давайте повторим это: использование функций логического вывода таким способом не вызывает никаких проблем и не имеет ничего общего с производными / градиентами.
Мы также используем ограничение неотрицательности на значении сигмы, изученной слоем. Почему? Поскольку значения сигмы меньше нуля не имеют смысла (т. Е. Диапазон (-sigma, sigma)
плохо определен, когда сигма отрицательна).
И, учитывая предыдущий пункт, мы позаботимся об инициализации правильное значение сигмы (то есть небольшое неотрицательное значение).
А также, пожалуйста, не делайте такие вещи, как 0.0 * inputs
! Это избыточно (и немного странно) и эквивалентно 0.0
; и оба имеют градиент 0.0
(относительно inputs
). Умножение нуля на тензор не добавляет ничего и не решает существующую проблему, по крайней мере, в этом случае!
Теперь давайте проверим, как это работает. Мы пишем несколько вспомогательных функций для генерации обучающих данных на основе фиксированного значения сигмы, а также для создания модели, содержащей один BumpLayer
с формой ввода (1,)
. Давайте посмотрим, сможет ли он узнать значение сигмы, которое используется для генерации обучающих данных:
import numpy as np
def generate_data(sigma, min_x=-1, max_x=1, shape=(100000,1)):
assert sigma >= 0, 'Sigma should be non-negative!'
x = np.random.uniform(min_x, max_x, size=shape)
xp2 = np.power(x, 2)
condition = np.logical_and(x < sigma, x > -sigma)
y = np.where(condition, np.exp(-sigma / (sigma - xp2)), 0.0)
dy = np.where(condition, xp2 * y / np.power((sigma - xp2), 2), 0)
return x, y, dy
def make_model(input_shape=(1,)):
model = tf.keras.Sequential()
model.add(BumpLayer(input_shape=input_shape))
model.compile(loss='mse', optimizer='adam')
return model
# Generate training data using a fixed sigma value.
sigma = 0.5
x, y, _ = generate_data(sigma=sigma, min_x=-0.1, max_x=0.1)
model = make_model()
# Store initial value of sigma, so that it could be compared after training.
sigma_before = model.layers[0].get_weights()[0][0]
model.fit(x, y, epochs=5)
print('Sigma before training:', sigma_before)
print('Sigma after training:', model.layers[0].get_weights()[0][0])
print('Sigma used for generating data:', sigma)
# Sigma before training: 0.08271004
# Sigma after training: 0.5000002
# Sigma used for generating data: 0.5
Да, он может узнать значение сигмы, используемое для генерации данных! Но гарантируется ли, что он действительно работает для всех различных значений обучающих данных и инициализации сигмы? Ответ - нет! На самом деле, возможно, что вы запустили приведенный выше код и получили nan
в качестве значения сигмы после тренировки или inf
в качестве значения потери! Так в чем проблема? Почему могут быть получены значения nan
или inf
? Давайте обсудим это ниже ...
Работа с числовой стабильностью
Одна из важных вещей, которую следует учитывать при построении модели машинного обучения и использовании методов оптимизации на основе градиента для их обучения , числовая устойчивость операций и расчетов в модели. Когда операция или ее градиент генерируют очень большие или маленькие значения, почти наверняка это нарушит процесс обучения (например, это одна из причин нормализации значений пикселей изображения в CNN для предотвращения этой проблемы).
Итак, давайте посмотрим на эту обобщенную функцию bump (и пока отбросим пороговое значение). Очевидно, что эта функция имеет особенности (т. Е. Точки, в которых функция или ее градиент не определены) в x^2 = sigma
(т.е. когда x = sqrt(sigma)
или x=-sqrt(sigma)
). На приведенной ниже анимированной диаграмме показана функция выпуклости (solid красная линия), ее производные по сигме (пунктирная зеленая линия) и x=sigma
и x=-sigma
(две вертикальные пунктирные синие линии), когда сигма начинается с нуля и увеличивается до 5:
![Generalized bump function when sigma starts from zero and is increased to five.](https://i.stack.imgur.com/PACDf.gif)
Как вы можете видеть, в области сингулярностей функция не ведет себя хорошо для всех значений сигма, в ощущение, что и функция, и ее производная принимают чрезвычайно большие значения в этих областях. Таким образом, при заданном входном значении в этих регионах для определенного значения сигмы будут сгенерированы взрывные выходные значения и значения градиента, следовательно, возникает проблема inf
значения потерь.
Еще больше, существует проблема c поведение tf.where
, которое вызывает проблему значений nan
для сигма-переменной в слое: удивительно, если полученное значение в неактивной ветви tf.where
очень велико или inf
, что с функцией bump приводит к Если значение градиента очень велико или inf
, тогда градиент tf.where
будет nan
, несмотря на то, что inf
находится в неактивной ветви и даже не выбран (см. * 1122). * Проблема Github , которая обсуждает именно это) !!
Так есть ли обходной путь для этого поведения tf.where
? Да, на самом деле есть способ как-то решить эту проблему, что объясняется в этом ответе : в основном мы можем использовать дополнительный tf.where
для предотвращения применения функции в этих регионах. Другими словами, вместо применения self.bump_function
к любому входному значению, мы фильтруем те значения, которые НЕ находятся в диапазоне (-self.sigma, self.sigma)
(т. Е. Фактический диапазон, к которому должна применяться функция), и вместо этого снабжаем функцию нулем (которое является всегда выдает безопасные значения, т. е. равно exp(-1)
):
output = tf.where(
condition,
self.bump_function(tf.where(condition, inputs, 0.0)),
0.0
)
Применение этого исправления полностью решит проблему значений nan
для сигмы. Давайте оценим его по значениям обучающих данных, сгенерированных с различными значениями сигмы, и посмотрим, как они будут работать:
true_learned_sigma = []
for s in np.arange(0.1, 10.0, 0.1):
model = make_model()
x, y, dy = generate_data(sigma=s, shape=(100000,1))
model.fit(x, y, epochs=3 if s < 1 else (5 if s < 5 else 10), verbose=False)
sigma = model.layers[0].get_weights()[0][0]
true_learned_sigma.append([s, sigma])
print(s, sigma)
# Check if the learned values of sigma
# are actually close to true values of sigma, for all the experiments.
res = np.array(true_learned_sigma)
print(np.allclose(res[:,0], res[:,1], atol=1e-2))
# True
Он может правильно выучить все значения сигмы! Это мило. Этот обходной путь работал! Хотя есть одно предостережение: это гарантированно будет работать должным образом и изучать любое сигма-значение, если входные значения для этого слоя больше -1 и меньше 1 (т.е. это случай по умолчанию нашей функции generate_data
); в противном случае все еще существует проблема потери inf
, которая может произойти, если входные значения имеют величину больше 1 (см. точки № 1 и № 2 ниже).
Вот некоторые продукты для размышления для кур ios и заинтересованного ума:
Только что было упомянуто, что если входные значения для этого слоя больше 1 или меньше, чем -1, то это может вызвать проблемы. Можете ли вы спорить, почему это так? (Подсказка: используйте приведенную выше анимированную диаграмму и рассмотрите случаи, когда sigma > 1
и входное значение находится между sqrt(sigma)
и sigma
(или между -sigma
и -sqrt(sigma)
.)
Можете ли вы предоставить исправление для проблемы в точке # 1, т.е. чтобы слой мог работать для всех входных значений? (Подсказка: как обходной путь для tf.where
, подумайте о том, как Вы можете дополнительно отфильтровать небезопасные значения , к которым может быть применена функция bump, и произвести взрывной вывод / градиент.)
Однако, если вы не заинтересованы в устранении этой проблемы и хотели бы использовать этот слой в модели в том виде, в каком она есть сейчас, тогда как бы вы гарантировали, что входные значения для этого слоя всегда будут между -1 и 1? (Подсказка: как В одном из решений существует широко используемая функция активации, которая выдает значения именно в этом диапазоне и может потенциально использоваться в качестве функции активации слоя, находящегося перед этим слоем.)
Если вы посмотрите на последний фрагмент кода, вы увидите, что мы использовали epochs=3 if s < 1 else (5 if s < 5 else 10)
. Это почему? Почему большие значения сигмы нуждаются в изучении большего количества эпох? (Подсказка: снова используйте анимированную диаграмму и рассматривайте производную функции для входных значений от -1 до 1 при увеличении значения сигмы. Какова их величина?)
Нужно ли проверять сгенерированные обучающие данные для любых nan
, inf
или очень больших значений y
и отфильтровывать их? (Подсказка: да, если sigma > 1
и диапазон значений, то есть min_x
и max_x
, выходят за пределы (-1, 1)
; в противном случае нет, это не нужно! Почему это так? Оставлено как упражнение!)