.. _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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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
pytorchmxnetjaxtensorflow
.. 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. いつ順方向微分を使い、いつ逆方向微分を使うべきだろうか。ヒント: 必要な中間データ量、各ステップの並列化可能性、関係する行列やベクトルのサイズを考えよ。