高度な自動微分

TensorFlow.orgで表示 GoogleColabで実行 GitHubでソースを表示 ノートブックをダウンロードする

勾配と自動微分入門ガイドはTensorFlowで計算勾配に必要なすべてが含まれています。このガイドでは、より深く、あまり一般的な機能に焦点を当ててtf.GradientTape API。

セットアップ

pip uninstall tensorflow keras -y
pip install tf-nightly
import tensorflow as tf

import matplotlib as mpl
import matplotlib.pyplot as plt

mpl.rcParams['figure.figsize'] = (8, 6)

勾配記録の制御

自動微分ガイドあなたは勾配計算を構築しながら、テープによって監視されている変数とテンソル制御する方法を説明しました。

テープには、録音を操作する方法もあります。

録音を停止します

あなたはグラデーションの記録を停止したい場合は、使用することができますtf.GradientTape.stop_recording一時的に記録を停止します。

これは、モデルの途中で複雑な操作を区別したくない場合に、オーバーヘッドを削減するのに役立つ場合があります。これには、メトリックまたは中間結果の計算が含まれる場合があります。

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  x_sq = x * x
  with t.stop_recording():
    y_sq = y * y
  z = x_sq + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None
2021-07-01 01:22:12.311927: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.319895: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.320536: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.322087: I tensorflow/core/platform/cpu_feature_guard.cc:142] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations:  AVX2 AVX512F FMA
To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.
2021-07-01 01:22:12.322666: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.323332: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.323939: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.907440: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.908098: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.908676: I tensorflow/stream_executor/cuda/cuda_gpu_executor.cc:937] successful NUMA node read from SysFS had negative value (-1), but there must be at least one NUMA node, so returning NUMA node zero
2021-07-01 01:22:12.909259: I tensorflow/core/common_runtime/gpu/gpu_device.cc:1510] Created device /job:localhost/replica:0/task:0/device:GPU:0 with 14646 MB memory:  -> device: 0, name: NVIDIA Tesla V100-SXM2-16GB, pci bus id: 0000:00:05.0, compute capability: 7.0

録音を最初からリセット/開始

あなたは完全に最初からやり直したい場合は、使用tf.GradientTape.reset 。単に勾配テープブロックを終了して再起動され、通常は読みやすいが、あなたは使用することができresetテープブロックは困難または不可能である終了時の方法を。

x = tf.Variable(2.0)
y = tf.Variable(3.0)
reset = True

with tf.GradientTape() as t:
  y_sq = y * y
  if reset:
    # Throw out all the tape recorded so far.
    t.reset()
  z = x * x + y_sq

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

グラジエントフローを正確に停止します

上記グローバルテープコントロールとは対照的に、 tf.stop_gradient機能は、はるかに正確です。テープ自体にアクセスすることなく、特定のパスに沿ってグラデーションが流れるのを防ぐために使用できます。

x = tf.Variable(2.0)
y = tf.Variable(3.0)

with tf.GradientTape() as t:
  y_sq = y**2
  z = x**2 + tf.stop_gradient(y_sq)

grad = t.gradient(z, {'x': x, 'y': y})

print('dz/dx:', grad['x'])  # 2*x => 4
print('dz/dy:', grad['y'])
dz/dx: tf.Tensor(4.0, shape=(), dtype=float32)
dz/dy: None

カスタムグラデーション

場合によっては、デフォルトを使用するのではなく、グラデーションの計算方法を正確に制御したいことがあります。これらの状況は次のとおりです。

  1. あなたが書いている新しいopのための定義されたグラデーションはありません。
  2. デフォルトの計算は数値的に不安定です。
  3. フォワードパスからの高価な計算をキャッシュしたいとします。
  4. あなたは(使用して、例えば値を変更したいtf.clip_by_valueまたはtf.math.round勾配を変更せずに)。

最初のケースでは、あなたが使用できる新しいオペアンプ書くことtf.RegisterGradient独自に設定することを(詳細については、APIドキュメントを参照してください)。 (グラデーションレジストリはグローバルであるため、注意して変更してください。)

後者の3例については、使用することができますtf.custom_gradient

ここで適用した例であるtf.clip_by_norm中間勾配は:

# Establish an identity operation, but clip during the gradient pass.
@tf.custom_gradient
def clip_gradients(y):
  def backward(dy):
    return tf.clip_by_norm(dy, 0.5)
  return y, backward

v = tf.Variable(2.0)
with tf.GradientTape() as t:
  output = clip_gradients(v * v)
print(t.gradient(output, v))  # calls "backward", which clips 4 to 2
tf.Tensor(2.0, shape=(), dtype=float32)

参照してくださいtf.custom_gradient詳細についてはデコレータのAPIドキュメント。

SavedModelのカスタムグラデーション

