.. _sec_autograd:
自動微分
========
:numref:`sec_calculus` で思い出したように、 導関数を計算することは、
深層ネットワークを学習させるために用いる
すべての最適化アルゴリズムにおいて 極めて重要なステップである。
計算自体は単純であるが、 手作業で求めるのは面倒で誤りも起こりやすく、
モデルが複雑になるほど こうした問題はさらに大きくなる。
幸いなことに、現代のあらゆる深層学習フレームワークは、\ *自動微分*\ (しばしば
*autograd*
と略される)を提供し、この手間のかかる作業を自動化してくれる。
データを各関数へ順に通していくと、
フレームワークは、各値が他の値にどのように依存しているかを追跡する
*計算グラフ*\ を構築する。 導関数を計算するには、
自動微分はこのグラフを逆向きにたどり、 連鎖律を適用する。
このように連鎖律を適用する計算アルゴリズムは *逆伝播*\ と呼ばれる。
autograd ライブラリは ここ10年ほどで大きな注目を集めてきたが、
その歴史は長く、 最初期の autograd に関する言及は
半世紀以上前にさかのぼります :cite:`Wengert.1964`\ 。
現代の逆伝播の核となる考え方は 1980年の博士論文にまでさかのぼり
:cite:`Speelpenning.1980`\ 、 1980年代後半にさらに発展した
:cite:`Griewank.1989`\ 。 逆伝播は勾配を計算するための
標準的な方法になっているが、唯一の選択肢ではない。 たとえば、Julia
プログラミング言語では 順伝播が用いられている
:cite:`Revels.Lubin.Papamarkou.2016`\ 。 方法を探る前に、 まずは
autograd パッケージを使いこなしよう。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import torch
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from mxnet import autograd, np, npx
npx.set_np()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from jax import numpy as jnp
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import tensorflow as tf
.. raw:: html
.. raw:: html
単純な関数
----------
関数 :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` を、 列ベクトル
:math:`\mathbf{x}` に関して微分したい としよう。 まず、\ ``x``
に初期値を割り当てる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = torch.arange(4.0)
x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([0., 1., 2., 3.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = np.arange(4.0)
x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
[07:04:30] ../src/storage/storage.cc:196: Using Pooled (Naive) StorageManager for CPU
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([0., 1., 2., 3.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = jnp.arange(4.0)
x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
No GPU/TPU found, falling back to CPU. (Set TF_CPP_MIN_LOG_LEVEL=0 and rerun for more info.)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([0., 1., 2., 3.], dtype=float32)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = tf.range(4, dtype=tf.float32)
x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
``y`` を :math:`\mathbf{x}` に関して微分する前に、
その勾配を保存する場所が必要である。
一般に、導関数を求めるたびに新しいメモリを割り当てることは避ける。
というのも、深層学習では 同じパラメータに関する導関数を
何度も連続して計算する必要があり、
メモリ不足に陥る危険があるからである。 スカラー値関数のベクトル
:math:`\mathbf{x}` に関する勾配は、 :math:`\mathbf{x}`
と同じ形状をもつベクトル値になる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# x = torch.arange(4.0, requires_grad=True) としてもよい
x.requires_grad_(True)
x.grad # 勾配の初期値はデフォルトで None
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# `attach_grad` を呼び出して、テンソルの勾配用メモリを確保する
x.attach_grad()
# `x` に関する勾配を計算した後は、`grad` 属性を通じて
# その値にアクセスできる。初期値は 0 である
x.grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([0., 0., 0., 0.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y = lambda x: 2 * jnp.dot(x, x)
y(x)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array(28., dtype=float32)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x = tf.Variable(x)
.. raw:: html
.. raw:: html
次に、\ ``x`` の関数を計算して、その結果を ``y`` に代入する。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y = 2 * torch.dot(x, x)
y
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor(28., grad_fn=)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# 計算グラフを構築するため、コードは `autograd.record` スコープ内にある
with autograd.record():
y = 2 * np.dot(x, x)
y
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array(28.)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from jax import grad
# `grad` 変換は、元の関数の勾配を計算する Python 関数を返す
x_grad = grad(y)(x)
x_grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([ 0., 4., 8., 12.], dtype=float32)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# すべての計算をテープに記録する
with tf.GradientTape() as t:
y = 2 * tf.tensordot(x, x, axes=1)
y
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
これで ``y`` を ``x`` に関して微分できる。 ``backward``
メソッドを呼び出す。 次に、\ ``x`` の ``grad``
属性を通じて勾配にアクセスできる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y.backward()
x.grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([ 0., 4., 8., 12.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y.backward()
x.grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
[07:04:30] ../src/base.cc:48: GPU context requested, but no GPUs found.
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([ 0., 4., 8., 12.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x_grad == 4 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([ True, True, True, True], dtype=bool)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x_grad = t.gradient(y, x)
x_grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
関数 :math:`y = 2\mathbf{x}^{\top}\mathbf{x}` の :math:`\mathbf{x}`
に関する勾配は :math:`4\mathbf{x}` になることはすでに分かっている。
これで、自動的に計算された勾配と
期待される結果が一致することを確認できる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad == 4 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad == 4 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y = lambda x: x.sum()
grad(y)(x)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([1., 1., 1., 1.], dtype=float32)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x_grad == 4 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
では、\ ``x`` の別の関数を計算して、その勾配を求めよう。 PyTorch
では、新しい勾配を記録しても 勾配バッファは自動的にはリセットされない。
その代わり、新しい勾配は すでに保存されている勾配に加算される。
この挙動は、 複数の目的関数の和を最適化したいときに便利である。
勾配バッファをリセットするには、 次のように ``x.grad.zero_()``
を呼び出す。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad.zero_() # 勾配をリセットする
y = x.sum()
y.backward()
x.grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([1., 1., 1., 1.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with autograd.record():
y = x.sum()
y.backward()
x.grad # 新しく計算された勾配で上書きされる
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([1., 1., 1., 1.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y = lambda x: x * x
# grad はスカラー出力関数に対してのみ定義される
grad(lambda x: y(x).sum())(x)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([0., 2., 4., 6.], dtype=float32)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with tf.GradientTape() as t:
y = tf.reduce_sum(x)
t.gradient(y, x) # 新しく計算された勾配で上書きされる
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
スカラーでない変数の逆伝播
--------------------------
``y`` がベクトルのとき、 ``y`` の ``x``
に関する導関数を表す最も自然な表現は、
*ヤコビアン*\ と呼ばれる行列である。 これは、\ ``y`` の各成分について
``x`` の各成分に関する偏導関数を含む。 同様に、\ ``y`` と ``x``
がより高次元であれば、
微分の結果はさらに高次のテンソルになることもある。
ヤコビアンは いくつかの高度な機械学習技法では現れるが、 より一般的には、
``y`` の各成分の勾配を ``x`` の成分ごとに合計して、 ``x``
と同じ形状のベクトルを得たいことが多いである。 たとえば、訓練例の
*バッチ* ごとに 別々に計算された損失関数の値を表すベクトルを
扱うことがよくある。 この場合、私たちが欲しいのは
各例ごとに個別に計算された勾配を足し合わせること だけである。
深層学習フレームワークは
スカラーでないテンソルの勾配の解釈がそれぞれ異なるため、 PyTorch
は混乱を避けるための手順をいくつか用意している。
スカラーでない対象に対して ``backward`` を呼び出すと、
それをスカラーに縮約する方法を PyTorch に伝えない限りエラーになる。
より形式的には、\ ``backward`` が
:math:`\partial_{\mathbf{x}} \mathbf{y}` ではなく
:math:`\mathbf{v}^\top \partial_{\mathbf{x}} \mathbf{y}`
を計算するようにするための ベクトル :math:`\mathbf{v}`
を与える必要がある。 この先は少し分かりにくいかもしれないが、
後で明らかになる理由により、 この引数(\ :math:`\mathbf{v}`
を表すもの)は ``gradient`` と名付けられている。 より詳しい説明は、Yang
Zhang の `Medium
の記事 `__
を参照しよ。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad.zero_()
y = x * x
y.backward(gradient=torch.ones(len(y))) # より高速: y.sum().backward()
x.grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([0., 2., 4., 6.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with autograd.record():
y = x * x
y.backward()
x.grad # y = sum(x * x) の勾配に等しい
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([0., 2., 4., 6.])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
import jax
y = lambda x: x * x
# jax.lax のプリミティブは XLA 演算の Python ラッパーである
u = jax.lax.stop_gradient(y(x))
z = lambda x: u * x
grad(lambda x: z(x).sum())(x) == y(x)
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([ True, True, True, True], dtype=bool)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with tf.GradientTape() as t:
y = x * x
t.gradient(y, x) # y = tf.reduce_sum(x * x) と同じ
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
計算の切り離し
--------------
ときには、いくつかの計算を
記録された計算グラフの外に出したいことがある。 たとえば、入力を使って
補助的な中間項を作るが、 その項については勾配を計算したくないとする。
この場合、最終結果から それぞれの計算グラフを *切り離す* 必要がある。
次の簡単な例でこれを明確にしよう。 ``z = x * y`` かつ ``y = x * x``
であるが、 ``y`` を介して伝わる影響ではなく、 ``z`` に対する ``x`` の
*直接的* な影響に注目したいとする。 この場合、\ ``y``
と同じ値を持つ新しい変数 ``u`` を作れるが、 その
*来歴*\ (どのように生成されたか)は消去される。 したがって ``u``
はグラフ内に祖先を持たず、 勾配は ``u`` を通って ``x`` へ流れない。
たとえば、\ ``z = x * u`` の勾配を取ると、 結果は ``u`` になる
(\ ``z = x * x * x`` だから ``3 * x * x`` になると
予想したかもしれないが、そうはならない)。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad.zero_()
y = x * x
u = y.detach()
z = u * x
z.sum().backward()
x.grad == u
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
with autograd.record():
y = x * x
u = y.detach()
z = u * x
z.backward()
x.grad == u
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
grad(lambda x: y(x).sum())(x) == 2 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array([ True, True, True, True], dtype=bool)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
# 計算グラフを保持するために persistent=True を設定する。
# これにより t.gradient を複数回実行できる
with tf.GradientTape(persistent=True) as t:
y = x * x
u = tf.stop_gradient(y)
z = u * x
x_grad = t.gradient(z, x)
x_grad == u
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
この手順は ``z`` へ至るグラフから ``y`` の祖先を 切り離するが、 ``y``
へ至る計算グラフ自体は 残っているので、\ ``x`` に関する ``y`` の勾配を
計算できる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
x.grad.zero_()
y.sum().backward()
x.grad == 2 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor([True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
y.backward()
x.grad == 2 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array([ True, True, True, True])
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(a):
b = a * 2
while jnp.linalg.norm(b) < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
t.gradient(y, x) == 2 * x
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
勾配と Python の制御フロー
--------------------------
これまで、入力から出力までの経路が ``z = x * x * x``
のような関数によって 明確に定義されている場合を見てきた。
プログラミングでは、結果の計算方法に もっと自由度がある。
たとえば、補助変数に依存させたり、
中間結果に応じて条件分岐を行ったりできる。 自動微分を使う利点の一つは、
たとえ計算グラフを構築するのに Python
の制御フローの迷路を通り抜ける必要があっても
(たとえば条件分岐、ループ、任意の関数呼び出し)、
最終的に得られる変数の勾配を計算できることである。
これを示すために、次のコード片を考えよう。 ここでは ``while``
ループの反復回数と ``if`` 文の評価の両方が 入力 ``a``
の値に依存している。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(a):
b = a * 2
while b.norm() < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(a):
b = a * 2
while np.linalg.norm(b) < 1000:
b = b * 2
if b.sum() > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
from jax import random
a = random.normal(random.PRNGKey(1), ())
d = f(a)
d_grad = grad(f)(a)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
def f(a):
b = a * 2
while tf.norm(b) < 1000:
b = b * 2
if tf.reduce_sum(b) > 0:
c = b
else:
c = 100 * b
return c
.. raw:: html
.. raw:: html
以下では、この関数にランダムな値を入力として渡して呼び出す。
入力は確率変数なので、 計算グラフがどのような形になるかは分からない。
しかし、特定の入力に対して ``f(a)`` を実行するたびに、
特定の計算グラフが実現され、 その後 ``backward`` を実行できる。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
a = torch.randn(size=(), requires_grad=True)
d = f(a)
d.backward()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
a = np.random.normal()
a.attach_grad()
with autograd.record():
d = f(a)
d.backward()
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
d_grad == d / a
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
Array(True, dtype=bool)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
a = tf.Variable(tf.random.normal(shape=()))
with tf.GradientTape() as t:
d = f(a)
d_grad = t.gradient(d, a)
d_grad
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
デモのために少し作為的な関数 ``f`` であるが、
入力への依存は非常に単純である。
それは、区分的に定義されたスケールをもつ ``a`` の *線形* 関数である。
したがって、\ ``f(a) / a`` は定数成分からなるベクトルであり、 さらに
``f(a) / a`` は ``a`` に関する ``f(a)`` の勾配と一致するはずである。
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
a.grad == d / a
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
tensor(True)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
a.grad == d / a
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
array(True)
.. raw:: html
.. raw:: html
.. raw:: latex
\diilbookstyleinputcell
.. code:: python
d_grad == d / a
.. raw:: latex
\diilbookstyleoutputcell
.. parsed-literal::
:class: output
.. raw:: html
.. raw:: html
動的な制御フローは深層学習で非常に一般的である。
たとえば、テキストを処理するとき、 計算グラフは入力の長さに依存する。
このような場合、自動微分は統計モデリングにとって不可欠である。
というのも、勾配を *a priori* に計算することは不可能だからである。
議論
----
これで、自動微分の力を少し体験できたはずである。
導関数を自動的かつ効率的に計算するライブラリの発展は、
深層学習の実践者にとって 生産性を大きく高めるものであり、
彼らが単純作業ではないことに集中できるようにしてくれた。
さらに、autograd を使えば、 紙と鉛筆での手計算では
膨大な時間がかかりすぎるような巨大なモデルも設計できる。
興味深いことに、私たちは autograd を使ってモデルを
(統計的な意味で)\ *最適化*\ するが、 autograd ライブラリ自体の
(計算機科学的な意味での)\ *最適化*\ は、 フレームワーク設計者にとって
非常に重要な関心事である 豊かな研究分野である。
ここでは、コンパイラやグラフ操作の技術が用いられ、
最も迅速でメモリ効率のよい方法で結果を計算する。
今のところ、次の基本を覚えておきよう。 (i)
導関数を求めたい変数に勾配を付与する; (ii) 目的値の計算を記録する; (iii)
逆伝播関数を実行する; (iv) 得られた勾配にアクセスする。
演習
----
1. 2階導関数を計算するのが1階導関数よりはるかに高コストなのはなぜですか。
2. 逆伝播の関数を実行した直後に、もう一度実行してみよ。何が起こるか調べよ。
3. ``d`` を ``a`` に関して微分する制御フローの例で、変数 ``a``
をランダムなベクトルや行列に変えたらどうなるだろうか。この時点で、\ ``f(a)``
の計算結果はもはやスカラーではない。結果はどうなるか。どう分析すればよいだろうか。
4. :math:`f(x) = \sin(x)` とする。\ :math:`f` のグラフとその導関数
:math:`f'` のグラフを描いよ。\ :math:`f'(x) = \cos(x)`
であることは使わず、自動微分を用いて結果を得よ。
5. :math:`f(x) = ((\log x^2) \cdot \sin x) + x^{-1}` とする。\ :math:`x`
から :math:`f(x)` までの結果をたどる依存グラフを書いよ。
6. 連鎖律を用いて、前述の関数の導関数 :math:`\frac{df}{dx}`
を計算し、先ほど構築した依存グラフ上の各項に対応付けよ。
7. グラフと中間導関数の結果が与えられたとき、勾配を計算する方法はいくつかある。\ :math:`x`
から :math:`f` へ向かって一度計算し、もう一度 :math:`f` から
:math:`x` に向かってたどって計算しよ。\ :math:`x` から :math:`f`
への経路は一般に *順方向微分* として知られ、\ :math:`f` から
:math:`x` への経路は逆方向微分として知られている。
8. いつ順方向微分を使い、いつ逆方向微分を使うべきだろうか。ヒント:
必要な中間データ量、各ステップの並列化可能性、関係する行列やベクトルのサイズを考えよ。