自動微分

自動微分（じどうびぶん、automatic differentiation, autodiff, AD）やアルゴリズム微分（algorithmic differentiation）とは、プログラムで定義された関数を解析し、関数の値と同時に偏導関数の値を計算するアルゴリズムである。
自動微分は複雑なプログラムであっても加減乗除などの基本的な算術演算や基本的な関数（指数関数・対数関数・三角関数など）のような基本的な演算の組み合わせで構成されていることを利用し、これらの演算に対して合成関数の偏微分の連鎖律を繰り返し適用することによって実現される。自動微分を用いることで偏導関数値を少ない計算量で自動的に求めることができる。

**他の微分方式との違い**
図1: 自動微分と記号微分の関係
自動微分は以下のどちらとも異なる。- 記号微分・数式微分（symbolic differentiation） - 原関数を表す数式から数式処理により導関数を導出する。数式処理システムで実装されている。- 数値微分（numerical differentiation） - 原関数の値から数値的に微分係数を算出する
記号微分は効率が悪くなりやすく、プログラムで定義された関数から微分表現を導くのは困難であるという問題がある。一方、数値微分では離散化の際の丸め誤差や桁落ちによる精度の低下が問題である。さらに、どちらの手法も計算量や誤差の関係で高次の微分係数を求めることが難しい。また、勾配を用いた最適化で必要となる、多くの入力変数を持つ関数に対する偏微分値の計算を行うには速度が遅い。自動微分はこれらの古典的手法の問題を解決する。
また、自動微分は計算フローを追いかけることで計算できるので、分岐（if文）やループや再帰を含むようなアルゴリズムでも偏微分できる。

**合成関数の偏微分の連鎖律**
自動微分の基本原理は、合成関数の偏微分の連鎖律を用いた偏微分の分解である。
合成関数の偏微分の連鎖律とは  の時、下記が成立することである。


**2種類の自動微分**
自動微分は2種類に分けられ、それぞれ- ボトムアップ型自動微分（フォーワード・モード、フォーワード・アキュムレーション、タンジェント・モード、狭義の自動微分）- トップダウン型自動微分（リバース・モード、リバース・アキュムレーション、随伴モード、高速自動微分）
と呼ばれる。
ボトムアップ型自動微分では連鎖律を内側から外側に計算し（∂w/∂xを計算した後で ∂y/∂w を計算する）、トップダウン型自動微分では外側から内側に計算する。
使い分けは、入力が n 次元、出力が m 次元とした場合、以下の違いがある。- n  m ならばトップダウン型の方が計算量が少ない。トップダウン型の計算回数はm回。
機械学習において、評価値はほぼ常に m = 1 の実数なので、トップダウン型が使われる。機械学習で用いられる多層パーセプトロンのバックプロパゲーションはトップダウン型自動微分の特殊なケースである。
ボトムアップ型はR.E. Wengertが1964年に発表したが、2ページの論文で特に名前を付けていない。Andreas Griewank によると、トップダウン型を誰が最初に発明したのか判然としないが、計算量を減らす工夫として1960年代後半には提案されていた。

**ボトムアップ型自動微分**
ボトムアップ型自動微分では最初に偏微分を行う入力変数を固定し、それぞれの部分式を再帰的に計算する。手計算においては連鎖律において内側の関数を繰り返し置き換えていくことに相当する。

多変数の場合はヤコビ行列の積として一般化できる。
トップダウン型自動微分と比較すると、ボトムアップ型自動微分は自然であり、偏微分に関する情報の流れが計算の順序と一致するため簡単に実行できる。それぞれの変数にその偏導関数値 

の計算値を保存する領域を付け加えるだけで、変数値の計算と同時に偏導関数値を計算することができる。
連鎖律より、 が計算グラフで先行ノードを持つ場合、
, j ∈ {i の先行ノード}
図2: ボトムアップ型自動微分の計算グラフの例
例として次の関数を考える。

それぞれの部分式を中間変数  としてラベル付けした。最初に  としている。
どの入力変数で偏微分するかによって  や  の初期値が異なる。 に関する偏微分を計算する場合の初期値は、
 
となる。初期値が決まったら下の表に示す連鎖律に従って各偏導関数値を順に計算していく。図2はこの処理を計算グラフとして表している。 が求めたい値である。

この関数 f に対する勾配を求めるためには  だけではなく  も求める必要がある。そのため、初期値を  とした同様の計算を追加で行わなければならない。
勾配を求める場合に必要なボトムアップ型自動微分の実行回数は入力変数の個数と等しく、トップダウン型自動微分では出力変数の個数に等しい。ボトムアップ型やトップダウン型の自動微分を1回行うのに必要な計算量は、元のプログラムの計算量に比例する。そのため、偏微分する関数f : ℝn → ℝm が n ≪ m を満たす場合、ボトムアップ型自動微分はトップダウン型自動微分よりも効率的である。

**トップダウン型自動微分**
トップダウン型自動微分では、はじめに偏微分される出力変数を固定して、それぞれの部分式に関する偏導関数値を再帰的に計算する。手計算においては部分式を連鎖律における外側の関数の偏微分で繰り返し置き換えていくことに相当する。

