与えられた警告の主な(そして唯一の)理由について誰も言及しなかったことに少し驚いています!どうやら、そのコードはバンプ関数の一般化されたバリアントを実装することになっています。ただし、再度実装された関数を見てください。
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)
エラーは明らかです。これらの関数では、レイヤーのトレーニング可能な重みを使用していません。したがって、そのためにグラデーションが存在しないというメッセージが表示されても驚くことはありません。まったく使用していないため、グラデーションを更新する必要はありません。むしろ、これはまさに元のバンプ関数です(つまり、トレーニング可能な重みはありません)。
しかし、「少なくとも、の条件でトレーニング可能なウェイトを使用した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
、何の違いを!)。繰り返しますが、ブール出力関数をこの方法で使用しても問題は発生せず、導関数/勾配とは関係ありません。
また、層によって学習されたシグマ値に非負性制約を使用しています。どうして?0未満のシグマ値は意味をなさないため(つまり、(-sigma, sigma)
シグマが負の場合、範囲は不明確です)。
また、前のポイントを考慮して、シグマ値を適切に(つまり、負でない小さな値に)初期化するように注意します。
また、次のようなことはしないでください0.0 * inputs
。これは冗長(そして少し奇妙です)で、と同等0.0
です。両方とも0.0
(wrtinputs
)のます。ゼロをテンソルで乗算しても、何も追加したり、既存の問題を解決したりすることはありません。少なくともこの場合はそうではありません!
次に、それをテストして、どのように機能するかを確認します。固定シグマ値に基づいてトレーニングデータを生成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
トレーニング後のsigmaの値、またはinf
損失値として取得することは可能です!だから問題は何ですか?なぜこれnan
またはinf
値が生成されるのですか?以下で説明しましょう...
数値安定性の扱い
機械学習モデルを構築し、勾配ベースの最適化手法を使用してそれらをトレーニングする場合に考慮すべき重要なことの1つは、モデル内の操作と計算の数値的安定性です。演算またはその勾配によって非常に大きな値または小さな値が生成されると、ほぼ確実にトレーニングプロセスが中断されます(たとえば、これがCNNの画像ピクセル値を正規化してこの問題を防ぐ理由の1つです)。
それでは、この一般化されたバンプ関数を見てみましょう(そして今のところしきい値処理を破棄しましょう)。この関数には特異点(つまり、関数またはその勾配のいずれかが定義されていない点)があるx^2 = sigma
(つまり、x = sqrt(sigma)
またはx=-sqrt(sigma)
)ことは明らかです。以下のアニメーションの図は、シグマがゼロから始まり5に増加したときのバンプ関数(赤い実線)、その微分wrtシグマ(緑の点線)、x=sigma
およびx=-sigma
ライン(2つの垂直の青い破線)を示しています。
ご覧のとおり、特異点の領域の周りでは、関数とその導関数の両方がそれらの領域で非常に大きな値を取るという意味で、関数はシグマのすべての値に対して適切に動作していません。したがって、シグマの特定の値に対するこれらの領域での入力値が与えられると、爆発する出力値と勾配値が生成されるため、次の問題が発生します。inf
損失値の発生します。
さらに、問題のある動作がtf.where
ありnan
、レイヤーのシグマ変数の値の問題が発生します。驚くべきことに、非アクティブなブランチで生成された値tf.where
が非常に大きい場合inf
、またはバンプ関数を使用すると、非常に大きいinf
値または勾配値になる場合、それからの勾配はですtf.where
がnan
、inf
は非アクティブなブランチであり、選択されていないという事実にもかかわらずです(これについて正確に説明している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より大きく1未満の場合に適切に機能し、シグマ値を学習することが保証されています(つまり、これがgenerate_data
関数のデフォルトのケースです)。それ以外のinf
場合、入力値の大きさが1より大きい場合に発生する可能性のある損失値の問題が依然として存在します(下記のポイント#1および#2を参照)。
好奇心と関心のある心のために考えるための食べ物をいくつか紹介します。
このレイヤーへの入力値が1より大きいか、または-1より小さい場合、問題が発生する可能性があると述べました。これが事実である理由を主張できますか?(ヒント:上記のアニメーション図を使用してケースを検討しsigma > 1
、入力値の間であり、sqrt(sigma)
且つsigma
(又は間-sigma
及び-sqrt(sigma)
)。
ポイント1の問題の修正を提供できますか?つまり、レイヤーがすべての入力値で機能するようにできますか?(ヒント:の回避策と同様に、バンプ関数が適用される危険な値をtf.where
さらに除外して、爆発する出力/勾配を生成する方法について考えてください。)
ただし、この問題の修正に関心がなく、このレイヤーを現在のモデルで使用したい場合、このレイヤーへの入力値が常に-1から1の間であることをどのように保証しますか?(ヒント:1つの解決策として、正確にこの範囲の値を生成する一般的に使用されるアクティベーション関数があり、このレイヤーの前にあるレイヤーのアクティベーション関数として使用される可能性があります。)
最後のコードスニペットを見ると、使用したことがわかりますepochs=3 if s < 1 else (5 if s < 5 else 10)
。何故ですか?大きな値のsigmaを学習するには、より多くのエポックが必要なのはなぜですか?(ヒント:再度、アニメーション図を使用して、シグマ値が増加するときの-1から1の間の入力値の関数の導関数を検討してください。それらの大きさは何ですか?)
また、の生成されたトレーニングデータnan
、inf
またはの非常に大きな値をチェックして、y
それらを除外する必要がありますか?(ヒント:はい、sigma > 1
そして値の範囲、つまりmin_x
and max_x
はの外にあり(-1, 1)
ます;そうでなければ、それは必要ありません!なぜですか?演習として残してください!)
input
ですか?それはスカラーですか?