グラデーションと自動微分の概要

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

自動微分とグラジエント

自動微分は、ニューラルネットワークをトレーニングするためのバックプロパゲーションなどの機械学習アルゴリズムを実装するのに役立ちます。

このガイドでは、特に熱心な実行において、TensorFlowを使用して勾配を計算する方法を探ります。

セットアップ

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

勾配の計算

自動的に区別するために、TensorFlowは、フォワードパス中にどの操作がどの順序で発生するかを記憶する必要があります。次に、バックワードパス中に、TensorFlowはこの操作のリストを逆の順序でトラバースして、勾配を計算します。

グラデーションテープ

TensorFlowが提供tf.GradientTape自動微分のためのAPIを。つまり、一部の入力(通常はtf.Variableに関する計算の勾配を計算します。 TensorFlowは、tf.GradientTapeのコンテキスト内で実行された関連操作を「テープ」に「記録」します。次に、TensorFlowはそのテープを使用して、逆モード微分を使用して「記録された」計算の勾配を計算します

簡単な例を次に示します。

x = tf.Variable(3.0)

with tf.GradientTape() as tape:
  y = x**2

いくつかの操作を記録したら、 GradientTape.gradient(target, sources)を使用して、あるソース(多くの場合モデルの変数)に対するあるターゲット(多くの場合損失)の勾配を計算します。

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
dy_dx.numpy()
6.0

上記の例ではスカラーを使用していますが、tf.GradientTapeはどのテンソルでも同じように簡単に機能します。

w = tf.Variable(tf.random.normal((3, 2)), name='w')
b = tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
x = [[1., 2., 3.]]

with tf.GradientTape(persistent=True) as tape:
  y = x @ w + b
  loss = tf.reduce_mean(y**2)

両方の変数に関するlossの勾配を取得するには、両方をソースとしてgradient法に渡すことができます。テープは、ソースの受け渡し方法に柔軟性があり、リストまたは辞書のネストされた組み合わせを受け入れ、同じ方法で構造化されたグラデーションを返します( tf.nest参照)。

[dl_dw, dl_db] = tape.gradient(loss, [w, b])

各ソースに関するグラデーションは、ソースの形状を持っています。

print(w.shape)
print(dl_dw.shape)
(3, 2)
(3, 2)

これが再び勾配計算です。今回は変数の辞書を渡します。

my_vars = {
    'w': w,
    'b': b
}

grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([ 2.5342524, -3.8607523], dtype=float32)>

モデルに関する勾配

収集するために、それの普通tf.Variablestf.Moduleまたはそのサブクラス(の1 layers.Layerkeras.Model用)チェックポイントおよびエクスポートします

ほとんどの場合、モデルのトレーニング可能な変数に関して勾配を計算する必要があります。 tf.Moduleすべてのサブクラスは、 Module.trainable_variablesプロパティで変数を集約するため、これらの勾配は数行のコードで計算できます。

layer = tf.keras.layers.Dense(2, activation='relu')
x = tf.constant([[1., 2., 3.]])

with tf.GradientTape() as tape:
  # Forward pass
  y = layer(x)
  loss = tf.reduce_mean(y**2)

# Calculate gradients with respect to every trainable variable
grad = tape.gradient(loss, layer.trainable_variables)
for var, g in zip(layer.trainable_variables, grad):
  print(f'{var.name}, shape: {g.shape}')
dense/kernel:0, shape: (3, 2)
dense/bias:0, shape: (2,)

テープが監視するものを制御する

デフォルトの動作では、トレーニング可能なtf.Variableにアクセスした後にすべての操作を記録します。この理由は次のとおりです。

  • テープは、後方パスの勾配を計算するために、前方パスに記録する操作を知る必要があります。
  • テープには中間出力への参照が含まれているため、不要な操作を記録する必要はありません。
  • 最も一般的な使用例は、すべてのモデルのトレーニング可能な変数に関する損失の勾配を計算することです。

たとえば、次のための勾配を計算するのに失敗したtf.Tensor 、デフォルトでは「見て」いない、とされてtf.Variableトレーニング可能ではありません。

# A trainable variable
x0 = tf.Variable(3.0, name='x0')
# Not trainable
x1 = tf.Variable(3.0, name='x1', trainable=False)
# Not a Variable: A variable + tensor returns a tensor.
x2 = tf.Variable(2.0, name='x2') + 1.0
# Not a variable
x3 = tf.constant(3.0, name='x3')

with tf.GradientTape() as tape:
  y = (x0**2) + (x1**2) + (x2**2)

grad = tape.gradient(y, [x0, x1, x2, x3])

for g in grad:
  print(g)
tf.Tensor(6.0, shape=(), dtype=float32)
None
None
None

GradientTape.watched_variablesメソッドを使用して、テープで監視されている変数を一覧表示できます。

[var.name for var in tape.watched_variables()]
['x0:0']

tf.GradientTapeは、何が監視されているか、何が監視されていないかをユーザーが制御できるようにするフックを提供します。

tf.Tensorに関するグラデーションを記録するには、 GradientTape.watch(x)を呼び出す必要があります。

x = tf.constant(3.0)
with tf.GradientTape() as tape:
  tape.watch(x)
  y = x**2

# dy = 2x * dx
dy_dx = tape.gradient(y, x)
print(dy_dx.numpy())
6.0

逆に、すべてのtf.Variablesを監視するデフォルトの動作を無効にするには、グラデーションテープの作成時にwatch_accessed_variables=False設定します。この計算では2つの変数を使用しますが、変数の1つの勾配のみを接続します。

x0 = tf.Variable(0.0)
x1 = tf.Variable(10.0)

with tf.GradientTape(watch_accessed_variables=False) as tape:
  tape.watch(x1)
  y0 = tf.math.sin(x0)
  y1 = tf.nn.softplus(x1)
  y = y0 + y1
  ys = tf.reduce_sum(y)

GradientTape.watchx0で呼び出されなかったため、それに関してグラデーションは計算されません。

# dys/dx1 = exp(x1) / (1 + exp(x1)) = sigmoid(x1)
grad = tape.gradient(ys, {'x0': x0, 'x1': x1})

print('dy/dx0:', grad['x0'])
print('dy/dx1:', grad['x1'].numpy())
dy/dx0: None
dy/dx1: 0.9999546

中間結果

tf.GradientTapeコンテキスト内で計算された中間値に関して出力の勾配を要求することもできます。

x = tf.constant(3.0)

with tf.GradientTape() as tape:
  tape.watch(x)
  y = x * x
  z = y * y

# Use the tape to compute the gradient of z with respect to the
# intermediate value y.
# dz_dy = 2 * y and y = x ** 2 = 9
print(tape.gradient(z, y).numpy())
18.0

デフォルトでは、 GradientTapeによって保持されているリソースは、 GradientTape.gradientメソッドが呼び出されるとすぐに解放されます。同じ計算で複数のグラデーションを計算するには、 persistent=Trueグラデーションテープを作成しpersistent=True 。これにより、テープオブジェクトがガベージコレクションされるときにリソースが解放されるため、 gradientメソッドを複数回呼び出すことができます。例えば:

x = tf.constant([1, 3.0])
with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  y = x * x
  z = y * y

print(tape.gradient(z, x).numpy())  # 108.0 (4 * x**3 at x = 3)
print(tape.gradient(y, x).numpy())  # 6.0 (2 * x)
[  4. 108.]
[2. 6.]
del tape   # Drop the reference to the tape

パフォーマンスに関する注意事項

  • グラデーションテープコンテキスト内での操作の実行に関連する小さなオーバーヘッドがあります。ほとんどの熱心な実行では、これは目立ったコストにはなりませんが、それでも必要な領域でのみテープコンテキストを使用する必要があります。

  • グラデーションテープは、メモリを使用して、バックワードパス中に使用するための入力と出力を含む中間結果を格納します。

    効率を上げるために、一部のops( ReLU )は中間結果を保持する必要がなく、フォワードパス中にプルーニングされます。ただし、テープでpersistent=Trueを使用すると、何も破棄されず、ピークメモリ使用量が高くなります。

非スカラーターゲットの勾配

グラデーションは基本的にスカラーの操作です。

x = tf.Variable(2.0)
with tf.GradientTape(persistent=True) as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient(y0, x).numpy())
print(tape.gradient(y1, x).numpy())
4.0
-0.25

