TensorFlow 2.4 : ガイド : 基本 – 勾配と自動微分へのイントロダクション (翻訳/解説)
翻訳 : (株)クラスキャット セールスインフォメーション
作成日時 : 12/24/2020
* 本ページは、TensorFlow org サイトの Guide – TensorFlow Basics の以下のページを翻訳した上で
適宜、補足説明したものです:
* サンプルコードの動作確認はしておりますが、必要な場合には適宜、追加改変しています。
* ご自由にリンクを張って頂いてかまいませんが、sales-info@classcat.com までご一報いただけると嬉しいです。
- お住まいの地域に関係なく Web ブラウザからご参加頂けます。事前登録 が必要ですのでご注意ください。
- Windows PC のブラウザからご参加が可能です。スマートデバイスもご利用可能です。
人工知能研究開発支援 | 人工知能研修サービス | テレワーク & オンライン授業を支援 |
PoC(概念実証)を失敗させないための支援 (本支援はセミナーに参加しアンケートに回答した方を対象としています。 |
◆ お問合せ : 本件に関するお問い合わせ先は下記までお願いいたします。
株式会社クラスキャット セールス・マーケティング本部 セールス・インフォメーション |
E-Mail:sales-info@classcat.com ; WebSite: https://www.classcat.com/ |
Facebook: https://www.facebook.com/ClassCatJP/ |
ガイド : 基本 – 勾配と自動微分へのイントロダクション
自動微分はニューラルネットワークを訓練するためのバックプロパゲーションのような機械学習アルゴリズムを実装するために有用です。
このガイドでは、TensorFlow、特に eager 実行で勾配を計算できる方法を議論します。
セットアップ
import numpy as np import matplotlib.pyplot as plt import tensorflow as tf
勾配を計算する
自動的に微分するには、TensorFlow は forward パスの間に何の演算がどんな順序で発生するかを記憶する必要があります。それから、backward パスの間、TensorFlow は勾配を計算するためにこの演算のリストを逆の順序で辿ります。
勾配テープ (= Gradient tapes)
TensorFlow は自動微分 (つまり、ある入力、通常は tf.Variable に関して計算の勾配を計算します) のために tf.GradientTape API を提供します。TensorFlow は tf.GradientTape のコンテキスト内で実行される関連する演算を テープ 上に 記録します。それから TensorFlow は reverse モード微分 を使用して 記録された 計算の勾配を計算するためにそのテープを使用します。
ここに単純なサンプルがあります :
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 は任意の tensor 上で容易に動作します :
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])
各ソースに関する勾配はソースと同じ shape を持ちます :
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.Variables を tf.Module かそのサブクラス (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,)
テープが監視するものを制御する
デフォルトの動作は trainable tf.Variable にアクセスした後総ての演算を記録することです。このための理由は :
- backward パスで勾配を計算するためテープは forward パスでどの演算を記録するか知る必要があります。
- テープは中間出力への参照を保持しますので、貴方は不必要な演算を記録することを望みません。
- 最も一般的なケースは総てのモデルの訓練可能な変数に関する損失の勾配を計算することを伴います。
例えば以下は勾配を計算することに失敗します、何故ならば 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.Variable を監視するデフォルトの挙動を無効にするには、勾配テープを作成するときに watch_accessed_variables=False を設定します。この計算は 2 つの変数を使用しますが、変数の一つのための勾配にだけ接続します :
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 上では呼び出されていませんので、それに関しては勾配は計算されません :
# 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 により保持されたリソースは GradientTape.gradient() メソッドが呼び出されるとすぐに解放されます。同じ計算に渡る複数の勾配を計算するためには、永続的な勾配テープを作成します。これは 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
パフォーマンス上のノート
- 勾配テープ・コンテキスト内で演算を行なうことに関連して小さいオーバーヘッドがあります。これは殆どの eager 実行についてこれは気付くようなコストではありませんが、依然としてそれが必要とされる領域まわりだけでテープ・コンテキストを使用するべきです。
- 勾配テープは、backward パスの間の使用のために入力と出力を含む、中間結果をストアするためにメモリを使用します。
効率のため、(ReLU のような) 幾つかの ops は中間結果を保持する必要がなくそれらは forward パスの間に除去されます (= pruned)。けれども、テープ上で persistent=True を使用する場合、何も破棄されずに peak メモリ使用量はより高くなります。
非-スカラー・ターゲットの勾配
勾配は基礎的にはスカラー上の演算です。
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
同様に、ターゲット(s) がスカラーでない場合、合計の勾配が計算されます :
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')
制御フロー
テープは演算をそれらが実行されるにつれて記録しますので、(例えば if と while を使用する) Python 制御フローは自然に処理されます。
ここでは 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
制御ステートメント自身は微分可能ではありませんので、それらは勾配ベースの optimizer には不可視であることを単に覚えておいてください。
上のサンプルでは x の値に依拠して、テープは result = v0 or 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. 変数を tensor で置き換えた
「テープが監視するものを制御する」のセクションではテープが (tf.Tensor ではなく) tf.Variable を自動的に監視していることを見ました。
一つの一般的なエラーは、tf.Variable を更新するために Variable.assign を使用する代わりに、tf.Variable を不注意に tf.Tensor で置き換えることです。ここにサンプルがあります :
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. stateful オブジェクトを通して勾配を取った
状態は勾配を停止します。stateful オブジェクトから読むときテープは (それに繋がる) 履歴ではなく現在の状態を見れるだけです。
tf.Tensor はイミュータブルです。ひとたび 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 iterator と tf.queues は stateful で、それらに渡す tensor 上の総ての勾配を停止します。
登録されていない勾配
幾つかの tf.Operation は 非-微分可能として登録されて None を返します。他は 登録された勾配を持ちません。
tf.raw_ops ページはどの低位 ops が登録された勾配を持つかを示します。
登録された勾配を持たない float op を通して勾配をとることを試みる場合、テーブは 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
この ops を通して微分する必要がある場合、勾配を実装してそれを (tf.RegisterGradient を使用して) 登録するか、他の ops を使用して関数を再実装する必要があります。
None の代わりにゼロ
幾つかのケースでは unconnected 勾配のために None の代わりに 0 を得ることが便利でしょう。unconnected_gradients 引数を使用して unconnected 勾配を持つとき何を返すかを決定できます :
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)
以上