カスタムグラデーションは、オプションの使用によりSavedModelに保存することができtf.saved_model.SaveOptions(experimental_custom_gradients=True)

SavedModelに保存するためには、勾配関数は、(チェックアウト、もっと学ぶためにトレーサブルでなければならないtf.functionの持つ優れた性能ガイド)。

class MyModule(tf.Module):

  @tf.function(input_signature=[tf.TensorSpec(None)])
  def call_custom_grad(self, x):
    return clip_gradients(x)

model = MyModule()
tf.saved_model.save(
    model,
    'saved_model',
    options=tf.saved_model.SaveOptions(experimental_custom_gradients=True))

# The loaded gradients will be the same as the above example.
v = tf.Variable(2.0)
loaded = tf.saved_model.load('saved_model')
with tf.GradientTape() as t:
  output = loaded.call_custom_grad(v * v)
print(t.gradient(output, v))
INFO:tensorflow:Assets written to: saved_model/assets
tf.Tensor(2.0, shape=(), dtype=float32)
2021-07-01 01:22:13.395687: I tensorflow/compiler/mlir/mlir_graph_optimization_pass.cc:185] None of the MLIR Optimization Passes are enabled (registered 2)

上記の例についての注意:あなたが上記のコードを交換してみた場合tf.saved_model.SaveOptions(experimental_custom_gradients=False) 、勾配がまだロードで同じ結果を生成します。その理由は、勾配レジストリがまだ機能で使用されるカスタム勾配含まれていることであるcall_custom_op 。あなたは下にロードされたモデルを実行し、カスタムグラデーションなしで保存した後、ランタイムを再起動する場合は、 tf.GradientTape :エラーがスローされますLookupError: No gradient defined for operation 'IdentityN' (op type: IdentityN)

複数のテープ

複数のテープがシームレスに相互作用します。

たとえば、ここでは、各テープが異なるテンソルのセットを監視しています。

x0 = tf.constant(0.0)
x1 = tf.constant(0.0)

with tf.GradientTape() as tape0, tf.GradientTape() as tape1:
  tape0.watch(x0)
  tape1.watch(x1)

  y0 = tf.math.sin(x0)
  y1 = tf.nn.sigmoid(x1)

  y = y0 + y1

  ys = tf.reduce_sum(y)
tape0.gradient(ys, x0).numpy()   # cos(x) => 1.0
1.0
tape1.gradient(ys, x1).numpy()   # sigmoid(x1)*(1-sigmoid(x1)) => 0.25
0.25

高次勾配

内部動作tf.GradientTapeコンテキストマネージャは、自動微分のために記録されています。そのコンテキストで勾配が計算される場合、勾配の計算も記録されます。その結果、まったく同じAPIが高次のグラデーションでも機能します。

例えば:

x = tf.Variable(1.0)  # Create a Tensorflow variable initialized to 1.0

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    y = x * x * x

  # Compute the gradient inside the outer `t2` context manager
  # which means the gradient computation is differentiable as well.
  dy_dx = t1.gradient(y, x)
d2y_dx2 = t2.gradient(dy_dx, x)

print('dy_dx:', dy_dx.numpy())  # 3 * x**2 => 3.0
print('d2y_dx2:', d2y_dx2.numpy())  # 6 * x => 6.0
dy_dx: 3.0
d2y_dx2: 6.0

それはあなたのスカラー関数の二次導関数を得んがため、このパターンは、ヘッセ行列を生成するために一般ないtf.GradientTape.gradientのみスカラーの勾配を計算します。構築するために、ヘッセ行列を、に行くヘッセ例の下ヤコビアンセクション

「ネストされた呼び出しtf.GradientTape.gradient 」あなたは傾きからスカラーを計算し、その後、得られたスカラーは、次の例のように、第二の勾配計算のための源として働く良いパターンです。

例:入力勾配の正則化

多くのモデルは「敵対的な例」の影響を受けやすくなっています。このテクニックのコレクションは、モデルの入力を変更して、モデルの出力を混乱させます。最も単純な実装のような敵対例えば高速勾配署名されたメソッドの攻撃を使用しては、入力に対する出力の勾配に沿って単一のステップを-takes。 「入力勾配」。

敵対例にロバスト性を増大させる1つの手法は、入力勾配正則、どの入力勾配の大きさを最小化する試み(フィンレイ&Oberman、2019)。入力勾配が小さい場合、出力の変化も小さいはずです。

以下は、入力勾配正則化の単純な実装です。実装は次のとおりです。

  1. 内側のテープを使用して、入力に対する出力の勾配を計算します。
  2. その入力勾配の大きさを計算します。
  3. モデルに関してその大きさの勾配を計算します。
x = tf.random.normal([7, 5])

layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)
with tf.GradientTape() as t2:
  # The inner tape only takes the gradient with respect to the input,
  # not the variables.
  with tf.GradientTape(watch_accessed_variables=False) as t1:
    t1.watch(x)
    y = layer(x)
    out = tf.reduce_sum(layer(x)**2)
  # 1. Calculate the input gradient.
  g1 = t1.gradient(out, x)
  # 2. Calculate the magnitude of the input gradient.
  g1_mag = tf.norm(g1)

# 3. Calculate the gradient of the magnitude with respect to the model.
dg1_mag = t2.gradient(g1_mag, layer.trainable_variables)
[var.shape for var in dg1_mag]
[TensorShape([5, 10]), TensorShape([10])]

ヤコビアン

これまでのすべての例では、いくつかのソーステンソルに関してスカラーターゲットの勾配を取りました。

ヤコビ行列は、ベクトル値関数の勾配を表します。各行には、ベクトルの要素の1つの勾配が含まれています。

tf.GradientTape.jacobian方法は、あなたが効率的にヤコビ行列を計算することができます。

ご了承ください:

  • 同様gradientsources引数はテンソルまたはテンソルのコンテナすることができます。
  • 異なりgradienttargetテンソルは、単一のテンソルでなければなりません。

スカラーソース

最初の例として、スカラーソースに関するベクトルターゲットのヤコビアンを次に示します。

x = tf.linspace(-10.0, 10.0, 200+1)
delta = tf.Variable(0.0)

with tf.GradientTape() as tape:
  y = tf.nn.sigmoid(x+delta)

dy_dx = tape.jacobian(y, delta)

あなたはスカラーに関してヤコビアンを取るときの結果は、ターゲットの形状を有しており、ソースに対する各要素の勾配を与えます:

print(y.shape)
print(dy_dx.shape)
(201,)
(201,)
plt.plot(x.numpy(), y, label='y')
plt.plot(x.numpy(), dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

テンソルソース

入力がスカラー又はテンソルであるかどうか、 tf.GradientTape.jacobian効率的にターゲット(複数可)の各要素に対してソースの各要素の勾配を計算します。

例えば、この層の出力は、形状有し(10, 7)

x = tf.random.normal([7, 5])
layer = tf.keras.layers.Dense(10, activation=tf.nn.relu)

with tf.GradientTape(persistent=True) as tape:
  y = layer(x)

y.shape
TensorShape([7, 10])

そして、層のカーネルの形状がある(5, 10)

layer.kernel.shape
TensorShape([5, 10])

カーネルに関する出力のヤコビアンの形状は、連結された2つの形状です。

j = tape.jacobian(y, layer.kernel)
j.shape
TensorShape([7, 10, 5, 10])

あなたは、ターゲットの大きさを超える合計した場合、次の方法で計算されていたであろう和の勾配で左しているtf.GradientTape.gradient

g = tape.gradient(y, layer.kernel)
print('g.shape:', g.shape)

j_sum = tf.reduce_sum(j, axis=[0, 1])
delta = tf.reduce_max(abs(g - j_sum)).numpy()
assert delta < 1e-3
print('delta:', delta)
g.shape: (5, 10)
delta: 2.3841858e-07

例:ヘシアン

一方でtf.GradientTape構築するための明示的な方法を与えるものではありませんヘッセ行列をそれが使用して1を構築することが可能ですtf.GradientTape.jacobian方法を。

x = tf.random.normal([7, 5])
layer1 = tf.keras.layers.Dense(8, activation=tf.nn.relu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.relu)

with tf.GradientTape() as t2:
  with tf.GradientTape() as t1:
    x = layer1(x)
    x = layer2(x)
    loss = tf.reduce_mean(x**2)

  g = t1.gradient(loss, layer1.kernel)

h = t2.jacobian(g, layer1.kernel)
print(f'layer.kernel.shape: {layer1.kernel.shape}')
print(f'h.shape: {h.shape}')
layer.kernel.shape: (5, 8)
h.shape: (5, 8, 5, 8)

このヘッセ行列を使用するにはニュートン法のステップは、第1の行列の中にその軸を平らにし、ベクターに勾配を平らになります。

n_params = tf.reduce_prod(layer1.kernel.shape)

g_vec = tf.reshape(g, [n_params, 1])
h_mat = tf.reshape(h, [n_params, n_params])

ヘッセ行列は対称である必要があります。

def imshow_zero_center(image, **kwargs):
  lim = tf.reduce_max(abs(image))
  plt.imshow(image, vmin=-lim, vmax=lim, cmap='seismic', **kwargs)
  plt.colorbar()
imshow_zero_center(h_mat)

png

ニュートン法の更新手順を以下に示します。

eps = 1e-3
eye_eps = tf.eye(h_mat.shape[0])*eps
# X(k+1) = X(k) - (\u2207\xb2f(X(k)))^-1 @ \u2207f(X(k))
# h_mat = \u2207\xb2f(X(k))
# g_vec = \u2207f(X(k))
update = tf.linalg.solve(h_mat + eye_eps, g_vec)

# Reshape the update and apply it to the variable.
_ = layer1.kernel.assign_sub(tf.reshape(update, layer1.kernel.shape))
2021-07-01 01:22:14.866543: I tensorflow/core/util/cuda_solvers.cc:180] Creating CudaSolver handles for stream 0x7278d90

