ChainerとPyTorchのコードを比較する - 線形回帰編

最近，Deep Learning Frameworkのリリースが続いている．私は，普段は TensorFlow を使うことが多いのだが，Blog記事やGitHubの情報について，ChainerやPyTorchのコードを参考にする機会も多い．特に最近，GitHubにてPyTorchコードが増えており，PyTorchが気になる存在である一方，学習する上での情報量はChainerが多いか？といった悩ましい状況となっている．

ここでは，簡単なコードを書きながら，2つのFramework(Chainer vs. PyTorch) を比較してみたい．

（プログラミング環境は，以下になります：
- Chainer 2.0.0
- PyTorch 0.1.12
- Python 3.5.2 (or 3.5.3)　　　　　　　　　　　）

## Chainerで線形回帰を実装
まず必要なパッケージをimportする．

```py
import chainer
from chainer import Function, Variable
import chainer.functions as F
```

できるだけシンプルな書き方を確認したいので，今回，`chainer.links` は用いないこととする．（注．Chainerドキュメントには，`chainer.links` の方が新しいパッケージなのでこちらの使用が推奨されるとありました．Chanier APIの変遷に対して，少し注意が必要のようです．）


```py
# Target値 (3.0, 4.0), これを元に学習データサンプルを作成する．
W_target = np.array([[3.0]], dtype=np.float32)  # size = [1, 1]
b_target = 4.0

# Model Parameters
# dtype = torch.cuda.FloatTensor # Uncomment this to run on GPU
W = Variable(np.random.randn(1, 1).astype(np.float32) * 0.01,
             requires_grad=True)
b = Variable(np.zeros([1, 1], dtype=np.float32), requires_grad=True)
```

ChainerのVariableクラスで`W`, `b`を定義している．Chainer初心者なのでドキュメントで**Variable**を確認する．（少し引用させていただきます．）

> ### Variables
> class chainer.Variable(data=None, *, name=None, grad=None, initializer=None, update_rule=None, requires_grad=True)
> Array with a structure to keep track of computation.
>
>Every variable holds a data array of type either `numpy.ndarray` or `cupy.ndarray`.
>
>A variable object holds a data array and a `VariableNode` object of a computational graph. If the variable is constructed by the user, the node is _root_ and does not hold any parent. If the variable is constructed by a `Function` object, the node holds a reference to its parent called creator. This reference is used in backpropagation to backtrack the graph.

Computational graphにおける変数ノードを保持するとある．TensorFlow/Theanoにおける`Tensor`と同様のものと推測されるが，"Tensor"という言葉は使っていない．使い方は，データを引数として`numpy.ndarray`（GPU演算では `cupy.ndarray`）を与え，Variable型インスタンスを得ている．後で，勾配計算が必要であれば（ほとんどのパラメータで勾配計算が必要のはずだが），`requires_grad=True` を指定する．

線形回帰のコードに戻る．

```py
def model(x, W, b):
    # 線形回帰モデルの定義
    y1 = F.matmul(x, W)
    b1 = F.broadcast_to(b, x.shape)
    y = y1 + b1

    return y

def get_batch(W_target, b_target, batch_size=32):
    # バッチ・データの準備
    x = np.random.randn(batch_size, 1).astype(np.float32)
    y = x * W_target + b_target

    return Variable(x), Variable(y)
```

関数 `model(x, W, b)` は，モデル式 `y = W * x + b` の計算を行う．"numpy"の計算であれば，bias `b` を加算する際，ブロードキャストして，`b` を適切な形に拡張（要素コピー）してくれるが，"Chainer"(`chainer.functions`) では明示的にブロードキャスト処理を行う．次の関数 `get_batch()` は，乱数を使って訓練データ(train data)を生成し，Variable型に変換して戻り値としている．

```py
# Train loop
for batch_idx in range(100):
    # Get data
    batch_x, batch_y = get_batch(W_target, b_target)

    # Forward pass
    y_pred = model(batch_x, W, b)

    # 損失関数 MSE(mean square error)
    loss = F.mean_squared_error(y_pred, batch_y)

    # Manually zero the gradients after updating weights
    # パラメータの勾配をゼロ化する．（重要）
    W.cleargrad()
    b.cleargrad()

    # Backward pass
    loss.backward()

    # Apply gradients - パラメータを更新
    learning_rate = 0.1
    W.data = W.data - learning_rate * W.grad
    b.data = b.data - learning_rate * b.grad  

    # Stop criterion
    if loss.data < 1.e-3:
        break
```

