我有点惊讶,没有人提到给定警告的主要(也是唯一)原因!看起来,该代码应该实现 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条件下的可训练权重,所以肯定有一些梯度?!”;但是,事实并非如此,让我澄清一下混乱:
(注意:从这里开始,我将阈值表示为sigma,就像在等式中一样)。
好吧!我们在实施中找到了错误背后的原因。我们能解决这个问题吗?当然!这是更新的工作实现:
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 值没有意义(即当 sigma 为负时,(-sigma, sigma) 的范围定义不明确)。
考虑到前一点,我们注意正确初始化 sigma 值(即设置为小的非负值)。
另外,请不要做0.0 * inputs之类的事情!它是多余的(而且有点奇怪),它相当于0.0;并且两者都有0.0的梯度(w.r.t.inputs)。将零与张量相乘不会添加任何内容或解决任何现有问题,至少在这种情况下不会!
现在,让我们测试一下它是如何工作的。我们编写了一些辅助函数来生成基于固定 sigma 值的训练数据,并创建一个包含单个 BumpLayer 输入形状为 (1,) 的模型。让我们看看它是否可以学习用于生成训练数据的 sigma 值:
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
是的,它可以学习用于生成数据的 sigma 的值!但是,它是否保证它实际上适用于所有不同的训练数据值和 sigma 的初始化?答案是不!实际上,你有可能运行上面的代码,得到nan作为训练后的sigma值,或者inf作为损失值!所以有什么问题?为什么会产生这个nan 或inf 值?让我们在下面讨论它......
处理数值稳定性
在构建机器学习模型并使用基于梯度的优化方法对其进行训练时,需要考虑的重要事项之一是模型中操作和计算的数值稳定性。当一个操作或其梯度生成极大或极小的值时,几乎可以肯定它会破坏训练过程(例如,这是在 CNN 中对图像像素值进行归一化以防止此问题的原因之一)。
那么,让我们来看看这个广义的凹凸函数(现在让我们放弃阈值处理)。很明显,该函数在x^2 = sigma(即x = sqrt(sigma) 或x=-sqrt(sigma))处具有奇点(即未定义函数或其梯度的点)。下面的动画图显示了凹凸函数(红色实线),它的导数 w.r.t. sigma(绿色虚线)和x=sigma 和x=-sigma 线(两条垂直的蓝色虚线),当 sigma 从零开始并增加到 5 时:
如您所见,在奇点区域附近,函数对于 sigma 的所有值都表现不佳,因为函数及其导数在这些区域都取非常大的值。因此,给定这些区域的特定 sigma 值的输入值,会生成爆炸输出和梯度值,因此会出现inf 损失值问题。
更进一步,tf.where 的问题行为会导致层中 sigma 变量的nan 值问题:令人惊讶的是,如果tf.where 的非活动分支中产生的值非常大或@ 987654362@,它与凹凸函数导致非常大或inf 梯度值,那么tf.where 的梯度将为nan,尽管inf 处于非活动状态分支,甚至没有被选中(参见Github issue,它正是讨论了这个)!!
那么tf.where 的这种行为有什么解决方法吗?是的,实际上有一个技巧可以以某种方式解决这个问题,在this answer 中进行了解释:基本上我们可以使用额外的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
)
应用此修复程序将完全解决 sigma 的nan 值问题。让我们根据使用不同 sigma 值生成的训练数据值来评估它,看看它的表现如何:
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
它可以正确学习所有的 sigma 值!那很好。该解决方法奏效了!不过,有一个警告:如果该层的输入值大于 -1 且小于 1,则保证可以正常工作并学习任何 sigma 值(即这是我们generate_data 函数的默认情况);否则,如果输入值的幅度大于 1,则可能会出现inf 损失值的问题(请参见下面的第 1 点和第 2 点)。
以下是一些供古玩和感兴趣的人思考的食物:
刚才提到,如果该层的输入值大于1或小于-1,则可能会导致问题。你能争论为什么会这样吗? (提示:使用上面的动画图并考虑sigma > 1 和输入值介于sqrt(sigma) 和sigma 之间(或-sigma 和-sqrt(sigma) 之间的情况。)
您能否为第 1 点中的问题提供解决方案,即该层可以适用于所有输入值? (提示:就像tf.where 的解决方法一样,考虑如何进一步过滤掉可以应用凹凸函数并产生爆炸输出/梯度的不安全值。)
1234563和 1? (提示:作为一种解决方案,有一个常用的激活函数,它产生的值恰好在这个范围内,并且可以潜在地用作该层之前的层的激活函数。) p>
如果你看一下最后的代码sn-p,你会发现我们使用了epochs=3 if s < 1 else (5 if s < 5 else 10)。这是为什么?为什么需要学习更多的 sigma 值? (提示:再次使用动画图,并考虑在 -1 和 1 之间的输入值随着 sigma 值增加的函数的导数。它们的大小是多少?)
我们是否还需要检查生成的训练数据中是否存在任何nan、inf 或非常大的y 值并将它们过滤掉? (提示:是的,如果sigma > 1 和值范围,即min_x 和max_x,不在(-1, 1) 范围内;否则,不,没有必要!为什么会这样?留作练习!)