Blog
こんにちは、得居です。先週末からインターンシップの3名を迎え、これからの二ヶ月間が楽しみです。
さて、昨年末に公開した実験用環境のmaf (Github)ですが、先週こっそりと v0.2 をリリースいたしました。今日は何が変わったのかをお伝えしたいと思います。
その前に、まずmafについて紹介します。mafは主に機械学習を用いた実験を書くための環境で、アルバイトの能地さん @nozyh と私の2人で開発しています。ビルドツールのwafを拡張する形で書かれていて、データセットから実験結果をビルドする過程を記述することができます。基本的な紹介は昨年末のブログ記事をご参照ください。特徴としては、学習や評価などの処理に付随するハイパーパラメータを管理する仕組みがあることです。詳細はmafのドキュメントをご参照ください。
それでは、v0.2で入った主な変更を紹介していきます。
お約束の簡素化、buildコマンドへの統合
以前まではお約束コードとしてimport文の他にいくつかload関数を呼び出す必要がありましたが、v0.2からはwafと同じディレクトリにおいたwscriptにおいてmaf.pyをimportするだけでmafが有効になります。また、今までは実験コードの内容をexperiment関数に書き、実行する際も./waf experimentと書いていましたが、v0.2からは通常のwafと同様にbuild関数に記述し、./waf buildで実行することができます。
import maf def configure(conf): pass def build(exp): # 実験コードをここに記述 pass
waf同様、以下のようにbuildコマンドでは”build”を省略することもできます。
$ ./waf configure $ ./waf
experiment関数およびexperimentコマンドも今までどおり利用可能です(よって今までに書いた実験コードも基本的には引き続き動作します)が、experiment関数に実験を書いた場合には後述のgraph機能などが使えません。
ruleデコレータ
新しく提供されるmaflib.util.ruleデコレータを用いてルール関数を定義することで、そのルールに指定するパラメータのうちどれを出力のパラメータに含むかをルールのユーザ(build関数の記述者)が自由に選べるようになります。
関数でルールを定義する際、今までは関数の挙動はパラメータによって制御するか、あるいはルールを返す高階関数を定義して、その引数を通してクロージャに束縛される値として設定するしかありませんでした。例えば、ファイルを読んで指定された文字列を後ろに付け足すルールはパラメータの使用・不使用に応じて以下のように書く必要がありました。
import maf """渡された文字列を入力ファイルの後ろに付け足して出力ファイルに書き出す例(v0.1)""" # パラメータで設定する場合 def add_str_by_parameter(task): content = task.inputs[0].read() content += task.parameter['myarg'] task.outputs[0].write(content) # 引数で設定する場合 def add_str_by_func_argument(myarg): def rule(task): content = task.inputs[0].read() content += myarg task.outputs[0].write(content) return rule def configure(conf): pass def experiment(exp): # パラメータで設定する場合 exp(source='src.txt', target='tgt1.txt', parameters=[{'myarg': 'hoge'}], rule=add_str_by_parameter) # 引数で設定する場合 exp(source='src.txt', target='tgt2.txt', rule=add_str_by_func_argument(myarg='hoge'))
この実験スクリプトの例では、ノード’tgt1.txt’はパラメータ付けられますが、ノード’tgt2.txt’はパラメータ付けられません。例えば別のファイルに’myarg’という名前で異なる値のパラメータがひも付けられたノード’other.txt’がある場合、’tgt1.txt’と’other.txt’は同じタスクの入力として組合せられませんが、’tgt2.txt’と’other.txt’は組合せることができます。この場合、add_str_by_func_argumentのようにパラメータを使わないようにする必要があります。一方、たくさんの’myarg’パラメータにおいてこのタスクを実行したい場合、パラメータに指定するべきです。今まではこれを上記のように実装をわけることで対処するしかありませんでした。v0.2からは次のようにmaflib.util.ruleデコレータを用いることで、これらを一つのルール定義だけでユーザ側が透過的に使い分けることができます。
import maf import maflib.util """渡された文字列を入力ファイルの後ろに付け足して出力ファイルに書き出す例(v0.2)""" @maflib.util.rule def add_str(task): content = task.inputs[0].read() content += task.parameter['myarg'] task.outputs[0].write(content) def configure(conf): pass def build(exp): # パラメータを使う場合 exp(source='src.txt', target='tgt1.txt', parameters=[{'myarg': 'hoge'}], rule=add_str()) # パラメータを使わない場合 exp(source='src.txt', target='tgt2.txt', rule=add_str(myarg='hoge'))
このように、maflib.util.ruleを用いて定義したルールadd_strでは、引数としてパラメータの一部を指定することができます。引数で指定されたパラメータは、出力ファイルのパラメータに含まれません。よって’tgt1.txt’はパラメータ付けられていますが、’tgt2.txt’はパラメータ付けられていません。
入出力にディレクトリを指定できるようになった
ディレクトリをsourceやtargetに指定できるようになりました。例えば複数のファイルからなるデータセットや結果などを扱う必要が時々あります。wafでは入出力にディレクトリを指定することはできませんが、mafではこれを拡張してディレクトリ入出力に対応しています。
以下はディレクトリ入出力を使った例です。最初にディレクトリを削除するのを忘れないようにしないと、スクリプトを修正して再実行した際に結果が混ざってしまう点には注意が必要です。この例では出てきませんが、もちろんパラメータと組合せることも可能です。
import maf def configure(conf): pass def build(exp): exp(target='dir1', rule=''' rm -rf ${TGT}; mkdir -p ${TGT}; echo hoge > ${TGT}/hoge; echo fuga > ${TGT}/fuga; ''') exp(source='dir1', target='dir2', rule=''' rm -rf ${TGT}; mkdir -p ${TGT}; cp ${SRC}/hoge ${TGT}/piyo; cp ${SRC}/fuga ${TGT}/poyo; ''')
exptestコマンド
maf v0.2では、ユーザが定義したルール関数のユニットテストを書けるようになりました。
exptest関数内にてテストケースのクラスやテストを書いたスクリプト名、あるいはtest_で始まるテストスクリプトを含んだディレクトリ名を指定することでテストを実行することができます。
テストはPythonのunittestモジュールを使って書くことができます。また、テストの際にはダミーの入力ノードやパラメータを指定することができます。
例えば先ほどのadd_strルールは次のようにテストできます。
import maf import maflib.util import maflib.test import unittest @maflib.util.rule def add_str(task): content = task.inputs[0].read() content += task.parameter['myarg'] task.outputs[0].write(content) class TestMyRule(unittest.TestCase): def test_add_str(self): # テスト用のモックタスク task = maflib.test.TestTask() # ダミーの入力とパラメータを設定する task.set_input(0, 'abcde\n') task.parameter['myarg'] = 'hoge' # ルールを実行 add_str(task) # 結果をチェック self.assertEqual( task.outputs[0].read(), 'abcde\nhoge') def configure(conf): pass def exptest(test): test.add([TestMyRule]) def build(exp): pass
以下のコマンドでテストを実行することができます。
$ ./waf configure $ ./waf exptest
graphコマンド
新しく実装されたgraphコマンドを使うことで、実験過程をグラフに描画することができます。グラフ上では各入出力ノードにパラメータが書かれ、タスクは黒丸で描かれます。mafを使っていると、パラメータに不備があってタスクが実行されないということがありますが、そういった実験のバグを調査するのに利用することができます。
例えば次の実験では、2種類のパラメータについて1つずつノードzが生成されて欲しいですが、実際にはノードzは1つしか生成されません。
import maf def configure(conf): pass def build(exp): exp(target='x', parameters=[{'p': 1}, {'p': 2}], rule='echo ${p] > ${TGT}') exp(target='y', parameters=[{'p': 1}, {'p': 20}], rule='echo ${p} > ${TGT}') exp(source='x y', target='z', rule='cat ${SRC} > ${TGT}')
どこが間違ってるでしょうか? グラフを描画してみましょう(graphvizがインストールされている必要があります)。
$ ./waf graph --graphpath=graph.png
すると次のグラフが描画されます。
p=2のノードxとp=20のノードyからzが生成されていないことがわかります。mafでは同じキーのパラメータで値が食い違った組合せではタスクを実行しません。つまりpの値を書き間違っていたということですね。この程度なら短い例なのでwscriptを読んでも気づきますが、実験が複雑になっていくとどこにミスがあるのかわかりづらくなります。グラフを書いて、目的のファイルが生成される過程を順に追うことで、どの部分でファイルの生成が途絶えているのかを発見することができます。
その他の主な変更
- Python2.6に対応しました(鈴木さん @shu65ありがとうございました!)。ちなみにdevelopブランチの方ではまもなくPython3にも対応する予定です。
- サブディレクトリに実験スクリプトを分けた場合の挙動がもとのwafと整合するようになりました。exp.recurse(list of subdirs)と書けばサブディレクトリ内のwscriptが実行されます。これで実験スクリプトと関連ファイルをサブディレクトリに分割して管理できるようになります。
- bz2モジュールへの依存がなくなりました。
その他、今後も開発をつづけていきますので、ぜひ使ってみていただければと思います。