学習ループでは，訓練データから予測値 `y_pred` を計算し，それを使って損失(loss)を求めている．その後，損失を `loss.backword()` で逆伝搬させ，勾配を計算し，パラメータを更新する．また学習ループは，損失が所定しきい値より小さくなったケースで処理を終えるようにしている．

計算結果は，次の通り．

```text
Loss:   0.0008 after 23 batches
==> Learned function:	y =   2.9819 x +   3.9815
==> Actual function:	y =   3.0000 x +   4.0000
```

## PyTorchで線形回帰を実装
同じことをPyTorchでやって見るが，コードの比較を始める前に，PyTorchの状況を把握しておきたい．

**Fig.1 History - Deep Learning Frameworks**
![deeplearning_frameworks_r1.png](https://qiita-image-store.s3.amazonaws.com/0/74152/bdb9ee27-bca9-832d-35b7-532365b8e95b.png)


（図は，Internetの情報を元にしており，詳細に関して不正確なところがあるかも知れません．）
（追記：初稿でTorch **7** を2017−02リリースと記載していましたが誤りでした．もっと歴史は古いようです．Torchでは，バーション・スキームで管理していないとのことで詳細はわかりませんでした．）

まず，Chainerは，2015年に最初の版(v1.0)がリリースされ，つい先日，2017年にv2．0がリリースされた．プログラミング言語として Lua を用いるTorchは，かなり歴史が古く，2002年に最初のリリースがあったようである．そのバージョンの更新を重ね，現在は Torch7 となっている．PyTorchは，2016年にGitHubのリポジトリが立ち上がり，2017年に入ってβバージョンがリリースされている．PyTorchの基本的なコンセプトは，Torchから引き継がれているが，勾配計算に関わる部分 `torch.autograd` は Chainer の影響を大きく受けている．

では，PyTorchのコードを見ていこう．

```py
import numpy as np
import torch
import torch.nn.functional as F
from torch.autograd import Variable
```

PyTorchでは，上記パッケージをimportする．

```py
# Target値 (3.0, 4.0)
W_target = torch.FloatTensor([[3.0]])   # size = [1, 1]
b_target = 4.0

# Model Parameters
dtype = torch.FloatTensor
# dtype = torch.cuda.FloatTensor # Uncomment this to run on GPU
W = Variable((torch.randn(1, 1) * 0.01).type(dtype), requires_grad=True)
b = Variable(torch.zeros(1, 1).type(dtype), requires_grad=True)
```

ここは，線形回帰モデルのターゲット値と，用いる変数（Variable）を定義する部分であるが，Chainerには登場しない，`torch.Tensor` を用いている．PyTorchのドキュメントを参照すると，Tensorは，
>A `torch.Tensor` is a multi-dimensional matrix containing elements of a single data type.

であるが，コンセプトとしては，Framework(PyTorch)専用のマトリクス・オブジェクトになるかと思う．この `torch.Tensor` には以下のものがあるとのこと．

**torch.Tensor**

| Data type  | CPU tensor | GPU tensor |
|:-----------|:-----------|:-----------|
| 32-bit floating point | `torch.FloatTensor` | `torch.cuda.FloatTensor` |
| 64-bit floating point | `torch.DoubleTensor` | `torch.cuda.DoubleTensor` |
| 16-bit floating point | N/A | `torch.cuda.HalfTensor` |
| 8-bit integer (unsigned) | `torch.ByteTensor`  | `torch.cuda.ByteTensor` |
| 8-bit integer (signed)   | `torch.CharTensor`  | `torch.cuda.CharTensor` |
| 16-bit integer (signed)  | `torch.ShortTensor` | `torch.cuda.ShortTensor` |
| 32-bit integer (signed)  | `torch.IntTensor`   | `torch.cuda.IntTensor` |
| 64-bit integer (signed)  | `torch.LongTensor`  | `torch.cuda.LongTensor` |


上記のように，CPU演算とGPU演算でTensorタイプを使い分けるようである．また，`Variable` コンストラクタに渡す引数も，Chainerは "numpy"クラスのもの(numpy.ndarray)であったのが，PyTorchでは torch.Tensor を渡すことに注意したい．上のコードでは，`torch.randn()` と `torch.zeros()` で乱数及びゼロの torch.Tensor を生成し，必要に応じて `.type(dtype)` で CPU tensor と GPU tensor に変換し直している．（CPU演算では，`torch.randn()`, `torch.zeros()` 自体が torch.Tensor インスタンスを作るので `.type(dtype)` は冗長となるが，GPU演算に対する互換性により上のようにしている．）`requires_grad` の使い方は Chainer と同じである．

```py
def model(x):
    # 線形回帰モデルの定義
    y = torch.mm(x, W) + b.expand_as(x)
    return y

def get_batch(batch_size=32):
    # バッチ・データの準備
    x = torch.randn(batch_size, 1)
    y = torch.mm(x, W_target) + b_target
    return Variable(x), Variable(y)
```

モデルの定義は，Tensorレベルで行う．このあたりは，"Theano", "TensorFlow"に近いように見える．関数 "get_batch()" では，訓練データを乱数で発生させ，Variableオフジェクトに変換して戻り値としている．Chainerとの違いを確認しておきたい．(Chainer: numpy.ndarrayからVariabelに変換，PyTorch: torch.TensorからVariableに変換．)

この後，損失を定義し勾配を計算する，という流れは，前のコードと同じとなる．

```py
# 損失関数 MSE(mean square error)
loss_fn = torch.nn.MSELoss(size_average=True)

# Train loop
for batch_idx in range(20):
    # Get data
    batch_x, batch_y = get_batch()

    # Forward pass
    y_pred = model(batch_x)
    loss = loss_fn(y_pred, batch_y)
    loss_np = loss.data[0]

    # Backward pass
    loss.backward()

    # Apply gradients
    learning_rate = 0.1
    W.data = W.data - learning_rate * W.grad.data
    b.data = b.data - learning_rate * b.grad.data

    # Manually zero the gradients by torch.Tensor.zero_()
    # パラメータの勾配をゼロ化する．（重要） 
    W.grad.data.zero_()
    b.grad.data.zero_()

    # Stop criterion
    if loss_np < 1.e-3:
        break
```

Chainerにあった，`cleargrad()` のVariableメソッドが無いので，`W.grad.data.zero_()`, `b.grad.data.zero_()` のように，torch.Tensorレベルで勾配をリセットしている．計算の結果は，次の通り．

```text
Loss: 3.9375e-03 after 19 batches
==> Learned function:	y = [ 2.98094225] x +   3.9527
==> Actual function:	y = [ 3.] x +   4.0000
```

（本来，乱数seedを設定して計算結果の再現性(Chainer vs. PyTorch)を見たほうがいいのかも知れませんが，本記事ではコーディングの比較が目的なので，計算結果の詳細比較は行っていません．）

（2つのコードは[こちら(gist)](https://gist.github.com/tomokishii/b154ea83f7dd060ac05844164fc4a4fb)にアップロードしました．）

## 雑感
いわゆる「Autograd系」の Chainer と PyTorch を簡単なコードで比較してみた．PyTorchでは，過去の遺産である torch.Tensor を引き継ぎ， numpy変数から，torch.Tensor を間に挟んでVariable(変数）に変換するというところが，なんとも Hybrid 感をかもし出している．ここのコンセプトの違いを理解すれば，Chainer - PyTorch間のコード移植は，スムーズに行くと予想される．

しかしながら，PyTorchの勢いはすごい．まだリリースされて半年だが，GitHubの至るところでPyTorchのコードを目にするようになってきた．自分自身は他のライブラリでコード作成を行っているが，「Autograd系」のFramework(Chainer / PyTorch) についても，使いこなせるように（少なくともコードを読めるように）勉強を進めていきたい．


## 参考文献，web site
- Chainer Documentation  
https://docs.chainer.org/en/stable/
- PyTorch Documentation  
http://pytorch.org/docs/index.html
- PyTorch Tutorials  
http://pytorch.org/tutorials/
- Stanford University, cn231n, Lecture 8 slides  
http://cs231n.stanford.edu/syllabus.html