メリークリスマス。@tereka114です。
この記事は「Chainer/CuPy Advent Calendar 2018」アドベントカレンダーの24日目です。
qiita.com
Chainerを利用する際の便利なトリックをクリスマスプレゼント代わりにご紹介します。
Chainerは非常に使いやすい良いフレームワークだと思います。
使っていく中で、デバッグやパラメータのチェック、あるいは既存モデルの利用など、かゆいことが必要になる時があり、その方法を調べる事が多くありました。
Chainerはそのようなことにもサポートしており、大変助かっています。
せっかくなので、使ってきたトリックをここで紹介します。
※Chainerのバージョンは5.1.0を用いています。
1. DEBUGモード
ニューラルネットワークは学習した結果、出力が常にNaNになることがあります。
NaNになる原因はゼロ除算や値が高すぎてinfになるなど様々です。
一つ言えることとして、この原因特定は相当厄介です。特定のために、まずは、いつ、どこで発生したのかを探る必要があります。
Chainerが標準で搭載しているDEBUGモードを利用することにより、NaNを検出できます。
例えば、次の例を見てみましょう。意図的にl1レイヤーの出力のあとに、NaNとなるよう変数を代入しています。
import chainer
import chainer.links as L
import numpy as np
class MLP(chainer.Chain):
def __init__(self):
super().__init__()
with self.init_scope():
self.l1 = L.Linear(2, 2)
self.l2 = L.Linear(2, 2)
def __call__(self, x):
h = self.l1(x)
h.data[0] = np.nan
return self.l2(h)
x = np.array([[10, 10]], dtype=np.float32)
model = MLP()
print (model(x))
計算の出力は次の通りになります。
variable([[nan nan]])
今回の例は直接埋め込んでもいるので、ソースを見れば一目瞭然です。
ただし、Residual Networkのように層の数が2桁以上で構成されているネットワークを解析するのは至難の技です。
Chainerでは標準でDEBUG機能を持っています。
各レイヤーの出力にNaNが存在するかを検知します。使い方は次の通りです。環境変数「CHAINER_DEBUG」を1にします。
CHAINER_DEBUG=1 python debug.py
出力結果は次の通りです。NaNを検出すると、Tracebackが出力されます。
Tracebackを確認するとself.l2で検知していることがわかります。
これによりself.l2を呼び出している付近を確認すれば良いと判断ができます。
Traceback (most recent call last):
File "debug.py", line 24, in <module>
print (model(x))
File "debug.py", line 19, in __call__
★ return self.l2(h) ★
File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/link.py", line 242, in __call__
out = forward(*args, **kwargs)
File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/links/connection/linear.py", line 138, in forward
return linear.linear(x, self.W, self.b, n_batch_axes=n_batch_axes)
File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/functions/connection/linear.py", line 289, in linear
y, = LinearFunction().apply(args)
File "/Users/Tereka/anaconda3/lib/python3.6/site-packages/chainer/function_node.py", line 288, in apply
raise RuntimeError(msg)
RuntimeError: NaN is detected on forward computation of LinearFunction
2. 問題のあるパラメータチェック
1のDEBUG_MODEではNaNを検出しました。
このDEBUG_MODEはあくまで各層の出力のチェックのみです。そのため、層のパラメータのどこに影響して得られたかは不明です。
当たり前ですが、数百万の数もあるパラメータを目視で確認するのは難しいです。
そのため、NaNのパラメータを再帰的に探索し、出力するコードを作成しました。
次の関数を使えば出力可能です。モデルを引数として与えれば、パラメータを出力します。
def parameter_check(model):
for child in model.children():
if isinstance(child, chainer.link.Link):
for name, param in child.namedparams():
if np.isnan(param.data).any():
print (name)
この関数を試したコードは次のコードです。
Chainの中にChainを作り、sub_l1の変数のWにNaNを代入しました。
import chainer
import chainer.links as L
import numpy as np
class SubMLP(chainer.link.Chain):
def __init__(self):
super().__init__()
with self.init_scope():
self.sub_l1 = L.Linear(2, 2)
self.sub_l2 = L.Linear(2, 2)
def __call__(self, x):
h = self.l1(x)
return self.l2(h)
class MLP(chainer.link.Chain):
def __init__(self):
super().__init__()
with self.init_scope():
self.l1 = L.Linear(2, 2)
self.sub_mlp = SubMLP()
self.l2 = L.Linear(2, 2)
def __call__(self, x):
h = self.sub_mlp(self.l1(x))
return self.l2(h)
def parameter_check(model):
for child in model.children():
if isinstance(child, chainer.link.Link):
for name, param in child.namedparams():
if np.isnan(param.data).any():
print (name)
model = MLP()
model.sub_mlp.sub_l1.W.data[0] = np.nan
parameter_check(model)
出力は「/sub_l1/W」となるため、発見できています。
3. 再現性の確保
CuDNNを利用する場合、ニューラルネットワークの計算では再現性を確保できないことがあります。
CuDNNを用いて計算した場合は計算順序が担保されないことが知られています。
そのため、計算による誤差が発生し、最終的な計算結果が計算のたびに変化します。
これは、大きく精度に影響する事象ではありません。
ただし、これはデバッグ時に対処に困ることがあります。
デバッグでは、問題事象が再現できなければ、修正できたのか否かの解析が難しくなります。
このCuDNNの動作を決定的に動かす設定があります。
v2以前では、Convolution系のクラスにdetermistic引数がありましたが、v3以降では、chainer.configで設定可能です。
変更するには次のコードを実行しましょう。
chainer.config.cudnn_deterministic = True
4.Fine tuningの実施
Fine tuningは既存のモデルを初期値にして再学習する手法です。
そのため、ベースはResNetを用いて、残りの出力までのネットワークの構造を変更したいといったケースがあります。
例えば、クラス数1000のImageNetの既存モデルからクラス数2のモデルを再学習させたい場合です。
Chainerでは、このようなユースケースにも対応し、簡単に実装できます。
Fine tuningの実装は次のとおりです。
ResNet50Layersのforwardメソッドの出力を"pool5"に指定するとpool5部の出力を獲得し、後の層に繋げられます。
from chainer.links.model.vision.resnet import ResNet50Layers
import chainer
import chainer.links as L
import numpy as np
class ResNetFineTune(chainer.Chain):
def __init__(self):
super().__init__()
with self.init_scope():
self.resnet = ResNet50Layers()
self.fc1 = L.Linear(2048, 2)
def __call__(self, x):
h = self.resnet.forward(x, layers=["pool5"])["pool5"]
return self.fc1(h)
x = np.random.rand(1, 3, 224, 224).astype(np.float32)
model = ResNetFineTune()
print(model(x))
5. 実行環境の確認
実行環境の確認は重要です。デバッグ時には、CUDA/CuDNNのバージョンを確認することがあります。
例えば、CUDA/CUDNNのパスを環境変数を設定しています。
しかし、問題が発生したときにどのバージョンがライブラリ内で使われているのかを確認したいです。
> import chainer
> chainer.print_runtime_info()
Platform: Linux-4.4.0-93-generic-x86_64-with-debian-stretch-sid
Chainer: 5.1.0
NumPy: 1.15.0
CuPy:
CuPy Version : 5.1.0
CUDA Root : /usr/local/cuda
CUDA Build Version : 9010
CUDA Driver Version : 10000
CUDA Runtime Version : 9010
cuDNN Build Version : 7102
cuDNN Version : 7102
NCCL Build Version : 2115
iDeep: Not Available