深層学習

自動微分

数値を揺らさず、計算グラフに沿って連鎖律を自動適用し、入力数と出力数に応じた向きで微分を計算する手続き。

  • A|中核
  • 深層学習

数式の記号で止まったら 記号の読み方 (∂・⊙・転置・上付き添字を、読み方から)

ひとことで言うと

自動微分は、プログラムとして実行した演算を計算グラフに記録し、各演算の局所的な微分を連鎖律でつないで導関数を求める仕組みです。式を人手で展開せず、関数を値として近似もしません。

レシートの合計を作る途中で、各商品の値段と合計への寄与を同時に記録しておく仕組みです。あとから「この商品の値段を少し変えたら合計はいくつ動くか」を、レシート全体を作り直さずにたどれます。

なぜ必要か

方法やっていること性質
数値微分入力をずらした関数値の差から傾きを近似刻み幅と評価回数に左右される
記号微分数式を変形して導関数を作る正確だが複雑なプログラムを追う
自動微分演算の局所微分を連鎖律で合成プログラムをそのまま微分できる

損失は通常スカラーなので、全パラメータへ勾配を返すリバースモードが適します。誤差逆伝播法は、その適用例です。

ここで困るのは、学習で必要な微分が「モデル全体を1つの式として書けるか」ではなく、実行時に分岐やテンソル演算を含むプログラムの中から生じる点です。パラメータを1つずつ少し動かして損失を測れば勾配らしき値は得られますが、パラメータが増えるほど関数評価の回数も増え、刻み幅によっては差が丸め誤差に埋もれます。複雑な式を展開して導関数を作る方法も、演算の組み合わせを変えるたびに式を作り直す負担が残ります。

自動微分はこの二つの負担の位置を変えます。微分可能なプログラムを実行するときに、値を計算した順序と各演算の局所的な微分を記録しておき、必要な方向へ連鎖律を適用します。そのため、求めるのは抽象的な関数の式ではなく、その実行で通った計算グラフに対する勾配です。同じコードでも入力や分岐が変われば、その実行に対応するグラフが作られます。

仕組み

入力を x∈Rn\mathbf{x}\in\mathbb{R}^n、出力を y=f(x)∈Rm\mathbf{y}=f(\mathbf{x})\in\mathbb{R}^m とします。フォワードモードは入力側から微分を運び、入力変化 x˙\dot{\mathbf{x}} に対して

y˙=Jx˙\dot{\mathbf{y}} = J\dot{\mathbf{x}}

を計算します。JJ はヤコビアン、y˙\dot{\mathbf{y}} は出力方向です。入力数 nn が少ないと有利です。

リバースモードは出力側から、出力方向 v\mathbf{v} に対して

vTJ\mathbf{v}^{\mathsf{T}}J

を計算します。v\mathbf{v} は出力側の重みです。出力数 mm が少ないと有利です。スカラー損失なら m=1m=1 なので、1回で全パラメータの勾配が得られます。PyTorch の backward() が引数なしで使えるのはこの形です。ベクトル出力では出力方向を渡します。

この二つの式は、ヤコビアン JJ 全体を実際に表として作るという意味ではありません。フォワードモードは x˙\dot{\mathbf{x}} という入力側の一方向に対する積を、リバースモードは v\mathbf{v} という出力側の一方向に対する積を、連鎖律に従って演算ごとに流します。したがって「入力数が少ない」「出力数が少ない」という比較は、テンソルの見た目の大きさではなく、必要な方向の本数を数える判断です。入力から出力への写像を何度も評価する場合と、1つの損失を多数のパラメータへ戻す場合とで、向いているモードが逆になります。

スカラー損失でリバースモードが効くのは、出力側の方向が1本で済むからです。まず損失からその1本の勾配を出力へ渡し、各演算の局所微分を通じて入力やパラメータへ分配します。パラメータが多いこと自体は、リバースモードを選ぶ理由というより、1回の逆向き計算で多くのパラメータの勾配を回収できる結果として現れます。逆に、ベクトル出力の各成分について別々の勾配が必要なら、出力方向を変えた計算が必要になります。

フォワード中は、微分に必要な演算履歴や中間テンソルが保持されます。出力の grad_fn は、そのテンソルを作った演算の逆向き計算を指します。backward() はグラフを出力側からたどり、リーフの .grad に勾配を加算します。グラフは backward() 後に使い捨てで、次のフォワードで作り直されます。

ここで保存されるのは最終出力だけではありません。逆向きの局所微分を評価するため、演算によっては順方向で得た入力や中間テンソルが必要です。これが自動微分を便利にする一方で、長い計算や大きなバッチではグラフの保持がメモリ使用量に影響する理由です。grad_fn があることは「そのテンソルまでの履歴がある」ことを示しますが、途中のテンソルの勾配が自動的に .grad に残ることとは別です。通常、利用者が直接確認するのはリーフに蓄積された値です。

