TensorFlow には GradientTape というクラスがあります。何もかもが不思議だったので調べました。
GradientTape とは勾配を求めるためのクラスです。精度の良い予測器をつくるには、適当に選んだパラメータで予想と、実際の結果を比較して、差が出来るだけ少なくなるようにパラメータを調整して行きます。このときにパラメータをどれだけ増やしたり減らしたりすれば良いのかを決めるのが勾配です。
それはさておき使ってみます。
import tensorflow as tf
tf.enable_eager_execution() # 計算グラフを即時実行する Eager execution を使います。
x = tf.constant(3.0) # TensorFlow に定数を作成
with tf.GradientTape() as g: # with 内部の計算を記録
g.watch(x) # x を記録
y = 2 * x # 微分する式
g.gradient(y, x).numpy() # x = 3 の時の y の傾きを求める。numpy() は見やすくするため入れました。
2.0
g.gradient(y, x) は微分を行い x = 3 の時の y の傾きを返します。2 * x を微分すると 2 なので、この場合 x がどんな時でも 2 が返ります。
このようにプログラム的に微分を行う方法を Automatic differentiation というそうです。
不思議な事その 1 は tf.constant(2.0) という TensorFlow の世界使うデータ構造です。こういうのを Tensor と呼びます。numpy のように行列演算を簡潔な構文で書くことが出来ます。numpy と違い可能であれば GPU のメモリにデータを保持します。
不思議な事その 2 は with tf.GradientTape() as g:
という構文です。with 文 は Python で自動的にファイルを閉じる文を書くのによく使われます。ブロックのはじめに指定したオブジェクトの __enter__()
が呼ばれ、出るときに __exit__()
が呼ばれます。as
で指定する変数は __enter__()
の戻り値で、target と呼びますが、こんなふうに with の外側からアクセス出来るとは今回始めて知りました。
不思議な事その 3 は、tf.GradientTape()
がブロック内の式を「記録」して微分を行う事。なぜこういう事が出来るのかさっぱり分からない。
上の例では g.watch(x)
を使って明示的に記録する変数を指定しましたが、ドキュメントによると、tf.Variable
などは自動的に記録対象に含まれるらしいので、試してみます。
x = tf.Variable(3.0) # TensorFlow に変数を作成
with tf.GradientTape() as g:
y = x * x # 微分する式
g.gradient(y, x).numpy()
6.0
x * x を微分すると 2 * x なので、x = 3 の時の傾き 6 が出ました。
さて、もっと意地悪なケースを見てみる。なんと if とかが入っても微分出来ます。
def with_if(x):
if x <= 0:
return x * 2
else:
return x * x * x
def with_if_gradient(n):
x = tf.Variable(n)
with tf.GradientTape() as g:
y = with_if(x)
print('n = %.1f の時: gradient = %.1f' % (n, g.gradient(y, x)))
with_if_gradient(-3.0)
with_if_gradient(0.0)
with_if_gradient(3.0)
n = -3.0 の時: gradient = 2.0
n = 0.0 の時: gradient = 2.0
n = 3.0 の時: gradient = 27.0
この例では
- x <= 0 の時は x * 2 なので微分すると傾きは常に 2
- x > 0 の時は x * x * x なので微分すると傾きは 3 * x * x となる。
ちゃんと答えも合ってます。不思議。tf.Variable が単純な値ではなく、微分に必要な式の情報を保持しているのだろうと想像しています。
g.gradient(y, x) の第二引数は分かりにくかった。第二引数は y の中で使われている変数なら何でもどの順番でも書ける。例えば以下のような多変数の式の微分を考える。
a = tf.Variable(3.0)
b = tf.Variable(4.0)
with tf.GradientTape() as g:
f = a ** 2 + 5 * b
g.gradient(f, [a, b])
[<tf.Tensor: id=2783, shape=(), dtype=float32, numpy=6.0>,
<tf.Tensor: id=2772, shape=(), dtype=float32, numpy=5.0>]
[a, b] の順に gradient に与えると [3, 4] の時の傾きは [6, 3] と出る。 ([2 * 3, 5])
gradient の戻り値は、第二引数の構造に応じてそのまま返る。例えば、トリッキーだが[b, [a]] のように与えると
a = tf.Variable(3.0)
b = tf.Variable(4.0)
with tf.GradientTape() as g:
f = a ** 2 + 5 * b
g.gradient(f, [b, [a]])
[<tf.Tensor: id=2820, shape=(), dtype=float32, numpy=5.0>,
[<tf.Tensor: id=2831, shape=(), dtype=float32, numpy=6.0>]]
傾きも第二引数に応じて [5, [6]] と出ます。
という事で、TensorFlow の Tensor は行列の計算の他に微分も出来るデータ構造だという事が分かりました。