トップダウン型自動微分において、求める値はボトムアップ型の随伴date=2016年3月であり、上線  で表す。これは中間変数  に関する偏微分

である。連鎖律より、 が計算グラフで後続ノードを持つ場合、以下のように計算できる。 は1で、y以外のノードで後続ノードがなければ0になる。
, j ∈ {i の後続ノード}
トップダウン型自動微分は連鎖律を外側から内側（図3の計算グラフでは上から下）にたどっていく。
偏微分する関数f : ℝn → ℝm が n ≫ mを満たすとき、トップダウン型自動微分はボトムアップ型自動微分よりも効率的である。例示した関数は m = 1 のスカラー値関数（1つだけ値を返す関数）なので、勾配を計算するには計算グラフを一度たどるだけでよい。これは n = 2回計算グラフをたどる必要があったボトムアップ型自動微分の計算量の半分である。
しかし、トップダウン型自動微分は、テープやWengert リスト（なお、Wengert はボトムアップ型を1964年に発表したが、トップダウン型を発表したわけではない）と呼ばれるサイズ変更が可能な配列に中間変数  の計算結果を追記していく必要があるためメモリ使用量の点で不利であり、計算グラフが巨大になるとメモリ使用量が問題となる可能性がある。この問題は保存する中間変数を限定し、必要な中間変数を再計算することによって軽減される。
図3: トップダウン型自動微分の計算グラフの例
トップダウン型自動微分を用いて偏導関数値を計算するための演算は以下の通りである（関数値を求める時と順番が逆であることに注意）。 と  が求めたい値である。

最後の  は以下の理由で成立する。図3で  から  と  に矢印が引かれていることより、 と  は  の後続ノードである。合成関数の偏微分の連鎖律より  であり、後は  と  の定義を代入すれば良い。どの中間変数がどの中間変数に影響を及ぼすか記録する必要がある。

**テープを使用しない場合**
テープを使用しない場合は、以下のように Python で実装できる。
import math
class Var:
def __init__(self, value, children=None):
self.value = value
self.children = children or 
self.grad = 0
def __add__(self, other):
return Var(self.value + other.value, (1, self), (1, other))
def __mul__(self, other):
return Var(self.value * other.value, (other.value, self), (self.value, other))
def sin(self):
return Var(math.sin(self.value), (math.cos(self.value), self))
def calc_grad(self, grad=1):
self.grad += grad
for coef, child in self.children:
child.calc_grad(grad * coef)
# 例: f(x, y) = x * y + sin(x)
x = Var(2)
y = Var(3)
f = x * y + x.sin()
# 偏微分の計算
f.calc_grad()
print("f =", f.value)
print("∂f/∂x =", x.grad)
print("∂f/∂y =", y.grad)

**テープを使用する場合**
テープを使用する場合のアルゴリズムの実装の流れは以下の通りである。
# テープをサイズ変更が可能な動的配列として用意する。テープの要素は中間変数  毎に作り、以下の5項目を記録する。
## 計算内容（足し算や掛け算など）
## 計算グラフの入辺：計算に使用した引数のテープ上のインデックスのリスト。 なら  と 。
## 計算グラフの出辺：この中間変数がどの計算で使われたかのテープ上のインデックスのリスト。計算しながら追記していく。 なら  と 。
##  の値
##  の値（最初は未定）
#  の値を計算していく。その際、上記の内容をテープに追記していく。また、テープには、どの中間変数の計算で、どの中間変数を使用したかも追記する（上記の3番）。同一の中間変数を2回以上計算しないように注意が必要。
# テープの最後の  を 1 として、その1つ前から逆順に  を計算していく。

**多次元配列（テンソル）の場合**
スカラー値ではなく NumPy のような多次元配列（テンソル）を扱う場合も処理すべき内容は同じである。 を1つの処理単位として実装する。これを vector-Jacobian product (VJP) や L-operator (Lop) と言う。行列積の VJP は行列積で実装できる。ただし、ある軸での和（NumPyのsum）の VJP は、その軸で値を繰り返して  の軸を復元する処理になるなど、直観的で無い物もある。ハーバード大学の HIPS が開発していた Autograd は、ほぼ全ての NumPy の関数の VJP を実装していて、簡潔に実装しているので参考になる。なお  は Jacobian-vector product (JVP) や R-operator (Rop) と言い、ボトムアップ型で使用する。

**行列積**
行列積の vector-Jacobian product (VJP) は、以下のように求める。 を考え、 を求める。行列積を行列の要素で表記する。
 
行列積はかけ算して足し算をするが、足し算のトップダウン型自動微分は  を上から降ろし、かけ算の自動微分は相手側の項を  とかけるということより、 が求まる。
 
これを要素表記から行列表記に直すと、転置行列との行列積になる。
 
同様に、 より  である。

**2次元畳み込み**
畳み込みニューラルネットワークのバイアス項無しの2次元畳み込み（Conv2d）は、Hは縦、Wは横、Cはチャンネルとして、入力を 、カーネルを 、出力を  とした場合、要素表記で下記式で表される。
 
行列積と同じくこれも積和演算なので、 は下記となる。
 