また、.grad への加算と計算グラフの寿命は別々に管理されます。前者を消さずに backward() を繰り返せば、同じリーフに前回分と今回分が足し合わされます。後者は一度の逆向き計算で解放されるため、同じ履歴を再利用する必要がある場合だけ保持を指定します。この二つを混同すると、「勾配が二倍になった」問題と「グラフがもうない」問題を同じ原因だと誤認します。

試験でどう問われるか

問われ方正解に寄る条件引っかけ
3手法の比較自動微分は連鎖律の機械的適用刻み幅を選ぶ手法とする
モードの選択入力数が少なければフォワード、出力数が少なければリバース常にリバースが速いとする
損失の勾配損失はスカラーなのでリバースモードが適するパラメータ数が少ないからフォワード、と入力と出力を取り違える
計算グラフ演算履歴を保持し、backward() で利用するグラフは一度だけ固定されるとする
PyTorch の勾配.grad は加算される。次の更新前に zero_grad() などで消すbackward() が毎回 .grad を上書きする

引っかけは、用語の定義ではなく「何を数えているか」をずらしてきます。フォワードとリバースの有利不利は、常に一方が高速という話ではなく、入力方向と出力方向の本数の比較です。損失がスカラーだからリバースが適する、という説明を「パラメータが多いからフォワードは使えない」と読み替えると、入力数が少ない別の問題で判断を誤ります。

PyTorch の問題では、requires_grad=True、grad_fn、リーフの .grad も同じものとして扱わないことがポイントです。前者は履歴を追う対象にする指定、二つ目は生成元の演算への手がかり、三つ目は逆伝播で蓄積される勾配の格納先です。また、no_grad() は勾配をゼロにする操作ではありません。記録そのものを止める文脈なので、学習用の順方向計算をそこへ入れると、後から backward() するための履歴がありません。

実装で確かめる

PyTorch では、微分対象のリーフに requires_grad=True を付けます。中間テンソルの grad_fn は履歴を示しますが、通常その .grad は保持されません。PyTorch 依存のためコードは実行対象外です。

import torch

x = torch.tensor([2.0], requires_grad=True)
y = x * x + 3 * x
y.backward()
print(x.grad)

y = x * x
y.backward()
print(x.grad)
x.grad.zero_()

with torch.no_grad():
    z = x * x
print(z.requires_grad)

no_grad() は推論や固定パラメータの順方向計算に使います。学習用のフォワードを囲むとグラフができません。同じグラフで複数回 backward() する場合は保持指定が必要です。

この短い例では、最初の backward() の後も x.grad の値が残り、二度目の計算結果がそこへ加わります。したがって二度目の出力を微分した値だけを見たいなら、二度目の前に勾配をゼロにします。一方、y は最初の逆伝播で使ったグラフに属するため、同じ y をもう一度たどるにはグラフを保持する指定が必要です。勾配のリセットとグラフの保持は、似たタイミングで現れても解決している対象が異なります。

取り違えやすいもの

用語自動微分との切り分け
数値微分関数値の差分による近似。自動微分は演算の微分を連鎖律で合成する
記号微分数式そのものを変形して導関数を得る。自動微分は実行グラフをたどる
フォワードモード入力方向から出力方向へ Jx˙J\dot{\mathbf{x}} を運ぶ。入力数が少ない側で選ぶ
リバースモード出力方向から入力側へ vTJ\mathbf{v}^{\mathsf{T}}J を運ぶ。スカラー損失で選ぶ
誤差逆伝播法リバースモード自動微分をニューラルネットの損失勾配に適用した呼び名
勾配降下法自動微分が返した勾配を使ってパラメータを更新する側の手法

数値微分との境界は「近似かどうか」です。数値微分は入力を実際にずらして関数値の差を取り、刻み幅を含む近似値を得ます。自動微分は入力をずらさず、実行された加算・乗算などの局所微分を組み合わせるため、数値微分の刻み幅を選ぶ問題を引き継ぎません。ただし、だからといって自動微分が数式を記号として変形しているわけでもありません。

記号微分との境界は「式を作るか、演算をたどるか」です。記号微分は式の形を変形して導関数を得るので、式が大きくなると表現の複雑さが問題になります。自動微分はプログラムの実行順序に沿った計算グラフを使うため、最終的な導関数を人間が展開した形で保持する必要がありません。

誤差逆伝播法との境界は、対象と呼び名の広さです。リバースモード自動微分は任意の計算グラフで使える一般的な枠組みで、ニューラルネットワークの損失から重みへ勾配を戻す場面を特に誤差逆伝播法と呼びます。どちらも連鎖律を使いますが、前者はモードと計算グラフの仕組み、後者はその仕組みを学習の勾配計算に適用した文脈を指します。

想起チェック

数値微分・記号微分・自動微分の違いは

数値微分は関数値の差分、記号微分は数式変形、自動微分は実行された演算の局所微分を連鎖律で合成します。

フォワードモードとリバースモードは何の大小で選ぶか

入力数が少なければフォワード、出力数が少なければリバースです。スカラー損失は出力数が1なのでリバースが適します。

backwardを繰り返すときに起きることは

勾配は .grad に累積されます。再利用するグラフを保持する指定と、不要な前回分を消す処理を分けて考えます。

出典