- サッとモデルとデータセットのコードだけ書いて何かを試してみたいときなどに毎度同じようなtrainingコードを書くのは面倒です。
- ChainerにはTrainerという便利なツールがありますが、それでも毎回同じようなExtensionを追加するコードを書いてtrain.pyを作るのは億劫です。
- 学習の設定やモデルファイル、データセットのファイルなどへのパスが書かれたYAMLファイルを引数で渡せば、あとはいつもの感じで学習を開始してくれるコマンドラインツールがあると楽やな、と思い作りました。
-
基本的に完全なるオレオレサボりツールなので、全く人に使われることは考えて作られてませんが、せっかくなので公開しました。
ChainerCMD
https://github.com/mitmul/chainercmd
1. インストール
$ pip install chainercmd
2. 使い方
$ chainer init
とすると以下の4つのファイルが作られます。
- config.yml
- custom_extension.yml
- dataset.py
- loss.py
- model.py
中身はこんな感じです。
stop_trigger: [20, "epoch"]
# max_workspace_size: 8388608 # default: 8 * 1024 * 1024
dataset:
train:
file: dataset.py
name: MNIST
batchsize: 128
args:
split: train
ndim: 3
valid:
file: dataset.py
name: MNIST
batchsize: 64
args:
split: valid
ndim: 3
model:
file: model.py
name: LeNet5
args:
n_class: 10
loss:
module: chainer.links.model.classifier
# file: loss.py # If you use your own loss definition, remove "module" key above and use "file" key to specify the path to the file which describes the "name" class for a Loss link.
name: Classifier
optimizer:
method: MomentumSGD
args:
lr: 0.01
weight_decay: 0.0001
lr_drop_ratio: 0.1
lr_drop_triggers:
points: [10, 15]
unit: epoch
# You can ommit this part
# updater_creator:
# file: updater_creator.py
# name: MyUpdaterCreator
# args:
# print: True
trainer_extension:
- custom:
file: custom_extension.py
name: CustomExtension
args:
message: 'I am learning...'
- LogReport:
trigger: [1, "epoch"]
- dump_graph:
root_name: main/loss
out_name: cg.dot
- observe_lr:
trigger: [1, "epoch"]
- ParameterStatistics:
links:
- conv1
- conv2
- conv3
- fc4
- fc5
report_params: True
report_grads: True
prefix: null
trigger: [1, "epoch"]
- Evaluator:
module: chainer.training.extensions
name: Evaluator
trigger: [1, "epoch"]
prefix: val
# You can specify other evaluator like this:
# module: chainercv.extensions
# name: SemanticSegmentationEvaluator
# trigger: [1, "epoch"]
# prefix: val
- PlotReport:
y_keys:
- conv1/W/data/mean
- conv2/W/data/mean
- conv3/W/data/mean
- conv4/W/data/mean
- fc5/W/data/mean
- fc6/W/data/mean
x_key: epoch
file_name: parameter_mean.png
trigger: [1, "epoch"]
- PlotReport:
y_keys:
- conv1/W/data/std
- conv2/W/data/std
- conv3/W/data/std
- conv4/W/data/std
- fc5/W/data/std
- fc6/W/data/std
x_key: epoch
file_name: parameter_std.png
trigger: [1, "epoch"]
- PlotReport:
y_keys:
- main/loss
- val/main/loss
x_key: epoch
file_name: loss.png
trigger: [1, "epoch"]
- PlotReport:
y_keys:
- main/accuracy
- val/main/accuracy
x_key: epoch
file_name: accuracy.png
trigger: [1, "epoch"]
- PrintReport:
entries:
- epoch
- iteration
- main/loss
- main/accuracy
- val/main/loss
- val/main/accuracy
- elapsed_time
- lr
trigger: [1, "epoch"]
- ProgressBar:
update_interval: 10
trigger: [10, "iteration"]
- snapshot:
filename: trainer_{.updater.epoch}_epoch
trigger: [10, "epoch"]
import chainer
class CustomExtension(chainer.training.Extension):
def __init__(self, message):
self._message = message
def initialize(self, trainer):
self._message += ' and Trainer ID is: {}'.format(id(trainer))
def __call__(self, trainer):
pass
def serialize(self, serializer):
self._message = serializer('_message', self._message)
import chainer
class Dataset(chainer.dataset.DatasetMixin):
def __init__(self, split='train'):
super().__init__()
def __len__(self):
pass
def get_example(self, i):
pass
# You can delete this
class MNIST(chainer.dataset.DatasetMixin):
def __init__(self, split='train', ndim=3):
super().__init__()
train, valid = chainer.datasets.get_mnist(ndim=ndim)
self.d = train if split == 'train' else valid
def __len__(self):
return len(self.d)
def get_example(self, i):
return self.d[i]
from chainer import link
from chainer import reporter
from chainer.functions.evaluation import accuracy
from chainer.functions.loss import softmax_cross_entropy
class MyLoss(link.Chain):
def __init__(self, predictor):
super().__init__()
self.lossfun = softmax_cross_entropy.softmax_cross_entropy
self.accfun = accuracy.accuracy
self.y = None
self.loss = None
self.accuracy = None
with self.init_scope():
self.predictor = predictor
def __call__(self, *args):
assert len(args) >= 2
x = args[:-1]
t = args[-1]
self.y = None
self.loss = None
self.accuracy = None
self.y = self.predictor(*x)
self.loss = self.lossfun(self.y, t)
reporter.report({'loss': self.loss}, self)
self.accuracy = self.accfun(self.y, t)
reporter.report({'accuracy': self.accuracy}, self)
return self.loss
import chainer
import chainer.functions as F
import chainer.links as L
class Model(chainer.Chain):
"""Model definition.
This is a template of model definition.
"""
def __init__(self, n_class):
super().__init__()
with self.init_scope():
pass
def __call__(self, x):
pass
# You can delete this! It's a sample model
class LeNet5(chainer.Chain):
def __init__(self, n_class):
super().__init__()
with self.init_scope():
self.conv1 = L.Convolution2D(None, 6, 5, 1)
self.conv2 = L.Convolution2D(6, 16, 5, 1)
self.conv3 = L.Convolution2D(16, 120, 4, 1)
self.fc4 = L.Linear(None, 84)
self.fc5 = L.Linear(84, n_class)
def __call__(self, x):
h = F.relu(self.conv1(x))
h = F.max_pooling_2d(h, 2, 2)
h = F.relu(self.conv2(h))
h = F.max_pooling_2d(h, 2, 2)
h = F.relu(self.conv3(h))
h = F.relu(self.fc4(h))
return self.fc5(h)
あとは基本的にこれを個別のタスク用に編集して、終わったら
$ chainer train config.yml --gpus 0
とやればGPU ID:0のデバイスを使って学習が始まります。複数のGPUを使いたい場合は、--gpus 0 1 2 3
のようにスペース区切りで使いたいGPUのIDを指定してください。
3. 各ファイルの説明
config.yml
学習の設定や、どのファイルをモデルファイルとして使うかなどが書いてあるファイルです。
dataset
dataset:
train:
file: dataset.py
name: MNIST
batchsize: 128
args:
split: train
ndim: 3
valid:
file: dataset.py
name: MNIST
batchsize: 64
args:
split: valid
ndim: 3
学習データセットと検証用データセットの設定です。予めそれらを別のファイルに定義しておき、file
キーでそのファイルまでのパスを、name
でそのファイル中のデータセットクラスのクラス名を指定します。args
の値は辞書である必要があり、これはそのまま **args
のようにしてデータセットクラスのコンストラクタにキーワード引数として渡されます。batchsize
は各データセットから作るミニバッチのサイズです。
model & loss
model:
file: model.py
name: LeNet5
args:
n_class: 10
これもほぼ同様で、file
で指定されたパスにあるファイル中のname
という名前のクラスをインスタンス化してモデルを作ります。その際にargs
があれば、それをキーワード引数として渡します。
loss:
module: chainer.links
# file: loss.py # If you use your own loss definition, remove "module" key above and use "file" key to specify the path to the file which describes the "name" class for a Loss link.
name: Classifier
loss
のパートも基本的には同様で、ここではargs
は省略されていますが、model
パートと同様にargs
をキーとして辞書を指定すれば、それがロスを計算するためのクラスのコンストラクタにそのままキーワード引数として渡されます。loss
に関しては、あらかじめChainerが用意してくれている chainer.links.Classifier
なども使えるように,module
というキーが使えるようになっています。module
と file
は同時に使うことはできません。
optimizer
optimizer:
method: MomentumSGD
args:
lr: 0.01
weight_decay: 0.0001
lr_drop_ratio: 0.1
lr_drop_triggers:
points: [10, 15]
unit: epoch
Optimizerの設定部分です。method
はそのままChainerのoptimizers
モジュール以下にあるクラス名になります。args
はそのコンストラクタに渡すキーワード引数です。weight_decay
キーが存在する場合は、Optimizer hookとしてWeight decayが追加されます。lr_drop_ratio
とlr_drop_triggers
が両方存在する場合は、ManualScheduleTriggerを使った学習率のdroppingが行われます。lr_drop_triggers
に渡される辞書は、points
がどのタイミングで学習率をlr_drop_ratio
倍にするか、unit
がそのタイミングの単位(epoch
もしくはiteration
が指定可能)です。上の例の場合は、MomentumSGDの学習率が10エポックで0.1倍、さらに15エポックでもう一度0.1倍になります。
updater
Updaterは,iterator, optimizer, devicesをとってUpdaterオブジェクトを返すような関数を自分で用意して,config.ymlのupdater_createrキーで指定してやることでカスタマイズすることができます.
その他のファイル
普通のChainerを使って書かれたデータセットクラス、モデルクラス、ロスを計算するラッパークラスです。custom_extension.py
は自分で作ったExtensionクラスを書いておき,config.ymlから指定してそれをTrainerに追加したいときに書き換えます。
4. 学習する
chainer init
してできるファイルには,最初からMNIST Exampleを実行するのに必要なモデルやデータセットなどが書かれており,さらにconfig.yml
はそのままだとそれらのモデル・データセットなどを用いてMNIST exampleを実行するように準備されています.なので,あとはchainer
コマンドを叩くだけです。サブコマンドにtrain
を指定します。
$ chainer train config.yml --gpus 0
サブコマンドのtrain
の次に使いたいコンフィグYAMLファイルのパスを指定します。ファイル名はconfig.ymlじゃなくてもなんでもOKです。GPUを使いたい場合は、--gpus
オプションでデバイスIDを指定します。もし--gpus 0 1 2 3
のように複数指定された場合は、自動的にParallelUpdaterもしくはMultiprocessParallelUpdater(NCCLが有効な場合)が選択されて、マルチGPUでの学習が行われます。
Ubuntu上でやる場合MPLBACKEND=Agg
と環境変数をセットしておけばPlotReport周りのエラーを回避できます。
実際に回した結果が以下です。無駄に4GPUs使ってみました。
$ MPLBACKEND=Agg chainer train config.yml --gpus 0 1 2 3
chainer version: 2.0.1
cuda: True, cudnn: True, nccl: True
result_dir: result/config_2017-07-18_23-26-41_0
train: 60000
valid: 10000
/home/shunta/.pyenv/versions/anaconda3-4.4.0/lib/python3.6/site-packages/chainer/training/updaters/multiprocess_parallel_updater.py:142: UserWarning: optimizer.lr is changed to 0.0025 by MultiprocessParallelUpdater for new batch size.
format(optimizer.lr))
epoch iteration main/loss validation/main/loss main/accuracy validation/main/accuracy elapsed_time lr
1 118 0.890775 0.234464 0.739672 0.928896 8.45449 0.0025
2 235 0.198075 0.141786 0.939503 0.957476 13.914 0.0025
3 352 0.128017 0.120378 0.960737 0.960839 19.1064 0.0025
4 469 0.100555 0.0979902 0.96895 0.969739 24.3107 0.0025
5 586 0.0865762 0.077968 0.971888 0.97587 29.2581 0.0025
6 704 0.0734014 0.0672336 0.976562 0.978837 34.3428 0.0025
7 821 0.0683174 0.0618281 0.977564 0.979826 39.1815 0.0025
8 938 0.0617364 0.0748559 0.980235 0.976958 44.0893 0.0025
9 1055 0.0573468 0.0596004 0.981904 0.980024 49.0457 0.0025
10 1172 0.0531992 0.0578394 0.98364 0.982694 54.3706 0.0025
11 1290 0.047489 0.0485524 0.986096 0.984573 59.3655 0.00025
12 1407 0.0417473 0.0482626 0.987513 0.984968 64.18 0.00025
13 1524 0.0406346 0.0473873 0.987914 0.984771 69.0114 0.00025
14 1641 0.0405981 0.0479212 0.987847 0.985265 74.0731 0.00025
15 1758 0.0394898 0.0478847 0.988114 0.986155 79.3369 0.00025
16 1875 0.0394069 0.0472816 0.988181 0.984968 84.2785 2.5e-05
17 1993 0.0389244 0.0471326 0.988546 0.985166 89.4715 2.5e-05
18 2110 0.0391655 0.046991 0.988181 0.985463 94.6602 2.5e-05
19 2227 0.0390729 0.0468674 0.988381 0.985364 99.7827 2.5e-05
20 2344 0.038587 0.0471131 0.988315 0.985166 104.962 2.5e-05
config.ymlで2つPlotReportを設定していたので、結果ディレクトリ(result以下にコンフィグファイルのbasenameと日付・時刻が連結されたような名前のディレクトリが作成されています)内にconfig.ymlで指定したファイル名の画像(loss.pngとaccuracy.png)ができています。
6. 結果ディレクトリについて
chainer
コマンドで学習を開始すると、自動的に指定したコンフィグファイルのファイル名(config.yml
を指定した場合、拡張子を覗いたconfig
の部分)と開始時点での時刻をもとにした結果ディレクトリが自動的に、実行した階層にできるresult
ディレクトリの下に作られ、そこにモデルファイル、ロスファイル、コンフィグファイルなどが勝手にコピーされます。そのディレクトリがTrainerのout
に指定されるので、snapshotやログファイルもそこに書き込まれます。
7. おわりに
こういうツールでできることは非常に限られていますが、割と毎回同じtrain.pyを書くことが多かったので、適当に一般化してみました。ただNLPとかGANとかやる場合Updaterも指定できたりしないと厳しいので使えません。シンプルな画像認識系タスクなら使えるかもしれません。