このページは Cloud Translation API によって翻訳されました。
Switch to English

勾配と自動微分の紹介

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

自動微分と勾配

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

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

セットアップ

import numpy as np
import matplotlib.pyplot as plt

import tensorflow as tf

勾配の計算

TensorFlowは、自動的に区別するために、 転送パス中にどの操作がどの順序で行われるかを覚えておく必要があります。次に、 バックワードパス中に、TensorFlowはこの演算のリストを逆順でトラバースして勾配を計算します。

グラデーションテープ

TensorFlowは、自動微分のためのtf.GradientTape APIを提供します。つまり、一部の入力、通常はtf.Variable sに関する計算の勾配を計算します。 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)

両方の変数に関してyの勾配を取得するには、両方をソースとして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': tf.Variable(tf.random.normal((3, 2)), name='w'),
    'b': tf.Variable(tf.zeros(2, dtype=tf.float32), name='b')
}

grad = tape.gradient(loss, my_vars)
grad['b']

モデルに関する勾配

収集するために、それの普通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で呼び出されなかったので、それに関して勾配は計算されません。

# dy = 2x * dx
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_dx = 2 * y, where y = x ** 2
print(tape.gradient(z, y).numpy())
18.0

デフォルトでは、 GradientTape.gradient()メソッドが呼び出されるとすぐに、 GradientTapeによって保持されているリソースが解放されます。同じ計算で複数の勾配を計算するには、 persistent勾配テープを作成します。これにより、tapeオブジェクトがガベージコレクションされたときにリソースが解放されるため、 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

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

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

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

    効率を上げるために、一部の操作( 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 sやwhile sなど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

よくあるエラーの1つは、 Variable.assignを使用してtf.Variableを更新する代わりに、 tf.Tensorを誤ってtf.Variable置き換えること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定数または変数が作成される可能性があります。

# The x0 variable has an `int` dtype.
x = tf.Variable([[2, 2],
                 [2, 2]])

with tf.GradientTape() as tape:
  # The path to x1 is blocked by the `int` dtype here.
  y = tf.cast(x, tf.float32)
  y = tf.reduce_sum(x)

print(tape.gradient(y, x))
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は内部状態とその値があります。変数を使用すると、状態が読み取られます。変数に関して勾配を計算することは正常ですが、変数の状態は勾配計算がそれ以上戻るのを妨げます。例えば:

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 + x2)
None

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

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

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

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

勾配が登録されていない浮動小数点演算を介して勾配を取得しようとすると、テープはNone通知せずにエラーをスローします。このようにして、何かが間違っていることを知ることができます。

たとえば、 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する必要がある場合は、勾配を実装して登録する( tf.RegisterGradientを使用)か、他の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)