在 Keras/Tensorflow 中实现可训练的广义 Bump 函数层

Implementing a trainable generalized Bump function layer in Keras/Tensorflow

我正在尝试编写 Bump function 的以下变体,应用组件方面:

,

其中σ是可训练的;但它不起作用(下面报告的错误)。


我的尝试:

这是我到目前为止编写的代码(如果有帮助的话)。假设我有两个函数(例如):

  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)

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):
        self.threshold_level = self.add_weight(name='threshlevel',
                                    shape=[1],
                                    initializer='GlorotUniform',
                                    trainable=True)

    def call(self, input):
        # Determine Thresholding Logic
        The_Logic = tf.math.less(input,self.threshold_level)
        # Apply Logic
        output_step_3 = tf.cond(The_Logic, 
                                lambda: f_True(input),
                                lambda: f_False(input))
        return output_step_3

错误报告:

    Train on 100 samples
Epoch 1/10
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
WARNING:tensorflow:Gradients do not exist for variables ['reconfiguration_unit_steps_3_3/threshlevel:0'] when minimizing the loss.
 32/100 [========>.....................] - ETA: 3s

...

tensorflow:Gradients do not exist for variables 

此外,它似乎没有按组件应用(除了不可训练的问题)。可能是什么问题?

不幸的是,检查 x 是否在 (-σ, σ) 内的操作是不可微分的,因此无法通过任何梯度下降方法学习 σ。具体来说,不可能计算关于 self.threshold_level 的梯度,因为 tf.math.less 对于条件是不可微的。

关于逐元素条件,您可以根据健康)状况。例如:

output_step_3 = tf.where(The_Logic, f_True(input), f_False(input))

注意: 我根据提供的代码回答,其中 self.threshold_level 未在 f_Truef_False 中使用。如果 self.threshold_level 在提供的公式中用于这些函数,则该函数当然可以相对于 self.threshold_level.

微分

2020 年 4 月 19 日更新:感谢@今天的澄清

我建议您尝试使用正态分布而不是凹凸分布。 在我这里的测试中,这个 bump 函数表现不佳(我找不到错误但不要丢弃它,但我的图表显示两个非常尖锐的颠簸,这对网络不利)

使用正态分布,您会得到一个规则且可区分的凸起,您可以控制其高度、宽度和中心。

所以,你可以试试这个功能:

y = a * exp ( - b * (x - c)²)

在一些图表中尝试它,看看它的行为如何。

为此:

class trainable_bump_layer(tf.keras.layers.Layer):

    def __init__(self, *args, **kwargs):
        super(trainable_bump_layer, self).__init__(*args, **kwargs)

    def build(self, input_shape):

        #suggested shape (has a different kernel for each input feature/channel)
        shape = tuple(1 for _ in input_shape[:-1]) + input_shape[-1:]

        #for your desired shape of only 1:
        shape = tuple(1 for _ in input_shape) #all ones

        #height
        self.kernel_a = self.add_weight(name='kernel_a ',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #inverse width
        self.kernel_b = self.add_weight(name='kernel_b',
                                    shape=shape
                                    initializer='ones',
                                    trainable=True)

        #center
        self.kernel_c = self.add_weight(name='kernel_c',
                                    shape=shape
                                    initializer='zeros',
                                    trainable=True)

    def call(self, input):
        exp_arg = - self.kernel_b * K.square(input - self.kernel_c)
        return self.kernel_a * K.exp(exp_arg)

我有点惊讶没有人提到给定警告的主要原因(也是唯一的)!看起来,该代码应该实现 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 函数(即没有可训练的权重)。

但是,你可能会说:"at least, I used the trainable weight in the condition of tf.cond, so there must be some gradients?!";然而,事实并非如此,让我来澄清一下:

  • 首先,正如您也注意到的,我们对逐元素调节很感兴趣。因此,您需要使用 tf.where.

  • 而不是 tf.cond
  • 另一个误解是声称因为 tf.less 被用作条件,并且因为它是不可微的,即它相对于它的输入没有梯度(这是真的:对于具有布尔输出 w.r.t 的函数没有定义的梯度。它的实数值输入!),然后导致给定的警告!

    • 这是完全错误的!这里的导数将取自 层的输出 w.r.t 可训练权重,并且选择条件不存在于输出中。相反,它只是一个布尔张量,用于确定要选择的输出分支。而已!条件的导数不会被采用,也永远不需要。所以这不是给定警告的原因;原因仅是我上面提到的:可训练权重在层的输出中没有贡献。 (注意:如果关于条件的观点让你有点吃惊,那么想一个简单的例子:ReLU 函数,定义为 relu(x) = 0 if x < 0 else x。如果条件的导数,即 x < 0,是considered/needed,它不存在,那么我们将无法在我们的模型中使用 ReLU 并使用基于梯度的优化方法来训练它们!)

(注意:从这里开始,我会将阈值表示为 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.lesstf.math.greater 以及 tf.logical_and找出输入值的大小是否小于 sigma(或者,我们可以只使用 tf.math.abstf.math.less;没有区别!)。让我们重复一遍:以这种方式使用布尔输出函数不会导致任何问题,并且与 derivatives/gradients.

  • 无关
  • 我们还对层学习的 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作为损失值!所以有什么问题?为什么会产生这个 naninf 值?让我们在下面讨论...


处理数值稳定性

在构建机器学习模型并使用基于梯度的优化方法对其进行训练时,需要考虑的重要事项之一是模型中运算和计算的数值稳定性。当操作或其梯度生成极大或极小的值时,几乎肯定会破坏训练过程(例如,这是在 CNN 中对图像像素值进行归一化以防止出现此问题的原因之一)。

那么,让我们来看看这个通用的凹凸函数(现在让我们放弃阈值化)。很明显,此函数在 x^2 = sigma(即 x = sqrt(sigma)x=-sqrt(sigma) 时)具有奇点(即未定义函数或其梯度的点)。下面的动画图显示了凹凸函数(红色实线)及其导数 w.r.t。 sigma(绿色虚线)和 x=sigmax=-sigma 线(两条垂直的蓝色虚线),当 sigma 从零开始增加到 5 时:

如您所见,在奇点区域周围,函数对于 sigma 的所有值都表现不佳,因为函数及其导数在这些区域都取了极大的值。因此,对于特定的 sigma 值,给定这些区域的输入值,将生成爆炸式输出和梯度值,因此会出现 inf 损失值的问题。

更进一步,tf.where 存在问题行为,导致层中 sigma 变量的 nan 值问题:令人惊讶的是,如果 [= 的非活动分支中的生成值17=] 非常大或 inf,它与 bump 函数一起导致非常大或 inf 梯度值,那么 tf.where 的梯度将是 nan,尽管事实上,infinactive 分支中,甚至没有被选中(请参阅 Github issue,其中讨论了这一点)!!

那么 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
     )

应用此修复程序将完全解决 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(即,这是我们的 generate_data 函数的默认情况),这保证可以正常工作并学习任何 sigma 值;否则,仍然存在 inf 损失值的问题,如果输入值的幅度大于 1,则可能会发生这种情况(请参见下面的点 #1 和 #2)。


这里有一些值得好奇和感兴趣的人思考的食物:

  1. 刚刚提到如果这一层的输入值大于1或者小于-1,那么可能会出问题。你能争论为什么会这样吗? (提示:使用上面的动画图并考虑 sigma > 1 且输入值介于 sqrt(sigma)sigma 之间(或介于 -sigma-sqrt(sigma).)

  2. 您能否针对第 1 点中的问题提供解决方案,即该层可以适用于所有输入值? (提示:就像 tf.where 的变通方法一样,考虑如何进一步过滤掉 不安全值 可以应用 bump 函数并生成爆炸output/gradient.)

  3. 但是,如果你不想解决这个问题,并且想在模型中使用这个层,那么你如何保证这个层的输入值是总是在 -1 和 1 之间? (提示:作为一种解决方案,有一个常用的激活函数,它产生的值正好在这个范围内,并且可以潜在地用作该层之前的层的激活函数。)

  4. 如果你看一下最后的代码片段,你会发现我们使用了 epochs=3 if s < 1 else (5 if s < 5 else 10)。这是为什么?为什么大的 sigma 值需要更多的 epochs 来学习? (提示:再次使用动画图,并考虑随着 sigma 值的增加,输入值在 -1 和 1 之间的函数的导数。它们的大小是多少?)

  5. 我们是否还需要检查生成的训练数据是否存在任何 naninf 或极大的 y 值并将其过滤掉? (提示:是的,如果 sigma > 1 和值的范围,即 min_xmax_x,落在 (-1, 1) 之外;否则,不,没有必要!这是为什么?留作练习!)