したがって、複数のターゲットの勾配を要求すると、各ソースの結果は次のようになります。

  • ターゲットの合計の勾配、または同等に
  • 各ターゲットの勾配の合計。
x = tf.Variable(2.0)
with tf.GradientTape() as tape:
  y0 = x**2
  y1 = 1 / x

print(tape.gradient({'y0': y0, 'y1': y1}, x).numpy())
3.75

同様に、ターゲットがスカラーでない場合、合計の勾配が計算されます。

x = tf.Variable(2.)

with tf.GradientTape() as tape:
  y = x * [3., 4.]

print(tape.gradient(y, x).numpy())
7.0

これにより、損失のコレクションの合計の勾配、または要素ごとの損失計算の合計の勾配を簡単に取得できます。

アイテムごとに個別のグラデーションが必要な場合は、ヤコビアンを参照してください。

場合によっては、ヤコビアンをスキップできます。要素ごとの計算では、各要素が独立しているため、合計の勾配により、入力要素に関する各要素の導関数が得られます。

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

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

dy_dx = tape.gradient(y, x)
plt.plot(x, y, label='y')
plt.plot(x, dy_dx, label='dy/dx')
plt.legend()
_ = plt.xlabel('x')

png

制御フロー

グラデーションテープは実行時に操作を記録するため、Pythonの制御フローは自然に処理されます(たとえば、 ifステートメントとwhileステートメント)。

