MXNet Tutorialを試す（2）: Symbol - ニューラルネットのグラフと自動微分

[MXNet Tutorial](https://mxnet.incubator.apache.org/tutorials/index.html)を順番にやっていくメモ(頑張って最後まで進む……)

- 今回は2番目の[Symbol - Neural network graphs and auto-differentiation](https://mxnet.incubator.apache.org/tutorials/basic/symbol.html)。

# Symbol

前節の`NDArray`だけでも科学計算は可能なため、これで全ての計算が済むのではないか？という疑問が生まれるかもしれない.

MXNetではシンボリックに書けるSymbol APIを提供している. シンボリックな書き方ではステップバイステップに計算を記述するのではなく、最初に計算グラフを定義する.
グラフにはの入出力のPlaceholderが含まれており、これをコンパイルしたのち、NDArrayを出力するような関数を与えて実行する...Caffeのネットワークの設定とTheanoのシンボリックな書き方にSymbol APIは似ている.

> シンボリック = 宣言的、とほぼ同義の模様

シンボリックなアプローチのもう一つの長所は最適化.
命令的に記述するとそれぞれの計算の際、その先何が必要になるのかわからない.
シンボリックな書き方では、アウトプットが事前に定義されているので、中間でのメモリを再割り当てして即座に計算ができる.
また同じネットワークでもメモリの必要量が小さくなる.

どちらの書き方がいいかは[ここ](https://mxnet.incubator.apache.org/architecture/program_model.html)で議論

ひとまずこの章ではSymbol APIについて述べる
グラフィカルな説明は[ここ](http://localhost:8888/?token=ebb31e9b782c15683f581f6b99d7cfecc115e4774c59cf7c)参照

## 基本的なSymbol の構築 
### 基本的な演算
`a + b`の表現方法. 最初に`mx.sym.Variable`でプレースホルダを作成する（作成時に名前を与える）.
次に + で繋げて`c`を定義、この際自動で命名される.

```python
import mxnet as mx
a = mx.sym.Variable('a')  # aを抜くとエラー
b = mx.sym.Variable('b')
c = a + b
(a, b, c)  # _plus0とcには自動で命名
```

```text:OUT
    (<Symbol a>, <Symbol b>, <Symbol _plus0>)
```

NDArrayの演算のほとんどがSymbleに対しても適用可能.

```python
# 要素ごとのかけ算
d = a * b
# 行列積
e = mx.sym.dot(a, b)   
# 変形
f = mx.sym.Reshape(d+e, shape=(1,4))  
# ブロードキャスト
g = mx.sym.broadcast_to(f, shape=(2,4))  
mx.viz.plot_network(symbol=g)  # ネットワーク可視化
```
![output_4_0.png](https://qiita-image-store.s3.amazonaws.com/0/163756/a2ebf52b-97d5-4f61-0ee8-20ad7f9321c0.png)

`bind`で入力を与えて評価する（詳細はこの後）


### 基本的なニューラルネット
ニューラルネットの層についても提供.
2層全結合ネットワークの記述例.

```python
# 出力グラフは場合によるとのこと
net = mx.sym.Variable('data')
net = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128)
net = mx.sym.Activation(data=net, name='relu1', act_type="relu")
net = mx.sym.FullyConnected(data=net, name='fc2', num_hidden=10)
net = mx.sym.SoftmaxOutput(data=net, name='out')
mx.viz.plot_network(net, shape={'data':(100,200)})
```

![output_7_0.png](https://qiita-image-store.s3.amazonaws.com/0/163756/3102b144-a404-31f7-9946-00b35e39635e.png)

それぞれのsymbolはユニークなな名前を持つ. NDArrayとSymbolは両方ともひとつのテンソルを示し演算子はテンソル間の計算を示している。演算子はSymbol（もしくはNDArray）を入力として受け取り、また場合によっては隠れ層の数（`hidden_num`）や活性化関数の種類（`act_type`）などハイパーパラメータを受け取って、出力する.

symbolをいくつかの引数を持つ関数として見ることもでき、この関数コールで引数一覧を見ることができる.


```python
net.list_arguments()
```

```text:OUT
    ['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias', 'out_label']
```

```python
mx.sym.Variable('data')
```

```text:OUT
    <Symbol data>
```

下記のパラメータやインプットがそれぞれのSymbolに必要

- `data`: 変数`data`に対して入力するデータ
- `fc1_weight`, `fc1_bias`: 最初の全結合層fc1の重みとバイアス
- `fc2_weight`, `fc2_bias`: 最初の全結合層fc2の重みとバイアス
- `out_label`: Lossに必要なラベル

明示的に宣言することもできる

```python
net = mx.symbol.Variable('data')
w = mx.symbol.Variable('myweight')
net = mx.symbol.FullyConnected(data=net, weight=w, name='fc1', num_hidden=128)
net.list_arguments()
```

```text:OUT
    ['data', 'myweight', 'fc1_bias']
```

上の例では`FullyConnected`へは`data`, `weight`, `bias`の入力が三つ
> と書かれているがコードにバイアスはなさそう、さらにいうと略記symを使っていない・・・

## より複雑な構築
MXNetではディープラーニングでよくつかわる層について最適化されたSymbolを提供.
新しい演算子もPythonで定義可能

次の例では要素ごとにSymbolを足し算した後、全結合層へ渡している

```python
lhs = mx.symbol.Variable('data1')
rhs = mx.symbol.Variable('data2')
net = mx.symbol.FullyConnected(data=lhs + rhs, name='fc1', num_hidden=128)
"""これは普通の演算ではなかろうか"""
net.list_arguments()
```

```text:OUT
    ['data1', 'data2', 'fc1_weight', 'fc1_bias']
```

単一方向の構築だけでなくよりフレキシブルな構築も可能

```python
data = mx.symbol.Variable('data')
net1 = mx.symbol.FullyConnected(data=data, name='fc1', num_hidden=10)
print(net1.list_arguments())
net2 = mx.symbol.Variable('data2')
net2 = mx.symbol.FullyConnected(data=net2, name='fc2', num_hidden=10)
composed = net2(data2=net1, name='composed')  # netを関数として使用
print(composed.list_arguments())
```

```text:OUT
    ['data', 'fc1_weight', 'fc1_bias']
    ['data', 'fc1_weight', 'fc1_bias', 'fc2_weight', 'fc2_bias']
```

この例ではnet2は既存のnet1をとる関数として指標されており、結果composedはnet1, net2両方の引数を持つことになる

symbol共通のPrefixをつけたいときは`Prefix`を利用できる

```python
data = mx.sym.Variable("data")
net = data
n_layer = 2
for i in range(n_layer):
    with mx.name.Prefix("layer%d_" % (i + 1)): # Prefix付与
        net = mx.sym.FullyConnected(data=net, name="fc", num_hidden=100)
net.list_arguments()
```

```text:OUT
    ['data',
     'layer1_fc_weight',
     'layer1_fc_bias',
     'layer2_fc_weight',
     'layer2_fc_bias']
```

### モジュール化してのディープニューラルネットワークの構築
Google Inceptionのような深いネットワークを逐次記述するのは大変.
そのためモジュール化して再利用する

次の例では最初にfuctory function（畳み込み、Batch Normalize,ReLUのひとまりを生成）を定義

```python
# Output may vary
def ConvFactory(data, num_filter, kernel, stride=(1,1), pad=(0, 0), name=None, suffix=''):
    conv = mx.symbol.Convolution(data=data, num_filter=num_filter, kernel=kernel, stride=stride, pad=pad, name='conv_%s%s' %(name, suffix))
    bn = mx.symbol.BatchNorm(data=conv, name='bn_%s%s' %(name, suffix))
    act = mx.symbol.Activation(data=bn, act_type='relu', name='relu_%s%s' %(name, suffix))
    return act
# 畳み込み→バッチノルム（バッチごとの正規化）→ReLUで活性化という一つのユニットを定義

prev = mx.symbol.Variable(name="Previos Output")
conv_comp = ConvFactory(data=prev, num_filter=64, kernel=(7,7), stride=(2, 2)) # 7×7のフィルタをストライド2でズラす、パディングはなし=> 11かけ11に
shape = {"Previos Output" : (128, 3, 28, 28)}
mx.viz.plot_network(symbol=conv_comp, shape=shape)
```

![output_24_0.png](https://qiita-image-store.s3.amazonaws.com/0/163756/80dab810-460c-ade4-1352-af72de7b1b24.png)


これを利用してInceptionを組む

```python
# @@@ AUTOTEST_OUTPUT_IGNORED_CELL
def InceptionFactoryA(data, num_1x1, num_3x3red, num_3x3, num_d3x3red, num_d3x3, pool, proj, name):
    # 1x1
    c1x1 = ConvFactory(data=data, num_filter=num_1x1, kernel=(1, 1), name=('%s_1x1' % name))
    # 3x3 reduce + 3x3
    c3x3r = ConvFactory(data=data, num_filter=num_3x3red, kernel=(1, 1), name=('%s_3x3' % name), suffix='_reduce')
    c3x3 = ConvFactory(data=c3x3r, num_filter=num_3x3, kernel=(3, 3), pad=(1, 1), name=('%s_3x3' % name))
    # double 3x3 reduce + double 3x3
    cd3x3r = ConvFactory(data=data, num_filter=num_d3x3red, kernel=(1, 1), name=('%s_double_3x3' % name), suffix='_reduce')
    cd3x3 = ConvFactory(data=cd3x3r, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_0' % name))
    cd3x3 = ConvFactory(data=cd3x3, num_filter=num_d3x3, kernel=(3, 3), pad=(1, 1), name=('%s_double_3x3_1' % name))
    # pool + proj
    pooling = mx.symbol.Pooling(data=data, kernel=(3, 3), stride=(1, 1), pad=(1, 1), pool_type=pool, name=('%s_pool_%s_pool' % (pool, name)))
    cproj = ConvFactory(data=pooling, num_filter=proj, kernel=(1, 1), name=('%s_proj' %  name))
    # concat
    concat = mx.symbol.Concat(*[c1x1, c3x3, cd3x3, cproj], name='ch_concat_%s_chconcat' % name)
    return concat
prev = mx.symbol.Variable(name="Previos Output")
in3a = InceptionFactoryA(prev, 64, 64, 64, 64, 96, "avg", 32, name="in3a")
mx.viz.plot_network(symbol=in3a, shape=shape)
```

![output_26_0.png](https://qiita-image-store.s3.amazonaws.com/0/163756/c299ed07-9d52-ed64-4842-c0d0177d0603.png)

完成しているものの例は[ここ](https://github.com/apache/incubator-mxnet/blob/master/example/image-classification/symbols/inception-bn.py)

### 複数のSimbolのグルーピング

複数のLossのレイヤを持つようなニューラルネットを組むときは、`mxnet.sym.Group`でグルーピングが可能


```python
net = mx.sym.Variable('data')
fc1 = mx.sym.FullyConnected(data=net, name='fc1', num_hidden=128)
net = mx.sym.Activation(data=fc1, name='relu1', act_type="relu")
out1 = mx.sym.SoftmaxOutput(data=net, name='softmax')
out2 = mx.sym.LinearRegressionOutput(data=net, name='regression')
group = mx.sym.Group([out1, out2])
group.list_outputs()
```

```text:OUT
    ['softmax_output', 'regression_output']
```

## NDArrayとの関連

`NDArray`は命令的なインターフェースを提供、計算は文ごとに評価される.
`Symbol`は宣言的なプログラミングに近い、最初に計算構造を宣言してそれからデータを評価.正規表現やSQLに近い.

NDArrayの利点

- 単純
- for, if-elseなどのプログラミング言語の特徴やNumPyなどのライブラリを活かしやすい
- ステップバイステップでのデバッグが行いやすい

Symbolの利点

- NDArrayの持つ関数のほぼ全てが提供されている（+, *, sin, reshapeなど）
- 保存と読み込み、ビジュアライズが簡単
- 計算やメモリ利用の最適化が行いやすい 

## Symbol の操作

SymbolとNDArrayの違いは上に述べた通り、ただしSymbolも直接操作することもできる.
ただし.`module`パッケージで大体はラップされていることに留意.

### Shape Inference
それぞれのSymbolに対して、引数、付加的な情報、出力を求めることができる.出力の形やsymbolの型を、入力の形や引数の型から推定も可能、これでメモリ割り当てをやりやすくする.


```python
# すっかり忘れがちだが c = a + b

arg_name = c.list_arguments()  # 入力の名前
out_name = c.list_outputs()    # 出力の名前
# 入力から出力の形を推定
arg_shape, out_shape, _ = c.infer_shape(a=(2,3), b=(2,3))
# 入力から出力の型を推定
arg_type, out_type, _ = c.infer_type(a='float32', b='float32')
print({'input' : dict(zip(arg_name, arg_shape)),
 'output' : dict(zip(out_name, out_shape))})
print({'input' : dict(zip(arg_name, arg_type)),
 'output' : dict(zip(out_name, out_type))})
```

```text:OUT
    {'output': {'_plus0_output': (2, 3)}, 'input': {'b': (2, 3), 'a': (2, 3)}}
    {'output': {'_plus0_output': <class 'numpy.float32'>}, 'input': {'b': <class 'numpy.float32'>, 'a': <class 'numpy.float32'>}}
```

### データによるバインドと評価
symbol`c`を評価するには引数としてデータをを与える必要がある.
これをするには`bind`メソッドを利用する.これはコンテクストとfree valuable名とNDArrayのマッピングをした辞書を渡すとexecuterを返すメソッド.
exeutorからは`forward`メソッドで評価が実行でき、`output`属性から結果が取り出せる.


```python
ex = c.bind(ctx=mx.cpu(), args={'a' : mx.nd.ones([2,3]), 
                                'b' : mx.nd.ones([2,3])})

ex.forward()
print('number of outputs = %d\nthe first output = \n%s' % (
           len(ex.outputs), ex.outputs[0].asnumpy()))
```

```text:OUT
    number of outputs = 1
    the first output = 
    [[ 2.  2.  2.]
     [ 2.  2.  2.]]
```

同じSymbolを異なるコンテクスト(GPU)、異なるデータで評価もできる

```python
ex_gpu = c.bind(ctx=mx.gpu(), args={'a' : mx.nd.ones([3,4], mx.gpu())*2,
                                    'b' : mx.nd.ones([3,4], mx.gpu())*3})
ex_gpu.forward()
ex_gpu.outputs[0].asnumpy()
```

```text:OUT
    array([[ 5.,  5.,  5.,  5.],
           [ 5.,  5.,  5.,  5.],
           [ 5.,  5.,  5.,  5.]], dtype=float32)
```

`eval`による評価も可能、これは`bind`と`forward`を束ねたもの

```python
ex = c.eval(ctx = mx.cpu(), a = mx.nd.ones([2,3]), b = mx.nd.ones([2,3]))
print('number of outputs = %d\nthe first output = \n%s' % (
            len(ex), ex[0].asnumpy()))
```

```text:OUT
    number of outputs = 1
    the first output = 
    [[ 2.  2.  2.]
     [ 2.  2.  2.]]
```

### 読み込みと保存

NDArrayと同様、pickleにできてsave, loadもできる.
ただSymbolはグラフであり、グラフは連続した計算から成り立っている。これらは出力のSymbolで暗黙的に表現されるので、出力Symbolのグラフをシリアライズする.
JSONでシリアライズすると可読性が上がるが、これには`tojson`を利用する.

```python
print(c.tojson())
c.save('symbol-c.json')
c2 = mx.symbol.load('symbol-c.json')
c.tojson() == c2.tojson()
```

```text:OUT
    {
      "nodes": [
        {
          "op": "null", 
          "name": "a", 
          "inputs": []
        }, 
        {
          "op": "null", 
          "name": "b", 
          "inputs": []
        }, 
        {
          "op": "elemwise_add", 
          "name": "_plus0", 
          "inputs": [[0, 0, 0], [1, 0, 0]]
        }
      ], 
      "arg_nodes": [0, 1], 
      "node_row_ptr": [0, 1, 2, 3], 
      "heads": [[2, 0, 0]], 
      "attrs": {"mxnet_version": ["int", 1000]}
    }

    True
```

## カスタムシンボル
`mx.sym.Convolution`、`mx.sym.Reshape`のような演算はパフォーマンスのためC++でインプリされている.
MXNetではPythonのような言語を使って新しい演算モジュールを作ることもできる、詳しくは[こちら](https://mxnet.incubator.apache.org/how_to/new_op.html)を参照

> Softmaxを継承して手前実装して見る感じ

## 発展的な利用方法

### 型キャスト
通常32bit小数点だが、高速化のために精度の低い型を利用することも可能.
`mx.sym.Cast` で型変換を行う

```python
a = mx.sym.Variable('data')
b = mx.sym.Cast(data=a, dtype='float16')
arg, out, _ = b.infer_type(data='float32')
print({'input':arg, 'output':out})

c = mx.sym.Cast(data=a, dtype='uint8')
arg, out, _ = c.infer_type(data='int32')
print({'input':arg, 'output':out})
```

```text:OUT
    {'output': [<class 'numpy.float16'>], 'input': [<class 'numpy.float32'>]}
    {'output': [<class 'numpy.uint8'>], 'input': [<class 'numpy.int32'>]}
```

### 変数の共有

Symbol間の共有は、同じアレイにSymbolをバインドすることで可能

```python
a = mx.sym.Variable('a')
b = mx.sym.Variable('b')
c = mx.sym.Variable('c')
d = a + b * c

data = mx.nd.ones((2,3))*2
ex = d.bind(ctx=mx.cpu(), args={'a':data, 'b':data, 'c':data})  # dataを入力値として共有
ex.forward()
ex.outputs[0].asnumpy()
```

```text:OUT
    array([[ 6.,  6.,  6.],
           [ 6.,  6.,  6.]], dtype=float32)
```

-----

- Python 3 + Ubuntu, GPU環境で学習中
- 全文和訳しているわけではないです、あくまでメモ程度に
- Jupyterからの出力に手を入れただけなのでレイアウト崩れるやも…


途中モジュールの呼び出し方が定まっていない？　（symと呼んだりsymbolと呼んだり）とまだまだチュートリアルとして定まっていない印象を受けました。

次はmoduleの予定。