はじめに
残念ながら(?)ChainerとPytorchの優劣を決めるわけではありません。
Chainerから移行し、Pytorchを利用していく中で自分が気に掛かったところを比較、メモした記事です。このメモ内容がPytorchビギナーにもしかしたら有用かもしれないと思い記事にしました。
基本的にAtomのHydrogenプラグインを用いて、Jupyter notebookを動作させた際の画像を貼っていきます(特に長いコード書くわけじゃないので)。
Torch.Tensor
多次元配列
torchとnumpyの多次元配列をそれぞれ比較。
個人的にはtorchの配列の表示は見やすいと思います(括弧がやたらと出てこないので)。また、サイズの明記してくれるのが素晴らしい。
3次元配列の場合は画面には2次元配列までしか表示できないので、以下のようにゼロ番目の要素に入っている2次元配列はどんなふうになっているのかを表示してくれます。
まあ、それに関してはnumpuも一緒ですが、何を表示しているのかがいつでも明示的になっているのがtorchの特徴と言えるでしょう。
もっと高次元配列になると、この見やすさは顕著になります(まあ、高次元配列を数値で見ようなんてそうそうないと思いますが)。
numpyの場合は以下のように表示されます。これは、4×3の2次元配列が2個表示され、それが5セット分あるという形で表示されます(括弧の数に注目)。
これがtorchの場合は、いつでもどのインデックスにどんな2次元配列が入っているのかというのを一貫して表示してきます。
以下で見ると、自分が何を見ているのかハッキリ分かりますね。
torch.Tensor
numpy.arrayに対応するものがtorch.Tensorになります。
numpyが様々なライブラリで利用されていることをtorch側も認知しているため、なんとtorch.Tenssorからnumpy.arrayに変換するメソッドまで持ちあわせております。
torch.Tensorの型変換
またtorch.Tensorはこの手のデータ型の変換をメソッドで行えるように一貫して実装されているため、例えばfloatからintにしたい場合でも以下のように実行可能です。
特に覚えておいて欲しいのがlong型です。
なぜかtorchではラベルにint型(32bit整数)ではなくlong型(64bit整数)を用います。
そんな大きな整数いつ使うんだ…?って感じですけど、決まりなので仕方がない(Pytorch使ってクラス分類するときに最初にここでハマりました)。
.cuda()
忘れちゃいけないのがgpu用の多次元配列です。gpuで演算を行う場合は全てこのcuda用の多次元配列でなければなりません。こちらももちろんメソッドを使った変換が可能であり、以下のように簡単に変換できます(こいつは便利だ!便利すぎてたまに忘れる)。
データ型、配列のサイズはもちろんのこと、どのGPUに渡されているのかも教えてくれます(私はシングルGPUなので自動的に0になります)。
仮にマルチGPUの場合はメソッドの引数に番号を入れることになります。
.cuda(i)
のような形です。
torchは基本的にnumpyとさして変わりません。numpy.arrayにあってtorch.Tensorにはないものなども実際はあるのですが、やりたいことは大抵できるでしょう。
torch.autograd.Variable
さて、お待ちかねのPytorchに入っていきます。Pytorchでは基本的にtorch.Tensorをtorch.autograd.Variableで包んでやることで、計算グラフ構築の機能を持たせてやることになります。基本的な計算機能はtorch.Tensorにまかせておいて、torch.autograd.Variableがバックプロパゲーションに必要な情報の取り扱いをしてくれるということです。
chainer.Variableとtorch.autograd.Variable
一応chainer.Variableとtorch.autograd.Variableを比較しておきましょう。
いずれのVariableもあくまで計算グラフの情報を扱っているということなので、配列に関する表示はそれぞれnumpyとtorchに準じており、それぞれVariableで包んでいますよということを明記しています。
torch.autograd.Variableの配列データを見る
Variableは何度も言いますがtorch.Tensorに計算グラフの情報を保持させる機能を追加しているというものです。VariableはTensorを中にself.dataの形で持っているため、以下のようにすぐにデータを見ることができます。
TensorFlowだとeval()メソッドにより計算グラフを実行することで(実質Session.runで)値を出力しますが、Pytorchはあくまで配列データをPythonのクラスでラッピングしているだけなので、ここらへんの取り扱いが非常に楽です(Chainerのモロパクリなんだけども)。
GPUへの転送
もちろんのこと、VariableもGPUへ転送することが可能で、torch.Tensor同様に.cuda()メソッドを有しています。またVariableをGPUに転送して、その配列データを見るとGPUに転送されているのが確認できます。
この操作はtorch.TensorをGPUに転送してしてからVariableでラッピングしても、VariableでラッピングしてからGPUに転送してもいいということです(上記のコードと下記のコードのcuda()メソッドの位置に注目)。
実はこれは地味にコードを書く際にきいてきます。torchはもともとGPU利用が想定されて作られたので、この辺が非常にシームレスに扱うことができます。
Chainerの場合は、cupyがnumpyに寄せた実装をしているものの、あくまで別物であるため、gpuあたりの取り扱いが割と面倒になっています(とは言っても、その面倒な部分は結構しっかり隠蔽されていると思われます)。
chainerのgpu利用補足
torch.Tensorはprintで表示することでデータのサイズや型、どのGPUにいるのかを明示してくれますが、numpyやcupyではそうではありません。printの表示ではもはやnumpyなのかcupyなのかcpuにいるのかgpuにいるのかがわからないのです(以下のコードを見てください)。
またnumpyをcupyに変換するには以下のような手続きを行います。
あるいは上記の手続きと、下記の手続きは等価です。
いずれにしても、torchほど楽とは行きません(それでも辛いわけではないですが)。
本質的な問題は、gpuへの転送に関してnumpyとcupyという多次元配列の間でしか行えないという点です(Variableで包んだ後は変更ができないということ)。
仮に下記のように、numpy.arrayをVariableで包んでからgpuへ割当を行おうとしてみましょう。
すると、これはデータ型が対応していないということで怒られてしまいます。
実際、これによって何か絶対にできないことが出てきてしまうということは無いと思いますが、コードをゴチャゴチャ書いている内に、これで怒られることはきっと出てくるでしょう。ただ、逆にcpuとgpu間を移動したデータ型が一体何であるかをハッキリと認識できるということになります。numpy⇔cupyのやりとりしかあり得ないからです。
cupyへの期待(余談)
しかし、ここまで完全にnumpyと表面上区別がつかないように実装されているとなると、numpyで実装されているいろいろなライブラリが、もしかしたらcupyに置き換えるだけでGPU利用になるんじゃないかという期待が持てます(PyMCとかscipyとか)。
ライブラリ実装のnp.の部分をxpに置き換えておいて
if gpu:
xp = cp
else:
xp = np
でなんとかなったりしないかな。無理か。
Variableを使った微分
ここからは基本的にPyorchの話です。Chainerを使っている人にはお馴染みの微分操作について見ていきましょう。Pytorchでは全てVariableで値を扱っていきます。torch.TensorをVariableでラッピングしてあげるだけなので特に難しいことはありません。
後に、微分操作を必要とするものに関しては以下のように、Variableに渡す際に、reauires_grad=Trueとしておきます(デフォルトでTrueなので実際は書かなくても良いです)。
さて、
という一次関数の式を見てみましょう。具体的にで見ていきます。そうするとになってくれるはずです。
期待通りの表示です。これは別にVariableでなくてもできることです。しかし、Variableで包んであげると、 どのような計算を辿ってその値になったのかを、変数自体が覚えておいてくれています。
がと掛け算されているというのを変数が覚えているため、のによる(偏)微分を求めることが簡単にできるのです。今回の場合
となっているはずです。これを確認するには、微分される変数についてy.backward()で微分を計算させておきましょう。するとに対して、x.grad()微分の値が格納されます。
実はy.backward()を行った時点で、他の全ての変数に関する微分が求まっています。すなわち
も計算されているということです。
実装上の注意
先ほどと完全に同じ式で、微分計算(y.backward())を5回連続で行ったとしましょう。
x.grad = 25
w.grad = 15
b.grad = 5
と出ています。実は5回連続でbackward()を呼ぶと、本来の微分値が5倍されてしまいます。1回微分するごとに足されていってしまってるのです(決して複数回微分操作を行ったところで、高階微分が求まるわけではないので注意)。
y.backward(retain_graph=True)
retain_graph=Trueの部分は、計算グラフを維持するかどうかを指定しています。仮に、この部分がFalseの場合は、1回微分操作を行ったら計算グラフを消去してしまうため、繰り返しの微分操作はできなくなります(代わりにメモリの消費は抑えられるというわけ。必要なときに必要に応じてしか計算グラフを維持しない)。
最後に
これは逐次更新するかもしれません。