Chainerのdouble backwardの使い方 (おまけ：オプションのグリークスを計算してみた)

本記事は、Chainer Advent Calendar 2017の16日目です．

# TL;DR

```python
gy, = chainer.grad([y], [x], enable_double_backprop=True)
ggy, = chainer.grad([gy], [x])
```

or

```python
y.backward(enable_double_backprop=True)
gy = x.grad_var
x.cleargrad()  # reset as x.grad = 0
gy.backward()
ggy = x.grad_var
```

# はじめに
最近、Chainerにはdouble backpropagation(微分値の自動微分)の機能が追加されました。それというのも、最近のモデルは1階微分では事足らず、Unrolled GANの実装やWGANの精度改善のために2階微分が必要だったりするからです。そこまで使用頻度は高くないかもしれないですが、Chainerでどのように2階微分を計算すればよいか説明できればと思います。

# 自動微分とは
自動微分とは、「プログラムで定義された関数を解析し、偏導関数の値を計算するプログラムを導出する技術である。自動微分は複雑なプログラムであっても加減乗除などの基本的な算術演算や基本的な関数（指数関数・対数関数・三角関数など）のような基本的な演算の組み合わせで構成されていることを利用し、これらの演算に対して連鎖律を繰り返し適用することによって実現される。」[[1]](https://ja.wikipedia.org/wiki/%E8%87%AA%E5%8B%95%E5%BE%AE%E5%88%86)というものです。

Chainerなどの多くのDeep Learning(DL)フレームワークでは、下記のように数式を計算グラフとして表現し、その情報を用いて自動的に微分を行います。

<img width="895" alt="スクリーンショット 2017-12-16 15.57.53.png" src="https://qiita-image-store.s3.amazonaws.com/0/111482/5d3f4998-615d-654e-9b82-af12e6517736.png">

# そもそもどこで自動微分が使われているか
世の中には多くのDLフレームワークが存在しますが、これらのフレームワークが提供する機能は大別すると3つの共通する機能を提供しています。

1. 多次元配列の計算機能
2. ニューラルネットワーク(NN)の定義機能
3. NNの最適化機能

自動微分は、「NNの定義機能」と「NNの最適化機能」に関わる話です。ChainerなどのDeep Learingフレームワークを使用していると、数式でモデルを定義し、そのモデルを最適化クラス(Chainerで言うところのTrainer、Optimizerなど)に渡すと自動でモデルパラメータの学習ができてしまいます。それなので、一見すると微分計算なんてどこで必要なのか？と思ってしまうかもしれないですが、実はモデルパラメータの最適化にパラメータによる損失関数の微分値を使用しています。さらにその微分値は、数式の表現できる全てのモデルに対して、微分値を計算できる必要があります。そのため、自動微分という技術が必要になってきます。

もし、自動微分という技術を導入していない場合どうなるかというと、新しくモデルを定義するたびに、そのモデルパラメータによる損失関数の微分値を計算するプログラムを別途定義しなくてはならなくなります。

# 自動微分、double backpropagationの計算方法

この資料のp15以降がかなり詳しいのでこちらにおまかせします。すみません。。
[comparison-of-deep-learning-frameworks-from-viewpoint-of-double-backpropagation](https://www.slideshare.net/KentaOono/comparison-of-deep-learning-frameworks-from-viewpoint-of-double-backpropagation)

# Chainerでのdouble backpropagation

- ``chainer.grad``を使う場合は、``enable_double_backprop=True``とすると、微分可能な型``Variable``として返却されるので、さらに微分をすることができます。

```python
import chainer
from chainer import Variable
import numpy as np

x = Variable(np.array(2.0))  # x = 2.0
y = x ** 2. + x + 1.0  # y = x^2 + x + 1

gy, = chainer.grad([y], [x], enable_double_backprop=True)  # gy = dy/dx = 2x + 1
print(gy)  # variable(5.0)

ggy, = chainer.grad([gy], [x])  # ggy = dydy/dxdx = 2
print(ggy)  # variable(2.0)
```

- ``Variable.backward``を使う場合は、``enable_double_backprop=True``とすると、``Variable.grad_var``で微分可能な型``Variable``を取得することができ、さらに微分をすることができます。

```python
import chainer
from chainer import Variable
import numpy as np

x = Variable(np.array(2.0))  # x = 2.0
y = x ** 2. + x + 1.0  # y = x^2 + x + 1

y.backward(enable_double_backprop=True)
gy = x.grad_var  # gy = dy/dx = 2x + 1
print(gy)  # variable(5.0)

x.cleargrad()  # reset as x.grad = 0

gy.backward()
ggy = x.grad_var  # ggy = dydy/dxdx = 2
print(ggy)  # variable(2.0)
```

# おまけ
また、最後には戯事ですが、2階微分が必要な応用として金融派生商品のグリークス(価格の微分値)を計算してみました。今回使用しているモデルはブラック–ショールズモデル[[3]](https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E2%80%93%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%AB%E3%82%BA%E6%96%B9%E7%A8%8B%E5%BC%8F)です。(あとで時間があったら詳しく説明できればと思います。)

```python
import numpy as np
import chainer
from chainer import Variable
import chainer.functions as F
from math import exp


def main():
    stock_price = 100.0
    strike = 100.0
    years = 1.0
    risk_free_rate = 0.01
    volatility = 0.1
    n_paths = 100000
    n_steps = 100

    times = np.linspace(0, years, n_steps, dtype=np.float64)

    r = risk_free_rate
    v = volatility
    k = strike
    T = years
    initial_stocks = Variable(stock_price * np.ones((n_paths,), dtype=np.float64))
    currents = initial_stocks
    for c, n in zip(times[:-1], times[1:]):
        dt = n - c
        zs = np.random.normal(0.0, 1.0, currents.shape)
        currents = currents * F.broadcast(np.exp((r - 0.5 * v * v) * dt + v * dt ** 0.5 * zs))

    call_price = F.average(exp(-r * T) * F.softplus(currents - k, beta=10))  # you should smooth the relu to calculate gamma
    # call_price = F.average(exp(-r * T) * F.relu(currents - k))  
    print('call price: {0}'.format(call_price))
    deltas = F.sum(chainer.grad([call_price], [initial_stocks], enable_double_backprop=True)[0])
    print('call delta: {0}'.format(deltas))
    gammas = chainer.grad([deltas], [initial_stocks], enable_double_backprop=True)
    print('call gamma: {0}'.format(np.sum(gammas)))


if __name__ == '__main__':
    main()
```

# 参考資料
- [1][自動微分 - Wikipedia](https://ja.wikipedia.org/wiki/%E8%87%AA%E5%8B%95%E5%BE%AE%E5%88%86)
- [2][comparison-of-deep-learning-frameworks-from-viewpoint-of-double-backpropagation](https://www.slideshare.net/KentaOono/comparison-of-deep-learning-frameworks-from-viewpoint-of-double-backpropagation)
- [3][ブラック–ショールズ方程式 - Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%96%E3%83%A9%E3%83%83%E3%82%AF%E2%80%93%E3%82%B7%E3%83%A7%E3%83%BC%E3%83%AB%E3%82%BA%E6%96%B9%E7%A8%8B%E5%BC%8F)