これは、単一のために比較的単純ですがtf.Variable 、非自明なモデルにこれを適用すると、複数の変数間の完全なヘッセ行列を生成するために慎重な連結とスライスを必要とします。

バッチヤコビアン

場合によっては、ソースのスタックに対してターゲットのスタックのそれぞれのヤコビアンを取得する必要があります。ここで、各ターゲットとソースのペアのヤコビアンは独立しています。

例えば、ここでは入力x成形される(batch, ins) 、出力y成形される(batch, outs)

x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = layer2(y)

y.shape
TensorShape([7, 6])

完全なヤコビアンyに関するxの形がある(batch, ins, batch, outs)あなたが唯一の希望しても、 (batch, ins, outs)

j = tape.jacobian(y, x)
j.shape
TensorShape([7, 6, 7, 5])

スタック内の各項目の勾配が独立している場合は、すべての(batch, batch)このテンソルのスライスは対角行列です。

imshow_zero_center(j[:, 0, :, 0])
_ = plt.title('A (batch, batch) slice')

png

def plot_as_patches(j):
  # Reorder axes so the diagonals will each form a contiguous patch.
  j = tf.transpose(j, [1, 0, 3, 2])
  # Pad in between each patch.
  lim = tf.reduce_max(abs(j))
  j = tf.pad(j, [[0, 0], [1, 1], [0, 0], [1, 1]],
             constant_values=-lim)
  # Reshape to form a single image.
  s = j.shape
  j = tf.reshape(j, [s[0]*s[1], s[2]*s[3]])
  imshow_zero_center(j, extent=[-0.5, s[2]-0.5, s[0]-0.5, -0.5])

plot_as_patches(j)
_ = plt.title('All (batch, batch) slices are diagonal')

png

目的の結果を取得するには、重複を超える合計することができbatchの寸法を、または他の使用して対角線を選択tf.einsum

j_sum = tf.reduce_sum(j, axis=2)
print(j_sum.shape)
j_select = tf.einsum('bxby->bxy', j)
print(j_select.shape)
(7, 6, 5)
(7, 6, 5)

そもそも余分な次元なしで計算を行う方がはるかに効率的です。 tf.GradientTape.batch_jacobian方法は正確に行います。

jb = tape.batch_jacobian(y, x)
jb.shape
WARNING:tensorflow:5 out of the last 5 calls to <function pfor.<locals>.f at 0x7fb5e8133560> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
TensorShape([7, 6, 5])
error = tf.reduce_max(abs(jb - j_sum))
assert error < 1e-3
print(error.numpy())
0.0
x = tf.random.normal([7, 5])

layer1 = tf.keras.layers.Dense(8, activation=tf.nn.elu)
bn = tf.keras.layers.BatchNormalization()
layer2 = tf.keras.layers.Dense(6, activation=tf.nn.elu)

with tf.GradientTape(persistent=True, watch_accessed_variables=False) as tape:
  tape.watch(x)
  y = layer1(x)
  y = bn(y, training=True)
  y = layer2(y)

j = tape.jacobian(y, x)
print(f'j.shape: {j.shape}')
WARNING:tensorflow:6 out of the last 6 calls to <function pfor.<locals>.f at 0x7fb5dc652830> triggered tf.function retracing. Tracing is expensive and the excessive number of tracings could be due to (1) creating @tf.function repeatedly in a loop, (2) passing tensors with different shapes, (3) passing Python objects instead of tensors. For (1), please define your @tf.function outside of the loop. For (2), @tf.function has experimental_relax_shapes=True option that relaxes argument shapes that can avoid unnecessary retracing. For (3), please refer to https://www.tensorflow.org/guide/function#controlling_retracing and https://www.tensorflow.org/api_docs/python/tf/function for  more details.
j.shape: (7, 6, 7, 5)
plot_as_patches(j)

_ = plt.title('These slices are not diagonal')
_ = plt.xlabel("Don't use `batch_jacobian`")

png

この場合、 batch_jacobianまだ実行され、期待される形状で返す何か、それの内容が不明確な意味を持ちます。

jb = tape.batch_jacobian(y, x)
print(f'jb.shape: {jb.shape}')
jb.shape: (7, 6, 5)