![]() | ![]() | ![]() | ![]() |
自動微分とグラジエント
自動微分は、ニューラルネットワークをトレーニングするためのバックプロパゲーションなどの機械学習アルゴリズムを実装するのに役立ちます。
このガイドでは、特に熱心な実行において、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)
両方の変数に関する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': w,
'b': b
}
grad = tape.gradient(loss, my_vars)
grad['b']
<tf.Tensor: shape=(2,), dtype=float32, numpy=array([-8.143469 , 3.9130046], dtype=float32)>
モデルに関する勾配
収集するために、それの普通tf.Variables
にtf.Module
またはそのサブクラス(の1 layers.Layer
、 keras.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.watch
はx0
で呼び出されなかったため、それに関してグラデーションは計算されません。
# 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_dx = 2 * y, where y = x ** 2
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')
制御フロー
グラデーションテープは実行時に操作を記録するため、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
定数または変数を作成する可能性があります。
# 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
は、内部状態、 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
ページには、グラデーションが登録されている低レベルのtf.raw_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)