は擬似コードを使い、 から始めて、下記の6重ループで計算できる。
for  in それぞれの値域
+= 
数式で書くと  となる。
バイアス項  は  であるが、 と  となる。

**2次元最大値プーリング**
畳み込みニューラルネットワークのカーネルの大きさが(p,q)の2次元最大値プーリング（MaxPool2d）は要素表記では下記となるが、
 
これの  は、最大値をとる所に  を戻せば良いので下記となる。argmax が複数の k, l で最大となる場合は1つ選ぶ。
 

**ReLU**
畳み込みニューラルネットワークの活性化関数の ReLU は要素表記では  だが、これの  は、 の所に  を戻せば良いので下記となる。
 

**二重数を用いた自動微分**
ボトムアップ型自動微分は実数の代数に（元を）添加して新しい算術を導入することによって可能である。全ての数（通常の実数）に対して、その数における関数の微分を表現する追加の成分が足され、全ての算術演算がこの添加代数に拡張される。すなわち二重数の代数である。このアプローチはAutomatic differentiation#Operational calculus on programming spacesの理論（つまり双対空間のテンソル代数）によって一般化される（Automatic differentiation#Analytic programming spaceを見よ)。
各数  を数  に置き換える。ここで  は実数だが、 は  を満たすabstract numberである（無限小；滑らかな無限小解析も参照）。ちょうどこれだけを用いて通常の演算が得られる：

引き算と割り算についても同様である。
いまやこの拡張算術のもとで多項式を計算できる。もし  ならば、
ここで  は  の最初の引数に対する微分であり、（種と呼ばれる）は任意に取れる。
上に述べたように、この新しい算術は、順序対（ と書かれる元）と、最初の成分に対しては通常の算術を、第二の成分に対しては一階微分の算術を、それぞれ与えたものからなる。多項式に関する上の結果を解析関数に広げれば、新しい算術に対する、基本的な算術と幾つかの標準的な関数のリストが得られる：

一般に、プリミティヴの関数  に対して、

ここで  と  はそれぞれ  の最初と2番目の引数に対する微分である。
基本的な二項算術演算を（実数と二重数の）混在した引数に対して、つまり順序対  と実数  に対して適用するとき、まずこの実数を  に引き上げる（lifting）。関数  の  に於ける微分はいまや  に上の算術を使って計算することによって得られる。これは  を結果として与える。

**ベクトル引数と関数**
多変数関数は、方向微分作用素を用いることで、一変数関数の場合と同様の効率と仕組みで取り扱える。つまり、 の  における  方向微分  を計算したい場合、それは  を上と同様の算術を使って計算すればよい。もし  の全ての要素が望みならば、 個の関数評価が要求される。ここで、多くの最適化アプリケーションでは、実際には方向微分があれば十分である。

**高階・多変数**
上の算術は多変数関数の二階やもっと高階の微分の計算の為に一般化出来る。しかし、その算術規則は直ちに極めて複雑なものとなる：複雑性は最高次の微分の次数に対して二次関数的となる。その代わりに、途中で打ち切った（truncated）テイラー多項式の代数を使用できる。結果得られる算術（一般化された二重数の上で定義された）は、関数を新しいデータ型であるかのように使って、効率的に計算することを可能にする。ひとたび関数のテイラー多項式が分かれば、その導関数たちは容易に抽出できる。
厳密で一般的な定式化はAutomatic differentiation#Tensor series expansionを通してAutomatic differentiation#Operational calculus on programming spacesを用いることにより達成される。

**実装**
自動微分の実装方法には大きく分けて、ソースコードの変換とオペレータオーバーローディングによる方法の2つがある。

**ソースコード変換**
図4: ソースコード変換の動作例
関数値を求める関数を記述した元のソースコードから、偏導関数値を計算する処理を含んだプログラムを自動的に生成する手法である。ソースコード変換はあらゆるプログラミング言語で実装でき、コンパイル時の最適化を行いやすいが、自動微分ツールの作成は難しい。

**オペレータオーバーローディング**
図5: オペレータオーバーローディングの動作例
この手法は演算子のオーバーロードがサポートされているプログラミング言語で記述されたソースコードに対してのみ適用可能である。元のソースコードの流れを大きく変更することなく実現できるが、基本データ型の変更などの小さな変更は必要である。
ボトムアップ型自動微分をオペレータオーバーロードで実現するのは容易である。トップダウン型自動微分についても可能であるが、現状のコンパイラではボトムアップ型自動微分と比べると最適化の面で不利である。

**ソフトウェア**
自動微分を実装したライブラリなどのソフトウェアが多数存在する。2010年代の第3次人工知能ブームの際にディープラーニングに自動微分が必要なため、TensorFlowやPyTorchなどトップダウン型の自動微分を含むライブラリが多数作られた。

**脚注**

**参考文献**
 伊理正夫、「 高速自動微分法(第2回年会特別講演)」『応用数理』 1993年 3巻 1号 p.58-66, 10.11540/bjsiam.3.1_58, 日本応用数理学会

**外部リンク**
  www.autodiff.org - Community Portal for Automatic Differentiation