はじめに
ディープラーニング関連の書籍に出てくるベクトルや行列を含む微分の計算について思うところを書いてみました。
発端は、書籍『詳解ディープラーニング』の、リカレントニューラルネットワークの誤差逆伝播に関する式なのですが、最終結果も途中計算もおかしいと思い、計算し直したことです。
計算をやっているうちに思った以下のようなことをまとめました。
- ベクトルをベクトルで微分したものを行列として定義するときの方法が一意でない
- どちらの定義を選んでも、そこから導かれる各種公式が行列積の順序を意識したり、転置が入ったりして面倒。
- この本に限らず、無理に行列使わず、テンソル表記使えば良いのに
必要な公式
ベクトルのベクトルによる微分 $∂\mathbf{y}/∂\mathbf{x}$ を行列で表すには、文献によって2通りの定義があり、どちらを使うかによって微分の公式も変わってきます。1
第1の定義
$$\left(\frac{∂\mathbf{y}}{∂\mathbf{x}}\right)_{ij} := \frac{∂y_j}{∂x_i}$$
第2の定義
$$\left(\frac{∂\mathbf{y}}{∂\mathbf{x}}\right)_{ij} := \frac{∂y_i}{∂x_j}$$
第1の定義では、以下の公式が成り立ちます。
スカラー関数 $f(\mathbf{u}(\mathbf{x}))$ について
$$\frac{∂f}{∂\mathbf{x}} = \frac{∂\mathbf{u}}{∂\mathbf{x}} \frac{∂f}{∂\mathbf{u}}$$
ベクトル関数 $\mathbf{y}(\mathbf{u}(\mathbf{x}))$ について
$$\frac{∂\mathbf{y}}{∂\mathbf{x}} = \frac{∂\mathbf{u}}{∂\mathbf{x}} \frac{∂\mathbf{y}}{∂\mathbf{u}}$$
ベクトル $\mathbf{A}\mathbf{x}$ について
$$\frac{∂(\mathbf{A}\mathbf{x})}{∂\mathbf{x}} = \mathbf{A}^T$$
これらが、第2の定義では、以下のように異なる形になります。
$$\frac{∂f}{∂\mathbf{x}} = \left(\frac{∂\mathbf{u}}{∂\mathbf{x}}\right)^T\frac{∂f}{∂\mathbf{u}} $$
$$\frac{∂\mathbf{y}}{∂\mathbf{x}} = \frac{∂\mathbf{y}}{∂\mathbf{u}} \frac{∂\mathbf{u}}{∂\mathbf{x}}$$
$$\frac{∂(\mathbf{A}\mathbf{x})}{∂\mathbf{x}} = \mathbf{A}$$
いずれの式にしても、右辺の掛け算の順序が意味をもっていることに注意してください。右辺の各項はベクトルまたは行列であり、積は交換できません。
問題設定
冒頭に挙げた本の題材は、リカレントニューラルネットワークの Backpropagation through time (BPTT) の計算です。
- 誤差関数 $E(\mathbf{h}(t))$
- 隠れ層 $\mathbf{h}(t) := f(\mathbf{p}(t))$
- 隠れ層の活性化前 $\mathbf{p}(t) := \mathbf{U}\mathbf{x}(t) + \mathbf{W}\mathbf{h}(t-1) + \mathbf{b}$
として、$∂E/∂\mathbf{p}(t-1)$ を求めるのがお題。$\mathbf{p}(t-1)$ は
$$\mathbf{p}(t-1) \to \mathbf{h}(t-1) \to \mathbf{p}(t) \to \mathbf{h}(t) \to E$$
のように誤差関数 $E$ に寄与しているので、これを逆順にたどる誤差逆伝播計算をすればよいわけです。
逆伝播のうち、$∂E/∂\mathbf{p}(t)$ までは $\mathbf{e}_h(t)$ として与えられています。
$$\mathbf{e}_h(t) := \frac{∂E}{∂\mathbf{p}(t)}$$
本に載っている結果は以下の通りですが、最初の変形からして間違っていると思われます…。
\begin{aligned}
\frac{∂E}{∂\mathbf{p}(t-1)} &= \frac{∂E}{∂\mathbf{p}(t)} \odot \frac{∂\mathbf{p}(t)}{∂\mathbf{p}(t-1)}\\
&= \mathbf{e}_h \odot \left(\frac{∂\mathbf{p}(t)}{∂\mathbf{h}(t-1)} \frac{∂\mathbf{h}(t-1)}{∂\mathbf{p}(t-1)}\right) \\
&= \mathbf{e}_h \odot (\mathbf{W} f'(\mathbf{p}(t-1))) \\
\end{aligned}
正しく計算してみる
第1の定義で計算する
\begin{aligned}
\frac{∂E}{∂\mathbf{p}(t-1)} &= \frac{∂\mathbf{p}(t)}{∂\mathbf{p}(t-1)} \frac{∂E}{∂\mathbf{p}(t)} \\
&= \frac{∂\mathbf{h}(t-1)}{∂\mathbf{p}(t-1)} \frac{∂\mathbf{p}(t)}{∂\mathbf{h}(t-1)} \mathbf{e}_h \\
&= \mathrm{diag}(f'(\mathbf{p}(t-1))) \mathbf{W}^T \mathbf{e}_h \\
\end{aligned}
$\mathrm{diag}(\mathbf{x})$ はベクトル $\mathbf{x}$ の成分を対角に並べた対角行列という意味です。ベクトルの要素積 $\odot$ という記号を使うなら、最後の結果は
$$f'(\mathbf{p}(t-1)) \odot \mathbf{W}^T \mathbf{e}_h$$
とも書けます。しかし、$∂\mathbf{h}(t-1)/∂\mathbf{p}(t-1)$ というのは本来行列であり、それがたまたま対角行列なので、ベクトルの要素積でも書けた、と捉えるべきです。
第2の定義で計算する
もう1つの定義を使っても、当然結果は同じになります。ただし、途中経過が違います。
\begin{aligned}
\frac{∂E}{∂\mathbf{p}(t-1)} &= \left( \frac{∂\mathbf{p}(t)}{∂\mathbf{p}(t-1)} \right)^T \frac{∂E}{∂\mathbf{p}(t)} \\
&= \left[ \frac{∂\mathbf{p}(t)}{∂\mathbf{h}(t-1)} \frac{∂\mathbf{h}(t-1)}{∂\mathbf{p}(t-1)} \right]^T \mathbf{e}_h \\
&= [ \mathbf{W} \mathrm{diag}(f'(\mathbf{p}(t-1))) ]^T \mathbf{e}_h \\
&= \mathrm{diag}(f'(\mathbf{p}(t-1))) \mathbf{W}^T \mathbf{e}_h
\end{aligned}
テンソル表記で計算するべき
このようにベクトルや行列の微分を入れると、定義にも任意性が残るし、変なところで転置が出てきたり、掛け算の順序も可換でなくなり、それらをいちいち覚えてもいられず、あまり筋の良いやり方と思えません。2
また、例えばベクトルを行列で微分した $∂\mathbf{p}/∂\mathbf{W}$ のような量は3階テンソル(プログラミング的に言えば、3次元配列)になり、そもそもベクトルや行列では表せなくなります。
この手の計算に慣れている立場からすると、普通、こういう場合は、ベクトルや行列を添字を使った成分表記して計算を進めます。テンソル表記と言っても良いです。テンソル表記で微分を計算すると、合成関数微分の公式通りの計算をすれば良いだけであり、混乱は生じません。
注意 : ただし、微分対象となる関数が行列やベクトルを使った典型的な形(1次形式、2次形式、内積...など)になっているとき、ベクトルや行列の種々の微分公式が威力を発揮するのも確かです。ニューラルネットの合成関数微分に関しては、合っていないと感じるということです。
今回の問題をテンソル表記で計算すると、以下のようになります(見づらいので、$e_h$ は $e$ と省略して書きます)。
\begin{aligned}
\frac{∂E}{∂p_i(t-1)} &= \sum_k\frac{∂p_k(t)}{∂p_i(t-1)} \frac{∂E}{∂p_k(t)} \\
&= \sum_{k,l} \frac{∂h_l(t-1)}{∂p_i(t-1)} \frac{∂p_k(t)}{∂h_l(t-1)} e_{k} \\
&= \sum_{k,l} \delta_{li} f'(p_l(t-1)) W_{kl} e_{k} \\
&= \sum_{k} f'(p_i(t-1)) W_{ki} e_{k}\\
&= \sum_{k} f'(p_i(t-1)) W^T_{ik} e_{k}
\end{aligned}
最後の結果をベクトルと行列で書き直せば、$\mathrm{diag}(f'(\mathbf{p}(t-1))) \mathbf{W}^T \mathbf{e}_h$ の 第 $i$ 成分であると分かります。
少し解説
テンソル表記の計算は、テンソル表記特有の添字の多さや和の多さ、クロネッカーのデルタの計算や行列の積の成分表示などに慣れる必要があるので、初学者向けの本では敬遠されるのだろうと思います。
和記号と添字の多さについては、ダミー添字と縮約の概念を知っておくと惑わされなくなります。
先ほどの2行目の式の $k, l$ はダミー添字です。
$$ \sum_{k,l} \frac{∂h_l(t-1)}{∂p_i(t-1)} \frac{∂p_k(t)}{∂h_l(t-1)} e_{k} $$
もともと、$∂E/∂p_i(t-1)$ という1階テンソルを計算しているので、上式も全体としては $i$ を添字とする1階テンソルになっているはずです。$k, l$ は和で縮約される添字であり、テンソルの成分として残るのは $i$ だけなのです。
あくまで全体として1階テンソルになるだけであり、右辺の和を取る前の各項を取り出すとそうとは限りません。右辺を順番に見ると、$∂h_l(t-1)/∂p_i(t-1)$, $∂p_k(t)/∂h_l(t-1)$ は2階テンソル、$e_{k}$ は1階テンソルです。
2行目から3行目は、$h_i(t-1)= f(p_i(t-1))$ より、
$$\frac{∂h_l(t-1)}{∂p_i(t-1)}=\delta_{li} f'(p_l(t-1))$$
としました。$p$ と $h$ は同じ成分間にのみ関数の関係を持っているので、1階テンソル同士の微分として書いた場合は、クロネッカーのデルタという特殊な2階テンソルを含んだもので書けます。行列でいえば、対角行列に相当します。
3行目から4行目はクロネッカーのデルタと縮約和が組み合わさったときの変形です。
$$\sum_{k,l} \delta_{li} f'(p_i(t-1)) W_{kl} e_{k} = \sum_{k} f'(p_i(t-1)) W_{ki} e_{k}$$
このように、クロネッカーのデルタと和が組み合わさると、和が消せるので、式が簡単になります。テンソル計算していて気持ち良い瞬間です。
最後の変形は、最終結果がベクトルと行列の積で表せるように転置をとっています。
$$\sum_{k} f'(p_i(t-1)) W_{ki} e_{k}= \sum_{k} f'(p_i(t-1)) W^T_{ik} e_{k}$$
$i \to ik \to k$ というように順番につながるように、添字を並び変える意識で変形しています。
-
ベクトルで微分・行列で微分公式まとめ:2通りの定義の存在にも言及されている素晴らしいまとめ。 ↩
-
ちなみに、一貫性のある定義をしようと考える場合は、元のベクトルとその微分成分を区別するのが良いと思われます。こららは互いに双対な関係になります。そして、元のベクトルの成分を行ラベルにして、双対な成分を列ラベルにする、というような約束をします。が、これ以上は立ち入りません。 ↩