Caffe2: 古豪Caffeの後継の深層学習フレームワーク

# はじめに
Caffe2の紹介記事です．
（たぶん）Caffeと比較しながら，特徴をざっくり紹介します．

# Caffe2の概要

Caffeとの差分はこんなところでしょうか．

* **Facebook**，NVIDIAのサポート  
企業がMaintainerになったのは大きいと思われます．特にドキュメント周りに期待できます．
Caffe時代は大学の一研究室だったので…
* Protobufの隠蔽  
中身はProtobufのようですが，インタフェース的にはProtobufスタートではなくなったようです．**Pythonベースで事が進む**ように配慮されているようです．モダン感が出てきました．
ネットワークを組んだり，学習させていると，やっていることがわかってきます．
* Pythonインタフェースの整備  
Python上でのネットワーク構築がやりやすくなりました（当者比）．
また，**ネットワーク構築から学習までPythonでできるようになりました**．  
なお，やろうと思えばC++でも同じプロセスをたどれるようです．brewのような高レベルモジュールの実装が（まだ）ないのでつらいですが．
* モバイル対応の強化  
公式曰く．Android，iPhoneなどに載るようです．mobile_deploymentなるモジュールが見えます．
* ドキュメントが充実した  
公式ドキュメントはCaffeの時よりも（たぶん）充実しています．とはいえ現段階では絶対量は（かなり）不足を感じます．時が解決してくれることと思われます．
* （たぶん）高速化  
なにがしかの高速化をしているようです．本稿で行ったベンチマークテストでは，Caffeより高いスコアを出していました．  
Caffe時はGPUまわりはパッチワーク感があり，魔改造版が出回る始末でした．Caffe2ではうまくやるようになったようで，マルチGPUもスマートになるように頑張っているようです．
* 独自オペレータ（層）定義時に，caffe.protoを汚さなくて済むようになった  
モジュール化の強化に取り組んだ結果，（著者は魔改造版と呼称する）**第三者実装をインストールしようとするとコンフリクトしまくってストレスフルだったのが解決した**ようです．

低レベル性，高速性への最適化を保ちつつ，より容易に取り扱えるフレームワークへの進化という意図が見て取られます．
依然初心者には分かりづらい部分が多いため，初心者はKerasやTorchで練習してから取り組むのをお勧めします．


Caffe2では，workspace，blob，operatorの3つの概念でネットワーク構築を行います．

* workspace  
workspace上で作業をします．workspace上に作成したモデルを載せたり，データ（blob）を置いたりします．
* blob  
データの塊です．ネットワーク上でやり取りされるあらゆるデータはblobとしてworkspaceに置かれるようです．  
blobはCaffe時代にもありましたが，Caffe2では，入出力，テストデータだけでなく，重みやバイアスなどのネットワークパラメータもBlobとして扱われるのがCaffeとの違いの一つです．
* operator  
関数だとか層だとかを総称してoperatorと言うことにしたそうです．
operatorの間でblobをやり取りする形でネットワークを組んでいくことになります．

