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

Posted

技术标签:

【中文标题】在 Keras/Tensorflow 中实现可训练的广义 Bump 函数层【英文标题】:Implementing a trainable generalized Bump function layer in Keras/Tensorflow 【发布时间】:2020-07-08 09:17:59 【问题描述】:

我正在尝试编写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 

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

【问题讨论】:

input 的维度是多少?是标量吗? 嗨@ProbablyAHuman,你能为你的场景提供一个最小的可重现代码并指定它到底是如何不工作的吗? @TF_Support 我添加了目标的详细信息以及错误报告... sigma 可以训练吗? 你能分享一下你想要什么的图表吗?这个图表中的内容可能会有所不同? 【参考方案1】:

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

关于逐元素条件,您可以改用tf.where 根据条件的逐组件布尔值从f_True(input)f_False(input) 中选择元素。例如:

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 日更新:感谢@today 的澄清

【讨论】:

如果数学不起作用,恐怕没有什么神奇的实现技巧可以让它可训练...... “错误消息恰恰表明了这一点 - 无法计算相对于 self.threshold_level 的梯度,因为 tf.math.less 就其输入而言不可微分。” --> 警告消息与在条件中使用tf.math.less 以及它不可区分的事实无关。条件不需要是可微的以使这项工作。错误在于根本没有使用可训练的权重来产生层的输出(即输出中没有它的踪迹)。有关更多信息,请参阅我的答案的第一部分。 同意,这不是警告信息所说的,我会更正我的措辞。但是,这一点保持不变 - 您不能通过操作来检查变量是否在特定范围内,并期望它相对于限制变量是可微的。也就是说,如果这个变量用于计算输出(我什至没有在公式中注意到,我必须承认)它当然会有梯度。【参考方案2】:

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

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

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

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)

【讨论】:

【参考方案3】:

我有点惊讶,没有人提到给定警告的主要(也是唯一)原因!看起来,该代码应该实现 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.where,而不是tf.cond

另一个误解是声称由于 tf.less 被用作条件,并且因为它不可微,即它没有关于其输入的梯度(这是真的:对于具有布尔输出 wrt 其实值输入!),然后导致给定警告!

这完全是错误的!这里的导数将取 层的输出 w.r.t 可训练权重,并且输出中不存在选择条件。相反,它只是一个布尔张量,用于确定要选择的输出分支。而已!条件的导数不被采用,也永远不需要。所以这不是给定警告的原因;原因只是我上面提到的:在层的输出中没有可训练权重的贡献。 (注意:如果关于条件的观点让你有点惊讶,那么考虑一个简单的例子:ReLU 函数,定义为relu(x) = 0 if x < 0 else x。如果考虑/需要条件的导数,即x < 0,不存在,那么我们将无法在我们的模型中使用 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.greatertf.logical_and 来确定输入值的大小是否为小于sigma(或者,我们可以只使用tf.math.abstf.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作为损失值!所以有什么问题?为什么会产生这个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 值问题:令人惊讶的是,如果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 &gt; 1 和输入值介于sqrt(sigma)sigma 之间(或-sigma-sqrt(sigma) 之间的情况。)

    您能否为第 1 点中的问题提供解决方案,即该层可以适用于所有输入值? (提示:就像tf.where 的解决方法一样,考虑如何进一步过滤掉可以应用凹凸函数并产生爆炸输出/梯度的不安全值。)

    1234563和 1? (提示:作为一种解决方案,有一个常用的激活函数,它产生的值恰好在这个范围内,并且可以潜在地用作该层之前的层的激活函数。) p>

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

    我们是否还需要检查生成的训练数据中是否存在任何naninf 或非常大的y 值并将它们过滤掉? (提示:是的,如果sigma &gt; 1 和值范围,即min_xmax_x,不在(-1, 1) 范围内;否则,不,没有必要!为什么会这样?留作练习!)

【讨论】:

干得好。 @ProbablyAHuman 这应该是公认的答案。 @今天。我认为这很棒,可能是我在任何堆栈上见过的最详细/精确/严谨的答案。非常感谢!

以上是关于在 Keras/Tensorflow 中实现可训练的广义 Bump 函数层的主要内容,如果未能解决你的问题,请参考以下文章

训练某些网络时,GPU 上的 Keras(Tensorflow 后端)比 CPU 上慢

如何在 RealmRecyclerViewAdapter 中实现可过滤

如何在 Flutter 中实现可扩展面板?

如何在pytorch中实现可微的汉明损失?

如何在 Flutter 中实现可滚动的画布?

如何在 UIScrollView 中实现可拖动的 UIView 子类?