LoginSignup
7
1

More than 5 years have passed since last update.

TensorflowにおけるRop (ヤコビアンとベクトルの積)の計算方法

Posted at

※本記事はTensorflowにおけるLopに記載された内容を前提としています

入力を$M$次元の$x$, 出力を$N$次元の$y$とする関数$f(x)$について考えます。このとき、ヤコビアン$J$は以下のように定義されます。

  J = \left(
    \begin{array}{cccc}
      \frac{\partial y_1}{\partial x_1} & \frac{\partial y_1}{\partial x_2} & \ldots & \frac{\partial y_{1}}{\partial x_{M}} \\
      \frac{\partial y_2}{\partial x_1} & \frac{\partial y_2}{\partial x_2} & \ldots & \frac{\partial y_{2}}{\partial x_{M}} \\
      \vdots & \vdots & \ddots & \vdots \\
      \frac{\partial y_N}{\partial x_1} & \frac{\partial y_N}{\partial x_2} & \ldots & \frac{\partial y_{N}}{\partial x_{M}} \\
    \end{array}
  \right)

この$J$に対して長さ$M$のベクトル$v$を右からかけたのが、Jacobian vector products (jvp)です。R-operator(Rop)とも呼ばれます。

jvp = J v

左からベクトルをかけるL-operator(Lop)と異なり、Ropはtensorflowの仕組み上、直接的には計算できません。
そこで、このブログで紹介されているLopを2回使うことでRopを実現する方法を用います。

原理を簡単に説明します。まず、ヤコビアン$J$に左から適当な長さ$N$のベクトル$w$をかけて転置した$g$を計算します。これはLopなので簡単に計算可能です。

g = [w^TJ]^T = J^T w

このとき、$g$のshape=$[M,1]$となります。次に$g$を、$x$ではなく$w$で微分し、左から長さ$N$のベクトル$v$をかけて転置した$h$を計算します。これもLopなので計算可能です。

h = [v^T \frac {\partial }{\partial w} (J^Tw)]^T = [v^T J^T]^T = Jv

これはまさに最初に求めようとしていたRopです。この2回のLopでRopを実現するtrickはtensorflowで直接実装することができます。以下にtensorflowでの実装例を示します。(実用上は$x$がTensorのlistになることが多いため、もう少し複雑な実装になります。詳しくはissueからリンクがはられた実装例を参照してください。)

def Lop(f, x, v):
    return gradients(f, x, grad_ys=v)

def Rop(f, x, v):
    w = tf.ones_like(f)
    return Lop(Lop(f, x, w), w, v)

これでRopを使うだけなら十分です。

Ropの内部構造

数式上はRopがLop2回で成り立つことはわかりました。では、tensorflowではLopが計算できるケースではRopが常に計算できるのでしょうか?
答えは残念ながらノーです。なぜなら、このRopでは計算グラフを構成する各Operatorの2階微分が必要となるため、2階微分をサポートしていないOperatorを用いている場合には計算できません。

Ropの計算過程を簡単なグラフ例で考えます。まず、以下のような入力$x$に対して、Operator $f$を適用し、さらにOperator $g$を適用するケースを考えます。

forward計算
f = f(x)
g = g(f)

今求めたいRopは、$g$の出力の各要素を$x$の各要素で微分して求まるヤコビアン$J=\frac{\partial g}{\partial x}$に対し、右からベクトル$v$をかけた$Jv$です。
これを求めるために、まず、Lopで$wJ$を求めます。これはtensorflow上では、

f = f(x)
g = g(f)
lop = tf.gradients(g, x, grad_ys=w)

と書けます。このように書くとtensorflowでは次のような計算グラフを生成します。