# インストール
[こちらの記事を](https://qiita.com/yshi12/items/607cc5a5074eddc98de4)もご参照ください．
Dockerが一番簡単です．WindowsでDockerだと現段階では（Windows10では？）GPU使えません．
完全にWindows10上で使う場合は苦行です．[拙著](https://qiita.com/suzuryo3893/items/e6bad5d095114746cb79)が参考になるかもしれません．

# ネットワーク構築
学習までPythonですべて片付くようになりました．
低レベルな実装がオープンになっていますが，基本的にはそれをラップするモジュールを使うのが良いと思われます．
学習までやってみたフローは以下の感じになるので，これをトレースすれば学習までできます（たぶん）．

```py

from caffe2.python import workspace, model_helper, brew, optimizer
import numpy as np

#XORのデータを用意
data = np.array([[0,0],[0,1],[1,0],[1,1]],dtype=np.float32)
target = np.array([[0],[1],[1],[0]],dtype=np.float32)

#Blobなるものに固めてworkspaceにいれる
workspace.FeedBlob('data',data)
workspace.FeedBlob('target',target)

#モデルのもとを作る
m = model_helper.ModelHelper(name="XOR_MLP")

#層を積む MLPの例
fc1 = brew.fc(m, 'data', 'fc1', 2, 2) #入力2，出力2
ac1 = brew.tanh(m, fc1, 'ac1')
fc2 = brew.fc(m, ac1, 'fc2', 2, 1) #入力2，出力1
ac2 = brew.tanh(m, fc2, 'ac2')

##評価だけならここまで実装する．


#学習用に評価関数をつける
#回帰問題の場合こんなかんじ
dist = m.net.SquaredL2Distance([ac2, 'target'], "dist")
loss = m.AveragedLoss([dist], ["loss"])

#学習には逆パスも作る．コマンド一発！
m.AddGradientOperators([loss])

#学習用設定する．optimizerが一括でやってくれるので便利
#自分でiterとかLearningRateとかSGDを全パラメータに登録とかやらないで済む
optim=optimizer.build_sgd(m,base_learning_rate=0.01, policy="step", stepsize=400, gamma=0.999)

#ちなみに手動設定版．めんどい
#iter = brew.iter(m,"iter")
#lr = m.net.LearningRate([iter], "lr", base_lr=0.01, policy="step", stepsize=400, gamma=0.999)
#for param in m.GetParams():
#    param_grad=m.param_grad[param]
#    param_momentum = model.param_init_net.ConstantFill(
#        [param], param + '_momentum', value=0.0
#    )
#    m.net.MomentumSGDUpdate(
#        [param_grad, param_momentum, lr, param],
#        [param_grad, param_momentum, param],
#        momentum=0.9,
#        # Nesterov Momentum works slightly better than standard momentum
#        nesterov=1
#    )

workspace.RunNetOnce(m.param_init_net) #パラメータ初期化
workspace.CreateNet(m.net,overwrite=True) #workspaceに登録

#学習する@20000epoch
#workspace.RunNet(m.net.Proto().name, 20000)

#100epochごとに様子をみる版
for i in range(200):
    workspace.RunNet(m.net.Proto().name,100)
    print(workspace.FetchBlob("ac2").reshape(4))


```

以下，解説します．

* データはユーザ側ではnumpy.ndarrayでテンソルで用意します．（画像の場合）NCHWデータオーダを採用しているそうです．Caffeと同様ですね．(Batch)Number，Channel，Height，Widthの順だそうですので，そのようにデータを用意しましょう．
* workspace.FeedBlobで用意したデータをblobにします．blobは文字列でラベリングして管理します．operatorが戻すblobオブジェクトも使えます．
* ネットワークいじりは，基本的に**ModelHelper**を使っていきます．使わなくてもできますが，苦行です．
* さらに**brew**なるモジュールがあり，ModelHelperのレイヤ追加部分をラップしてくれていて便利です．なるべくbrewを使ってネットワーク構造を実装していきます．すべてbrewに実装されているわけではありませんが，クラス分類CNNであれば基本的なoperatorがそろっています．
* operatorにはblobを入れて，blobが出てくる，という構造になります．blobの指定には，文字列ラベル指定，あるいはoperatorの戻り値オブジェクトによる指定のどちらでもよいようです．numpy.ndarrayなどのその他のオブジェクトはムリです．
    * 手で頑張ると，重み，バイアスのblobも各オペレータごとに作って入力する必要があるのですが，brewはそこを適当にやってくれます．途中で``print(str(m.net.Proto()))``としてみると，xxxxx_wやxxxxx_bなどのblobが勝手にできているのが確認できます．
* ふつうに組むと，順伝播のみサポートするネットワークが組めます．
* **学習のためには，さらに損失関数，逆伝播パス，optimizerを付け足して**いきます．これは書いていくとmodelの中にモリモリと組み込まれていきます．途中で``print(str(m.net.Proto()))``で見てみるとネットワークが伸びていくのがわかります．ここは他のフレームワークとは違うところですね．ネットワーク構造がかさ張る代わりに（たぶん）最適化されて高速に学習できるようです．
* いざ学習を回します．データをミニバッチに分ける作業はループの中で自力でやりましょう．その場合，numpy.ndarrayをデータ数軸でスライスして，workspace.FeedBlobでblobを上書きして，workspace.RunNetでまた回すというサイクルになります．

    ```py
    batch_size=50
    for e in range(12):
        tdata=data[e*batch_size:(e+1)*batch_size]
        workspace.FeedBlob("data",tdata)
        workspace.RunNet(m.net.Proto().name)
    ```

* 1イテレーションだけするworkspace.RunNetOnceはオーバヘッドが大きいようなので，高速に回したい場合はworkspace.RunNetを使うべきです．
* print(workspace.FetchBlob("loss"))などにより学習の進展を確認できます．

# デプロイする
先述の通り，学習までやると，テスト的にはいらないパスがもりもりくっ付いてきて邪魔です．
順伝播までの小さいネットワークを新たに作り，重み，バイアスなどのパラメータを学習済みモデルからFeedBlobで該当のところだけコピーし，テスト用学習済みネットワークを作ります．
上のコードの後に，以下を実行してみます．

```py

#デプロイ版ネットワーク（順伝播だけ組んであるネットワーク）を作る
m_d = model_helper.ModelHelper(name="XOR_MLP_deploy")
#forward propergationだけ実装　同じworkspace上で，blobの名前を同じにすれば，学習済みのネットワークパラメータのblobも共有できるので楽
fc1 = brew.fc(m_d, 'data', 'fc1', 2, 2) #入力2，出力2
ac1 = brew.tanh(m_d, fc1, 'ac1')
fc2 = brew.fc(m_d, ac1, 'fc2', 2, 1) #入力2，出力1
ac2 = brew.tanh(m_d, fc2, 'ac2')

workspace.CreateNet(m_d.net,overwrite=True) #workspaceに登録

#回帰器として使ってみる
#入力データを入れる
input=data[1]
workspace.FeedBlob('data',input)
#1回実行
workspace.RunNet(m_d.net.Proto().name)
#結果
print(workspace.FetchBlob('ac2'))

```

その後，シリアライズしましょう．mobile_deploymentというモジュールが使えるようです．

#GPUを使う場合

全体的に，以下のwithブロックに置くと，blobデータをCUDA仕様で作るようになります．

```py

with core.DeviceScope(core.DeviceOption(caffe2_pb2.CUDA, 0)):
    workspace.FeedBlob('data',data) #CUDA仕様でBlobができる
```

そのうえで，ネットワークを組んだ後，実行前に以下を書くと，ネットワークがCUDNN仕様になります．

```py

m.net.RunAllOnGPU(gpu_id=gpu_no, use_cudnn=True)
m.param_init_net.RunAllOnGPU(gpu_id=gpu_no, use_cudnn=True)
```

以下，GPU版にしてみたコードです．

```py

from caffe2.python import workspace, model_helper, brew, optimizer, core
from caffe2.proto import caffe2_pb2
import numpy as np

#XORのデータを用意
data = np.array([[0,0],[0,1],[1,0],[1,1]],dtype=np.float32)
target = np.array([[0],[1],[1],[0]],dtype=np.float32)

#blobを全部CUDA仕様にする設定
with core.DeviceScope(core.DeviceOption(caffe2_pb2.CUDA, 0)):
    #Blobなるものに固めてworkspaceにいれる
    workspace.FeedBlob('data',data)
    workspace.FeedBlob('target',target)

    #モデルのもとを作る
    m = model_helper.ModelHelper(name="XOR_MLP")

    #層を積む MLPの例
    fc1 = brew.fc(m, 'data', 'fc1', 2, 2) #入力2，出力2
    ac1 = brew.tanh(m, fc1, 'ac1')
    fc2 = brew.fc(m, ac1, 'fc2', 2, 1) #入力2，出力1
    ac2 = brew.tanh(m, fc2, 'ac2')

    ##評価だけならここまで実装する．


    #学習用に評価関数をつける
    #回帰問題の場合こんなかんじ
    dist = m.net.SquaredL2Distance([ac2, 'target'], "dist")
    loss = m.AveragedLoss([dist], ["loss"])

    #学習には逆パスも作る．コマンド一発！
    m.AddGradientOperators([loss])

    #学習用設定する．optimizerが一括でやってくれるので便利
    #自分でiterとかLearningRateとかSGDを全パラメータに登録とかやらないで済む
    optim=optimizer.build_sgd(m,base_learning_rate=0.01, policy="step", stepsize=400, gamma=0.999)

    workspace.RunNetOnce(m.param_init_net) #パラメータ初期化
    workspace.CreateNet(m.net,overwrite=True) #workspaceに登録

    #ネットワークのoperatorを全部CUDA仕様にする設定
    m.net.RunAllOnGPU(gpu_id=0, use_cudnn=True)
    m.param_init_net.RunAllOnGPU(gpu_id=0, use_cudnn=True)

    #100epochごとに様子をみながら学習
    for i in range(200):
        workspace.RunNet(m.net.Proto().name,100)
        print(workspace.FetchBlob("ac2").reshape(4))

```

なお，withブロックで囲む代わりに，FeedBlobの時にDeviceOptionを引数で指定することで実現することもできるようです．

# 独自モジュール実装，導入
C++で実装することになります．時間のかかるコンパイルが必要なので，試行錯誤には完全に向いていません．
ModelHelperやbrewに乗ったりしないかな…

# 実行速度
学習にかかる速度を実験してみました．XOR_MLPについては10000エポック分，MNIST_CNNについては1エポック分の学習にかかった時間になります．
はやい．ネットワークが浅いですが，この時点でCaffeより早いです．
Windowsのほうがおそい．NNPACKを入れていないせい？

| Network    | OS     | GPU? | time (s)  |
|:-----------|:-------|:-----|----------:|
| XOR_MLP    | Ubuntu | CPU  | 0.0361    |
| XOR_MLP    | Ubuntu | GPU  | 1.6146    |
| MNIST_CNN  | Ubuntu | CPU  | 54.9      |
| MNIST_CNN  | Ubuntu | GPU  | 1.3       |

実験設定の詳細です．

* XOR_MLP
    * データ：XOR
    * ネットワーク：ノード数2 -> ノード数2のMLP　各層にTanh
    * 最適化：SGD
* MNIST_CNN
    * データ：MNIST手書き文字画像データベース
    * ネットワーク：Conv(5x5) -> MaxPool(2x2) -> Conv(5x5) -> MaxPool(2x2) -> FC(->64) -> FC(->10)  
      各層にReLU，最終層はCrossEntropy
    * 最適化：Adam

* OS
    * Ubuntu 14.04 LTS
        * GCC/G++
        * Docker Image: XXX
    * Windows 10 Professional
        * Visual Studio 2015 Professional
        * BLAS: Eigen
    * 共通
        * Intel Core i7
        * NVIDIA GeForce GTX 1080 Ti
        * CUDA 8.0 / CUDNN 6.0

# まとめ
学習したネットワークパラメータが手元に近くなって，そこら辺をいじるときに便利なように感じます．
開発しやすくなりましたが，Caffeよりは，というレベルなので，Caffe2から入るのはあまりお勧めしません．わかっている人であれば，その高速さを十分使いこなすことができると思います．
アプリケーション派の人はCaffe2でもいいかもしれません．でもPython系の高級なものと比べるとキツいので，開発部隊だけ使えばいいとも思います．PyTorchに任せましょう．