ここでは、 if各ブランチで異なる変数が使用されています。グラデーションは、使用された変数にのみ接続します。

x = tf.constant(1.0)

v0 = tf.Variable(2.0)
v1 = tf.Variable(2.0)

with tf.GradientTape(persistent=True) as tape:
  tape.watch(x)
  if x > 0.0:
    result = v0
  else:
    result = v1**2 

dv0, dv1 = tape.gradient(result, [v0, v1])

print(dv0)
print(dv1)
tf.Tensor(1.0, shape=(), dtype=float32)
None

制御ステートメント自体は微分可能ではないため、グラデーションベースのオプティマイザーには表示されないことに注意してください。

上記の例のxの値に応じて、テープはresult = v0またはresult = v1**2いずれかを記録します。 xに関する勾配は常にNoneです。

dx = tape.gradient(result, x)

print(dx)
None

Noneグラデーションを取得する

ターゲットがソースに接続されていない場合、 Noneグラデーションが表示されます。

x = tf.Variable(2.)
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y * y
print(tape.gradient(z, x))
None

ここで、 zは明らかにxに接続されていませんが、グラデーションを切断する方法はそれほど明白ではありません。

1.変数をテンソルに置き換えました

「テープが監視するものの制御」のセクションで、テープは自動的にtf.Variable監視しtf.Variableが、 tf.Variable監視しないことをtf.Tensor

一つの一般的なエラーが誤って交換することですtf.Variableしてtf.Tensor代わりに使用するのでは、 Variable.assign更新するtf.Variable 。次に例を示します。

x = tf.Variable(2.0)

for epoch in range(2):
  with tf.GradientTape() as tape:
    y = x+1

  print(type(x).__name__, ":", tape.gradient(y, x))
  x = x + 1   # This should be `x.assign_add(1)`
ResourceVariable : tf.Tensor(1.0, shape=(), dtype=float32)
EagerTensor : None

2.TensorFlowの外部で計算を行いました

計算がTensorFlowを終了する場合、テープは勾配パスを記録できません。例えば:

x = tf.Variable([[1.0, 2.0],
                 [3.0, 4.0]], dtype=tf.float32)

with tf.GradientTape() as tape:
  x2 = x**2

  # This step is calculated with NumPy
  y = np.mean(x2, axis=0)

  # Like most ops, reduce_mean will cast the NumPy array to a constant tensor
  # using `tf.convert_to_tensor`.
  y = tf.reduce_mean(y, axis=0)