ここでf_grad, g_gradはそれぞれf(x)とg(f)に対応する勾配関数です。勾配関数は一般的には引数として、元になった関数の入力すべてと、損失Lに対して元になった関数の出力に関する勾配を取ります。
今回のケースでは損失Lが明示的に出てきていませんが、grad_ys=wとしたことにより、$L=w^Tg$となるような損失Lがあるかのように計算されます。よって、g(f)の勾配関数g_gradが受け取る引数は、g(f)の入力であるfと、損失Lのgに関する勾配$\frac{\partial L}{\partial g} = \frac{\partial w^Tg}{\partial g} = w$となります。f_gradも同様にf(x)の入力であるxと、勾配$df = \frac{\partial L}{\partial f}$を入力として受け取ります。$\frac{\partial L}{\partial f}$を計算するのはg_gradの責任です。

このようにして得られたlopは

lop = \frac{\partial L}{\partial x} = [\frac{\partial L}{\partial g}\frac{\partial g}{\partial x}]^T = [w^T J]^T

となります。lopを得るにはf, f_grad, g_gradの計算が必要になります。gは必要ありません。

次に、このlopとベクトル$v$の積$L_2=v^TJ^Tw$を$w$で微分します。これは

rop = tf.gradients(lop, w, grad_ys=v)

と書きます。

これに対して$Lop(f, x, w)$を計算すると、次のような計算グラフが構築されます。

f_grad_gradとg_grad_gradはそれぞれf_gradとg_gradの勾配関数です。勾配関数は元の関数の各入力に関する勾配を出力するので、f_grad_gradとg_grad_gradが出力するTensorは2つなのですが、簡略化のため省いています。

このようにして得られたropは

\begin{align}
rop &= \frac {\partial L_2}{\partial w} \\
    &= [\frac{\partial L_2}{\partial lop} \frac{\partial lop}{\partial w}]^T \\
    &= [v^T \frac{\partial}{\partial w}(J^T w)] ^T \\
    &= [v^T J^T] ^T = Jv
\end{align}

となり、求めていたものであることがわかります。

最終的に、ropを求めるのに、f, g_grad, f_grad_grad, g_grad_gradの計算が必要になります。おもしろいのは、ropの計算にはlopが不要であるため、f_gradが計算されなくなる点です。lopがf, f_grad, g_gradの評価で済むことを考えると、ropの計算コストはlopと比べて上昇する可能性が高いでしょう。

Discussion

RopをLop2回で計算するtrickを説明したブログでは、Lop2回で実装したRopとtheanoの機能で実装したRopが等価な計算グラフに最適化される例が示されており、Lop2回での実装が、十分に効率的である可能性が示唆されています。

しかし、FusedBatchNormとFusedBatchNormGradのように中で行われている計算がC++にしか記述されていない場合、TensorflowのGraph Optimizerから隠蔽されているため、RopはLop2回分のコストを支払う必要がある可能性が高いです。

Ropの論文とそれを実装していると思われるtheanoではRopの計算コストはLopとほとんど変わらない筈です(たぶん)。それに比べてLop2回でRopを実現する手法では、計算時間や各オペレーターの二階微分が必要という点で不利です(ただし、theanoでは各OperatorにRopの定義が必要と思われる)。

(おまけ)Ropの別実装

計算過程で使われるベクトル$w$はグラフの最適化で消えるべきだという信念を利用したRopの実装案がこちらのissueで提案されています。概要は以下のようになります。

def Rop(f, x, v):
    w = tf.placeholder(f.dtype, shape=f.shape())
    return Lop(Lop(f, x, w), w, v)

先ほどのRopの実装と異なり、$w$がones_likeからplaceholderに変更されています。もし、$w$が除去されずに計算グラフに残った場合、このRopを計算しようとすると「placeholderの値がfeed_dictで指定されていない」旨のエラーが発生するため、無駄な演算を検知することができます。

こちらのRopは無駄な演算を検知することはできるのですが、実用上は計算グラフの最適化で$w$への依存を除去しきれないケースがあるため、placeholder版ではRopで計算できる範囲が狭まってしまいます。

7
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
1