print(tape.gradient(y, x))
None

3.整数または文字列を介してグラデーションを取得しました

整数と文字列は微分可能ではありません。計算パスがこれらのデータ型を使用する場合、勾配はありません。

文字列が微分可能であることを誰も期待していませんが、 dtype指定しないと、誤ってint定数または変数を作成する可能性があります。

x = tf.constant(10)

with tf.GradientTape() as g:
  g.watch(x)
  y = x * x

print(g.gradient(y, x))
WARNING:tensorflow:The dtype of the watched tensor must be floating (e.g. tf.float32), got tf.int32
WARNING:tensorflow:The dtype of the target tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
WARNING:tensorflow:The dtype of the source tensor must be floating (e.g. tf.float32) when calling GradientTape.gradient, got tf.int32
None

TensorFlowは型間で自動的にキャストされないため、実際には、グラデーションが欠落しているのではなく、型エラーが発生することがよくあります。

4.ステートフルオブジェクトを介してグラデーションを取得しました

状態は勾配を停止します。ステートフルオブジェクトから読み取る場合、テープは現在の状態のみを監視でき、それにつながる履歴は監視できません。

tf.Tensorは不変です。一度作成したテンソルを変更することはできません。はありますが、状態はありません。これまでに説明したすべての操作もステートレスですtf.matmulの出力はその入力にのみ依存します。

tf.Variableは、内部状態、 tf.Variableその値があります。変数を使用すると、状態が読み取られます。変数に関して勾配を計算するのは正常ですが、変数の状態により、勾配の計算がそれ以上戻ることはできません。例えば:

x0 = tf.Variable(3.0)
x1 = tf.Variable(0.0)

with tf.GradientTape() as tape:
  # Update x1 = x1 + x0.
  x1.assign_add(x0)
  # The tape starts recording from x1.
  y = x1**2   # y = (x1 + x0)**2

# This doesn't work.
print(tape.gradient(y, x0))   #dy/dx0 = 2*(x1 + x0)
None

同様に、tf.data.Datasetイテレータとtf.queueはステートフルであり、それらを通過するテンソルのすべての勾配を停止します。

グラデーションが登録されていません

一部のtf.Operation微分不可能として登録されており、 Noneを返します。その他はグラデーションが登録されていません

tf.raw_opsページには、どの低レベルのopsにグラデーションが登録されているかが表示されます。

グラデーションが登録されていないfloatopを介してグラデーションを取得しようとすると、テープはサイレントにNone返すのではなく、エラーをスローします。このようにして、何かがうまくいかなかったことがわかります。

たとえば、 tf.image.adjust_contrast関数はtf.image.adjust_contrastラップしraw_ops.AdjustContrastv2 。これはグラデーションを持つことができますが、グラデーションは実装されていません。

image = tf.Variable([[[0.5, 0.0, 0.0]]])
delta = tf.Variable(0.1)

with tf.GradientTape() as tape:
  new_image = tf.image.adjust_contrast(image, delta)

try:
  print(tape.gradient(new_image, [image, delta]))
  assert False   # This should not happen.
except LookupError as e:
  print(f'{type(e).__name__}: {e}')
LookupError: gradient registry has no entry for: AdjustContrastv2

この操作で区別する必要がある場合は、グラデーションを実装して登録するか( tf.RegisterGradientを使用)、他の操作を使用して関数を再実装する必要があります。

なしではなくゼロ

接続されていないグラデーションの場合、 Noneではなく0を取得すると便利な場合があります。 unconnected_gradients引数を使用して、 unconnected_gradientsていないグラデーションがある場合に何を返すかを決定できます。

x = tf.Variable([2., 2.])
y = tf.Variable(3.)

with tf.GradientTape() as tape:
  z = y**2
print(tape.gradient(z, x, unconnected_gradients=tf.UnconnectedGradients.ZERO))
tf.Tensor([0. 0.], shape=(2,), dtype=float32)